@spooky-sync/core 0.0.1-canary.30 → 0.0.1-canary.301

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/AGENTS.md +81 -0
  2. package/README.md +56 -16
  3. package/dist/index.d.ts +2026 -137
  4. package/dist/index.js +14939 -2432
  5. package/dist/live.d.ts +141 -0
  6. package/dist/live.js +709 -0
  7. package/dist/otel/index.d.ts +1 -1
  8. package/dist/otel/index.js +3 -3
  9. package/dist/pure.d.ts +329 -0
  10. package/dist/pure.js +24 -0
  11. package/dist/sqlite-open.js +358 -0
  12. package/dist/sqlite-worker.d.ts +1 -0
  13. package/dist/sqlite-worker.js +439 -0
  14. package/dist/sw.d.ts +207 -0
  15. package/dist/sw.js +393 -0
  16. package/dist/tabs-broker-worker.d.ts +8 -0
  17. package/dist/tabs-broker-worker.js +472 -0
  18. package/dist/types.d.ts +1765 -38
  19. package/dist/worker-edges.js +423 -0
  20. package/dist/worker-impersonation.js +60 -0
  21. package/dist/worker-ref-tables.d.ts +46 -0
  22. package/dist/worker-types.d.ts +112 -0
  23. package/dist/worker-types.js +73 -0
  24. package/package.json +27 -7
  25. package/scripts/check-broker-bundle.mjs +33 -0
  26. package/scripts/check-worker-entries.mjs +172 -0
  27. package/skills/sp00ky-core/SKILL.md +31 -6
  28. package/skills/sp00ky-core/references/config.md +6 -0
  29. package/src/boot/auth-flip.saga.test.ts +82 -0
  30. package/src/boot/auth-flip.saga.ts +62 -0
  31. package/src/boot/boot.saga.test.ts +158 -0
  32. package/src/boot/boot.saga.ts +131 -0
  33. package/src/boot/bucket-switch.saga.test.ts +187 -0
  34. package/src/boot/bucket-switch.saga.ts +133 -0
  35. package/src/boot/preload.saga.test.ts +51 -0
  36. package/src/boot/preload.saga.ts +31 -0
  37. package/src/bucket-blurhash.test.ts +148 -0
  38. package/src/build-globals.d.ts +12 -0
  39. package/src/client/bucket-handle.test.ts +117 -0
  40. package/src/client/bucket-handle.ts +227 -0
  41. package/src/client/router.test.ts +58 -0
  42. package/src/client/router.ts +100 -0
  43. package/src/client/runtime.test.ts +195 -0
  44. package/src/client/runtime.ts +296 -0
  45. package/src/client/services.ts +370 -0
  46. package/src/client/sp00ky-client.test.ts +303 -0
  47. package/src/client/sp00ky-client.ts +586 -0
  48. package/src/events/events.test.ts +2 -1
  49. package/src/events/index.ts +3 -0
  50. package/src/index.ts +69 -1
  51. package/src/kernel/constants.test.ts +19 -0
  52. package/src/kernel/constants.ts +80 -0
  53. package/src/kernel/effects.test.ts +40 -0
  54. package/src/kernel/effects.ts +161 -0
  55. package/src/kernel/events.ts +57 -0
  56. package/src/kernel/interpreter.test.ts +116 -0
  57. package/src/kernel/interpreter.ts +148 -0
  58. package/src/kernel/saga.test.ts +89 -0
  59. package/src/kernel/saga.ts +73 -0
  60. package/src/live/index.test.ts +478 -0
  61. package/src/live/index.ts +1013 -0
  62. package/src/modules/app-release/index.test.ts +125 -0
  63. package/src/modules/app-release/index.ts +209 -0
  64. package/src/modules/auth/auth.local-first.test.ts +141 -0
  65. package/src/modules/auth/events/index.ts +9 -2
  66. package/src/modules/auth/impersonation-banner.ts +443 -0
  67. package/src/modules/auth/impersonation.test.ts +355 -0
  68. package/src/modules/auth/impersonation.ts +179 -0
  69. package/src/modules/auth/index.ts +386 -17
  70. package/src/modules/crdt/crdt-field.ts +294 -0
  71. package/src/modules/crdt/crdt-hydration.test.ts +210 -0
  72. package/src/modules/crdt/crdt-reconnect.test.ts +195 -0
  73. package/src/modules/crdt/index.ts +463 -0
  74. package/src/modules/crdt/loro-loader.ts +25 -0
  75. package/src/modules/devtools/flags.ts +349 -0
  76. package/src/modules/devtools/index.ts +746 -95
  77. package/src/modules/devtools/mutations.test.ts +92 -0
  78. package/src/modules/devtools/mutations.ts +160 -0
  79. package/src/modules/devtools/notify-throttle.test.ts +166 -0
  80. package/src/modules/devtools/state-shape.test.ts +263 -0
  81. package/src/modules/devtools/storage-info.test.ts +79 -0
  82. package/src/modules/devtools/storage-info.ts +168 -0
  83. package/src/modules/devtools/versions.test.ts +74 -0
  84. package/src/modules/devtools/versions.ts +110 -0
  85. package/src/modules/feature-flag/index.test.ts +251 -0
  86. package/src/modules/feature-flag/index.ts +316 -0
  87. package/src/modules/ref-tables.test.ts +91 -0
  88. package/src/modules/ref-tables.ts +88 -0
  89. package/src/modules/web-push/index.test.ts +824 -0
  90. package/src/modules/web-push/index.ts +1027 -0
  91. package/src/mutation/jobs.test.ts +25 -0
  92. package/src/mutation/jobs.ts +41 -0
  93. package/src/mutation/mutation-id.test.ts +37 -0
  94. package/src/mutation/mutation-id.ts +35 -0
  95. package/src/mutation/pending-writes.integration.test.ts +175 -0
  96. package/src/mutation/push.saga.test.ts +285 -0
  97. package/src/mutation/push.saga.ts +217 -0
  98. package/src/mutation/rows.test.ts +231 -0
  99. package/src/mutation/rows.ts +427 -0
  100. package/src/mutation/tray.saga.test.ts +63 -0
  101. package/src/mutation/tray.saga.ts +48 -0
  102. package/src/mutation/write.saga.test.ts +316 -0
  103. package/src/mutation/write.saga.ts +268 -0
  104. package/src/otel/index.ts +10 -7
  105. package/src/pure.ts +65 -0
  106. package/src/push/types.ts +174 -0
  107. package/src/query/env.test.ts +23 -0
  108. package/src/query/env.ts +56 -0
  109. package/src/query/fetch.saga.test.ts +199 -0
  110. package/src/query/fetch.saga.ts +120 -0
  111. package/src/query/hash.test.ts +14 -0
  112. package/src/query/hash.ts +18 -0
  113. package/src/query/id-set-plan.test.ts +132 -0
  114. package/src/query/lifecycle.saga.test.ts +274 -0
  115. package/src/query/lifecycle.saga.ts +159 -0
  116. package/src/query/materialize.saga.test.ts +115 -0
  117. package/src/query/materialize.saga.ts +84 -0
  118. package/src/query/materialize.test.ts +58 -0
  119. package/src/query/materialize.ts +57 -0
  120. package/src/query/membership.saga.test.ts +350 -0
  121. package/src/query/membership.saga.ts +278 -0
  122. package/src/query/membership.test.ts +118 -0
  123. package/src/query/membership.ts +201 -0
  124. package/src/query/register.saga.test.ts +293 -0
  125. package/src/query/register.saga.ts +226 -0
  126. package/src/query/render-set.test.ts +47 -0
  127. package/src/query/render-set.ts +71 -0
  128. package/src/query/sql.test.ts +63 -0
  129. package/src/query/sql.ts +93 -0
  130. package/src/query/window-query.test.ts +70 -0
  131. package/src/query/window-query.ts +195 -0
  132. package/src/services/blobs/blob-cache.test.ts +359 -0
  133. package/src/services/blobs/blob-cache.ts +603 -0
  134. package/src/services/blobs/blob-manifest.ts +227 -0
  135. package/src/services/blobs/blob-store.test.ts +77 -0
  136. package/src/services/blobs/blob-store.ts +359 -0
  137. package/src/services/blobs/blob.fixture.ts +90 -0
  138. package/src/services/blobs/index.ts +70 -0
  139. package/src/services/database/cache-engine.ts +199 -0
  140. package/src/services/database/connection-supervisor.test.ts +318 -0
  141. package/src/services/database/connection-supervisor.ts +422 -0
  142. package/src/services/database/database.query-timeout.test.ts +140 -0
  143. package/src/services/database/database.responses.test.ts +43 -0
  144. package/src/services/database/database.ts +146 -61
  145. package/src/services/database/engine-factory.ts +33 -0
  146. package/src/services/database/errors.ts +34 -0
  147. package/src/services/database/events/index.ts +2 -1
  148. package/src/services/database/index.ts +7 -0
  149. package/src/services/database/landed-body-clear.integration.test.ts +236 -0
  150. package/src/services/database/local-migrator.integration.test.ts +110 -0
  151. package/src/services/database/local-migrator.ts +59 -16
  152. package/src/services/database/local.test.ts +64 -0
  153. package/src/services/database/local.ts +487 -63
  154. package/src/services/database/plan-render.test.ts +159 -0
  155. package/src/services/database/plan-render.ts +108 -0
  156. package/src/services/database/relation-resolver.test.ts +467 -0
  157. package/src/services/database/relation-resolver.ts +0 -0
  158. package/src/services/database/remote.ts +213 -6
  159. package/src/services/database/sqlite-cache-engine.test.ts +617 -0
  160. package/src/services/database/sqlite-cache-engine.timeout.test.ts +61 -0
  161. package/src/services/database/sqlite-cache-engine.ts +1372 -0
  162. package/src/services/database/sqlite-devtools-queries.integration.test.ts +143 -0
  163. package/src/services/database/sqlite-devtools-queries.test.ts +154 -0
  164. package/src/services/database/sqlite-lock-verify.test.ts +33 -0
  165. package/src/services/database/sqlite-lock-verify.ts +45 -0
  166. package/src/services/database/sqlite-open.test.ts +150 -0
  167. package/src/services/database/sqlite-open.ts +164 -0
  168. package/src/services/database/sqlite-plan-sql.test.ts +139 -0
  169. package/src/services/database/sqlite-plan-sql.ts +179 -0
  170. package/src/services/database/sqlite-projection.test.ts +121 -0
  171. package/src/services/database/sqlite-select.integration.test.ts +185 -0
  172. package/src/services/database/sqlite-select.test.ts +246 -0
  173. package/src/services/database/sqlite-select.ts +131 -0
  174. package/src/services/database/sqlite-transport.fixture.ts +30 -0
  175. package/src/services/database/sqlite-transport.ts +224 -0
  176. package/src/services/database/sqlite-worker.ts +437 -0
  177. package/src/services/database/surql-translate.ts +416 -0
  178. package/src/services/database/surreal-cache-engine.ts +161 -0
  179. package/src/services/logger/index.test.ts +132 -0
  180. package/src/services/logger/index.ts +189 -7
  181. package/src/services/persistence/localstorage.ts +2 -2
  182. package/src/services/persistence/resilient.ts +10 -3
  183. package/src/services/persistence/surrealdb.ts +4 -4
  184. package/src/services/stream-processor/index.ts +779 -67
  185. package/src/services/stream-processor/permissions.test.ts +47 -0
  186. package/src/services/stream-processor/permissions.ts +53 -0
  187. package/src/services/stream-processor/stream-processor.batch.test.ts +186 -0
  188. package/src/services/stream-processor/stream-processor.prime.test.ts +198 -0
  189. package/src/services/stream-processor/stream-processor.reset.test.ts +226 -0
  190. package/src/services/stream-processor/stream-processor.test.ts +1 -1
  191. package/src/services/stream-processor/wasm-types.ts +58 -2
  192. package/src/services/tabs/broker-client.ts +283 -0
  193. package/src/services/tabs/broker.test.ts +327 -0
  194. package/src/services/tabs/coordinator.test.ts +386 -0
  195. package/src/services/tabs/coordinator.ts +638 -0
  196. package/src/services/tabs/fake-ports.fixture.ts +112 -0
  197. package/src/services/tabs/leader-locks.ts +75 -0
  198. package/src/services/tabs/protocol.ts +262 -0
  199. package/src/services/tabs/support.ts +36 -0
  200. package/src/services/tabs/tabs-broker-worker.ts +640 -0
  201. package/src/sp00ky.ts +5 -395
  202. package/src/state/client-state.ts +200 -0
  203. package/src/state/lifecycle.test.ts +118 -0
  204. package/src/state/lifecycle.ts +86 -0
  205. package/src/state/reducers.test.ts +304 -0
  206. package/src/state/reducers.ts +526 -0
  207. package/src/state/selectors.test.ts +167 -0
  208. package/src/state/selectors.ts +216 -0
  209. package/src/sw/index.test.ts +582 -0
  210. package/src/sw/index.ts +607 -0
  211. package/src/sw/store.ts +96 -0
  212. package/src/sync/connection.saga.test.ts +86 -0
  213. package/src/sync/connection.saga.ts +85 -0
  214. package/src/sync/live.saga.test.ts +178 -0
  215. package/src/sync/live.saga.ts +107 -0
  216. package/src/sync/policy.health.test.ts +56 -0
  217. package/src/sync/policy.test.ts +659 -0
  218. package/src/sync/policy.ts +502 -0
  219. package/src/sync/poll.saga.test.ts +51 -0
  220. package/src/sync/poll.saga.ts +36 -0
  221. package/src/sync/tabs.saga.test.ts +78 -0
  222. package/src/sync/tabs.saga.ts +96 -0
  223. package/src/testing/adapters.ts +133 -0
  224. package/src/testing/build.ts +63 -0
  225. package/src/testing/fake-services.ts +48 -0
  226. package/src/testing/run-pure.test.ts +92 -0
  227. package/src/testing/run-pure.ts +128 -0
  228. package/src/testing/services.ts +17 -0
  229. package/src/types.ts +624 -10
  230. package/src/utils/blurhash.ts +90 -0
  231. package/src/utils/edges.ts +39 -0
  232. package/src/utils/error-classification.test.ts +54 -0
  233. package/src/utils/error-classification.ts +25 -0
  234. package/src/utils/index.ts +75 -9
  235. package/src/utils/parser.test.ts +67 -102
  236. package/src/utils/parser.ts +64 -2
  237. package/src/utils/semver.test.ts +32 -0
  238. package/src/utils/semver.ts +30 -0
  239. package/src/utils/sha256.ts +15 -0
  240. package/src/utils/surql.ts +30 -18
  241. package/src/utils/utils.test.ts +2 -1
  242. package/tsdown.config.ts +117 -3
  243. package/vitest.config.ts +48 -0
  244. package/src/modules/cache/index.ts +0 -241
  245. package/src/modules/cache/types.ts +0 -19
  246. package/src/modules/data/data.test.ts +0 -58
  247. package/src/modules/data/index.ts +0 -879
  248. package/src/modules/sync/engine.ts +0 -173
  249. package/src/modules/sync/events/index.ts +0 -77
  250. package/src/modules/sync/index.ts +0 -3
  251. package/src/modules/sync/queue/index.ts +0 -2
  252. package/src/modules/sync/queue/queue-down.ts +0 -89
  253. package/src/modules/sync/queue/queue-up.ts +0 -223
  254. package/src/modules/sync/scheduler.ts +0 -84
  255. package/src/modules/sync/sync.ts +0 -421
  256. package/src/modules/sync/utils.test.ts +0 -311
  257. 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 };