diff --git a/dist/index.cjs b/dist/index.cjs index 758bf0e..db7f08d 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -1,3 +1,3 @@ -"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});let i=null,l=null;function h(){if(i)return i;if(window.location.protocol==="neboapp:")return i=window.location.hostname,i;const s=window.location.pathname.match(/^\/apps\/([^/]+)\/ui/);if(s)return i=s[1],i;const e=document.querySelector('meta[name="nebo-app-id"]');if(e)return i=e.getAttribute("content")||"",i;throw new Error('[nebo-sdk] Cannot detect app ID. In production, serve from /apps/{id}/ui/. In dev, add to index.html.')}function u(){if(l)return l;if(window.location.protocol==="neboapp:")return l="http://localhost:27895",l;const s=document.querySelector('meta[name="nebo-base-url"]');return s?(l=s.getAttribute("content")||"",l):(l=window.location.origin,l)}function O(s){i=s}function $(s){l=s}async function E(s,e){const t=h(),n=u();if(s.startsWith("http://")||s.startsWith("https://")){const f=`${n}/api/v1/apps/${t}/http/proxy`,r={};e!=null&&e.headers&&new Headers(e.headers).forEach((C,A)=>{r[A]=C});const p={url:s,method:(e==null?void 0:e.method)||"GET",headers:r,body:e!=null&&e.body?String(e.body):void 0},d=await(await fetch(f,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(p)})).json();return new Response(d.body,{status:d.status,headers:d.headers})}const a=s.startsWith("/")?s:`/${s}`,o=`${n}/api/v1/apps/${t}/api${a}`;return fetch(o,e)}class v{constructor(e){this.ws=null,this.reconnectTimer=null,this.reconnectDelay=1e3,this.maxReconnectDelay=3e4,this._closed=!1,this.onopen=null,this.onmessage=null,this.onerror=null,this.onclose=null;const t=h(),a=u().replace(/^http/,"ws");this.url=`${a}/ws/app/${t}`,this.connect()}connect(){this._closed||(this.ws=new WebSocket(this.url),this.ws.onopen=e=>{var t;this.reconnectDelay=1e3,(t=this.onopen)==null||t.call(this,e)},this.ws.onmessage=e=>{var t;return(t=this.onmessage)==null?void 0:t.call(this,e)},this.ws.onerror=e=>{var t;return(t=this.onerror)==null?void 0:t.call(this,e)},this.ws.onclose=e=>{var t;(t=this.onclose)==null||t.call(this,e),this._closed||this.scheduleReconnect()})}scheduleReconnect(){this.reconnectTimer||this._closed||(this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.reconnectDelay=Math.min(this.reconnectDelay*2,this.maxReconnectDelay),this.connect()},this.reconnectDelay))}get readyState(){var e;return((e=this.ws)==null?void 0:e.readyState)??WebSocket.CLOSED}send(e){var t;(t=this.ws)==null||t.send(e)}close(e,t){var n;this._closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),(n=this.ws)==null||n.close(e,t)}}function b(s){const e=h(),t=u();return s?`${t}/api/v1/apps/${e}/storage/${encodeURIComponent(s)}`:`${t}/api/v1/apps/${e}/storage`}const j={async getItem(s){const e=await fetch(b(s));if(e.status===404)return null;const t=await e.json();try{return JSON.parse(t.value)}catch{return t.value}},async setItem(s,e){const t=typeof e=="string"?e:JSON.stringify(e);await fetch(b(s),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({value:t})})},async removeItem(s){await fetch(b(s),{method:"DELETE"})},async clear(){const s=await this.keys();await Promise.all(s.map(e=>this.removeItem(e)))},async keys(){return((await(await fetch(b())).json()).items||[]).map(t=>t[0])}};function T(s){const e=h();return`${u()}/api/v1/apps/${e}/agents/${s}`}const k={async invoke(s,e){return(await fetch(T("invoke"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:s,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})})).json()},async*stream(s,e){const t=await fetch(T("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:s,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})});if(!t.body)throw new Error("No response body for streaming");const n=t.body.getReader(),a=new TextDecoder;let o="";for(;;){const{done:f,value:r}=await n.read();if(f)break;o+=a.decode(r,{stream:!0});const p=o.split(` -`);o=p.pop()||"";for(const y of p)if(y.startsWith("data: ")){const d=y.slice(6);if(d==="[DONE]")return;try{yield JSON.parse(d)}catch{yield{text:d,done:!1}}}}}};function I(s){const e=h();return`${u()}/api/v1/apps/${e}/janus/${s}`}const D={async complete(s){const t=await(await fetch(I("complete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})).json();return t.text||t.content||""},async*stream(s){const e=await fetch(I("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!e.body)throw new Error("No response body for streaming");const t=e.body.getReader(),n=new TextDecoder;let a="";for(;;){const{done:o,value:f}=await t.read();if(o)break;a+=n.decode(f,{stream:!0});const r=a.split(` -`);a=r.pop()||"";for(const p of r)if(p.startsWith("data: ")){const y=p.slice(6);if(y==="[DONE]")return;try{yield JSON.parse(y).text}catch{yield y}}}}};class x{constructor(){this.ws=null,this.listeners=new Map,this._connected=!1,this._a2uiHandler=null,this.state={}}get connected(){return this._connected}connect(){this.ws||(this.ws=new v,this.ws.onopen=()=>{this._connected=!0},this.ws.onmessage=e=>{var t,n;try{const a=JSON.parse(e.data);if(a.type==="a2ui_message"&&((t=a.data)!=null&&t.message)){(n=this._a2uiHandler)==null||n.call(this,a.data.message);return}this.handleEvent(a)}catch{}},this.ws.onclose=()=>{this._connected=!1},this.ws.onerror=()=>{this._connected=!1})}disconnect(){this.ws&&(this.ws.close(),this.ws=null),this._connected=!1}on(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),()=>{var n;(n=this.listeners.get(e))==null||n.delete(t)}}off(e,t){var n;(n=this.listeners.get(e))==null||n.delete(t)}send(e,t){!this.ws||this.ws.readyState!==WebSocket.OPEN||this.ws.send(JSON.stringify({type:"action",name:e,...t}))}requestState(){this.send("request_state")}_rawSend(e){this.ws&&this.ws.readyState===WebSocket.OPEN&&this.ws.send(e)}handleEvent(e){e.type==="state_snapshot"?this.state={...e.snapshot}:e.type==="state_delta"&&this.applyDelta(e.delta);const t=this.listeners.get(e.type);if(t)for(const a of t)try{a(e)}catch{}const n=this.listeners.get("*");if(n)for(const a of n)try{a(e)}catch{}}applyDelta(e){for(const t of e){const n=t.path.split("/").filter(Boolean);if(n.length===0)continue;const a=this.resolveParent(n);if(!a)continue;const o=n[n.length-1];switch(t.op){case"add":case"replace":a[o]=t.value;break;case"remove":delete a[o];break}}}resolveParent(e){let t=this.state;for(let n=0;n{g=g.filter(e=>e!==s)}},setContext(s){var e;(e=c==null?void 0:c.contentWindow)==null||e.postMessage({type:"nebo:set-context",context:s},"*")},newThread(){var s;(s=c==null?void 0:c.contentWindow)==null||s.postMessage({type:"nebo:new-thread"},"*")}};class P{constructor(){this.fetch=E,this.WebSocket=v,this.storage=j,this.agents=k,this.janus=D,this.surfaces=_,this.a2ui=N,this.identity=W,this.chat=J,_._a2uiHandler=e=>N._handleMessage(e),N._setSendFn(e=>_._rawSend(e))}configure(e){e.appId&&O(e.appId),e.baseUrl&&$(e.baseUrl)}}const F=new P;exports.NeboA2UI=U;exports.NeboSDK=P;exports.NeboSurfaces=x;exports.NeboWebSocket=v;exports.a2ui=N;exports.agents=k;exports.chat=J;exports.getAppId=h;exports.getBaseUrl=u;exports.identity=W;exports.janus=D;exports.nebo=F;exports.neboFetch=E;exports.setAppId=O;exports.setBaseUrl=$;exports.storage=j;exports.surfaces=_; +"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});let i=null,d=null;function l(){if(i)return i;if(window.location.protocol==="neboapp:")return i=window.location.hostname,i;const s=window.location.pathname.match(/^\/apps\/([^/]+)\/ui/);if(s)return i=s[1],i;const e=document.querySelector('meta[name="nebo-app-id"]');if(e)return i=e.getAttribute("content")||"",i;throw new Error('[nebo-sdk] Cannot detect app ID. In production, serve from /apps/{id}/ui/. In dev, add to index.html.')}function u(){if(d)return d;if(window.location.protocol==="neboapp:")return d="http://localhost:27895",d;const s=document.querySelector('meta[name="nebo-base-url"]');return s?(d=s.getAttribute("content")||"",d):(d=window.location.origin,d)}function E(s){i=s}function k(s){d=s}async function j(s,e){const t=l(),n=u();if(s.startsWith("http://")||s.startsWith("https://")){const f=`${n}/api/v1/apps/${t}/http/proxy`,r={};e!=null&&e.headers&&new Headers(e.headers).forEach((F,M)=>{r[M]=F});const p={url:s,method:(e==null?void 0:e.method)||"GET",headers:r,body:e!=null&&e.body?String(e.body):void 0},h=await(await fetch(f,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(p)})).json();return new Response(h.body,{status:h.status,headers:h.headers})}const a=s.startsWith("/")?s:`/${s}`,o=`${n}/api/v1/apps/${t}/api${a}`;return fetch(o,e)}class I{constructor(e){this.ws=null,this.reconnectTimer=null,this.reconnectDelay=1e3,this.maxReconnectDelay=3e4,this._closed=!1,this.onopen=null,this.onmessage=null,this.onerror=null,this.onclose=null;const t=l(),a=u().replace(/^http/,"ws");this.url=`${a}/ws/app/${t}`,this.connect()}connect(){this._closed||(this.ws=new WebSocket(this.url),this.ws.onopen=e=>{var t;this.reconnectDelay=1e3,(t=this.onopen)==null||t.call(this,e)},this.ws.onmessage=e=>{var t;return(t=this.onmessage)==null?void 0:t.call(this,e)},this.ws.onerror=e=>{var t;return(t=this.onerror)==null?void 0:t.call(this,e)},this.ws.onclose=e=>{var t;(t=this.onclose)==null||t.call(this,e),this._closed||this.scheduleReconnect()})}scheduleReconnect(){this.reconnectTimer||this._closed||(this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.reconnectDelay=Math.min(this.reconnectDelay*2,this.maxReconnectDelay),this.connect()},this.reconnectDelay))}get readyState(){var e;return((e=this.ws)==null?void 0:e.readyState)??WebSocket.CLOSED}send(e){var t;(t=this.ws)==null||t.send(e)}close(e,t){var n;this._closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),(n=this.ws)==null||n.close(e,t)}}const T=new Set;let O=null;function R(){O||(O=new I,O.onmessage=s=>{let e;try{e=JSON.parse(s.data)}catch{return}e.type!=="app_data_changed"||!e.data||e.data.appId&&e.data.appId!==l()||T.forEach(t=>t(e.data))})}function B(s){if(typeof s!="string")return s;let e;try{e=JSON.parse(s)}catch{return s}if(typeof e!="string")return e;try{return JSON.parse(e)}catch{return e}}function b(s){const e=l(),t=u();return s?`${t}/api/v1/apps/${e}/storage/${encodeURIComponent(s)}`:`${t}/api/v1/apps/${e}/storage`}const x={async getItem(s){const e=await fetch(b(s));if(e.status===404)return null;const t=await e.json();return B(t.value)},async setItem(s,e){const t=typeof e=="string"?e:JSON.stringify(e);await fetch(b(s),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({value:t})})},async removeItem(s){await fetch(b(s),{method:"DELETE"})},async clear(){const s=await this.keys();await Promise.all(s.map(e=>this.removeItem(e)))},async keys(){return((await(await fetch(b())).json()).items||[]).map(t=>t[0])},onChange(s){return T.add(s),R(),()=>{T.delete(s)}}};function v(s){const e=l();return`${u()}/api/v1/apps/${e}/agents/${s}`}const D={async invoke(s,e){return(await fetch(v("invoke"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:s,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})})).json()},async*stream(s,e){const t=await fetch(v("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:s,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})});if(!t.body)throw new Error("No response body for streaming");const n=t.body.getReader(),a=new TextDecoder;let o="";for(;;){const{done:f,value:r}=await n.read();if(f)break;o+=a.decode(r,{stream:!0});const p=o.split(` +`);o=p.pop()||"";for(const y of p)if(y.startsWith("data: ")){const h=y.slice(6);if(h==="[DONE]")return;try{yield JSON.parse(h)}catch{yield{text:h,done:!1}}}}}};function $(s){const e=l();return`${u()}/api/v1/apps/${e}/janus/${s}`}const U={async complete(s){const t=await(await fetch($("complete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})).json();return t.text||t.content||""},async*stream(s){const e=await fetch($("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!e.body)throw new Error("No response body for streaming");const t=e.body.getReader(),n=new TextDecoder;let a="";for(;;){const{done:o,value:f}=await t.read();if(o)break;a+=n.decode(f,{stream:!0});const r=a.split(` +`);a=r.pop()||"";for(const p of r)if(p.startsWith("data: ")){const y=p.slice(6);if(y==="[DONE]")return;try{yield JSON.parse(y).text}catch{yield y}}}}};class J{constructor(){this.ws=null,this.listeners=new Map,this._connected=!1,this._a2uiHandler=null,this.state={}}get connected(){return this._connected}connect(){this.ws||(this.ws=new I,this.ws.onopen=()=>{this._connected=!0},this.ws.onmessage=e=>{var t,n;try{const a=JSON.parse(e.data);if(a.type==="a2ui_message"&&((t=a.data)!=null&&t.message)){(n=this._a2uiHandler)==null||n.call(this,a.data.message);return}this.handleEvent(a)}catch{}},this.ws.onclose=()=>{this._connected=!1},this.ws.onerror=()=>{this._connected=!1})}disconnect(){this.ws&&(this.ws.close(),this.ws=null),this._connected=!1}on(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),()=>{var n;(n=this.listeners.get(e))==null||n.delete(t)}}off(e,t){var n;(n=this.listeners.get(e))==null||n.delete(t)}send(e,t){!this.ws||this.ws.readyState!==WebSocket.OPEN||this.ws.send(JSON.stringify({type:"action",name:e,...t}))}requestState(){this.send("request_state")}_rawSend(e){this.ws&&this.ws.readyState===WebSocket.OPEN&&this.ws.send(e)}handleEvent(e){e.type==="state_snapshot"?this.state={...e.snapshot}:e.type==="state_delta"&&this.applyDelta(e.delta);const t=this.listeners.get(e.type);if(t)for(const a of t)try{a(e)}catch{}const n=this.listeners.get("*");if(n)for(const a of n)try{a(e)}catch{}}applyDelta(e){for(const t of e){const n=t.path.split("/").filter(Boolean);if(n.length===0)continue;const a=this.resolveParent(n);if(!a)continue;const o=n[n.length-1];switch(t.op){case"add":case"replace":a[o]=t.value;break;case"remove":delete a[o];break}}}resolveParent(e){let t=this.state;for(let n=0;n{m=m.filter(e=>e!==s)}},setContext(s){var e;(e=c==null?void 0:c.contentWindow)==null||e.postMessage({type:"nebo:set-context",context:s},"*")},newThread(){var s;(s=c==null?void 0:c.contentWindow)==null||s.postMessage({type:"nebo:new-thread"},"*")}};class A{constructor(){this.fetch=j,this.WebSocket=I,this.storage=x,this.agents=D,this.janus=U,this.surfaces=_,this.a2ui=N,this.identity=C,this.chat=P,_._a2uiHandler=e=>N._handleMessage(e),N._setSendFn(e=>_._rawSend(e))}configure(e){e.appId&&E(e.appId),e.baseUrl&&k(e.baseUrl)}}const L=new A;exports.NeboA2UI=W;exports.NeboSDK=A;exports.NeboSurfaces=J;exports.NeboWebSocket=I;exports.a2ui=N;exports.agents=D;exports.chat=P;exports.getAppId=l;exports.getBaseUrl=u;exports.identity=C;exports.janus=U;exports.nebo=L;exports.neboFetch=j;exports.setAppId=E;exports.setBaseUrl=k;exports.storage=x;exports.surfaces=_; diff --git a/dist/index.d.ts b/dist/index.d.ts index 27a297b..4312636 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -19,6 +19,7 @@ export { NeboWebSocket } from './websocket'; export { NeboSurfaces, surfaces } from './surfaces'; export { NeboA2UI, a2ui } from './a2ui'; export { storage } from './storage'; +export type { StorageChange } from './storage'; export { agents } from './agents'; export { janus } from './janus'; export { neboFetch } from './fetch'; diff --git a/dist/index.js b/dist/index.js index d40c210..3877c85 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,5 +1,5 @@ -let i = null, l = null; -function p() { +let i = null, d = null; +function l() { if (i) return i; if (window.location.protocol === "neboapp:") return i = window.location.hostname, i; @@ -14,47 +14,47 @@ function p() { ); } function y() { - if (l) return l; + if (d) return d; if (window.location.protocol === "neboapp:") - return l = "http://localhost:27895", l; + return d = "http://localhost:27895", d; const s = document.querySelector('meta[name="nebo-base-url"]'); - return s ? (l = s.getAttribute("content") || "", l) : (l = window.location.origin, l); + return s ? (d = s.getAttribute("content") || "", d) : (d = window.location.origin, d); } -function E(s) { +function k(s) { i = s; } -function x(s) { - l = s; +function D(s) { + d = s; } -async function k(s, e) { - const t = p(), n = y(); +async function j(s, e) { + const t = l(), n = y(); if (s.startsWith("http://") || s.startsWith("https://")) { - const h = `${n}/api/v1/apps/${t}/http/proxy`, r = {}; - e != null && e.headers && new Headers(e.headers).forEach((N, I) => { - r[I] = N; + const u = `${n}/api/v1/apps/${t}/http/proxy`, r = {}; + e != null && e.headers && new Headers(e.headers).forEach((E, x) => { + r[x] = E; }); - const u = { + const f = { url: s, method: (e == null ? void 0 : e.method) || "GET", headers: r, body: e != null && e.body ? String(e.body) : void 0 - }, d = await (await fetch(h, { + }, h = await (await fetch(u, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(u) + body: JSON.stringify(f) })).json(); - return new Response(d.body, { - status: d.status, - headers: d.headers + return new Response(h.body, { + status: h.status, + headers: h.headers }); } const a = s.startsWith("/") ? s : `/${s}`, o = `${n}/api/v1/apps/${t}/api${a}`; return fetch(o, e); } -class O { +class $ { constructor(e) { this.ws = null, this.reconnectTimer = null, this.reconnectDelay = 1e3, this.maxReconnectDelay = 3e4, this._closed = !1, this.onopen = null, this.onmessage = null, this.onerror = null, this.onclose = null; - const t = p(), a = y().replace(/^http/, "ws"); + const t = l(), a = y().replace(/^http/, "ws"); this.url = `${a}/ws/app/${t}`, this.connect(); } connect() { @@ -90,20 +90,44 @@ class O { this._closed = !0, this.reconnectTimer && (clearTimeout(this.reconnectTimer), this.reconnectTimer = null), (n = this.ws) == null || n.close(e, t); } } +const T = /* @__PURE__ */ new Set(); +let _ = null; +function J() { + _ || (_ = new $(), _.onmessage = (s) => { + let e; + try { + e = JSON.parse(s.data); + } catch { + return; + } + e.type !== "app_data_changed" || !e.data || e.data.appId && e.data.appId !== l() || T.forEach((t) => t(e.data)); + }); +} +function W(s) { + if (typeof s != "string") return s; + let e; + try { + e = JSON.parse(s); + } catch { + return s; + } + if (typeof e != "string") return e; + try { + return JSON.parse(e); + } catch { + return e; + } +} function b(s) { - const e = p(), t = y(); + const e = l(), t = y(); return s ? `${t}/api/v1/apps/${e}/storage/${encodeURIComponent(s)}` : `${t}/api/v1/apps/${e}/storage`; } -const D = { +const C = { async getItem(s) { const e = await fetch(b(s)); if (e.status === 404) return null; const t = await e.json(); - try { - return JSON.parse(t.value); - } catch { - return t.value; - } + return W(t.value); }, async setItem(s, e) { const t = typeof e == "string" ? e : JSON.stringify(e); @@ -122,15 +146,24 @@ const D = { }, async keys() { return ((await (await fetch(b())).json()).items || []).map((t) => t[0]); + }, + /** + * Call `handler` after every change to the store: the employee's writes + * and other open views'. Returns a function that stops listening. + */ + onChange(s) { + return T.add(s), J(), () => { + T.delete(s); + }; } }; -function T(s) { - const e = p(); +function v(s) { + const e = l(); return `${y()}/api/v1/apps/${e}/agents/${s}`; } -const j = { +const U = { async invoke(s, e) { - return (await fetch(T("invoke"), { + return (await fetch(v("invoke"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -141,7 +174,7 @@ const j = { })).json(); }, async *stream(s, e) { - const t = await fetch(T("stream"), { + const t = await fetch(v("stream"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -155,32 +188,32 @@ const j = { const n = t.body.getReader(), a = new TextDecoder(); let o = ""; for (; ; ) { - const { done: h, value: r } = await n.read(); - if (h) break; + const { done: u, value: r } = await n.read(); + if (u) break; o += a.decode(r, { stream: !0 }); - const u = o.split(` + const f = o.split(` `); - o = u.pop() || ""; - for (const f of u) - if (f.startsWith("data: ")) { - const d = f.slice(6); - if (d === "[DONE]") return; + o = f.pop() || ""; + for (const p of f) + if (p.startsWith("data: ")) { + const h = p.slice(6); + if (h === "[DONE]") return; try { - yield JSON.parse(d); + yield JSON.parse(h); } catch { - yield { text: d, done: !1 }; + yield { text: h, done: !1 }; } } } } }; -function $(s) { - const e = p(); +function I(s) { + const e = l(); return `${y()}/api/v1/apps/${e}/janus/${s}`; } -const W = { +const P = { async complete(s) { - const t = await (await fetch($("complete"), { + const t = await (await fetch(I("complete"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(s) @@ -188,7 +221,7 @@ const W = { return t.text || t.content || ""; }, async *stream(s) { - const e = await fetch($("stream"), { + const e = await fetch(I("stream"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(s) @@ -198,26 +231,26 @@ const W = { const t = e.body.getReader(), n = new TextDecoder(); let a = ""; for (; ; ) { - const { done: o, value: h } = await t.read(); + const { done: o, value: u } = await t.read(); if (o) break; - a += n.decode(h, { stream: !0 }); + a += n.decode(u, { stream: !0 }); const r = a.split(` `); a = r.pop() || ""; - for (const u of r) - if (u.startsWith("data: ")) { - const f = u.slice(6); - if (f === "[DONE]") return; + for (const f of r) + if (f.startsWith("data: ")) { + const p = f.slice(6); + if (p === "[DONE]") return; try { - yield JSON.parse(f).text; + yield JSON.parse(p).text; } catch { - yield f; + yield p; } } } } }; -class J { +class F { constructor() { this.ws = null, this.listeners = /* @__PURE__ */ new Map(), this._connected = !1, this._a2uiHandler = null, this.state = {}; } @@ -227,7 +260,7 @@ class J { } /** Connect to the app's surface WebSocket */ connect() { - this.ws || (this.ws = new O(), this.ws.onopen = () => { + this.ws || (this.ws = new $(), this.ws.onopen = () => { this._connected = !0; }, this.ws.onmessage = (e) => { var t, n; @@ -319,8 +352,8 @@ class J { return t; } } -const _ = new J(); -class U { +const O = new F(); +class M { constructor() { this.processor = null, this.sendFn = null; } @@ -387,12 +420,12 @@ class U { })); } } -const v = new U(); +const N = new M(); let w = null; -const P = { +const R = { async get() { if (w) return w; - const s = p(), e = y(), t = await fetch(`${e}/api/v1/apps/${s}/identity`); + const s = l(), e = y(), t = await fetch(`${e}/api/v1/apps/${s}/identity`); if (!t.ok) throw new Error(`[nebo-sdk] identity fetch failed: ${t.status}`); return w = await t.json(), w; @@ -402,20 +435,20 @@ const P = { } }; let c = null, S = null, g = [], m = null; -function C(s) { +function A(s) { if (!(!s.data || typeof s.data.type != "string") && s.data.type.startsWith("nebo:")) { s.data.type === "nebo:resize" && c && s.data.height && (c.style.height = `${s.data.height}px`); for (const e of g) e(s.data); } } -const M = { +const H = { mount(s, e) { c && this.unmount(); - const t = p(), n = y(), a = new URLSearchParams(); + const t = l(), n = y(), a = new URLSearchParams(); e != null && e.placeholder && a.set("placeholder", e.placeholder), e != null && e.theme && a.set("theme", e.theme), e != null && e.borderless && a.set("borderless", "1"), e != null && e.contextId && a.set("ctx", e.contextId), e != null && e.scope && a.set("scope", e.scope); - const o = a.toString(), h = `${n}/chat-embed/${t}${o ? "?" + o : ""}`, r = document.createElement("iframe"); - r.src = h, r.style.width = "100%", r.style.height = (e == null ? void 0 : e.height) || "400px", r.style.border = e != null && e.borderless ? "none" : "", r.style.borderRadius = e != null && e.borderless ? "0" : "0.5rem", r.style.colorScheme = "normal", r.setAttribute("allow", "microphone"), s.appendChild(r), c = r, S = s, m = C, window.addEventListener("message", m); + const o = a.toString(), u = `${n}/chat-embed/${t}${o ? "?" + o : ""}`, r = document.createElement("iframe"); + r.src = u, r.style.width = "100%", r.style.height = (e == null ? void 0 : e.height) || "400px", r.style.border = e != null && e.borderless ? "none" : "", r.style.borderRadius = e != null && e.borderless ? "0" : "0.5rem", r.style.colorScheme = "normal", r.setAttribute("allow", "microphone"), s.appendChild(r), c = r, S = s, m = A, window.addEventListener("message", m); }, unmount() { c && S && S.removeChild(c), m && (window.removeEventListener("message", m), m = null), c = null, S = null, g = []; @@ -454,34 +487,34 @@ const M = { ); } }; -class R { +class L { constructor() { - this.fetch = k, this.WebSocket = O, this.storage = D, this.agents = j, this.janus = W, this.surfaces = _, this.a2ui = v, this.identity = P, this.chat = M, _._a2uiHandler = (e) => v._handleMessage(e), v._setSendFn((e) => _._rawSend(e)); + this.fetch = j, this.WebSocket = $, this.storage = C, this.agents = U, this.janus = P, this.surfaces = O, this.a2ui = N, this.identity = R, this.chat = H, O._a2uiHandler = (e) => N._handleMessage(e), N._setSendFn((e) => O._rawSend(e)); } /** * Manually configure the SDK (optional — auto-detection works in most cases). */ configure(e) { - e.appId && E(e.appId), e.baseUrl && x(e.baseUrl); + e.appId && k(e.appId), e.baseUrl && D(e.baseUrl); } } -const A = new R(); +const B = new L(); export { - U as NeboA2UI, - R as NeboSDK, - J as NeboSurfaces, - O as NeboWebSocket, - v as a2ui, - j as agents, - M as chat, - p as getAppId, + M as NeboA2UI, + L as NeboSDK, + F as NeboSurfaces, + $ as NeboWebSocket, + N as a2ui, + U as agents, + H as chat, + l as getAppId, y as getBaseUrl, - P as identity, - W as janus, - A as nebo, - k as neboFetch, - E as setAppId, - x as setBaseUrl, - D as storage, - _ as surfaces + R as identity, + P as janus, + B as nebo, + j as neboFetch, + k as setAppId, + D as setBaseUrl, + C as storage, + O as surfaces }; diff --git a/dist/nebo.global.js b/dist/nebo.global.js index c1b2b46..fdd2a7e 100644 --- a/dist/nebo.global.js +++ b/dist/nebo.global.js @@ -1,3 +1,3 @@ -var NeboAppSDK=(function(r){"use strict";let l=null,d=null;function h(){if(l)return l;if(window.location.protocol==="neboapp:")return l=window.location.hostname,l;const s=window.location.pathname.match(/^\/apps\/([^/]+)\/ui/);if(s)return l=s[1],l;const e=document.querySelector('meta[name="nebo-app-id"]');if(e)return l=e.getAttribute("content")||"",l;throw new Error('[nebo-sdk] Cannot detect app ID. In production, serve from /apps/{id}/ui/. In dev, add to index.html.')}function u(){if(d)return d;if(window.location.protocol==="neboapp:")return d="http://localhost:27895",d;const s=document.querySelector('meta[name="nebo-base-url"]');return s?(d=s.getAttribute("content")||"",d):(d=window.location.origin,d)}function I(s){l=s}function O(s){d=s}async function $(s,e){const t=h(),n=u();if(s.startsWith("http://")||s.startsWith("https://")){const p=`${n}/api/v1/apps/${t}/http/proxy`,c={};e!=null&&e.headers&&new Headers(e.headers).forEach((R,B)=>{c[B]=R});const y={url:s,method:(e==null?void 0:e.method)||"GET",headers:c,body:e!=null&&e.body?String(e.body):void 0},f=await(await fetch(p,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(y)})).json();return new Response(f.body,{status:f.status,headers:f.headers})}const a=s.startsWith("/")?s:`/${s}`,i=`${n}/api/v1/apps/${t}/api${a}`;return fetch(i,e)}class T{constructor(e){this.ws=null,this.reconnectTimer=null,this.reconnectDelay=1e3,this.maxReconnectDelay=3e4,this._closed=!1,this.onopen=null,this.onmessage=null,this.onerror=null,this.onclose=null;const t=h(),a=u().replace(/^http/,"ws");this.url=`${a}/ws/app/${t}`,this.connect()}connect(){this._closed||(this.ws=new WebSocket(this.url),this.ws.onopen=e=>{var t;this.reconnectDelay=1e3,(t=this.onopen)==null||t.call(this,e)},this.ws.onmessage=e=>{var t;return(t=this.onmessage)==null?void 0:t.call(this,e)},this.ws.onerror=e=>{var t;return(t=this.onerror)==null?void 0:t.call(this,e)},this.ws.onclose=e=>{var t;(t=this.onclose)==null||t.call(this,e),this._closed||this.scheduleReconnect()})}scheduleReconnect(){this.reconnectTimer||this._closed||(this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.reconnectDelay=Math.min(this.reconnectDelay*2,this.maxReconnectDelay),this.connect()},this.reconnectDelay))}get readyState(){var e;return((e=this.ws)==null?void 0:e.readyState)??WebSocket.CLOSED}send(e){var t;(t=this.ws)==null||t.send(e)}close(e,t){var n;this._closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),(n=this.ws)==null||n.close(e,t)}}function S(s){const e=h(),t=u();return s?`${t}/api/v1/apps/${e}/storage/${encodeURIComponent(s)}`:`${t}/api/v1/apps/${e}/storage`}const D={async getItem(s){const e=await fetch(S(s));if(e.status===404)return null;const t=await e.json();try{return JSON.parse(t.value)}catch{return t.value}},async setItem(s,e){const t=typeof e=="string"?e:JSON.stringify(e);await fetch(S(s),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({value:t})})},async removeItem(s){await fetch(S(s),{method:"DELETE"})},async clear(){const s=await this.keys();await Promise.all(s.map(e=>this.removeItem(e)))},async keys(){return((await(await fetch(S())).json()).items||[]).map(t=>t[0])}};function E(s){const e=h();return`${u()}/api/v1/apps/${e}/agents/${s}`}const j={async invoke(s,e){return(await fetch(E("invoke"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:s,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})})).json()},async*stream(s,e){const t=await fetch(E("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:s,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})});if(!t.body)throw new Error("No response body for streaming");const n=t.body.getReader(),a=new TextDecoder;let i="";for(;;){const{done:p,value:c}=await n.read();if(p)break;i+=a.decode(c,{stream:!0});const y=i.split(` -`);i=y.pop()||"";for(const w of y)if(w.startsWith("data: ")){const f=w.slice(6);if(f==="[DONE]")return;try{yield JSON.parse(f)}catch{yield{text:f,done:!1}}}}}};function k(s){const e=h();return`${u()}/api/v1/apps/${e}/janus/${s}`}const U={async complete(s){const t=await(await fetch(k("complete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})).json();return t.text||t.content||""},async*stream(s){const e=await fetch(k("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!e.body)throw new Error("No response body for streaming");const t=e.body.getReader(),n=new TextDecoder;let a="";for(;;){const{done:i,value:p}=await t.read();if(i)break;a+=n.decode(p,{stream:!0});const c=a.split(` -`);a=c.pop()||"";for(const y of c)if(y.startsWith("data: ")){const w=y.slice(6);if(w==="[DONE]")return;try{yield JSON.parse(w).text}catch{yield w}}}}};class W{constructor(){this.ws=null,this.listeners=new Map,this._connected=!1,this._a2uiHandler=null,this.state={}}get connected(){return this._connected}connect(){this.ws||(this.ws=new T,this.ws.onopen=()=>{this._connected=!0},this.ws.onmessage=e=>{var t,n;try{const a=JSON.parse(e.data);if(a.type==="a2ui_message"&&((t=a.data)!=null&&t.message)){(n=this._a2uiHandler)==null||n.call(this,a.data.message);return}this.handleEvent(a)}catch{}},this.ws.onclose=()=>{this._connected=!1},this.ws.onerror=()=>{this._connected=!1})}disconnect(){this.ws&&(this.ws.close(),this.ws=null),this._connected=!1}on(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),()=>{var n;(n=this.listeners.get(e))==null||n.delete(t)}}off(e,t){var n;(n=this.listeners.get(e))==null||n.delete(t)}send(e,t){!this.ws||this.ws.readyState!==WebSocket.OPEN||this.ws.send(JSON.stringify({type:"action",name:e,...t}))}requestState(){this.send("request_state")}_rawSend(e){this.ws&&this.ws.readyState===WebSocket.OPEN&&this.ws.send(e)}handleEvent(e){e.type==="state_snapshot"?this.state={...e.snapshot}:e.type==="state_delta"&&this.applyDelta(e.delta);const t=this.listeners.get(e.type);if(t)for(const a of t)try{a(e)}catch{}const n=this.listeners.get("*");if(n)for(const a of n)try{a(e)}catch{}}applyDelta(e){for(const t of e){const n=t.path.split("/").filter(Boolean);if(n.length===0)continue;const a=this.resolveParent(n);if(!a)continue;const i=n[n.length-1];switch(t.op){case"add":case"replace":a[i]=t.value;break;case"remove":delete a[i];break}}}resolveParent(e){let t=this.state;for(let n=0;n{g=g.filter(e=>e!==s)}},setContext(s){var e;(e=o==null?void 0:o.contentWindow)==null||e.postMessage({type:"nebo:set-context",context:s},"*")},newThread(){var s;(s=o==null?void 0:o.contentWindow)==null||s.postMessage({type:"nebo:new-thread"},"*")}};class C{constructor(){this.fetch=$,this.WebSocket=T,this.storage=D,this.agents=j,this.janus=U,this.surfaces=_,this.a2ui=N,this.identity=P,this.chat=A,_._a2uiHandler=e=>N._handleMessage(e),N._setSendFn(e=>_._rawSend(e))}configure(e){e.appId&&I(e.appId),e.baseUrl&&O(e.baseUrl)}}const F=new C;return r.NeboA2UI=J,r.NeboSDK=C,r.NeboSurfaces=W,r.NeboWebSocket=T,r.a2ui=N,r.agents=j,r.chat=A,r.getAppId=h,r.getBaseUrl=u,r.identity=P,r.janus=U,r.nebo=F,r.neboFetch=$,r.setAppId=I,r.setBaseUrl=O,r.storage=D,r.surfaces=_,Object.defineProperty(r,Symbol.toStringTag,{value:"Module"}),r})({}); +var NeboAppSDK=(function(r){"use strict";let d=null,l=null;function h(){if(d)return d;if(window.location.protocol==="neboapp:")return d=window.location.hostname,d;const s=window.location.pathname.match(/^\/apps\/([^/]+)\/ui/);if(s)return d=s[1],d;const e=document.querySelector('meta[name="nebo-app-id"]');if(e)return d=e.getAttribute("content")||"",d;throw new Error('[nebo-sdk] Cannot detect app ID. In production, serve from /apps/{id}/ui/. In dev, add to index.html.')}function u(){if(l)return l;if(window.location.protocol==="neboapp:")return l="http://localhost:27895",l;const s=document.querySelector('meta[name="nebo-base-url"]');return s?(l=s.getAttribute("content")||"",l):(l=window.location.origin,l)}function $(s){d=s}function E(s){l=s}async function k(s,e){const t=h(),n=u();if(s.startsWith("http://")||s.startsWith("https://")){const p=`${n}/api/v1/apps/${t}/http/proxy`,c={};e!=null&&e.headers&&new Headers(e.headers).forEach((q,z)=>{c[z]=q});const y={url:s,method:(e==null?void 0:e.method)||"GET",headers:c,body:e!=null&&e.body?String(e.body):void 0},f=await(await fetch(p,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(y)})).json();return new Response(f.body,{status:f.status,headers:f.headers})}const a=s.startsWith("/")?s:`/${s}`,i=`${n}/api/v1/apps/${t}/api${a}`;return fetch(i,e)}class S{constructor(e){this.ws=null,this.reconnectTimer=null,this.reconnectDelay=1e3,this.maxReconnectDelay=3e4,this._closed=!1,this.onopen=null,this.onmessage=null,this.onerror=null,this.onclose=null;const t=h(),a=u().replace(/^http/,"ws");this.url=`${a}/ws/app/${t}`,this.connect()}connect(){this._closed||(this.ws=new WebSocket(this.url),this.ws.onopen=e=>{var t;this.reconnectDelay=1e3,(t=this.onopen)==null||t.call(this,e)},this.ws.onmessage=e=>{var t;return(t=this.onmessage)==null?void 0:t.call(this,e)},this.ws.onerror=e=>{var t;return(t=this.onerror)==null?void 0:t.call(this,e)},this.ws.onclose=e=>{var t;(t=this.onclose)==null||t.call(this,e),this._closed||this.scheduleReconnect()})}scheduleReconnect(){this.reconnectTimer||this._closed||(this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.reconnectDelay=Math.min(this.reconnectDelay*2,this.maxReconnectDelay),this.connect()},this.reconnectDelay))}get readyState(){var e;return((e=this.ws)==null?void 0:e.readyState)??WebSocket.CLOSED}send(e){var t;(t=this.ws)==null||t.send(e)}close(e,t){var n;this._closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),(n=this.ws)==null||n.close(e,t)}}const v=new Set;let T=null;function R(){T||(T=new S,T.onmessage=s=>{let e;try{e=JSON.parse(s.data)}catch{return}e.type!=="app_data_changed"||!e.data||e.data.appId&&e.data.appId!==h()||v.forEach(t=>t(e.data))})}function B(s){if(typeof s!="string")return s;let e;try{e=JSON.parse(s)}catch{return s}if(typeof e!="string")return e;try{return JSON.parse(e)}catch{return e}}function _(s){const e=h(),t=u();return s?`${t}/api/v1/apps/${e}/storage/${encodeURIComponent(s)}`:`${t}/api/v1/apps/${e}/storage`}const D={async getItem(s){const e=await fetch(_(s));if(e.status===404)return null;const t=await e.json();return B(t.value)},async setItem(s,e){const t=typeof e=="string"?e:JSON.stringify(e);await fetch(_(s),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({value:t})})},async removeItem(s){await fetch(_(s),{method:"DELETE"})},async clear(){const s=await this.keys();await Promise.all(s.map(e=>this.removeItem(e)))},async keys(){return((await(await fetch(_())).json()).items||[]).map(t=>t[0])},onChange(s){return v.add(s),R(),()=>{v.delete(s)}}};function j(s){const e=h();return`${u()}/api/v1/apps/${e}/agents/${s}`}const U={async invoke(s,e){return(await fetch(j("invoke"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:s,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})})).json()},async*stream(s,e){const t=await fetch(j("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:s,agent:e==null?void 0:e.agent,data:e==null?void 0:e.data})});if(!t.body)throw new Error("No response body for streaming");const n=t.body.getReader(),a=new TextDecoder;let i="";for(;;){const{done:p,value:c}=await n.read();if(p)break;i+=a.decode(c,{stream:!0});const y=i.split(` +`);i=y.pop()||"";for(const w of y)if(w.startsWith("data: ")){const f=w.slice(6);if(f==="[DONE]")return;try{yield JSON.parse(f)}catch{yield{text:f,done:!1}}}}}};function J(s){const e=h();return`${u()}/api/v1/apps/${e}/janus/${s}`}const W={async complete(s){const t=await(await fetch(J("complete"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})).json();return t.text||t.content||""},async*stream(s){const e=await fetch(J("stream"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!e.body)throw new Error("No response body for streaming");const t=e.body.getReader(),n=new TextDecoder;let a="";for(;;){const{done:i,value:p}=await t.read();if(i)break;a+=n.decode(p,{stream:!0});const c=a.split(` +`);a=c.pop()||"";for(const y of c)if(y.startsWith("data: ")){const w=y.slice(6);if(w==="[DONE]")return;try{yield JSON.parse(w).text}catch{yield w}}}}};class C{constructor(){this.ws=null,this.listeners=new Map,this._connected=!1,this._a2uiHandler=null,this.state={}}get connected(){return this._connected}connect(){this.ws||(this.ws=new S,this.ws.onopen=()=>{this._connected=!0},this.ws.onmessage=e=>{var t,n;try{const a=JSON.parse(e.data);if(a.type==="a2ui_message"&&((t=a.data)!=null&&t.message)){(n=this._a2uiHandler)==null||n.call(this,a.data.message);return}this.handleEvent(a)}catch{}},this.ws.onclose=()=>{this._connected=!1},this.ws.onerror=()=>{this._connected=!1})}disconnect(){this.ws&&(this.ws.close(),this.ws=null),this._connected=!1}on(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),()=>{var n;(n=this.listeners.get(e))==null||n.delete(t)}}off(e,t){var n;(n=this.listeners.get(e))==null||n.delete(t)}send(e,t){!this.ws||this.ws.readyState!==WebSocket.OPEN||this.ws.send(JSON.stringify({type:"action",name:e,...t}))}requestState(){this.send("request_state")}_rawSend(e){this.ws&&this.ws.readyState===WebSocket.OPEN&&this.ws.send(e)}handleEvent(e){e.type==="state_snapshot"?this.state={...e.snapshot}:e.type==="state_delta"&&this.applyDelta(e.delta);const t=this.listeners.get(e.type);if(t)for(const a of t)try{a(e)}catch{}const n=this.listeners.get("*");if(n)for(const a of n)try{a(e)}catch{}}applyDelta(e){for(const t of e){const n=t.path.split("/").filter(Boolean);if(n.length===0)continue;const a=this.resolveParent(n);if(!a)continue;const i=n[n.length-1];switch(t.op){case"add":case"replace":a[i]=t.value;break;case"remove":delete a[i];break}}}resolveParent(e){let t=this.state;for(let n=0;n{m=m.filter(e=>e!==s)}},setContext(s){var e;(e=o==null?void 0:o.contentWindow)==null||e.postMessage({type:"nebo:set-context",context:s},"*")},newThread(){var s;(s=o==null?void 0:o.contentWindow)==null||s.postMessage({type:"nebo:new-thread"},"*")}};class M{constructor(){this.fetch=k,this.WebSocket=S,this.storage=D,this.agents=U,this.janus=W,this.surfaces=N,this.a2ui=I,this.identity=A,this.chat=F,N._a2uiHandler=e=>I._handleMessage(e),I._setSendFn(e=>N._rawSend(e))}configure(e){e.appId&&$(e.appId),e.baseUrl&&E(e.baseUrl)}}const L=new M;return r.NeboA2UI=P,r.NeboSDK=M,r.NeboSurfaces=C,r.NeboWebSocket=S,r.a2ui=I,r.agents=U,r.chat=F,r.getAppId=h,r.getBaseUrl=u,r.identity=A,r.janus=W,r.nebo=L,r.neboFetch=k,r.setAppId=$,r.setBaseUrl=E,r.storage=D,r.surfaces=N,Object.defineProperty(r,Symbol.toStringTag,{value:"Module"}),r})({}); diff --git a/dist/sdk.d.ts b/dist/sdk.d.ts index ec64586..2a79b3f 100644 --- a/dist/sdk.d.ts +++ b/dist/sdk.d.ts @@ -12,6 +12,7 @@ export declare class NeboSDK { removeItem(key: string): Promise; clear(): Promise; keys(): Promise; + onChange(handler: (change: import("./storage").StorageChange) => void): () => void; }; agents: { invoke(message: string, options?: import("./types").InvokeOptions): Promise; diff --git a/dist/storage.d.ts b/dist/storage.d.ts index e8b7e02..03c6f45 100644 --- a/dist/storage.d.ts +++ b/dist/storage.d.ts @@ -1,10 +1,29 @@ /** * nebo.storage — mirrors localStorage API with server-persisted async KV. + * + * The same store the app's employee reads and writes with its `app_data` + * tool: same keys, same values. `onChange` hears every write to it, the + * employee's and other open views'. */ +/** One change to the app's store (the `app_data_changed` event). */ +export interface StorageChange { + appId: string; + keys: string[]; + action: 'set' | 'delete'; + /** `employee`: the app's employee wrote it; `page`: an open view did. */ + source: 'employee' | 'page'; +} +type ChangeHandler = (change: StorageChange) => void; export declare const storage: { getItem(key: string): Promise; setItem(key: string, value: unknown): Promise; removeItem(key: string): Promise; clear(): Promise; keys(): Promise; + /** + * Call `handler` after every change to the store: the employee's writes + * and other open views'. Returns a function that stops listening. + */ + onChange(handler: ChangeHandler): () => void; }; +export {}; diff --git a/src/index.ts b/src/index.ts index f0d9334..2d61d1a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,7 @@ export { NeboWebSocket } from './websocket'; export { NeboSurfaces, surfaces } from './surfaces'; export { NeboA2UI, a2ui } from './a2ui'; export { storage } from './storage'; +export type { StorageChange } from './storage'; export { agents } from './agents'; export { janus } from './janus'; export { neboFetch } from './fetch'; diff --git a/src/storage.ts b/src/storage.ts index 0c67c88..cd04d79 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -1,8 +1,60 @@ /** * nebo.storage — mirrors localStorage API with server-persisted async KV. + * + * The same store the app's employee reads and writes with its `app_data` + * tool: same keys, same values. `onChange` hears every write to it, the + * employee's and other open views'. */ import { getAppId, getBaseUrl } from './config'; +import { NeboWebSocket } from './websocket'; + +/** One change to the app's store (the `app_data_changed` event). */ +export interface StorageChange { + appId: string; + keys: string[]; + action: 'set' | 'delete'; + /** `employee`: the app's employee wrote it; `page`: an open view did. */ + source: 'employee' | 'page'; +} + +type ChangeHandler = (change: StorageChange) => void; + +const changeHandlers = new Set(); +let changeSocket: NeboWebSocket | null = null; + +function listenForChanges(): void { + if (changeSocket) return; + changeSocket = new NeboWebSocket(); + changeSocket.onmessage = (msg) => { + let parsed: { type?: string; data?: StorageChange }; + try { + parsed = JSON.parse(msg.data); + } catch { + return; + } + if (parsed.type !== 'app_data_changed' || !parsed.data) return; + if (parsed.data.appId && parsed.data.appId !== getAppId()) return; + changeHandlers.forEach((h) => h(parsed.data as StorageChange)); + }; +} + +/** The value the server holds: the text setItem sent, then that text parsed. */ +function decode(stored: unknown): unknown { + if (typeof stored !== 'string') return stored; + let text: unknown; + try { + text = JSON.parse(stored); + } catch { + return stored; + } + if (typeof text !== 'string') return text; + try { + return JSON.parse(text); + } catch { + return text; + } +} function storageUrl(key?: string): string { const appId = getAppId(); @@ -18,11 +70,7 @@ export const storage = { const resp = await fetch(storageUrl(key)); if (resp.status === 404) return null; const data = await resp.json(); - try { - return JSON.parse(data.value); - } catch { - return data.value; - } + return decode(data.value); }, async setItem(key: string, value: unknown): Promise { @@ -47,5 +95,17 @@ export const storage = { const resp = await fetch(storageUrl()); const data = await resp.json(); return (data.items || []).map((item: [string, string]) => item[0]); + }, + + /** + * Call `handler` after every change to the store: the employee's writes + * and other open views'. Returns a function that stops listening. + */ + onChange(handler: ChangeHandler): () => void { + changeHandlers.add(handler); + listenForChanges(); + return () => { + changeHandlers.delete(handler); + }; } };