@lunora/client 1.0.0-alpha.231 → 1.0.0-alpha.233

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/auth/index.d.mts +1 -1
  2. package/dist/auth/index.d.ts +1 -1
  3. package/dist/index.d.mts +82 -3
  4. package/dist/index.d.ts +82 -3
  5. package/dist/index.mjs +1 -1
  6. package/dist/packem_shared/LunoraClient-CN4shVl4.mjs +1 -0
  7. package/dist/packem_shared/OfflineQueue-BLRESpAe.mjs +1 -0
  8. package/dist/packem_shared/TabCoordinator-DBuR7Dwv.mjs +1 -0
  9. package/dist/packem_shared/client-tuning-C10B2KCO.mjs +1 -0
  10. package/dist/packem_shared/connectLiveStream-CXD8k-Sp.mjs +1 -0
  11. package/dist/packem_shared/{createAsyncStorageQueryCache-f5023fdB.mjs → createAsyncStorageQueryCache-COqH5_SV.mjs} +1 -1
  12. package/dist/packem_shared/{createInMemoryPersistence-Cz2W6XVj.mjs → createInMemoryPersistence-BfgXApi_.mjs} +1 -1
  13. package/dist/packem_shared/{createInMemoryQueryCache-D2uNHLfM.mjs → createInMemoryQueryCache-B5wX-gTy.mjs} +1 -1
  14. package/dist/packem_shared/{createServerClient-BqCdfoE1.mjs → createServerClient-CwEIYb-6.mjs} +1 -1
  15. package/dist/packem_shared/httpStream-DceAMeMf.mjs +9 -0
  16. package/dist/packem_shared/idb-utility-BS7xvou1.mjs +1 -0
  17. package/dist/packem_shared/live-stream-Dil9byXF.mjs +10 -0
  18. package/dist/packem_shared/{liveTopicSubscription-Bs-ULKfs.mjs → liveTopicSubscription-gOmRbuhH.mjs} +1 -1
  19. package/dist/packem_shared/{lunora-client.d-dOEEoF4x.d.mts → lunora-client.d-D7gNlYT7.d.mts} +153 -17
  20. package/dist/packem_shared/{lunora-client.d-DpmxnYmL.d.ts → lunora-client.d-DTJ5zJKH.d.ts} +153 -17
  21. package/dist/packem_shared/offline-queue-Dt_um8sR.mjs +1 -0
  22. package/dist/packem_shared/{preload.d-XVLTUaah.d.mts → preload.d-B0FFchsh.d.mts} +1 -1
  23. package/dist/packem_shared/{preload.d-BkXsKMmU.d.ts → preload.d-d05S6fB-.d.ts} +1 -1
  24. package/dist/packem_shared/stream-idle-C7CsIT_m.mjs +1 -0
  25. package/dist/packem_shared/{wire-errors-Bw04a4ZQ.mjs → wire-errors-BTLdHNoh.mjs} +1 -1
  26. package/dist/query/index.d.mts +2 -2
  27. package/dist/query/index.d.ts +2 -2
  28. package/dist/service.mjs +1 -1
  29. package/dist/ssr/index.d.mts +2 -2
  30. package/dist/ssr/index.d.ts +2 -2
  31. package/dist/ssr/index.mjs +1 -1
  32. package/package.json +1 -1
  33. package/dist/packem_shared/LunoraClient-D6MyuSWy.mjs +0 -1
  34. package/dist/packem_shared/OfflineQueue-BWamzR6p.mjs +0 -1
  35. package/dist/packem_shared/TabCoordinator-CXD7vulJ.mjs +0 -1
  36. package/dist/packem_shared/connectLiveStream-DCgklEjI.mjs +0 -1
  37. package/dist/packem_shared/httpStream-DaqP70Ne.mjs +0 -9
  38. package/dist/packem_shared/idb-utility-C8WS390w.mjs +0 -1
  39. package/dist/packem_shared/live-stream-ct8j7T75.mjs +0 -10
  40. package/dist/packem_shared/offline-queue-D_fr-uQ3.mjs +0 -1
