@spooky-sync/core 0.0.1-canary.32 → 0.0.1-canary.321
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/AGENTS.md +81 -0
- package/README.md +56 -16
- package/dist/index.d.ts +2034 -137
- package/dist/index.js +15236 -2467
- package/dist/live.d.ts +141 -0
- package/dist/live.js +733 -0
- package/dist/otel/index.d.ts +1 -1
- package/dist/otel/index.js +3 -3
- package/dist/pure.d.ts +365 -0
- package/dist/pure.js +24 -0
- package/dist/sqlite-open.js +379 -0
- package/dist/sqlite-worker.d.ts +1 -0
- package/dist/sqlite-worker.js +439 -0
- package/dist/sw.d.ts +207 -0
- package/dist/sw.js +393 -0
- package/dist/tabs-broker-worker.d.ts +8 -0
- package/dist/tabs-broker-worker.js +472 -0
- package/dist/types.d.ts +1821 -38
- package/dist/worker-edges.js +470 -0
- package/dist/worker-impersonation.js +60 -0
- package/dist/worker-ref-tables.d.ts +48 -0
- package/dist/worker-types.d.ts +112 -0
- package/dist/worker-types.js +73 -0
- package/package.json +27 -7
- package/scripts/check-broker-bundle.mjs +33 -0
- package/scripts/check-worker-entries.mjs +172 -0
- package/skills/sp00ky-core/SKILL.md +31 -6
- package/skills/sp00ky-core/references/config.md +6 -0
- package/src/boot/auth-flip.saga.test.ts +135 -0
- package/src/boot/auth-flip.saga.ts +77 -0
- package/src/boot/boot.saga.test.ts +180 -0
- package/src/boot/boot.saga.ts +142 -0
- package/src/boot/bucket-switch.saga.test.ts +187 -0
- package/src/boot/bucket-switch.saga.ts +133 -0
- package/src/boot/preload.saga.test.ts +51 -0
- package/src/boot/preload.saga.ts +31 -0
- package/src/bucket-blurhash.test.ts +148 -0
- package/src/build-globals.d.ts +12 -0
- package/src/client/bucket-handle.test.ts +117 -0
- package/src/client/bucket-handle.ts +227 -0
- package/src/client/router.test.ts +58 -0
- package/src/client/router.ts +100 -0
- package/src/client/runtime.test.ts +195 -0
- package/src/client/runtime.ts +296 -0
- package/src/client/services.test.ts +60 -0
- package/src/client/services.ts +418 -0
- package/src/client/sp00ky-client.test.ts +303 -0
- package/src/client/sp00ky-client.ts +586 -0
- package/src/events/events.test.ts +2 -1
- package/src/events/index.ts +3 -0
- package/src/index.ts +69 -1
- package/src/kernel/constants.test.ts +19 -0
- package/src/kernel/constants.ts +80 -0
- package/src/kernel/effects.test.ts +40 -0
- package/src/kernel/effects.ts +163 -0
- package/src/kernel/events.ts +57 -0
- package/src/kernel/interpreter.test.ts +116 -0
- package/src/kernel/interpreter.ts +148 -0
- package/src/kernel/saga.test.ts +89 -0
- package/src/kernel/saga.ts +73 -0
- package/src/live/index.test.ts +501 -0
- package/src/live/index.ts +1039 -0
- package/src/modules/app-release/index.test.ts +125 -0
- package/src/modules/app-release/index.ts +209 -0
- package/src/modules/auth/auth.local-first.test.ts +169 -0
- package/src/modules/auth/events/index.ts +9 -2
- package/src/modules/auth/impersonation-banner.ts +443 -0
- package/src/modules/auth/impersonation.integration.test.ts +205 -0
- package/src/modules/auth/impersonation.test.ts +377 -0
- package/src/modules/auth/impersonation.ts +179 -0
- package/src/modules/auth/index.ts +466 -19
- package/src/modules/crdt/crdt-field.ts +294 -0
- package/src/modules/crdt/crdt-hydration.test.ts +210 -0
- package/src/modules/crdt/crdt-reconnect.test.ts +195 -0
- package/src/modules/crdt/index.ts +463 -0
- package/src/modules/crdt/loro-loader.ts +25 -0
- package/src/modules/devtools/flags.ts +349 -0
- package/src/modules/devtools/index.ts +746 -95
- package/src/modules/devtools/mutations.test.ts +92 -0
- package/src/modules/devtools/mutations.ts +160 -0
- package/src/modules/devtools/notify-throttle.test.ts +166 -0
- package/src/modules/devtools/state-shape.test.ts +263 -0
- package/src/modules/devtools/storage-info.test.ts +79 -0
- package/src/modules/devtools/storage-info.ts +168 -0
- package/src/modules/devtools/versions.test.ts +74 -0
- package/src/modules/devtools/versions.ts +110 -0
- package/src/modules/feature-flag/index.test.ts +298 -0
- package/src/modules/feature-flag/index.ts +316 -0
- package/src/modules/ref-tables.test.ts +93 -0
- package/src/modules/ref-tables.ts +91 -0
- package/src/modules/web-push/index.test.ts +824 -0
- package/src/modules/web-push/index.ts +1027 -0
- package/src/mutation/jobs.test.ts +25 -0
- package/src/mutation/jobs.ts +41 -0
- package/src/mutation/mutation-id.test.ts +37 -0
- package/src/mutation/mutation-id.ts +35 -0
- package/src/mutation/pending-writes.integration.test.ts +175 -0
- package/src/mutation/push.saga.test.ts +285 -0
- package/src/mutation/push.saga.ts +217 -0
- package/src/mutation/rows.test.ts +231 -0
- package/src/mutation/rows.ts +427 -0
- package/src/mutation/tray.saga.test.ts +63 -0
- package/src/mutation/tray.saga.ts +48 -0
- package/src/mutation/write.saga.test.ts +316 -0
- package/src/mutation/write.saga.ts +268 -0
- package/src/otel/index.ts +10 -7
- package/src/pure.ts +66 -0
- package/src/push/types.ts +174 -0
- package/src/query/anon-query-isolation.integration.test.ts +337 -0
- package/src/query/env.test.ts +23 -0
- package/src/query/env.ts +56 -0
- package/src/query/fetch.saga.test.ts +199 -0
- package/src/query/fetch.saga.ts +120 -0
- package/src/query/hash.test.ts +14 -0
- package/src/query/hash.ts +18 -0
- package/src/query/id-set-plan.test.ts +132 -0
- package/src/query/lifecycle.saga.test.ts +274 -0
- package/src/query/lifecycle.saga.ts +159 -0
- package/src/query/list-ref-live.integration.test.ts +203 -0
- package/src/query/materialize.saga.test.ts +115 -0
- package/src/query/materialize.saga.ts +84 -0
- package/src/query/materialize.test.ts +58 -0
- package/src/query/materialize.ts +57 -0
- package/src/query/membership.saga.test.ts +350 -0
- package/src/query/membership.saga.ts +278 -0
- package/src/query/membership.test.ts +118 -0
- package/src/query/membership.ts +201 -0
- package/src/query/register.saga.test.ts +311 -0
- package/src/query/register.saga.ts +229 -0
- package/src/query/render-set.test.ts +47 -0
- package/src/query/render-set.ts +71 -0
- package/src/query/sql.test.ts +112 -0
- package/src/query/sql.ts +149 -0
- package/src/query/window-query.test.ts +70 -0
- package/src/query/window-query.ts +195 -0
- package/src/services/blobs/blob-cache.test.ts +359 -0
- package/src/services/blobs/blob-cache.ts +603 -0
- package/src/services/blobs/blob-manifest.ts +227 -0
- package/src/services/blobs/blob-store.test.ts +77 -0
- package/src/services/blobs/blob-store.ts +359 -0
- package/src/services/blobs/blob.fixture.ts +90 -0
- package/src/services/blobs/index.ts +70 -0
- package/src/services/database/cache-engine.ts +199 -0
- package/src/services/database/connection-supervisor.test.ts +318 -0
- package/src/services/database/connection-supervisor.ts +422 -0
- package/src/services/database/database.query-timeout.test.ts +140 -0
- package/src/services/database/database.responses.test.ts +43 -0
- package/src/services/database/database.ts +146 -61
- package/src/services/database/engine-factory.ts +33 -0
- package/src/services/database/errors.ts +34 -0
- package/src/services/database/events/index.ts +2 -1
- package/src/services/database/index.ts +7 -0
- package/src/services/database/landed-body-clear.integration.test.ts +236 -0
- package/src/services/database/local-migrator.integration.test.ts +110 -0
- package/src/services/database/local-migrator.ts +59 -16
- package/src/services/database/local.test.ts +64 -0
- package/src/services/database/local.ts +487 -63
- package/src/services/database/plan-render.test.ts +159 -0
- package/src/services/database/plan-render.ts +108 -0
- package/src/services/database/relation-resolver.test.ts +484 -0
- package/src/services/database/relation-resolver.ts +0 -0
- package/src/services/database/remote.ts +273 -6
- package/src/services/database/sqlite-cache-engine.test.ts +617 -0
- package/src/services/database/sqlite-cache-engine.timeout.test.ts +61 -0
- package/src/services/database/sqlite-cache-engine.ts +1372 -0
- package/src/services/database/sqlite-devtools-queries.integration.test.ts +143 -0
- package/src/services/database/sqlite-devtools-queries.test.ts +154 -0
- package/src/services/database/sqlite-lock-verify.test.ts +33 -0
- package/src/services/database/sqlite-lock-verify.ts +45 -0
- package/src/services/database/sqlite-open.test.ts +150 -0
- package/src/services/database/sqlite-open.ts +164 -0
- package/src/services/database/sqlite-plan-sql.test.ts +145 -0
- package/src/services/database/sqlite-plan-sql.ts +182 -0
- package/src/services/database/sqlite-projection.test.ts +121 -0
- package/src/services/database/sqlite-select.integration.test.ts +185 -0
- package/src/services/database/sqlite-select.test.ts +261 -0
- package/src/services/database/sqlite-select.ts +131 -0
- package/src/services/database/sqlite-transport.fixture.ts +30 -0
- package/src/services/database/sqlite-transport.ts +224 -0
- package/src/services/database/sqlite-worker.ts +437 -0
- package/src/services/database/surql-translate.ts +416 -0
- package/src/services/database/surreal-cache-engine.ts +161 -0
- package/src/services/logger/index.test.ts +132 -0
- package/src/services/logger/index.ts +189 -7
- package/src/services/persistence/localstorage.ts +2 -2
- package/src/services/persistence/resilient.ts +10 -3
- package/src/services/persistence/surrealdb.ts +4 -4
- package/src/services/stream-processor/index.ts +783 -68
- package/src/services/stream-processor/permissions.test.ts +47 -0
- package/src/services/stream-processor/permissions.ts +53 -0
- package/src/services/stream-processor/stream-processor.batch.test.ts +186 -0
- package/src/services/stream-processor/stream-processor.prime.test.ts +198 -0
- package/src/services/stream-processor/stream-processor.reset.test.ts +226 -0
- package/src/services/stream-processor/stream-processor.test.ts +1 -1
- package/src/services/stream-processor/wasm-types.ts +58 -2
- package/src/services/tabs/broker-client.ts +283 -0
- package/src/services/tabs/broker.test.ts +327 -0
- package/src/services/tabs/coordinator.test.ts +386 -0
- package/src/services/tabs/coordinator.ts +638 -0
- package/src/services/tabs/fake-ports.fixture.ts +112 -0
- package/src/services/tabs/leader-locks.ts +75 -0
- package/src/services/tabs/protocol.ts +262 -0
- package/src/services/tabs/support.ts +36 -0
- package/src/services/tabs/tabs-broker-worker.ts +640 -0
- package/src/sp00ky.ts +5 -395
- package/src/state/client-state.ts +200 -0
- package/src/state/lifecycle.test.ts +141 -0
- package/src/state/lifecycle.ts +99 -0
- package/src/state/reducers.test.ts +304 -0
- package/src/state/reducers.ts +526 -0
- package/src/state/selectors.test.ts +175 -0
- package/src/state/selectors.ts +222 -0
- package/src/sw/index.test.ts +582 -0
- package/src/sw/index.ts +607 -0
- package/src/sw/store.ts +96 -0
- package/src/sync/connection.saga.test.ts +97 -0
- package/src/sync/connection.saga.ts +89 -0
- package/src/sync/live.saga.test.ts +178 -0
- package/src/sync/live.saga.ts +107 -0
- package/src/sync/policy.health.test.ts +56 -0
- package/src/sync/policy.test.ts +663 -0
- package/src/sync/policy.ts +515 -0
- package/src/sync/poll.saga.test.ts +51 -0
- package/src/sync/poll.saga.ts +36 -0
- package/src/sync/tabs.saga.test.ts +78 -0
- package/src/sync/tabs.saga.ts +96 -0
- package/src/testing/adapters.ts +133 -0
- package/src/testing/build.ts +63 -0
- package/src/testing/fake-services.ts +48 -0
- package/src/testing/run-pure.test.ts +92 -0
- package/src/testing/run-pure.ts +128 -0
- package/src/testing/services.ts +17 -0
- package/src/types.ts +624 -10
- package/src/utils/blurhash.ts +90 -0
- package/src/utils/edges.ts +39 -0
- package/src/utils/error-classification.test.ts +54 -0
- package/src/utils/error-classification.ts +25 -0
- package/src/utils/index.ts +78 -12
- package/src/utils/parser.test.ts +67 -102
- package/src/utils/parser.ts +64 -2
- package/src/utils/semver.test.ts +32 -0
- package/src/utils/semver.ts +30 -0
- package/src/utils/sha256.ts +15 -0
- package/src/utils/surql.ts +30 -18
- package/src/utils/utils.test.ts +3 -2
- package/tsdown.config.ts +117 -3
- package/vitest.config.ts +48 -0
- package/src/modules/cache/index.ts +0 -241
- package/src/modules/cache/types.ts +0 -19
- package/src/modules/data/data.test.ts +0 -58
- package/src/modules/data/index.ts +0 -879
- package/src/modules/sync/engine.ts +0 -173
- package/src/modules/sync/events/index.ts +0 -77
- package/src/modules/sync/index.ts +0 -3
- package/src/modules/sync/queue/index.ts +0 -2
- package/src/modules/sync/queue/queue-down.ts +0 -89
- package/src/modules/sync/queue/queue-up.ts +0 -223
- package/src/modules/sync/scheduler.ts +0 -84
- package/src/modules/sync/sync.ts +0 -421
- package/src/modules/sync/utils.test.ts +0 -311
- package/src/modules/sync/utils.ts +0 -167
package/dist/sw.js
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { n as decodeTokenClaims } from "./worker-impersonation.js";
|
|
2
|
+
import { a as sameBytes, i as isPushPayload, n as base64UrlToBytes, r as bytesToBase64Url, t as PUSH_PAYLOAD_VERSION } from "./worker-types.js";
|
|
3
|
+
|
|
4
|
+
//#region src/sw/store.ts
|
|
5
|
+
const PUSH_DB_NAME = "sp00ky-push";
|
|
6
|
+
const PUSH_STORE_NAME = "kv";
|
|
7
|
+
function memoryStore() {
|
|
8
|
+
const map = /* @__PURE__ */ new Map();
|
|
9
|
+
return {
|
|
10
|
+
get: async (key) => map.get(key),
|
|
11
|
+
set: async (key, value) => void map.set(key, value),
|
|
12
|
+
delete: async (key) => void map.delete(key)
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function idbStore(dbName = PUSH_DB_NAME, storeName = PUSH_STORE_NAME, factory) {
|
|
16
|
+
let opening = null;
|
|
17
|
+
const open = () => {
|
|
18
|
+
if (opening) return opening;
|
|
19
|
+
const idb = factory ?? globalThis.indexedDB;
|
|
20
|
+
if (!idb) return Promise.reject(/* @__PURE__ */ new Error("indexedDB unavailable"));
|
|
21
|
+
opening = new Promise((resolve, reject) => {
|
|
22
|
+
const req = idb.open(dbName, 1);
|
|
23
|
+
req.onupgradeneeded = () => {
|
|
24
|
+
if (!req.result.objectStoreNames.contains(storeName)) req.result.createObjectStore(storeName);
|
|
25
|
+
};
|
|
26
|
+
req.onsuccess = () => {
|
|
27
|
+
const db = req.result;
|
|
28
|
+
db.onversionchange = () => {
|
|
29
|
+
db.close();
|
|
30
|
+
opening = null;
|
|
31
|
+
};
|
|
32
|
+
resolve(db);
|
|
33
|
+
};
|
|
34
|
+
req.onerror = () => {
|
|
35
|
+
opening = null;
|
|
36
|
+
reject(req.error);
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
return opening;
|
|
40
|
+
};
|
|
41
|
+
const run = async (mode, op) => {
|
|
42
|
+
const db = await open();
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const tx = db.transaction(storeName, mode);
|
|
45
|
+
const req = op(tx.objectStore(storeName));
|
|
46
|
+
tx.oncomplete = () => resolve(req.result);
|
|
47
|
+
tx.onerror = () => reject(tx.error ?? req.error);
|
|
48
|
+
tx.onabort = () => reject(tx.error ?? req.error);
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
get: (key) => run("readonly", (s) => s.get(key)),
|
|
53
|
+
set: async (key, value) => {
|
|
54
|
+
await run("readwrite", (s) => s.put(value, key));
|
|
55
|
+
},
|
|
56
|
+
delete: async (key) => {
|
|
57
|
+
await run("readwrite", (s) => s.delete(key));
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
let shared = null;
|
|
62
|
+
/** IndexedDB when it exists, else one process-wide memory store. */
|
|
63
|
+
function defaultStore() {
|
|
64
|
+
if (shared) return shared;
|
|
65
|
+
shared = typeof globalThis.indexedDB !== "undefined" ? idbStore() : memoryStore();
|
|
66
|
+
return shared;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/sw/index.ts
|
|
71
|
+
const DEFAULT_PLACEHOLDER = {
|
|
72
|
+
title: "New notification",
|
|
73
|
+
body: "Open the app to see what is new."
|
|
74
|
+
};
|
|
75
|
+
const PLACEHOLDER_TAG = "sp00ky-placeholder";
|
|
76
|
+
const TOKEN_KEY = "token";
|
|
77
|
+
/** The session the page bridged, or null (none, signed out, or its JWT expired). */
|
|
78
|
+
async function getBridgedToken(store = defaultStore()) {
|
|
79
|
+
let value;
|
|
80
|
+
try {
|
|
81
|
+
value = await store.get(TOKEN_KEY);
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
if (!value || typeof value.token !== "string") return null;
|
|
86
|
+
const { exp } = decodeTokenClaims(value.token);
|
|
87
|
+
if (exp !== null && exp * 1e3 <= Date.now()) return null;
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
function isTokenMessage(data) {
|
|
91
|
+
const m = data;
|
|
92
|
+
return !!m && m.type === "sp00ky:token" && typeof m.token === "string" && typeof m.userId === "string" && typeof m.endpoint === "string" && typeof m.namespace === "string" && typeof m.database === "string";
|
|
93
|
+
}
|
|
94
|
+
function dropUndefined(obj) {
|
|
95
|
+
for (const key of Object.keys(obj)) if (obj[key] === void 0) delete obj[key];
|
|
96
|
+
return obj;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* `showNotification(title, options)` arguments for a spec: every key passes
|
|
100
|
+
* through except `title` and `url`; `tag` defaults to the topic; `data` gets
|
|
101
|
+
* `url`, the payload and per-action urls next to the spec's own data.
|
|
102
|
+
*/
|
|
103
|
+
function notificationOptions(spec, payload, extraData) {
|
|
104
|
+
const { title, url, data, tag, actions, renotify, ...rest } = spec;
|
|
105
|
+
const actionUrls = {};
|
|
106
|
+
for (const a of actions ?? []) if (a && typeof a.url === "string") actionUrls[a.action] = a.url;
|
|
107
|
+
const finalTag = tag ?? payload?.topic;
|
|
108
|
+
return [title, dropUndefined({
|
|
109
|
+
...rest,
|
|
110
|
+
tag: finalTag,
|
|
111
|
+
actions: actions?.map(({ url: _url, ...a }) => a),
|
|
112
|
+
renotify: finalTag ? renotify : void 0,
|
|
113
|
+
data: dropUndefined({
|
|
114
|
+
...data,
|
|
115
|
+
url,
|
|
116
|
+
payload,
|
|
117
|
+
actionUrls: Object.keys(actionUrls).length > 0 ? actionUrls : void 0,
|
|
118
|
+
...extraData
|
|
119
|
+
})
|
|
120
|
+
})];
|
|
121
|
+
}
|
|
122
|
+
/** Wire the push, click, close, message and subscription-change handlers. Returns an uninstall. */
|
|
123
|
+
function installPushHandlers(options = {}) {
|
|
124
|
+
const scope = options.scope ?? globalThis;
|
|
125
|
+
const store = options.store ?? defaultStore();
|
|
126
|
+
const suppress = options.suppressWhenVisible !== false;
|
|
127
|
+
const placeholder = options.placeholder ?? DEFAULT_PLACEHOLDER;
|
|
128
|
+
const resolveUrl = (url, payload) => {
|
|
129
|
+
if (options.resolveUrl) return options.resolveUrl(url, payload);
|
|
130
|
+
try {
|
|
131
|
+
return new URL(url, scope.registration?.scope || scope.location.origin).href;
|
|
132
|
+
} catch {
|
|
133
|
+
return url;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const windows = async () => {
|
|
137
|
+
return (await scope.clients.matchAll({
|
|
138
|
+
type: "window",
|
|
139
|
+
includeUncontrolled: true
|
|
140
|
+
})).filter((c) => {
|
|
141
|
+
try {
|
|
142
|
+
return new URL(c.url).origin === scope.location.origin;
|
|
143
|
+
} catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
const visibleClients = async () => (await windows()).filter((c) => c.visibilityState === "visible");
|
|
149
|
+
const show = async (spec, payload, extraData) => {
|
|
150
|
+
const [title, opts] = notificationOptions(spec, payload, extraData);
|
|
151
|
+
await scope.registration.showNotification(title, opts);
|
|
152
|
+
};
|
|
153
|
+
const showPlaceholder = () => show({
|
|
154
|
+
...placeholder,
|
|
155
|
+
tag: PLACEHOLDER_TAG,
|
|
156
|
+
url: "/"
|
|
157
|
+
});
|
|
158
|
+
const ctxFor = (event, flag) => ({
|
|
159
|
+
event,
|
|
160
|
+
registration: scope.registration,
|
|
161
|
+
show: async (spec) => {
|
|
162
|
+
flag.shown = true;
|
|
163
|
+
await show(spec);
|
|
164
|
+
},
|
|
165
|
+
showPlaceholder: async () => {
|
|
166
|
+
flag.shown = true;
|
|
167
|
+
await showPlaceholder();
|
|
168
|
+
},
|
|
169
|
+
visibleClients,
|
|
170
|
+
bridged: () => getBridgedToken(store)
|
|
171
|
+
});
|
|
172
|
+
const renderLive = async (live, payload) => {
|
|
173
|
+
const bridged = await getBridgedToken(store);
|
|
174
|
+
if (!bridged) throw new Error("sp00ky push: no bridged session for live rendering");
|
|
175
|
+
const budget = live.timeoutMs ?? 8e3;
|
|
176
|
+
const started = Date.now();
|
|
177
|
+
const feed = await live.feed(bridged);
|
|
178
|
+
let rows = [];
|
|
179
|
+
try {
|
|
180
|
+
const sub = feed.subscribe(live.query(bridged.userId, payload), { onSet: (r) => void (rows = r) });
|
|
181
|
+
let timer;
|
|
182
|
+
const deadline = new Promise((_, reject) => {
|
|
183
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error("sp00ky push: live rows did not load in time")), budget);
|
|
184
|
+
});
|
|
185
|
+
try {
|
|
186
|
+
rows = await Promise.race([sub.ready, deadline]);
|
|
187
|
+
} finally {
|
|
188
|
+
clearTimeout(timer);
|
|
189
|
+
}
|
|
190
|
+
await feed.idle({
|
|
191
|
+
timeoutMs: Math.max(0, budget - (Date.now() - started)),
|
|
192
|
+
quietMs: live.quietMs
|
|
193
|
+
});
|
|
194
|
+
rows = sub.rows();
|
|
195
|
+
} finally {
|
|
196
|
+
await feed.close().catch(() => void 0);
|
|
197
|
+
}
|
|
198
|
+
const specs = rows.filter((row) => live.visible(row)).map((row) => live.render(row));
|
|
199
|
+
const wanted = new Set(specs.map((s) => s.tag));
|
|
200
|
+
const shown = await scope.registration.getNotifications();
|
|
201
|
+
let closed = 0;
|
|
202
|
+
for (const n of shown) if (n.data?.sp00kyLive && !wanted.has(n.tag)) {
|
|
203
|
+
n.close();
|
|
204
|
+
closed += 1;
|
|
205
|
+
}
|
|
206
|
+
for (const spec of specs) {
|
|
207
|
+
const existing = shown.find((n) => n.tag === spec.tag && n.data?.sp00kyLive);
|
|
208
|
+
if (existing && existing.title === spec.title && (existing.body ?? "") === (spec.body ?? "")) continue;
|
|
209
|
+
await show(spec, void 0, { sp00kyLive: true });
|
|
210
|
+
}
|
|
211
|
+
if ((await scope.registration.getNotifications()).length === 0 && (closed === 0 || live.silentClose === false)) await showPlaceholder();
|
|
212
|
+
};
|
|
213
|
+
const handlePush = async (event) => {
|
|
214
|
+
let payload = null;
|
|
215
|
+
try {
|
|
216
|
+
payload = event.data ? event.data.json() : null;
|
|
217
|
+
} catch {
|
|
218
|
+
payload = null;
|
|
219
|
+
}
|
|
220
|
+
if (!isPushPayload(payload)) {
|
|
221
|
+
await showPlaceholder();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const flag = { shown: false };
|
|
225
|
+
const ctx = ctxFor(event, flag);
|
|
226
|
+
try {
|
|
227
|
+
if (options.onPush && await options.onPush(payload, ctx) === true) return;
|
|
228
|
+
} catch {
|
|
229
|
+
if (!flag.shown) await showPlaceholder();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (suppress) {
|
|
233
|
+
const visible = await visibleClients().catch(() => []);
|
|
234
|
+
if (visible.length > 0) {
|
|
235
|
+
const message = {
|
|
236
|
+
type: "sp00ky:push",
|
|
237
|
+
payload
|
|
238
|
+
};
|
|
239
|
+
for (const client of visible) client.postMessage(message);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (payload.notification) {
|
|
244
|
+
await show(options.render?.(payload) ?? {
|
|
245
|
+
...payload.notification,
|
|
246
|
+
title: payload.notification.title ?? placeholder.title
|
|
247
|
+
}, payload);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
if (options.onNudge) {
|
|
252
|
+
await options.onNudge(payload, ctx);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const custom = options.render?.(payload);
|
|
256
|
+
if (custom) {
|
|
257
|
+
await show(custom, payload);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (options.live) {
|
|
261
|
+
await renderLive(options.live, payload);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
} catch {
|
|
265
|
+
if (!flag.shown) await showPlaceholder();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
await showPlaceholder();
|
|
269
|
+
};
|
|
270
|
+
const handleClick = async (event) => {
|
|
271
|
+
const notification = event.notification;
|
|
272
|
+
const data = notification.data ?? {};
|
|
273
|
+
const payload = isPushPayload(data.payload) ? data.payload : void 0;
|
|
274
|
+
const action = event.action || void 0;
|
|
275
|
+
if (options.onClick && await options.onClick(event, payload, action) === true) return;
|
|
276
|
+
notification.close();
|
|
277
|
+
const url = resolveUrl(action && data.actionUrls?.[action] || data.url || "/", payload);
|
|
278
|
+
const open = await windows();
|
|
279
|
+
const target = open.find((c) => c.focused) ?? open.find((c) => c.visibilityState === "visible") ?? open[0];
|
|
280
|
+
if (!target) {
|
|
281
|
+
await scope.clients.openWindow(url);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
await target.focus().catch(() => void 0);
|
|
285
|
+
const message = {
|
|
286
|
+
type: "sp00ky:navigate",
|
|
287
|
+
url,
|
|
288
|
+
payload,
|
|
289
|
+
action
|
|
290
|
+
};
|
|
291
|
+
if (options.clickMode === "message") {
|
|
292
|
+
target.postMessage(message);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (target.url === url) return;
|
|
296
|
+
if (target.navigate) try {
|
|
297
|
+
await target.navigate(url);
|
|
298
|
+
return;
|
|
299
|
+
} catch {}
|
|
300
|
+
target.postMessage(message);
|
|
301
|
+
};
|
|
302
|
+
const handleClose = async (event) => {
|
|
303
|
+
const data = event.notification.data ?? {};
|
|
304
|
+
await options.onClose?.(event, isPushPayload(data.payload) ? data.payload : void 0);
|
|
305
|
+
};
|
|
306
|
+
const signOut = async () => {
|
|
307
|
+
await store.delete(TOKEN_KEY).catch(() => void 0);
|
|
308
|
+
const shown = await scope.registration.getNotifications().catch(() => []);
|
|
309
|
+
for (const n of shown) n.close();
|
|
310
|
+
};
|
|
311
|
+
const handleMessage = (event) => {
|
|
312
|
+
const data = event.data;
|
|
313
|
+
if (!data || typeof data !== "object") return;
|
|
314
|
+
if (event.origin && event.origin !== scope.location.origin) return;
|
|
315
|
+
let work = null;
|
|
316
|
+
if (isTokenMessage(data)) {
|
|
317
|
+
const value = {
|
|
318
|
+
token: data.token,
|
|
319
|
+
userId: data.userId,
|
|
320
|
+
endpoint: data.endpoint,
|
|
321
|
+
namespace: data.namespace,
|
|
322
|
+
database: data.database,
|
|
323
|
+
publicKey: typeof data.publicKey === "string" ? data.publicKey : void 0,
|
|
324
|
+
pushEndpoint: typeof data.pushEndpoint === "string" ? data.pushEndpoint : void 0,
|
|
325
|
+
at: Date.now()
|
|
326
|
+
};
|
|
327
|
+
work = store.set(TOKEN_KEY, dropUndefined({ ...value })).catch(() => void 0);
|
|
328
|
+
} else if (data.type === "sp00ky:signout") work = signOut();
|
|
329
|
+
if (work) event.waitUntil?.(work);
|
|
330
|
+
};
|
|
331
|
+
const handleSubscriptionChange = async (event) => {
|
|
332
|
+
if (options.resubscribe === false) return;
|
|
333
|
+
const bridged = await getBridgedToken(store);
|
|
334
|
+
let sub = event.newSubscription ?? null;
|
|
335
|
+
if (!sub) {
|
|
336
|
+
const key = event.oldSubscription?.options?.applicationServerKey ?? (bridged?.publicKey ? base64UrlToBytes(bridged.publicKey) : null);
|
|
337
|
+
const manager = scope.registration.pushManager;
|
|
338
|
+
if (key && manager) try {
|
|
339
|
+
sub = await manager.subscribe({
|
|
340
|
+
userVisibleOnly: true,
|
|
341
|
+
applicationServerKey: key
|
|
342
|
+
});
|
|
343
|
+
} catch {
|
|
344
|
+
sub = null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const factory = options.live?.feed ?? options.feed;
|
|
348
|
+
if (sub && bridged && factory) {
|
|
349
|
+
let feed = null;
|
|
350
|
+
try {
|
|
351
|
+
feed = await factory(bridged);
|
|
352
|
+
const oldEndpoint = event.oldSubscription?.endpoint ?? bridged.pushEndpoint;
|
|
353
|
+
const [devices] = await feed.query("RETURN fn::push::list()");
|
|
354
|
+
const old = Array.isArray(devices) ? devices.find((d) => d.endpoint === oldEndpoint) : void 0;
|
|
355
|
+
await feed.query("RETURN fn::push::subscribe($sub, $opts)", {
|
|
356
|
+
sub: sub.toJSON(),
|
|
357
|
+
opts: dropUndefined({
|
|
358
|
+
label: old?.label ?? void 0,
|
|
359
|
+
rules: old?.rules ?? void 0,
|
|
360
|
+
meta: old?.meta ?? void 0,
|
|
361
|
+
userAgent: scope.navigator?.userAgent
|
|
362
|
+
})
|
|
363
|
+
});
|
|
364
|
+
if (oldEndpoint && oldEndpoint !== sub.endpoint) await feed.query("RETURN fn::push::unsubscribe($endpoint)", { endpoint: oldEndpoint });
|
|
365
|
+
await store.set(TOKEN_KEY, {
|
|
366
|
+
...bridged,
|
|
367
|
+
pushEndpoint: sub.endpoint
|
|
368
|
+
});
|
|
369
|
+
} catch {} finally {
|
|
370
|
+
await feed?.close().catch(() => void 0);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const message = {
|
|
374
|
+
type: "sp00ky:subscriptionchange",
|
|
375
|
+
endpoint: sub?.endpoint
|
|
376
|
+
};
|
|
377
|
+
for (const client of await windows().catch(() => [])) client.postMessage(message);
|
|
378
|
+
};
|
|
379
|
+
const listeners = [
|
|
380
|
+
["push", (event) => event.waitUntil(handlePush(event).catch(() => showPlaceholder().catch(() => void 0)))],
|
|
381
|
+
["notificationclick", (event) => event.waitUntil(handleClick(event).catch(() => void 0))],
|
|
382
|
+
["notificationclose", (event) => event.waitUntil?.(handleClose(event).catch(() => void 0))],
|
|
383
|
+
["message", handleMessage],
|
|
384
|
+
["pushsubscriptionchange", (event) => event.waitUntil(handleSubscriptionChange(event).catch(() => void 0))]
|
|
385
|
+
];
|
|
386
|
+
for (const [type, listener] of listeners) scope.addEventListener(type, listener);
|
|
387
|
+
return () => {
|
|
388
|
+
for (const [type, listener] of listeners) scope.removeEventListener(type, listener);
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
//#endregion
|
|
393
|
+
export { DEFAULT_PLACEHOLDER, PUSH_DB_NAME, PUSH_PAYLOAD_VERSION, PUSH_STORE_NAME, base64UrlToBytes, bytesToBase64Url, defaultStore, getBridgedToken, idbStore, installPushHandlers, isPushPayload, memoryStore, notificationOptions, sameBytes };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/services/tabs/tabs-broker-worker.d.ts
|
|
2
|
+
/** Attach a connecting tab's port. Exported for the node test harness, which
|
|
3
|
+
* drives the broker with fake MessagePort pairs instead of a SharedWorker. */
|
|
4
|
+
declare function handleConnect(port: MessagePort): void;
|
|
5
|
+
/** Test-only: wipe module state so each test starts with a fresh broker. */
|
|
6
|
+
declare function __resetBrokerForTests(): void;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { __resetBrokerForTests, handleConnect };
|