@sweidos/eidos 1.1.0 → 1.2.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.
- package/README.md +75 -0
- package/dist/action.js +167 -94
- package/dist/cli.js +102 -0
- package/dist/devtools.js +80 -20
- package/dist/eidos-sw.js +280 -188
- package/dist/eidos.cjs +2 -2
- package/dist/index.d.ts +99 -4
- package/dist/index.js +42 -39
- package/dist/push.cjs +120 -0
- package/dist/push.d.ts +28 -0
- package/dist/push.js +113 -0
- package/dist/runtime.js +37 -20
- package/dist/sw-bridge.js +44 -31
- package/dist/version.js +1 -1
- package/package.json +10 -2
package/dist/eidos.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});let
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});let m=require("react"),H=require("react/jsx-runtime");function ce(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 ue(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 w,D=new Set;function V(){D.forEach(e=>e())}function k(e){w={...w,...e(w)},V()}w={isOnline:typeof navigator>"u"||navigator.onLine!==!1,swStatus:"idle",swError:void 0,resources:{},queue:[],setOnline:e=>k(()=>({isOnline:e})),setSwStatus:(e,t)=>k(()=>({swStatus:e,swError:t})),...ce(k),...ue(k)};function de(){return w}function le(e){return D.add(e),()=>{D.delete(e)}}var u={getState:de,subscribe:le,setState:e=>{const t=typeof e=="function"?e(w):e;w={...w,...t},V()}},v=null,N=[];function G(){return v}async function fe(e){if(typeof navigator>"u"||!("serviceWorker"in navigator)){u.getState().setSwStatus("unsupported");return}const t=u.getState();t.setSwStatus("registering");try{v=await navigator.serviceWorker.register(e,{scope:"/"}),await pe(v),t.setSwStatus("active"),navigator.serviceWorker.addEventListener("message",we),window.addEventListener("online",()=>t.setOnline(!0)),window.addEventListener("offline",()=>t.setOnline(!1)),Se()}catch(n){t.setSwStatus("error",String(n))}}function pe(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 R(e){const t=v?.active;t?t.postMessage(e):N.push(e)}var Y=null;function he(e){Y=e}function ye(){try{return typeof navigator<"u"&&"serviceWorker"in navigator&&v!==null&&"sync"in v}catch{return!1}}var T={};function ge(e){T=e}function we(e){const t=e.data;if(!t?.type)return;const n=u.getState(),{type:r,url:s}=t;if(r==="EIDOS_BACKGROUND_SYNC"){Y?.();return}if(r==="EIDOS_NOTIFICATION_CLICK"){T.onNotificationClick?.(t.data);return}if(r==="EIDOS_SUBSCRIPTION_EXPIRED"){T.onSubscriptionExpired?.(t.subscription);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 ve(e){R({type:"EIDOS_SIMULATE_OFFLINE",enabled:e}),u.getState().setOnline(!e)}function Se(){const e=v?.active;if(e){for(const t of N)e.postMessage(t);N=[]}}var I=new Map,C=new Map,J=null;function me(e){J=e}function S(e){return e.includes("*")||/:[^/]+/.test(e)}function be(e){return"^"+e.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*\*/g,".+").replace(/\*/g,"[^/]+").replace(/:[^/]+/g,"[^/]+")+"$"}function _(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 Ee(e,t){if(I.has(e))return I.get(e);const n=Oe(t),r=S(e)?be(e):void 0,s={url:e,config:t,strategy:n,status:"idle",cacheHits:0,cacheMisses:0};u.getState().registerResource(e,s),R({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 _(e,"fetch");const i=C.get(e);if(i)return i.then(c=>c.clone());const o=Re(e,t,n);return C.set(e,o),o.finally(()=>C.delete(e)).catch(()=>{}),o.then(c=>c.clone())},json:async()=>{if(S(e))throw _(e,"json");return(await a.fetch()).json()},query:()=>{if(S(e))throw _(e,"query");return{queryKey:["eidos",e],queryFn:()=>a.json()}},prefetch:async()=>{if(S(e))throw _(e,"prefetch");await a.fetch()},invalidate:async()=>{R({type:"EIDOS_CLEAR_CACHE",url:e});const i=await caches.open(n.cacheName).catch(()=>null);if(i){const o=await i.keys(),c=r?new RegExp(r):null,d=e.startsWith("http");await Promise.all(o.filter(l=>{const f=l.url,y=new URL(f).pathname;return c?c.test(d?f:y):d?f===e:f===e||y===e}).map(l=>i.delete(l)))}S(e)||u.getState().updateResource(e,{status:"stale",cachedAt:void 0,lastEvent:"cache-cleared",cacheHits:0,cacheMisses:0}),J?.(["eidos",e])},unregister:()=>{I.delete(e),R({type:"EIDOS_UNREGISTER_RESOURCE",url:e}),u.getState().unregisterResource(e)}};return I.set(e,a),a}async function Re(e,t,n){const r=u.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 o=s?await s.match(e).catch(()=>null):null,c=u.getState().resources[e],d=t.maxAge!==void 0&&c?.cachedAt!==void 0&&Date.now()-c.cachedAt>t.maxAge;if(o&&!d)return r.updateResource(e,{status:"fresh",lastEvent:"cache-hit",cacheHits:(c?.cacheHits??0)+1}),n.swStrategy==="stale-while-revalidate"&&fetch(e,{signal:AbortSignal.timeout(5e3)}).then(async f=>{f.ok&&s&&(await s.put(e,f.clone()),u.getState().updateResource(e,{cachedAt:Date.now(),lastEvent:"cache-updated"}))}).catch(()=>{}),o;const l=u.getState().resources[e];r.updateResource(e,{cacheMisses:(l?.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 o=u.getState().resources[e];return r.updateResource(e,{status:"fresh",lastEvent:"cache-hit",cacheHits:(o?.cacheHits??0)+1}),i}throw r.updateResource(e,{status:"error"}),a}}function Oe(e){const t=e.strategy;return e.offline?$(t??"stale-while-revalidate",e.cacheName):$(t??"network-first",e.cacheName)}var ke={"stale-while-revalidate":"StaleWhileRevalidate","cache-first":"CacheFirst","network-first":"NetworkFirst"},Ie={"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
2
|
new StaleWhileRevalidate({
|
|
3
3
|
cacheName: 'eidos-resources-v1',
|
|
4
4
|
plugins: [new ExpirationPlugin({ maxEntries: 60 })],
|
|
@@ -10,6 +10,6 @@ new CacheFirst({
|
|
|
10
10
|
new NetworkFirst({
|
|
11
11
|
cacheName: 'eidos-resources-v1',
|
|
12
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;
|
|
13
|
+
})`}};function $(e,t){const n=Ie[e];return{name:ke[e],swStrategy:e,cacheName:t??"eidos-resources-v1",reasoning:n.reasoning,behavior:n.behavior,equivalentCode:""}}async function _e(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 Ae="eidos",qe=1,p="action-queue",x=null;function b(){return x?Promise.resolve(x):new Promise((e,t)=>{const n=indexedDB.open(Ae,qe);n.onupgradeneeded=r=>{const s=r.target.result;if(!s.objectStoreNames.contains(p)){const a=s.createObjectStore(p,{keyPath:"id"});a.createIndex("status","status",{unique:!1}),a.createIndex("actionId","actionId",{unique:!1})}},n.onsuccess=()=>{x=n.result,e(n.result)},n.onerror=()=>t(n.error)})}async function Ce(e){const t=await b();return new Promise((n,r)=>{const s=t.transaction(p,"readwrite");s.objectStore(p).add(e),s.oncomplete=()=>n(),s.onerror=()=>r(s.error)})}async function X(){const e=await b();return new Promise((t,n)=>{const r=e.transaction(p,"readonly").objectStore(p).getAll();r.onsuccess=()=>t(r.result),r.onerror=()=>n(r.error)})}async function xe(e,t){const n=await b();return new Promise((r,s)=>{const a=n.transaction(p,"readwrite"),i=a.objectStore(p),o=i.get(e);o.onsuccess=()=>{o.result&&i.put({...o.result,...t})},a.oncomplete=()=>r(),a.onerror=()=>s(a.error)})}async function Qe(e){const t=await b();return new Promise((n,r)=>{const s=t.transaction(p,"readwrite");s.objectStore(p).delete(e),s.oncomplete=()=>n(),s.onerror=()=>r(s.error)})}async function Pe(){const e=await b();function t(s){return new Promise((a,i)=>{const o=e.transaction(p,"readonly").objectStore(p).index("status"),c=[],d=o.openCursor(IDBKeyRange.only(s));d.onsuccess=l=>{const f=l.target.result;f?(c.push(f.value),f.continue()):a(c)},d.onerror=()=>i(d.error)})}const[n,r]=await Promise.all([t("pending"),t("failed")]);return[...n,...r]}async function De(){const e=await b();return new Promise((t,n)=>{const r=e.transaction(p,"readwrite");r.objectStore(p).clear(),r.oncomplete=()=>t(),r.onerror=()=>n(r.error)})}var z={add:Ce,getAll:X,getPending:Pe,update:xe,remove:Qe,clear:De},Z=null;function Ne(e){Z=e}function K(){return Z}var L=new Map,ee=new Map,te=new Map,ne=new Map,re=new Map,O=new Map;function h(){return K()??z}function j(){return crypto.randomUUID()}function M(e,t,n){return e(...t,n)}function Te(e,t){const n=t.name||e.name||j(),r=t.namespace?`${t.namespace}::${n}`:n;L.set(r,e),re.set(r,t),t.onRollback&&ee.set(r,t.onRollback),t.onConflict&&te.set(r,t.onConflict),t.conflict&&ne.set(r,t.conflict);const s=async(...i)=>{const{isOnline:o}=u.getState(),c=t.reliability==="neverLose"||t.cancellable,d=c?j():"";let l;if(t.cancellable){const y=new AbortController;O.set(d,y),l=y.signal}const f={idempotencyKey:d,attempt:0,signal:l};t.onOptimistic?.(...i,f);try{if(t.reliability==="neverLose"){if(!o)return B(r,r,i,t,d);try{return await M(e,i,f)}catch(y){if(se(y))throw y;return B(r,r,i,t,d)}}try{return c?await M(e,i,f):await e(...i)}catch(y){throw t.onRollback?.(...i),y}}finally{t.cancellable&&O.delete(d)}},a=async i=>{const o=O.get(i);if(o)return o.abort(),!0;const c=(await h().getAll()).find(d=>d.idempotencyKey===i&&d.status==="pending");return c?(u.getState().removeQueueItem(c.id),await h().remove(c.id),!0):!1};return Object.defineProperty(s,"id",{value:r,writable:!1}),Object.defineProperty(s,"config",{value:t,writable:!1}),Object.defineProperty(s,"cancel",{value:a,writable:!1}),s}async function B(e,t,n,r,s){const a=j(),i={schemaVersion:2,id:a,actionId:e,actionName:t,idempotencyKey:s,args:n,queuedAt:Date.now(),retryCount:0,maxRetries:r.maxRetries??3,status:"pending",priority:r.priority??"normal"};await h().add(i),u.getState().addQueueItem(i);try{const o=G();o&&"sync"in o&&await o.sync.register("eidos-queue-replay")}catch{}return{queued:!0,id:a,message:`"${t}" queued — will execute when online`}}function se(e){return e instanceof DOMException&&e.name==="AbortError"}function je(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 Me(e){return Math.min(2e3*2**e,3e5)*(.8+Math.random()*.4)}function A(){return{attempted:0,succeeded:0,failed:0,retrying:0,skipped:0,conflicted:0,cancelled:0}}var Q=!1,Ue="eidos-queue-replay";async function q(){const e=u.getState();if(!e.isOnline)return A();if(typeof navigator<"u"&&navigator.locks)return navigator.locks.request(Ue,{ifAvailable:!0},async t=>t?F(e):A());if(Q)return A();Q=!0;try{return await F(e)}finally{Q=!1}}async function We(e,t){const n=L.get(e.actionId);if(!n)return"skipped";const r=re.get(e.actionId)?.cancellable;let s;if(r){const i=new AbortController;O.set(e.idempotencyKey,i),s=i.signal}const a={idempotencyKey:e.idempotencyKey,attempt:e.retryCount,signal:s};try{await M(n,e.args,a);const i=Date.now();return t.updateQueueItem(e.id,{status:"succeeded",completedAt:i}),await h().update(e.id,{status:"succeeded",completedAt:i}),setTimeout(()=>{t.removeQueueItem(e.id),h().remove(e.id)},3e3),"succeeded"}catch(i){if(se(i))return t.removeQueueItem(e.id),await h().remove(e.id),"cancelled";if(je(i)){const c=ne.get(e.actionId);let d;if(c)switch(c.strategy){case"serverWins":d="skip";break;case"clientWins":case"lastWriteWins":d="retry";break;case"merge":case"custom":{const l={error:i,args:e.args,attempt:e.retryCount,idempotencyKey:e.idempotencyKey};d=c.resolve?.(l)??"retry";break}}else{const l=te.get(e.actionId);l&&(d=l(i,e.args))}if(d==="skip")return t.removeQueueItem(e.id),await h().remove(e.id),"conflicted";d&&typeof d=="object"&&(e.args=d.resolved,t.updateQueueItem(e.id,{args:d.resolved}),await h().update(e.id,{args:d.resolved}))}const o=e.retryCount+1;if(o>=e.maxRetries)return t.updateQueueItem(e.id,{status:"failed",error:String(i),retryCount:o}),await h().update(e.id,{status:"failed",error:String(i),retryCount:o}),ee.get(e.actionId)?.(...e.args),"failed";{const c=Date.now()+Me(o);return t.updateQueueItem(e.id,{status:"pending",retryCount:o,nextRetryAt:c}),await h().update(e.id,{status:"pending",retryCount:o,nextRetryAt:c}),"retrying"}}finally{r&&O.delete(e.idempotencyKey)}}async function Ke(e,t,n){if(e.length===0)return;const r=e.filter(a=>L.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=>We(a,t)));for(const a of s){const i=a.status==="fulfilled"?a.value:"failed";i==="skipped"?n.skipped++:i==="conflicted"?n.conflicted++:i==="cancelled"?n.cancelled++:(n.attempted++,n[i]++)}}async function F(e){const t=await h().getPending(),n=Date.now(),r=t.filter(a=>a.retryCount<a.maxRetries&&(!a.nextRetryAt||a.nextRetryAt<=n)),s=A();for(const a of["high","normal","low"])await Ke(r.filter(i=>(i.priority??"normal")===a),e,s);return s}async function Le(){await h().clear(),u.getState().hydrateQueue([])}function ae(){let e=u.getState().isOnline;const t=u.subscribe(()=>{const{isOnline:s}=u.getState(),a=s&&!e;e=s,a&&setTimeout(q,600)}),n=u.getState(),r=n.queue.some(s=>s.status==="pending");return n.isOnline&&r&&setTimeout(q,1200),t}async function He(e){if(e.schemaVersion===2&&e.idempotencyKey)return e;const t={...e,schemaVersion:2,idempotencyKey:e.idempotencyKey??crypto.randomUUID()};return await(K()??z).update(t.id,{schemaVersion:t.schemaVersion,idempotencyKey:t.idempotencyKey}).catch(()=>{}),t}var U=!1,W=null;async function ie(e={}){if(typeof window>"u"||U)return;U=!0;const t=e.swPath??"/eidos-sw.js",n=e.autoReplay??!0;try{const r=await X();if(r.length>0){const s=await Promise.all(r.map(He));u.getState().hydrateQueue(s)}}catch{}try{await fe(t)}catch{}he(()=>{u.getState().isOnline&&setTimeout(q,200)}),n&&(W=ae())}function $e(){W?.(),W=null,U=!1}var P="@eidos:queue",Be=class{constructor(e){this.storage=e}async readAll(){try{const e=await this.storage.getItem(P);return e?JSON.parse(e):[]}catch{return[]}}async writeAll(e){await this.storage.setItem(P,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(P)}};function Fe({children:e,swPath:t,autoReplay:n}){return(0,m.useEffect)(()=>{ie({swPath:t,autoReplay:n})},[]),(0,H.jsx)(H.Fragment,{children:e})}function g(e){const t=e??(n=>n);return(0,m.useSyncExternalStore)(u.subscribe,()=>t(u.getState()))}function Ve(){return g()}function Ge(){return g(e=>e.resources)}function Ye(e){return g(t=>t.resources[e])}function Je(){return g(e=>e.queue)}function Xe(e){return g(t=>t.queue.find(n=>n.id===e))}function ze(){return{isOnline:g(e=>e.isOnline),swStatus:g(e=>e.swStatus),swError:g(e=>e.swError)}}function Ze(){const[e,t,n,r]=g(s=>{let a=0,i=0,o=0;for(const c of s.queue)c.status==="pending"?a++:c.status==="failed"?i++:c.status==="replaying"&&o++;return`${a},${i},${o},${s.queue.length}`}).split(",");return{pending:+e,failed:+t,replaying:+n,total:+r}}function et(e){const t=g(s=>s.queue.length),n=(0,m.useRef)(0),r=(0,m.useRef)(e);(0,m.useEffect)(()=>{r.current=e}),(0,m.useEffect)(()=>{n.current>0&&t===0&&r.current(),n.current=t},[t])}var tt="1.2.0";function nt(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 oe(e,t){return nt(e,t)}function E(e,t=Object.is){return{subscribe(n){let r=e(u.getState());return n(r),u.subscribe(()=>{const s=e(u.getState());t(r,s)||(r=s,n(s))})},getState(){return e(u.getState())}}}var rt=E(e=>e),st=E(e=>e.queue),at=E(e=>({isOnline:e.isOnline,swStatus:e.swStatus,swError:e.swError}),oe),it=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}},oe);function ot(e){return E(t=>t.resources[e])}function ct(e){return E(t=>t.queue.find(n=>n.id===e))}exports.AsyncStorageQueueStorage=Be;exports.EidosProvider=Fe;exports.VERSION=tt;exports._getQueueStorage=K;exports._resetEidos=$e;exports.action=Te;exports.clearQueue=Le;exports.eidosAction=ct;exports.eidosQueue=st;exports.eidosQueueStats=it;exports.eidosResource=ot;exports.eidosStatus=at;exports.eidosStore=rt;exports.getSwRegistration=G;exports.initEidos=ie;exports.isBgSyncSupported=ye;exports.registerPushCallbacks=ge;exports.replayQueue=q;exports.resource=Ee;exports.sendToWorker=R;exports.setOfflineSimulation=ve;exports.setQueryInvalidator=me;exports.setQueueStorage=Ne;exports.subscribeReplayOnReconnect=ae;exports.useEidos=Ve;exports.useEidosAction=Xe;exports.useEidosOnDrain=et;exports.useEidosQueue=Je;exports.useEidosQueueStats=Ze;exports.useEidosResource=Ye;exports.useEidosResources=Ge;exports.useEidosStatus=ze;exports.useEidosStore=u;exports.warmCache=_e;
|
|
14
14
|
|
|
15
15
|
//# sourceMappingURL=eidos.cjs.map
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,13 @@ export declare interface ActionConfig {
|
|
|
13
13
|
maxRetries?: number;
|
|
14
14
|
/** Human-readable name for the action (used in devtools). */
|
|
15
15
|
name?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Prefixes the registered action id (`namespace::name`). Use to avoid
|
|
18
|
+
* collisions when two actions share a name (e.g. across micro-frontends,
|
|
19
|
+
* or two `createOrder` actions in different modules) — without a
|
|
20
|
+
* namespace, the second registration silently overwrites the first.
|
|
21
|
+
*/
|
|
22
|
+
namespace?: string;
|
|
16
23
|
/**
|
|
17
24
|
* Replay order when multiple queued actions are pending.
|
|
18
25
|
* `'high'` items replay before `'normal'`, which replay before `'low'`.
|
|
@@ -21,9 +28,11 @@ export declare interface ActionConfig {
|
|
|
21
28
|
*/
|
|
22
29
|
priority?: 'high' | 'normal' | 'low';
|
|
23
30
|
/**
|
|
24
|
-
* Called immediately before the async function executes, with the same args
|
|
25
|
-
* Use to apply an optimistic UI update (add
|
|
26
|
-
*
|
|
31
|
+
* Called immediately before the async function executes, with the same args
|
|
32
|
+
* plus a trailing `ActionContext`. Use to apply an optimistic UI update (add
|
|
33
|
+
* item, mark as pending, etc.) and to capture `idempotencyKey` for later
|
|
34
|
+
* `handle.cancel(idempotencyKey)` calls. Called on every invocation —
|
|
35
|
+
* online, offline, and during queue replay.
|
|
27
36
|
*/
|
|
28
37
|
onOptimistic?: (...args: any[]) => void;
|
|
29
38
|
/**
|
|
@@ -46,7 +55,25 @@ export declare interface ActionConfig {
|
|
|
46
55
|
* The `error` argument is whatever `fn` threw — typically a `Response` object
|
|
47
56
|
* or a custom error with a `.status` property.
|
|
48
57
|
*/
|
|
58
|
+
/**
|
|
59
|
+
* @deprecated Use `conflict` instead. If both are set, `conflict` wins.
|
|
60
|
+
* Return `'retry'` to keep the item in the queue and retry per normal
|
|
61
|
+
* backoff, or `'skip'` to silently remove the item.
|
|
62
|
+
*/
|
|
49
63
|
onConflict?: (error: unknown, args: any[]) => 'retry' | 'skip';
|
|
64
|
+
/**
|
|
65
|
+
* Declarative conflict-resolution strategy used during queue replay when
|
|
66
|
+
* the server responds with a 4xx status (conflict, gone, unprocessable,
|
|
67
|
+
* etc.). Replaces `onConflict` for new code — see `ConflictConfig`.
|
|
68
|
+
*/
|
|
69
|
+
conflict?: ConflictConfig;
|
|
70
|
+
/**
|
|
71
|
+
* When `true`, each invocation gets an `AbortController` whose `signal` is
|
|
72
|
+
* passed via `ActionContext.signal`. Forward it to `fetch`/etc. so
|
|
73
|
+
* `handle.cancel(idempotencyKey)` can abort an in-flight call, or remove a
|
|
74
|
+
* not-yet-replayed queued item.
|
|
75
|
+
*/
|
|
76
|
+
cancellable?: boolean;
|
|
50
77
|
}
|
|
51
78
|
|
|
52
79
|
declare type ActionFn<TArgs extends any[], TReturn> = (...args: TArgs) => Promise<TReturn>;
|
|
@@ -55,13 +82,28 @@ export declare interface ActionHandle<TArgs extends any[], TReturn> {
|
|
|
55
82
|
(...args: TArgs): Promise<TReturn | QueuedResult>;
|
|
56
83
|
readonly id: string;
|
|
57
84
|
readonly config: ActionConfig;
|
|
85
|
+
/**
|
|
86
|
+
* Cancel an invocation by its `idempotencyKey` (from `ActionContext` /
|
|
87
|
+
* `onOptimistic`). Aborts the in-flight call if `cancellable: true` and
|
|
88
|
+
* still running, otherwise removes a not-yet-replayed queued item.
|
|
89
|
+
* Returns `true` if something was cancelled/removed.
|
|
90
|
+
*/
|
|
91
|
+
cancel: (idempotencyKey: string) => Promise<boolean>;
|
|
58
92
|
}
|
|
59
93
|
|
|
60
94
|
export declare interface ActionQueueItem {
|
|
95
|
+
/** Shape version this item was persisted with. Items from before v2 are migrated on load. */
|
|
96
|
+
schemaVersion: number;
|
|
61
97
|
id: string;
|
|
62
98
|
/** ID of the registered action (maps to the function in the registry). */
|
|
63
99
|
actionId: string;
|
|
64
100
|
actionName: string;
|
|
101
|
+
/**
|
|
102
|
+
* Stable per-invocation key, generated once and reused across every retry/replay
|
|
103
|
+
* of this item. Pass to your server as an idempotency key so retries that reach
|
|
104
|
+
* the server after a dropped response don't double-execute.
|
|
105
|
+
*/
|
|
106
|
+
idempotencyKey: string;
|
|
65
107
|
args: unknown[];
|
|
66
108
|
queuedAt: number;
|
|
67
109
|
retryCount: number;
|
|
@@ -105,6 +147,46 @@ export declare type CacheStrategy = 'cache-first' | 'stale-while-revalidate' | '
|
|
|
105
147
|
/** Remove all items from the action queue (storage + in-memory store). */
|
|
106
148
|
export declare function clearQueue(): Promise<void>;
|
|
107
149
|
|
|
150
|
+
declare interface ConflictConfig {
|
|
151
|
+
/**
|
|
152
|
+
* - `'serverWins'`: drop the queued item, keeping the server's state.
|
|
153
|
+
* - `'clientWins'`: keep retrying — the client's write should eventually
|
|
154
|
+
* be accepted (e.g. once the server-side conflict is cleared).
|
|
155
|
+
* - `'lastWriteWins'`: same as `'clientWins'` for now — requires a
|
|
156
|
+
* server-supplied timestamp contract to compare against `queuedAt`
|
|
157
|
+
* (see Phase 3 of the roadmap). Treated as `'clientWins'` until then.
|
|
158
|
+
* - `'merge'` / `'custom'`: call `resolve` to decide.
|
|
159
|
+
*/
|
|
160
|
+
strategy: 'serverWins' | 'clientWins' | 'lastWriteWins' | 'merge' | 'custom';
|
|
161
|
+
/** Required for `'merge'` and `'custom'`. */
|
|
162
|
+
resolve?: (ctx: ConflictContext) => ConflictResolution;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Passed to `ConflictConfig.resolve` (for `'merge'`/`'custom'` strategies)
|
|
167
|
+
* when a queued action's replay receives a 4xx response.
|
|
168
|
+
*/
|
|
169
|
+
declare interface ConflictContext {
|
|
170
|
+
/** Whatever `fn` threw — typically a `Response` or an error with `.status`. */
|
|
171
|
+
error: unknown;
|
|
172
|
+
/** The original arguments the action was queued with. */
|
|
173
|
+
args: any[];
|
|
174
|
+
/** Number of replay attempts so far (0 on first replay). */
|
|
175
|
+
attempt: number;
|
|
176
|
+
idempotencyKey: string;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Outcome of `ConflictConfig.resolve`:
|
|
181
|
+
* - `'retry'`: keep the item queued, retry per normal backoff.
|
|
182
|
+
* - `'skip'`: silently remove the item (no `onRollback`).
|
|
183
|
+
* - `{ resolved: args }`: replace the queued args and retry immediately
|
|
184
|
+
* on the next replay pass.
|
|
185
|
+
*/
|
|
186
|
+
declare type ConflictResolution = 'retry' | 'skip' | {
|
|
187
|
+
resolved: any[];
|
|
188
|
+
};
|
|
189
|
+
|
|
108
190
|
/**
|
|
109
191
|
* Live state for a single queue item by ID. Returns `undefined` once the item
|
|
110
192
|
* is removed from the queue (after a successful replay or `clearQueue()`).
|
|
@@ -209,12 +291,19 @@ export declare function _getQueueStorage(): QueueStorage | null;
|
|
|
209
291
|
|
|
210
292
|
declare function _getState(): EidosStore;
|
|
211
293
|
|
|
294
|
+
export declare function getSwRegistration(): ServiceWorkerRegistration | null;
|
|
295
|
+
|
|
212
296
|
export declare function initEidos(config?: EidosConfig): Promise<void>;
|
|
213
297
|
|
|
214
298
|
export declare function isBgSyncSupported(): boolean;
|
|
215
299
|
|
|
216
300
|
declare type Listener = () => void;
|
|
217
301
|
|
|
302
|
+
declare interface PushHandlers {
|
|
303
|
+
onNotificationClick?: (data: unknown) => void;
|
|
304
|
+
onSubscriptionExpired?: (sub: PushSubscriptionJSON) => void;
|
|
305
|
+
}
|
|
306
|
+
|
|
218
307
|
declare type QueryInvalidator = (queryKey: [string, string]) => void;
|
|
219
308
|
|
|
220
309
|
declare interface QueueActions {
|
|
@@ -243,6 +332,8 @@ export declare interface QueueStorage {
|
|
|
243
332
|
clear(): Promise<void>;
|
|
244
333
|
}
|
|
245
334
|
|
|
335
|
+
export declare function registerPushCallbacks(handlers: PushHandlers): void;
|
|
336
|
+
|
|
246
337
|
export declare function replayQueue(): Promise<ReplayResult>;
|
|
247
338
|
|
|
248
339
|
/** Summary returned by replayQueue(). */
|
|
@@ -259,6 +350,8 @@ export declare interface ReplayResult {
|
|
|
259
350
|
skipped: number;
|
|
260
351
|
/** Items that received a 4xx response and were dropped via `onConflict: () => 'skip'`. */
|
|
261
352
|
conflicted: number;
|
|
353
|
+
/** Items removed via `handle.cancel(idempotencyKey)` before/during replay. */
|
|
354
|
+
cancelled: number;
|
|
262
355
|
}
|
|
263
356
|
|
|
264
357
|
export declare function _resetEidos(): void;
|
|
@@ -311,6 +404,8 @@ export declare interface ResourceHandle<T = unknown> {
|
|
|
311
404
|
unregister: () => void;
|
|
312
405
|
}
|
|
313
406
|
|
|
407
|
+
export declare function sendToWorker(message: Record<string, unknown>): void;
|
|
408
|
+
|
|
314
409
|
export declare function setOfflineSimulation(enabled: boolean): void;
|
|
315
410
|
|
|
316
411
|
/* Excluded from this release type: setQueryInvalidator */
|
|
@@ -394,7 +489,7 @@ export declare const useEidosStore: {
|
|
|
394
489
|
setState: (partial: Partial<EidosStore> | ((s: EidosStore) => Partial<EidosStore>)) => void;
|
|
395
490
|
};
|
|
396
491
|
|
|
397
|
-
export declare const VERSION = "1.
|
|
492
|
+
export declare const VERSION = "1.2.0";
|
|
398
493
|
|
|
399
494
|
/**
|
|
400
495
|
* Bulk-prefetch an array of resource handles concurrently, warming the cache
|
package/dist/index.js
CHANGED
|
@@ -1,45 +1,48 @@
|
|
|
1
1
|
import { useEidosStore as o } from "./store.js";
|
|
2
|
-
import {
|
|
3
|
-
import { resource as
|
|
4
|
-
import { _getQueueStorage as
|
|
5
|
-
import { action as
|
|
6
|
-
import { subscribeReplayOnReconnect as
|
|
7
|
-
import { _resetEidos as
|
|
8
|
-
import { AsyncStorageQueueStorage as
|
|
9
|
-
import { EidosProvider as
|
|
10
|
-
import { useEidos as
|
|
11
|
-
import { VERSION as
|
|
12
|
-
import { eidosAction as
|
|
2
|
+
import { getSwRegistration as s, isBgSyncSupported as i, registerPushCallbacks as t, sendToWorker as u, setOfflineSimulation as m } from "./sw-bridge.js";
|
|
3
|
+
import { resource as a, setQueryInvalidator as p, warmCache as n } from "./resource.js";
|
|
4
|
+
import { _getQueueStorage as S, setQueueStorage as f } from "./queue-storage.js";
|
|
5
|
+
import { action as Q, clearQueue as g, replayQueue as l } from "./action.js";
|
|
6
|
+
import { subscribeReplayOnReconnect as y } from "./replay.js";
|
|
7
|
+
import { _resetEidos as b, initEidos as A } from "./runtime.js";
|
|
8
|
+
import { AsyncStorageQueueStorage as k } from "./async-storage-adapter.js";
|
|
9
|
+
import { EidosProvider as w } from "./react/Provider.js";
|
|
10
|
+
import { useEidos as I, useEidosAction as P, useEidosOnDrain as _, useEidosQueue as x, useEidosQueueStats as B, useEidosResource as D, useEidosResources as N, useEidosStatus as T } from "./react/hooks.js";
|
|
11
|
+
import { VERSION as W } from "./version.js";
|
|
12
|
+
import { eidosAction as q, eidosQueue as z, eidosQueueStats as F, eidosResource as G, eidosStatus as H, eidosStore as J } from "./stores.js";
|
|
13
13
|
export {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
14
|
+
k as AsyncStorageQueueStorage,
|
|
15
|
+
w as EidosProvider,
|
|
16
|
+
W as VERSION,
|
|
17
|
+
S as _getQueueStorage,
|
|
18
|
+
b as _resetEidos,
|
|
19
|
+
Q as action,
|
|
20
|
+
g as clearQueue,
|
|
21
|
+
q as eidosAction,
|
|
22
|
+
z as eidosQueue,
|
|
23
|
+
F as eidosQueueStats,
|
|
24
|
+
G as eidosResource,
|
|
25
|
+
H as eidosStatus,
|
|
26
|
+
J as eidosStore,
|
|
27
|
+
s as getSwRegistration,
|
|
28
|
+
A as initEidos,
|
|
29
|
+
i as isBgSyncSupported,
|
|
30
|
+
t as registerPushCallbacks,
|
|
31
|
+
l as replayQueue,
|
|
32
|
+
a as resource,
|
|
33
|
+
u as sendToWorker,
|
|
34
|
+
m as setOfflineSimulation,
|
|
35
|
+
p as setQueryInvalidator,
|
|
36
|
+
f as setQueueStorage,
|
|
37
|
+
y as subscribeReplayOnReconnect,
|
|
38
|
+
I as useEidos,
|
|
39
|
+
P as useEidosAction,
|
|
40
|
+
_ as useEidosOnDrain,
|
|
38
41
|
x as useEidosQueue,
|
|
39
42
|
B as useEidosQueueStats,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
D as useEidosResource,
|
|
44
|
+
N as useEidosResources,
|
|
45
|
+
T as useEidosStatus,
|
|
43
46
|
o as useEidosStore,
|
|
44
|
-
|
|
47
|
+
n as warmCache
|
|
45
48
|
};
|
package/dist/push.cjs
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _sweidos_eidos = require("@sweidos/eidos");
|
|
3
|
+
//#region src/push.ts
|
|
4
|
+
/**
|
|
5
|
+
* @sweidos/eidos/push
|
|
6
|
+
*
|
|
7
|
+
* Web Push integration. Framework-agnostic: register click/expiry handlers
|
|
8
|
+
* once at app init (any tab), and trigger subscription from a user gesture.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { registerPushHandlers, subscribeToPush } from '@sweidos/eidos/push'
|
|
13
|
+
*
|
|
14
|
+
* // App init — every tab, no permission prompt
|
|
15
|
+
* registerPushHandlers({
|
|
16
|
+
* onNotificationClick: (data) => router.push(data.url),
|
|
17
|
+
* onSubscriptionExpired: (sub) => fetch('/api/push-subscribe', { method: 'POST', body: JSON.stringify(sub) }),
|
|
18
|
+
* })
|
|
19
|
+
*
|
|
20
|
+
* // User gesture (button click)
|
|
21
|
+
* const result = await subscribeToPush({
|
|
22
|
+
* vapidPublicKey: import.meta.env.VITE_EIDOS_VAPID_PUBLIC_KEY,
|
|
23
|
+
* onSubscribe: (sub) => fetch('/api/push-subscribe', { method: 'POST', body: JSON.stringify(sub) }),
|
|
24
|
+
* })
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
/** Why push is unavailable on this device, or null if it's supported. */
|
|
28
|
+
function getPushUnsupportedReason() {
|
|
29
|
+
if (typeof window === "undefined") return "no-push-api";
|
|
30
|
+
if (/iPhone|iPad|iPod/.test(navigator.userAgent) && !navigator.standalone) return "ios-not-installed";
|
|
31
|
+
if (!("serviceWorker" in navigator) || !("PushManager" in window) || !("Notification" in window)) return "no-push-api";
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function isPushSupported() {
|
|
35
|
+
return getPushUnsupportedReason() === null;
|
|
36
|
+
}
|
|
37
|
+
function getPushPermissionState() {
|
|
38
|
+
if (!isPushSupported()) return "unsupported";
|
|
39
|
+
return Notification.permission;
|
|
40
|
+
}
|
|
41
|
+
function registerPushHandlers(handlers) {
|
|
42
|
+
(0, _sweidos_eidos.registerPushCallbacks)(handlers);
|
|
43
|
+
}
|
|
44
|
+
async function subscribeToPush(config) {
|
|
45
|
+
if (!isPushSupported()) return { status: "unsupported" };
|
|
46
|
+
let permission;
|
|
47
|
+
try {
|
|
48
|
+
permission = await Notification.requestPermission();
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return {
|
|
51
|
+
status: "error",
|
|
52
|
+
error
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (permission !== "granted") return { status: "denied" };
|
|
56
|
+
const registration = (0, _sweidos_eidos.getSwRegistration)();
|
|
57
|
+
if (!registration) return { status: "sw-not-ready" };
|
|
58
|
+
try {
|
|
59
|
+
const applicationServerKey = urlBase64ToUint8Array(config.vapidPublicKey);
|
|
60
|
+
let subscription = await registration.pushManager.getSubscription();
|
|
61
|
+
if (subscription && !subscriptionKeyMatches(subscription, config.vapidPublicKey)) {
|
|
62
|
+
await subscription.unsubscribe();
|
|
63
|
+
subscription = null;
|
|
64
|
+
}
|
|
65
|
+
if (!subscription) subscription = await registration.pushManager.subscribe({
|
|
66
|
+
userVisibleOnly: true,
|
|
67
|
+
applicationServerKey
|
|
68
|
+
});
|
|
69
|
+
(0, _sweidos_eidos.sendToWorker)({
|
|
70
|
+
type: "EIDOS_CACHE_VAPID_KEY",
|
|
71
|
+
key: config.vapidPublicKey
|
|
72
|
+
});
|
|
73
|
+
const json = subscription.toJSON();
|
|
74
|
+
config.onSubscribe?.(json);
|
|
75
|
+
return {
|
|
76
|
+
status: "subscribed",
|
|
77
|
+
subscription: json
|
|
78
|
+
};
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return {
|
|
81
|
+
status: "error",
|
|
82
|
+
error
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function unsubscribeFromPush() {
|
|
87
|
+
const registration = (0, _sweidos_eidos.getSwRegistration)();
|
|
88
|
+
if (!registration) return;
|
|
89
|
+
await (await registration.pushManager.getSubscription())?.unsubscribe();
|
|
90
|
+
}
|
|
91
|
+
async function getCurrentPushSubscription() {
|
|
92
|
+
const registration = (0, _sweidos_eidos.getSwRegistration)();
|
|
93
|
+
if (!registration) return null;
|
|
94
|
+
const subscription = await registration.pushManager.getSubscription();
|
|
95
|
+
return subscription ? subscription.toJSON() : null;
|
|
96
|
+
}
|
|
97
|
+
function urlBase64ToUint8Array(base64Url) {
|
|
98
|
+
const base64 = (base64Url + "=".repeat((4 - base64Url.length % 4) % 4)).replace(/-/g, "+").replace(/_/g, "/");
|
|
99
|
+
const raw = atob(base64);
|
|
100
|
+
return Uint8Array.from(raw, (c) => c.charCodeAt(0));
|
|
101
|
+
}
|
|
102
|
+
function uint8ArrayToUrlBase64(bytes) {
|
|
103
|
+
let binary = "";
|
|
104
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
105
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
106
|
+
}
|
|
107
|
+
/** Compares an existing subscription's key against the configured VAPID public key. */
|
|
108
|
+
function subscriptionKeyMatches(subscription, vapidPublicKey) {
|
|
109
|
+
const key = subscription.options.applicationServerKey;
|
|
110
|
+
if (!key) return false;
|
|
111
|
+
return uint8ArrayToUrlBase64(new Uint8Array(key)) === uint8ArrayToUrlBase64(urlBase64ToUint8Array(vapidPublicKey));
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
exports.getCurrentPushSubscription = getCurrentPushSubscription;
|
|
115
|
+
exports.getPushPermissionState = getPushPermissionState;
|
|
116
|
+
exports.getPushUnsupportedReason = getPushUnsupportedReason;
|
|
117
|
+
exports.isPushSupported = isPushSupported;
|
|
118
|
+
exports.registerPushHandlers = registerPushHandlers;
|
|
119
|
+
exports.subscribeToPush = subscribeToPush;
|
|
120
|
+
exports.unsubscribeFromPush = unsubscribeFromPush;
|
package/dist/push.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface PushHandlers {
|
|
2
|
+
/** Fired when the user clicks a notification while a tab is open. */
|
|
3
|
+
onNotificationClick?: (data: unknown) => void;
|
|
4
|
+
/** Fired when the browser silently rotates the push subscription. Re-send to your backend. */
|
|
5
|
+
onSubscriptionExpired?: (sub: PushSubscriptionJSON) => void;
|
|
6
|
+
}
|
|
7
|
+
export interface PushConfig {
|
|
8
|
+
/** Base64url-encoded VAPID public key. Generate with `npx @sweidos/eidos generate-vapid-keys`. */
|
|
9
|
+
vapidPublicKey: string;
|
|
10
|
+
/** Called with the new subscription right after a successful subscribe. Send this to your backend. */
|
|
11
|
+
onSubscribe?: (sub: PushSubscriptionJSON) => void;
|
|
12
|
+
}
|
|
13
|
+
export type PushResult = {
|
|
14
|
+
status: 'subscribed';
|
|
15
|
+
subscription: PushSubscriptionJSON;
|
|
16
|
+
} | {
|
|
17
|
+
status: 'denied' | 'unsupported' | 'sw-not-ready' | 'error';
|
|
18
|
+
error?: unknown;
|
|
19
|
+
};
|
|
20
|
+
export type PushUnsupportedReason = 'no-push-api' | 'ios-not-installed' | null;
|
|
21
|
+
/** Why push is unavailable on this device, or null if it's supported. */
|
|
22
|
+
export declare function getPushUnsupportedReason(): PushUnsupportedReason;
|
|
23
|
+
export declare function isPushSupported(): boolean;
|
|
24
|
+
export declare function getPushPermissionState(): NotificationPermission | 'unsupported';
|
|
25
|
+
export declare function registerPushHandlers(handlers: PushHandlers): void;
|
|
26
|
+
export declare function subscribeToPush(config: PushConfig): Promise<PushResult>;
|
|
27
|
+
export declare function unsubscribeFromPush(): Promise<void>;
|
|
28
|
+
export declare function getCurrentPushSubscription(): Promise<PushSubscriptionJSON | null>;
|
package/dist/push.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { getSwRegistration, registerPushCallbacks, sendToWorker } from "@sweidos/eidos";
|
|
2
|
+
//#region src/push.ts
|
|
3
|
+
/**
|
|
4
|
+
* @sweidos/eidos/push
|
|
5
|
+
*
|
|
6
|
+
* Web Push integration. Framework-agnostic: register click/expiry handlers
|
|
7
|
+
* once at app init (any tab), and trigger subscription from a user gesture.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { registerPushHandlers, subscribeToPush } from '@sweidos/eidos/push'
|
|
12
|
+
*
|
|
13
|
+
* // App init — every tab, no permission prompt
|
|
14
|
+
* registerPushHandlers({
|
|
15
|
+
* onNotificationClick: (data) => router.push(data.url),
|
|
16
|
+
* onSubscriptionExpired: (sub) => fetch('/api/push-subscribe', { method: 'POST', body: JSON.stringify(sub) }),
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* // User gesture (button click)
|
|
20
|
+
* const result = await subscribeToPush({
|
|
21
|
+
* vapidPublicKey: import.meta.env.VITE_EIDOS_VAPID_PUBLIC_KEY,
|
|
22
|
+
* onSubscribe: (sub) => fetch('/api/push-subscribe', { method: 'POST', body: JSON.stringify(sub) }),
|
|
23
|
+
* })
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
/** Why push is unavailable on this device, or null if it's supported. */
|
|
27
|
+
function getPushUnsupportedReason() {
|
|
28
|
+
if (typeof window === "undefined") return "no-push-api";
|
|
29
|
+
if (/iPhone|iPad|iPod/.test(navigator.userAgent) && !navigator.standalone) return "ios-not-installed";
|
|
30
|
+
if (!("serviceWorker" in navigator) || !("PushManager" in window) || !("Notification" in window)) return "no-push-api";
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function isPushSupported() {
|
|
34
|
+
return getPushUnsupportedReason() === null;
|
|
35
|
+
}
|
|
36
|
+
function getPushPermissionState() {
|
|
37
|
+
if (!isPushSupported()) return "unsupported";
|
|
38
|
+
return Notification.permission;
|
|
39
|
+
}
|
|
40
|
+
function registerPushHandlers(handlers) {
|
|
41
|
+
registerPushCallbacks(handlers);
|
|
42
|
+
}
|
|
43
|
+
async function subscribeToPush(config) {
|
|
44
|
+
if (!isPushSupported()) return { status: "unsupported" };
|
|
45
|
+
let permission;
|
|
46
|
+
try {
|
|
47
|
+
permission = await Notification.requestPermission();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return {
|
|
50
|
+
status: "error",
|
|
51
|
+
error
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (permission !== "granted") return { status: "denied" };
|
|
55
|
+
const registration = getSwRegistration();
|
|
56
|
+
if (!registration) return { status: "sw-not-ready" };
|
|
57
|
+
try {
|
|
58
|
+
const applicationServerKey = urlBase64ToUint8Array(config.vapidPublicKey);
|
|
59
|
+
let subscription = await registration.pushManager.getSubscription();
|
|
60
|
+
if (subscription && !subscriptionKeyMatches(subscription, config.vapidPublicKey)) {
|
|
61
|
+
await subscription.unsubscribe();
|
|
62
|
+
subscription = null;
|
|
63
|
+
}
|
|
64
|
+
if (!subscription) subscription = await registration.pushManager.subscribe({
|
|
65
|
+
userVisibleOnly: true,
|
|
66
|
+
applicationServerKey
|
|
67
|
+
});
|
|
68
|
+
sendToWorker({
|
|
69
|
+
type: "EIDOS_CACHE_VAPID_KEY",
|
|
70
|
+
key: config.vapidPublicKey
|
|
71
|
+
});
|
|
72
|
+
const json = subscription.toJSON();
|
|
73
|
+
config.onSubscribe?.(json);
|
|
74
|
+
return {
|
|
75
|
+
status: "subscribed",
|
|
76
|
+
subscription: json
|
|
77
|
+
};
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return {
|
|
80
|
+
status: "error",
|
|
81
|
+
error
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function unsubscribeFromPush() {
|
|
86
|
+
const registration = getSwRegistration();
|
|
87
|
+
if (!registration) return;
|
|
88
|
+
await (await registration.pushManager.getSubscription())?.unsubscribe();
|
|
89
|
+
}
|
|
90
|
+
async function getCurrentPushSubscription() {
|
|
91
|
+
const registration = getSwRegistration();
|
|
92
|
+
if (!registration) return null;
|
|
93
|
+
const subscription = await registration.pushManager.getSubscription();
|
|
94
|
+
return subscription ? subscription.toJSON() : null;
|
|
95
|
+
}
|
|
96
|
+
function urlBase64ToUint8Array(base64Url) {
|
|
97
|
+
const base64 = (base64Url + "=".repeat((4 - base64Url.length % 4) % 4)).replace(/-/g, "+").replace(/_/g, "/");
|
|
98
|
+
const raw = atob(base64);
|
|
99
|
+
return Uint8Array.from(raw, (c) => c.charCodeAt(0));
|
|
100
|
+
}
|
|
101
|
+
function uint8ArrayToUrlBase64(bytes) {
|
|
102
|
+
let binary = "";
|
|
103
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
104
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
105
|
+
}
|
|
106
|
+
/** Compares an existing subscription's key against the configured VAPID public key. */
|
|
107
|
+
function subscriptionKeyMatches(subscription, vapidPublicKey) {
|
|
108
|
+
const key = subscription.options.applicationServerKey;
|
|
109
|
+
if (!key) return false;
|
|
110
|
+
return uint8ArrayToUrlBase64(new Uint8Array(key)) === uint8ArrayToUrlBase64(urlBase64ToUint8Array(vapidPublicKey));
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
export { getCurrentPushSubscription, getPushPermissionState, getPushUnsupportedReason, isPushSupported, registerPushHandlers, subscribeToPush, unsubscribeFromPush };
|