@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/README.md +127 -32
- package/dist/action.js +223 -112
- package/dist/action.js.map +1 -0
- package/dist/async-storage-adapter.js.map +1 -0
- package/dist/cli.js +102 -0
- package/dist/devtools.js +208 -71
- package/dist/eidos-sw.js +283 -188
- package/dist/eidos.cjs +2 -2
- package/dist/eidos.cjs.map +1 -0
- package/dist/idb.js.map +1 -0
- package/dist/index.d.ts +160 -26
- package/dist/index.js +45 -41
- package/dist/push.cjs +123 -0
- package/dist/push.d.ts +28 -0
- package/dist/push.js +116 -0
- package/dist/query.cjs +1 -1
- package/dist/query.d.ts +1 -2
- package/dist/query.js +1 -1
- package/dist/queue-storage.js.map +1 -0
- package/dist/queue-sync.js +34 -0
- package/dist/queue-sync.js.map +1 -0
- package/dist/react/Provider.js.map +1 -0
- package/dist/react/hooks.js +23 -23
- package/dist/react/hooks.js.map +1 -0
- package/dist/replay.js.map +1 -0
- package/dist/resource.js +121 -107
- package/dist/resource.js.map +1 -0
- package/dist/runtime.js +37 -19
- package/dist/runtime.js.map +1 -0
- package/dist/store-slices.js.map +1 -0
- package/dist/store.js.map +1 -0
- package/dist/stores.js +23 -31
- package/dist/stores.js.map +1 -0
- package/dist/sw-bridge.js +44 -31
- package/dist/sw-bridge.js.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -0
- package/package.json +11 -4
package/dist/index.js
CHANGED
|
@@ -1,45 +1,49 @@
|
|
|
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 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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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"}
|
package/dist/react/hooks.js
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import { useEidosStore as
|
|
2
|
-
import {
|
|
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
|
|
6
|
+
return p(o.subscribe, () => t(o.getState()));
|
|
6
7
|
}
|
|
7
|
-
function
|
|
8
|
+
function R() {
|
|
8
9
|
return u();
|
|
9
10
|
}
|
|
10
11
|
function q() {
|
|
11
12
|
return u((e) => e.resources);
|
|
12
13
|
}
|
|
13
|
-
function
|
|
14
|
+
function w(e) {
|
|
14
15
|
return u((t) => t.resources[e]);
|
|
15
16
|
}
|
|
16
|
-
function
|
|
17
|
+
function y() {
|
|
17
18
|
return u((e) => e.queue);
|
|
18
19
|
}
|
|
19
|
-
function
|
|
20
|
+
function $(e) {
|
|
20
21
|
return u((t) => t.queue.find((n) => n.id === e));
|
|
21
22
|
}
|
|
22
|
-
function
|
|
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
|
-
|
|
32
|
-
|
|
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
|
|
43
|
-
const t = u((s) => s.queue.length), n =
|
|
44
|
-
|
|
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
|
-
}),
|
|
46
|
+
}), i(() => {
|
|
47
47
|
n.current > 0 && t === 0 && r.current(), n.current = t;
|
|
48
48
|
}, [t]);
|
|
49
49
|
}
|
|
50
50
|
export {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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"}
|