@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.
Files changed (266) hide show
  1. package/AGENTS.md +91 -0
  2. package/README.md +59 -16
  3. package/dist/index.d.ts +2238 -144
  4. package/dist/index.js +18149 -2475
  5. package/dist/live.d.ts +149 -0
  6. package/dist/live.js +739 -0
  7. package/dist/otel/index.d.ts +1 -1
  8. package/dist/pure.d.ts +401 -0
  9. package/dist/pure.js +24 -0
  10. package/dist/sqlite-open.js +392 -0
  11. package/dist/sqlite-worker.d.ts +1 -0
  12. package/dist/sqlite-worker.js +439 -0
  13. package/dist/sw.d.ts +207 -0
  14. package/dist/sw.js +393 -0
  15. package/dist/tabs-broker-worker.d.ts +8 -0
  16. package/dist/tabs-broker-worker.js +477 -0
  17. package/dist/types.d.ts +2082 -39
  18. package/dist/worker-edges.js +498 -0
  19. package/dist/worker-impersonation.js +60 -0
  20. package/dist/worker-ref-tables.d.ts +48 -0
  21. package/dist/worker-types.d.ts +112 -0
  22. package/dist/worker-types.js +73 -0
  23. package/package.json +28 -7
  24. package/scripts/check-broker-bundle.mjs +33 -0
  25. package/scripts/check-worker-entries.mjs +173 -0
  26. package/skills/sp00ky-core/SKILL.md +42 -8
  27. package/skills/sp00ky-core/references/config.md +8 -2
  28. package/src/boot/auth-flip.saga.test.ts +174 -0
  29. package/src/boot/auth-flip.saga.ts +77 -0
  30. package/src/boot/boot.saga.test.ts +203 -0
  31. package/src/boot/boot.saga.ts +146 -0
  32. package/src/boot/bucket-switch.saga.test.ts +191 -0
  33. package/src/boot/bucket-switch.saga.ts +151 -0
  34. package/src/boot/preload.saga.test.ts +96 -0
  35. package/src/boot/preload.saga.ts +45 -0
  36. package/src/bucket-blurhash.test.ts +148 -0
  37. package/src/build-globals.d.ts +12 -0
  38. package/src/client/bucket-handle.test.ts +195 -0
  39. package/src/client/bucket-handle.ts +352 -0
  40. package/src/client/router.test.ts +59 -0
  41. package/src/client/router.ts +104 -0
  42. package/src/client/runtime.test.ts +319 -0
  43. package/src/client/runtime.ts +307 -0
  44. package/src/client/services.test.ts +74 -0
  45. package/src/client/services.ts +437 -0
  46. package/src/client/sp00ky-client.test.ts +428 -0
  47. package/src/client/sp00ky-client.ts +737 -0
  48. package/src/events/index.ts +3 -0
  49. package/src/index.ts +86 -1
  50. package/src/kernel/constants.test.ts +19 -0
  51. package/src/kernel/constants.ts +94 -0
  52. package/src/kernel/effects.test.ts +40 -0
  53. package/src/kernel/effects.ts +171 -0
  54. package/src/kernel/events.ts +60 -0
  55. package/src/kernel/interpreter.test.ts +116 -0
  56. package/src/kernel/interpreter.ts +148 -0
  57. package/src/kernel/saga.test.ts +89 -0
  58. package/src/kernel/saga.ts +73 -0
  59. package/src/live/index.test.ts +512 -0
  60. package/src/live/index.ts +1059 -0
  61. package/src/modules/app-release/index.test.ts +125 -0
  62. package/src/modules/app-release/index.ts +209 -0
  63. package/src/modules/auth/auth.local-first.test.ts +228 -0
  64. package/src/modules/auth/auth.session-recheck.test.ts +140 -0
  65. package/src/modules/auth/events/index.ts +7 -1
  66. package/src/modules/auth/impersonation-banner.ts +443 -0
  67. package/src/modules/auth/impersonation.integration.test.ts +209 -0
  68. package/src/modules/auth/impersonation.test.ts +377 -0
  69. package/src/modules/auth/impersonation.ts +179 -0
  70. package/src/modules/auth/index.ts +538 -14
  71. package/src/modules/channel/channel.test.ts +752 -0
  72. package/src/modules/channel/index.ts +1153 -0
  73. package/src/modules/channel/webrtc.ts +472 -0
  74. package/src/modules/crdt/acceptance.ts +75 -0
  75. package/src/modules/crdt/bytes.ts +50 -0
  76. package/src/modules/crdt/crdt-field.ts +125 -0
  77. package/src/modules/crdt/crdt.test.ts +546 -0
  78. package/src/modules/crdt/hash.ts +103 -0
  79. package/src/modules/crdt/index.ts +821 -0
  80. package/src/modules/crdt/loro-loader.ts +25 -0
  81. package/src/modules/crdt/mirror.ts +37 -0
  82. package/src/modules/devtools/flags.ts +349 -0
  83. package/src/modules/devtools/index.ts +746 -93
  84. package/src/modules/devtools/mutations.test.ts +92 -0
  85. package/src/modules/devtools/mutations.ts +160 -0
  86. package/src/modules/devtools/notify-throttle.test.ts +166 -0
  87. package/src/modules/devtools/state-shape.test.ts +263 -0
  88. package/src/modules/devtools/storage-info.test.ts +79 -0
  89. package/src/modules/devtools/storage-info.ts +168 -0
  90. package/src/modules/devtools/versions.test.ts +74 -0
  91. package/src/modules/devtools/versions.ts +110 -0
  92. package/src/modules/feature-flag/index.test.ts +298 -0
  93. package/src/modules/feature-flag/index.ts +316 -0
  94. package/src/modules/ref-tables.test.ts +93 -0
  95. package/src/modules/ref-tables.ts +91 -0
  96. package/src/modules/web-push/index.test.ts +824 -0
  97. package/src/modules/web-push/index.ts +1027 -0
  98. package/src/mutation/ingest-outage.integration.test.ts +400 -0
  99. package/src/mutation/jobs.test.ts +25 -0
  100. package/src/mutation/jobs.ts +41 -0
  101. package/src/mutation/mutation-id.test.ts +37 -0
  102. package/src/mutation/mutation-id.ts +35 -0
  103. package/src/mutation/pending-writes.integration.test.ts +175 -0
  104. package/src/mutation/push.saga.test.ts +361 -0
  105. package/src/mutation/push.saga.ts +245 -0
  106. package/src/mutation/rows.test.ts +278 -0
  107. package/src/mutation/rows.ts +515 -0
  108. package/src/mutation/tray.saga.test.ts +63 -0
  109. package/src/mutation/tray.saga.ts +48 -0
  110. package/src/mutation/write.saga.test.ts +436 -0
  111. package/src/mutation/write.saga.ts +384 -0
  112. package/src/pure.ts +66 -0
  113. package/src/push/types.ts +174 -0
  114. package/src/query/anon-query-isolation.integration.test.ts +531 -0
  115. package/src/query/env.test.ts +23 -0
  116. package/src/query/env.ts +65 -0
  117. package/src/query/fetch.saga.test.ts +199 -0
  118. package/src/query/fetch.saga.ts +120 -0
  119. package/src/query/hash.test.ts +14 -0
  120. package/src/query/hash.ts +18 -0
  121. package/src/query/id-set-plan.test.ts +132 -0
  122. package/src/query/lifecycle.saga.test.ts +274 -0
  123. package/src/query/lifecycle.saga.ts +186 -0
  124. package/src/query/list-ref-live.integration.test.ts +203 -0
  125. package/src/query/materialize.saga.test.ts +200 -0
  126. package/src/query/materialize.saga.ts +102 -0
  127. package/src/query/materialize.test.ts +58 -0
  128. package/src/query/materialize.ts +57 -0
  129. package/src/query/membership.saga.test.ts +514 -0
  130. package/src/query/membership.saga.ts +339 -0
  131. package/src/query/membership.test.ts +151 -0
  132. package/src/query/membership.ts +234 -0
  133. package/src/query/register.saga.test.ts +369 -0
  134. package/src/query/register.saga.ts +293 -0
  135. package/src/query/render-set.test.ts +47 -0
  136. package/src/query/render-set.ts +71 -0
  137. package/src/query/session-guard.integration.test.ts +304 -0
  138. package/src/query/sql.test.ts +118 -0
  139. package/src/query/sql.ts +174 -0
  140. package/src/query/window-query.test.ts +70 -0
  141. package/src/query/window-query.ts +208 -0
  142. package/src/services/blobs/blob-cache.test.ts +359 -0
  143. package/src/services/blobs/blob-cache.ts +603 -0
  144. package/src/services/blobs/blob-manifest.ts +227 -0
  145. package/src/services/blobs/blob-store.test.ts +77 -0
  146. package/src/services/blobs/blob-store.ts +359 -0
  147. package/src/services/blobs/blob.fixture.ts +90 -0
  148. package/src/services/blobs/index.ts +70 -0
  149. package/src/services/database/cache-engine.ts +209 -0
  150. package/src/services/database/connection-supervisor.test.ts +318 -0
  151. package/src/services/database/connection-supervisor.ts +422 -0
  152. package/src/services/database/database.query-timeout.test.ts +140 -0
  153. package/src/services/database/database.responses.test.ts +43 -0
  154. package/src/services/database/database.ts +151 -56
  155. package/src/services/database/engine-factory.ts +33 -0
  156. package/src/services/database/errors.ts +34 -0
  157. package/src/services/database/index.ts +7 -0
  158. package/src/services/database/landed-body-clear.integration.test.ts +236 -0
  159. package/src/services/database/local-migrator.integration.test.ts +110 -0
  160. package/src/services/database/local-migrator.ts +54 -9
  161. package/src/services/database/local.test.ts +150 -0
  162. package/src/services/database/local.ts +503 -59
  163. package/src/services/database/plan-render.test.ts +159 -0
  164. package/src/services/database/plan-render.ts +108 -0
  165. package/src/services/database/relation-resolver.test.ts +484 -0
  166. package/src/services/database/relation-resolver.ts +0 -0
  167. package/src/services/database/remote.ts +283 -4
  168. package/src/services/database/sqlite-cache-engine.test.ts +628 -0
  169. package/src/services/database/sqlite-cache-engine.timeout.test.ts +61 -0
  170. package/src/services/database/sqlite-cache-engine.ts +1401 -0
  171. package/src/services/database/sqlite-column-reset.integration.test.ts +100 -0
  172. package/src/services/database/sqlite-column-reset.ts +91 -0
  173. package/src/services/database/sqlite-devtools-queries.integration.test.ts +143 -0
  174. package/src/services/database/sqlite-devtools-queries.test.ts +154 -0
  175. package/src/services/database/sqlite-lock-verify.test.ts +33 -0
  176. package/src/services/database/sqlite-lock-verify.ts +45 -0
  177. package/src/services/database/sqlite-open.test.ts +150 -0
  178. package/src/services/database/sqlite-open.ts +164 -0
  179. package/src/services/database/sqlite-plan-sql.test.ts +164 -0
  180. package/src/services/database/sqlite-plan-sql.ts +196 -0
  181. package/src/services/database/sqlite-projection.test.ts +121 -0
  182. package/src/services/database/sqlite-select.integration.test.ts +185 -0
  183. package/src/services/database/sqlite-select.test.ts +261 -0
  184. package/src/services/database/sqlite-select.ts +131 -0
  185. package/src/services/database/sqlite-transport.fixture.ts +30 -0
  186. package/src/services/database/sqlite-transport.ts +224 -0
  187. package/src/services/database/sqlite-worker.ts +437 -0
  188. package/src/services/database/surql-translate.ts +417 -0
  189. package/src/services/database/surreal-cache-engine.ts +163 -0
  190. package/src/services/logger/index.test.ts +132 -0
  191. package/src/services/logger/index.ts +187 -6
  192. package/src/services/persistence/resilient.ts +8 -1
  193. package/src/services/persistence/surrealdb.ts +2 -2
  194. package/src/services/stream-processor/index.ts +801 -63
  195. package/src/services/stream-processor/permissions.test.ts +47 -0
  196. package/src/services/stream-processor/permissions.ts +53 -0
  197. package/src/services/stream-processor/stream-processor.batch.test.ts +186 -0
  198. package/src/services/stream-processor/stream-processor.prime.test.ts +234 -0
  199. package/src/services/stream-processor/stream-processor.reset.test.ts +226 -0
  200. package/src/services/stream-processor/wasm-types.ts +57 -1
  201. package/src/services/tabs/broker-client.ts +284 -0
  202. package/src/services/tabs/broker.test.ts +381 -0
  203. package/src/services/tabs/coordinator.test.ts +386 -0
  204. package/src/services/tabs/coordinator.ts +646 -0
  205. package/src/services/tabs/fake-ports.fixture.ts +112 -0
  206. package/src/services/tabs/leader-locks.ts +75 -0
  207. package/src/services/tabs/protocol.ts +262 -0
  208. package/src/services/tabs/support.ts +36 -0
  209. package/src/services/tabs/tabs-broker-worker.ts +652 -0
  210. package/src/sp00ky.ts +5 -394
  211. package/src/state/client-state.ts +231 -0
  212. package/src/state/lifecycle.test.ts +184 -0
  213. package/src/state/lifecycle.ts +127 -0
  214. package/src/state/reducers.test.ts +337 -0
  215. package/src/state/reducers.ts +571 -0
  216. package/src/state/selectors.test.ts +203 -0
  217. package/src/state/selectors.ts +264 -0
  218. package/src/sw/index.test.ts +582 -0
  219. package/src/sw/index.ts +607 -0
  220. package/src/sw/store.ts +96 -0
  221. package/src/sync/connection.saga.test.ts +97 -0
  222. package/src/sync/connection.saga.ts +89 -0
  223. package/src/sync/live.saga.test.ts +229 -0
  224. package/src/sync/live.saga.ts +116 -0
  225. package/src/sync/policy.health.test.ts +56 -0
  226. package/src/sync/policy.test.ts +663 -0
  227. package/src/sync/policy.ts +515 -0
  228. package/src/sync/poll.saga.test.ts +51 -0
  229. package/src/sync/poll.saga.ts +36 -0
  230. package/src/sync/tabs.saga.test.ts +78 -0
  231. package/src/sync/tabs.saga.ts +96 -0
  232. package/src/testing/adapters.ts +137 -0
  233. package/src/testing/build.ts +63 -0
  234. package/src/testing/fake-services.ts +63 -0
  235. package/src/testing/run-pure.test.ts +92 -0
  236. package/src/testing/run-pure.ts +128 -0
  237. package/src/testing/services.ts +17 -0
  238. package/src/types.ts +694 -7
  239. package/src/utils/blurhash.ts +90 -0
  240. package/src/utils/edges.ts +57 -0
  241. package/src/utils/error-classification.test.ts +116 -0
  242. package/src/utils/error-classification.ts +74 -0
  243. package/src/utils/index.ts +83 -6
  244. package/src/utils/parser.test.ts +67 -102
  245. package/src/utils/parser.ts +63 -2
  246. package/src/utils/semver.test.ts +32 -0
  247. package/src/utils/semver.ts +30 -0
  248. package/src/utils/sha256.ts +15 -0
  249. package/src/utils/surql.ts +22 -11
  250. package/src/utils/utils.test.ts +3 -2
  251. package/tsdown.config.ts +117 -3
  252. package/vitest.config.ts +48 -0
  253. package/src/modules/cache/index.ts +0 -241
  254. package/src/modules/cache/types.ts +0 -19
  255. package/src/modules/data/data.test.ts +0 -58
  256. package/src/modules/data/index.ts +0 -882
  257. package/src/modules/sync/engine.ts +0 -173
  258. package/src/modules/sync/events/index.ts +0 -78
  259. package/src/modules/sync/index.ts +0 -3
  260. package/src/modules/sync/queue/index.ts +0 -2
  261. package/src/modules/sync/queue/queue-down.ts +0 -90
  262. package/src/modules/sync/queue/queue-up.ts +0 -224
  263. package/src/modules/sync/scheduler.ts +0 -84
  264. package/src/modules/sync/sync.ts +0 -422
  265. package/src/modules/sync/utils.test.ts +0 -311
  266. 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 { LocalDatabaseService } from './local';
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: LocalDatabaseService,
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 _00_query SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;
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.query(statement);
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.query<any>(
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.query(`DEFINE DATABASE _00_temp;`);
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.query(`
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.query(`
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.query(
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
+ });