@sweidos/eidos 1.1.0 → 2.0.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/dist/index.js CHANGED
@@ -1,45 +1,49 @@
1
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";
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 d, resourcePattern as n, setQueryInvalidator as p, warmCache as c } from "./resource.js";
4
+ import { _getQueueStorage as f, setQueueStorage as E } from "./queue-storage.js";
5
+ import { action as g, clearQueue as l, replayQueue as R } from "./action.js";
6
+ import { subscribeReplayOnReconnect as O } from "./replay.js";
7
+ import { _resetEidos as A, initEidos as P } 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 _, useEidosOnDrain as x, useEidosQueue as B, useEidosQueueStats as D, useEidosResource as N, useEidosResources as T, useEidosStatus as V } from "./react/hooks.js";
11
+ import { VERSION as j } from "./version.js";
12
+ import { eidosAction as z, eidosQueue as F, eidosQueueStats as G, eidosResource as H, eidosStatus as J, eidosStore as K } from "./stores.js";
13
13
  export {
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,
33
- c as setQueueStorage,
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,
14
+ k as AsyncStorageQueueStorage,
15
+ w as EidosProvider,
16
+ j as VERSION,
17
+ f as _getQueueStorage,
18
+ A as _resetEidos,
19
+ g as action,
20
+ l as clearQueue,
21
+ z as eidosAction,
22
+ F as eidosQueue,
23
+ G as eidosQueueStats,
24
+ H as eidosResource,
25
+ J as eidosStatus,
26
+ K as eidosStore,
27
+ s as getSwRegistration,
28
+ P as initEidos,
29
+ i as isBgSyncSupported,
30
+ t as registerPushCallbacks,
31
+ R as replayQueue,
32
+ d as resource,
33
+ n as resourcePattern,
34
+ u as sendToWorker,
35
+ m as setOfflineSimulation,
36
+ p as setQueryInvalidator,
37
+ E as setQueueStorage,
38
+ O as subscribeReplayOnReconnect,
39
+ I as useEidos,
40
+ _ as useEidosAction,
41
+ x as useEidosOnDrain,
42
+ B as useEidosQueue,
43
+ D as useEidosQueueStats,
44
+ N as useEidosResource,
45
+ T as useEidosResources,
46
+ V as useEidosStatus,
43
47
  o as useEidosStore,
44
- d as warmCache
48
+ c as warmCache
45
49
  };