@@ -0,0 +1 @@
1
+ import{O as s,n,r as o}from"./offline-queue-Dt_um8sR.mjs";export{s as OfflineQueue,n as nextId,o as reportPersistenceError};
@@ -0,0 +1 @@
1
+ let d=0;const s="lunora-bridge",o=1e3,h=3e3;class l{bc;tabId;heartbeatInterval;leaderTimeout;knownLeader=void 0;leader=!1;running=!1;promotionPending=!1;lastHeartbeat=0;pendingData=new Map;heartbeatTimer=void 0;leaderCheckTimer=void 0;onBecomeLeader;onStopBeingLeader;onConnectionStatus;onSubscriptionData;onLeaderClaimAnswered;onSubscriptionError;onSubscriptionSettled;constructor(t={}){if(d+=1,this.tabId=`tab_${String(d)}_${crypto.randomUUID()}`,this.heartbeatInterval=t.heartbeatInterval??o,this.leaderTimeout=t.leaderTimeout??h,this.onBecomeLeader=t.onBecomeLeader,this.onStopBeingLeader=t.onStopBeingLeader,this.onConnectionStatus=t.onConnectionStatus,this.onSubscriptionData=t.onSubscriptionData,this.onLeaderClaimAnswered=t.onLeaderClaimAnswered,this.onSubscriptionError=t.onSubscriptionError,this.onSubscriptionSettled=t.onSubscriptionSettled,typeof BroadcastChannel>"u"){this.bc=void 0;return}this.bc=new BroadcastChannel(t.channelName??s),this.bc.addEventListener("message",e=>{this.handleMessage(e.data)})}start(){if(!this.running){if(this.running=!0,this.bc===void 0){this.becomeLeader();return}this.broadcast({type:"claim-leadership",tabId:this.tabId,ts:Date.now()}),setTimeout(()=>{this.running&&this.knownLeader===void 0&&this.becomeLeader()},this.leaderTimeout),this.leaderCheckTimer=setInterval(()=>{this.checkLeaderHealth()},this.leaderTimeout),this.heartbeatTimer=setInterval(()=>{this.sendHeartbeat()},this.heartbeatInterval)}}stop(){this.running=!1,this.leader&&(this.broadcast({type:"yield-leadership",tabId:this.tabId}),this.leader=!1,this.knownLeader=void 0,this.onStopBeingLeader?.()),this.heartbeatTimer!==void 0&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=void 0),this.leaderCheckTimer!==void 0&&(clearInterval(this.leaderCheckTimer),this.leaderCheckTimer=void 0),this.bc?.close()}isLeader(){return this.leader}promoteImmediately(){this.running&&this.becomeLeader()}get leaderTabId(){return this.knownLeader}get id(){return this.tabId}get isRunning(){return this.running}broadcastSubscriptionData(t,e,i,a,r){this.leader&&(this.pendingData.size===0&&queueMicrotask(()=>{this.flushPendingData()}),this.pendingData.set(t,{type:"subscription-data",tabId:this.tabId,key:t,data:e,...i===void 0?{}:{cursor:i},...a===void 0?{}:{epoch:a},...r===void 0?{}:{identity:r}}))}broadcastSubscriptionError(t,e,i){this.leader&&this.broadcast({type:"subscription-error",tabId:this.tabId,key:t,error:e,...i===void 0?{}:{identity:i}})}broadcastSubscriptionSettled(t,e,i,a,r,n){this.leader&&this.broadcast({type:"subscription-settled",tabId:this.tabId,key:t,...e===void 0?{}:{cursor:e},...i===void 0?{}:{epoch:i},...a===void 0?{}:{lastMutationId:a},...r===void 0?{}:{clientId:r},...n===void 0?{}:{identity:n}})}broadcastConnectionStatus(t,e){this.leader&&this.broadcast({type:"connection-status",tabId:this.tabId,status:t,...e===void 0?{}:{identity:e}})}broadcast(t){this.flushPendingData(),this.bc?.postMessage(t)}flushPendingData(){if(this.pendingData.size===0)return;const t=[...this.pendingData.values()];if(this.pendingData.clear(),!!this.leader)for(const e of t)this.bc?.postMessage(e)}handleMessage(t){switch(t.type){case"claim-leadership":{this.handleClaimLeadership(t);break}case"connection-status":{if(this.leader||t.tabId===this.tabId)break;this.onConnectionStatus?.(t.status,t.identity);break}case"heartbeat":{this.handleHeartbeat(t);break}case"subscription-data":{if(this.leader||t.tabId===this.tabId)break;this.onSubscriptionData?.(t.key,t.data,t.cursor,t.epoch,t.identity);break}case"subscription-error":{if(this.leader||t.tabId===this.tabId)break;this.onSubscriptionError?.(t.key,t.error,t.identity);break}case"subscription-settled":{if(this.leader||t.tabId===this.tabId)break;this.onSubscriptionSettled?.(t.key,t.cursor,t.epoch,t.lastMutationId,t.clientId,t.identity);break}case"yield-leadership":{this.knownLeader===t.tabId&&(this.knownLeader=void 0,this.running&&!this.leader&&this.claimAndPromote());break}}}handleClaimLeadership(t){if(t.tabId!==this.tabId){if(this.leader){this.broadcast({type:"heartbeat",tabId:this.tabId,ts:Date.now()}),this.onLeaderClaimAnswered?.();return}this.knownLeader===void 0&&t.tabId<this.tabId&&(this.knownLeader=t.tabId,this.lastHeartbeat=t.ts)}}handleHeartbeat(t){if(t.tabId!==this.tabId){if(this.leader){this.resolveLeaderVsLeaderTieBreak(t);return}this.lastHeartbeat=t.ts,this.knownLeader=t.tabId}}resolveLeaderVsLeaderTieBreak(t){t.tabId<this.tabId?(this.leader=!1,this.knownLeader=t.tabId,this.lastHeartbeat=t.ts,this.onStopBeingLeader?.()):this.broadcast({type:"heartbeat",tabId:this.tabId,ts:Date.now()})}becomeLeader(){this.leader||(this.leader=!0,this.knownLeader=this.tabId,this.lastHeartbeat=Date.now(),this.onBecomeLeader?.(),this.broadcast({type:"heartbeat",tabId:this.tabId,ts:Date.now()}))}sendHeartbeat(){this.leader&&this.broadcast({type:"heartbeat",tabId:this.tabId,ts:Date.now()})}checkLeaderHealth(){if(!this.running||this.leader)return;if(this.knownLeader===void 0){this.claimAndPromote();return}Date.now()-this.lastHeartbeat>this.leaderTimeout&&(this.knownLeader=void 0,this.claimAndPromote())}claimAndPromote(){this.promotionPending||(this.promotionPending=!0,this.broadcast({type:"claim-leadership",tabId:this.tabId,ts:Date.now()}),setTimeout(()=>{this.promotionPending=!1,this.running&&this.knownLeader===void 0&&this.becomeLeader()},this.leaderTimeout))}}export{l as TabCoordinator};
@@ -0,0 +1 @@
1
+ const _=3e4,E=1e4,T=3e4,A=5e3,s=3,S=250,M=5e3,L=5e3,I=64,R=3,C=1e4,U=5,a=8;export{_ as D,L as I,I as M,S as Q,R,M as S,E as a,T as b,A as c,s as d,C as e,U as f,a as g};
@@ -0,0 +1 @@
1
+ import{c as i}from"./live-stream-Dil9byXF.mjs";import"./client-tuning-C10B2KCO.mjs";import"./createReconnect-CjTmjJDH.mjs";import"./stream-idle-C7CsIT_m.mjs";export{i as connectLiveStream};
@@ -1 +1 @@
1
- import{assertMaxEntries as y}from"./createInMemoryQueryCache-D2uNHLfM.mjs";import{s as u}from"./single-blob-store-DpqSxsgi.mjs";const w="lunora:query-cache",d=50,E=s=>{const n=y(s.maxEntries??d,"createAsyncStorageQueryCache"),r=u(s.storage,s.key??w),c=async()=>{const e=await r.read();return typeof e=="object"&&e!==null&&!Array.isArray(e)?new Map(Object.entries(e)):new Map},o=e=>r.write(Object.fromEntries(e)),i=e=>{if(e.size<=n)return;const a=[...e.values()].toSorted((t,l)=>t.ts-l.ts);for(const t of a){if(e.size<=n)break;e.delete(t.key)}};return{clear:r.clear,load:()=>r.serialize(async()=>[...(await c()).values()]),put:(e,a)=>r.serialize(async()=>{const t=await c();t.set(e,{...a,key:e}),i(t),await o(t)}),remove:e=>r.serialize(async()=>{const a=await c();a.delete(e)&&await o(a)})}};export{E as createAsyncStorageQueryCache};
1
+ import{assertMaxEntries as y}from"./createInMemoryQueryCache-B5wX-gTy.mjs";import{s as u}from"./single-blob-store-DpqSxsgi.mjs";const w="lunora:query-cache",d=50,E=s=>{const n=y(s.maxEntries??d,"createAsyncStorageQueryCache"),r=u(s.storage,s.key??w),c=async()=>{const e=await r.read();return typeof e=="object"&&e!==null&&!Array.isArray(e)?new Map(Object.entries(e)):new Map},o=e=>r.write(Object.fromEntries(e)),i=e=>{if(e.size<=n)return;const a=[...e.values()].toSorted((t,l)=>t.ts-l.ts);for(const t of a){if(e.size<=n)break;e.delete(t.key)}};return{clear:r.clear,load:()=>r.serialize(async()=>[...(await c()).values()]),put:(e,a)=>r.serialize(async()=>{const t=await c();t.set(e,{...a,key:e}),i(t),await o(t)}),remove:e=>r.serialize(async()=>{const a=await c();a.delete(e)&&await o(a)})}};export{E as createAsyncStorageQueryCache};
@@ -1 +1 @@
1
- import{LunoraError as u}from"@lunora/errors";import{p as s,c as f,a as y}from"./idb-utility-C8WS390w.mjs";const x=()=>{const r=new Map,t=e=>({...e,args:{...e.args}});return{append:e=>(r.set(e.id,t(e)),Promise.resolve()),clear:()=>(r.clear(),Promise.resolve()),load:()=>Promise.resolve([...r.values()].map(e=>t(e))),remove:e=>(r.delete(e),Promise.resolve()),replace:e=>(r.has(e.id)&&r.set(e.id,t(e)),Promise.resolve())}},m="lunora-outbox",p="offline-mutations",d="by_id",w=(r={})=>{const t=r.indexedDB??(typeof indexedDB>"u"?void 0:indexedDB);if(!t)throw new u("INTERNAL","createIndexedDbPersistence: no IndexedDB available — pass `indexedDB` or use createInMemoryPersistence()");const e=r.databaseName??m,c=r.storeName??p,l=y(t,e,1,a=>{a.objectStoreNames.contains(c)||a.createObjectStore(c,{autoIncrement:!0}).createIndex(d,"id",{unique:!0})}),o=f(l,c);return{append:async a=>{await o("readwrite",n=>s(n.add(a)))},clear:async()=>{await o("readwrite",a=>s(a.clear()))},load:async()=>o("readonly",a=>s(a.getAll())),remove:async a=>{await o("readwrite",async n=>{const i=await s(n.index(d).getKey(a));i!==void 0&&await s(n.delete(i))})},replace:async a=>{await o("readwrite",async n=>{const i=await s(n.index(d).getKey(a.id));i!==void 0&&await s(n.put(a,i))})}}},b=(r,t=!0)=>{if(r!==!1){if(r)return r;if(!(!t||typeof indexedDB>"u"))return w()}};export{x as createInMemoryPersistence,w as createIndexedDbPersistence,b as resolvePersistenceAdapter};
1
+ import{LunoraError as u}from"@lunora/errors";import{p as s,c as f,a as y}from"./idb-utility-BS7xvou1.mjs";const x=()=>{const r=new Map,t=e=>({...e,args:{...e.args}});return{append:e=>(r.set(e.id,t(e)),Promise.resolve()),clear:()=>(r.clear(),Promise.resolve()),load:()=>Promise.resolve([...r.values()].map(e=>t(e))),remove:e=>(r.delete(e),Promise.resolve()),replace:e=>(r.has(e.id)&&r.set(e.id,t(e)),Promise.resolve())}},m="lunora-outbox",p="offline-mutations",d="by_id",w=(r={})=>{const t=r.indexedDB??(typeof indexedDB>"u"?void 0:indexedDB);if(!t)throw new u("INTERNAL","createIndexedDbPersistence: no IndexedDB available — pass `indexedDB` or use createInMemoryPersistence()");const e=r.databaseName??m,c=r.storeName??p,l=y(t,e,1,a=>{a.objectStoreNames.contains(c)||a.createObjectStore(c,{autoIncrement:!0}).createIndex(d,"id",{unique:!0})}),o=f(l,c);return{append:async a=>{await o("readwrite",n=>s(n.add(a)))},clear:async()=>{await o("readwrite",a=>s(a.clear()))},load:async()=>o("readonly",a=>s(a.getAll())),remove:async a=>{await o("readwrite",async n=>{const i=await s(n.index(d).getKey(a));i!==void 0&&await s(n.delete(i))})},replace:async a=>{await o("readwrite",async n=>{const i=await s(n.index(d).getKey(a.id));i!==void 0&&await s(n.put(a,i))})}}},b=(r,t=!0)=>{if(r!==!1){if(r)return r;if(!(!t||typeof indexedDB>"u"))return w()}};export{x as createInMemoryPersistence,w as createIndexedDbPersistence,b as resolvePersistenceAdapter};
@@ -1 +1 @@
1
- import{LunoraError as v}from"@lunora/errors";import{p as u,c as h,a as I}from"./idb-utility-C8WS390w.mjs";const T=(e,o,a)=>`${e}::${o}::${a??""}`,w=500,E=(e,o)=>{if(!Number.isInteger(e)||e<1)throw new v("INTERNAL",`${o}: maxEntries must be a positive integer, got ${String(e)}`);return e},B=(e={})=>{const o=E(e.maxEntries??w,"createInMemoryQueryCache"),a=new Map,s=r=>({...r,value:structuredClone(r.value)}),l=()=>{if(a.size<=o)return;const r=[...a.values()].toSorted((n,y)=>n.ts-y.ts);for(const n of r){if(a.size<=o)break;a.delete(n.key)}};return{clear:()=>(a.clear(),Promise.resolve()),load:()=>Promise.resolve([...a.values()].map(r=>s(r))),put:(r,n)=>(a.set(r,s({...n,key:r})),l(),Promise.resolve()),remove:r=>(a.delete(r),Promise.resolve())}},A="lunora-query-cache",b="query-cache",f="by_ts",p=1,N=(e={})=>{const o=e.indexedDB??(typeof indexedDB>"u"?void 0:indexedDB);if(!o)throw new v("INTERNAL","createIndexedDbQueryCache: no IndexedDB available — pass `indexedDB` or use createInMemoryQueryCache()");const a=e.databaseName??A,s=e.storeName??b,l=E(e.maxEntries??w,"createIndexedDbQueryCache"),r=I(o,a,p,t=>{t.objectStoreNames.contains(s)||t.createObjectStore(s,{keyPath:"key"}).createIndex(f,"ts",{unique:!1})}),n=h(r,s),y=async t=>{let c=await u(t.count())-l;c<=0||await new Promise((D,x)=>{const d=t.index(f).openCursor();d.addEventListener("success",()=>{const m=d.result;if(!m||c<=0){D();return}m.delete(),c-=1,m.continue()}),d.addEventListener("error",()=>{x(d.error??new Error("IndexedDB eviction failed"))})})};return{clear:async()=>{await n("readwrite",t=>u(t.clear()))},load:async()=>n("readonly",t=>u(t.getAll())),put:async(t,i)=>{await n("readwrite",async c=>{await u(c.put({...i,key:t})),await y(c)})},remove:async t=>{await n("readwrite",i=>u(i.delete(t)))}}},L=e=>{if(e!==!1){if(e)return e;if(!(typeof indexedDB>"u"))return N()}};export{E as assertMaxEntries,B as createInMemoryQueryCache,N as createIndexedDbQueryCache,T as queryCacheKey,L as resolveQueryCacheAdapter};
1
+ import{LunoraError as v}from"@lunora/errors";import{p as u,c as h,a as I}from"./idb-utility-BS7xvou1.mjs";const T=(e,o,a)=>`${e}::${o}::${a??""}`,w=500,E=(e,o)=>{if(!Number.isInteger(e)||e<1)throw new v("INTERNAL",`${o}: maxEntries must be a positive integer, got ${String(e)}`);return e},B=(e={})=>{const o=E(e.maxEntries??w,"createInMemoryQueryCache"),a=new Map,s=r=>({...r,value:structuredClone(r.value)}),l=()=>{if(a.size<=o)return;const r=[...a.values()].toSorted((n,y)=>n.ts-y.ts);for(const n of r){if(a.size<=o)break;a.delete(n.key)}};return{clear:()=>(a.clear(),Promise.resolve()),load:()=>Promise.resolve([...a.values()].map(r=>s(r))),put:(r,n)=>(a.set(r,s({...n,key:r})),l(),Promise.resolve()),remove:r=>(a.delete(r),Promise.resolve())}},A="lunora-query-cache",b="query-cache",f="by_ts",p=1,N=(e={})=>{const o=e.indexedDB??(typeof indexedDB>"u"?void 0:indexedDB);if(!o)throw new v("INTERNAL","createIndexedDbQueryCache: no IndexedDB available — pass `indexedDB` or use createInMemoryQueryCache()");const a=e.databaseName??A,s=e.storeName??b,l=E(e.maxEntries??w,"createIndexedDbQueryCache"),r=I(o,a,p,t=>{t.objectStoreNames.contains(s)||t.createObjectStore(s,{keyPath:"key"}).createIndex(f,"ts",{unique:!1})}),n=h(r,s),y=async t=>{let c=await u(t.count())-l;c<=0||await new Promise((D,x)=>{const d=t.index(f).openCursor();d.addEventListener("success",()=>{const m=d.result;if(!m||c<=0){D();return}m.delete(),c-=1,m.continue()}),d.addEventListener("error",()=>{x(d.error??new Error("IndexedDB eviction failed"))})})};return{clear:async()=>{await n("readwrite",t=>u(t.clear()))},load:async()=>n("readonly",t=>u(t.getAll())),put:async(t,i)=>{await n("readwrite",async c=>{await u(c.put({...i,key:t})),await y(c)})},remove:async t=>{await n("readwrite",i=>u(i.delete(t)))}}},L=e=>{if(e!==!1){if(e)return e;if(!(typeof indexedDB>"u"))return N()}};export{E as assertMaxEntries,B as createInMemoryQueryCache,N as createIndexedDbQueryCache,T as queryCacheKey,L as resolveQueryCacheAdapter};
@@ -1 +1 @@
1
- import{LunoraClient as r}from"./LunoraClient-D6MyuSWy.mjs";const c=e=>{const t=new r({fetch:e.fetch,url:e.url});return e.token!==void 0&&t.setAuthToken(e.token),t};export{c as createServerClient};
1
+ import{LunoraClient as r}from"./LunoraClient-CN4shVl4.mjs";const c=e=>{const t=new r({fetch:e.fetch,url:e.url});return e.token!==void 0&&t.setAuthToken(e.token),t};export{c as createServerClient};
@@ -0,0 +1,9 @@
1
+ import{LunoraError as u}from"@lunora/errors";import{decodeWire as L}from"@lunora/wire";import{createReconnect as I}from"./createReconnect-CjTmjJDH.mjs";import{createStream as C}from"./DEFAULT_MAX_BUFFER-Bd-_iyiV.mjs";import{r as H}from"./stream-idle-C7CsIT_m.mjs";const y=/^ /u,$=/^\d+$/u,x=2147483647,B=10,k=1e3,_=t=>typeof t=="object"&&t!==null?JSON.stringify(t):String(t),F=t=>{let n="",e,a;const s=[];for(const r of t.split(`
2
+ `))if(r.startsWith("event:"))n=r.slice(6).replace(y,"");else if(r.startsWith("data:"))s.push(r.slice(5).replace(y,""));else if(r.startsWith("id:")){const c=r.slice(3).replace(y,"");c.includes("\0")||(e=c)}else if(r.startsWith("retry:")){const c=r.slice(6).replace(y,"");$.test(c)&&Number.isSafeInteger(Number(c))&&(a=Math.min(Number(c),x))}return{data:s.join(`
3
+ `),event:n,id:e,retry:a}},W=(t,n,e)=>{const a=n.params??{},s=t.path.split("/").map(o=>{if(!o.startsWith(":"))return o;const f=o.slice(1),p=a[f];if(p===void 0)throw new u("HTTP_STREAM_MISSING_PARAM",`httpStream: missing path param ":${f}" for route ${t.path}`);return encodeURIComponent(_(p))}).join("/"),r=new URLSearchParams;for(const[o,f]of Object.entries(n.searchParams??{}))f!==void 0&&r.set(o,_(f));const c=r.toString();return`${e.endsWith("/")?e.slice(0,-1):e}${s}${c===""?"":`?${c}`}`},j=(t,n)=>{if(t.event==="complete")return n.complete(),!0;if(t.event==="error"){let e={};try{e=JSON.parse(t.data)}catch{}const a=typeof e.message=="string"?e.message:"stream error",s=typeof e.code=="string"?e.code:"HTTP_STREAM_ERROR",r=new u(s,a);return typeof e.why=="string"&&(r.why=e.why),n.fail(r),!0}if((t.event===""||t.event==="message")&&t.data!=="")try{n.push(L(JSON.parse(t.data)))}catch{return n.fail(new u("HTTP_STREAM_BAD_CHUNK","httpStream: malformed SSE chunk (invalid JSON)")),!0}return!1},D=async(t,n,e,a,s=0)=>{const r=t.getReader(),c=new TextDecoder;let m="",o=!1;const f=d=>{let l=o?`\r${d}`:d;o=l.endsWith("\r"),o&&(l=l.slice(0,-1)),m+=l.replaceAll(`\r
4
+ `,`
5
+ `)},p=()=>{let d=m.indexOf(`
6
+
7
+ `);for(;d!==-1;){const l=F(m.slice(0,d));if(m=m.slice(d+2),e.onFrame?.(),l.id!==void 0&&e.onId?.(l.id),l.retry!==void 0&&e.onRetry?.(l.retry),j(l,n))return!0;d=m.indexOf(`
8
+
9
+ `)}return!1},T=()=>r.cancel().then(()=>{},()=>{});try{for(;;){const{done:d,value:l}=await H(r,s);if(d)break;if(f(c.decode(l,{stream:!0})),p())return await T(),!0}return f(c.decode()),p()}catch(d){throw a.abort(),T().catch(()=>{}),d}},U=async(t,n)=>{n.aborted||await new Promise(e=>{let a;const s=()=>{clearTimeout(a),e()};a=setTimeout(()=>{n.removeEventListener("abort",s),e()},t),n.addEventListener("abort",s,{once:!0})})},J=(t,n)=>{if(typeof t!="object"||t===null)return;const{code:e,error:a,why:s}=t;if(!(typeof e!="string"||typeof a!="string"))return new u("HTTP_STREAM_STATUS",`httpStream: request failed (status ${n.toString()}, ${e}): ${a}`,{status:n,why:typeof s=="string"?s:void 0})},Y=async(t,n)=>{let e;const a=new Promise(r=>{e=setTimeout(()=>{r(void 0)},k)}),s=t.json().catch(r=>{if(n.aborted)throw r});try{return await Promise.race([s,a])}finally{clearTimeout(e),await t.body?.cancel().catch(()=>{})}},q=(t,n)=>{const e=Object.entries(t??{}).filter(([a])=>n===void 0||a.toLowerCase()!=="last-event-id");return{accept:"text/event-stream",...Object.fromEntries(e),...n===void 0?{}:{"last-event-id":n}}},A=t=>t instanceof Error&&"code"in t?t:new u("HTTP_STREAM_TRANSPORT",t instanceof Error?t.message:String(t),{cause:t}),V=(t,n,e={})=>{const a=e.fetch??(typeof fetch=="function"?fetch.bind(globalThis):void 0);if(!a)throw new u("INTERNAL","httpStream: no `fetch` implementation available");const s=W(t,n??{},e.baseUrl??""),r=new AbortController;let c;e.signal&&(e.signal.aborted?r.abort():(c=()=>{r.abort()},e.signal.addEventListener("abort",c,{once:!0})));const m=()=>{c&&e.signal?.removeEventListener("abort",c)},{handle:o,iterable:f}=C({maxBuffer:e.maxBuffer,onCancel:()=>{r.abort()},onFail:()=>{r.abort()}}),p=e.reconnect??!1,T=I();let{lastEventId:d}=e,l,v=0;const M=()=>{T.reset(),v=0},E={},R=i=>{try{i()}catch(S){throw E.value={error:S},S}},P={onFrame:M,onId:i=>{R(()=>e.onEventId?.(i)),d=i},onRetry:i=>{R(()=>e.onRetry?.(i)),l=i}},O=async()=>{const i=q(e.headers,d),S=new u("HTTP_STREAM_INTERRUPTED","httpStream: stream ended without a complete frame"),b=new AbortController,g=()=>{b.abort()};r.signal.addEventListener("abort",g,{once:!0});try{const h=await a(s,{headers:i,method:t.method,signal:b.signal});if(!h.ok){const N=await Y(h,r.signal);o.fail(J(N,h.status)??new u("HTTP_STREAM_STATUS",`httpStream: request failed (status ${h.status.toString()})`,{status:h.status}));return}if(!h.body){o.fail(new u("HTTP_STREAM_NO_BODY","httpStream: response has no body"));return}return{completed:await D(h.body,o,P,b,e.idleTimeoutMs),error:S}}catch(h){if(r.signal.aborted){o.complete();return}if(E.value){const{error:w}=E.value;o.fail(w instanceof Error?w:new u("HTTP_STREAM_CALLBACK","httpStream: an onEventId/onRetry callback threw"));return}return{completed:!1,error:A(h)}}finally{r.signal.removeEventListener("abort",g)}};return(async()=>{try{for(;;){if(r.signal.aborted){o.complete();return}const i=await O();if(!i)return;if(i.completed||!p||v>=B){i.completed||o.fail(i.error);return}v+=1,await U(l??T.next(),r.signal)}}finally{m()}})().catch(i=>{if(r.signal.aborted){o.complete();return}o.fail(A(i))}),f};export{V as httpStream};
@@ -0,0 +1 @@
1
+ const c=t=>new Promise((n,s)=>{t.addEventListener("success",()=>{n(t.result)}),t.addEventListener("error",()=>{s(t.error??new Error("IndexedDB request failed"))})}),E=(t,n,s,o)=>{let a;return()=>{if(a)return a;const e=new Promise((d,i)=>{const r=t.open(n,s);r.addEventListener("upgradeneeded",()=>{o(r.result)}),r.addEventListener("success",()=>{d(r.result)}),r.addEventListener("error",()=>{i(r.error??new Error("IndexedDB open failed"))})});return a=e,e.catch(()=>{a===e&&(a=void 0)}),e}},u=(t,n)=>async(s,o)=>{const e=(await t()).transaction(n,s),d=await o(e.objectStore(n));return await new Promise((i,r)=>{e.addEventListener("complete",()=>{i()}),e.addEventListener("error",()=>{r(e.error??new Error("IndexedDB transaction failed"))}),e.addEventListener("abort",()=>{r(e.error??new Error("IndexedDB transaction aborted"))})}),d};export{E as a,u as c,c as p};
@@ -0,0 +1,10 @@
1
+ import{decodeWire as K}from"@lunora/wire";import{S as q}from"./client-tuning-C10B2KCO.mjs";import{createReconnect as F}from"./createReconnect-CjTmjJDH.mjs";import{r as z}from"./stream-idle-C7CsIT_m.mjs";const Q="x-lunora-live-connection",X="live",ie=c=>`/live/${c}/connect`,D=c=>c===void 0?void 0:(...s)=>{c(...s)},Y=/^ /u,Z=1e4,ee=c=>{const s=c.indexOf("?"),f=s===-1?c:c.slice(0,s);if(!f.endsWith("/connect"))throw new Error(`connectLiveStream: expected a connect URL ending in "/connect", got "${c}"`);return`${f.slice(0,-8)}/control${s===-1?"":c.slice(s)}`},te=c=>{let s="message";const f=[];for(const m of c.split(`
2
+ `))m.startsWith("event:")?s=m.slice(6).trim():m.startsWith("data:")&&f.push(m.slice(5).replace(Y,""));return f.length===0?void 0:{data:f.join(`
3
+ `),event:s}},ae=(c,s={})=>{const f=s.fetch??fetch,m=ee(c),k=s.maxDelayMs??3e4,U=s.idleTimeoutMs??75e3,O=F({initialDelayMs:s.retryDelay??1e3,maxDelayMs:k}),M=s.withCredentials===!0?"include":"same-origin",_=new URL(c,"https://lunora.invalid").searchParams.get("shard")??void 0,l=new Map,y=new Set;let h,v,I,C=!1,N=0;const W=()=>{let e;do N+=1,e=`live-${String(N)}`;while(l.has(e));return e},L=()=>C;let g,E;const P=new Promise((e,t)=>{g=e,E=t});P.catch(()=>{});const R={...s.headers},S=new Map,T=new Set,j=async e=>{const t=new AbortController,r=setTimeout(()=>{t.abort()},Z);T.add(t);try{const n=await f(m,{body:JSON.stringify(_===void 0?e:{...e,shardKey:_}),credentials:M,headers:{"content-type":"application/json",...R},method:"POST",signal:t.signal});return await n.text(),n.status}catch{return}finally{clearTimeout(r),T.delete(t)}},A=(e,t)=>{const n=(S.get(e)??Promise.resolve()).then(()=>L()?void 0:j(t)),o=n.then(()=>{},()=>{});return S.set(e,o),o.then(()=>{S.get(e)===o&&S.delete(e)}).catch(()=>{}),n},V=(e,t)=>{for(const r of l.values())if(r.topic===e)for(const n of r.handlers)try{n(t)}catch{}for(const r of y)try{r({payload:t,topic:e})}catch{}},B=e=>{const t=te(e);if(t?.event===X)try{const r=JSON.parse(t.data);V(r.topic,K(r.data))}catch{}},$=(e,t,r)=>{if(l.get(e)===t){l.delete(e);for(const n of t.errors)try{n({status:r,subscriptionId:e,topic:t.topic})}catch{}}},H=async()=>{const e=await Promise.all([...l].map(async([t,r])=>{const n=await A(t,{action:"subscribe",connectionId:h,subscriptionId:t,topic:r.topic});return{entry:r,id:t,status:n}}));for(const{entry:t,id:r,status:n}of e){if(n===void 0||n>=500||n===404||n===401){v?.abort();return}n>=400&&$(r,t,n)}},G=async e=>{h=e.headers.get(Q)??void 0,I=setTimeout(()=>{O.reset()},q);const r=(async()=>{try{await H()}catch{v?.abort();return}g?.(),g=void 0,E=void 0})(),n=e.body?.getReader();if(n===void 0){await r;return}const o=new TextDecoder;let a="",i=!1;for(;;){let u;try{u=await z(n,U)}catch(J){throw n.cancel().catch(()=>{}),v?.abort(),J}const{done:w,value:x}=u;if(w)break;const b=o.decode(x,{stream:!0}),p=i?`\r${b}`:b;i=p.endsWith("\r"),a+=(i?p.slice(0,-1):p).replaceAll(`\r
4
+ `,`
5
+ `).replaceAll("\r",`
6
+ `);let d=a.indexOf(`
7
+
8
+ `);for(;d!==-1;)B(a.slice(0,d)),a=a.slice(d+2),d=a.indexOf(`
9
+
10
+ `)}await r};return(async()=>{for(;!L();){v=new AbortController;try{const t=await f(c,{credentials:M,headers:{accept:"text/event-stream",...R},signal:v.signal});if(!t.ok)throw new Error(`live stream connect failed: HTTP ${String(t.status)}`);await G(t)}catch{}if(clearTimeout(I),h=void 0,L())return;const e=O.next();await new Promise(t=>{setTimeout(t,e)})}})().catch(()=>{}),{close:()=>{C=!0,v?.abort();for(const e of T)e.abort();T.clear(),l.clear(),y.clear(),E?.(new Error("connectLiveStream: closed before the first connection")),g=void 0,E=void 0},get connectionId(){return h},onEvent:e=>(y.add(e),()=>y.delete(e)),ready:P,subscribe:({id:e,onError:t,onEvent:r,topic:n})=>{if(C)throw new Error("connectLiveStream: subscribe on a closed stream");const o=e??W(),a=l.get(o);if(a!==void 0&&a.topic!==n)throw new Error(`connectLiveStream: subscription id "${o}" already follows "${a.topic}"; unsubscribe it first`);const i=a??{errors:new Set,handlers:new Set,refs:0,topic:n},u=D(r),w=D(t);if(i.refs+=1,u!==void 0&&i.handlers.add(u),w!==void 0&&i.errors.add(w),a===void 0&&(l.set(o,i),h!==void 0)){const b=h,p=v;A(o,{action:"subscribe",connectionId:b,subscriptionId:o,topic:n}).then(d=>{if(d===void 0||d===401||d===404||d>=500){b===h&&l.get(o)===i&&p?.abort();return}d>=400&&$(o,i,d)}).catch(()=>{})}let x=!1;return()=>{x||(x=!0,u!==void 0&&i.handlers.delete(u),w!==void 0&&i.errors.delete(w),i.refs-=1,!(i.refs>0||l.get(o)!==i)&&(l.delete(o),A(o,{action:"unsubscribe",connectionId:h,subscriptionId:o,topic:n}).catch(()=>{})))}}}};export{ae as c,ie as l};
@@ -1 +1 @@
1
- import{c as u,l as d}from"./live-stream-ct8j7T75.mjs";import{j as y,w as f}from"./client-urls-BHLUjjcG.mjs";const o=new Map,v=e=>JSON.stringify([e.url,e.sessionKey,e.withCredentials===!0,e.retryDelay,e.maxDelayMs,Object.entries(e.headers??{}).toSorted(([t],[r])=>t.localeCompare(r))]),T=e=>{const t=v(e);let r=o.get(t);if(r===void 0){const{headers:i,maxDelayMs:l,retryDelay:n,withCredentials:m}=e;r={closeTimer:void 0,refs:0,stream:u(e.url,{headers:i,maxDelayMs:l,retryDelay:n,withCredentials:m})},o.set(t,r)}clearTimeout(r.closeTimer),r.closeTimer=void 0,r.refs+=1;const s=r,a=s.stream.subscribe({id:e.topic,onError:e.onError,onEvent:e.onEvent,topic:e.topic});let c=!1;return()=>{c||(c=!0,a(),s.refs-=1,s.refs===0&&(s.closeTimer=setTimeout(()=>{o.delete(t),s.stream.close()},0)))}},w=(e,t,r={})=>{const{onEvent:s,shardKey:a,...c}=r,{__lunoraLive:i,topic:l}=t;return{...c,onEvent:s,topic:l,url:y(e.url,f(d(i),{shard:a}))}};export{w as liveTopicSubscription,T as subscribeLiveTopic};
1
+ import{c as u,l as d}from"./live-stream-Dil9byXF.mjs";import{j as y,w as f}from"./client-urls-BHLUjjcG.mjs";const o=new Map,v=e=>JSON.stringify([e.url,e.sessionKey,e.withCredentials===!0,e.retryDelay,e.maxDelayMs,Object.entries(e.headers??{}).toSorted(([t],[r])=>t.localeCompare(r))]),T=e=>{const t=v(e);let r=o.get(t);if(r===void 0){const{headers:i,maxDelayMs:l,retryDelay:n,withCredentials:m}=e;r={closeTimer:void 0,refs:0,stream:u(e.url,{headers:i,maxDelayMs:l,retryDelay:n,withCredentials:m})},o.set(t,r)}clearTimeout(r.closeTimer),r.closeTimer=void 0,r.refs+=1;const s=r,a=s.stream.subscribe({id:e.topic,onError:e.onError,onEvent:e.onEvent,topic:e.topic});let c=!1;return()=>{c||(c=!0,a(),s.refs-=1,s.refs===0&&(s.closeTimer=setTimeout(()=>{o.delete(t),s.stream.close()},0)))}},w=(e,t,r={})=>{const{onEvent:s,shardKey:a,...c}=r,{__lunoraLive:i,topic:l}=t;return{...c,onEvent:s,topic:l,url:y(e.url,f(d(i),{shard:a}))}};export{w as liveTopicSubscription,T as subscribeLiveTopic};
@@ -530,6 +530,15 @@ interface LunoraClientOptions {
530
530
  */
531
531
  queryCache?: QueryCacheAdapter | false;
532
532
  reconnect?: ReconnectOptions;
533
+ /**
534
+ * Deadline (ms) for one HTTP RPC round trip (`query`, `mutation`, a replayed
535
+ * write), covering the response body as well as the connection. A request
536
+ * that stalls past it fails with a retryable `TransportError` — and a durable
537
+ * write is re-queued under the same `mutationId` — instead of freezing every
538
+ * write queued behind it on the shard. `action` is exempt: it may legitimately
539
+ * run for longer. Defaults to 30000 (30s); set to `0` (or negative) to disable.
540
+ */
541
+ rpcTimeoutMs?: number;
533
542
  url: string;
534
543
  WebSocket?: typeof WebSocket;
535
544
  /**
@@ -1355,6 +1364,14 @@ interface SubscriptionState {
1355
1364
  */
1356
1365
  serverEpoch?: string;
1357
1366
  readonly shardKey?: string;
1367
+ /**
1368
+ * Whether a poll or the leader tab has delivered a value for this subscription on the
1369
+ * CURRENT connection. A polling connection and a follower tab see no server `ack`, so this
1370
+ * is their only evidence of an answer, and `serverBase` cannot stand in for it: a hydrated
1371
+ * cache sets that before anything has arrived. A WebSocket never sets it, `acked` is its
1372
+ * evidence.
1373
+ */
1374
+ valueReceived: boolean;
1358
1375
  /**
1359
1376
  * The wire-encoded form of `args`, computed once at `subscribe` time (so an
1360
1377
  * unsupported value fails loud at the call site, not inside a reconnect's
@@ -1619,6 +1636,22 @@ interface ClientDebugSnapshot {
1619
1636
  shards: ClientDebugShard[];
1620
1637
  subscriptions: ClientDebugSubscription[];
1621
1638
  }
1639
+ /**
1640
+ * Whether the client is actually caught up, which `connectionStatus()` alone cannot
1641
+ * say: it reads `"connected"` as soon as any socket is open, though a subscription
1642
+ * on it may not have been acknowledged yet (it has no data) and writes may still be
1643
+ * waiting in the offline queue. See {@link LunoraClient.syncStatus}.
1644
+ */
1645
+ interface SyncStatus {
1646
+ /** The aggregate socket status, as {@link LunoraClient.connectionStatus} reports it. */
1647
+ connection: ConnectionStatus;
1648
+ /** Live queries and shapes the server has not answered yet on the current connection. */
1649
+ pendingSubscriptions: number;
1650
+ /** Writes queued or being replayed from the built-in offline queue (not the `@lunora/db` outbox). */
1651
+ pendingWrites: number;
1652
+ /** `true` only on a live connection with every subscription answered and no write queued. */
1653
+ synced: boolean;
1654
+ }
1622
1655
  /**
1623
1656
  * Terminal verdict for a mutation that passed through the offline queue,
1624
1657
  * delivered to {@link LunoraClient.onMutationSettled}.
@@ -1670,6 +1703,13 @@ interface MutationSettledEvent {
1670
1703
  interface ActionCallOptions {
1671
1704
  /** Route the call to a specific shard. */
1672
1705
  shardKey?: string;
1706
+ /**
1707
+ * Abort the call. The request is cancelled and the promise rejects with the
1708
+ * signal's reason (an `AbortError` by default). The server may already have
1709
+ * started — or finished — the action, which is not transactional, so an abort
1710
+ * releases the caller; it does not undo the work.
1711
+ */
1712
+ signal?: AbortSignal;
1673
1713
  }
