@sweidos/eidos 1.0.34 → 1.1.0

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 (48) hide show
  1. package/README.md +96 -89
  2. package/dist/action.js +119 -86
  3. package/dist/async-storage-adapter.js +15 -12
  4. package/dist/devtools.js +953 -555
  5. package/dist/eidos.cjs +15 -0
  6. package/dist/idb.js +59 -56
  7. package/dist/index.d.ts +37 -15
  8. package/dist/index.js +42 -41
  9. package/dist/nextjs.js +1 -10
  10. package/dist/query.cjs +131 -0
  11. package/dist/query.js +121 -41
  12. package/dist/queue-storage.js +5 -4
  13. package/dist/react/Devtools.d.ts +1 -1
  14. package/dist/react/Provider.js +11 -7
  15. package/dist/react/hooks.js +48 -38
  16. package/dist/react-native.js +47 -53
  17. package/dist/replay.js +15 -0
  18. package/dist/resource.js +77 -79
  19. package/dist/runtime.js +22 -28
  20. package/dist/store-slices.js +43 -0
  21. package/dist/store.js +32 -49
  22. package/dist/stores.js +25 -22
  23. package/dist/sveltekit.js +22 -6
  24. package/dist/sw-bridge.js +48 -46
  25. package/dist/testing.cjs +165 -0
  26. package/dist/testing.js +140 -70
  27. package/dist/version.js +4 -3
  28. package/dist/vite.cjs +48 -0
  29. package/dist/vite.js +45 -29
  30. package/package.json +48 -27
  31. package/dist/action.js.map +0 -1
  32. package/dist/async-storage-adapter.js.map +0 -1
  33. package/dist/eidos.cjs.js +0 -14
  34. package/dist/eidos.cjs.js.map +0 -1
  35. package/dist/idb.js.map +0 -1
  36. package/dist/index.js.map +0 -1
  37. package/dist/query.cjs.js +0 -48
  38. package/dist/queue-storage.js.map +0 -1
  39. package/dist/react/Provider.js.map +0 -1
  40. package/dist/react/hooks.js.map +0 -1
  41. package/dist/resource.js.map +0 -1
  42. package/dist/runtime.js.map +0 -1
  43. package/dist/store.js.map +0 -1
  44. package/dist/stores.js.map +0 -1
  45. package/dist/sw-bridge.js.map +0 -1
  46. package/dist/testing.cjs.js +0 -86
  47. package/dist/version.js.map +0 -1
  48. package/dist/vite.cjs.js +0 -31
