@spooky-sync/core 0.0.1-canary.240 → 0.0.1-canary.241
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 +34 -15
- package/README.md +56 -16
- package/dist/index.d.ts +1136 -1846
- package/dist/index.js +10168 -11267
- package/dist/types.d.ts +22 -79
- package/package.json +8 -4
- package/skills/sp00ky-core/SKILL.md +18 -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 +151 -0
- package/src/boot/boot.saga.ts +128 -0
- package/src/boot/bucket-switch.saga.test.ts +158 -0
- package/src/boot/bucket-switch.saga.ts +115 -0
- package/src/boot/preload.saga.test.ts +51 -0
- package/src/boot/preload.saga.ts +31 -0
- package/src/client/bucket-handle.test.ts +117 -0
- package/src/client/bucket-handle.ts +227 -0
- package/src/client/router.test.ts +56 -0
- package/src/client/router.ts +94 -0
- package/src/client/runtime.test.ts +192 -0
- package/src/client/runtime.ts +296 -0
- package/src/client/services.ts +349 -0
- package/src/client/sp00ky-client.test.ts +241 -0
- package/src/client/sp00ky-client.ts +482 -0
- package/src/index.ts +1 -0
- package/src/kernel/constants.test.ts +19 -0
- package/src/kernel/constants.ts +54 -0
- package/src/kernel/effects.test.ts +40 -0
- package/src/kernel/effects.ts +159 -0
- package/src/kernel/events.ts +55 -0
- package/src/kernel/interpreter.test.ts +116 -0
- package/src/kernel/interpreter.ts +143 -0
- package/src/kernel/saga.test.ts +89 -0
- package/src/kernel/saga.ts +73 -0
- package/src/modules/app-release/index.ts +13 -5
- package/src/modules/devtools/index.ts +10 -3
- package/src/modules/devtools/state-shape.test.ts +4 -1
- package/src/modules/feature-flag/index.ts +13 -5
- package/src/mutation/jobs.test.ts +25 -0
- package/src/mutation/jobs.ts +41 -0
- package/src/{modules/data → mutation}/mutation-id.test.ts +12 -0
- package/src/mutation/push.saga.test.ts +285 -0
- package/src/mutation/push.saga.ts +215 -0
- package/src/mutation/rows.test.ts +201 -0
- package/src/mutation/rows.ts +377 -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 +163 -0
- package/src/mutation/write.saga.ts +166 -0
- package/src/query/env.test.ts +23 -0
- package/src/query/env.ts +56 -0
- package/src/query/fetch.saga.test.ts +156 -0
- package/src/query/fetch.saga.ts +109 -0
- package/src/query/hash.test.ts +14 -0
- package/src/query/hash.ts +18 -0
- package/src/{modules/data → query}/id-set-plan.test.ts +12 -2
- package/src/query/lifecycle.saga.test.ts +122 -0
- package/src/query/lifecycle.saga.ts +101 -0
- package/src/query/materialize.saga.test.ts +82 -0
- package/src/query/materialize.saga.ts +75 -0
- package/src/query/materialize.test.ts +50 -0
- package/src/query/materialize.ts +44 -0
- package/src/query/membership.saga.test.ts +246 -0
- package/src/query/membership.saga.ts +208 -0
- package/src/query/membership.test.ts +118 -0
- package/src/query/membership.ts +158 -0
- package/src/query/register.saga.test.ts +251 -0
- package/src/query/register.saga.ts +190 -0
- package/src/query/render-set.test.ts +36 -0
- package/src/query/render-set.ts +60 -0
- package/src/query/sql.test.ts +62 -0
- package/src/query/sql.ts +79 -0
- package/src/{modules/data → query}/window-query.test.ts +18 -0
- package/src/{modules/data → query}/window-query.ts +2 -1
- package/src/services/database/connection-supervisor.test.ts +29 -0
- package/src/services/database/connection-supervisor.ts +7 -0
- package/src/services/database/database.responses.test.ts +43 -0
- package/src/services/database/database.ts +25 -10
- package/src/services/database/local-migrator.ts +2 -2
- package/src/services/database/plan-render.test.ts +1 -1
- package/src/services/database/sqlite-cache-engine.test.ts +4 -3
- package/src/services/database/sqlite-cache-engine.ts +2 -11
- package/src/services/database/sqlite-devtools-queries.integration.test.ts +1 -1
- package/src/services/tabs/protocol.ts +7 -3
- package/src/sp00ky.ts +5 -1854
- package/src/state/client-state.ts +184 -0
- package/src/state/lifecycle.test.ts +118 -0
- package/src/state/lifecycle.ts +86 -0
- package/src/state/reducers.test.ts +280 -0
- package/src/state/reducers.ts +476 -0
- package/src/state/selectors.test.ts +149 -0
- package/src/state/selectors.ts +184 -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 +69 -0
- package/src/sync/live.saga.ts +49 -0
- package/src/sync/policy.health.test.ts +56 -0
- package/src/{modules/sync/utils.test.ts → sync/policy.test.ts} +31 -3
- package/src/{modules/sync/utils.ts → sync/policy.ts} +67 -5
- package/src/sync/poll.saga.test.ts +51 -0
- package/src/sync/poll.saga.ts +36 -0
- package/src/sync/tabs.saga.test.ts +74 -0
- package/src/sync/tabs.saga.ts +92 -0
- package/src/testing/adapters.ts +133 -0
- package/src/testing/build.ts +61 -0
- package/src/testing/fake-services.ts +34 -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 +8 -7
- package/src/utils/utils.test.ts +2 -1
- package/vitest.config.ts +48 -0
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +0 -476
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -210
- package/coverage/lcov-report/src/boot/auth-flip.saga.ts.html +0 -271
- package/coverage/lcov-report/src/boot/boot.saga.ts.html +0 -469
- package/coverage/lcov-report/src/boot/bucket-switch.saga.ts.html +0 -430
- package/coverage/lcov-report/src/boot/index.html +0 -161
- package/coverage/lcov-report/src/boot/preload.saga.ts.html +0 -178
- package/coverage/lcov-report/src/client/bucket-handle.ts.html +0 -766
- package/coverage/lcov-report/src/client/index.html +0 -161
- package/coverage/lcov-report/src/client/router.ts.html +0 -367
- package/coverage/lcov-report/src/client/runtime.ts.html +0 -940
- package/coverage/lcov-report/src/client/sp00ky-client.ts.html +0 -1531
- package/coverage/lcov-report/src/events/index.html +0 -116
- package/coverage/lcov-report/src/events/index.ts.html +0 -877
- package/coverage/lcov-report/src/index.html +0 -146
- package/coverage/lcov-report/src/index.ts.html +0 -202
- package/coverage/lcov-report/src/kernel/constants.ts.html +0 -247
- package/coverage/lcov-report/src/kernel/effects.ts.html +0 -562
- package/coverage/lcov-report/src/kernel/events.ts.html +0 -250
- package/coverage/lcov-report/src/kernel/index.html +0 -176
- package/coverage/lcov-report/src/kernel/interpreter.ts.html +0 -514
- package/coverage/lcov-report/src/kernel/saga.ts.html +0 -304
- package/coverage/lcov-report/src/modules/app-release/index.html +0 -116
- package/coverage/lcov-report/src/modules/app-release/index.ts.html +0 -712
- package/coverage/lcov-report/src/modules/auth/events/index.html +0 -116
- package/coverage/lcov-report/src/modules/auth/events/index.ts.html +0 -142
- package/coverage/lcov-report/src/modules/auth/index.html +0 -116
- package/coverage/lcov-report/src/modules/auth/index.ts.html +0 -1225
- package/coverage/lcov-report/src/modules/crdt/crdt-field.ts.html +0 -967
- package/coverage/lcov-report/src/modules/crdt/index.html +0 -146
- package/coverage/lcov-report/src/modules/crdt/index.ts.html +0 -1474
- package/coverage/lcov-report/src/modules/crdt/loro-loader.ts.html +0 -160
- package/coverage/lcov-report/src/modules/devtools/flags.ts.html +0 -1132
- package/coverage/lcov-report/src/modules/devtools/index.html +0 -161
- package/coverage/lcov-report/src/modules/devtools/index.ts.html +0 -2413
- package/coverage/lcov-report/src/modules/devtools/storage-info.ts.html +0 -589
- package/coverage/lcov-report/src/modules/devtools/versions.ts.html +0 -415
- package/coverage/lcov-report/src/modules/feature-flag/index.html +0 -116
- package/coverage/lcov-report/src/modules/feature-flag/index.ts.html +0 -1033
- package/coverage/lcov-report/src/modules/index.html +0 -116
- package/coverage/lcov-report/src/modules/ref-tables.ts.html +0 -349
- package/coverage/lcov-report/src/mutation/index.html +0 -191
- package/coverage/lcov-report/src/mutation/jobs.ts.html +0 -208
- package/coverage/lcov-report/src/mutation/mutation-id.ts.html +0 -190
- package/coverage/lcov-report/src/mutation/push.saga.ts.html +0 -730
- package/coverage/lcov-report/src/mutation/rows.ts.html +0 -1216
- package/coverage/lcov-report/src/mutation/tray.saga.ts.html +0 -229
- package/coverage/lcov-report/src/mutation/write.saga.ts.html +0 -583
- package/coverage/lcov-report/src/otel/index.html +0 -116
- package/coverage/lcov-report/src/otel/index.ts.html +0 -466
- package/coverage/lcov-report/src/query/env.ts.html +0 -253
- package/coverage/lcov-report/src/query/fetch.saga.ts.html +0 -412
- package/coverage/lcov-report/src/query/hash.ts.html +0 -139
- package/coverage/lcov-report/src/query/index.html +0 -281
- package/coverage/lcov-report/src/query/lifecycle.saga.ts.html +0 -388
- package/coverage/lcov-report/src/query/materialize.saga.ts.html +0 -310
- package/coverage/lcov-report/src/query/materialize.ts.html +0 -217
- package/coverage/lcov-report/src/query/membership.saga.ts.html +0 -709
- package/coverage/lcov-report/src/query/membership.ts.html +0 -559
- package/coverage/lcov-report/src/query/register.saga.ts.html +0 -655
- package/coverage/lcov-report/src/query/render-set.ts.html +0 -265
- package/coverage/lcov-report/src/query/sql.ts.html +0 -322
- package/coverage/lcov-report/src/query/window-query.ts.html +0 -670
- package/coverage/lcov-report/src/services/blobs/blob-cache.ts.html +0 -1894
- package/coverage/lcov-report/src/services/blobs/blob-manifest.ts.html +0 -766
- package/coverage/lcov-report/src/services/blobs/blob-store.ts.html +0 -1162
- package/coverage/lcov-report/src/services/blobs/index.html +0 -161
- package/coverage/lcov-report/src/services/blobs/index.ts.html +0 -295
- package/coverage/lcov-report/src/services/database/cache-engine.ts.html +0 -664
- package/coverage/lcov-report/src/services/database/connection-supervisor.ts.html +0 -1330
- package/coverage/lcov-report/src/services/database/database.ts.html +0 -658
- package/coverage/lcov-report/src/services/database/engine-factory.ts.html +0 -184
- package/coverage/lcov-report/src/services/database/errors.ts.html +0 -187
- package/coverage/lcov-report/src/services/database/events/index.html +0 -116
- package/coverage/lcov-report/src/services/database/events/index.ts.html +0 -184
- package/coverage/lcov-report/src/services/database/index.html +0 -386
- package/coverage/lcov-report/src/services/database/index.ts.html +0 -121
- package/coverage/lcov-report/src/services/database/local-migrator.ts.html +0 -703
- package/coverage/lcov-report/src/services/database/local.ts.html +0 -1633
- package/coverage/lcov-report/src/services/database/plan-render.ts.html +0 -409
- package/coverage/lcov-report/src/services/database/relation-resolver.ts.html +0 -682
- package/coverage/lcov-report/src/services/database/remote.ts.html +0 -1036
- package/coverage/lcov-report/src/services/database/sqlite-cache-engine.ts.html +0 -4132
- package/coverage/lcov-report/src/services/database/sqlite-lock-verify.ts.html +0 -220
- package/coverage/lcov-report/src/services/database/sqlite-open.ts.html +0 -577
- package/coverage/lcov-report/src/services/database/sqlite-plan-sql.ts.html +0 -499
- package/coverage/lcov-report/src/services/database/sqlite-select.ts.html +0 -478
- package/coverage/lcov-report/src/services/database/sqlite-transport.ts.html +0 -757
- package/coverage/lcov-report/src/services/database/surql-translate.ts.html +0 -1333
- package/coverage/lcov-report/src/services/database/surreal-cache-engine.ts.html +0 -568
- package/coverage/lcov-report/src/services/logger/index.html +0 -116
- package/coverage/lcov-report/src/services/logger/index.ts.html +0 -154
- package/coverage/lcov-report/src/services/persistence/index.html +0 -146
- package/coverage/lcov-report/src/services/persistence/localstorage.ts.html +0 -163
- package/coverage/lcov-report/src/services/persistence/resilient.ts.html +0 -208
- package/coverage/lcov-report/src/services/persistence/surrealdb.ts.html +0 -271
- package/coverage/lcov-report/src/services/stream-processor/index.html +0 -146
- package/coverage/lcov-report/src/services/stream-processor/index.ts.html +0 -3313
- package/coverage/lcov-report/src/services/stream-processor/permissions.ts.html +0 -244
- package/coverage/lcov-report/src/services/stream-processor/wasm-types.ts.html +0 -346
- package/coverage/lcov-report/src/services/tabs/broker-client.ts.html +0 -934
- package/coverage/lcov-report/src/services/tabs/coordinator.ts.html +0 -1984
- package/coverage/lcov-report/src/services/tabs/index.html +0 -191
- package/coverage/lcov-report/src/services/tabs/leader-locks.ts.html +0 -310
- package/coverage/lcov-report/src/services/tabs/protocol.ts.html +0 -871
- package/coverage/lcov-report/src/services/tabs/support.ts.html +0 -193
- package/coverage/lcov-report/src/services/tabs/tabs-broker-worker.ts.html +0 -2005
- package/coverage/lcov-report/src/sp00ky.ts.html +0 -100
- package/coverage/lcov-report/src/state/client-state.ts.html +0 -637
- package/coverage/lcov-report/src/state/index.html +0 -161
- package/coverage/lcov-report/src/state/lifecycle.ts.html +0 -343
- package/coverage/lcov-report/src/state/reducers.ts.html +0 -1471
- package/coverage/lcov-report/src/state/selectors.ts.html +0 -637
- package/coverage/lcov-report/src/sync/connection.saga.ts.html +0 -343
- package/coverage/lcov-report/src/sync/index.html +0 -176
- package/coverage/lcov-report/src/sync/live.saga.ts.html +0 -232
- package/coverage/lcov-report/src/sync/policy.ts.html +0 -1606
- package/coverage/lcov-report/src/sync/poll.saga.ts.html +0 -193
- package/coverage/lcov-report/src/sync/tabs.saga.ts.html +0 -361
- package/coverage/lcov-report/src/types.ts.html +0 -2419
- package/coverage/lcov-report/src/utils/blurhash.ts.html +0 -355
- package/coverage/lcov-report/src/utils/error-classification.ts.html +0 -244
- package/coverage/lcov-report/src/utils/index.html +0 -191
- package/coverage/lcov-report/src/utils/index.ts.html +0 -820
- package/coverage/lcov-report/src/utils/parser.ts.html +0 -313
- package/coverage/lcov-report/src/utils/semver.ts.html +0 -175
- package/coverage/lcov-report/src/utils/surql.ts.html +0 -667
- package/coverage/lcov.info +0 -19079
- package/src/modules/cache/cache.relay.test.ts +0 -95
- package/src/modules/cache/index.ts +0 -361
- package/src/modules/cache/types.ts +0 -19
- package/src/modules/data/data.activity.test.ts +0 -109
- package/src/modules/data/data.authority.test.ts +0 -122
- package/src/modules/data/data.heartbeat-wake.test.ts +0 -115
- package/src/modules/data/data.hydration.test.ts +0 -142
- package/src/modules/data/data.membership.test.ts +0 -623
- package/src/modules/data/data.notify-table.test.ts +0 -41
- package/src/modules/data/data.pending-ids.test.ts +0 -199
- package/src/modules/data/data.rebind.test.ts +0 -170
- package/src/modules/data/data.rematerialize.test.ts +0 -114
- package/src/modules/data/data.run.test.ts +0 -113
- package/src/modules/data/data.settled-writes.test.ts +0 -235
- package/src/modules/data/data.status.test.ts +0 -249
- package/src/modules/data/data.test.ts +0 -58
- package/src/modules/data/index.ts +0 -2745
- package/src/modules/sync/engine.ts +0 -250
- package/src/modules/sync/events/index.ts +0 -84
- package/src/modules/sync/index.ts +0 -3
- package/src/modules/sync/queue/index.ts +0 -2
- package/src/modules/sync/queue/queue-down.test.ts +0 -228
- package/src/modules/sync/queue/queue-down.ts +0 -184
- package/src/modules/sync/queue/queue-up.forwarded.test.ts +0 -164
- package/src/modules/sync/queue/queue-up.ts +0 -407
- package/src/modules/sync/scheduler.pause.test.ts +0 -109
- package/src/modules/sync/scheduler.retry.test.ts +0 -237
- package/src/modules/sync/scheduler.ts +0 -286
- package/src/modules/sync/sync.cleanup.test.ts +0 -116
- package/src/modules/sync/sync.health.test.ts +0 -167
- package/src/modules/sync/sync.heartbeat.test.ts +0 -80
- package/src/modules/sync/sync.list-ref-meta.test.ts +0 -193
- package/src/modules/sync/sync.live-removal.test.ts +0 -289
- package/src/modules/sync/sync.reconnect.test.ts +0 -145
- package/src/modules/sync/sync.rematerialize-after-fetch.test.ts +0 -93
- package/src/modules/sync/sync.settled-poll.test.ts +0 -58
- package/src/modules/sync/sync.subquery.test.ts +0 -82
- package/src/modules/sync/sync.tabs.test.ts +0 -249
- package/src/modules/sync/sync.ts +0 -2626
- package/src/sp00ky.auth-order.test.ts +0 -112
- package/src/sp00ky.init-query.test.ts +0 -183
- package/src/sp00ky.local-first.test.ts +0 -60
- /package/src/{modules/data → mutation}/mutation-id.ts +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { RecordVersionArray } from '../types';
|
|
2
|
+
import type { QueryPhase } from '../state/lifecycle';
|
|
3
|
+
import type { Overlay } from '../state/selectors';
|
|
4
|
+
|
|
5
|
+
export interface RenderInput {
|
|
6
|
+
phase: QueryPhase;
|
|
7
|
+
remoteArray: RecordVersionArray;
|
|
8
|
+
localArray: RecordVersionArray;
|
|
9
|
+
isWindow: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The id-set a query renders from, or `null` when it must fall back to a
|
|
14
|
+
* local predicate scan (a cold query that was never resolved on this device).
|
|
15
|
+
* A cold window has no usable scan (re-applying `START m` against the shared
|
|
16
|
+
* store returns the wrong rows), so it renders the SSP's local window instead.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveMembership(input: RenderInput): RecordVersionArray | null {
|
|
19
|
+
if (input.phase !== 'cold') return input.remoteArray;
|
|
20
|
+
if (input.isWindow) return input.localArray;
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RenderOptions {
|
|
25
|
+
hasExplicitOrder: boolean;
|
|
26
|
+
isWindow: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* render = (membership ∪ (writes ∩ localView)) − deletes
|
|
31
|
+
*
|
|
32
|
+
* `writes`/`deletes` are the outbox overlay (pending and acked items). The
|
|
33
|
+
* middle term keeps optimistic writes visible without re-admitting stale rows:
|
|
34
|
+
* `localView` (the SSP's local id-set) says whether the written row matches
|
|
35
|
+
* the query per local truth. Sorted unless the query orders itself or is a
|
|
36
|
+
* window (whose id order is the slice order).
|
|
37
|
+
*/
|
|
38
|
+
export function buildRenderIds(
|
|
39
|
+
membership: RecordVersionArray,
|
|
40
|
+
localView: RecordVersionArray,
|
|
41
|
+
overlay: Overlay,
|
|
42
|
+
opts: RenderOptions
|
|
43
|
+
): string[] {
|
|
44
|
+
const ordered: string[] = [];
|
|
45
|
+
const seen = new Set<string>();
|
|
46
|
+
for (const [id] of membership) {
|
|
47
|
+
if (overlay.deletes.has(id) || seen.has(id)) continue;
|
|
48
|
+
seen.add(id);
|
|
49
|
+
ordered.push(id);
|
|
50
|
+
}
|
|
51
|
+
if (overlay.writes.size > 0) {
|
|
52
|
+
for (const [id] of localView) {
|
|
53
|
+
if (!overlay.writes.has(id) || overlay.deletes.has(id) || seen.has(id)) continue;
|
|
54
|
+
seen.add(id);
|
|
55
|
+
ordered.push(id);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!opts.hasExplicitOrder && !opts.isWindow) ordered.sort();
|
|
59
|
+
return ordered;
|
|
60
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { RecordId } from 'surrealdb';
|
|
3
|
+
import * as sql from './sql';
|
|
4
|
+
import {
|
|
5
|
+
buildListRefBatchSelect,
|
|
6
|
+
buildListRefSelect,
|
|
7
|
+
buildQueryRowCountBatchSelect,
|
|
8
|
+
buildQueryRowCountSelect,
|
|
9
|
+
buildSubqueryListRefSelect,
|
|
10
|
+
} from '../sync/policy';
|
|
11
|
+
|
|
12
|
+
describe('query sql builders (golden against the previous builders)', () => {
|
|
13
|
+
it('edge and meta selects are byte-identical to the old helpers', () => {
|
|
14
|
+
expect(sql.listRefSelect('_00_list_ref_user_x')).toBe(buildListRefSelect('_00_list_ref_user_x'));
|
|
15
|
+
expect(sql.subqueryListRefSelect('t')).toBe(buildSubqueryListRefSelect('t'));
|
|
16
|
+
expect(sql.queryRowCountSelect()).toBe(buildQueryRowCountSelect());
|
|
17
|
+
expect(sql.listRefBatchSelect('t')).toBe(buildListRefBatchSelect('t'));
|
|
18
|
+
expect(sql.queryRowCountBatchSelect()).toBe(buildQueryRowCountBatchSelect());
|
|
19
|
+
});
|
|
20
|
+
it('composes the single, batch and register requests', () => {
|
|
21
|
+
expect(sql.singleSnapshotSelect('t').split(';\n')).toEqual([
|
|
22
|
+
sql.listRefSelect('t'),
|
|
23
|
+
sql.queryRowCountSelect(),
|
|
24
|
+
sql.subqueryListRefSelect('t'),
|
|
25
|
+
]);
|
|
26
|
+
expect(sql.batchSnapshotSelect('t')).toBe(`${sql.listRefBatchSelect('t')};\n${sql.queryRowCountBatchSelect()}`);
|
|
27
|
+
expect(sql.registerSelect('t').startsWith('fn::query::register($config);\n')).toBe(true);
|
|
28
|
+
const id = new RecordId('_00_query', 'h');
|
|
29
|
+
expect(sql.registerVars({ id, surql: 's', params: { a: 1 }, ttl: '10m' })).toEqual({
|
|
30
|
+
config: { id, surql: 's', params: { a: 1 }, ttl: '10m' },
|
|
31
|
+
in: id,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
it('view rows and ids', () => {
|
|
35
|
+
expect(sql.viewRecordId('k')).toEqual(new RecordId('_00_view', 'k'));
|
|
36
|
+
expect(sql.viewRow([['t:1', 1]], true, 5)).toEqual({ ids: [['t:1', 1]], confirmed: true, updatedAt: 5 });
|
|
37
|
+
expect(sql.readLegacyViewRows()).toBe('SELECT * FROM _00_window');
|
|
38
|
+
expect(sql.countViewRows()).toContain('_00_view');
|
|
39
|
+
expect(sql.bodySelect()).toBe('SELECT * FROM $ids');
|
|
40
|
+
});
|
|
41
|
+
it('heartbeat batch answers per index and detects reclaimed rows', () => {
|
|
42
|
+
const a = new RecordId('_00_query', 'a');
|
|
43
|
+
const b = new RecordId('_00_query', 'b');
|
|
44
|
+
expect(sql.heartbeatBatch([a, b])).toEqual({
|
|
45
|
+
sql: 'fn::query::heartbeat($id0);\nfn::query::heartbeat($id1)',
|
|
46
|
+
vars: { id0: a, id1: b },
|
|
47
|
+
});
|
|
48
|
+
expect(sql.heartbeatRowGone([])).toBe(true);
|
|
49
|
+
expect(sql.heartbeatRowGone([{ id: a }])).toBe(false);
|
|
50
|
+
expect(sql.heartbeatRowGone(undefined)).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
it('upsertBodiesTx MERGEs each body inside one transaction', () => {
|
|
53
|
+
const { query, vars } = sql.upsertBodiesTx([
|
|
54
|
+
{ id: 'thing:1', content: { a: 1 } },
|
|
55
|
+
{ id: 'thing:2', content: { b: 2 } },
|
|
56
|
+
]);
|
|
57
|
+
expect(query.sql).toBe(
|
|
58
|
+
'BEGIN TRANSACTION;\nUPSERT ONLY $id0 MERGE $content0;UPSERT ONLY $id1 MERGE $content1;\nCOMMIT TRANSACTION;'
|
|
59
|
+
);
|
|
60
|
+
expect(vars).toEqual({ id0: 'thing:1', content0: { a: 1 }, id1: 'thing:2', content1: { b: 2 } });
|
|
61
|
+
});
|
|
62
|
+
});
|
package/src/query/sql.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { RecordId } from 'surrealdb';
|
|
2
|
+
import type { RecordVersionArray } from '../types';
|
|
3
|
+
import type { Vars } from '../kernel/effects';
|
|
4
|
+
import { surql } from '../utils/surql';
|
|
5
|
+
import type { SealedQuery } from '../utils/surql';
|
|
6
|
+
|
|
7
|
+
/** Every SurrealQL statement the query side sends, as pure builders. */
|
|
8
|
+
|
|
9
|
+
export const VIEW_TABLE = '_00_view';
|
|
10
|
+
export const LEGACY_VIEW_TABLE = '_00_window';
|
|
11
|
+
|
|
12
|
+
export const viewRecordId = (viewKey: string): RecordId<string> => new RecordId(VIEW_TABLE, viewKey);
|
|
13
|
+
|
|
14
|
+
export const viewRow = (ids: RecordVersionArray, confirmed: boolean, now: number): Record<string, unknown> => ({
|
|
15
|
+
ids,
|
|
16
|
+
confirmed,
|
|
17
|
+
updatedAt: now,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/** Copy every legacy `_00_window` row into `_00_view` once. */
|
|
21
|
+
export const readLegacyViewRows = (): string => `SELECT * FROM ${LEGACY_VIEW_TABLE}`;
|
|
22
|
+
export const countViewRows = (): string => `SELECT count() FROM ${VIEW_TABLE} GROUP ALL`;
|
|
23
|
+
|
|
24
|
+
export const listRefSelect = (table: string): string =>
|
|
25
|
+
`SELECT out, version FROM ${table} WHERE in = $in AND parent IS NONE`;
|
|
26
|
+
export const subqueryListRefSelect = (table: string): string =>
|
|
27
|
+
`SELECT out, version FROM ${table} WHERE in = $in AND parent IS NOT NONE`;
|
|
28
|
+
export const queryRowCountSelect = (): string => 'SELECT VALUE { rowCount: rowCount, state: state } FROM ONLY $in';
|
|
29
|
+
export const listRefBatchSelect = (table: string): string =>
|
|
30
|
+
`SELECT in, out, version, parent FROM ${table} WHERE in IN $ins`;
|
|
31
|
+
export const queryRowCountBatchSelect = (): string => 'SELECT VALUE { id: id, rowCount: rowCount, state: state } FROM $ins';
|
|
32
|
+
|
|
33
|
+
/** The single-query read: edges, meta, subquery children in one request. */
|
|
34
|
+
export const singleSnapshotSelect = (table: string): string =>
|
|
35
|
+
`${listRefSelect(table)};\n${queryRowCountSelect()};\n${subqueryListRefSelect(table)}`;
|
|
36
|
+
/** The many-query read: all edges + all metas in one request. */
|
|
37
|
+
export const batchSnapshotSelect = (table: string): string =>
|
|
38
|
+
`${listRefBatchSelect(table)};\n${queryRowCountBatchSelect()}`;
|
|
39
|
+
|
|
40
|
+
export interface RegisterPayload {
|
|
41
|
+
id: RecordId<string>;
|
|
42
|
+
surql: string;
|
|
43
|
+
params: Record<string, unknown>;
|
|
44
|
+
ttl: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Register + read back edges/meta/children in ONE request. */
|
|
48
|
+
export const registerSelect = (table: string): string =>
|
|
49
|
+
`fn::query::register($config);\n${singleSnapshotSelect(table)}`;
|
|
50
|
+
export const registerVars = (payload: RegisterPayload): Vars => ({ config: payload, in: payload.id });
|
|
51
|
+
|
|
52
|
+
/** One heartbeat statement per view id; result index i answers id i. */
|
|
53
|
+
export function heartbeatBatch(ids: ReadonlyArray<RecordId<string>>): { sql: string; vars: Vars } {
|
|
54
|
+
const vars: Vars = {};
|
|
55
|
+
const stmts = ids.map((id, i) => {
|
|
56
|
+
vars[`id${i}`] = id;
|
|
57
|
+
return `fn::query::heartbeat($id${i})`;
|
|
58
|
+
});
|
|
59
|
+
return { sql: stmts.join(';\n'), vars };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A heartbeat answer whose statement matched no row: the view was reclaimed. */
|
|
63
|
+
export const heartbeatRowGone = (result: unknown): boolean => Array.isArray(result) && result.length === 0;
|
|
64
|
+
|
|
65
|
+
export const bodySelect = (): string => 'SELECT * FROM $ids';
|
|
66
|
+
|
|
67
|
+
/** Local transaction that MERGEs many fetched bodies. */
|
|
68
|
+
export function upsertBodiesTx(records: ReadonlyArray<{ id: unknown; content: Record<string, unknown> }>): {
|
|
69
|
+
query: SealedQuery<unknown>;
|
|
70
|
+
vars: Vars;
|
|
71
|
+
} {
|
|
72
|
+
const vars: Vars = {};
|
|
73
|
+
const stmts = records.map((r, i) => {
|
|
74
|
+
vars[`id${i}`] = r.id;
|
|
75
|
+
vars[`content${i}`] = r.content;
|
|
76
|
+
return surql.upsertMerge(`id${i}`, `content${i}`);
|
|
77
|
+
});
|
|
78
|
+
return { query: surql.seal<void>(surql.tx(stmts)) as SealedQuery<unknown>, vars };
|
|
79
|
+
}
|
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { buildWindowMaterialization } from './window-query';
|
|
3
3
|
|
|
4
|
+
describe('scanner edge cases', () => {
|
|
5
|
+
it('skips escaped quotes inside strings and ignores clauses in parens', () => {
|
|
6
|
+
const q = "SELECT * FROM t WHERE a = 'it\\'s (FROM)' AND b IN (SELECT id FROM u LIMIT 1 START 1) LIMIT 5 START 5";
|
|
7
|
+
expect(buildWindowMaterialization(q)).toEqual({ query: 'SELECT * FROM $__win' });
|
|
8
|
+
});
|
|
9
|
+
it('requires whitespace inside ORDER BY and a number after START', () => {
|
|
10
|
+
expect(buildWindowMaterialization('SELECT * FROM t ORDERBY a LIMIT 5 START 5')).toEqual({ query: 'SELECT * FROM $__win' });
|
|
11
|
+
expect(buildWindowMaterialization('SELECT * FROM t LIMIT 5 START $n')).toBeNull();
|
|
12
|
+
expect(buildWindowMaterialization('SELECT * FROM t LIMIT 5 START')).toBeNull();
|
|
13
|
+
});
|
|
14
|
+
it('matches a keyword at the very start and at the end of the string', () => {
|
|
15
|
+
expect(buildWindowMaterialization('FROM t LIMIT 5 START 5')).toEqual({ query: ' FROM $__win' });
|
|
16
|
+
expect(buildWindowMaterialization('SELECT * FROM t ORDER BY a LIMIT 5 START 5 ORDER')).toEqual({
|
|
17
|
+
query: 'SELECT * FROM $__win ORDER BY a',
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
4
22
|
describe('buildWindowMaterialization', () => {
|
|
5
23
|
it('rewrites the game-list window query (START 30) to select the id-set, keeping ORDER BY', () => {
|
|
6
24
|
const surql =
|
|
@@ -104,8 +104,9 @@ export function buildWindowMaterialization(
|
|
|
104
104
|
idsParam = '__win'
|
|
105
105
|
): { query: string } | null {
|
|
106
106
|
const kw = scanTopLevelClauses(surql);
|
|
107
|
+
// `START` is only recorded once `FROM` was seen, so a positive start value
|
|
108
|
+
// implies a FROM index.
|
|
107
109
|
if (kw.startValue === null || kw.startValue <= 0) return null;
|
|
108
|
-
if (kw.fromIndex === null) return null;
|
|
109
110
|
return { query: renderIdSetSelect(surql, kw, idsParam) };
|
|
110
111
|
}
|
|
111
112
|
|
|
@@ -229,6 +229,35 @@ describe('ConnectionSupervisor', () => {
|
|
|
229
229
|
sup.dispose();
|
|
230
230
|
});
|
|
231
231
|
|
|
232
|
+
it('un-sticks the forced offline state when the SDK slept through the outage', async () => {
|
|
233
|
+
const listeners = new Map<string, Array<() => void>>();
|
|
234
|
+
vi.stubGlobal('window', {
|
|
235
|
+
addEventListener: (e: string, cb: () => void) => {
|
|
236
|
+
listeners.set(e, [...(listeners.get(e) ?? []), cb]);
|
|
237
|
+
},
|
|
238
|
+
removeEventListener: () => {},
|
|
239
|
+
});
|
|
240
|
+
const { remote, emit } = makeRemote();
|
|
241
|
+
const sup = makeSupervisor(remote);
|
|
242
|
+
emit('connected');
|
|
243
|
+
sup.start();
|
|
244
|
+
|
|
245
|
+
// The browser reports offline, but the SDK's own socket never notices: its
|
|
246
|
+
// status stays `connected` throughout, so it emits no transition at all.
|
|
247
|
+
listeners.get('offline')?.forEach((cb) => cb());
|
|
248
|
+
expect(sup.connection).toBe('disconnected');
|
|
249
|
+
|
|
250
|
+
// Back online. Nothing but the probe can correct the forced state here -
|
|
251
|
+
// there is no SDK event coming - and traffic flowing under a permanent
|
|
252
|
+
// "offline" indicator is exactly the bug.
|
|
253
|
+
listeners.get('online')?.forEach((cb) => cb());
|
|
254
|
+
await vi.advanceTimersByTimeAsync(10);
|
|
255
|
+
expect(remote.query).toHaveBeenCalled();
|
|
256
|
+
expect(sup.connection).toBe('connected');
|
|
257
|
+
|
|
258
|
+
sup.dispose();
|
|
259
|
+
});
|
|
260
|
+
|
|
232
261
|
it('probes immediately when a hidden tab becomes visible', async () => {
|
|
233
262
|
const listeners = new Map<string, Array<() => void>>();
|
|
234
263
|
vi.stubGlobal('document', {
|
|
@@ -286,6 +286,13 @@ export class ConnectionSupervisor {
|
|
|
286
286
|
`Heartbeat timed out after ${this.config.heartbeatTimeoutMs}ms`
|
|
287
287
|
);
|
|
288
288
|
this.heartbeatFailures = 0;
|
|
289
|
+
// A round trip is the strongest evidence there is that the socket works,
|
|
290
|
+
// so it also RECONCILES the reported state. The `offline` handler forces
|
|
291
|
+
// `disconnected` without the SDK's agreement; if the SDK never noticed
|
|
292
|
+
// the outage it emits no transition when the network returns, and that
|
|
293
|
+
// forced state would otherwise stick for the life of the page - traffic
|
|
294
|
+
// flowing while every indicator says offline.
|
|
295
|
+
this.setState('connected');
|
|
289
296
|
this.startHeartbeat();
|
|
290
297
|
} catch (err) {
|
|
291
298
|
this.heartbeatFailures++;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { AbstractDatabaseService } from './database';
|
|
3
|
+
import { createDatabaseEventSystem, DatabaseEventTypes } from './events/index';
|
|
4
|
+
|
|
5
|
+
class Fake extends AbstractDatabaseService {
|
|
6
|
+
protected eventType = DatabaseEventTypes.RemoteQuery;
|
|
7
|
+
constructor(client: any) {
|
|
8
|
+
const noop = () => {};
|
|
9
|
+
const logger: any = { debug: noop, trace: noop, error: noop, info: noop, warn: noop, child: () => logger };
|
|
10
|
+
super(client, logger, createDatabaseEventSystem());
|
|
11
|
+
}
|
|
12
|
+
async connect() {}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('queryResponses', () => {
|
|
16
|
+
it('maps per-statement successes and failures without rejecting', async () => {
|
|
17
|
+
const client = {
|
|
18
|
+
query: () => ({
|
|
19
|
+
responses: async () => [
|
|
20
|
+
{ success: true, result: [1] },
|
|
21
|
+
{ success: false, error: new Error('denied') },
|
|
22
|
+
{ success: false, error: 'plain' },
|
|
23
|
+
],
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
const out = await new Fake(client).queryResponses('A; B; C');
|
|
27
|
+
expect(out).toEqual([
|
|
28
|
+
{ status: 'OK', result: [1] },
|
|
29
|
+
{ status: 'ERR', error: 'denied' },
|
|
30
|
+
{ status: 'ERR', error: 'plain' },
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
it('still rejects on a transport failure', async () => {
|
|
34
|
+
const client = {
|
|
35
|
+
query: () => ({
|
|
36
|
+
responses: async () => {
|
|
37
|
+
throw new Error('socket closed');
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
await expect(new Fake(client).queryResponses('A')).rejects.toThrow('socket closed');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -104,6 +104,30 @@ export abstract class AbstractDatabaseService {
|
|
|
104
104
|
* SDK's pending map.
|
|
105
105
|
*/
|
|
106
106
|
async query<T extends unknown[]>(query: string, vars?: Record<string, unknown>): Promise<T> {
|
|
107
|
+
return this.runStatement<T>(query, vars, () => this.client.query(query, vars) as unknown as Promise<T>);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Like {@link query} but never rejects on a statement error: every
|
|
112
|
+
* statement answers on its own as `{ status: 'OK', result }` or
|
|
113
|
+
* `{ status: 'ERR', error }`. This is what lets a batched outbox push judge
|
|
114
|
+
* each mutation separately.
|
|
115
|
+
*/
|
|
116
|
+
async queryResponses(
|
|
117
|
+
query: string,
|
|
118
|
+
vars?: Record<string, unknown>
|
|
119
|
+
): Promise<Array<{ status: 'OK'; result: unknown } | { status: 'ERR'; error: string }>> {
|
|
120
|
+
return this.runStatement(query, vars, async () => {
|
|
121
|
+
const responses = await this.client.query(query, vars).responses();
|
|
122
|
+
return responses.map((r) =>
|
|
123
|
+
r.success
|
|
124
|
+
? { status: 'OK' as const, result: r.result }
|
|
125
|
+
: { status: 'ERR' as const, error: r.error instanceof Error ? r.error.message : String(r.error) }
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private async runStatement<T>(query: string, vars: Record<string, unknown> | undefined, dispatch: () => Promise<T>): Promise<T> {
|
|
107
131
|
await this.acquireQuerySlot();
|
|
108
132
|
try {
|
|
109
133
|
await this.beforeQuery();
|
|
@@ -113,20 +137,12 @@ export abstract class AbstractDatabaseService {
|
|
|
113
137
|
{ query, vars, Category: 'sp00ky-client::Database::query' },
|
|
114
138
|
'Executing query'
|
|
115
139
|
);
|
|
116
|
-
const pending = this.client.query(query, vars);
|
|
117
|
-
// In SurrealDB 2.0, .query() collects results by default.
|
|
118
|
-
// We cast to T directly as proper typing depends on the caller knowing the return structure.
|
|
119
140
|
// "timed out" in the message is load-bearing: `classifySyncError`
|
|
120
141
|
// keys off it to classify this as `network` so the sync queues
|
|
121
142
|
// retry rather than rolling the mutation back.
|
|
122
|
-
const result =
|
|
123
|
-
pending as unknown as Promise<T>,
|
|
124
|
-
this.queryTimeoutMs,
|
|
125
|
-
() => this.timeoutError(query)
|
|
126
|
-
)) as T;
|
|
143
|
+
const result = await withTimeout(dispatch(), this.queryTimeoutMs, () => this.timeoutError(query));
|
|
127
144
|
const duration = performance.now() - startTime;
|
|
128
145
|
|
|
129
|
-
// Emit query event
|
|
130
146
|
this.events.emit(this.eventType, {
|
|
131
147
|
query,
|
|
132
148
|
vars,
|
|
@@ -143,7 +159,6 @@ export abstract class AbstractDatabaseService {
|
|
|
143
159
|
} catch (err) {
|
|
144
160
|
const duration = performance.now() - startTime;
|
|
145
161
|
|
|
146
|
-
// Emit query event with error
|
|
147
162
|
this.events.emit(this.eventType, {
|
|
148
163
|
query,
|
|
149
164
|
vars,
|
|
@@ -44,9 +44,9 @@ export class LocalMigrator {
|
|
|
44
44
|
|
|
45
45
|
const systemSchema = `
|
|
46
46
|
DEFINE TABLE IF NOT EXISTS _00_stream_processor_state SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
|
|
47
|
-
DEFINE TABLE IF NOT EXISTS
|
|
48
|
-
DEFINE TABLE IF NOT EXISTS _00_preload SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
|
|
47
|
+
DEFINE TABLE IF NOT EXISTS _00_view SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
|
|
49
48
|
DEFINE TABLE IF NOT EXISTS _00_window SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
|
|
49
|
+
DEFINE TABLE IF NOT EXISTS _00_failed_mutations SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
|
|
50
50
|
DEFINE TABLE IF NOT EXISTS _00_schema SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
|
|
51
51
|
DEFINE TABLE IF NOT EXISTS _00_pending_mutations SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
|
|
52
52
|
DEFINE TABLE IF NOT EXISTS _00_blob SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import type { QueryPlan } from '@spooky-sync/query-builder';
|
|
3
3
|
import { renderBaseSelectSurql, renderRelationFetchSurql } from './plan-render';
|
|
4
|
-
import { buildWindowMaterializationPlan } from '../../
|
|
4
|
+
import { buildWindowMaterializationPlan } from '../../query/window-query';
|
|
5
5
|
|
|
6
6
|
describe('renderBaseSelectSurql', () => {
|
|
7
7
|
it('renders projection, where, order, limit, offset', () => {
|
|
@@ -91,7 +91,8 @@ describe('SqliteCacheEngine system-table seeding', () => {
|
|
|
91
91
|
|
|
92
92
|
await engine.connect('user:fresh');
|
|
93
93
|
const open = msgs.find((m) => m.type === 'open');
|
|
94
|
-
expect(open?.payload?.systemTables).toContain('
|
|
94
|
+
expect(open?.payload?.systemTables).toContain('_00_view');
|
|
95
|
+
expect(open?.payload?.systemTables).toContain('_00_failed_mutations');
|
|
95
96
|
expect(open?.payload?.systemTables).toContain('_00_pending_mutations');
|
|
96
97
|
});
|
|
97
98
|
});
|
|
@@ -271,7 +272,7 @@ describe('SqliteCacheEngine role modes', () => {
|
|
|
271
272
|
return port;
|
|
272
273
|
}
|
|
273
274
|
|
|
274
|
-
it('adoptOwner opens under the worker lock
|
|
275
|
+
it('adoptOwner opens under the worker lock without wiping anything', async () => {
|
|
275
276
|
const { engine, log } = makeSharedEngine();
|
|
276
277
|
const health = await engine.adoptOwner('anon', {
|
|
277
278
|
workerLockName: 'sp00ky-tabs:fp:anon:worker:1',
|
|
@@ -281,7 +282,7 @@ describe('SqliteCacheEngine role modes', () => {
|
|
|
281
282
|
expect(health.status).toBe('persistent');
|
|
282
283
|
expect(engine.storageHealth.role).toBe('leader');
|
|
283
284
|
expect(log[0]).toBe('open');
|
|
284
|
-
expect(log).toContain('run'); //
|
|
285
|
+
expect(log).not.toContain('run'); // no local _00_query table any more, nothing to wipe
|
|
285
286
|
expect(engine.currentBucketId).toBe('anon');
|
|
286
287
|
});
|
|
287
288
|
|
|
@@ -60,9 +60,9 @@ const SYSTEM_TABLES = [
|
|
|
60
60
|
// In-browser circuit snapshot: one BLOB row (`circuit`) + a JSON meta row.
|
|
61
61
|
// Excluded from DevTools' table listing, since its data column is not JSON.
|
|
62
62
|
'_00_circuit_snapshot',
|
|
63
|
-
'
|
|
64
|
-
'_00_preload',
|
|
63
|
+
'_00_view',
|
|
65
64
|
'_00_window',
|
|
65
|
+
'_00_failed_mutations',
|
|
66
66
|
'_00_schema',
|
|
67
67
|
'_00_pending_mutations',
|
|
68
68
|
// Blob cache manifest. Read before its first write on every boot (reconcile
|
|
@@ -605,15 +605,6 @@ export class SqliteCacheEngine implements LocalStore {
|
|
|
605
605
|
disallowMemoryFallback: !opts.allowMemoryFallback,
|
|
606
606
|
},
|
|
607
607
|
});
|
|
608
|
-
// Leader wipe-on-pool-open: local `_00_query` rows from earlier sessions
|
|
609
|
-
// are dead (query hashes are session-salted) and this is the one moment
|
|
610
|
-
// no other tab is attached, so clearing here replaces the per-switch
|
|
611
|
-
// DELETE that solo mode does in doSwitchBucket. Followers never wipe.
|
|
612
|
-
try {
|
|
613
|
-
await this.rawCall('run', { sql: 'DELETE FROM "_00_query"' });
|
|
614
|
-
} catch {
|
|
615
|
-
/* fresh bucket: table just seeded, nothing to wipe */
|
|
616
|
-
}
|
|
617
608
|
getStats().roleChanges = (getStats().roleChanges ?? 0) + 1;
|
|
618
609
|
this.hadStore = true;
|
|
619
610
|
this.closeRoleGate();
|
|
@@ -122,7 +122,7 @@ describe('DevTools explorer against real SQLite', () => {
|
|
|
122
122
|
expect(Object.keys(info.tables)).toContain('game_insight');
|
|
123
123
|
// Seeded system tables show up too — the panel's "internal" toggle filters
|
|
124
124
|
// them, the engine must not pre-filter.
|
|
125
|
-
expect(Object.keys(info.tables)).toContain('
|
|
125
|
+
expect(Object.keys(info.tables)).toContain('_00_view');
|
|
126
126
|
expect(Object.keys(info.tables).some((t) => t.startsWith('sqlite_'))).toBe(false);
|
|
127
127
|
});
|
|
128
128
|
|
|
@@ -247,12 +247,16 @@ export type LeaderToFollowerMessage =
|
|
|
247
247
|
error: string;
|
|
248
248
|
}
|
|
249
249
|
/** The leader's drain pushed a mutation and deleted its outbox row from the
|
|
250
|
-
* SHARED store. Every follower
|
|
251
|
-
* was rendering as a pending write does not blink out before its
|
|
250
|
+
* SHARED store. Every follower keeps the row in its overlay until its
|
|
252
251
|
* `_00_list_ref` membership arrives. */
|
|
253
252
|
| {
|
|
254
253
|
type: 'mutation-settled';
|
|
255
254
|
mutationId: string;
|
|
256
255
|
recordId: string;
|
|
257
256
|
eventType: 'create' | 'update' | 'delete';
|
|
258
|
-
}
|
|
257
|
+
}
|
|
258
|
+
/** The leader's LIVE saw edges of these queries change; followers re-read
|
|
259
|
+
* membership for the ones they hold. */
|
|
260
|
+
| { type: 'membership-dirty'; hashes: string[] }
|
|
261
|
+
/** The failed-writes tray changed size on the shared store. */
|
|
262
|
+
| { type: 'failed-mutations-changed'; count: number };
|