1674
1714
  /**
1675
1715
  * Per-call options for {@link LunoraClient.mutation} — the optimistic-update
@@ -1753,6 +1793,14 @@ interface MutationCallOptions<TCurrent = unknown, TValue = unknown, TArgs = unkn
1753
1793
  */
1754
1794
  replayCredential?: ReplayCredential;
1755
1795
  shardKey?: string;
1796
+ /**
1797
+ * Stop waiting for the mutation. The promise rejects with the signal's reason
1798
+ * (an `AbortError` by default), and nothing is sent when the signal is already
1799
+ * aborted. A write already on the wire or in the offline queue is NOT
1800
+ * retracted: it still commits or replays on its own, and its optimistic patch
1801
+ * is settled by that outcome — an abort releases the caller, not the write.
1802
+ */
1803
+ signal?: AbortSignal;
1756
1804
  }
1757
1805
  declare const replayCredentialBrand: unique symbol;
1758
1806
  /**
@@ -1811,6 +1859,8 @@ declare class LunoraClient {
1811
1859
  private readonly connectTimeoutMs;
1812
1860
  /** Keepalive cadence (ms); `0` disables the heartbeat. See {@link LunoraClientOptions.heartbeatIntervalMs}. */
1813
1861
  private readonly heartbeatIntervalMs;