package/dist/eidos.cjs ADDED
@@ -0,0 +1,15 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});let v=require("react"),j=require("react/jsx-runtime");function X(e){return{registerResource:(t,n)=>e(r=>({resources:{...r.resources,[t]:n}})),updateResource:(t,n)=>e(r=>({resources:{...r.resources,[t]:r.resources[t]?{...r.resources[t],...n}:r.resources[t]}})),unregisterResource:t=>e(n=>({resources:Object.fromEntries(Object.entries(n.resources).filter(([r])=>r!==t))}))}}function Z(e){return{addQueueItem:t=>e(n=>({queue:[...n.queue,t]})),updateQueueItem:(t,n)=>e(r=>({queue:r.queue.map(s=>s.id===t?{...s,...n}:s)})),batchUpdateQueueItems:t=>e(n=>{const r=new Map(t.map(s=>[s.id,s.update]));return{queue:n.queue.map(s=>{const a=r.get(s.id);return a?{...s,...a}:s})}}),removeQueueItem:t=>e(n=>({queue:n.queue.filter(r=>r.id!==t)})),hydrateQueue:t=>e(()=>({queue:t}))}}var g,x=new Set;function W(){x.forEach(e=>e())}function b(e){g={...g,...e(g)},W()}g={isOnline:typeof navigator>"u"||navigator.onLine!==!1,swStatus:"idle",swError:void 0,resources:{},queue:[],setOnline:e=>b(()=>({isOnline:e})),setSwStatus:(e,t)=>b(()=>({swStatus:e,swError:t})),...X(b),...Z(b)};function ee(){return g}function te(e){return x.add(e),()=>{x.delete(e)}}var o={getState:ee,subscribe:te,setState:e=>{const t=typeof e=="function"?e(g):e;g={...g,...t},W()}},y=null,C=[];function ne(){return y}async function re(e){if(typeof navigator>"u"||!("serviceWorker"in navigator)){o.getState().setSwStatus("unsupported");return}const t=o.getState();t.setSwStatus("registering");try{y=await navigator.serviceWorker.register(e,{scope:"/"}),await se(y),t.setSwStatus("active"),navigator.serviceWorker.addEventListener("message",oe),window.addEventListener("online",()=>t.setOnline(!0)),window.addEventListener("offline",()=>t.setOnline(!1)),ue()}catch(n){t.setSwStatus("error",String(n))}}function se(e){return new Promise(t=>{if(e.active){t();return}const n=e.installing??e.waiting;if(!n){t();return}const r=setTimeout(t,1e4);n.addEventListener("statechange",function s(){n.state==="activated"&&(clearTimeout(r),n.removeEventListener("statechange",s),t())})})}function k(e){const t=y?.active;t?t.postMessage(e):C.push(e)}var H=null;function ae(e){H=e}function ie(){try{return typeof navigator<"u"&&"serviceWorker"in navigator&&y!==null&&"sync"in y}catch{return!1}}function oe(e){const t=e.data;if(!t?.type)return;const n=o.getState(),{type:r,url:s}=t;if(r==="EIDOS_BACKGROUND_SYNC"){H?.();return}if(s)switch(r){case"EIDOS_CACHE_HIT":{const a=n.resources[s];n.updateResource(s,{status:"fresh",lastEvent:"cache-hit",cacheHits:(a?.cacheHits??0)+1});break}case"EIDOS_CACHE_UPDATED":n.updateResource(s,{status:"fresh",lastEvent:"cache-updated",cachedAt:Date.now()});break;case"EIDOS_NETWORK_ERROR":n.updateResource(s,{status:"error",lastEvent:"network-error"});break}}function ce(e){k({type:"EIDOS_SIMULATE_OFFLINE",enabled:e}),o.getState().setOnline(!e)}function ue(){const e=y?.active;if(e){for(const t of C)e.postMessage(t);C=[]}}var R=new Map,_=new Map,$=null;function de(e){$=e}function S(e){return e.includes("*")||/:[^/]+/.test(e)}function le(e){return"^"+e.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*\*/g,".+").replace(/\*/g,"[^/]+").replace(/:[^/]+/g,"[^/]+")+"$"}function O(e,t){return new Error(`[eidos] resource('${e}') is a URL pattern — ${t}() is not supported on pattern handles. The SW intercepts matching requests automatically; call fetch(specificUrl) directly in your app code.`)}function fe(e,t){if(R.has(e))return R.get(e);const n=pe(t),r=S(e)?le(e):void 0,s={url:e,config:t,strategy:n,status:"idle",cacheHits:0,cacheMisses:0};o.getState().registerResource(e,s),k({type:"EIDOS_REGISTER_RESOURCE",url:e,strategy:n.swStrategy,cacheName:n.cacheName,...r!==void 0&&{pattern:r}});const a={url:e,config:t,strategy:n,fetch:async()=>{if(S(e))throw O(e,"fetch");const i=_.get(e);if(i)return i.then(u=>u.clone());const c=he(e,t,n);return _.set(e,c),c.finally(()=>_.delete(e)).catch(()=>{}),c.then(u=>u.clone())},json:async()=>{if(S(e))throw O(e,"json");return(await a.fetch()).json()},query:()=>{if(S(e))throw O(e,"query");return{queryKey:["eidos",e],queryFn:()=>a.json()}},prefetch:async()=>{if(S(e))throw O(e,"prefetch");await a.fetch()},invalidate:async()=>{k({type:"EIDOS_CLEAR_CACHE",url:e});const i=await caches.open(n.cacheName).catch(()=>null);if(i){const c=await i.keys(),u=r?new RegExp(r):null,p=e.startsWith("http");await Promise.all(c.filter(w=>{const l=w.url,T=new URL(l).pathname;return u?u.test(p?l:T):p?l===e:l===e||T===e}).map(w=>i.delete(w)))}S(e)||o.getState().updateResource(e,{status:"stale",cachedAt:void 0,lastEvent:"cache-cleared",cacheHits:0,cacheMisses:0}),$?.(["eidos",e])},unregister:()=>{R.delete(e),k({type:"EIDOS_UNREGISTER_RESOURCE",url:e}),o.getState().unregisterResource(e)}};return R.set(e,a),a}async function he(e,t,n){const r=o.getState();r.updateResource(e,{status:"fetching",fetchedAt:Date.now()});const s=await caches.open(n.cacheName).catch(()=>null);try{if(n.swStrategy!=="network-first"){const c=s?await s.match(e).catch(()=>null):null,u=o.getState().resources[e],p=t.maxAge!==void 0&&u?.cachedAt!==void 0&&Date.now()-u.cachedAt>t.maxAge;if(c&&!p)return r.updateResource(e,{status:"fresh",lastEvent:"cache-hit",cacheHits:(u?.cacheHits??0)+1}),n.swStrategy==="stale-while-revalidate"&&fetch(e,{signal:AbortSignal.timeout(5e3)}).then(async l=>{l.ok&&s&&(await s.put(e,l.clone()),o.getState().updateResource(e,{cachedAt:Date.now(),lastEvent:"cache-updated"}))}).catch(()=>{}),c;const w=o.getState().resources[e];r.updateResource(e,{cacheMisses:(w?.cacheMisses??0)+1})}const a=await fetch(e);if(a.ok)return s&&await s.put(e,a.clone()),r.updateResource(e,{status:"fresh",cachedAt:Date.now(),lastEvent:"cache-updated"}),a;r.updateResource(e,{status:a.status===503?"offline":"error"});const i=a.headers.get("X-Eidos-Offline")==="true";throw new Error(i?`offline: no cached response for ${e}`:`${a.status} ${a.statusText}`)}catch(a){const i=s?await s.match(e).catch(()=>null):null;if(i){const c=o.getState().resources[e];return r.updateResource(e,{status:"fresh",lastEvent:"cache-hit",cacheHits:(c?.cacheHits??0)+1}),i}throw r.updateResource(e,{status:"error"}),a}}function pe(e){const t=e.strategy;return e.offline?M(t??"stale-while-revalidate",e.cacheName):M(t??"network-first",e.cacheName)}var ge={"stale-while-revalidate":"StaleWhileRevalidate","cache-first":"CacheFirst","network-first":"NetworkFirst"},we={"stale-while-revalidate":{reasoning:"offline: true signals resilience. SWR returns cached data instantly while revalidating in the background — the best tradeoff between speed and freshness for offline-capable resources.",behavior:["Cache hit → return immediately, kick off background revalidation","Cache miss → fetch from network, cache the response, return it","Offline → return cached version if available, 503 if not","Reconnect → next request triggers a background refresh"],equivalentCode:`// Workbox equivalent
2
+ new StaleWhileRevalidate({
3
+ cacheName: 'eidos-resources-v1',
4
+ plugins: [new ExpirationPlugin({ maxEntries: 60 })],
5
+ })`},"cache-first":{reasoning:"cache-first maximises speed and offline availability. Network is consulted only on cache miss. Best for static or infrequently-updated data.",behavior:["Cache hit → return immediately, no network request made","Cache miss → fetch from network, cache the response, return it","Offline → return cached version, 503 if cache is empty","Cache never expires unless explicitly invalidated"],equivalentCode:`// Workbox equivalent
6
+ new CacheFirst({
7
+ cacheName: 'eidos-resources-v1',
8
+ plugins: [new ExpirationPlugin({ maxEntries: 60 })],
9
+ })`},"network-first":{reasoning:"network-first prioritises fresh data. Cache acts as a safety net when offline. Best for frequently-updated resources where stale data causes problems.",behavior:["Always try network first","Network success → update cache, return fresh response","Network failure → fall back to cached version","Offline with empty cache → return 503 error response"],equivalentCode:`// Workbox equivalent
10
+ new NetworkFirst({
11
+ cacheName: 'eidos-resources-v1',
12
+ networkTimeoutSeconds: 3,
13
+ })`}};function M(e,t){const n=we[e];return{name:ge[e],swStrategy:e,cacheName:t??"eidos-resources-v1",reasoning:n.reasoning,behavior:n.behavior,equivalentCode:""}}async function ye(e){const t=await Promise.allSettled(e.map(r=>r.prefetch())),n=t.filter(r=>r.status==="rejected").map(r=>r.reason);return{warmed:t.filter(r=>r.status==="fulfilled").length,failed:n.length,errors:n}}var Se="eidos",ve=1,d="action-queue",A=null;function m(){return A?Promise.resolve(A):new Promise((e,t)=>{const n=indexedDB.open(Se,ve);n.onupgradeneeded=r=>{const s=r.target.result;if(!s.objectStoreNames.contains(d)){const a=s.createObjectStore(d,{keyPath:"id"});a.createIndex("status","status",{unique:!1}),a.createIndex("actionId","actionId",{unique:!1})}},n.onsuccess=()=>{A=n.result,e(n.result)},n.onerror=()=>t(n.error)})}async function me(e){const t=await m();return new Promise((n,r)=>{const s=t.transaction(d,"readwrite");s.objectStore(d).add(e),s.oncomplete=()=>n(),s.onerror=()=>r(s.error)})}async function L(){const e=await m();return new Promise((t,n)=>{const r=e.transaction(d,"readonly").objectStore(d).getAll();r.onsuccess=()=>t(r.result),r.onerror=()=>n(r.error)})}async function Ee(e,t){const n=await m();return new Promise((r,s)=>{const a=n.transaction(d,"readwrite"),i=a.objectStore(d),c=i.get(e);c.onsuccess=()=>{c.result&&i.put({...c.result,...t})},a.oncomplete=()=>r(),a.onerror=()=>s(a.error)})}async function be(e){const t=await m();return new Promise((n,r)=>{const s=t.transaction(d,"readwrite");s.objectStore(d).delete(e),s.oncomplete=()=>n(),s.onerror=()=>r(s.error)})}async function Re(){const e=await m();function t(s){return new Promise((a,i)=>{const c=e.transaction(d,"readonly").objectStore(d).index("status"),u=[],p=c.openCursor(IDBKeyRange.only(s));p.onsuccess=w=>{const l=w.target.result;l?(u.push(l.value),l.continue()):a(u)},p.onerror=()=>i(p.error)})}const[n,r]=await Promise.all([t("pending"),t("failed")]);return[...n,...r]}async function Oe(){const e=await m();return new Promise((t,n)=>{const r=e.transaction(d,"readwrite");r.objectStore(d).clear(),r.oncomplete=()=>t(),r.onerror=()=>n(r.error)})}var ke={add:me,getAll:L,getPending:Re,update:Ee,remove:be,clear:Oe},B=null;function qe(e){B=e}function F(){return B}var N=new Map,G=new Map,K=new Map;function h(){return F()??ke}function V(){return crypto.randomUUID()}function _e(e,t){const n=t.name||e.name||V();N.set(n,e),t.onRollback&&G.set(n,t.onRollback),t.onConflict&&K.set(n,t.onConflict);const r=async(...s)=>{const{isOnline:a}=o.getState();if(t.onOptimistic?.(...s),t.reliability==="neverLose"){if(!a)return U(n,n,s,t);try{return await e(...s)}catch{return U(n,n,s,t)}}try{return await e(...s)}catch(i){throw t.onRollback?.(...s),i}};return Object.defineProperty(r,"id",{value:n,writable:!1}),Object.defineProperty(r,"config",{value:t,writable:!1}),r}async function U(e,t,n,r){const s=V(),a={id:s,actionId:e,actionName:t,args:n,queuedAt:Date.now(),retryCount:0,maxRetries:r.maxRetries??3,status:"pending",priority:r.priority??"normal"};await h().add(a),o.getState().addQueueItem(a);try{const i=ne();i&&"sync"in i&&await i.sync.register("eidos-queue-replay")}catch{}return{queued:!0,id:s,message:`"${t}" queued — will execute when online`}}function Ae(e){if(e instanceof Response)return e.status>=400&&e.status<500;if(typeof e=="object"&&e!==null){const t=e.status;if(typeof t=="number")return t>=400&&t<500}return!1}function Ie(e){return Math.min(2e3*2**e,3e5)*(.8+Math.random()*.4)}var I=!1;async function q(){const e=o.getState();if(!e.isOnline||I)return{attempted:0,succeeded:0,failed:0,retrying:0,skipped:0,conflicted:0};I=!0;try{return await Ce(e)}finally{I=!1}}async function Qe(e,t){const n=N.get(e.actionId);if(!n)return"skipped";try{await n(...e.args);const r=Date.now();return t.updateQueueItem(e.id,{status:"succeeded",completedAt:r}),await h().update(e.id,{status:"succeeded",completedAt:r}),setTimeout(()=>{t.removeQueueItem(e.id),h().remove(e.id)},3e3),"succeeded"}catch(r){if(Ae(r)){const a=K.get(e.actionId);if(a&&a(r,e.args)==="skip")return t.removeQueueItem(e.id),await h().remove(e.id),"conflicted"}const s=e.retryCount+1;if(s>=e.maxRetries)return t.updateQueueItem(e.id,{status:"failed",error:String(r),retryCount:s}),await h().update(e.id,{status:"failed",error:String(r),retryCount:s}),G.get(e.actionId)?.(...e.args),"failed";{const a=Date.now()+Ie(s);return t.updateQueueItem(e.id,{status:"pending",retryCount:s,nextRetryAt:a}),await h().update(e.id,{status:"pending",retryCount:s,nextRetryAt:a}),"retrying"}}}async function xe(e,t,n){if(e.length===0)return;const r=e.filter(a=>N.has(a.actionId));if(n.skipped+=e.length-r.length,r.length>0){t.batchUpdateQueueItems(r.map(a=>({id:a.id,update:{status:"replaying"}})));for(const a of r)h().update(a.id,{status:"replaying"})}const s=await Promise.allSettled(r.map(a=>Qe(a,t)));for(const a of s){const i=a.status==="fulfilled"?a.value:"failed";i==="skipped"?n.skipped++:i==="conflicted"?n.conflicted++:(n.attempted++,n[i]++)}}async function Ce(e){const t=await h().getPending(),n=Date.now(),r=t.filter(a=>a.retryCount<a.maxRetries&&(!a.nextRetryAt||a.nextRetryAt<=n)),s={attempted:0,succeeded:0,failed:0,retrying:0,skipped:0,conflicted:0};for(const a of["high","normal","low"])await xe(r.filter(i=>(i.priority??"normal")===a),e,s);return s}async function Pe(){await h().clear(),o.getState().hydrateQueue([])}function Y(){let e=o.getState().isOnline;const t=o.subscribe(()=>{const{isOnline:s}=o.getState(),a=s&&!e;e=s,a&&setTimeout(q,600)}),n=o.getState(),r=n.queue.some(s=>s.status==="pending");return n.isOnline&&r&&setTimeout(q,1200),t}var P=!1,D=null;async function J(e={}){if(typeof window>"u"||P)return;P=!0;const t=e.swPath??"/eidos-sw.js",n=e.autoReplay??!0;try{const r=await L();r.length>0&&o.getState().hydrateQueue(r)}catch{}try{await re(t)}catch{}ae(()=>{o.getState().isOnline&&setTimeout(q,200)}),n&&(D=Y())}function De(){D?.(),D=null,P=!1}var Q="@eidos:queue",Ne=class{constructor(e){this.storage=e}async readAll(){try{const e=await this.storage.getItem(Q);return e?JSON.parse(e):[]}catch{return[]}}async writeAll(e){await this.storage.setItem(Q,JSON.stringify(e))}async add(e){const t=await this.readAll();t.push(e),await this.writeAll(t)}async getAll(){return this.readAll()}async getPending(){return(await this.readAll()).filter(e=>e.status==="pending"||e.status==="failed")}async update(e,t){const n=await this.readAll(),r=n.findIndex(s=>s.id===e);r!==-1&&(n[r]={...n[r],...t}),await this.writeAll(n)}async remove(e){const t=await this.readAll();await this.writeAll(t.filter(n=>n.id!==e))}async clear(){await this.storage.removeItem(Q)}};function Te({children:e,swPath:t,autoReplay:n}){return(0,v.useEffect)(()=>{J({swPath:t,autoReplay:n})},[]),(0,j.jsx)(j.Fragment,{children:e})}function f(e){const t=e??(n=>n);return(0,v.useSyncExternalStore)(o.subscribe,()=>t(o.getState()))}function je(){return f()}function Me(){return f(e=>e.resources)}function Ue(e){return f(t=>t.resources[e])}function We(){return f(e=>e.queue)}function He(e){return f(t=>t.queue.find(n=>n.id===e))}function $e(){return{isOnline:f(e=>e.isOnline),swStatus:f(e=>e.swStatus),swError:f(e=>e.swError)}}function Le(){const[e,t,n,r]=f(s=>{let a=0,i=0,c=0;for(const u of s.queue)u.status==="pending"?a++:u.status==="failed"?i++:u.status==="replaying"&&c++;return`${a},${i},${c},${s.queue.length}`}).split(",");return{pending:+e,failed:+t,replaying:+n,total:+r}}function Be(e){const t=f(s=>s.queue.length),n=(0,v.useRef)(0),r=(0,v.useRef)(e);(0,v.useEffect)(()=>{r.current=e}),(0,v.useEffect)(()=>{n.current>0&&t===0&&r.current(),n.current=t},[t])}var Fe="1.1.0";function Ge(e,t){const n=Object.keys(e);if(n.length!==Object.keys(t).length)return!1;for(const r of n)if(e[r]!==t[r])return!1;return!0}function z(e,t){return Ge(e,t)}function E(e,t=Object.is){return{subscribe(n){let r=e(o.getState());return n(r),o.subscribe(()=>{const s=e(o.getState());t(r,s)||(r=s,n(s))})},getState(){return e(o.getState())}}}var Ke=E(e=>e),Ve=E(e=>e.queue),Ye=E(e=>({isOnline:e.isOnline,swStatus:e.swStatus,swError:e.swError}),z),Je=E(e=>{let t=0,n=0,r=0;for(const s of e.queue)s.status==="pending"?t++:s.status==="failed"?n++:s.status==="replaying"&&r++;return{pending:t,failed:n,replaying:r,total:e.queue.length}},z);function ze(e){return E(t=>t.resources[e])}function Xe(e){return E(t=>t.queue.find(n=>n.id===e))}exports.AsyncStorageQueueStorage=Ne;exports.EidosProvider=Te;exports.VERSION=Fe;exports._getQueueStorage=F;exports._resetEidos=De;exports.action=_e;exports.clearQueue=Pe;exports.eidosAction=Xe;exports.eidosQueue=Ve;exports.eidosQueueStats=Je;exports.eidosResource=ze;exports.eidosStatus=Ye;exports.eidosStore=Ke;exports.initEidos=J;exports.isBgSyncSupported=ie;exports.replayQueue=q;exports.resource=fe;exports.setOfflineSimulation=ce;exports.setQueryInvalidator=de;exports.setQueueStorage=qe;exports.subscribeReplayOnReconnect=Y;exports.useEidos=je;exports.useEidosAction=He;exports.useEidosOnDrain=Be;exports.useEidosQueue=We;exports.useEidosQueueStats=Le;exports.useEidosResource=Ue;exports.useEidosResources=Me;exports.useEidosStatus=$e;exports.useEidosStore=o;exports.warmCache=ye;
14
+
15
+ //# sourceMappingURL=eidos.cjs.map
package/dist/idb.js CHANGED
@@ -1,80 +1,83 @@
1
- const p = "eidos";
2
- const o = "action-queue";
3
- let b = null;
4
- function a() {
1
+ var f = "eidos", p = 1, o = "action-queue", b = null;
2
+ function c() {
5
3
  return b ? Promise.resolve(b) : new Promise((s, n) => {
6
- const t = indexedDB.open(p, 1);
7
- t.onupgradeneeded = (r) => {
8
- const e = r.target.result;
9
- if (!e.objectStoreNames.contains(o)) {
10
- const c = e.createObjectStore(o, { keyPath: "id" });
11
- c.createIndex("status", "status", { unique: !1 }), c.createIndex("actionId", "actionId", { unique: !1 });
4
+ const r = indexedDB.open(f, p);
5
+ r.onupgradeneeded = (e) => {
6
+ const t = e.target.result;
7
+ if (!t.objectStoreNames.contains(o)) {
8
+ const a = t.createObjectStore(o, { keyPath: "id" });
9
+ a.createIndex("status", "status", { unique: !1 }), a.createIndex("actionId", "actionId", { unique: !1 });
12
10
  }
13
- }, t.onsuccess = () => {
14
- b = t.result, s(t.result);
15
- }, t.onerror = () => n(t.error);
11
+ }, r.onsuccess = () => {
12
+ b = r.result, s(r.result);
13
+ }, r.onerror = () => n(r.error);
16
14
  });
17
15
  }
18
- async function x(s) {
19
- const n = await a();
20
- return new Promise((t, r) => {
21
- const e = n.transaction(o, "readwrite");
22
- e.objectStore(o).add(s), e.oncomplete = () => t(), e.onerror = () => r(e.error);
16
+ async function g(s) {
17
+ const n = await c();
18
+ return new Promise((r, e) => {
19
+ const t = n.transaction(o, "readwrite");
20
+ t.objectStore(o).add(s), t.oncomplete = () => r(), t.onerror = () => e(t.error);
23
21
  });
24
22
  }
25
23
  async function y() {
26
- const s = await a();
27
- return new Promise((n, t) => {
24
+ const s = await c();
25
+ return new Promise((n, r) => {
28
26
  const e = s.transaction(o, "readonly").objectStore(o).getAll();
29
- e.onsuccess = () => n(e.result), e.onerror = () => t(e.error);
27
+ e.onsuccess = () => n(e.result), e.onerror = () => r(e.error);
30
28
  });
31
29
  }
32
30
  async function P(s, n) {
33
- const t = await a();
34
- return new Promise((r, e) => {
35
- const c = t.transaction(o, "readwrite"), i = c.objectStore(o), u = i.get(s);
36
- u.onsuccess = () => {
37
- u.result && i.put({ ...u.result, ...n });
38
- }, c.oncomplete = () => r(), c.onerror = () => e(c.error);
31
+ const r = await c();
32
+ return new Promise((e, t) => {
33
+ const a = r.transaction(o, "readwrite"), u = a.objectStore(o), i = u.get(s);
34
+ i.onsuccess = () => {
35
+ i.result && u.put({
36
+ ...i.result,
37
+ ...n
38
+ });
39
+ }, a.oncomplete = () => e(), a.onerror = () => t(a.error);
39
40
  });
40
41
  }
41
42
  async function S(s) {
42
- const n = await a();
43
- return new Promise((t, r) => {
44
- const e = n.transaction(o, "readwrite");
45
- e.objectStore(o).delete(s), e.oncomplete = () => t(), e.onerror = () => r(e.error);
43
+ const n = await c();
44
+ return new Promise((r, e) => {
45
+ const t = n.transaction(o, "readwrite");
46
+ t.objectStore(o).delete(s), t.oncomplete = () => r(), t.onerror = () => e(t.error);
46
47
  });
47
48
  }
48
- async function g() {
49
- const s = await a();
50
- function n(e) {
51
- return new Promise((c, i) => {
52
- const w = s.transaction(o, "readonly").objectStore(o).index("status"), m = [], d = w.openCursor(IDBKeyRange.only(e));
53
- d.onsuccess = (f) => {
54
- const l = f.target.result;
55
- l ? (m.push(l.value), l.continue()) : c(m);
56
- }, d.onerror = () => i(d.error);
49
+ async function x() {
50
+ const s = await c();
51
+ function n(t) {
52
+ return new Promise((a, u) => {
53
+ const i = s.transaction(o, "readonly").objectStore(o).index("status"), m = [], d = i.openCursor(IDBKeyRange.only(t));
54
+ d.onsuccess = (w) => {
55
+ const l = w.target.result;
56
+ l ? (m.push(l.value), l.continue()) : a(m);
57
+ }, d.onerror = () => u(d.error);
57
58
  });
58
59
  }
59
- const [t, r] = await Promise.all([
60
- n("pending"),
61
- n("failed")
62
- ]);
63
- return [...t, ...r];
60
+ const [r, e] = await Promise.all([n("pending"), n("failed")]);
61
+ return [...r, ...e];
64
62
  }
65
- async function I() {
66
- const s = await a();
67
- return new Promise((n, t) => {
68
- const r = s.transaction(o, "readwrite");
69
- r.objectStore(o).clear(), r.oncomplete = () => n(), r.onerror = () => t(r.error);
63
+ async function v() {
64
+ const s = await c();
65
+ return new Promise((n, r) => {
66
+ const e = s.transaction(o, "readwrite");
67
+ e.objectStore(o).clear(), e.oncomplete = () => n(), e.onerror = () => r(e.error);
70
68
  });
71
69
  }
70
+ var j = {
71
+ add: g,
72
+ getAll: y,
73
+ getPending: x,
74
+ update: P,
75
+ remove: S,
76
+ clear: v
77
+ };
72
78
  export {
73
- x as idbAddToQueue,
74
- I as idbClearQueue,
75
- g as idbGetPendingItems,
76
79
  y as idbGetQueue,
77
- S as idbRemoveFromQueue,
78
- P as idbUpdateQueueItem
80
+ j as idbQueueStorage
79
81
  };
80
- //# sourceMappingURL=idb.js.map
82
+
83
+ //# sourceMappingURL=idb.js.map
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { JSX as JSX_2 } from 'react';
1
+ import { JSX } from 'react';
2
2
  import { ReactNode } from 'react';
3
3
 
4
4
  export declare function action<TArgs extends any[], TReturn>(fn: ActionFn<TArgs, TReturn>, config: ActionConfig): ActionHandle<TArgs, TReturn>;
@@ -131,7 +131,7 @@ export declare interface EidosConfig {
131
131
  * <App />
132
132
  * </EidosProvider>
133
133
  */
134
- export declare function EidosProvider({ children, swPath, autoReplay }: EidosProviderProps): JSX_2.Element;
134
+ export declare function EidosProvider({ children, swPath, autoReplay }: EidosProviderProps): JSX.Element;
135
135
 
136
136
  declare interface EidosProviderProps extends EidosConfig {
137
137
  children: ReactNode;
@@ -185,20 +185,9 @@ export declare const eidosStatus: EidosReadable<{
185
185
  swError: string | undefined;
186
186
  }>;
187
187
 
188
- export declare interface EidosStore extends EidosState {
188
+ export declare interface EidosStore extends EidosState, ResourceActions, QueueActions {
189
189
  setOnline: (online: boolean) => void;
190
190
  setSwStatus: (status: EidosState['swStatus'], error?: string) => void;
191
- registerResource: (url: string, entry: ResourceEntry) => void;
192
- updateResource: (url: string, update: Partial<ResourceEntry>) => void;
193
- unregisterResource: (url: string) => void;
194
- addQueueItem: (item: ActionQueueItem) => void;
195
- updateQueueItem: (id: string, update: Partial<ActionQueueItem>) => void;
196
- batchUpdateQueueItems: (updates: Array<{
197
- id: string;
198
- update: Partial<ActionQueueItem>;
199
- }>) => void;
200
- removeQueueItem: (id: string) => void;
201
- hydrateQueue: (items: ActionQueueItem[]) => void;
202
191
  }
203
192
 
204
193
  /** Full Eidos state snapshot. Prefer the narrower stores below. */
@@ -228,6 +217,17 @@ declare type Listener = () => void;
228
217
 
229
218
  declare type QueryInvalidator = (queryKey: [string, string]) => void;
230
219
 
220
+ declare interface QueueActions {
221
+ addQueueItem: (item: ActionQueueItem) => void;
222
+ updateQueueItem: (id: string, update: Partial<ActionQueueItem>) => void;
223
+ batchUpdateQueueItems: (updates: Array<{
224
+ id: string;
225
+ update: Partial<ActionQueueItem>;
226
+ }>) => void;
227
+ removeQueueItem: (id: string) => void;
228
+ hydrateQueue: (items: ActionQueueItem[]) => void;
229
+ }
230
+
231
231
  export declare interface QueuedResult {
232
232
  readonly queued: true;
233
233
  readonly id: string;
@@ -265,6 +265,12 @@ export declare function _resetEidos(): void;
265
265
 
266
266
  export declare function resource<T = unknown>(url: string, config: ResourceConfig): ResourceHandle<T>;
267
267
 
268
+ declare interface ResourceActions {
269
+ registerResource: (url: string, entry: ResourceEntry) => void;
270
+ updateResource: (url: string, update: Partial<ResourceEntry>) => void;
271
+ unregisterResource: (url: string) => void;
272
+ }
273
+
268
274
  export declare interface ResourceConfig {
269
275
  /** Make this resource available when the device is offline. */
270
276
  offline: boolean;
@@ -314,6 +320,22 @@ export declare function setQueueStorage(s: QueueStorage): void;
314
320
 
315
321
  declare function _subscribe(listener: Listener): () => void;
316
322
 
323
+ /**
324
+ * Subscribe to online/offline transitions and trigger replayQueue() on
325
+ * reconnect, plus replay any pending items left over from a previous session.
326
+ *
327
+ * Shared by the web (runtime.ts) and React Native (runtime-rn.ts) init paths.
328
+ *
329
+ * WHY subscribe to the store instead of window.addEventListener('online'):
330
+ * setOfflineSimulation() updates the store directly but never fires a real
331
+ * browser `online` event. Watching the store catches both:
332
+ * • Real network reconnects (sw-bridge updates store on window.online)
333
+ * • Simulation toggled off (setOfflineSimulation(false) → store.setOnline(true))
334
+ *
335
+ * Returns an unsubscribe function.
336
+ */
337
+ export declare function subscribeReplayOnReconnect(): () => void;
338
+
317
339
  /** Full Eidos store — prefer the narrower hooks below for performance. */
318
340
  export declare function useEidos(): EidosStore;
319
341
 
@@ -372,7 +394,7 @@ export declare const useEidosStore: {
372
394
  setState: (partial: Partial<EidosStore> | ((s: EidosStore) => Partial<EidosStore>)) => void;
373
395
  };
374
396
 
375
- export declare const VERSION = "1.0.34";
397
+ export declare const VERSION = "1.1.0";
376
398
 
377
399
  /**
378
400
  * Bulk-prefetch an array of resource handles concurrently, warming the cache
package/dist/index.js CHANGED
@@ -1,44 +1,45 @@
1
- import { resource as r, setQueryInvalidator as s, warmCache as t } from "./resource.js";
2
- import { action as i, clearQueue as d, replayQueue as a } from "./action.js";
3
- import { _resetEidos as S, initEidos as f } from "./runtime.js";
4
- import { _getQueueStorage as E, setQueueStorage as c } from "./queue-storage.js";
5
- import { AsyncStorageQueueStorage as x } from "./async-storage-adapter.js";
6
- import { EidosProvider as g } from "./react/Provider.js";
7
- import { useEidos as y, useEidosAction as R, useEidosOnDrain as A, useEidosQueue as O, useEidosQueueStats as v, useEidosResource as I, useEidosResources as _, useEidosStatus as h } from "./react/hooks.js";
8
- import { VERSION as B } from "./version.js";
9
- import { isBgSyncSupported as D, setOfflineSimulation as N } from "./sw-bridge.js";
10
- import { useEidosStore as V } from "./store.js";
11
- import { eidosAction as j, eidosQueue as k, eidosQueueStats as q, eidosResource as z, eidosStatus as F, eidosStore as G } from "./stores.js";
1
+ import { useEidosStore as o } from "./store.js";
2
+ import { isBgSyncSupported as s, setOfflineSimulation as i } from "./sw-bridge.js";
3
+ import { resource as u, setQueryInvalidator as m, warmCache as d } from "./resource.js";
4
+ import { _getQueueStorage as p, setQueueStorage as c } from "./queue-storage.js";
5
+ import { action as n, clearQueue as S, replayQueue as E } from "./action.js";
6
+ import { subscribeReplayOnReconnect as g } from "./replay.js";
7
+ import { _resetEidos as R, initEidos as y } from "./runtime.js";
8
+ import { AsyncStorageQueueStorage as A } from "./async-storage-adapter.js";
9
+ import { EidosProvider as v } from "./react/Provider.js";
10
+ import { useEidos as _, useEidosAction as h, useEidosOnDrain as w, useEidosQueue as x, useEidosQueueStats as B, useEidosResource as C, useEidosResources as D, useEidosStatus as N } from "./react/hooks.js";
11
+ import { VERSION as V } from "./version.js";
12
+ import { eidosAction as k, eidosQueue as q, eidosQueueStats as z, eidosResource as F, eidosStatus as G, eidosStore as H } from "./stores.js";
12
13
  export {
13
- x as AsyncStorageQueueStorage,
14
- g as EidosProvider,
15
- B as VERSION,
16
- E as _getQueueStorage,
17
- S as _resetEidos,
18
- i as action,
19
- d as clearQueue,
20
- j as eidosAction,
21
- k as eidosQueue,
22
- q as eidosQueueStats,
23
- z as eidosResource,
24
- F as eidosStatus,
25
- G as eidosStore,
26
- f as initEidos,
27
- D as isBgSyncSupported,
28
- a as replayQueue,
29
- r as resource,
30
- N as setOfflineSimulation,
31
- s as setQueryInvalidator,
14
+ A as AsyncStorageQueueStorage,
15
+ v as EidosProvider,
16
+ V as VERSION,
17
+ p as _getQueueStorage,
18
+ R as _resetEidos,
19
+ n as action,
20
+ S as clearQueue,
21
+ k as eidosAction,
22
+ q as eidosQueue,
23
+ z as eidosQueueStats,
24
+ F as eidosResource,
25
+ G as eidosStatus,
26
+ H as eidosStore,
27
+ y as initEidos,
28
+ s as isBgSyncSupported,
29
+ E as replayQueue,
30
+ u as resource,
31
+ i as setOfflineSimulation,
32
+ m as setQueryInvalidator,
32
33
  c as setQueueStorage,
33
- y as useEidos,
34
- R as useEidosAction,
35
- A as useEidosOnDrain,
36
- O as useEidosQueue,
37
- v as useEidosQueueStats,
38
- I as useEidosResource,
39
- _ as useEidosResources,
40
- h as useEidosStatus,
41
- V as useEidosStore,
42
- t as warmCache
34
+ g as subscribeReplayOnReconnect,
35
+ _ as useEidos,
36
+ h as useEidosAction,
37
+ w as useEidosOnDrain,
38
+ x as useEidosQueue,
39
+ B as useEidosQueueStats,
40
+ C as useEidosResource,
41
+ D as useEidosResources,
42
+ N as useEidosStatus,
43
+ o as useEidosStore,
44
+ d as warmCache
43
45
  };
44
- //# sourceMappingURL=index.js.map
package/dist/nextjs.js CHANGED
@@ -1,12 +1,3 @@
1
1
  "use client";
2
2
  import { EidosProvider, useEidos, useEidosAction, useEidosOnDrain, useEidosQueue, useEidosQueueStats, useEidosResource, useEidosStatus } from "@sweidos/eidos";
3
- export {
4
- EidosProvider,
5
- useEidos,
6
- useEidosAction,
7
- useEidosOnDrain,
8
- useEidosQueue,
9
- useEidosQueueStats,
10
- useEidosResource,
11
- useEidosStatus
12
- };
3
+ export { EidosProvider, useEidos, useEidosAction, useEidosOnDrain, useEidosQueue, useEidosQueueStats, useEidosResource, useEidosStatus };
package/dist/query.cjs ADDED
@@ -0,0 +1,131 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _tanstack_react_query = require("@tanstack/react-query");
3
+ let _sweidos_eidos = require("@sweidos/eidos");
4
+ //#region src/query.ts
5
+ /**
6
+ * @sweidos/eidos/query
7
+ *
8
+ * TanStack Query (React Query) integration for Eidos.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * // Register once (e.g. alongside new QueryClient())
13
+ * import { withEidosQueryClient } from '@sweidos/eidos/query'
14
+ * withEidosQueryClient(queryClient)
15
+ *
16
+ * // In components
17
+ * import { useEidosQuery, useEidosMutation } from '@sweidos/eidos/query'
18
+ *
19
+ * const { data, isPending } = useEidosQuery(products)
20
+ *
21
+ * const mutation = useEidosMutation(createOrder, {
22
+ * invalidates: [products],
23
+ * })
24
+ * ```
25
+ */
26
+ var _globalClient = null;
27
+ /**
28
+ * Register a QueryClient with Eidos.
29
+ *
30
+ * Once called, `handle.invalidate()` will also call
31
+ * `queryClient.invalidateQueries({ queryKey: ['eidos', url] })`, keeping
32
+ * TanStack Query's cache in sync with Eidos's Cache Storage.
33
+ *
34
+ * Call this once, before rendering — e.g. alongside `new QueryClient()`.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const queryClient = new QueryClient()
39
+ * withEidosQueryClient(queryClient)
40
+ *
41
+ * // Wrap your app as usual
42
+ * <QueryClientProvider client={queryClient}>
43
+ * <App />
44
+ * </QueryClientProvider>
45
+ * ```
46
+ */
47
+ function withEidosQueryClient(client) {
48
+ _globalClient = client;
49
+ (0, _sweidos_eidos.setQueryInvalidator)((queryKey) => {
50
+ client.invalidateQueries({ queryKey });
51
+ });
52
+ }
53
+ /**
54
+ * Wraps `useQuery` with Eidos-smart defaults.
55
+ *
56
+ * Key differences from plain `useQuery`:
57
+ * - `networkMode: 'always'` — Eidos owns offline logic; queries run even when
58
+ * `navigator.onLine` is false (the SW cache or IndexedDB serves the data).
59
+ * - `retry: false` — Eidos handles retries at the SW / replay layer; TQ
60
+ * retrying on top would double-fire and fight Eidos's backoff.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * const products = resource('/api/products', { offline: true })
65
+ *
66
+ * // Automatically typed as UseQueryResult<Product[]>
67
+ * const { data, isPending, isError } = useEidosQuery<Product[]>(products)
68
+ *
69
+ * // Override any TQ option
70
+ * const { data } = useEidosQuery(products, { staleTime: 30_000 })
71
+ * ```
72
+ */
73
+ function useEidosQuery(handle, options) {
74
+ return (0, _tanstack_react_query.useQuery)({
75
+ networkMode: "always",
76
+ retry: false,
77
+ ...options,
78
+ ...handle.query()
79
+ });
80
+ }
81
+ /**
82
+ * Wraps `useMutation` for a single-argument Eidos action handle.
83
+ *
84
+ * Key differences from plain `useMutation`:
85
+ * - `networkMode: 'always'` — action executes (or queues) even when offline.
86
+ * - `invalidates` — shorthand to clear resource caches on success. Triggers
87
+ * both Eidos Cache Storage and TanStack Query invalidation (requires
88
+ * `withEidosQueryClient` for the TQ half).
89
+ * - Return type is `TData | QueuedResult`. Narrow with `'queued' in data` to
90
+ * detect the offline-queued case.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * const mutation = useEidosMutation(createOrder, {
95
+ * invalidates: [products],
96
+ * onSuccess(data) {
97
+ * if ('queued' in data) toast('Saved offline — will sync when back online')
98
+ * else toast(`Order #${data.id} created!`)
99
+ * },
100
+ * })
101
+ *
102
+ * // Trigger
103
+ * mutation.mutate({ productId: 1, qty: 2 })
104
+ * ```
105
+ */
106
+ function useEidosMutation(handle, options) {
107
+ let contextClient = null;
108
+ try {
109
+ contextClient = (0, _tanstack_react_query.useQueryClient)();
110
+ } catch {}
111
+ const { invalidates, onSuccess, ...rest } = options ?? {};
112
+ return (0, _tanstack_react_query.useMutation)({
113
+ networkMode: "always",
114
+ ...rest,
115
+ mutationFn: (arg) => handle(arg),
116
+ onSuccess: async (...args) => {
117
+ const [data] = args;
118
+ if (invalidates?.length) {
119
+ await Promise.all(invalidates.map((h) => h.invalidate()));
120
+ if (!_globalClient && contextClient) invalidates.forEach((h) => {
121
+ contextClient.invalidateQueries({ queryKey: h.query().queryKey });
122
+ });
123
+ }
124
+ if (onSuccess) await onSuccess(...args);
125
+ }
126
+ });
127
+ }
128
+ //#endregion
129
+ exports.useEidosMutation = useEidosMutation;
130
+ exports.useEidosQuery = useEidosQuery;
131
+ exports.withEidosQueryClient = withEidosQueryClient;