@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.
- 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,73 @@
|
|
|
1
|
+
//#region src/push/types.ts
|
|
2
|
+
/**
|
|
3
|
+
* Web Push wire types shared by the page (`db.webPush`), the service worker
|
|
4
|
+
* bridge (`@spooky-sync/core/sw`) and the live feed. Pure: types plus a few
|
|
5
|
+
* dependency-free helpers, safe in any context.
|
|
6
|
+
*
|
|
7
|
+
* `PushPayload` mirrors `packages/push-core/src/config.rs` (`PushPayload`,
|
|
8
|
+
* `PayloadKind`, `Op`, `NotificationTemplate`). Keep the two in step.
|
|
9
|
+
*/
|
|
10
|
+
/** Payload format version this build understands. */
|
|
11
|
+
const PUSH_PAYLOAD_VERSION = 1;
|
|
12
|
+
/** `urlBase64ToUint8Array`: the VAPID key as `applicationServerKey` wants it. */
|
|
13
|
+
function base64UrlToBytes(value) {
|
|
14
|
+
const padded = value.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - value.length % 4) % 4);
|
|
15
|
+
const raw = typeof atob === "function" ? atob(padded) : decodeBase64Fallback(padded);
|
|
16
|
+
const out = new Uint8Array(raw.length);
|
|
17
|
+
for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i);
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
function bytesToBase64Url(bytes) {
|
|
21
|
+
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
22
|
+
let raw = "";
|
|
23
|
+
for (let i = 0; i < view.length; i++) raw += String.fromCharCode(view[i]);
|
|
24
|
+
return (typeof btoa === "function" ? btoa(raw) : encodeBase64Fallback(raw)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
25
|
+
}
|
|
26
|
+
function sameBytes(a, b) {
|
|
27
|
+
if (!a || !b) return false;
|
|
28
|
+
const x = a instanceof Uint8Array ? a : new Uint8Array(a);
|
|
29
|
+
const y = b instanceof Uint8Array ? b : new Uint8Array(b);
|
|
30
|
+
if (x.length !== y.length) return false;
|
|
31
|
+
for (let i = 0; i < x.length; i++) if (x[i] !== y[i]) return false;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
/** A payload this build can act on (`v` known, `kind` set). */
|
|
35
|
+
function isPushPayload(value) {
|
|
36
|
+
if (!value || typeof value !== "object") return false;
|
|
37
|
+
const v = value;
|
|
38
|
+
return v.v === PUSH_PAYLOAD_VERSION && (v.kind === "rule" || v.kind === "message");
|
|
39
|
+
}
|
|
40
|
+
const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
41
|
+
function decodeBase64Fallback(input) {
|
|
42
|
+
const clean = input.replace(/=+$/, "");
|
|
43
|
+
let out = "";
|
|
44
|
+
let buffer = 0;
|
|
45
|
+
let bits = 0;
|
|
46
|
+
for (const ch of clean) {
|
|
47
|
+
const idx = B64.indexOf(ch);
|
|
48
|
+
if (idx < 0) continue;
|
|
49
|
+
buffer = buffer << 6 | idx;
|
|
50
|
+
bits += 6;
|
|
51
|
+
if (bits >= 8) {
|
|
52
|
+
bits -= 8;
|
|
53
|
+
out += String.fromCharCode(buffer >> bits & 255);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
function encodeBase64Fallback(raw) {
|
|
59
|
+
let out = "";
|
|
60
|
+
for (let i = 0; i < raw.length; i += 3) {
|
|
61
|
+
const a = raw.charCodeAt(i);
|
|
62
|
+
const b = i + 1 < raw.length ? raw.charCodeAt(i + 1) : 0;
|
|
63
|
+
const c = i + 2 < raw.length ? raw.charCodeAt(i + 2) : 0;
|
|
64
|
+
const n = a << 16 | b << 8 | c;
|
|
65
|
+
out += B64[n >> 18 & 63] + B64[n >> 12 & 63];
|
|
66
|
+
out += i + 1 < raw.length ? B64[n >> 6 & 63] : "=";
|
|
67
|
+
out += i + 2 < raw.length ? B64[n & 63] : "=";
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
export { sameBytes as a, isPushPayload as i, base64UrlToBytes as n, bytesToBase64Url as r, PUSH_PAYLOAD_VERSION as t };
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spooky-sync/core",
|
|
3
|
-
"version": "0.0.1-canary.
|
|
3
|
+
"version": "0.0.1-canary.301",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
5
6
|
"main": "./dist/index.js",
|
|
6
7
|
"types": "./dist/index.d.ts",
|
|
7
8
|
"exports": {
|
|
@@ -12,13 +13,28 @@
|
|
|
12
13
|
"./otel": {
|
|
13
14
|
"types": "./dist/otel/index.d.ts",
|
|
14
15
|
"import": "./dist/otel/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./pure": {
|
|
18
|
+
"types": "./dist/pure.d.ts",
|
|
19
|
+
"import": "./dist/pure.js"
|
|
20
|
+
},
|
|
21
|
+
"./live": {
|
|
22
|
+
"types": "./dist/live.d.ts",
|
|
23
|
+
"import": "./dist/live.js"
|
|
24
|
+
},
|
|
25
|
+
"./sw": {
|
|
26
|
+
"types": "./dist/sw.d.ts",
|
|
27
|
+
"import": "./dist/sw.js"
|
|
15
28
|
}
|
|
16
29
|
},
|
|
17
30
|
"scripts": {
|
|
18
31
|
"dev": "tsdown --watch",
|
|
19
32
|
"dev:example": "pnpm dev",
|
|
20
|
-
"build": "tsdown",
|
|
21
|
-
"test": "vitest"
|
|
33
|
+
"build": "tsdown && node scripts/check-broker-bundle.mjs && node scripts/check-worker-entries.mjs",
|
|
34
|
+
"test": "vitest",
|
|
35
|
+
"test:run": "vitest run",
|
|
36
|
+
"test:coverage": "vitest run --coverage",
|
|
37
|
+
"type-check": "tsc --noEmit"
|
|
22
38
|
},
|
|
23
39
|
"repository": {
|
|
24
40
|
"type": "git",
|
|
@@ -59,15 +75,19 @@
|
|
|
59
75
|
}
|
|
60
76
|
},
|
|
61
77
|
"dependencies": {
|
|
62
|
-
"@spooky-sync/query-builder": "
|
|
63
|
-
"@spooky-sync/ssp-wasm": "
|
|
64
|
-
"@
|
|
78
|
+
"@spooky-sync/query-builder": "0.0.1-canary.301",
|
|
79
|
+
"@spooky-sync/ssp-wasm": "0.0.1-canary.301",
|
|
80
|
+
"@sqlite.org/sqlite-wasm": "3.53.0-build1",
|
|
81
|
+
"@surrealdb/wasm": "^3.0.3",
|
|
82
|
+
"blurhash": "^2.0.5",
|
|
65
83
|
"fast-json-patch": "^3.1.1",
|
|
84
|
+
"loro-crdt": "^1.5.6",
|
|
66
85
|
"pino": "^10.1.0",
|
|
67
|
-
"surrealdb": "2.0.
|
|
86
|
+
"surrealdb": "2.0.3"
|
|
68
87
|
},
|
|
69
88
|
"devDependencies": {
|
|
70
89
|
"@types/node": "^22.5.2",
|
|
90
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
71
91
|
"tsdown": "^0.12.4",
|
|
72
92
|
"typescript": "^5.6.2",
|
|
73
93
|
"vitest": "^3.2.0"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Build gate: dist/tabs-broker-worker.js must be self-contained ESM.
|
|
2
|
+
// Bundlers cannot trace `new SharedWorker(url)` module graphs, so any
|
|
3
|
+
// import statement surviving into the emitted broker file would 404 at
|
|
4
|
+
// runtime in consumers (the file is fetched as a bare URL, its relative
|
|
5
|
+
// chunk siblings are not copied along). Run after `tsdown` in the build.
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { resolve, dirname } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const dist = resolve(dirname(fileURLToPath(import.meta.url)), '../dist/tabs-broker-worker.js');
|
|
11
|
+
|
|
12
|
+
let code;
|
|
13
|
+
try {
|
|
14
|
+
code = readFileSync(dist, 'utf8');
|
|
15
|
+
} catch {
|
|
16
|
+
console.error(`check-broker-bundle: ${dist} missing; did tsdown run?`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const problems = [];
|
|
21
|
+
// Static or dynamic imports of sibling chunks.
|
|
22
|
+
const importRe = /(?:^|\n)\s*import[\s(]/;
|
|
23
|
+
if (importRe.test(code)) problems.push('contains an import statement');
|
|
24
|
+
if (!code.includes('onconnect')) problems.push('missing onconnect handler');
|
|
25
|
+
|
|
26
|
+
if (problems.length > 0) {
|
|
27
|
+
console.error(`check-broker-bundle: dist/tabs-broker-worker.js ${problems.join('; ')}.`);
|
|
28
|
+
console.error(
|
|
29
|
+
'The broker worker must stay free of runtime imports (type-only imports are fine in source).'
|
|
30
|
+
);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
console.log('check-broker-bundle: ok');
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Build gate for the wasm-free subpath entries: dist/pure.js, dist/live.js and
|
|
2
|
+
// dist/sw.js (`@spooky-sync/core/pure`, `/live`, `/sw`). They are imported by
|
|
3
|
+
// service workers, dedicated workers and Node, so across each entry and every
|
|
4
|
+
// chunk it reaches:
|
|
5
|
+
// - no import of a wasm engine, the circuit, CRDTs or the logger;
|
|
6
|
+
// - no reference to `window`, `document` or `localStorage` (a service worker
|
|
7
|
+
// has none of them; an access at module top level would throw on import);
|
|
8
|
+
// - `/sw` does not even pull in the `surrealdb` SDK.
|
|
9
|
+
// Each entry is then really imported in this Node process (no DOM globals)
|
|
10
|
+
// to catch top-level side effects the static scan cannot see. Prints the
|
|
11
|
+
// gzipped size of each entry (own code, and fully bundled with its npm
|
|
12
|
+
// dependencies when rolldown is reachable). Run after `tsdown` in the build.
|
|
13
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
14
|
+
import { resolve, dirname, join } from 'node:path';
|
|
15
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
16
|
+
import { gzipSync } from 'node:zlib';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
|
|
19
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
const dist = join(root, 'dist');
|
|
21
|
+
|
|
22
|
+
const ENTRIES = ['pure.js', 'live.js', 'sw.js'];
|
|
23
|
+
const FORBIDDEN_IMPORTS = [
|
|
24
|
+
'@spooky-sync/ssp-wasm',
|
|
25
|
+
'@surrealdb/wasm',
|
|
26
|
+
'@sqlite.org/sqlite-wasm',
|
|
27
|
+
'loro-crdt',
|
|
28
|
+
'pino',
|
|
29
|
+
'blurhash',
|
|
30
|
+
];
|
|
31
|
+
const FORBIDDEN_PER_ENTRY = { 'sw.js': ['surrealdb'] };
|
|
32
|
+
const FORBIDDEN_GLOBALS = ['window', 'document', 'localStorage'];
|
|
33
|
+
|
|
34
|
+
const importRe =
|
|
35
|
+
/(?:import|export)\s*(?:[^'"`;]*?\sfrom\s*)?["']([^"']+)["']|import\(\s*["']([^"']+)["']\s*\)/g;
|
|
36
|
+
|
|
37
|
+
/** Blank out comments and string/template literals so identifier scans see code only. */
|
|
38
|
+
function codeOnly(src) {
|
|
39
|
+
let out = '';
|
|
40
|
+
let i = 0;
|
|
41
|
+
while (i < src.length) {
|
|
42
|
+
const c = src[i];
|
|
43
|
+
const n = src[i + 1];
|
|
44
|
+
if (c === '/' && n === '/') {
|
|
45
|
+
while (i < src.length && src[i] !== '\n') i++;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (c === '/' && n === '*') {
|
|
49
|
+
i += 2;
|
|
50
|
+
while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) i++;
|
|
51
|
+
i += 2;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
55
|
+
const q = c;
|
|
56
|
+
i++;
|
|
57
|
+
while (i < src.length && src[i] !== q) i += src[i] === '\\' ? 2 : 1;
|
|
58
|
+
i++;
|
|
59
|
+
out += '""';
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
out += c;
|
|
63
|
+
i++;
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function walk(entry) {
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
const bare = new Set();
|
|
71
|
+
const stack = [join(dist, entry)];
|
|
72
|
+
while (stack.length > 0) {
|
|
73
|
+
const file = stack.pop();
|
|
74
|
+
if (seen.has(file)) continue;
|
|
75
|
+
seen.add(file);
|
|
76
|
+
if (!existsSync(file)) throw new Error(`${entry}: missing ${file}`);
|
|
77
|
+
const src = readFileSync(file, 'utf8');
|
|
78
|
+
for (const m of src.matchAll(importRe)) {
|
|
79
|
+
const spec = m[1] ?? m[2];
|
|
80
|
+
if (spec.startsWith('.')) stack.push(resolve(dirname(file), spec));
|
|
81
|
+
else bare.add(spec);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { files: [...seen], bare: [...bare] };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const problems = [];
|
|
88
|
+
const sizes = [];
|
|
89
|
+
|
|
90
|
+
for (const entry of ENTRIES) {
|
|
91
|
+
let graph;
|
|
92
|
+
try {
|
|
93
|
+
graph = walk(entry);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
problems.push(String(error.message ?? error));
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const forbidden = [...FORBIDDEN_IMPORTS, ...(FORBIDDEN_PER_ENTRY[entry] ?? [])];
|
|
99
|
+
for (const spec of graph.bare) {
|
|
100
|
+
if (forbidden.some((f) => spec === f || spec.startsWith(`${f}/`)))
|
|
101
|
+
problems.push(`${entry} imports ${spec}`);
|
|
102
|
+
}
|
|
103
|
+
let own = '';
|
|
104
|
+
for (const file of graph.files) {
|
|
105
|
+
const src = readFileSync(file, 'utf8');
|
|
106
|
+
own += src;
|
|
107
|
+
if (/\.wasm\b/.test(src))
|
|
108
|
+
problems.push(`${entry}: ${file.slice(dist.length + 1)} references a .wasm file`);
|
|
109
|
+
const code = codeOnly(src);
|
|
110
|
+
for (const name of FORBIDDEN_GLOBALS) {
|
|
111
|
+
// `x.window` (a property) is fine; a bare identifier is not.
|
|
112
|
+
const re = new RegExp(`(?<![.\\w$])${name}(?![\\w$])`);
|
|
113
|
+
if (re.test(code))
|
|
114
|
+
problems.push(`${entry}: ${file.slice(dist.length + 1)} references \`${name}\``);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
sizes.push({
|
|
118
|
+
entry,
|
|
119
|
+
files: graph.files.length,
|
|
120
|
+
raw: Buffer.byteLength(own),
|
|
121
|
+
gzip: gzipSync(own).length,
|
|
122
|
+
deps: graph.bare,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Import each entry for real: top-level code must run without DOM globals.
|
|
127
|
+
for (const entry of ENTRIES) {
|
|
128
|
+
try {
|
|
129
|
+
await import(pathToFileURL(join(dist, entry)).href);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
problems.push(`${entry} fails to import in Node: ${error?.message ?? error}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Fully bundled size (entry + npm deps, minified), when rolldown is reachable
|
|
136
|
+
// through tsdown. Informational only.
|
|
137
|
+
async function bundledGzip(entry) {
|
|
138
|
+
try {
|
|
139
|
+
const req = createRequire(createRequire(join(root, 'package.json')).resolve('tsdown'));
|
|
140
|
+
const { rolldown } = await import(pathToFileURL(req.resolve('rolldown')).href);
|
|
141
|
+
const bundle = await rolldown({
|
|
142
|
+
input: join(dist, entry),
|
|
143
|
+
platform: 'browser',
|
|
144
|
+
logLevel: 'silent',
|
|
145
|
+
});
|
|
146
|
+
const { output } = await bundle.generate({ format: 'esm', minify: true });
|
|
147
|
+
await bundle.close();
|
|
148
|
+
const code = output.map((o) => o.code ?? '').join('');
|
|
149
|
+
return gzipSync(code).length;
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const s of sizes) s.bundled = await bundledGzip(s.entry);
|
|
156
|
+
|
|
157
|
+
const kb = (n) => `${(n / 1024).toFixed(2)} kB`;
|
|
158
|
+
for (const s of sizes) {
|
|
159
|
+
const bundled = s.bundled === null ? '' : `, bundled with deps + minified ${kb(s.bundled)} gzip`;
|
|
160
|
+
console.log(
|
|
161
|
+
`check-worker-entries: ${s.entry.padEnd(8)} ${kb(s.raw)} (${kb(s.gzip)} gzip, ${s.files} file${s.files === 1 ? '' : 's'}; deps: ${s.deps.join(', ') || 'none'})${bundled}`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (problems.length > 0) {
|
|
166
|
+
for (const p of problems) console.error(`check-worker-entries: ${p}`);
|
|
167
|
+
console.error(
|
|
168
|
+
'The /pure, /live and /sw entries must stay free of wasm, the logger and DOM globals.'
|
|
169
|
+
);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
console.log('check-worker-entries: ok');
|
|
@@ -82,6 +82,15 @@ unsubscribe();
|
|
|
82
82
|
|
|
83
83
|
Queries have a time-to-live. Supported values: `'1m'`, `'5m'`, `'10m'` (default), `'15m'`, `'20m'`, `'25m'`, `'30m'`, `'1h'`–`'12h'`, `'1d'`.
|
|
84
84
|
|
|
85
|
+
### Preloading
|
|
86
|
+
|
|
87
|
+
`db.preload(query, { signal? })` registers a query without subscribing to it. If
|
|
88
|
+
the server answered this query on this device before (a durable `_00_view` row
|
|
89
|
+
exists) it resolves at once and the rows paint from the local store; otherwise
|
|
90
|
+
it resolves once the server's membership and every record are local. Nothing
|
|
91
|
+
to tear down: the entry is evicted a ttl after registration unless a view mounts
|
|
92
|
+
the same query. `refresh` / `staleTime` are accepted but ignored.
|
|
93
|
+
|
|
85
94
|
## Mutations
|
|
86
95
|
|
|
87
96
|
Mutations are optimistic and automatically synced to the remote SurrealDB instance.
|
|
@@ -111,6 +120,19 @@ await client.delete('post', 'post:abc123');
|
|
|
111
120
|
Use `debounced` option for frequent updates (e.g., live typing):
|
|
112
121
|
- `key: 'recordId'` — Debounce by record ID only (latest write wins, no merge)
|
|
113
122
|
- `key: 'recordId_x_fields'` — Debounce by record ID + changed fields (recommended)
|
|
123
|
+
- `flushOnHide` (default `true`): also keep the merged patch in the local `_00_pending_writes`
|
|
124
|
+
table and push it as soon as the page is hidden (tab switch, reload, close). A reload or closed
|
|
125
|
+
tab inside the delay no longer loses the edit: the next boot moves the leftover patch into the
|
|
126
|
+
outbox. Set `false` for throwaway values (cursor position) to keep the patch in memory only.
|
|
127
|
+
|
|
128
|
+
### Failed writes
|
|
129
|
+
|
|
130
|
+
A push the server rejects (permission, validation) is rolled back locally
|
|
131
|
+
(create, update and delete) and moved to the `_00_failed_mutations` tray in the
|
|
132
|
+
same local transaction. Network errors never do this; they retry. Inspect and
|
|
133
|
+
act on the tray with `db.failedMutationCount`, `db.subscribeToFailedMutations(cb)`,
|
|
134
|
+
`db.listFailedMutations()`, `db.retryFailedMutation(id)` (a fresh optimistic
|
|
135
|
+
write) and `db.discardFailedMutation(id)`.
|
|
114
136
|
|
|
115
137
|
## Authentication
|
|
116
138
|
|
|
@@ -164,12 +186,15 @@ Backend runs let you trigger server-side HTTP operations via an **outbox pattern
|
|
|
164
186
|
### `db.run()` Signature
|
|
165
187
|
|
|
166
188
|
```typescript
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
189
|
+
// A method of the client: called as `db.run(backend, route, payload, options)`.
|
|
190
|
+
interface Sp00kyClient<S> {
|
|
191
|
+
run<B extends BackendNames<S>, R extends BackendRoutes<S, B>>(
|
|
192
|
+
backend: B, // Backend name from sp00ky.yml (e.g., 'api')
|
|
193
|
+
route: R, // Route path from OpenAPI spec (e.g., '/spookify')
|
|
194
|
+
payload: RoutePayload<S, B, R>, // Typed args for the route
|
|
195
|
+
options?: RunOptions
|
|
196
|
+
): Promise<void>;
|
|
197
|
+
}
|
|
173
198
|
```
|
|
174
199
|
|
|
175
200
|
All parameters are type-safe — `backend`, `route`, and `payload` are inferred from the generated schema.
|
|
@@ -71,6 +71,12 @@ interface UpdateOptions {
|
|
|
71
71
|
key?: 'recordId' | 'recordId_x_fields';
|
|
72
72
|
/** Debounce delay in ms */
|
|
73
73
|
delay?: number;
|
|
74
|
+
/**
|
|
75
|
+
* Default true. Keep the merged patch in the local `_00_pending_writes` table and
|
|
76
|
+
* push it as soon as the page is hidden (tab switch, reload, close), so a reload
|
|
77
|
+
* inside the delay does not lose it. false: memory only.
|
|
78
|
+
*/
|
|
79
|
+
flushOnHide?: boolean;
|
|
74
80
|
};
|
|
75
81
|
}
|
|
76
82
|
```
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { RecordId } from 'surrealdb';
|
|
3
|
+
import { runPure } from '../testing/run-pure';
|
|
4
|
+
import { fakeServices } from '../testing/services';
|
|
5
|
+
import { buildEntry, buildState } from '../testing/build';
|
|
6
|
+
import * as R from '../state/reducers';
|
|
7
|
+
import { defaultEnv } from '../query/env';
|
|
8
|
+
import { authFlip, persistVerifiedUser } from './auth-flip.saga';
|
|
9
|
+
|
|
10
|
+
const env = defaultEnv({ tables: [{ name: 'user', columns: { id: {}, email_verified: {} } }] } as any);
|
|
11
|
+
|
|
12
|
+
describe('authFlip', () => {
|
|
13
|
+
it('sets identity and $auth before the bucket work, writes the hint, switches, rotates the salt for a new principal, persists the user row', async () => {
|
|
14
|
+
const released: string[] = [];
|
|
15
|
+
const svc = fakeServices({
|
|
16
|
+
'auth.sessionAuthId': () => 'user:abc',
|
|
17
|
+
'auth.access': () => 'account',
|
|
18
|
+
'local.currentBucketId': () => 'anon',
|
|
19
|
+
'local.beginSwitch': () => () => void released.push('gate'),
|
|
20
|
+
'local.usesSurqlSchema': () => false,
|
|
21
|
+
'auth.token': () => 'jwt',
|
|
22
|
+
'auth.currentUser': () => ({ id: new RecordId('user', 'abc'), email_verified: true }),
|
|
23
|
+
});
|
|
24
|
+
const state = { ...buildState(), sessionId: 'old', saltUserId: null };
|
|
25
|
+
const out = await runPure(authFlip(env, 'user:abc'), {
|
|
26
|
+
state,
|
|
27
|
+
handlers: { service: svc.handler, 'local.query': () => [], 'local.execute': () => undefined, 'ssp.ingest': () => undefined },
|
|
28
|
+
});
|
|
29
|
+
const names = svc.names();
|
|
30
|
+
expect(names.slice(0, 5)).toEqual(['auth.sessionAuthId', 'auth.access', 'ssp.setSessionAuth', 'hint.write', 'local.currentBucketId']);
|
|
31
|
+
expect(svc.calls[3]).toEqual(['hint.write', ['abc']]);
|
|
32
|
+
expect(names).toContain('local.switchStore');
|
|
33
|
+
expect(names).toContain('crdt.setSessionId');
|
|
34
|
+
expect(released).toEqual(['gate']);
|
|
35
|
+
expect(out.state).toMatchObject({ userId: 'user:abc', saltUserId: 'user:abc', sessionId: 'salt-1', bucketId: 'abc' });
|
|
36
|
+
expect(out.state.versions.get('user:abc')).toBe(1);
|
|
37
|
+
});
|
|
38
|
+
it('same principal on the same bucket: no switch, no salt rotation', async () => {
|
|
39
|
+
const svc = fakeServices({
|
|
40
|
+
'auth.sessionAuthId': () => 'user:abc',
|
|
41
|
+
'auth.access': () => 'account',
|
|
42
|
+
'local.currentBucketId': () => 'abc',
|
|
43
|
+
'auth.currentUser': () => null,
|
|
44
|
+
});
|
|
45
|
+
const state = { ...buildState(), sessionId: 'keep', saltUserId: 'user:abc', bucketId: 'abc' };
|
|
46
|
+
const out = await runPure(authFlip(env, 'user:abc'), { state, handlers: { service: svc.handler } });
|
|
47
|
+
expect(svc.names()).not.toContain('local.switchStore');
|
|
48
|
+
expect(svc.names()).not.toContain('local.beginSwitch');
|
|
49
|
+
expect(out.state.sessionId).toBe('keep');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('persistVerifiedUser', () => {
|
|
54
|
+
it('skips when there is no verified row, an unknown table, or a bare id; writes otherwise and logs failures', async () => {
|
|
55
|
+
for (const row of [null, { id: 'user:x', a: 1 }, { id: new RecordId('user', 'x') }, { id: new RecordId('nope', 'x'), a: 1 }]) {
|
|
56
|
+
const svc = fakeServices({ 'auth.currentUser': () => row as any });
|
|
57
|
+
const out = await runPure(persistVerifiedUser(env), { state: buildState(), handlers: { service: svc.handler } });
|
|
58
|
+
expect(out.log.filter((e) => e.kind === 'local.execute')).toHaveLength(0);
|
|
59
|
+
}
|
|
60
|
+
const svc = fakeServices({ 'auth.currentUser': () => ({ id: new RecordId('user', 'x'), email_verified: true, junk: 1 }) });
|
|
61
|
+
const s = buildState([buildEntry({ def: { hash: 'q', tableName: 'user' } })], R.setVersions([['user:x', 4]]));
|
|
62
|
+
const out = await runPure(persistVerifiedUser(env), {
|
|
63
|
+
state: s,
|
|
64
|
+
handlers: {
|
|
65
|
+
service: svc.handler,
|
|
66
|
+
'local.execute': (e: any) => expect(e.vars.content0).toEqual({ email_verified: true, _00_rv: 4 }),
|
|
67
|
+
'ssp.ingest': (e: any) => expect(e.records[0].record.junk).toBeUndefined(),
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
expect(out.state.dirty.has('q')).toBe(true);
|
|
71
|
+
const failing = await runPure(persistVerifiedUser(env), {
|
|
72
|
+
state: s,
|
|
73
|
+
handlers: {
|
|
74
|
+
service: svc.handler,
|
|
75
|
+
'local.execute': () => {
|
|
76
|
+
throw new Error('locked');
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
expect(failing.emitted).toEqual([expect.objectContaining({ level: 'warn' })]);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { RecordId } from 'surrealdb';
|
|
2
|
+
import type { Saga } from '../kernel/saga';
|
|
3
|
+
import { fx } from '../kernel/effects';
|
|
4
|
+
import * as R from '../state/reducers';
|
|
5
|
+
import { bucketIdForUser } from '../modules/ref-tables';
|
|
6
|
+
import type { SagaEnv } from '../query/env';
|
|
7
|
+
import { columnsFor } from '../query/env';
|
|
8
|
+
import * as sql from '../query/sql';
|
|
9
|
+
import { encodeRecordId } from '../utils/index';
|
|
10
|
+
import { cleanRecord } from '../utils/parser';
|
|
11
|
+
import { bucketSwitch } from './bucket-switch.saga';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The signed-in principal changed (sign-in, sign-out, boot verification).
|
|
15
|
+
* Identity first (routing and `$auth` for the local SSP), then the bucket,
|
|
16
|
+
* then the salt if the principal really changed, then the verified user row.
|
|
17
|
+
*/
|
|
18
|
+
export function* authFlip(env: SagaEnv, userId: string | null): Saga<void> {
|
|
19
|
+
const authId = (yield fx.service('auth.sessionAuthId')) as string | null;
|
|
20
|
+
const access = (yield fx.service('auth.access')) as string | null;
|
|
21
|
+
yield fx.state.update(R.setIdentity({ userId }));
|
|
22
|
+
yield fx.service('ssp.setSessionAuth', authId, access);
|
|
23
|
+
const target = bucketIdForUser(userId);
|
|
24
|
+
yield fx.service('hint.write', target);
|
|
25
|
+
yield fx.state.update(R.setIdentity({ pendingBucket: target }));
|
|
26
|
+
const current = (yield fx.service('local.currentBucketId')) as string;
|
|
27
|
+
const release = current === target ? null : ((yield fx.service('local.beginSwitch')) as () => void);
|
|
28
|
+
yield* bucketSwitch(env, target, release);
|
|
29
|
+
const saltUserId = (yield fx.state.read((s) => s.saltUserId)) as string | null;
|
|
30
|
+
if (authId !== saltUserId) {
|
|
31
|
+
const salt = (yield fx.id('salt')) as string;
|
|
32
|
+
yield fx.state.update(R.setIdentity({ sessionId: salt, saltUserId: authId }));
|
|
33
|
+
yield fx.service('crdt.setSessionId', salt);
|
|
34
|
+
}
|
|
35
|
+
yield* persistVerifiedUser(env);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Write the user's own row, as the server just returned it, into the local
|
|
40
|
+
* store so `user` queries see `email_verified` and friends without waiting
|
|
41
|
+
* for membership. A field refresh, not a version claim.
|
|
42
|
+
*/
|
|
43
|
+
export function* persistVerifiedUser(env: SagaEnv): Saga<void> {
|
|
44
|
+
const row = (yield fx.service('auth.currentUser')) as Record<string, unknown> | null;
|
|
45
|
+
if (!row || !(row.id instanceof RecordId) || Object.keys(row).length <= 1) return;
|
|
46
|
+
const rid = row.id as RecordId<string>;
|
|
47
|
+
const table = String(rid.table);
|
|
48
|
+
const columns = columnsFor(env, table);
|
|
49
|
+
if (!columns) return;
|
|
50
|
+
const id = encodeRecordId(rid);
|
|
51
|
+
const version = ((yield fx.state.read((s) => s.versions.get(id))) as number | undefined) || 1;
|
|
52
|
+
const cleaned = cleanRecord(columns as never, row);
|
|
53
|
+
const { id: _id, ...content } = cleaned;
|
|
54
|
+
try {
|
|
55
|
+
const tx = sql.upsertBodiesTx([{ id: rid, content: { ...content, _00_rv: version } }]);
|
|
56
|
+
yield fx.local.execute(tx.query, tx.vars);
|
|
57
|
+
yield fx.ssp.ingest([{ table, op: 'UPDATE', id, record: { ...cleaned, _00_rv: version } }]);
|
|
58
|
+
yield fx.state.update(R.compose(R.setVersions([[id, version]]), R.markTableDirty(table)));
|
|
59
|
+
} catch (error) {
|
|
60
|
+
yield fx.emit({ type: 'log', level: 'warn', message: 'could not persist the verified user row', data: { error } });
|
|
61
|
+
}
|
|
62
|
+
}
|