@spooky-sync/core 0.0.1-canary.38 → 0.0.1-canary.381
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 +91 -0
- package/README.md +59 -16
- package/dist/index.d.ts +2238 -144
- package/dist/index.js +18149 -2475
- package/dist/live.d.ts +149 -0
- package/dist/live.js +739 -0
- package/dist/otel/index.d.ts +1 -1
- package/dist/pure.d.ts +401 -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 +2082 -39
- package/dist/worker-edges.js +498 -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 +42 -8
- package/skills/sp00ky-core/references/config.md +8 -2
- package/src/boot/auth-flip.saga.test.ts +174 -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 +191 -0
- package/src/boot/bucket-switch.saga.ts +151 -0
- package/src/boot/preload.saga.test.ts +96 -0
- package/src/boot/preload.saga.ts +45 -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 +59 -0
- package/src/client/router.ts +104 -0
- package/src/client/runtime.test.ts +319 -0
- package/src/client/runtime.ts +307 -0
- package/src/client/services.test.ts +74 -0
- package/src/client/services.ts +437 -0
- package/src/client/sp00ky-client.test.ts +428 -0
- package/src/client/sp00ky-client.ts +737 -0
- package/src/events/index.ts +3 -0
- package/src/index.ts +86 -1
- package/src/kernel/constants.test.ts +19 -0
- package/src/kernel/constants.ts +94 -0
- package/src/kernel/effects.test.ts +40 -0
- package/src/kernel/effects.ts +171 -0
- package/src/kernel/events.ts +60 -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/auth.session-recheck.test.ts +140 -0
- package/src/modules/auth/events/index.ts +7 -1
- package/src/modules/auth/impersonation-banner.ts +443 -0
- package/src/modules/auth/impersonation.integration.test.ts +209 -0
- package/src/modules/auth/impersonation.test.ts +377 -0
- package/src/modules/auth/impersonation.ts +179 -0
- package/src/modules/auth/index.ts +538 -14
- package/src/modules/channel/channel.test.ts +752 -0
- package/src/modules/channel/index.ts +1153 -0
- package/src/modules/channel/webrtc.ts +472 -0
- 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 +546 -0
- package/src/modules/crdt/hash.ts +103 -0
- package/src/modules/crdt/index.ts +821 -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 -93
- 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/ingest-outage.integration.test.ts +400 -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 +361 -0
- package/src/mutation/push.saga.ts +245 -0
- package/src/mutation/rows.test.ts +278 -0
- package/src/mutation/rows.ts +515 -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 +436 -0
- package/src/mutation/write.saga.ts +384 -0
- package/src/pure.ts +66 -0
- package/src/push/types.ts +174 -0
- package/src/query/anon-query-isolation.integration.test.ts +531 -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 +200 -0
- package/src/query/materialize.saga.ts +102 -0
- package/src/query/materialize.test.ts +58 -0
- package/src/query/materialize.ts +57 -0
- package/src/query/membership.saga.test.ts +514 -0
- package/src/query/membership.saga.ts +339 -0
- package/src/query/membership.test.ts +151 -0
- package/src/query/membership.ts +234 -0
- package/src/query/register.saga.test.ts +369 -0
- package/src/query/register.saga.ts +293 -0
- package/src/query/render-set.test.ts +47 -0
- package/src/query/render-set.ts +71 -0
- package/src/query/session-guard.integration.test.ts +304 -0
- package/src/query/sql.test.ts +118 -0
- package/src/query/sql.ts +174 -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 +151 -56
- package/src/services/database/engine-factory.ts +33 -0
- package/src/services/database/errors.ts +34 -0
- 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 +54 -9
- package/src/services/database/local.test.ts +150 -0
- package/src/services/database/local.ts +503 -59
- 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 +283 -4
- 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 +187 -6
- package/src/services/persistence/resilient.ts +8 -1
- package/src/services/persistence/surrealdb.ts +2 -2
- package/src/services/stream-processor/index.ts +801 -63
- 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/wasm-types.ts +57 -1
- 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 -394
- package/src/state/client-state.ts +231 -0
- package/src/state/lifecycle.test.ts +184 -0
- package/src/state/lifecycle.ts +127 -0
- package/src/state/reducers.test.ts +337 -0
- package/src/state/reducers.ts +571 -0
- package/src/state/selectors.test.ts +203 -0
- package/src/state/selectors.ts +264 -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 +229 -0
- package/src/sync/live.saga.ts +116 -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 +694 -7
- package/src/utils/blurhash.ts +90 -0
- package/src/utils/edges.ts +57 -0
- package/src/utils/error-classification.test.ts +116 -0
- package/src/utils/error-classification.ts +74 -0
- package/src/utils/index.ts +83 -6
- package/src/utils/parser.test.ts +67 -102
- package/src/utils/parser.ts +63 -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 +22 -11
- 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 -882
- package/src/modules/sync/engine.ts +0 -173
- package/src/modules/sync/events/index.ts +0 -78
- 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 -90
- package/src/modules/sync/queue/queue-up.ts +0 -224
- package/src/modules/sync/scheduler.ts +0 -84
- package/src/modules/sync/sync.ts +0 -422
- package/src/modules/sync/utils.test.ts +0 -311
- package/src/modules/sync/utils.ts +0 -169
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Logger} from '../logger/index';
|
|
2
2
|
import { createLogger } from '../logger/index';
|
|
3
|
-
import type {
|
|
3
|
+
import type { LocalStore } from './cache-engine';
|
|
4
4
|
|
|
5
5
|
export interface SchemaRecord {
|
|
6
6
|
hash: string;
|
|
@@ -15,11 +15,19 @@ export const sha1 = async (str: string): Promise<string> => {
|
|
|
15
15
|
.join('');
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Local-only state a schema change must not throw away: the outbox, the
|
|
20
|
+
* failed-mutation tray, debounced patches not yet in the outbox and CRDT
|
|
21
|
+
* documents with edits not yet sent. Cached rows refill from the server;
|
|
22
|
+
* these exist nowhere else.
|
|
23
|
+
*/
|
|
24
|
+
const KEPT_TABLES = ['_00_pending_mutations', '_00_failed_mutations', '_00_pending_writes', '_00_crdt_local'] as const;
|
|
25
|
+
|
|
18
26
|
export class LocalMigrator {
|
|
19
27
|
private logger: Logger;
|
|
20
28
|
|
|
21
29
|
constructor(
|
|
22
|
-
private localDb:
|
|
30
|
+
private localDb: LocalStore,
|
|
23
31
|
logger: Logger
|
|
24
32
|
) {
|
|
25
33
|
this.logger = logger.child({ service: 'LocalMigrator' });
|
|
@@ -40,13 +48,19 @@ export class LocalMigrator {
|
|
|
40
48
|
return;
|
|
41
49
|
}
|
|
42
50
|
|
|
51
|
+
const kept = await this.readKeptRows();
|
|
43
52
|
await this.recreateDatabase(database);
|
|
44
53
|
|
|
45
54
|
const systemSchema = `
|
|
46
55
|
DEFINE TABLE IF NOT EXISTS _00_stream_processor_state SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
|
|
47
|
-
DEFINE TABLE IF NOT EXISTS
|
|
56
|
+
DEFINE TABLE IF NOT EXISTS _00_view SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
|
|
57
|
+
DEFINE TABLE IF NOT EXISTS _00_window SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
|
|
58
|
+
DEFINE TABLE IF NOT EXISTS _00_failed_mutations SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
|
|
48
59
|
DEFINE TABLE IF NOT EXISTS _00_schema SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
|
|
49
60
|
DEFINE TABLE IF NOT EXISTS _00_pending_mutations SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
|
|
61
|
+
DEFINE TABLE IF NOT EXISTS _00_pending_writes SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
|
|
62
|
+
DEFINE TABLE IF NOT EXISTS _00_blob SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
|
|
63
|
+
DEFINE TABLE IF NOT EXISTS _00_crdt_local SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
|
|
50
64
|
`;
|
|
51
65
|
const fullSchema = schemaSurql + '\n' + systemSchema;
|
|
52
66
|
|
|
@@ -71,7 +85,7 @@ export class LocalMigrator {
|
|
|
71
85
|
{ Category: 'sp00ky-client::LocalMigrator::provision' },
|
|
72
86
|
`[Provisioning] (${i + 1}/${statements.length}) Executing: ${statement.substring(0, 50)}...`
|
|
73
87
|
);
|
|
74
|
-
await this.localDb.
|
|
88
|
+
await this.localDb.queryUngated(statement);
|
|
75
89
|
this.logger.info(
|
|
76
90
|
{ Category: 'sp00ky-client::LocalMigrator::provision' },
|
|
77
91
|
`[Provisioning] (${i + 1}/${statements.length}) Done`
|
|
@@ -85,12 +99,43 @@ export class LocalMigrator {
|
|
|
85
99
|
}
|
|
86
100
|
}
|
|
87
101
|
|
|
102
|
+
await this.restoreKeptRows(kept);
|
|
88
103
|
await this.createHashRecord(hash);
|
|
89
104
|
}
|
|
90
105
|
|
|
106
|
+
private async readKeptRows(): Promise<Array<[string, unknown[]]>> {
|
|
107
|
+
const kept: Array<[string, unknown[]]> = [];
|
|
108
|
+
for (const table of KEPT_TABLES) {
|
|
109
|
+
try {
|
|
110
|
+
const [rows] = await this.localDb.queryUngated<[unknown[]]>(`SELECT * FROM ${table}`);
|
|
111
|
+
if (Array.isArray(rows) && rows.length > 0) kept.push([table, rows]);
|
|
112
|
+
} catch {
|
|
113
|
+
// A fresh store has no such table yet: nothing to keep.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return kept;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private async restoreKeptRows(kept: Array<[string, unknown[]]>): Promise<void> {
|
|
120
|
+
for (const [table, rows] of kept) {
|
|
121
|
+
try {
|
|
122
|
+
await this.localDb.queryUngated(`INSERT INTO ${table} $rows`, { rows });
|
|
123
|
+
this.logger.info(
|
|
124
|
+
{ table, count: rows.length, Category: 'sp00ky-client::LocalMigrator::provision' },
|
|
125
|
+
'[Provisioning] Kept local rows across the schema change'
|
|
126
|
+
);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
this.logger.error(
|
|
129
|
+
{ table, count: rows.length, error, Category: 'sp00ky-client::LocalMigrator::provision' },
|
|
130
|
+
'[Provisioning] Could not restore local rows after the schema change; they are lost'
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
91
136
|
private async isSchemaUpToDate(hash: string): Promise<boolean> {
|
|
92
137
|
try {
|
|
93
|
-
const [lastSchemaRecord] = await this.localDb.
|
|
138
|
+
const [lastSchemaRecord] = await this.localDb.queryUngated<any>(
|
|
94
139
|
`SELECT hash, created_at FROM ONLY _00_schema ORDER BY created_at DESC LIMIT 1;`
|
|
95
140
|
);
|
|
96
141
|
return lastSchemaRecord?.hash === hash;
|
|
@@ -101,13 +146,13 @@ export class LocalMigrator {
|
|
|
101
146
|
|
|
102
147
|
private async recreateDatabase(database: string) {
|
|
103
148
|
try {
|
|
104
|
-
await this.localDb.
|
|
149
|
+
await this.localDb.queryUngated(`DEFINE DATABASE _00_temp;`);
|
|
105
150
|
} catch (_e) {
|
|
106
151
|
// Ignore if exists
|
|
107
152
|
}
|
|
108
153
|
|
|
109
154
|
try {
|
|
110
|
-
await this.localDb.
|
|
155
|
+
await this.localDb.queryUngated(`
|
|
111
156
|
USE DB _00_temp;
|
|
112
157
|
REMOVE DATABASE ${database};
|
|
113
158
|
`);
|
|
@@ -115,7 +160,7 @@ export class LocalMigrator {
|
|
|
115
160
|
// Ignore error if database doesn't exist
|
|
116
161
|
}
|
|
117
162
|
|
|
118
|
-
await this.localDb.
|
|
163
|
+
await this.localDb.queryUngated(`
|
|
119
164
|
DEFINE DATABASE ${database};
|
|
120
165
|
USE DB ${database};
|
|
121
166
|
`);
|
|
@@ -195,7 +240,7 @@ export class LocalMigrator {
|
|
|
195
240
|
}
|
|
196
241
|
|
|
197
242
|
private async createHashRecord(hash: string) {
|
|
198
|
-
await this.localDb.
|
|
243
|
+
await this.localDb.queryUngated(
|
|
199
244
|
`UPSERT _00_schema SET hash = $hash, created_at = time::now() WHERE hash = $hash;`,
|
|
200
245
|
{ hash }
|
|
201
246
|
);
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { isLocalStoreOpenError } from './local';
|
|
3
|
+
|
|
4
|
+
// `LocalDatabaseService.connect` recovers (drops the store + reconnects, falling
|
|
5
|
+
// back to in-memory) only when the failure is the SurrealDB-WASM IndexedDB
|
|
6
|
+
// open/transaction error — NOT for unrelated errors. This pins the message match
|
|
7
|
+
// against the real error the engine throws so a recovery path doesn't silently
|
|
8
|
+
// stop triggering (or start swallowing unrelated failures).
|
|
9
|
+
describe('isLocalStoreOpenError', () => {
|
|
10
|
+
it('matches the real SurrealDB-WASM IndexedDB key-value store error', () => {
|
|
11
|
+
const real = new Error(
|
|
12
|
+
'There was a problem with the key-value store: There was a problem with a ' +
|
|
13
|
+
'transaction: An IndexedDB error occured: idb error'
|
|
14
|
+
);
|
|
15
|
+
expect(isLocalStoreOpenError(real)).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('matches on the individual signals (indexeddb / idb error / key-value store)', () => {
|
|
19
|
+
expect(isLocalStoreOpenError(new Error('IndexedDB is not available'))).toBe(true);
|
|
20
|
+
expect(isLocalStoreOpenError(new Error('idb error'))).toBe(true);
|
|
21
|
+
expect(isLocalStoreOpenError(new Error('problem with the key-value store'))).toBe(true);
|
|
22
|
+
// Non-Error inputs are stringified.
|
|
23
|
+
expect(isLocalStoreOpenError('An IndexedDB error occured')).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('does NOT match unrelated errors (so we never clear the store spuriously)', () => {
|
|
27
|
+
expect(isLocalStoreOpenError(new Error('WebSocket connection refused'))).toBe(false);
|
|
28
|
+
expect(isLocalStoreOpenError(new Error('Parse error: unexpected token'))).toBe(false);
|
|
29
|
+
expect(isLocalStoreOpenError(new Error('permission denied'))).toBe(false);
|
|
30
|
+
expect(isLocalStoreOpenError(null)).toBe(false);
|
|
31
|
+
expect(isLocalStoreOpenError(undefined)).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Per-user local buckets: URL/name derivation and the SCOPE of the tier-2
|
|
36
|
+
// corruption drop. The drop must only ever match the failing bucket's store —
|
|
37
|
+
// a substring wipe would take every user's cache AND their un-pushed mutation
|
|
38
|
+
// outboxes down with one corrupt store.
|
|
39
|
+
import { bucketStoreName, bucketStoreUrl, matchesBucketStore } from './local';
|
|
40
|
+
|
|
41
|
+
describe('bucket store naming', () => {
|
|
42
|
+
it('derives the store url/name from the bucket id', () => {
|
|
43
|
+
expect(bucketStoreUrl('abc')).toBe('indxdb://sp00ky-abc');
|
|
44
|
+
expect(bucketStoreName('abc')).toBe('sp00ky-abc');
|
|
45
|
+
expect(bucketStoreUrl('anon')).toBe('indxdb://sp00ky-anon');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('matchesBucketStore', () => {
|
|
50
|
+
it('matches the exact store and derived names', () => {
|
|
51
|
+
expect(matchesBucketStore('sp00ky-abc', 'sp00ky-abc')).toBe(true);
|
|
52
|
+
expect(matchesBucketStore('sp00ky-abc-wal', 'sp00ky-abc')).toBe(true);
|
|
53
|
+
expect(matchesBucketStore('surrealdb/sp00ky-abc', 'sp00ky-abc')).toBe(true);
|
|
54
|
+
expect(matchesBucketStore('SP00KY-ABC', 'sp00ky-abc')).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("never matches another bucket's store", () => {
|
|
58
|
+
expect(matchesBucketStore('sp00ky-abcdef', 'sp00ky-abc')).toBe(false);
|
|
59
|
+
expect(matchesBucketStore('sp00ky-anon', 'sp00ky-abc')).toBe(false);
|
|
60
|
+
expect(matchesBucketStore('sp00ky', 'sp00ky-abc')).toBe(false);
|
|
61
|
+
// The legacy shared store is not any bucket's store.
|
|
62
|
+
expect(matchesBucketStore('sp00ky', 'sp00ky-anon')).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// A bucket switch that lands while the boot open is still in flight (a reload
|
|
67
|
+
// of a signed-in tab, with IndexedDB slow to open) must wait for it. Run
|
|
68
|
+
// concurrently, the switch closed the client `connect` was opening and both
|
|
69
|
+
// provisioned a store at once, so boot never finished.
|
|
70
|
+
import { vi } from 'vitest';
|
|
71
|
+
import { Surreal } from 'surrealdb';
|
|
72
|
+
import { LocalDatabaseService } from './local';
|
|
73
|
+
|
|
74
|
+
vi.mock('@surrealdb/wasm', () => ({ createWasmWorkerEngines: () => ({}) }));
|
|
75
|
+
|
|
76
|
+
describe('LocalDatabaseService open and switch', () => {
|
|
77
|
+
it('a switch waits for the in-flight connect instead of closing the client it opens', async () => {
|
|
78
|
+
const opens: Array<{ url: string; done: () => void }> = [];
|
|
79
|
+
const closed: string[] = [];
|
|
80
|
+
const urlOf = new WeakMap<object, string>();
|
|
81
|
+
vi.spyOn(Surreal.prototype, 'connect').mockImplementation(function (this: Surreal, url: string | URL) {
|
|
82
|
+
urlOf.set(this, String(url));
|
|
83
|
+
return new Promise<true>((resolve) => opens.push({ url: String(url), done: () => resolve(true) }));
|
|
84
|
+
});
|
|
85
|
+
vi.spyOn(Surreal.prototype, 'use').mockResolvedValue({});
|
|
86
|
+
vi.spyOn(Surreal.prototype, 'close').mockImplementation(async function (this: Surreal) {
|
|
87
|
+
closed.push(urlOf.get(this) ?? 'placeholder');
|
|
88
|
+
return true as const;
|
|
89
|
+
});
|
|
90
|
+
const logger: any = { trace: vi.fn(), debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
91
|
+
logger.child = () => logger;
|
|
92
|
+
const local = new LocalDatabaseService({ namespace: 'n', database: 'd', store: 'indexeddb' } as any, logger);
|
|
93
|
+
|
|
94
|
+
const booting = local.connect('u1');
|
|
95
|
+
await vi.waitFor(() => expect(opens).toHaveLength(1));
|
|
96
|
+
const switching = local.switchStore('anon');
|
|
97
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
98
|
+
expect(opens.map((o) => o.url)).toEqual(['indxdb://sp00ky-u1']);
|
|
99
|
+
expect(closed).toEqual([]);
|
|
100
|
+
|
|
101
|
+
opens[0].done();
|
|
102
|
+
await booting;
|
|
103
|
+
await vi.waitFor(() => expect(opens).toHaveLength(2));
|
|
104
|
+
expect(opens[1].url).toBe('indxdb://sp00ky-anon');
|
|
105
|
+
opens[1].done();
|
|
106
|
+
await switching;
|
|
107
|
+
expect(closed).toEqual(['indxdb://sp00ky-u1']);
|
|
108
|
+
expect(local.currentBucketId).toBe('anon');
|
|
109
|
+
vi.restoreAllMocks();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// The engine streams a statement's result from its worker; closing the
|
|
113
|
+
// client under it left the statement pending until its 30 s deadline, and
|
|
114
|
+
// every later local statement queued behind it (sign-in hung on "Loading").
|
|
115
|
+
it.each(['memory', 'indexeddb'] as const)('%s: a switch lets a statement in flight finish before it closes the client', async (store) => {
|
|
116
|
+
let answer!: (rows: unknown[]) => void;
|
|
117
|
+
const events: string[] = [];
|
|
118
|
+
vi.spyOn(Surreal.prototype, 'connect').mockResolvedValue(true);
|
|
119
|
+
vi.spyOn(Surreal.prototype, 'use').mockResolvedValue({});
|
|
120
|
+
vi.spyOn(Surreal.prototype, 'query').mockImplementation((() =>
|
|
121
|
+
new Promise((r) => {
|
|
122
|
+
answer = (rows) => {
|
|
123
|
+
events.push('answered');
|
|
124
|
+
r(rows);
|
|
125
|
+
};
|
|
126
|
+
})) as any);
|
|
127
|
+
vi.spyOn(Surreal.prototype, 'close').mockImplementation(async () => {
|
|
128
|
+
events.push('close');
|
|
129
|
+
return true as const;
|
|
130
|
+
});
|
|
131
|
+
const logger: any = { trace: vi.fn(), debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
132
|
+
logger.child = () => logger;
|
|
133
|
+
const local = new LocalDatabaseService({ namespace: 'n', database: 'd', store } as any, logger);
|
|
134
|
+
await local.connect('anon');
|
|
135
|
+
|
|
136
|
+
const reading = local.query('SELECT * FROM thing');
|
|
137
|
+
await vi.waitFor(() => expect(answer).toBeDefined());
|
|
138
|
+
const release = local.beginSwitch();
|
|
139
|
+
const switching = local.switchStore('u1');
|
|
140
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
141
|
+
expect(events).toEqual([]);
|
|
142
|
+
|
|
143
|
+
answer([[]]);
|
|
144
|
+
await reading;
|
|
145
|
+
await switching;
|
|
146
|
+
release();
|
|
147
|
+
expect(events).toEqual(['answered', 'close']);
|
|
148
|
+
vi.restoreAllMocks();
|
|
149
|
+
});
|
|
150
|
+
});
|