1862
+ /** Deadline (ms) for one HTTP RPC round trip; `0` disables it. See {@link LunoraClientOptions.rpcTimeoutMs}. */
1863
+ private readonly rpcTimeoutMs;
1814
1864
  /** The socket-lifecycle inputs, built once after the constructor assigns them (none is reassigned later). */
1815
1865
  private readonly managedSocketOptions;
1816
1866
  /** Failed-open run that engages the HTTP polling fallback; `0` disables it. */
@@ -1880,10 +1930,13 @@ declare class LunoraClient {
1880
1930
  private readonly clientWatermarks;
1881
1931
  /** Monotonic per-client mutation counter backing the server `__client_watermark`. */
1882
1932
  private outboxMutationCounter;
1883
- private readonly onPersistenceError;
1933
+ /** Orders offline-queued writes by when they were issued; see {@link QueuedMutation.issueSeq}. */
1934
+ private mutationIssueCounter;
1884
1935
  private readonly persistence;
1885
1936
  /** App/schema version stamped on persisted writes + cached reads; mismatches are purged. */
1886
1937
  private readonly persistenceVersion;
1938
+ /** Removes the page-hide flush listeners on close (see `installPageHideFlush`). */
1939
+ private releasePageHideFlush;
1887
1940
  /** Releases the multi-tab outbox-leader Web Lock on close (see `hydrateAsOutboxLeader`). */
1888
1941
  private outboxLeaderRelease;
1889
1942
  /** Durable read cache (Pillar 2); `undefined` when `queryCache` is omitted or `false`. */
@@ -2096,6 +2149,9 @@ declare class LunoraClient {
2096
2149
  private readonly mutationSettledListeners;
2097
2150
  /** Subscribers to the offline-queue pending-count (see `onPendingChange`). */
2098
2151
  private readonly pendingChangeListeners;
2152
+ private readonly syncStatusListeners;
2153
+ /** The last value `syncStatusListeners` was told, to emit only on a real change. */
2154
+ private lastSyncStatus;
2099
2155
  /**
2100
2156
  * Whisper-topic handlers, keyed by `connectionKey(shardKey)` → topic → set
2101
2157
  * of callbacks. Membership doubles as the resubscribe set replayed on every
@@ -2173,6 +2229,8 @@ declare class LunoraClient {
2173
2229
  * entry per document it ever wrote.
2174
2230
  */
2175
2231
  private readonly replayRetryState;
2232
+ /** Failed replay attempts collected so far, per ambiguous-send write id (see `spendsLastReplayAttempt`); cleared when the write settles. */
2233
+ private readonly replayAttempts;
2176
2234
  /** Pending rate-limit retry flushes, keyed by {@link connectionKey}, so `close()` can cancel them. */
2177
2235
  private readonly replayRetryTimers;
2178
2236
  private readonly shapeSubscriptions;
@@ -2634,6 +2692,29 @@ declare class LunoraClient {
2634
2692
  * unsubscribe function.
2635
2693
  */
2636
2694
  onConnectionStatus(listener: (status: ConnectionStatus) => void): Unsubscribe;
2695
+ /**
2696
+ * Whether the client is caught up: a live connection, every subscription
2697
+ * answered by the server, and no write waiting in the offline queue. Use this
2698
+ * rather than `connectionStatus() === "connected"` for a "synced" indicator —
2699
+ * that reads `"connected"` the moment a socket opens, while subscriptions still
2700
+ * have no data and queued writes are still waiting to go out.
2701
+ *
2702
+ * Over the HTTP polling fallback (judged per shard, so one polling shard does
2703
+ * not wait on an ack while another holds an open socket), and on a cross-tab
2704
+ * follower (neither sees server acknowledgements), a subscription counts as
2705
+ * answered once the current connection has delivered a value for it; a
2706
+ * hydrated cache does not.
2707
+ * A write counts as pending from the moment it is queued until the server
2708
+ * settles it, including while its replay request is on the wire.
2709
+ */
2710
+ syncStatus(): SyncStatus;
2711
+ /**
2712
+ * Subscribe to {@link syncStatus} changes. Invokes `listener` immediately with
2713
+ * the current value, then whenever the connection status, the number of
2714
+ * unanswered subscriptions or the number of unsettled writes (queued, or
2715
+ * being replayed) changes. Returns an unsubscribe function.
2716
+ */
2717
+ onSyncStatus(listener: (status: SyncStatus) => void): Unsubscribe;
2637
2718
  /**
2638
2719
  * Number of offline writes waiting in the built-in queue to be sent — the
2639
2720
  * depth for a "N changes waiting to sync" indicator. Counts writes that are
@@ -2743,7 +2824,9 @@ declare class LunoraClient {
2743
2824
  * (enabled) render of a live query behind hydration, so the user sees
2744
2825
  * cached data instead of an undefined flash before the socket round-trip.
2745
2826
  */
2746
- whenReady(): Promise<void>;
2827
+ whenReady(options?: {
2828
+ signal?: AbortSignal;
2829
+ }): Promise<void>;
2747
2830
  /**
2748
2831
  * Synchronously reports whether {@link whenReady} has already resolved (the
2749
2832
  * durable read cache is loaded, or none is configured). Framework adapters
@@ -2792,6 +2875,7 @@ declare class LunoraClient {
2792
2875
  };
2793
2876
  query<F extends FunctionReference>(function_: F, args: ArgsOf<F>, options?: {
2794
2877
  shardKey?: string;
2878
+ signal?: AbortSignal;
2795
2879
  }): Promise<ReturnOf<F>>;
2796
2880
  /**
2797
2881
  * Batch several independent calls into ONE round trip (plan 088). Each call is
@@ -2806,6 +2890,12 @@ declare class LunoraClient {
2806
2890
  * No promise pipelining and no capability passing — a call's args cannot
2807
2891
  * reference another call's result (see plan 088 §fence; capabilities are
2808
2892
  * incompatible with DO hibernation).
2893
+ *
2894
+ * The whole exchange, response body included, is bounded by
2895
+ * {@link LunoraClientOptions.rpcTimeoutMs}, rejecting with a retryable
2896
+ * {@link TransportError}. A function's kind is a type-level phantom with no
2897
+ * runtime marker, so unlike {@link action} a batched call cannot be exempted:
2898
+ * call a long-running action through `action()` instead.
2809
2899
  */
2810
2900
  batch(calls: ReadonlyArray<{
2811
2901
  args?: Record<string, unknown>;
@@ -3547,6 +3637,7 @@ declare class LunoraClient {
3547
3637
  onCheckpoint?: (watermark: SyncWatermark) => void;
3548
3638
  onError?: SubscriptionErrorCallback;
3549
3639
  shardKey?: string;
3640
+ signal?: AbortSignal;
3550
3641
  }): Unsubscribe;
3551
3642
  /**
3552
3643
  * Subscribe to a declarative **shape** — server-side partial replication
@@ -3722,7 +3813,7 @@ declare class LunoraClient {
3722
3813
  * stamp elsewhere in this file) — moved verbatim, not reflowed.
3723
3814
  */
3724
3815
  private createTabCoordinator;
3725
- /** The body of {@link batch}, optionally abortable (the polling fallback's timeout). */
3816
+ /** The body of {@link batch}, abortable by the caller's deadline (`batch`'s own, or the polling fallback's timeout). */
3726
3817
  private sendBatch;
3727
3818
  /**
3728
3819
  * The body of {@link mutation}, also saying whether the write committed. `committed` is
@@ -3893,6 +3984,14 @@ declare class LunoraClient {
3893
3984
  private persistQueryValue;
3894
3985
  /** Drain {@link pendingCacheWrites} to the durable store. */
3895
3986
  private flushQueryCacheWrites;
3987
+ /**
3988
+ * Flush what is buffered for durable storage when the page is hidden or going
3989
+ * away: writes whose append was rejected get another try, and the debounced
3990
+ * read-cache writes (which would otherwise wait out their timer on a page
3991
+ * that never gets to run it) are written out. Browser lifecycle events only —
3992
+ * a runtime without them (SSR, workers, React Native) installs nothing.
3993
+ */
3994
+ private installPageHideFlush;
3896
3995
  /** Derive the aggregate status from the per-shard socket states. */
3897
3996
  private computeStatus;
3898
3997
  /**
@@ -3957,6 +4056,8 @@ declare class LunoraClient {
3957
4056
  * (even a refusal) is the proof. `false` only when `fetch` itself rejects.
3958
4057
  */
3959
4058
  private probeOrigin;
4059
+ /** Recompute {@link syncStatus} and notify its listeners if any field changed. */
4060
+ private emitSyncStatus;
3960
4061
  /** Recompute the aggregate status and notify listeners if it changed. */
3961
4062
  private emitConnectionStatus;
3962
4063
  /**
@@ -4118,7 +4219,11 @@ declare class LunoraClient {
4118
4219
  */
4119
4220
  private cancelStream;
4120
4221
  private handleDisconnect;
4121
- /** Mark every subscription bound to `shardKey` as needing a fresh ack. */
4222
+ /**
4223
+ * Mark every subscription and shape bound to `shardKey` as needing a fresh answer from the server.
4224
+ * `socketOpened` also forgets the values a poll delivered: the socket replaces that connection,
4225
+ * whereas a socket that merely failed to open (or dropped) leaves polling, and its answers, in place.
4226
+ */
4122
4227
  private markShardPendingAck;
4123
4228
  /**
4124
4229
  * Re-send `state`'s `subscribe` frame, but only once fewer than
@@ -4135,13 +4240,16 @@ declare class LunoraClient {
4135
4240
  * Cannot wedge: a queued subscription whose frame does not actually go out
4136
4241
  * (socket closed, already acked, unsubscribed while queued) takes no slot,
4137
4242
  * every sent frame is released by the first server frame carrying its id
4138
- * (see {@link handleServerMessage}) or by its own watchdog, and a disconnect
4243
+ * (see {@link handleServerMessage}) or by its watchdog, and a disconnect
4139
4244
  * drops the queue wholesale — the next `open` re-queues every subscription
4140
4245
  * on the shard, because `markShardPendingAck` un-acked them all.
4141
4246
  */
4142
4247
  private drainResubscribeQueue;
4143
- /** Free the in-flight slot `id` holds on `conn` (if any) and let the next queued subscribe go out. */
4144
- private releaseResubscribeSlot;
4248
+ /**
4249
+ * Stand down `id`'s watchdog on `conn` — its `subscribe` frame was answered or the socket went
4250
+ * away — and, if it held a pacing slot, free it so the next queued subscribe goes out.
4251
+ */
4252
+ private settleSubscribeWatchdog;
4145
4253
  /**
4146
4254
  * Abandon this connection's paced resubscribe: clear every watchdog and
4147
4255
  * forget both the in-flight and the waiting entries. Called wherever the
@@ -4151,9 +4259,20 @@ declare class LunoraClient {
4151
4259
  private clearResubscribeQueue;
4152
4260
  /** Send `state`'s `subscribe` frame when the shard's socket can carry it; `true` when a frame actually went out. */
4153
4261
  private sendSubscribeIfOpen;
4262
+ /**
4263
+ * Watch `state`'s `subscribe` frame: if no frame bearing its id arrives within
4264
+ * {@link RESUBSCRIBE_ACK_TIMEOUT_MS}, free the pacing slot it holds (if any) and
4265
+ * re-send the frame once. A server that drops the frame without replying (a
4266
+ * shard still waking, a swallowed message) would otherwise leave the
4267
+ * subscription un-acked on a perfectly live socket until the next reconnect.
4268
+ * One retry, not a loop: a query that is merely slow must not be re-run repeatedly.
4269
+ */
4270
+ private armUnackedWatchdog;
4154
4271
  private sendShapeSubscribeIfOpen;
4155
4272
  private handleServerMessage;
4156
4273
  private handleErrorMessage;
4274
+ /** Count a query the server refused as answered, by the evidence {@link syncStatus} asks of it, emitting only on a change. */
4275
+ private markQueryRejected;
4157
4276
  private handlePokeEnd;
4158
4277
  /**
4159
4278
  * Commit one shape's slice of a poke. The decision and the state change live in
@@ -4198,8 +4317,10 @@ declare class LunoraClient {
4198
4317
  /**
4199
4318
  * Mark `state` acked and, when the frame carries a newer cursor/epoch than
4200
4319
  * the cached position, advance the resume watermark and re-persist. Shared by
4201
- * the `resume` and `settled` frame handlers — both acknowledge "nothing the
4202
- * client must re-render changed, but the resume position may have moved".
4320
+ * the `ack`, `resume` and `settled` frame handlers — each acknowledges the
4321
+ * subscription ("nothing the client must re-render changed, but the resume
4322
+ * position may have moved"; an `ack` carries no position at all). The sync
4323
+ * status is only recomputed when this is the acknowledgement that changes it.
4203
4324
  */
4204
4325
  private ackAndAdvanceCursor;
4205
4326
  /**
@@ -4701,13 +4822,12 @@ declare class LunoraClient {
4701
4822
  * Consume this shard's retry delay and re-flush it once the delay has
4702
4823
  * elapsed.
4703
4824
  *
4704
- * Two things record a delay for it to consume: a failure the server or an
4705
- * edge ANSWERED (see {@link replayRetryDelayMs}) and a flush that HELD every
4706
- * entry it drained (see {@link LunoraClient.noteHeldRetryDelay}). Both happen
4707
- * over a socket that stays open, so without this the writes sit queued
4708
- * indefinitely; a `fetch` that never landed records nothing, because the
4709
- * reconnect that follows flushes the queue anyway. One pending timer per
4710
- * shard; a second delay replaces it rather than stacking flushes.
4825
+ * Two things record a delay for it to consume: a transient failure (a server
4826
+ * or edge answer, or a `fetch` that rejected; see {@link replayRetryDelayMs})
4827
+ * and a flush that HELD every entry it drained (see
4828
+ * {@link LunoraClient.noteHeldRetryDelay}). Both happen over a socket that
4829
+ * stays open, so without this the writes sit queued indefinitely. One pending
4830
+ * timer per shard; a second delay replaces it rather than stacking flushes.
4711
4831
  *
4712
4832
  * Nothing left to retry on this key (drained, closed, or no delay) drops its
4713
4833
  * backoff state, which is both the reset after progress and what bounds the
@@ -4839,6 +4959,22 @@ declare class LunoraClient {
4839
4959
  * rejected with the decode error and the settled event carries it.
4840
4960
  */
4841
4961
  private settleReplayUndecodable;
4962
+ /**
4963
+ * Count a failed replay attempt against `item` and report whether it was the
4964
+ * last attempt the write gets.
4965
+ *
4966
+ * Only a write queued after an ambiguous direct send is capped: its caller is
4967
+ * still waiting, so every transient failure of it except a rate limit or a
4968
+ * refused credential (a transport error, a timeout, `SHARD_UNAVAILABLE`, ...)
4969
+ * counts against {@link MAX_AMBIGUOUS_SEND_ATTEMPTS}. Any other write is never
4970
+ * capped here: a write the shard fails deterministically arrives with a coded
4971
+ * verdict (the handler's own code, `RPC_FAILED`, `BATCH_ENTRY_FAILED`) that
4972
+ * {@link isTransientReplayFailure} settles terminally on the first attempt, and
4973
+ * a transient failure says nothing about the write itself.
4974
+ */
4975
+ private spendsLastReplayAttempt;
4976
+ /** Settle, terminally, the ambiguous-send writes among `items` that `error` was the last attempt for; the rest stay queued. */
4977
+ private keepUnexhaustedAmbiguousWrites;
4842
4978
  /** Settle a write the server reached a coded verdict on: replaying would re-trigger the same failure (a poison-message loop), so drop it. */
4843
4979
  private settleReplayTerminal;
4844
4980
  /**
@@ -4913,4 +5049,4 @@ declare class LunoraClient {
4913
5049
  */
4914
5050
  private settleReplayBatchSlots;
4915
5051
  }
4916
- export { SchedulerStatus as $, ActionCallOptions as A, BookmarkStorage as B, ConnectionStatus as C, DEFAULT_MAX_BUFFER as D, OutboxMutation as E, FunctionArgumentDescriptor as F, GlobalFacetResult as G, HttpStreamRef as H, OutboxSink as I, PersistedMutation as J, ReplayCredential as K, LunoraClient as L, MutationCallOptions as M, ReplayIdentityVerdict as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReconnectOptions as R, SubscriptionError as S, RoomRef as T, User as U, RowOp as V, RpcEnvelope as W, RpcResponseBody as X, ScheduleRecord as Y, ScheduleRetryPolicy as Z, SchedulerPoolStatus as _, Unsubscribe as a, ServerMessage as a0, ServerPokeEndMessage as a1, ServerPokePartMessage as a2, ServerPokeStartMessage as a3, ShardTrafficEntry as a4, ShardTrafficResult as a5, StorageListPage as a6, StorageObject as a7, StoredQuery as a8, StreamHandle as a9, SubscriptionCallback as aa, SubscriptionRegistry as ab, SubscriptionState as ac, SyncWatermark as ad, WorkflowInstanceAction as ae, WorkflowInstanceDetail as af, WorkflowInstancePage as ag, WorkflowInstanceStatus as ah, WorkflowInstanceSummary as ai, WorkflowStepDetail as aj, WsTokenProvider as ak, createClientQuery as al, createLocalStore as am, createStream as an, SubscriptionErrorCallback as b, PersistenceAdapter as c, HttpStreamArgsOf as d, HttpStreamChunkOf as e, StreamIterable as f, BatchSlot as g, CachedQuery as h, ClientDebugShard as i, ClientDebugSnapshot as j, ClientDebugSubscription as k, ClientMessage as l, ClientQueryRef as m, ClientShapeSubscribeMessage as n, ClientShapeUnsubscribeMessage as o, FunctionDescriptor as p, GlobalFacetValue as q, GlobalFilterClause as r, GlobalTableInfo as s, GlobalTablePage as t, HttpStreamCallArgs as u, LunoraClientError as v, LunoraClientOptions as w, MutationSettledEvent as x, OptimisticLocalStore as y, OptimisticUpdate as z };
5052
+ export { SchedulerStatus as $, ActionCallOptions as A, BookmarkStorage as B, ConnectionStatus as C, DEFAULT_MAX_BUFFER as D, OutboxMutation as E, FunctionArgumentDescriptor as F, GlobalFacetResult as G, HttpStreamRef as H, OutboxSink as I, PersistedMutation as J, ReplayCredential as K, LunoraClient as L, MutationCallOptions as M, ReplayIdentityVerdict as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReconnectOptions as R, SubscriptionError as S, RoomRef as T, User as U, RowOp as V, RpcEnvelope as W, RpcResponseBody as X, ScheduleRecord as Y, ScheduleRetryPolicy as Z, SchedulerPoolStatus as _, Unsubscribe as a, ServerMessage as a0, ServerPokeEndMessage as a1, ServerPokePartMessage as a2, ServerPokeStartMessage as a3, ShardTrafficEntry as a4, ShardTrafficResult as a5, StorageListPage as a6, StorageObject as a7, StoredQuery as a8, StreamHandle as a9, SubscriptionCallback as aa, SubscriptionRegistry as ab, SubscriptionState as ac, SyncStatus as ad, SyncWatermark as ae, WorkflowInstanceAction as af, WorkflowInstanceDetail as ag, WorkflowInstancePage as ah, WorkflowInstanceStatus as ai, WorkflowInstanceSummary as aj, WorkflowStepDetail as ak, WsTokenProvider as al, createClientQuery as am, createLocalStore as an, createStream as ao, SubscriptionErrorCallback as b, PersistenceAdapter as c, HttpStreamArgsOf as d, HttpStreamChunkOf as e, StreamIterable as f, BatchSlot as g, CachedQuery as h, ClientDebugShard as i, ClientDebugSnapshot as j, ClientDebugSubscription as k, ClientMessage as l, ClientQueryRef as m, ClientShapeSubscribeMessage as n, ClientShapeUnsubscribeMessage as o, FunctionDescriptor as p, GlobalFacetValue as q, GlobalFilterClause as r, GlobalTableInfo as s, GlobalTablePage as t, HttpStreamCallArgs as u, LunoraClientError as v, LunoraClientOptions as w, MutationSettledEvent as x, OptimisticLocalStore as y, OptimisticUpdate as z };