@spooky-sync/core 0.0.1-canary.33 → 0.0.1-canary.331
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 +84 -0
- package/README.md +56 -16
- package/dist/index.d.ts +2080 -142
- package/dist/index.js +16009 -2480
- package/dist/live.d.ts +149 -0
- package/dist/live.js +739 -0
- package/dist/otel/index.d.ts +1 -1
- package/dist/otel/index.js +3 -3
- package/dist/pure.d.ts +373 -0
- package/dist/pure.js +24 -0
- package/dist/sqlite-open.js +392 -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 +477 -0
- package/dist/types.d.ts +1857 -38
- package/dist/worker-edges.js +480 -0
- package/dist/worker-impersonation.js +60 -0
- package/dist/worker-ref-tables.d.ts +48 -0
- package/dist/worker-types.d.ts +112 -0
- package/dist/worker-types.js +73 -0
- package/package.json +28 -7
- package/scripts/check-broker-bundle.mjs +33 -0
- package/scripts/check-worker-entries.mjs +173 -0
- package/skills/sp00ky-core/SKILL.md +32 -7
- package/skills/sp00ky-core/references/config.md +7 -1
- package/src/boot/auth-flip.saga.test.ts +135 -0
- package/src/boot/auth-flip.saga.ts +77 -0
- package/src/boot/boot.saga.test.ts +203 -0
- package/src/boot/boot.saga.ts +146 -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 +80 -0
- package/src/boot/preload.saga.ts +40 -0
- package/src/bucket-blurhash.test.ts +148 -0
- package/src/build-globals.d.ts +12 -0
- package/src/client/bucket-handle.test.ts +195 -0
- package/src/client/bucket-handle.ts +352 -0
- package/src/client/router.test.ts +58 -0
- package/src/client/router.ts +100 -0
- package/src/client/runtime.test.ts +195 -0
- package/src/client/runtime.ts +296 -0
- package/src/client/services.test.ts +74 -0
- package/src/client/services.ts +434 -0
- package/src/client/sp00ky-client.test.ts +341 -0
- package/src/client/sp00ky-client.ts +625 -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 +167 -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 +512 -0
- package/src/live/index.ts +1059 -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 +228 -0
- package/src/modules/auth/events/index.ts +9 -2
- package/src/modules/auth/impersonation-banner.ts +443 -0
- package/src/modules/auth/impersonation.integration.test.ts +205 -0
- package/src/modules/auth/impersonation.test.ts +377 -0
- package/src/modules/auth/impersonation.ts +179 -0
- package/src/modules/auth/index.ts +476 -22
- package/src/modules/crdt/acceptance.ts +75 -0
- package/src/modules/crdt/bytes.ts +50 -0
- package/src/modules/crdt/crdt-field.ts +125 -0
- package/src/modules/crdt/crdt.test.ts +469 -0
- package/src/modules/crdt/hash.ts +103 -0
- package/src/modules/crdt/index.ts +757 -0
- package/src/modules/crdt/loro-loader.ts +25 -0
- package/src/modules/crdt/mirror.ts +37 -0
- package/src/modules/devtools/flags.ts +349 -0
- package/src/modules/devtools/index.ts +746 -95
- package/src/modules/devtools/mutations.test.ts +92 -0
- package/src/modules/devtools/mutations.ts +160 -0
- package/src/modules/devtools/notify-throttle.test.ts +166 -0
- package/src/modules/devtools/state-shape.test.ts +263 -0
- package/src/modules/devtools/storage-info.test.ts +79 -0
- package/src/modules/devtools/storage-info.ts +168 -0
- package/src/modules/devtools/versions.test.ts +74 -0
- package/src/modules/devtools/versions.ts +110 -0
- package/src/modules/feature-flag/index.test.ts +298 -0
- package/src/modules/feature-flag/index.ts +316 -0
- package/src/modules/ref-tables.test.ts +93 -0
- package/src/modules/ref-tables.ts +91 -0
- package/src/modules/web-push/index.test.ts +824 -0
- package/src/modules/web-push/index.ts +1027 -0
- package/src/mutation/jobs.test.ts +25 -0
- package/src/mutation/jobs.ts +41 -0
- package/src/mutation/mutation-id.test.ts +37 -0
- package/src/mutation/mutation-id.ts +35 -0
- package/src/mutation/pending-writes.integration.test.ts +175 -0
- package/src/mutation/push.saga.test.ts +301 -0
- package/src/mutation/push.saga.ts +225 -0
- package/src/mutation/rows.test.ts +231 -0
- package/src/mutation/rows.ts +427 -0
- package/src/mutation/tray.saga.test.ts +63 -0
- package/src/mutation/tray.saga.ts +48 -0
- package/src/mutation/write.saga.test.ts +316 -0
- package/src/mutation/write.saga.ts +268 -0
- package/src/otel/index.ts +10 -7
- package/src/pure.ts +66 -0
- package/src/push/types.ts +174 -0
- package/src/query/anon-query-isolation.integration.test.ts +527 -0
- package/src/query/env.test.ts +23 -0
- package/src/query/env.ts +65 -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 +186 -0
- package/src/query/list-ref-live.integration.test.ts +203 -0
- package/src/query/materialize.saga.test.ts +115 -0
- package/src/query/materialize.saga.ts +84 -0
- package/src/query/materialize.test.ts +58 -0
- package/src/query/materialize.ts +57 -0
- package/src/query/membership.saga.test.ts +407 -0
- package/src/query/membership.saga.ts +282 -0
- package/src/query/membership.test.ts +118 -0
- package/src/query/membership.ts +201 -0
- package/src/query/register.saga.test.ts +311 -0
- package/src/query/register.saga.ts +235 -0
- package/src/query/render-set.test.ts +47 -0
- package/src/query/render-set.ts +71 -0
- package/src/query/sql.test.ts +118 -0
- package/src/query/sql.ts +157 -0
- package/src/query/window-query.test.ts +70 -0
- package/src/query/window-query.ts +208 -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 +209 -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 +156 -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 +61 -16
- package/src/services/database/local.test.ts +150 -0
- package/src/services/database/local.ts +508 -63
- package/src/services/database/plan-render.test.ts +159 -0
- package/src/services/database/plan-render.ts +108 -0
- package/src/services/database/relation-resolver.test.ts +484 -0
- package/src/services/database/relation-resolver.ts +0 -0
- package/src/services/database/remote.ts +285 -6
- package/src/services/database/sqlite-cache-engine.test.ts +628 -0
- package/src/services/database/sqlite-cache-engine.timeout.test.ts +61 -0
- package/src/services/database/sqlite-cache-engine.ts +1401 -0
- package/src/services/database/sqlite-column-reset.integration.test.ts +100 -0
- package/src/services/database/sqlite-column-reset.ts +91 -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 +164 -0
- package/src/services/database/sqlite-plan-sql.ts +196 -0
- package/src/services/database/sqlite-projection.test.ts +121 -0
- package/src/services/database/sqlite-select.integration.test.ts +185 -0
- package/src/services/database/sqlite-select.test.ts +261 -0
- package/src/services/database/sqlite-select.ts +131 -0
- package/src/services/database/sqlite-transport.fixture.ts +30 -0
- package/src/services/database/sqlite-transport.ts +224 -0
- package/src/services/database/sqlite-worker.ts +437 -0
- package/src/services/database/surql-translate.ts +417 -0
- package/src/services/database/surreal-cache-engine.ts +163 -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 +806 -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 +234 -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 +284 -0
- package/src/services/tabs/broker.test.ts +381 -0
- package/src/services/tabs/coordinator.test.ts +386 -0
- package/src/services/tabs/coordinator.ts +646 -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 +652 -0
- package/src/sp00ky.ts +5 -395
- package/src/state/client-state.ts +208 -0
- package/src/state/lifecycle.test.ts +141 -0
- package/src/state/lifecycle.ts +99 -0
- package/src/state/reducers.test.ts +306 -0
- package/src/state/reducers.ts +531 -0
- package/src/state/selectors.test.ts +188 -0
- package/src/state/selectors.ts +258 -0
- package/src/sw/index.test.ts +582 -0
- package/src/sw/index.ts +607 -0
- package/src/sw/store.ts +96 -0
- package/src/sync/connection.saga.test.ts +97 -0
- package/src/sync/connection.saga.ts +89 -0
- package/src/sync/live.saga.test.ts +178 -0
- package/src/sync/live.saga.ts +107 -0
- package/src/sync/policy.health.test.ts +56 -0
- package/src/sync/policy.test.ts +663 -0
- package/src/sync/policy.ts +515 -0
- package/src/sync/poll.saga.test.ts +51 -0
- package/src/sync/poll.saga.ts +36 -0
- package/src/sync/tabs.saga.test.ts +78 -0
- package/src/sync/tabs.saga.ts +96 -0
- package/src/testing/adapters.ts +137 -0
- package/src/testing/build.ts +63 -0
- package/src/testing/fake-services.ts +63 -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 +633 -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 +89 -12
- package/src/utils/parser.test.ts +67 -102
- package/src/utils/parser.ts +64 -2
- package/src/utils/semver.test.ts +32 -0
- package/src/utils/semver.ts +30 -0
- package/src/utils/sha256.ts +15 -0
- package/src/utils/surql.ts +29 -18
- package/src/utils/utils.test.ts +3 -2
- package/tsdown.config.ts +117 -3
- package/vitest.config.ts +48 -0
- package/src/modules/cache/index.ts +0 -241
- package/src/modules/cache/types.ts +0 -19
- package/src/modules/data/data.test.ts +0 -58
- package/src/modules/data/index.ts +0 -879
- package/src/modules/sync/engine.ts +0 -173
- package/src/modules/sync/events/index.ts +0 -77
- package/src/modules/sync/index.ts +0 -3
- package/src/modules/sync/queue/index.ts +0 -2
- package/src/modules/sync/queue/queue-down.ts +0 -89
- package/src/modules/sync/queue/queue-up.ts +0 -223
- package/src/modules/sync/scheduler.ts +0 -84
- package/src/modules/sync/sync.ts +0 -421
- package/src/modules/sync/utils.test.ts +0 -311
- package/src/modules/sync/utils.ts +0 -167
package/src/utils/parser.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { ColumnSchema
|
|
1
|
+
import type { ColumnSchema} from '@spooky-sync/query-builder';
|
|
2
|
+
import { RecordId, baseFieldOfParam } from '@spooky-sync/query-builder';
|
|
2
3
|
import { parseRecordIdString } from './index';
|
|
3
4
|
import { DateTime } from 'surrealdb';
|
|
4
5
|
|
|
@@ -8,13 +9,49 @@ export function cleanRecord(
|
|
|
8
9
|
): Record<string, any> {
|
|
9
10
|
const cleaned: Record<string, any> = {};
|
|
10
11
|
for (const [key, value] of Object.entries(record)) {
|
|
11
|
-
if (key === 'id' || key in tableSchema) {
|
|
12
|
+
if (key === 'id' || key.startsWith('_00_') || key in tableSchema) {
|
|
12
13
|
cleaned[key] = value;
|
|
13
14
|
}
|
|
14
15
|
}
|
|
15
16
|
return cleaned;
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The OPTIONAL schema columns a full server row does not carry, each mapped to
|
|
21
|
+
* `undefined`, for spreading into a body that is landed with MERGE.
|
|
22
|
+
*
|
|
23
|
+
* The server omits an optional field that is unset (`UPDATE t UNSET club`
|
|
24
|
+
* leaves no `club` key in `SELECT *` or the SSP body), and MERGE keeps every
|
|
25
|
+
* key the patch does not mention, so without this the local store held on to
|
|
26
|
+
* a cleared value forever. `undefined` is the one representation both engines
|
|
27
|
+
* read as "remove": the SurrealDB SDK encodes it as NONE, which MERGE removes
|
|
28
|
+
* (NULL would be a type error on an `option<record<...>>` field), and the
|
|
29
|
+
* SQLite engine turns it into the `null` member that `json_patch` removes (see
|
|
30
|
+
* `serializeMergeRow`).
|
|
31
|
+
*
|
|
32
|
+
* Required columns are left alone. A SCHEMAFULL server cannot hold NONE in
|
|
33
|
+
* one, so its absence means the row was not readable in full (a field
|
|
34
|
+
* permission), not that it was cleared, and NONE in a typed local store is a
|
|
35
|
+
* coercion error that would fail the whole chunk's transaction. `id` and the
|
|
36
|
+
* local-only `_00_*` bookkeeping are never touched.
|
|
37
|
+
*/
|
|
38
|
+
export function absentOptionalColumns(
|
|
39
|
+
tableSchema: Record<string, ColumnSchema> | undefined,
|
|
40
|
+
record: Record<string, unknown>
|
|
41
|
+
): Record<string, undefined> {
|
|
42
|
+
const cleared: Record<string, undefined> = {};
|
|
43
|
+
if (!tableSchema) return cleared;
|
|
44
|
+
for (const [key, column] of Object.entries(tableSchema)) {
|
|
45
|
+
if (key === 'id' || key.startsWith('_00_') || column?.optional !== true) continue;
|
|
46
|
+
if (record[key] === undefined) cleared[key] = undefined;
|
|
47
|
+
}
|
|
48
|
+
return cleared;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse a RECORD's fields against the table schema. Anything the schema does not
|
|
53
|
+
* know is dropped, which is what keeps a stray field out of a write.
|
|
54
|
+
*/
|
|
18
55
|
export function parseParams(
|
|
19
56
|
tableSchema: Record<string, ColumnSchema>,
|
|
20
57
|
params: Record<string, any>
|
|
@@ -30,6 +67,31 @@ export function parseParams(
|
|
|
30
67
|
return parsedParams;
|
|
31
68
|
}
|
|
32
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Parse a QUERY's params. Unlike a record's fields, a param name is not always a
|
|
72
|
+
* column: an `_or` branch binds under a synthetic `white__or0` (see
|
|
73
|
+
* `orParamName` in @spooky-sync/query-builder), and the surql that references it
|
|
74
|
+
* is already written. So resolve the column through the synthetic name, and keep
|
|
75
|
+
* a param we cannot type rather than dropping it.
|
|
76
|
+
*
|
|
77
|
+
* Dropping was the bug: `parseParams` kept only column-named params, so every
|
|
78
|
+
* `_or` query registered with `$or0`/`$or1` unbound and matched no rows at all,
|
|
79
|
+
* silently (rowCount 0, errorCount 0).
|
|
80
|
+
*/
|
|
81
|
+
export function parseQueryParams(
|
|
82
|
+
tableSchema: Record<string, ColumnSchema>,
|
|
83
|
+
params: Record<string, any>
|
|
84
|
+
) {
|
|
85
|
+
const parsedParams: Record<string, any> = {};
|
|
86
|
+
for (const [key, value] of Object.entries(params)) {
|
|
87
|
+
if (value === undefined) continue;
|
|
88
|
+
const column = tableSchema[key] ?? tableSchema[baseFieldOfParam(key)];
|
|
89
|
+
parsedParams[key] = column ? parseValue(key, column, value) : value;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return parsedParams;
|
|
93
|
+
}
|
|
94
|
+
|
|
33
95
|
function parseValue(name: string, column: ColumnSchema, value: any) {
|
|
34
96
|
if (column.recordId) {
|
|
35
97
|
if (value instanceof RecordId) return value;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { semverGt } from './semver';
|
|
3
|
+
|
|
4
|
+
describe('semverGt', () => {
|
|
5
|
+
it('compares strictly greater', () => {
|
|
6
|
+
expect(semverGt('1.0.1', '1.0.0')).toBe(true);
|
|
7
|
+
expect(semverGt('1.1.0', '1.0.9')).toBe(true);
|
|
8
|
+
expect(semverGt('2.0.0', '1.9.9')).toBe(true);
|
|
9
|
+
expect(semverGt('1.10.0', '1.9.0')).toBe(true); // numeric, not lexicographic
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('equal and lower are not greater', () => {
|
|
13
|
+
expect(semverGt('1.0.0', '1.0.0')).toBe(false);
|
|
14
|
+
expect(semverGt('1.0.0', '1.0.1')).toBe(false);
|
|
15
|
+
expect(semverGt('0.9.9', '1.0.0')).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('missing parts default to zero', () => {
|
|
19
|
+
expect(semverGt('1.2', '1.2.0')).toBe(false);
|
|
20
|
+
expect(semverGt('1.2.1', '1.2')).toBe(true);
|
|
21
|
+
expect(semverGt('2', '1.9.9')).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('junk never compares greater', () => {
|
|
25
|
+
expect(semverGt('abc', '1.0.0')).toBe(false);
|
|
26
|
+
expect(semverGt('1.0.0', 'abc')).toBe(false);
|
|
27
|
+
expect(semverGt('', '0.0.0')).toBe(false);
|
|
28
|
+
expect(semverGt(null, undefined)).toBe(false);
|
|
29
|
+
expect(semverGt('1.0.0-beta', '0.9.0')).toBe(false); // prerelease unsupported
|
|
30
|
+
expect(semverGt('1.2.3.4', '1.0.0')).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Minimal semver comparison for app-release version checks. "X.Y.Z" numeric
|
|
2
|
+
// with missing parts read as 0 ("1.2" == "1.2.0"); any malformed input never
|
|
3
|
+
// compares greater, so a bad release row can never nag (or force-reload)
|
|
4
|
+
// every client.
|
|
5
|
+
|
|
6
|
+
function parse(v: unknown): [number, number, number] | null {
|
|
7
|
+
const parts = String(v ?? '')
|
|
8
|
+
.trim()
|
|
9
|
+
.split('.');
|
|
10
|
+
if (parts.length === 0 || parts.length > 3 || parts[0] === '') return null;
|
|
11
|
+
const nums: number[] = [];
|
|
12
|
+
for (let i = 0; i < 3; i++) {
|
|
13
|
+
const raw = parts[i] ?? '0';
|
|
14
|
+
if (!/^\d+$/.test(raw)) return null;
|
|
15
|
+
nums.push(parseInt(raw, 10));
|
|
16
|
+
}
|
|
17
|
+
return nums as [number, number, number];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** True when `a` is a valid version strictly greater than valid version `b`. */
|
|
21
|
+
export function semverGt(a: unknown, b: unknown): boolean {
|
|
22
|
+
const pa = parse(a);
|
|
23
|
+
const pb = parse(b);
|
|
24
|
+
if (!pa || !pb) return false;
|
|
25
|
+
for (let i = 0; i < 3; i++) {
|
|
26
|
+
if (pa[i] > pb[i]) return true;
|
|
27
|
+
if (pa[i] < pb[i]) return false;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lowercase hex SHA-256 of a UTF-8 string, through WebCrypto.
|
|
3
|
+
*
|
|
4
|
+
* Pure and dependency free so it can live in `@spooky-sync/core/pure` and run
|
|
5
|
+
* in a service worker, a dedicated worker or Node (`globalThis.crypto` is
|
|
6
|
+
* there on Node 19+). Query keys are `sha256Hex(JSON.stringify(..))`, see
|
|
7
|
+
* `query/hash.ts`.
|
|
8
|
+
*/
|
|
9
|
+
export async function sha256Hex(input: string): Promise<string> {
|
|
10
|
+
const bytes = new TextEncoder().encode(input);
|
|
11
|
+
const digest = await crypto.subtle.digest('SHA-256', bytes);
|
|
12
|
+
return Array.from(new Uint8Array(digest))
|
|
13
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
14
|
+
.join('');
|
|
15
|
+
}
|
package/src/utils/surql.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MutationEventType } from '../types';
|
|
1
|
+
import type { MutationEventType } from '../types';
|
|
2
2
|
|
|
3
3
|
// ==================== TYPES ====================
|
|
4
4
|
|
|
@@ -34,6 +34,7 @@ export interface SurqlHelper {
|
|
|
34
34
|
keyDataVars: ({ key: string; variable: string } | { statement: string } | string)[]
|
|
35
35
|
): string;
|
|
36
36
|
upsert(idVar: string, dataVar: string): string;
|
|
37
|
+
upsertMerge(idVar: string, dataVar: string): string;
|
|
37
38
|
updateMerge(idVar: string, dataVar: string): string;
|
|
38
39
|
updateSet(
|
|
39
40
|
idVar: string,
|
|
@@ -90,17 +91,9 @@ export const surql: SurqlHelper = {
|
|
|
90
91
|
returnValues: ({ field: string; alias: string } | string)[]
|
|
91
92
|
) {
|
|
92
93
|
return `SELECT ${returnValues
|
|
93
|
-
.map((
|
|
94
|
-
typeof returnValues === 'string'
|
|
95
|
-
? returnValues
|
|
96
|
-
: `${returnValues.field} as ${returnValues.alias}`
|
|
97
|
-
)
|
|
94
|
+
.map((rv) => (typeof rv === 'string' ? rv : `${rv.field} as ${rv.alias}`))
|
|
98
95
|
.join(',')} FROM ${table} WHERE ${whereVar
|
|
99
|
-
.map((
|
|
100
|
-
typeof whereVar === 'string'
|
|
101
|
-
? `${whereVar} = $${whereVar}`
|
|
102
|
-
: `${whereVar.field} = $${whereVar.variable}`
|
|
103
|
-
)
|
|
96
|
+
.map((wv) => (typeof wv === 'string' ? `${wv} = $${wv}` : `${wv.field} = $${wv.variable}`))
|
|
104
97
|
.join(' AND ')}`;
|
|
105
98
|
},
|
|
106
99
|
|
|
@@ -127,6 +120,13 @@ export const surql: SurqlHelper = {
|
|
|
127
120
|
return `UPSERT ONLY $${idVar} REPLACE $${dataVar}`;
|
|
128
121
|
},
|
|
129
122
|
|
|
123
|
+
// Use this for sync-down ingestion: the remote payload can omit local-only
|
|
124
|
+
// fields injected by the CLI's local-schema setup, and REPLACE would wipe
|
|
125
|
+
// them on every round-trip.
|
|
126
|
+
upsertMerge(idVar: string, dataVar: string) {
|
|
127
|
+
return `UPSERT ONLY $${idVar} MERGE $${dataVar}`;
|
|
128
|
+
},
|
|
129
|
+
|
|
130
130
|
updateMerge(idVar: string, dataVar: string) {
|
|
131
131
|
return `UPDATE ONLY $${idVar} MERGE $${dataVar}`;
|
|
132
132
|
},
|
|
@@ -136,12 +136,12 @@ export const surql: SurqlHelper = {
|
|
|
136
136
|
keyDataVar: ({ key: string; variable: string } | { statement: string } | string)[]
|
|
137
137
|
) {
|
|
138
138
|
return `UPDATE $${idVar} SET ${keyDataVar
|
|
139
|
-
.map((
|
|
140
|
-
typeof
|
|
141
|
-
? `${
|
|
142
|
-
: 'statement' in
|
|
143
|
-
?
|
|
144
|
-
: `${
|
|
139
|
+
.map((kdv) =>
|
|
140
|
+
typeof kdv === 'string'
|
|
141
|
+
? `${kdv} = $${kdv}`
|
|
142
|
+
: 'statement' in kdv
|
|
143
|
+
? kdv.statement
|
|
144
|
+
: `${kdv.key} = $${kdv.variable}`
|
|
145
145
|
)
|
|
146
146
|
.join(', ')}`;
|
|
147
147
|
},
|
|
@@ -163,7 +163,18 @@ export const surql: SurqlHelper = {
|
|
|
163
163
|
) {
|
|
164
164
|
switch (t) {
|
|
165
165
|
case 'create':
|
|
166
|
-
|
|
166
|
+
// `data` is REQUIRED here, not decorative. The outbox row is the only
|
|
167
|
+
// copy of a pending create once the in-memory UpEvent is gone (reload,
|
|
168
|
+
// or a shared-tabs follower whose row is replayed by the leader). This
|
|
169
|
+
// used to drop `dataVar` on the floor, so every replayed create arrived
|
|
170
|
+
// with `data: undefined` and `processUpEvent` threw on
|
|
171
|
+
// `Object.keys(event.data)` before it ever reached the network: the
|
|
172
|
+
// create was unsendable AND it sat at the head of the queue blocking
|
|
173
|
+
// every later mutation. The payload was simply never persisted, so
|
|
174
|
+
// those creates were unrecoverable.
|
|
175
|
+
return dataVar
|
|
176
|
+
? `CREATE ONLY $${mutationIdVar} SET mutationType = 'create', recordId = $${recordIdVar}, data = $${dataVar}`
|
|
177
|
+
: `CREATE ONLY $${mutationIdVar} SET mutationType = 'create', recordId = $${recordIdVar}`;
|
|
167
178
|
case 'update': {
|
|
168
179
|
let stmt = `CREATE ONLY $${mutationIdVar} SET mutationType = 'update', recordId = $${recordIdVar}, data = $${dataVar}`;
|
|
169
180
|
if (beforeRecordVar) {
|
package/src/utils/utils.test.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
generateId,
|
|
10
10
|
parseDuration,
|
|
11
11
|
} from './index';
|
|
12
|
+
import type { QueryTimeToLive } from '../types';
|
|
12
13
|
|
|
13
14
|
describe('compareRecordIds', () => {
|
|
14
15
|
it('returns true for equal strings', () => {
|
|
@@ -104,7 +105,7 @@ describe('parseRecordIdString', () => {
|
|
|
104
105
|
|
|
105
106
|
it('parses id containing colons', () => {
|
|
106
107
|
const rid = parseRecordIdString('table:part1:part2');
|
|
107
|
-
expect(rid.table.
|
|
108
|
+
expect(rid.table.name).toBe('table');
|
|
108
109
|
expect(rid.id).toBe('part1:part2');
|
|
109
110
|
});
|
|
110
111
|
});
|
|
@@ -123,7 +124,7 @@ describe('generateId', () => {
|
|
|
123
124
|
|
|
124
125
|
describe('parseDuration', () => {
|
|
125
126
|
it('parses seconds', () => {
|
|
126
|
-
expect(parseDuration('30s')).toBe(30000);
|
|
127
|
+
expect(parseDuration('30s' as QueryTimeToLive)).toBe(30000);
|
|
127
128
|
});
|
|
128
129
|
|
|
129
130
|
it('parses minutes', () => {
|
package/tsdown.config.ts
CHANGED
|
@@ -1,9 +1,123 @@
|
|
|
1
1
|
import { defineConfig } from 'tsdown';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
|
|
6
|
+
// Resolve real package versions at build time so the DevTools can report the
|
|
7
|
+
// actual bundled frontend versions (used to detect frontend/backend drift).
|
|
8
|
+
const corePkg = require('./package.json');
|
|
9
|
+
const coreVersion: string = corePkg.version;
|
|
10
|
+
// ssp-wasm has no `exports` map; resolve its package.json directly, falling
|
|
11
|
+
// back to the relative workspace path if module resolution can't find it.
|
|
12
|
+
let wasmVersion: string;
|
|
13
|
+
try {
|
|
14
|
+
wasmVersion = require('@spooky-sync/ssp-wasm/package.json').version;
|
|
15
|
+
} catch {
|
|
16
|
+
wasmVersion = require('../ssp-wasm/package.json').version;
|
|
17
|
+
}
|
|
18
|
+
// Report the SurrealDB WASM ENGINE version (`@surrealdb/wasm`) — the engine the
|
|
19
|
+
// in-browser local DB actually runs — NOT the `surrealdb` JS client library
|
|
20
|
+
// version. The WASM engine is the one that must stay compatible with the server
|
|
21
|
+
// engine, so it's the meaningful number to compare in DevTools. Prefer the
|
|
22
|
+
// actually-installed version; fall back to our pinned range if its `exports`
|
|
23
|
+
// map blocks importing its package.json.
|
|
24
|
+
let surrealVersion: string;
|
|
25
|
+
try {
|
|
26
|
+
surrealVersion = require('@surrealdb/wasm/package.json').version;
|
|
27
|
+
} catch {
|
|
28
|
+
surrealVersion = String(corePkg.dependencies?.['@surrealdb/wasm'] ?? 'unknown').replace(
|
|
29
|
+
/^[\^~]/,
|
|
30
|
+
''
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// tsdown 0.12.x forwards a top-level `define` to rolldown's inputOptions, which
|
|
35
|
+
// rejects it — so we do the substitution ourselves with a tiny transform
|
|
36
|
+
// plugin. Only our own modules reference the `__SP00KY_*__` identifiers, and
|
|
37
|
+
// the early `includes` check skips everything else.
|
|
38
|
+
const replacements: Record<string, string> = {
|
|
39
|
+
__SP00KY_CORE_VERSION__: JSON.stringify(coreVersion),
|
|
40
|
+
__SP00KY_WASM_VERSION__: JSON.stringify(wasmVersion),
|
|
41
|
+
__SP00KY_SURREAL_VERSION__: JSON.stringify(surrealVersion),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const versionDefinePlugin = {
|
|
45
|
+
name: 'sp00ky-version-define',
|
|
46
|
+
transform(code: string) {
|
|
47
|
+
if (!code.includes('__SP00KY_')) return null;
|
|
48
|
+
let out = code;
|
|
49
|
+
for (const [token, value] of Object.entries(replacements)) {
|
|
50
|
+
out = out.split(token).join(value);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Worker URLs are written as `./<name>.ts` in source so Vite's src-bundling
|
|
57
|
+
// consumers (the example app aliases core to `src`) resolve them. In the
|
|
58
|
+
// published build each worker is emitted at `dist/<name>.js`, so rewrite the
|
|
59
|
+
// references to `.js` here — otherwise the flat `dist/index.js` carries
|
|
60
|
+
// dangling `.ts` URLs that a consumer's bundler can't resolve.
|
|
61
|
+
const workerUrlPlugin = {
|
|
62
|
+
name: 'sp00ky-worker-url',
|
|
63
|
+
transform(code: string) {
|
|
64
|
+
if (!code.includes('sqlite-worker.ts') && !code.includes('tabs-broker-worker.ts')) return null;
|
|
65
|
+
return code
|
|
66
|
+
.split('sqlite-worker.ts')
|
|
67
|
+
.join('sqlite-worker.js')
|
|
68
|
+
.split('tabs-broker-worker.ts')
|
|
69
|
+
.join('tabs-broker-worker.js');
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default defineConfig([
|
|
74
|
+
{
|
|
75
|
+
// `sqlite-worker` is emitted at `dist/sqlite-worker.js` (top level, NOT under
|
|
76
|
+
// services/database) so the `new URL('./sqlite-worker.js', import.meta.url)`
|
|
77
|
+
// in `SqliteCacheEngine` — which gets bundled into the flat `dist/index.js` —
|
|
78
|
+
// resolves correctly. It (and `@sqlite.org/sqlite-wasm`) load lazily in the
|
|
79
|
+
// Worker; never pulled into the main bundle unless `localEngine: 'sqlite'`.
|
|
80
|
+
entry: {
|
|
81
|
+
index: 'src/index.ts',
|
|
82
|
+
'otel/index': 'src/otel/index.ts',
|
|
83
|
+
'sqlite-worker': 'src/services/database/sqlite-worker.ts',
|
|
84
|
+
// The shared-tabs broker MUST stay a self-contained emit (no chunk
|
|
85
|
+
// imports): bundlers cannot trace `new SharedWorker(url)` graphs, so a
|
|
86
|
+
// relative chunk import would 404 in consumers. The source keeps zero
|
|
87
|
+
// runtime imports and scripts/check-broker-bundle.mjs enforces it.
|
|
88
|
+
'tabs-broker-worker': 'src/services/tabs/tabs-broker-worker.ts',
|
|
89
|
+
},
|
|
5
90
|
format: ['esm'],
|
|
6
91
|
dts: true,
|
|
7
92
|
clean: true,
|
|
8
93
|
hash: false,
|
|
9
|
-
|
|
94
|
+
plugins: [versionDefinePlugin, workerUrlPlugin],
|
|
95
|
+
},
|
|
96
|
+
// The wasm-free entries a service worker, a dedicated worker or Node can
|
|
97
|
+
// import: `/pure` (protocol builders), `/live` (the live feed over the
|
|
98
|
+
// pure-JS `surrealdb` SDK) and `/sw` (push handlers). Built separately so
|
|
99
|
+
// they never share a chunk with `index` (whose graph holds the wasm engines)
|
|
100
|
+
// and so `dist/index.js` stays one flat file. They may share chunks among
|
|
101
|
+
// themselves; scripts/check-worker-entries.mjs walks those too and fails
|
|
102
|
+
// the build on any wasm/pino import or top-level window/document access.
|
|
103
|
+
{
|
|
104
|
+
entry: {
|
|
105
|
+
pure: 'src/pure.ts',
|
|
106
|
+
live: 'src/live/index.ts',
|
|
107
|
+
sw: 'src/sw/index.ts',
|
|
108
|
+
},
|
|
109
|
+
format: ['esm'],
|
|
110
|
+
dts: true,
|
|
111
|
+
clean: false,
|
|
112
|
+
hash: false,
|
|
113
|
+
// Own prefix so a chunk of this build never overwrites one of the first
|
|
114
|
+
// (both write `dist/`; `types.d.ts` collided).
|
|
115
|
+
// The entries' own declaration files arrive as chunks named `<entry>.d`
|
|
116
|
+
// and must keep their plain name.
|
|
117
|
+
outputOptions: {
|
|
118
|
+
chunkFileNames: (chunk: { name: string }) =>
|
|
119
|
+
['pure.d', 'live.d', 'sw.d'].includes(chunk.name) ? '[name].js' : 'worker-[name].js',
|
|
120
|
+
},
|
|
121
|
+
plugins: [versionDefinePlugin],
|
|
122
|
+
},
|
|
123
|
+
]);
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
// Thresholds start at the numbers the suite hits today and are ratcheted per
|
|
4
|
+
// directory as the saga core lands (see the plan's commit 10). Exclusions:
|
|
5
|
+
// *.fixture.ts / src/testing/** test-only harness code
|
|
6
|
+
// sqlite-worker.ts runs inside a Worker (sqlite-wasm + OPFS +
|
|
7
|
+
// postMessage globals); covered by the sqlite
|
|
8
|
+
// integration tests and the build check
|
|
9
|
+
// client/services.ts constructs the real adapters (SurrealDB
|
|
10
|
+
// WASM store, socket, SharedWorker broker,
|
|
11
|
+
// OPFS blob cache); exercised by the example
|
|
12
|
+
// app and the WhitePawn A/B, not by vitest
|
|
13
|
+
export default defineConfig({
|
|
14
|
+
test: {
|
|
15
|
+
environment: 'node',
|
|
16
|
+
include: ['src/**/*.test.ts'],
|
|
17
|
+
coverage: {
|
|
18
|
+
provider: 'v8',
|
|
19
|
+
all: true,
|
|
20
|
+
reporter: ['text-summary', 'lcov'],
|
|
21
|
+
include: ['src/**/*.ts'],
|
|
22
|
+
exclude: [
|
|
23
|
+
'src/**/*.test.ts',
|
|
24
|
+
'src/**/*.d.ts',
|
|
25
|
+
'src/**/*.fixture.ts',
|
|
26
|
+
'src/testing/**',
|
|
27
|
+
'src/services/database/sqlite-worker.ts',
|
|
28
|
+
'src/client/services.ts',
|
|
29
|
+
],
|
|
30
|
+
thresholds: {
|
|
31
|
+
// Global numbers are held back by the legacy adapters (devtools,
|
|
32
|
+
// sqlite engine, tabs broker, blobs, crdt); the saga core below is 100%.
|
|
33
|
+
lines: 75,
|
|
34
|
+
functions: 82,
|
|
35
|
+
branches: 87,
|
|
36
|
+
statements: 75,
|
|
37
|
+
'src/kernel/**': { lines: 100, functions: 100, branches: 100, statements: 100 },
|
|
38
|
+
'src/state/**': { lines: 100, functions: 100, branches: 100, statements: 100 },
|
|
39
|
+
'src/query/**': { lines: 100, functions: 100, branches: 100, statements: 100 },
|
|
40
|
+
'src/mutation/**': { lines: 100, functions: 100, branches: 100, statements: 100 },
|
|
41
|
+
'src/sync/**': { lines: 100, functions: 100, branches: 100, statements: 100 },
|
|
42
|
+
'src/boot/**': { lines: 100, functions: 100, branches: 100, statements: 100 },
|
|
43
|
+
// The facade's shared-tabs host callbacks need a SharedWorker broker to fire.
|
|
44
|
+
'src/client/**': { lines: 93, functions: 92, branches: 89, statements: 93 },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
});
|