From 243f6e5b8034758f0cf7773adefd8c920a26b85c Mon Sep 17 00:00:00 2001 From: Alma Tuck Date: Thu, 1 Oct 2026 09:19:30 -0600 Subject: [PATCH] storage: getItem returns what setItem stored; onChange hears every write getItem decoded the server's value once, so an object or list came back as JSON text. It now returns the value setItem was given. storage.onChange(cb) calls cb({appId, keys, action, source}) on the app_data_changed event: the app's employee changed the store (app_data), or another open view did. Returns a function that stops listening. --- dist/index.cjs | 6 +- dist/index.d.ts | 1 + dist/index.js | 209 +++++++++++++++++++++++++------------------- dist/nebo.global.js | 6 +- dist/sdk.d.ts | 1 + dist/storage.d.ts | 19 ++++ src/index.ts | 1 + src/storage.ts | 70 +++++++++++++-- 8 files changed, 214 insertions(+), 99 deletions(-) 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); + }; } };