@spooky-sync/core 0.0.1-canary.30 → 0.0.1-canary.300
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 +2026 -137
- package/dist/index.js +14939 -2432
- package/dist/live.d.ts +141 -0
- package/dist/live.js +709 -0
- package/dist/otel/index.d.ts +1 -1
- package/dist/otel/index.js +3 -3
- package/dist/pure.d.ts +329 -0
- package/dist/pure.js +24 -0
- package/dist/sqlite-open.js +358 -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 +1765 -38
- package/dist/worker-edges.js +423 -0
- package/dist/worker-impersonation.js +60 -0
- package/dist/worker-ref-tables.d.ts +46 -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 +82 -0
- package/src/boot/auth-flip.saga.ts +62 -0
- package/src/boot/boot.saga.test.ts +158 -0
- package/src/boot/boot.saga.ts +131 -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.ts +370 -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 +161 -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 +478 -0
- package/src/live/index.ts +1013 -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 +141 -0
- package/src/modules/auth/events/index.ts +9 -2
- package/src/modules/auth/impersonation-banner.ts +443 -0
- package/src/modules/auth/impersonation.test.ts +355 -0
- package/src/modules/auth/impersonation.ts +179 -0
- package/src/modules/auth/index.ts +386 -17
- 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 +251 -0
- package/src/modules/feature-flag/index.ts +316 -0
- package/src/modules/ref-tables.test.ts +91 -0
- package/src/modules/ref-tables.ts +88 -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 +65 -0
- package/src/push/types.ts +174 -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/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 +293 -0
- package/src/query/register.saga.ts +226 -0
- package/src/query/render-set.test.ts +47 -0
- package/src/query/render-set.ts +71 -0
- package/src/query/sql.test.ts +63 -0
- package/src/query/sql.ts +93 -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 +467 -0
- package/src/services/database/relation-resolver.ts +0 -0
- package/src/services/database/remote.ts +213 -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 +139 -0
- package/src/services/database/sqlite-plan-sql.ts +179 -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 +246 -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 +779 -67
- 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 +118 -0
- package/src/state/lifecycle.ts +86 -0
- package/src/state/reducers.test.ts +304 -0
- package/src/state/reducers.ts +526 -0
- package/src/state/selectors.test.ts +167 -0
- package/src/state/selectors.ts +216 -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 +86 -0
- package/src/sync/connection.saga.ts +85 -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 +659 -0
- package/src/sync/policy.ts +502 -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 +75 -9
- 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 +2 -1
- 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
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { a as renderOrderSql, d as stableKey, i as projectedDataSql, n as openDb, o as renderWhereSql, r as project, s as reviveRow, u as resolveRelations } from "./sqlite-open.js";
|
|
2
|
+
import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
|
|
3
|
+
|
|
4
|
+
//#region src/services/database/sqlite-lock-verify.ts
|
|
5
|
+
const LOCK_VERIFY_TIMEOUT_MS = 2e3;
|
|
6
|
+
async function verifyLockStillHeld$1(locks, name, fence, timeoutMs = LOCK_VERIFY_TIMEOUT_MS, warn = () => {}) {
|
|
7
|
+
if (!locks || !name || typeof locks.query !== "function") return;
|
|
8
|
+
let timer;
|
|
9
|
+
try {
|
|
10
|
+
const state = await Promise.race([locks.query(), new Promise((resolve) => {
|
|
11
|
+
timer = setTimeout(() => resolve(null), timeoutMs);
|
|
12
|
+
})]);
|
|
13
|
+
if (state === null) {
|
|
14
|
+
warn(`lock verification did not answer within ${timeoutMs}ms; trusting the steal callback`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (!(state.held ?? []).some((l) => l.name === name)) await fence("lock missing after suspected freeze");
|
|
18
|
+
} catch {} finally {
|
|
19
|
+
if (timer) clearTimeout(timer);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/services/database/sqlite-select.ts
|
|
25
|
+
function ensureTable(db, table) {
|
|
26
|
+
if (db.knownTables.has(table)) return;
|
|
27
|
+
db.run(`CREATE TABLE IF NOT EXISTS "${table}" (id TEXT PRIMARY KEY, data TEXT NOT NULL)`);
|
|
28
|
+
db.knownTables.add(table);
|
|
29
|
+
}
|
|
30
|
+
function execRows(db, sql, bind) {
|
|
31
|
+
return db.exec(sql, bind).map((r) => reviveRow(r.data));
|
|
32
|
+
}
|
|
33
|
+
/** Mirrors the engine's `selectByIds`: fetch by primary id, preserving `ids`
|
|
34
|
+
* order unless an ORDER BY overrides it. */
|
|
35
|
+
function selectByIds(db, table, ids, opts) {
|
|
36
|
+
if (ids.length === 0) return [];
|
|
37
|
+
ensureTable(db, table);
|
|
38
|
+
const keys = ids.map(stableKey);
|
|
39
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
40
|
+
const bind = [];
|
|
41
|
+
const dataCol = opts?.select ? projectedDataSql(opts.select, bind) : "data";
|
|
42
|
+
bind.push(...keys);
|
|
43
|
+
let sql = `SELECT ${dataCol} FROM "${table}" WHERE id IN (${placeholders})`;
|
|
44
|
+
if (opts?.orderBy && opts.orderBy.length > 0) sql += renderOrderSql(opts.orderBy);
|
|
45
|
+
let rows = execRows(db, sql, bind);
|
|
46
|
+
if (!opts?.orderBy || opts.orderBy.length === 0) {
|
|
47
|
+
const pos = new Map(keys.map((k, i) => [k, i]));
|
|
48
|
+
rows = rows.sort((a, b) => (pos.get(stableKey(a.id)) ?? 0) - (pos.get(stableKey(b.id)) ?? 0));
|
|
49
|
+
}
|
|
50
|
+
return rows;
|
|
51
|
+
}
|
|
52
|
+
/** Mirrors the engine's `fetchRelation` SQL exactly. */
|
|
53
|
+
function fetchRelation(db, req) {
|
|
54
|
+
ensureTable(db, req.table);
|
|
55
|
+
const keys = req.keys.map(stableKey);
|
|
56
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
57
|
+
const bind = [...keys];
|
|
58
|
+
const lhs = req.matchField === "id" ? "id" : `json_extract(data, '$.${req.matchField}')`;
|
|
59
|
+
let sql = `SELECT data FROM "${req.table}" WHERE ${lhs} IN (${placeholders})`;
|
|
60
|
+
if (req.where && req.where.length > 0) sql += ` AND ${renderWhereSql(req.where, bind, {})}`;
|
|
61
|
+
if (req.orderBy && req.orderBy.length > 0) sql += renderOrderSql(req.orderBy);
|
|
62
|
+
const rows = execRows(db, sql, bind);
|
|
63
|
+
return req.select ? rows.map((r) => project(r, req.select)) : rows;
|
|
64
|
+
}
|
|
65
|
+
async function executeSelect(plan, params, db) {
|
|
66
|
+
const counter = { n: 0 };
|
|
67
|
+
const fetcher = { fetchRelation: (req) => {
|
|
68
|
+
counter.n++;
|
|
69
|
+
return Promise.resolve(fetchRelation(db, req));
|
|
70
|
+
} };
|
|
71
|
+
if (plan.ids) {
|
|
72
|
+
const rows = selectByIds(db, plan.table, plan.ids, {
|
|
73
|
+
select: plan.select,
|
|
74
|
+
orderBy: plan.orderBy
|
|
75
|
+
});
|
|
76
|
+
await resolveRelations(rows, plan.relations, fetcher);
|
|
77
|
+
return {
|
|
78
|
+
rows,
|
|
79
|
+
relationFetches: counter.n
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
ensureTable(db, plan.table);
|
|
83
|
+
const bind = [];
|
|
84
|
+
let sql = `SELECT ${plan.select ? projectedDataSql(plan.select, bind) : "data"} FROM "${plan.table}"`;
|
|
85
|
+
if (plan.where && plan.where.length > 0) sql += ` WHERE ${renderWhereSql(plan.where, bind, params)}`;
|
|
86
|
+
if (plan.orderBy && plan.orderBy.length > 0) sql += renderOrderSql(plan.orderBy);
|
|
87
|
+
else sql += ` ORDER BY id`;
|
|
88
|
+
if (plan.limit !== void 0) sql += ` LIMIT ${Number(plan.limit)}`;
|
|
89
|
+
if (plan.offset !== void 0) sql += ` OFFSET ${Number(plan.offset)}`;
|
|
90
|
+
const rows = execRows(db, sql, bind);
|
|
91
|
+
await resolveRelations(rows, plan.relations, fetcher);
|
|
92
|
+
return {
|
|
93
|
+
rows,
|
|
94
|
+
relationFetches: counter.n
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/services/database/sqlite-worker.ts
|
|
100
|
+
/**
|
|
101
|
+
* Dedicated Web Worker that owns the SQLite-WASM handle. All DB access is
|
|
102
|
+
* funnelled through here (the main thread never touches the wasm module), which
|
|
103
|
+
* is also what the OPFS VFS requires — file access must happen off the main
|
|
104
|
+
* thread. Persistence uses the **OPFS SAHPool VFS**: durable, and (unlike the
|
|
105
|
+
* classic OPFS VFS) it does NOT require COOP/COEP cross-origin isolation
|
|
106
|
+
* headers, so host apps embedding the client need no server changes. When OPFS
|
|
107
|
+
* is unavailable it retries, then falls back to an in-memory DB and REPORTS the
|
|
108
|
+
* loss of durability (see `sqlite-open.ts`) instead of degrading silently.
|
|
109
|
+
*
|
|
110
|
+
* Message protocol (request/response keyed by `id`; replies go to the channel
|
|
111
|
+
* the request arrived on):
|
|
112
|
+
* { id, type: 'open', payload: { dbName, useOpfs, workerLockName? } }
|
|
113
|
+
* -> { id, ok, persisted, opfsError? }
|
|
114
|
+
* { id, type: 'exec', payload: { sql, bind } } -> { id, ok, rows }
|
|
115
|
+
* { id, type: 'run', payload: { sql, bind } } -> { id, ok }
|
|
116
|
+
* { id, type: 'batch', payload: [{ sql, bind }] } (atomic BEGIN/COMMIT)
|
|
117
|
+
* { id, type: 'select', payload: { plan, params } } -> { id, ok, rows, relationFetches }
|
|
118
|
+
* { id, type: 'close' }
|
|
119
|
+
* { id, type: 'shutdown' } (owner only: close + pauseVfs + self.close)
|
|
120
|
+
* { id, type: 'relock', payload: { workerLockName } } (owner only)
|
|
121
|
+
* { id, type: 'add-client', payload: { clientId } } + ev.ports[0] (owner only)
|
|
122
|
+
* { id, type: 'remove-client', payload: { clientId } } (owner only)
|
|
123
|
+
*
|
|
124
|
+
* Multi-client (shared-tabs mode): the OWNER (the leader tab's engine) speaks
|
|
125
|
+
* on the worker's own channel and controls the lifecycle. Follower tabs get a
|
|
126
|
+
* MessagePort each (`add-client`) and may only issue data ops
|
|
127
|
+
* (exec/run/batch/select). Every op, from any channel, runs through ONE op
|
|
128
|
+
* chain so an async `select` from one client can never interleave with another
|
|
129
|
+
* client's `batch` at the VFS layer.
|
|
130
|
+
*
|
|
131
|
+
* Leader fencing (shared-tabs mode): `open` may carry `workerLockName`, a Web
|
|
132
|
+
* Lock this worker acquires BEFORE opening the pool and holds for the DB's
|
|
133
|
+
* lifetime. The name is unique per leadership, so the lock being gone has
|
|
134
|
+
* exactly one meaning: the broker stole it because this tab was presumed dead
|
|
135
|
+
* (usually frozen). When that happens the worker FENCES itself: closes the DB,
|
|
136
|
+
* releases the OPFS sync access handles (pauseVfs), tells the owner, and
|
|
137
|
+
* refuses every further op. A frozen-then-thawed ex-leader therefore can never
|
|
138
|
+
* write the durable file after a new leader took over. The thaw gate below
|
|
139
|
+
* closes the wake-up ordering race where a queued write could run before the
|
|
140
|
+
* lock-loss callback fires.
|
|
141
|
+
*
|
|
142
|
+
* `select` executes a whole QueryPlan — table creation, base select, the full
|
|
143
|
+
* `.related()` tree (shared `resolveRelations`), and JSON row parsing — in ONE
|
|
144
|
+
* round-trip, returning structured-clone row objects. This is the first-load
|
|
145
|
+
* hot path; the per-statement ops remain for the write/shim paths.
|
|
146
|
+
*/
|
|
147
|
+
let db = null;
|
|
148
|
+
let pool = null;
|
|
149
|
+
/** Once fenced, every op is refused and the DB stays closed. Terminal. */
|
|
150
|
+
let fenced = false;
|
|
151
|
+
/** The per-leadership lock currently held (null = no fencing active). A
|
|
152
|
+
* relock (same-leader re-promotion after a broker restart) briefly holds two
|
|
153
|
+
* locks: the new one is installed here first, then the old one's own release
|
|
154
|
+
* is called, so there is no window where neither is held. */
|
|
155
|
+
let currentLock = null;
|
|
156
|
+
function workerLocks() {
|
|
157
|
+
const nav = globalThis.navigator;
|
|
158
|
+
return nav?.locks && typeof nav.locks.request === "function" ? nav.locks : null;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Acquire `name` exclusively without waiting. Resolves with a handle when
|
|
162
|
+
* granted, null when someone else holds it. If the request settles while the
|
|
163
|
+
* holder still believes it owns the lock, the broker stole it: fence.
|
|
164
|
+
*/
|
|
165
|
+
function acquireWorkerLock(name) {
|
|
166
|
+
const locks = workerLocks();
|
|
167
|
+
if (!locks) return Promise.resolve({
|
|
168
|
+
name,
|
|
169
|
+
release() {}
|
|
170
|
+
});
|
|
171
|
+
return new Promise((resolve) => {
|
|
172
|
+
let granted = false;
|
|
173
|
+
let intentional = false;
|
|
174
|
+
let releaseGrant = null;
|
|
175
|
+
locks.request(name, {
|
|
176
|
+
mode: "exclusive",
|
|
177
|
+
ifAvailable: true
|
|
178
|
+
}, (lock) => {
|
|
179
|
+
if (!lock) {
|
|
180
|
+
resolve(null);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
granted = true;
|
|
184
|
+
resolve({
|
|
185
|
+
name,
|
|
186
|
+
release() {
|
|
187
|
+
intentional = true;
|
|
188
|
+
releaseGrant?.();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
return new Promise((release) => {
|
|
192
|
+
releaseGrant = release;
|
|
193
|
+
});
|
|
194
|
+
}).then(() => {
|
|
195
|
+
if (granted && !intentional) fence("worker lock stolen");
|
|
196
|
+
}, () => {
|
|
197
|
+
if (granted && !intentional) fence("worker lock request failed");
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
function releaseHeldLock() {
|
|
202
|
+
currentLock?.release();
|
|
203
|
+
currentLock = null;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Terminal teardown after leadership loss. Closing the DB first makes
|
|
207
|
+
* pauseVfs legal (it throws while files are open); pausing releases the sync
|
|
208
|
+
* access handles so the NEW leader's open succeeds without waiting for this
|
|
209
|
+
* worker to be garbage collected.
|
|
210
|
+
*/
|
|
211
|
+
async function fence(reason) {
|
|
212
|
+
if (fenced) return;
|
|
213
|
+
fenced = true;
|
|
214
|
+
console.error(`[sp00ky] sqlite worker fenced (${reason}): leadership was taken over, closing the database and refusing further ops.`);
|
|
215
|
+
try {
|
|
216
|
+
db?.close();
|
|
217
|
+
} catch {}
|
|
218
|
+
db = null;
|
|
219
|
+
try {
|
|
220
|
+
pool?.pauseVfs?.();
|
|
221
|
+
} catch {}
|
|
222
|
+
pool = null;
|
|
223
|
+
try {
|
|
224
|
+
self.postMessage({
|
|
225
|
+
type: "lock-lost",
|
|
226
|
+
reason
|
|
227
|
+
});
|
|
228
|
+
} catch {}
|
|
229
|
+
releaseHeldLock();
|
|
230
|
+
self.close();
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* A frozen tab's worker resumes with its message queue intact, and a queued
|
|
234
|
+
* write could run BEFORE the lock-steal callback fires. Both the 1s interval
|
|
235
|
+
* and every op call `noteTick()`; whichever runs first after a long gap kicks
|
|
236
|
+
* off a lock verification, and ops await it before touching the DB. The gap
|
|
237
|
+
* threshold must stay BELOW the broker's pong timeout: a freeze shorter than
|
|
238
|
+
* the threshold cannot have triggered a steal yet, so skipping the check for
|
|
239
|
+
* small gaps is safe.
|
|
240
|
+
*/
|
|
241
|
+
const FREEZE_SUSPECT_MS = 1e4;
|
|
242
|
+
let lastTickAt = performance.now();
|
|
243
|
+
let thawVerification = null;
|
|
244
|
+
function noteTick() {
|
|
245
|
+
const now = performance.now();
|
|
246
|
+
const gap = now - lastTickAt;
|
|
247
|
+
lastTickAt = now;
|
|
248
|
+
if (gap > FREEZE_SUSPECT_MS && currentLock && !fenced && !thawVerification) thawVerification = verifyLockStillHeld().finally(() => {
|
|
249
|
+
thawVerification = null;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
setInterval(noteTick, 1e3);
|
|
253
|
+
function verifyLockStillHeld() {
|
|
254
|
+
return verifyLockStillHeld$1(workerLocks(), currentLock?.name, (reason) => fence(reason), void 0, (msg) => console.warn(`[sqlite-worker] ${msg}`));
|
|
255
|
+
}
|
|
256
|
+
async function open(dbName, useOpfs, systemTables = [], workerLockName, openOptions) {
|
|
257
|
+
if (workerLockName) {
|
|
258
|
+
const handle = await acquireWorkerLock(workerLockName);
|
|
259
|
+
if (!handle) throw new Error("worker-lock-unavailable");
|
|
260
|
+
releaseHeldLock();
|
|
261
|
+
currentLock = handle;
|
|
262
|
+
}
|
|
263
|
+
const result = await openDb(await sqlite3InitModule(), dbName, useOpfs, openOptions ?? {});
|
|
264
|
+
db = result.db;
|
|
265
|
+
pool = result.pool ?? null;
|
|
266
|
+
for (const t of systemTables) db.exec({ sql: `CREATE TABLE IF NOT EXISTS "${t}" (id TEXT PRIMARY KEY, data TEXT NOT NULL)` });
|
|
267
|
+
try {
|
|
268
|
+
db.exec({ sql: "PRAGMA busy_timeout = 5000; PRAGMA cache_size = -32000;" });
|
|
269
|
+
} catch {}
|
|
270
|
+
return {
|
|
271
|
+
persisted: result.persisted,
|
|
272
|
+
opfsError: result.opfsError
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function closeDb() {
|
|
276
|
+
db?.close();
|
|
277
|
+
db = null;
|
|
278
|
+
pool = null;
|
|
279
|
+
selectDb.knownTables.clear();
|
|
280
|
+
releaseHeldLock();
|
|
281
|
+
}
|
|
282
|
+
function exec(sql, bind) {
|
|
283
|
+
if (!db) throw new Error("sqlite: DB not open");
|
|
284
|
+
return db.exec({
|
|
285
|
+
sql,
|
|
286
|
+
bind,
|
|
287
|
+
rowMode: "object",
|
|
288
|
+
returnValue: "resultRows"
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
function run(sql, bind) {
|
|
292
|
+
if (!db) throw new Error("sqlite: DB not open");
|
|
293
|
+
db.exec({
|
|
294
|
+
sql,
|
|
295
|
+
bind
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
function batch(stmts) {
|
|
299
|
+
if (!db) throw new Error("sqlite: DB not open");
|
|
300
|
+
db.exec({ sql: "BEGIN" });
|
|
301
|
+
try {
|
|
302
|
+
for (const s of stmts) db.exec({
|
|
303
|
+
sql: s.sql,
|
|
304
|
+
bind: s.bind
|
|
305
|
+
});
|
|
306
|
+
db.exec({ sql: "COMMIT" });
|
|
307
|
+
} catch (e) {
|
|
308
|
+
try {
|
|
309
|
+
db.exec({ sql: "ROLLBACK" });
|
|
310
|
+
} catch {}
|
|
311
|
+
throw e;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/** DB handle for `executeSelect` (see `sqlite-select.ts`, the unit-testable
|
|
315
|
+
* plan executor). `knownTables` is cleared on open/close, which covers both
|
|
316
|
+
* the solo fresh-worker path and the shared-mode same-worker bucket switch. */
|
|
317
|
+
const selectDb = {
|
|
318
|
+
exec: (sql, bind) => exec(sql, bind),
|
|
319
|
+
run,
|
|
320
|
+
knownTables: /* @__PURE__ */ new Set()
|
|
321
|
+
};
|
|
322
|
+
/** Follower client ports, keyed by the clientId the owner assigned. */
|
|
323
|
+
const clients = /* @__PURE__ */ new Map();
|
|
324
|
+
/** Data ops a follower client port may issue; lifecycle stays owner-only. */
|
|
325
|
+
const CLIENT_OPS = new Set([
|
|
326
|
+
"exec",
|
|
327
|
+
"run",
|
|
328
|
+
"batch",
|
|
329
|
+
"select"
|
|
330
|
+
]);
|
|
331
|
+
/**
|
|
332
|
+
* ONE chain for every op from every channel. The handlers for `open` and
|
|
333
|
+
* `select` are async; without this, a follower's select could interleave with
|
|
334
|
+
* the owner's batch mid-transaction at the VFS layer. (Solo mode kept this
|
|
335
|
+
* invariant on the main thread via the engine's opQueue; with multiple client
|
|
336
|
+
* ports only the worker can.)
|
|
337
|
+
*/
|
|
338
|
+
let opChain = Promise.resolve();
|
|
339
|
+
async function handle(type, payload, source) {
|
|
340
|
+
if (fenced) throw new Error("sqlite: fenced, leadership was taken over");
|
|
341
|
+
if (source === "client" && !CLIENT_OPS.has(type)) throw new Error(`sqlite worker: op ${type} is not allowed on a client port`);
|
|
342
|
+
switch (type) {
|
|
343
|
+
case "open":
|
|
344
|
+
selectDb.knownTables.clear();
|
|
345
|
+
{
|
|
346
|
+
const result = await open(payload.dbName, payload.useOpfs, payload.systemTables, payload.workerLockName, payload.openOptions);
|
|
347
|
+
for (const t of payload.systemTables ?? []) selectDb.knownTables.add(t);
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
350
|
+
case "relock": {
|
|
351
|
+
const handle = await acquireWorkerLock(payload.workerLockName);
|
|
352
|
+
if (!handle) throw new Error("worker-lock-unavailable");
|
|
353
|
+
releaseHeldLock();
|
|
354
|
+
currentLock = handle;
|
|
355
|
+
return {};
|
|
356
|
+
}
|
|
357
|
+
case "select": return executeSelect(payload.plan, payload.params ?? {}, selectDb);
|
|
358
|
+
case "exec": return { rows: exec(payload.sql, payload.bind) };
|
|
359
|
+
case "run":
|
|
360
|
+
run(payload.sql, payload.bind);
|
|
361
|
+
return {};
|
|
362
|
+
case "batch":
|
|
363
|
+
batch(payload);
|
|
364
|
+
return {};
|
|
365
|
+
case "close":
|
|
366
|
+
closeDb();
|
|
367
|
+
return {};
|
|
368
|
+
case "shutdown":
|
|
369
|
+
try {
|
|
370
|
+
db?.close();
|
|
371
|
+
} catch {}
|
|
372
|
+
db = null;
|
|
373
|
+
try {
|
|
374
|
+
pool?.pauseVfs?.();
|
|
375
|
+
} catch {}
|
|
376
|
+
pool = null;
|
|
377
|
+
releaseHeldLock();
|
|
378
|
+
queueMicrotask(() => self.close());
|
|
379
|
+
return {};
|
|
380
|
+
case "remove-client":
|
|
381
|
+
clients.get(payload.clientId)?.close();
|
|
382
|
+
clients.delete(payload.clientId);
|
|
383
|
+
return {};
|
|
384
|
+
default: throw new Error(`sqlite worker: unknown message ${type}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function dispatch(target, data, source) {
|
|
388
|
+
const { id, type, payload } = data ?? {};
|
|
389
|
+
noteTick();
|
|
390
|
+
const t0 = performance.now();
|
|
391
|
+
const runOp = async () => {
|
|
392
|
+
try {
|
|
393
|
+
if (thawVerification) await thawVerification;
|
|
394
|
+
const result = await handle(type, payload, source);
|
|
395
|
+
target.postMessage({
|
|
396
|
+
id,
|
|
397
|
+
ok: true,
|
|
398
|
+
wt: performance.now() - t0,
|
|
399
|
+
...result
|
|
400
|
+
});
|
|
401
|
+
} catch (err) {
|
|
402
|
+
target.postMessage({
|
|
403
|
+
id,
|
|
404
|
+
ok: false,
|
|
405
|
+
error: err instanceof Error ? err.message : String(err)
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
opChain = opChain.then(runOp, runOp);
|
|
410
|
+
}
|
|
411
|
+
self.onmessage = (ev) => {
|
|
412
|
+
const data = ev.data ?? {};
|
|
413
|
+
if (data.type === "add-client") {
|
|
414
|
+
const port = ev.ports?.[0];
|
|
415
|
+
const clientId = data.payload?.clientId;
|
|
416
|
+
if (!port || !clientId) {
|
|
417
|
+
self.postMessage({
|
|
418
|
+
id: data.id,
|
|
419
|
+
ok: false,
|
|
420
|
+
error: "sqlite worker: add-client needs a clientId and a transferred port"
|
|
421
|
+
});
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
clients.get(clientId)?.close();
|
|
425
|
+
clients.set(clientId, port);
|
|
426
|
+
port.onmessage = (pe) => dispatch(port, pe.data, "client");
|
|
427
|
+
port.start?.();
|
|
428
|
+
self.postMessage({
|
|
429
|
+
id: data.id,
|
|
430
|
+
ok: true,
|
|
431
|
+
wt: 0
|
|
432
|
+
});
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
dispatch(self, data, "owner");
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
//#endregion
|
|
439
|
+
export { };
|
package/dist/sw.d.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { LiveFeed, LiveQuery } from "./live.js";
|
|
2
|
+
import { a as PushNotificationAction, c as PushPayloadKind, d as base64UrlToBytes, f as bytesToBase64Url, i as PushNotification, l as PushUrgency, m as sameBytes, n as BridgeTokenMessage, o as PushOp, p as isPushPayload, r as PUSH_PAYLOAD_VERSION, s as PushPayload, t as BridgeSignOutMessage, u as WebPushClientMessage } from "./worker-types.js";
|
|
3
|
+
|
|
4
|
+
//#region src/sw/store.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The service worker's tiny key-value store: IndexedDB database `sp00ky-push`,
|
|
8
|
+
* object store `kv`. Holds the bridged session (`token`) so a push that wakes
|
|
9
|
+
* the worker with no page open can still read live data. Falls back to memory
|
|
10
|
+
* where IndexedDB does not exist (Node, tests).
|
|
11
|
+
*/
|
|
12
|
+
interface KvStore {
|
|
13
|
+
get<T>(key: string): Promise<T | undefined>;
|
|
14
|
+
set(key: string, value: unknown): Promise<void>;
|
|
15
|
+
delete(key: string): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
declare const PUSH_DB_NAME = "sp00ky-push";
|
|
18
|
+
declare const PUSH_STORE_NAME = "kv";
|
|
19
|
+
declare function memoryStore(): KvStore;
|
|
20
|
+
type IdbFactoryLike = {
|
|
21
|
+
open(name: string, version?: number): IDBOpenDBRequest;
|
|
22
|
+
};
|
|
23
|
+
declare function idbStore(dbName?: string, storeName?: string, factory?: IdbFactoryLike): KvStore;
|
|
24
|
+
/** IndexedDB when it exists, else one process-wide memory store. */
|
|
25
|
+
declare function defaultStore(): KvStore;
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/sw/index.d.ts
|
|
28
|
+
interface SwNotification {
|
|
29
|
+
title: string;
|
|
30
|
+
tag: string;
|
|
31
|
+
body?: string;
|
|
32
|
+
data?: any;
|
|
33
|
+
close(): void;
|
|
34
|
+
}
|
|
35
|
+
interface SwPushSubscription {
|
|
36
|
+
endpoint: string;
|
|
37
|
+
options?: {
|
|
38
|
+
applicationServerKey?: ArrayBuffer | null;
|
|
39
|
+
};
|
|
40
|
+
toJSON(): unknown;
|
|
41
|
+
}
|
|
42
|
+
interface SwRegistration {
|
|
43
|
+
scope: string;
|
|
44
|
+
showNotification(title: string, options?: Record<string, unknown>): Promise<void>;
|
|
45
|
+
getNotifications(filter?: {
|
|
46
|
+
tag?: string;
|
|
47
|
+
}): Promise<SwNotification[]>;
|
|
48
|
+
pushManager?: {
|
|
49
|
+
getSubscription(): Promise<SwPushSubscription | null>;
|
|
50
|
+
subscribe(options: {
|
|
51
|
+
userVisibleOnly: boolean;
|
|
52
|
+
applicationServerKey: unknown;
|
|
53
|
+
}): Promise<SwPushSubscription>;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
interface SwWindowClient {
|
|
57
|
+
url: string;
|
|
58
|
+
visibilityState?: string;
|
|
59
|
+
focused?: boolean;
|
|
60
|
+
focus(): Promise<unknown>;
|
|
61
|
+
navigate?(url: string): Promise<unknown>;
|
|
62
|
+
postMessage(message: unknown): void;
|
|
63
|
+
}
|
|
64
|
+
interface SwClients {
|
|
65
|
+
matchAll(options?: {
|
|
66
|
+
type?: string;
|
|
67
|
+
includeUncontrolled?: boolean;
|
|
68
|
+
}): Promise<SwWindowClient[]>;
|
|
69
|
+
openWindow(url: string): Promise<unknown>;
|
|
70
|
+
}
|
|
71
|
+
interface SwScope {
|
|
72
|
+
registration: SwRegistration;
|
|
73
|
+
clients: SwClients;
|
|
74
|
+
location: {
|
|
75
|
+
origin: string;
|
|
76
|
+
};
|
|
77
|
+
navigator?: {
|
|
78
|
+
userAgent?: string;
|
|
79
|
+
};
|
|
80
|
+
addEventListener(type: string, listener: (event: any) => void): void;
|
|
81
|
+
removeEventListener(type: string, listener: (event: any) => void): void;
|
|
82
|
+
}
|
|
83
|
+
interface SwExtendableEvent {
|
|
84
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
85
|
+
}
|
|
86
|
+
interface SwPushEvent extends SwExtendableEvent {
|
|
87
|
+
data: {
|
|
88
|
+
json(): unknown;
|
|
89
|
+
text(): string;
|
|
90
|
+
} | null;
|
|
91
|
+
}
|
|
92
|
+
interface SwNotificationEvent extends SwExtendableEvent {
|
|
93
|
+
notification: SwNotification;
|
|
94
|
+
action: string;
|
|
95
|
+
}
|
|
96
|
+
interface SwPushSubscriptionChangeEvent extends SwExtendableEvent {
|
|
97
|
+
oldSubscription?: SwPushSubscription | null;
|
|
98
|
+
newSubscription?: SwPushSubscription | null;
|
|
99
|
+
}
|
|
100
|
+
/** What `showNotification` gets: a title plus any notification option. */
|
|
101
|
+
interface NotificationSpec extends PushNotification {
|
|
102
|
+
title: string;
|
|
103
|
+
}
|
|
104
|
+
/** The session a page bridged over (`db.webPush.bridge()`). Fits `createLiveFeed` as is. */
|
|
105
|
+
interface BridgedToken {
|
|
106
|
+
token: string;
|
|
107
|
+
userId: string;
|
|
108
|
+
endpoint: string;
|
|
109
|
+
namespace: string;
|
|
110
|
+
database: string;
|
|
111
|
+
publicKey?: string;
|
|
112
|
+
pushEndpoint?: string;
|
|
113
|
+
/** Epoch ms it was stored. */
|
|
114
|
+
at: number;
|
|
115
|
+
}
|
|
116
|
+
interface PushHandlerContext {
|
|
117
|
+
event: SwPushEvent;
|
|
118
|
+
registration: SwRegistration;
|
|
119
|
+
/** Show a notification the way the bridge would (tag, url and payload in `data`). */
|
|
120
|
+
show(spec: NotificationSpec): Promise<void>;
|
|
121
|
+
showPlaceholder(): Promise<void>;
|
|
122
|
+
/** Visible windows of this app, right now. */
|
|
123
|
+
visibleClients(): Promise<SwWindowClient[]>;
|
|
124
|
+
/** The bridged session, or null (signed out, never bridged, expired). */
|
|
125
|
+
bridged(): Promise<BridgedToken | null>;
|
|
126
|
+
}
|
|
127
|
+
interface LiveRenderOptions<Row = Record<string, any>> {
|
|
128
|
+
/** Open a feed for the bridged session: `(t) => createLiveFeed(t)`. */
|
|
129
|
+
feed(bridged: BridgedToken): LiveFeed | Promise<LiveFeed>;
|
|
130
|
+
/** What to read for this user (a built query, or `{ surql, params }`). */
|
|
131
|
+
query(userId: string, payload: PushPayload): LiveQuery;
|
|
132
|
+
/** Should this row be on screen as a notification? */
|
|
133
|
+
visible(row: Row): boolean;
|
|
134
|
+
/** The notification for one row; `tag` identifies it (one notification per row). */
|
|
135
|
+
render(row: Row): NotificationSpec & {
|
|
136
|
+
tag: string;
|
|
137
|
+
};
|
|
138
|
+
/** Budget for loading the rows, ms. Default 8000 (the push event lives ~30 s). */
|
|
139
|
+
timeoutMs?: number;
|
|
140
|
+
/** Quiet period `idle()` waits for, ms. Default 300. */
|
|
141
|
+
quietMs?: number;
|
|
142
|
+
/**
|
|
143
|
+
* A push whose only effect was closing notifications (their rows stopped
|
|
144
|
+
* being visible, e.g. read on another device) shows nothing. Default true.
|
|
145
|
+
* `false` shows the placeholder instead, for origins that hit Chrome's
|
|
146
|
+
* silent-push allowance.
|
|
147
|
+
*/
|
|
148
|
+
silentClose?: boolean;
|
|
149
|
+
}
|
|
150
|
+
interface InstallPushHandlersOptions<Row = Record<string, any>> {
|
|
151
|
+
/** Turn a notification url into the one to open. Default: resolved against the worker's scope. */
|
|
152
|
+
resolveUrl?(url: string, payload: PushPayload | undefined): string;
|
|
153
|
+
/** Sees every valid push first. Return `true` when you handled it (and showed something). */
|
|
154
|
+
onPush?(payload: PushPayload, ctx: PushHandlerContext): Promise<boolean | void> | boolean | void;
|
|
155
|
+
/** Handles content-free nudges (you must show a notification). */
|
|
156
|
+
onNudge?(payload: PushPayload, ctx: PushHandlerContext): Promise<void> | void;
|
|
157
|
+
/**
|
|
158
|
+
* Custom rendering. For a content push, replaces the default mapping of
|
|
159
|
+
* `payload.notification`; for a nudge, a returned spec is shown instead of
|
|
160
|
+
* the live renderer. `null`/`undefined` keeps the default.
|
|
161
|
+
*/
|
|
162
|
+
render?(payload: PushPayload): NotificationSpec | null | undefined;
|
|
163
|
+
/** Post `{ type: 'sp00ky:push', payload }` to visible windows instead of showing. Default true. */
|
|
164
|
+
suppressWhenVisible?: boolean;
|
|
165
|
+
/** Shown when a push cannot be rendered (browsers require a notification per push). */
|
|
166
|
+
placeholder?: {
|
|
167
|
+
title: string;
|
|
168
|
+
body?: string;
|
|
169
|
+
icon?: string;
|
|
170
|
+
[key: string]: unknown;
|
|
171
|
+
};
|
|
172
|
+
/** Return `true` when you handled the click yourself. */
|
|
173
|
+
onClick?(event: SwNotificationEvent, payload: PushPayload | undefined, action: string | undefined): Promise<boolean | void> | boolean | void;
|
|
174
|
+
onClose?(event: SwNotificationEvent, payload: PushPayload | undefined): Promise<void> | void;
|
|
175
|
+
/**
|
|
176
|
+
* How a click reaches an already open window: `navigate` (default) loads the
|
|
177
|
+
* url in it, `message` posts `{ type: 'sp00ky:navigate', url }` for in-app
|
|
178
|
+
* routing (`db.webPush.onMessage`).
|
|
179
|
+
*/
|
|
180
|
+
clickMode?: 'navigate' | 'message';
|
|
181
|
+
/** Render nudges from live data. */
|
|
182
|
+
live?: LiveRenderOptions<Row>;
|
|
183
|
+
/** Feed factory for `pushsubscriptionchange` when `live` is not configured. */
|
|
184
|
+
feed?(bridged: BridgedToken): LiveFeed | Promise<LiveFeed>;
|
|
185
|
+
/** Re-subscribe on `pushsubscriptionchange`. Default true. */
|
|
186
|
+
resubscribe?: boolean;
|
|
187
|
+
/** Defaults to `self`. */
|
|
188
|
+
scope?: SwScope;
|
|
189
|
+
/** Defaults to IndexedDB `sp00ky-push` / `kv`. */
|
|
190
|
+
store?: KvStore;
|
|
191
|
+
}
|
|
192
|
+
declare const DEFAULT_PLACEHOLDER: {
|
|
193
|
+
title: string;
|
|
194
|
+
body: string;
|
|
195
|
+
};
|
|
196
|
+
/** The session the page bridged, or null (none, signed out, or its JWT expired). */
|
|
197
|
+
declare function getBridgedToken(store?: KvStore): Promise<BridgedToken | null>;
|
|
198
|
+
/**
|
|
199
|
+
* `showNotification(title, options)` arguments for a spec: every key passes
|
|
200
|
+
* through except `title` and `url`; `tag` defaults to the topic; `data` gets
|
|
201
|
+
* `url`, the payload and per-action urls next to the spec's own data.
|
|
202
|
+
*/
|
|
203
|
+
declare function notificationOptions(spec: NotificationSpec, payload?: PushPayload, extraData?: Record<string, unknown>): [string, Record<string, unknown>];
|
|
204
|
+
/** Wire the push, click, close, message and subscription-change handlers. Returns an uninstall. */
|
|
205
|
+
declare function installPushHandlers<Row = Record<string, any>>(options?: InstallPushHandlersOptions<Row>): () => void;
|
|
206
|
+
//#endregion
|
|
207
|
+
export { BridgeSignOutMessage, BridgeTokenMessage, BridgedToken, DEFAULT_PLACEHOLDER, InstallPushHandlersOptions, type KvStore, LiveRenderOptions, NotificationSpec, PUSH_DB_NAME, PUSH_PAYLOAD_VERSION, PUSH_STORE_NAME, PushHandlerContext, PushNotification, PushNotificationAction, PushOp, PushPayload, PushPayloadKind, PushUrgency, SwClients, SwExtendableEvent, SwNotification, SwNotificationEvent, SwPushEvent, SwPushSubscription, SwPushSubscriptionChangeEvent, SwRegistration, SwScope, SwWindowClient, WebPushClientMessage, base64UrlToBytes, bytesToBase64Url, defaultStore, getBridgedToken, idbStore, installPushHandlers, isPushPayload, memoryStore, notificationOptions, sameBytes };
|