package/dist/push.cjs ADDED
@@ -0,0 +1,123 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _sweidos_eidos = require("@sweidos/eidos");
3
+ //#region src/internal/url-base64.ts
4
+ /** Decodes a base64url string (e.g. a VAPID public key) into raw bytes. */
5
+ function urlBase64ToUint8Array(base64Url) {
6
+ const base64 = (base64Url + "=".repeat((4 - base64Url.length % 4) % 4)).replace(/-/g, "+").replace(/_/g, "/");
7
+ const raw = atob(base64);
8
+ return Uint8Array.from(raw, (c) => c.charCodeAt(0));
9
+ }
10
+ //#endregion
11
+ //#region src/push.ts
12
+ /**
13
+ * @sweidos/eidos/push
14
+ *
15
+ * Web Push integration. Framework-agnostic: register click/expiry handlers
16
+ * once at app init (any tab), and trigger subscription from a user gesture.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import { registerPushHandlers, subscribeToPush } from '@sweidos/eidos/push'
21
+ *
22
+ * // App init — every tab, no permission prompt
23
+ * registerPushHandlers({
24
+ * onNotificationClick: (data) => router.push(data.url),
25
+ * onSubscriptionExpired: (sub) => fetch('/api/push-subscribe', { method: 'POST', body: JSON.stringify(sub) }),
26
+ * })
27
+ *
28
+ * // User gesture (button click)
29
+ * const result = await subscribeToPush({
30
+ * vapidPublicKey: import.meta.env.VITE_EIDOS_VAPID_PUBLIC_KEY,
31
+ * onSubscribe: (sub) => fetch('/api/push-subscribe', { method: 'POST', body: JSON.stringify(sub) }),
32
+ * })
33
+ * ```
34
+ */
35
+ /** Why push is unavailable on this device, or null if it's supported. */
36
+ function getPushUnsupportedReason() {
37
+ if (typeof window === "undefined") return "no-push-api";
38
+ if (/iPhone|iPad|iPod/.test(navigator.userAgent) && !navigator.standalone) return "ios-not-installed";
39
+ if (!("serviceWorker" in navigator) || !("PushManager" in window) || !("Notification" in window)) return "no-push-api";
40
+ return null;
41
+ }
42
+ function isPushSupported() {
43
+ return getPushUnsupportedReason() === null;
44
+ }
45
+ function getPushPermissionState() {
46
+ if (!isPushSupported()) return "unsupported";
47
+ return Notification.permission;
48
+ }
49
+ function registerPushHandlers(handlers) {
50
+ (0, _sweidos_eidos.registerPushCallbacks)(handlers);
51
+ }
52
+ async function subscribeToPush(config) {
53
+ if (!isPushSupported()) return { status: "unsupported" };
54
+ let permission;
55
+ try {
56
+ permission = await Notification.requestPermission();
57
+ } catch (error) {
58
+ return {
59
+ status: "error",
60
+ error
61
+ };
62
+ }
63
+ if (permission !== "granted") return { status: "denied" };
64
+ const registration = (0, _sweidos_eidos.getSwRegistration)();
65
+ if (!registration) return { status: "sw-not-ready" };
66
+ try {
67
+ const applicationServerKey = urlBase64ToUint8Array(config.vapidPublicKey);
68
+ let subscription = await registration.pushManager.getSubscription();
69
+ if (subscription && !subscriptionKeyMatches(subscription, config.vapidPublicKey)) {
70
+ await subscription.unsubscribe();
71
+ subscription = null;
72
+ }
73
+ if (!subscription) subscription = await registration.pushManager.subscribe({
74
+ userVisibleOnly: true,
75
+ applicationServerKey
76
+ });
77
+ (0, _sweidos_eidos.sendToWorker)({
78
+ type: "EIDOS_CACHE_VAPID_KEY",
79
+ key: config.vapidPublicKey
80
+ });
81
+ const json = subscription.toJSON();
82
+ config.onSubscribe?.(json);
83
+ return {
84
+ status: "subscribed",
85
+ subscription: json
86
+ };
87
+ } catch (error) {
88
+ return {
89
+ status: "error",
90
+ error
91
+ };
92
+ }
93
+ }
94
+ async function unsubscribeFromPush() {
95
+ const registration = (0, _sweidos_eidos.getSwRegistration)();
96
+ if (!registration) return;
97
+ await (await registration.pushManager.getSubscription())?.unsubscribe();
98
+ }
99
+ async function getCurrentPushSubscription() {
100
+ const registration = (0, _sweidos_eidos.getSwRegistration)();
101
+ if (!registration) return null;
102
+ const subscription = await registration.pushManager.getSubscription();
103
+ return subscription ? subscription.toJSON() : null;
104
+ }
105
+ function uint8ArrayToUrlBase64(bytes) {
106
+ let binary = "";
107
+ for (const byte of bytes) binary += String.fromCharCode(byte);
108
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
109
+ }
110
+ /** Compares an existing subscription's key against the configured VAPID public key. */
111
+ function subscriptionKeyMatches(subscription, vapidPublicKey) {
112
+ const key = subscription.options.applicationServerKey;
113
+ if (!key) return false;
114
+ return uint8ArrayToUrlBase64(new Uint8Array(key)) === uint8ArrayToUrlBase64(urlBase64ToUint8Array(vapidPublicKey));
115
+ }
116
+ //#endregion
117
+ exports.getCurrentPushSubscription = getCurrentPushSubscription;
118
+ exports.getPushPermissionState = getPushPermissionState;
119
+ exports.getPushUnsupportedReason = getPushUnsupportedReason;
120
+ exports.isPushSupported = isPushSupported;
121
+ exports.registerPushHandlers = registerPushHandlers;
122
+ exports.subscribeToPush = subscribeToPush;
123
+ 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,116 @@
1
+ import { getSwRegistration, registerPushCallbacks, sendToWorker } from "@sweidos/eidos";
2
+ //#region src/internal/url-base64.ts
3
+ /** Decodes a base64url string (e.g. a VAPID public key) into raw bytes. */
4
+ function urlBase64ToUint8Array(base64Url) {
5
+ const base64 = (base64Url + "=".repeat((4 - base64Url.length % 4) % 4)).replace(/-/g, "+").replace(/_/g, "/");
6
+ const raw = atob(base64);
7
+ return Uint8Array.from(raw, (c) => c.charCodeAt(0));
8
+ }
9
+ //#endregion
10
+ //#region src/push.ts
11
+ /**
12
+ * @sweidos/eidos/push
13
+ *
14
+ * Web Push integration. Framework-agnostic: register click/expiry handlers
15
+ * once at app init (any tab), and trigger subscription from a user gesture.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { registerPushHandlers, subscribeToPush } from '@sweidos/eidos/push'
20
+ *
21
+ * // App init — every tab, no permission prompt
22
+ * registerPushHandlers({
23
+ * onNotificationClick: (data) => router.push(data.url),
24
+ * onSubscriptionExpired: (sub) => fetch('/api/push-subscribe', { method: 'POST', body: JSON.stringify(sub) }),
25
+ * })
26
+ *
27
+ * // User gesture (button click)
28
+ * const result = await subscribeToPush({
29
+ * vapidPublicKey: import.meta.env.VITE_EIDOS_VAPID_PUBLIC_KEY,
30
+ * onSubscribe: (sub) => fetch('/api/push-subscribe', { method: 'POST', body: JSON.stringify(sub) }),
31
+ * })
32
+ * ```
33
+ */
34
+ /** Why push is unavailable on this device, or null if it's supported. */
35
+ function getPushUnsupportedReason() {
36
+ if (typeof window === "undefined") return "no-push-api";
37
+ if (/iPhone|iPad|iPod/.test(navigator.userAgent) && !navigator.standalone) return "ios-not-installed";
38
+ if (!("serviceWorker" in navigator) || !("PushManager" in window) || !("Notification" in window)) return "no-push-api";
39
+ return null;
40
+ }
41
+ function isPushSupported() {
42
+ return getPushUnsupportedReason() === null;
43
+ }
44
+ function getPushPermissionState() {
45
+ if (!isPushSupported()) return "unsupported";
46
+ return Notification.permission;
47
+ }
48
+ function registerPushHandlers(handlers) {
49
+ registerPushCallbacks(handlers);
50
+ }
51
+ async function subscribeToPush(config) {
52
+ if (!isPushSupported()) return { status: "unsupported" };
53
+ let permission;
54
+ try {
55
+ permission = await Notification.requestPermission();
56
+ } catch (error) {
57
+ return {
58
+ status: "error",
59
+ error
60
+ };
61
+ }
62
+ if (permission !== "granted") return { status: "denied" };
63
+ const registration = getSwRegistration();
64
+ if (!registration) return { status: "sw-not-ready" };
65
+ try {
66
+ const applicationServerKey = urlBase64ToUint8Array(config.vapidPublicKey);
67
+ let subscription = await registration.pushManager.getSubscription();
68
+ if (subscription && !subscriptionKeyMatches(subscription, config.vapidPublicKey)) {
69
+ await subscription.unsubscribe();
70
+ subscription = null;
71
+ }
72
+ if (!subscription) subscription = await registration.pushManager.subscribe({
73
+ userVisibleOnly: true,
74
+ applicationServerKey
75
+ });
76
+ sendToWorker({
77
+ type: "EIDOS_CACHE_VAPID_KEY",
78
+ key: config.vapidPublicKey
79
+ });
80
+ const json = subscription.toJSON();
81
+ config.onSubscribe?.(json);
82
+ return {
83
+ status: "subscribed",
84
+ subscription: json
85
+ };
86
+ } catch (error) {
87
+ return {
88
+ status: "error",
89
+ error
90
+ };
91
+ }
92
+ }
93
+ async function unsubscribeFromPush() {
94
+ const registration = getSwRegistration();
95
+ if (!registration) return;
96
+ await (await registration.pushManager.getSubscription())?.unsubscribe();
97
+ }
98
+ async function getCurrentPushSubscription() {
99
+ const registration = getSwRegistration();
100
+ if (!registration) return null;
101
+ const subscription = await registration.pushManager.getSubscription();
102
+ return subscription ? subscription.toJSON() : null;
103
+ }
104
+ function uint8ArrayToUrlBase64(bytes) {
105
+ let binary = "";
106
+ for (const byte of bytes) binary += String.fromCharCode(byte);
107
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
108
+ }
109
+ /** Compares an existing subscription's key against the configured VAPID public key. */
110
+ function subscriptionKeyMatches(subscription, vapidPublicKey) {
111
+ const key = subscription.options.applicationServerKey;
112
+ if (!key) return false;
113
+ return uint8ArrayToUrlBase64(new Uint8Array(key)) === uint8ArrayToUrlBase64(urlBase64ToUint8Array(vapidPublicKey));
114
+ }
115
+ //#endregion
116
+ export { getCurrentPushSubscription, getPushPermissionState, getPushUnsupportedReason, isPushSupported, registerPushHandlers, subscribeToPush, unsubscribeFromPush };
package/dist/query.cjs CHANGED
@@ -118,7 +118,7 @@ function useEidosMutation(handle, options) {
118
118
  if (invalidates?.length) {
119
119
  await Promise.all(invalidates.map((h) => h.invalidate()));
120
120
  if (!_globalClient && contextClient) invalidates.forEach((h) => {
121
- contextClient.invalidateQueries({ queryKey: h.query().queryKey });
121
+ contextClient.invalidateQueries({ queryKey: ["eidos", h.url] });
122
122
  });
123
123
  }
124
124
  if (onSuccess) await onSuccess(...args);
package/dist/query.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { UseQueryOptions, UseQueryResult, UseMutationOptions, UseMutationResult, QueryClient } from '@tanstack/react-query';
2
- import { ResourceHandle, ActionHandle, QueuedResult } from '@sweidos/eidos';
2
+ import { ResourceHandle, AnyResourceHandle, ActionHandle, QueuedResult } from '@sweidos/eidos';
3
3
 
4
4
  /**
5
5
  * Register a QueryClient with Eidos.
@@ -44,7 +44,6 @@ type EidosQueryOptions<T> = Omit<UseQueryOptions<T, Error, T, [string, string]>,
44
44
  * ```
45
45
  */
46
46
  export declare function useEidosQuery<T>(handle: ResourceHandle<T>, options?: EidosQueryOptions<T>): UseQueryResult<T, Error>;
47
- type AnyResourceHandle = ResourceHandle<any>;
48
47
  export interface EidosMutationOptions<TArg, TData> extends Omit<UseMutationOptions<TData | QueuedResult, Error, TArg>, 'mutationFn' | 'networkMode'> {
49
48
  /**
50
49
  * Resource handles to invalidate (Cache Storage + TanStack Query) after
package/dist/query.js CHANGED
@@ -117,7 +117,7 @@ function useEidosMutation(handle, options) {
117
117
  if (invalidates?.length) {
118
118
  await Promise.all(invalidates.map((h) => h.invalidate()));
119
119
  if (!_globalClient && contextClient) invalidates.forEach((h) => {
120
- contextClient.invalidateQueries({ queryKey: h.query().queryKey });
120
+ contextClient.invalidateQueries({ queryKey: ["eidos", h.url] });
121
121
  });
122
122
  }
123
123
  if (onSuccess) await onSuccess(...args);
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue-storage.js","names":[],"sources":["../src/queue-storage.ts"],"sourcesContent":["import type { ActionQueueItem } from './types';\n\nexport interface QueueStorage {\n add(item: ActionQueueItem): Promise<void>;\n getAll(): Promise<ActionQueueItem[]>;\n getPending(): Promise<ActionQueueItem[]>;\n update(id: string, patch: Partial<ActionQueueItem>): Promise<void>;\n remove(id: string): Promise<void>;\n clear(): Promise<void>;\n}\n\nlet _storage: QueueStorage | null = null;\n\n/** Override the default IndexedDB queue with a custom storage backend (e.g. AsyncStorage for React Native). */\nexport function setQueueStorage(s: QueueStorage): void {\n _storage = s;\n}\n\nexport function _getQueueStorage(): QueueStorage | null {\n return _storage;\n}\n"],"mappings":"AAWA,IAAI,IAAgC;AAGpC,SAAgB,EAAgB,GAAuB;AACrD,EAAA,IAAW;AACb;AAEA,SAAgB,IAAwC;AACtD,SAAO;AACT"}
@@ -0,0 +1,34 @@
1
+ import { useEidosStore as o } from "./store.js";
2
+ var c = "eidos-queue-sync", a;
3
+ function r() {
4
+ return a !== void 0 || (a = typeof BroadcastChannel > "u" ? null : new BroadcastChannel(c)), a;
5
+ }
6
+ function i(e) {
7
+ r()?.postMessage(e);
8
+ }
9
+ function m() {
10
+ const e = r();
11
+ if (!e) return () => {
12
+ };
13
+ const s = (u) => {
14
+ const n = o.getState(), t = u.data;
15
+ switch (t.type) {
16
+ case "update":
17
+ n.updateQueueItem(t.id, t.update);
18
+ break;
19
+ case "batchUpdate":
20
+ n.batchUpdateQueueItems(t.updates);
21
+ break;
22
+ case "remove":
23
+ n.removeQueueItem(t.id);
24
+ break;
25
+ }
26
+ };
27
+ return e.addEventListener("message", s), () => e.removeEventListener("message", s);
28
+ }
29
+ export {
30
+ i as broadcastQueueSync,
31
+ m as subscribeQueueSync
32
+ };
33
+
34
+ //# sourceMappingURL=queue-sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue-sync.js","names":[],"sources":["../src/queue-sync.ts"],"sourcesContent":["import { useEidosStore } from './store';\nimport type { ActionQueueItem } from './types';\n\nconst CHANNEL_NAME = 'eidos-queue-sync';\n\ntype QueueSyncMessage =\n | { type: 'update'; id: string; update: Partial<ActionQueueItem> }\n | { type: 'batchUpdate'; updates: Array<{ id: string; update: Partial<ActionQueueItem> }> }\n | { type: 'remove'; id: string };\n\nlet _channel: BroadcastChannel | null | undefined;\n\nfunction getChannel(): BroadcastChannel | null {\n if (_channel !== undefined) return _channel;\n _channel = typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel(CHANNEL_NAME);\n return _channel;\n}\n\n/**\n * Broadcasts a queue-item status change to other tabs sharing the same\n * IndexedDB queue. The replay-lock holder (see `replayQueue` in action.ts)\n * is the only tab that mutates queue-item status, so non-leader tabs would\n * otherwise show stale status until their own store re-hydrates.\n *\n * No-ops in environments without BroadcastChannel (React Native, old Safari).\n */\nexport function broadcastQueueSync(message: QueueSyncMessage): void {\n getChannel()?.postMessage(message);\n}\n\n/**\n * Applies queue-item status updates broadcast by the replay-lock holder to\n * this tab's store. Returns an unsubscribe function.\n */\nexport function subscribeQueueSync(): () => void {\n const channel = getChannel();\n if (!channel) return () => {};\n\n const handler = (event: MessageEvent<QueueSyncMessage>) => {\n const store = useEidosStore.getState();\n const message = event.data;\n switch (message.type) {\n case 'update':\n store.updateQueueItem(message.id, message.update);\n break;\n case 'batchUpdate':\n store.batchUpdateQueueItems(message.updates);\n break;\n case 'remove':\n store.removeQueueItem(message.id);\n break;\n }\n };\n\n channel.addEventListener('message', handler);\n return () => channel.removeEventListener('message', handler);\n}\n\n/** Test-only: reset the cached channel so each test gets a fresh instance. */\nexport function _resetQueueSyncChannel(): void {\n _channel?.close();\n _channel = undefined;\n}\n"],"mappings":";AAGA,IAAM,IAAe,oBAOjB;AAEJ,SAAS,IAAsC;AAC7C,SAAI,MAAa,WACjB,IAAW,OAAO,mBAAqB,MAAc,OAAO,IAAI,iBAAiB,CAAY,IACtF;AACT;AAUA,SAAgB,EAAmB,GAAiC;AAClE,EAAA,EAAW,GAAG,YAAY,CAAO;AACnC;AAMA,SAAgB,IAAiC;AAC/C,QAAM,IAAU,EAAW;AAC3B,MAAI,CAAC,EAAS,QAAA,MAAa;AAAA,EAAC;AAE5B,QAAM,IAAA,CAAW,MAA0C;AACzD,UAAM,IAAQ,EAAc,SAAS,GAC/B,IAAU,EAAM;AACtB,YAAQ,EAAQ,MAAhB;AAAA,MACE,KAAK;AACH,QAAA,EAAM,gBAAgB,EAAQ,IAAI,EAAQ,MAAM;AAChD;AAAA,MACF,KAAK;AACH,QAAA,EAAM,sBAAsB,EAAQ,OAAO;AAC3C;AAAA,MACF,KAAK;AACH,QAAA,EAAM,gBAAgB,EAAQ,EAAE;AAChC;AAAA,IACJ;AAAA,EACF;AAEA,SAAA,EAAQ,iBAAiB,WAAW,CAAO,GAC3C,MAAa,EAAQ,oBAAoB,WAAW,CAAO;AAC7D"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Provider.js","names":[],"sources":["../../src/react/Provider.tsx"],"sourcesContent":["import { useEffect, type ReactNode } from 'react';\nimport { initEidos, type EidosConfig } from '../runtime';\n\ninterface EidosProviderProps extends EidosConfig {\n children: ReactNode;\n}\n\n/**\n * Mount once at the root of your application.\n * Registers the service worker and initialises the Eidos runtime.\n *\n * @example\n * <EidosProvider swPath=\"/eidos-sw.js\">\n * <App />\n * </EidosProvider>\n */\nexport function EidosProvider({ children, swPath, autoReplay }: EidosProviderProps) {\n useEffect(() => {\n initEidos({ swPath, autoReplay });\n // Run once on mount only\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return <>{children}</>;\n}\n"],"mappings":";;;AAgBA,SAAgB,EAAc,EAAE,UAAA,GAAU,QAAA,GAAQ,YAAA,EAAA,GAAkC;AAClF,SAAA,EAAA,MAAgB;AACd,IAAA,EAAU;AAAA,MAAE,QAAA;AAAA,MAAQ,YAAA;AAAA,IAAW,CAAC;AAAA,EAGlC,GAAG,CAAC,CAAC,GAEE,gBAAA,EAAA,GAAA,EAAG,UAAA,EAAW,CAAA;AACvB"}
@@ -1,36 +1,36 @@
1
- import { useEidosStore as a } from "../store.js";
2
- import { useEffect as d, useRef as l, useSyncExternalStore as E } from "react";
1
+ import { useEidosStore as o } from "../store.js";
2
+ import { countQueueByStatus as l } from "../types.js";
3
+ import { useEffect as i, useRef as c, useSyncExternalStore as p } from "react";
3
4
  function u(e) {
4
5
  const t = e ?? ((n) => n);
5
- return E(a.subscribe, () => t(a.getState()));
6
+ return p(o.subscribe, () => t(o.getState()));
6
7
  }
7
- function S() {
8
+ function R() {
8
9
  return u();
9
10
  }
10
11
  function q() {
11
12
  return u((e) => e.resources);
12
13
  }
13
- function R(e) {
14
+ function w(e) {
14
15
  return u((t) => t.resources[e]);
15
16
  }
16
- function m() {
17
+ function y() {
17
18
  return u((e) => e.queue);
18
19
  }
19
- function w(e) {
20
+ function $(e) {
20
21
  return u((t) => t.queue.find((n) => n.id === e));
21
22
  }
22
- function y() {
23
+ function O() {
23
24
  return {
24
25
  isOnline: u((e) => e.isOnline),
25
26
  swStatus: u((e) => e.swStatus),
26
27
  swError: u((e) => e.swError)
27
28
  };
28
29
  }
29
- function $() {
30
+ function Q() {
30
31
  const [e, t, n, r] = u((s) => {
31
- let o = 0, f = 0, c = 0;
32
- for (const i of s.queue) i.status === "pending" ? o++ : i.status === "failed" ? f++ : i.status === "replaying" && c++;
33
- return `${o},${f},${c},${s.queue.length}`;
32
+ const { pending: f, failed: a, replaying: d, total: E } = l(s.queue);
33
+ return `${f},${a},${d},${E}`;
34
34
  }).split(",");
35
35
  return {
36
36
  pending: +e,
@@ -39,23 +39,23 @@ function $() {
39
39
  total: +r
40
40
  };
41
41
  }
42
- function O(e) {
43
- const t = u((s) => s.queue.length), n = l(0), r = l(e);
44
- d(() => {
42
+ function b(e) {
43
+ const t = u((s) => s.queue.length), n = c(0), r = c(e);
44
+ i(() => {
45
45
  r.current = e;
46
- }), d(() => {
46
+ }), i(() => {
47
47
  n.current > 0 && t === 0 && r.current(), n.current = t;
48
48
  }, [t]);
49
49
  }
50
50
  export {
51
- S as useEidos,
52
- w as useEidosAction,
53
- O as useEidosOnDrain,
54
- m as useEidosQueue,
55
- $ as useEidosQueueStats,
56
- R as useEidosResource,
51
+ R as useEidos,
52
+ $ as useEidosAction,
53
+ b as useEidosOnDrain,
54
+ y as useEidosQueue,
55
+ Q as useEidosQueueStats,
56
+ w as useEidosResource,
57
57
  q as useEidosResources,
58
- y as useEidosStatus
58
+ O as useEidosStatus
59
59
  };
60
60
 
61
61
  //# sourceMappingURL=hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.js","names":[],"sources":["../../src/react/hooks.ts"],"sourcesContent":["import { useEffect, useRef, useSyncExternalStore } from 'react';\nimport { useEidosStore } from '../store';\nimport type { EidosStore } from '../store';\nimport { countQueueByStatus } from '../types';\n\nfunction useStore(): EidosStore;\nfunction useStore<T>(selector: (state: EidosStore) => T): T;\nfunction useStore<T = EidosStore>(selector?: (state: EidosStore) => T): T {\n const fn = selector ?? ((s: EidosStore) => s as unknown as T);\n return useSyncExternalStore(useEidosStore.subscribe, () => fn(useEidosStore.getState()));\n}\n\n/** Full Eidos store — prefer the narrower hooks below for performance. */\nexport function useEidos() {\n return useStore();\n}\n\n/** All registered resources — only re-renders when the resources map changes, not on queue mutations. */\nexport function useEidosResources() {\n return useStore((s) => s.resources);\n}\n\n/** Live state for a single registered resource URL. */\nexport function useEidosResource(url: string) {\n return useStore((s) => s.resources[url]);\n}\n\n/** The current action queue. */\nexport function useEidosQueue() {\n return useStore((s) => s.queue);\n}\n\n/**\n * Live state for a single queue item by ID. Only re-renders when that specific\n * item changes — cheaper than `useEidosQueue().find(id)` which re-renders on\n * any queue mutation.\n */\nexport function useEidosAction(id: string) {\n return useStore((s) => s.queue.find((item) => item.id === id));\n}\n\n/**\n * Online + SW status — cheap subscription, safe to use in header components.\n * Three separate primitive selectors so each only triggers a re-render when\n * its own value changes (no object-reference churn from a combined selector).\n */\nexport function useEidosStatus() {\n const isOnline = useStore((s) => s.isOnline);\n const swStatus = useStore((s) => s.swStatus);\n const swError = useStore((s) => s.swError);\n return { isOnline, swStatus, swError };\n}\n\n/**\n * Queue counts — single subscription, single loop. Re-renders only when a\n * count changes, not on every queue mutation. Use for badges and status bars\n * instead of `useEidosQueue()` when you only need numbers, not full items.\n */\nexport function useEidosQueueStats() {\n // Encode as a comma-separated string so useSyncExternalStore's Object.is\n // comparison bails out correctly when counts haven't changed. One loop,\n // one subscription — cheaper than four separate filter() passes.\n const encoded = useStore((s) => {\n const { pending, failed, replaying, total } = countQueueByStatus(s.queue);\n return `${pending},${failed},${replaying},${total}`;\n });\n const [p, f, r, t] = encoded.split(',');\n return { pending: +p, failed: +f, replaying: +r, total: +t };\n}\n\n/**\n * Calls `callback` once each time the action queue drains from non-empty → 0.\n * Stable callback reference not required — always calls the latest version.\n * Use for \"all offline actions synced!\" toasts.\n *\n * @example\n * useEidosOnDrain(() => toast.success('All offline actions synced!'))\n */\nexport function useEidosOnDrain(callback: () => void) {\n const total = useStore((s) => s.queue.length);\n const prevRef = useRef(0);\n const callbackRef = useRef(callback);\n\n useEffect(() => {\n callbackRef.current = callback;\n });\n\n useEffect(() => {\n if (prevRef.current > 0 && total === 0) {\n callbackRef.current();\n }\n prevRef.current = total;\n }, [total]);\n}\n"],"mappings":";;;AAOA,SAAS,EAAyB,GAAwC;AACxE,QAAM,IAAK,MAAA,CAAc,MAAkB;AAC3C,SAAO,EAAqB,EAAc,WAAA,MAAiB,EAAG,EAAc,SAAS,CAAC,CAAC;AACzF;AAGA,SAAgB,IAAW;AACzB,SAAO,EAAS;AAClB;AAGA,SAAgB,IAAoB;AAClC,SAAO,EAAA,CAAU,MAAM,EAAE,SAAS;AACpC;AAGA,SAAgB,EAAiB,GAAa;AAC5C,SAAO,EAAA,CAAU,MAAM,EAAE,UAAU,CAAA,CAAI;AACzC;AAGA,SAAgB,IAAgB;AAC9B,SAAO,EAAA,CAAU,MAAM,EAAE,KAAK;AAChC;AAOA,SAAgB,EAAe,GAAY;AACzC,SAAO,EAAA,CAAU,MAAM,EAAE,MAAM,KAAA,CAAM,MAAS,EAAK,OAAO,CAAE,CAAC;AAC/D;AAOA,SAAgB,IAAiB;AAI/B,SAAO;AAAA,IAAE,UAHQ,EAAA,CAAU,MAAM,EAAE,QAG1B;AAAA,IAAU,UAFF,EAAA,CAAU,MAAM,EAAE,QAEhB;AAAA,IAAU,SADb,EAAA,CAAU,MAAM,EAAE,OACL;AAAA,EAAQ;AACvC;AAOA,SAAgB,IAAqB;AAQnC,QAAM,CAAC,GAAG,GAAG,GAAG,CAAA,IAJA,EAAA,CAAU,MAAM;AAC9B,UAAM,EAAE,SAAA,GAAS,QAAA,GAAQ,WAAA,GAAW,OAAA,EAAA,IAAU,EAAmB,EAAE,KAAK;AACxE,WAAO,GAAG,CAAA,IAAW,CAAA,IAAU,CAAA,IAAa,CAAA;AAAA,EAC9C,CACqB,EAAQ,MAAM,GAAG;AACtC,SAAO;AAAA,IAAE,SAAS,CAAC;AAAA,IAAG,QAAQ,CAAC;AAAA,IAAG,WAAW,CAAC;AAAA,IAAG,OAAO,CAAC;AAAA,EAAE;AAC7D;AAUA,SAAgB,EAAgB,GAAsB;AACpD,QAAM,IAAQ,EAAA,CAAU,MAAM,EAAE,MAAM,MAAM,GACtC,IAAU,EAAO,CAAC,GAClB,IAAc,EAAO,CAAQ;AAEnC,EAAA,EAAA,MAAgB;AACd,IAAA,EAAY,UAAU;AAAA,EACxB,CAAC,GAED,EAAA,MAAgB;AACd,IAAI,EAAQ,UAAU,KAAK,MAAU,KACnC,EAAY,QAAQ,GAEtB,EAAQ,UAAU;AAAA,EACpB,GAAG,CAAC,CAAK,CAAC;AACZ"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay.js","names":[],"sources":["../src/replay.ts"],"sourcesContent":["import { useEidosStore } from './store';\nimport { replayQueue } from './action';\n\n/**\n * Subscribe to online/offline transitions and trigger replayQueue() on\n * reconnect, plus replay any pending items left over from a previous session.\n *\n * Shared by the web (runtime.ts) and React Native (runtime-rn.ts) init paths.\n *\n * WHY subscribe to the store instead of window.addEventListener('online'):\n * setOfflineSimulation() updates the store directly but never fires a real\n * browser `online` event. Watching the store catches both:\n * • Real network reconnects (sw-bridge updates store on window.online)\n * • Simulation toggled off (setOfflineSimulation(false) → store.setOnline(true))\n *\n * Returns an unsubscribe function.\n */\nexport function subscribeReplayOnReconnect(): () => void {\n let prevIsOnline = useEidosStore.getState().isOnline;\n\n const unsubscribe = useEidosStore.subscribe(() => {\n const { isOnline } = useEidosStore.getState();\n const justCameOnline = isOnline && !prevIsOnline;\n prevIsOnline = isOnline;\n\n if (justCameOnline) {\n // Small delay so the connection (or simulation reset) settles first\n setTimeout(replayQueue, 600);\n }\n });\n\n // Replay any pending items that survived a reload/restart.\n // 'failed' items have already exhausted maxRetries and are never\n // re-replayed (see _doReplayQueue), so they don't count here.\n const store = useEidosStore.getState();\n const hasPending = store.queue.some((q) => q.status === 'pending');\n if (store.isOnline && hasPending) {\n setTimeout(replayQueue, 1200);\n }\n\n return unsubscribe;\n}\n"],"mappings":";;AAiBA,SAAgB,IAAyC;AACvD,MAAI,IAAe,EAAc,SAAS,EAAE;AAE5C,QAAM,IAAc,EAAc,UAAA,MAAgB;AAChD,UAAM,EAAE,UAAA,EAAA,IAAa,EAAc,SAAS,GACtC,IAAiB,KAAY,CAAC;AACpC,IAAA,IAAe,GAEX,KAEF,WAAW,GAAa,GAAG;AAAA,EAE/B,CAAC,GAKK,IAAQ,EAAc,SAAS,GAC/B,IAAa,EAAM,MAAM,KAAA,CAAM,MAAM,EAAE,WAAW,SAAS;AACjE,SAAI,EAAM,YAAY,KACpB,WAAW,GAAa,IAAI,GAGvB;AACT"}