@verdant-web/store 2.1.0 → 2.3.0

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 (105) hide show
  1. package/dist/cjs/DocumentManager.js +4 -4
  2. package/dist/cjs/DocumentManager.js.map +1 -1
  3. package/dist/cjs/client/Client.js +4 -16
  4. package/dist/cjs/client/Client.js.map +1 -1
  5. package/dist/cjs/client/ClientDescriptor.d.ts +1 -0
  6. package/dist/cjs/client/ClientDescriptor.js +7 -3
  7. package/dist/cjs/client/ClientDescriptor.js.map +1 -1
  8. package/dist/cjs/files/utils.js +1 -1
  9. package/dist/cjs/files/utils.js.map +1 -1
  10. package/dist/cjs/idb.d.ts +1 -0
  11. package/dist/cjs/idb.js +17 -1
  12. package/dist/cjs/idb.js.map +1 -1
  13. package/dist/cjs/index.d.ts +1 -0
  14. package/dist/cjs/index.js +3 -1
  15. package/dist/cjs/index.js.map +1 -1
  16. package/dist/cjs/migration/db.d.ts +8 -0
  17. package/dist/cjs/migration/db.js +109 -0
  18. package/dist/cjs/migration/db.js.map +1 -0
  19. package/dist/cjs/migration/errors.d.ts +5 -0
  20. package/dist/cjs/migration/errors.js +12 -0
  21. package/dist/cjs/migration/errors.js.map +1 -0
  22. package/dist/cjs/{openDocumentDatabase.d.ts → migration/openDatabase.d.ts} +2 -2
  23. package/dist/cjs/{openDocumentDatabase.js → migration/openDatabase.js} +52 -155
  24. package/dist/cjs/migration/openDatabase.js.map +1 -0
  25. package/dist/cjs/migration/paths.d.ts +6 -0
  26. package/dist/cjs/migration/paths.js +53 -0
  27. package/dist/cjs/migration/paths.js.map +1 -0
  28. package/dist/cjs/migration/paths.test.d.ts +1 -0
  29. package/dist/cjs/migration/paths.test.js +91 -0
  30. package/dist/cjs/migration/paths.test.js.map +1 -0
  31. package/dist/cjs/queries2/GetQuery.js +1 -1
  32. package/dist/cjs/queries2/GetQuery.js.map +1 -1
  33. package/dist/cjs/reactives/DocumentFamiliyCache.d.ts +5 -1
  34. package/dist/cjs/reactives/DocumentFamiliyCache.js +11 -7
  35. package/dist/cjs/reactives/DocumentFamiliyCache.js.map +1 -1
  36. package/dist/cjs/reactives/Entity.d.ts +0 -1
  37. package/dist/cjs/reactives/Entity.js +4 -6
  38. package/dist/cjs/reactives/Entity.js.map +1 -1
  39. package/dist/cjs/sync/Heartbeat.js +1 -1
  40. package/dist/cjs/sync/Heartbeat.js.map +1 -1
  41. package/dist/cjs/sync/WebSocketSync.js +6 -0
  42. package/dist/cjs/sync/WebSocketSync.js.map +1 -1
  43. package/dist/esm/DocumentManager.js +4 -4
  44. package/dist/esm/DocumentManager.js.map +1 -1
  45. package/dist/esm/client/Client.js +3 -15
  46. package/dist/esm/client/Client.js.map +1 -1
  47. package/dist/esm/client/ClientDescriptor.d.ts +1 -0
  48. package/dist/esm/client/ClientDescriptor.js +6 -2
  49. package/dist/esm/client/ClientDescriptor.js.map +1 -1
  50. package/dist/esm/files/utils.js +1 -1
  51. package/dist/esm/files/utils.js.map +1 -1
  52. package/dist/esm/idb.d.ts +1 -0
  53. package/dist/esm/idb.js +15 -0
  54. package/dist/esm/idb.js.map +1 -1
  55. package/dist/esm/index.d.ts +1 -0
  56. package/dist/esm/index.js +1 -0
  57. package/dist/esm/index.js.map +1 -1
  58. package/dist/esm/migration/db.d.ts +8 -0
  59. package/dist/esm/migration/db.js +101 -0
  60. package/dist/esm/migration/db.js.map +1 -0
  61. package/dist/esm/migration/errors.d.ts +5 -0
  62. package/dist/esm/migration/errors.js +8 -0
  63. package/dist/esm/migration/errors.js.map +1 -0
  64. package/dist/esm/{openDocumentDatabase.d.ts → migration/openDatabase.d.ts} +2 -2
  65. package/dist/esm/{openDocumentDatabase.js → migration/openDatabase.js} +44 -147
  66. package/dist/esm/migration/openDatabase.js.map +1 -0
  67. package/dist/esm/migration/paths.d.ts +6 -0
  68. package/dist/esm/migration/paths.js +49 -0
  69. package/dist/esm/migration/paths.js.map +1 -0
  70. package/dist/esm/migration/paths.test.d.ts +1 -0
  71. package/dist/esm/migration/paths.test.js +89 -0
  72. package/dist/esm/migration/paths.test.js.map +1 -0
  73. package/dist/esm/queries2/GetQuery.js +1 -1
  74. package/dist/esm/queries2/GetQuery.js.map +1 -1
  75. package/dist/esm/reactives/DocumentFamiliyCache.d.ts +5 -1
  76. package/dist/esm/reactives/DocumentFamiliyCache.js +11 -7
  77. package/dist/esm/reactives/DocumentFamiliyCache.js.map +1 -1
  78. package/dist/esm/reactives/Entity.d.ts +0 -1
  79. package/dist/esm/reactives/Entity.js +4 -6
  80. package/dist/esm/reactives/Entity.js.map +1 -1
  81. package/dist/esm/sync/Heartbeat.js +1 -1
  82. package/dist/esm/sync/Heartbeat.js.map +1 -1
  83. package/dist/esm/sync/WebSocketSync.js +6 -0
  84. package/dist/esm/sync/WebSocketSync.js.map +1 -1
  85. package/dist/tsconfig-cjs.tsbuildinfo +1 -1
  86. package/dist/tsconfig.tsbuildinfo +1 -1
  87. package/package.json +2 -2
  88. package/src/DocumentManager.ts +4 -6
  89. package/src/client/Client.ts +7 -17
  90. package/src/client/ClientDescriptor.ts +7 -3
  91. package/src/files/utils.ts +1 -1
  92. package/src/idb.ts +19 -0
  93. package/src/index.ts +1 -0
  94. package/src/migration/db.ts +147 -0
  95. package/src/migration/errors.ts +7 -0
  96. package/src/{openDocumentDatabase.ts → migration/openDatabase.ts} +73 -206
  97. package/src/migration/paths.test.ts +93 -0
  98. package/src/migration/paths.ts +73 -0
  99. package/src/queries2/GetQuery.ts +1 -1
  100. package/src/reactives/DocumentFamiliyCache.ts +31 -9
  101. package/src/reactives/Entity.ts +2 -9
  102. package/src/sync/Heartbeat.ts +1 -1
  103. package/src/sync/WebSocketSync.ts +9 -0
  104. package/dist/cjs/openDocumentDatabase.js.map +0 -1
  105. package/dist/esm/openDocumentDatabase.js.map +0 -1
@@ -0,0 +1,147 @@
1
+ export async function getDatabaseVersion(
2
+ indexedDB: IDBFactory,
3
+ namespace: string,
4
+ version: number,
5
+ log?: (...args: any[]) => void,
6
+ ): Promise<number> {
7
+ function openAndGetVersion(
8
+ resolve: (res: [number, IDBDatabase]) => void,
9
+ reject: (err: Error) => void,
10
+ ) {
11
+ let currentVersion: number;
12
+ let database: IDBDatabase;
13
+ const request = indexedDB.open(
14
+ [namespace, 'collections'].join('_'),
15
+ version,
16
+ );
17
+ request.onupgradeneeded = async (event) => {
18
+ currentVersion = event.oldVersion;
19
+ const transaction = request.transaction!;
20
+ database = request.result;
21
+ transaction.abort();
22
+ };
23
+ request.onsuccess = (event) => {
24
+ resolve([request.result.version, request.result]);
25
+ };
26
+ request.onblocked = (event) => {
27
+ // retry if blocked
28
+ log?.('Database blocked, waiting...');
29
+ // setTimeout(() => {
30
+ // openAndGetVersion(resolve, reject);
31
+ // }, 200);
32
+ };
33
+ request.onerror = (event) => {
34
+ // FIXME: this fails if the code is older than the local database
35
+ resolve([currentVersion!, database!]);
36
+ };
37
+ }
38
+ const [currentVersion, db] = await new Promise<[number, IDBDatabase]>(
39
+ openAndGetVersion,
40
+ );
41
+ await closeDatabase(db);
42
+ return currentVersion;
43
+ }
44
+
45
+ export async function closeDatabase(db: IDBDatabase) {
46
+ db.close();
47
+ // FIXME: this isn't right!!!!
48
+ await new Promise<void>((resolve) => resolve());
49
+ }
50
+
51
+ /**
52
+ * Upgrades the database to the given version, using the given upgrader function.
53
+ */
54
+ export async function upgradeDatabase(
55
+ indexedDb: IDBFactory,
56
+ namespace: string,
57
+ version: number,
58
+ upgrader: (
59
+ transaction: IDBTransaction,
60
+ db: IDBDatabase,
61
+ event: IDBVersionChangeEvent,
62
+ ) => void,
63
+ log?: (...args: any[]) => void,
64
+ ): Promise<void> {
65
+ function openAndUpgrade(resolve: () => void, reject: (err: Error) => void) {
66
+ const request = indexedDb.open(
67
+ [namespace, 'collections'].join('_'),
68
+ version,
69
+ );
70
+ let wasUpgraded = false;
71
+ request.onupgradeneeded = (event) => {
72
+ const transaction = request.transaction!;
73
+ upgrader(transaction, request.result, event);
74
+ wasUpgraded = true;
75
+ };
76
+ request.onsuccess = (event) => {
77
+ request.result.close();
78
+ if (wasUpgraded) {
79
+ resolve();
80
+ } else {
81
+ reject(
82
+ new Error(
83
+ 'Database was not upgraded when a version change was expected',
84
+ ),
85
+ );
86
+ }
87
+ };
88
+ request.onerror = (event) => {
89
+ reject(request.error || new Error('Unknown error'));
90
+ };
91
+ request.onblocked = (event) => {
92
+ log?.('Database upgrade blocked, waiting...');
93
+ // setTimeout(() => {
94
+ // openAndUpgrade(resolve, reject);
95
+ // }, 200);
96
+ };
97
+ }
98
+ return new Promise(openAndUpgrade);
99
+ }
100
+
101
+ export async function acquireLock(
102
+ namespace: string,
103
+ procedure: () => Promise<void>,
104
+ ) {
105
+ if (typeof navigator !== 'undefined' && navigator.locks) {
106
+ await navigator.locks.request(`verdant_migration_${namespace}`, procedure);
107
+ } else {
108
+ // TODO: is there a fallback?
109
+ await procedure();
110
+ }
111
+ }
112
+
113
+ export async function openDatabase(
114
+ indexedDb: IDBFactory,
115
+ namespace: string,
116
+ version: number,
117
+ ): Promise<IDBDatabase> {
118
+ const db = await new Promise<IDBDatabase>((resolve, reject) => {
119
+ const request = indexedDb.open(
120
+ [namespace, 'collections'].join('_'),
121
+ version,
122
+ );
123
+ request.onupgradeneeded = async (event) => {
124
+ const transaction = request.transaction!;
125
+ transaction.abort();
126
+
127
+ reject(
128
+ new Error('Migration error: database version changed while migrating'),
129
+ );
130
+ };
131
+ request.onsuccess = (event) => {
132
+ resolve(request.result);
133
+ };
134
+ request.onblocked = (event) => {
135
+ reject(new Error('Migration error: database blocked'));
136
+ };
137
+ request.onerror = (event) => {
138
+ reject(new Error('Migration error: database error'));
139
+ };
140
+ });
141
+
142
+ db.addEventListener('versionchange', (event) => {
143
+ db.close();
144
+ });
145
+
146
+ return db;
147
+ }
@@ -0,0 +1,7 @@
1
+ export class MigrationPathError extends Error {
2
+ readonly name = 'MigrationPathError';
3
+
4
+ constructor(public readonly message: string) {
5
+ super(message);
6
+ }
7
+ }
@@ -5,21 +5,33 @@ import {
5
5
  ObjectIdentifier,
6
6
  StorageSchema,
7
7
  addFieldDefaults,
8
+ assert,
8
9
  assignIndexValues,
10
+ assignOid,
9
11
  assignOidPropertiesToAllSubObjects,
10
12
  assignOidsToAllSubObjects,
11
13
  cloneDeep,
12
14
  createOid,
13
15
  decomposeOid,
14
16
  diffToPatches,
17
+ getOid,
18
+ hasOid,
15
19
  initialToPatches,
16
20
  migrationRange,
17
21
  removeOidPropertiesFromAllSubObjects,
18
22
  removeOidsFromAllSubObjects,
19
23
  } from '@verdant-web/common';
20
- import { Context } from './context.js';
21
- import { Metadata } from './metadata/Metadata.js';
22
- import { findAllOids, findOneOid } from './queries2/dbQueries.js';
24
+ import { Context } from '../context.js';
25
+ import { Metadata } from '../metadata/Metadata.js';
26
+ import { findAllOids, findOneOid } from '../queries2/dbQueries.js';
27
+ import {
28
+ acquireLock,
29
+ closeDatabase,
30
+ getDatabaseVersion,
31
+ openDatabase,
32
+ upgradeDatabase,
33
+ } from './db.js';
34
+ import { getMigrationPath } from './paths.js';
23
35
 
24
36
  const globalIDB =
25
37
  typeof window !== 'undefined' ? window.indexedDB : (undefined as any);
@@ -53,13 +65,11 @@ export async function openDocumentDatabase({
53
65
  version,
54
66
  );
55
67
 
56
- const toRunVersions = migrationRange(currentVersion, version);
57
- const toRun = toRunVersions.map((ver) =>
58
- migrations.find((m) => m.version === ver),
59
- );
60
- if (toRun.some((m) => !m)) {
61
- throw new Error(`No migration found for version(s) ${toRunVersions}`);
62
- }
68
+ const toRun = getMigrationPath({
69
+ currentVersion,
70
+ targetVersion: version,
71
+ migrations,
72
+ });
63
73
 
64
74
  if (toRun.length > 0) {
65
75
  await acquireLock(context.namespace, async () => {
@@ -153,7 +163,7 @@ export async function openDocumentDatabase({
153
163
  const readStore = documentReadTransaction.objectStore(collection);
154
164
  const keys = await getAllKeys(readStore);
155
165
  // map the keys to OIDs
156
- const oids = keys.map((key) => createOid(collection, `${key}`, []));
166
+ const oids = keys.map((key) => createOid(collection, `${key}`));
157
167
  oids.push(
158
168
  ...engine.newOids.filter((oid) => {
159
169
  return decomposeOid(oid).collection === collection;
@@ -240,86 +250,6 @@ export async function openDocumentDatabase({
240
250
  }
241
251
  }
242
252
 
243
- async function getDatabaseVersion(
244
- indexedDB: IDBFactory,
245
- namespace: string,
246
- version: number,
247
- log?: (...args: any[]) => void,
248
- ): Promise<number> {
249
- function openAndGetVersion(
250
- resolve: (res: [number, IDBDatabase]) => void,
251
- reject: (err: Error) => void,
252
- ) {
253
- let currentVersion: number;
254
- let database: IDBDatabase;
255
- const request = indexedDB.open(
256
- [namespace, 'collections'].join('_'),
257
- version,
258
- );
259
- request.onupgradeneeded = async (event) => {
260
- currentVersion = event.oldVersion;
261
- const transaction = request.transaction!;
262
- database = request.result;
263
- transaction.abort();
264
- };
265
- request.onsuccess = (event) => {
266
- resolve([request.result.version, request.result]);
267
- };
268
- request.onblocked = (event) => {
269
- // retry if blocked
270
- log?.('Database blocked, waiting...');
271
- // setTimeout(() => {
272
- // openAndGetVersion(resolve, reject);
273
- // }, 200);
274
- };
275
- request.onerror = (event) => {
276
- // FIXME: this fails if the code is older than the local database
277
- resolve([currentVersion!, database!]);
278
- };
279
- }
280
- const [currentVersion, db] = await new Promise<[number, IDBDatabase]>(
281
- openAndGetVersion,
282
- );
283
- await closeDatabase(db);
284
- return currentVersion;
285
- }
286
-
287
- async function openDatabase(
288
- indexedDb: IDBFactory,
289
- namespace: string,
290
- version: number,
291
- ): Promise<IDBDatabase> {
292
- const db = await new Promise<IDBDatabase>((resolve, reject) => {
293
- const request = indexedDb.open(
294
- [namespace, 'collections'].join('_'),
295
- version,
296
- );
297
- request.onupgradeneeded = async (event) => {
298
- const transaction = request.transaction!;
299
- transaction.abort();
300
-
301
- reject(
302
- new Error('Migration error: database version changed while migrating'),
303
- );
304
- };
305
- request.onsuccess = (event) => {
306
- resolve(request.result);
307
- };
308
- request.onblocked = (event) => {
309
- reject(new Error('Migration error: database blocked'));
310
- };
311
- request.onerror = (event) => {
312
- reject(new Error('Migration error: database error'));
313
- };
314
- });
315
-
316
- db.addEventListener('versionchange', (event) => {
317
- db.close();
318
- });
319
-
320
- return db;
321
- }
322
-
323
253
  function getMigrationMutations({
324
254
  migration,
325
255
  meta,
@@ -338,7 +268,7 @@ function getMigrationMutations({
338
268
  addFieldDefaults(migration.newSchema.collections[collectionName], doc);
339
269
  const primaryKey =
340
270
  doc[migration.newSchema.collections[collectionName].primaryKey];
341
- const oid = createOid(collectionName, primaryKey, []);
271
+ const oid = createOid(collectionName, primaryKey);
342
272
  newOids.push(oid);
343
273
  await meta.insertLocalOperation(
344
274
  initialToPatches(doc, oid, getMigrationNow),
@@ -346,7 +276,7 @@ function getMigrationMutations({
346
276
  return doc;
347
277
  },
348
278
  delete: (id: string) => {
349
- const oid = createOid(collectionName, id, []);
279
+ const oid = createOid(collectionName, id);
350
280
  return meta.insertLocalOperation([
351
281
  {
352
282
  oid,
@@ -360,28 +290,21 @@ function getMigrationMutations({
360
290
  }, {} as any);
361
291
  }
362
292
 
363
- function getMigrationEngine({
364
- meta,
293
+ function getMigrationQueries({
365
294
  migration,
366
295
  context,
296
+ meta,
367
297
  }: {
368
- log?: (...args: any[]) => void;
369
- migration: Migration;
370
- meta: Metadata;
298
+ migration: Migration<any>;
371
299
  context: Context;
372
- }): MigrationEngine<any, any> {
373
- function getMigrationNow() {
374
- return meta.time.zero(migration.version);
375
- }
376
-
377
- const newOids = new Array<ObjectIdentifier>();
378
-
379
- const queries = migration.oldCollections.reduce((acc, collectionName) => {
300
+ meta: Metadata;
301
+ }) {
302
+ return migration.oldCollections.reduce((acc, collectionName) => {
380
303
  acc[collectionName] = {
381
304
  get: async (id: string) => {
382
- const oid = createOid(collectionName, id, []);
305
+ const oid = createOid(collectionName, id);
383
306
  const doc = await meta.getDocumentSnapshot(oid);
384
- removeOidsFromAllSubObjects(doc);
307
+ // removeOidsFromAllSubObjects(doc);
385
308
  return doc;
386
309
  },
387
310
  findOne: async (filter: CollectionFilter) => {
@@ -392,7 +315,7 @@ function getMigrationEngine({
392
315
  });
393
316
  if (!oid) return null;
394
317
  const doc = await meta.getDocumentSnapshot(oid);
395
- removeOidsFromAllSubObjects(doc);
318
+ // removeOidsFromAllSubObjects(doc);
396
319
  return doc;
397
320
  },
398
321
  findAll: async (filter: CollectionFilter) => {
@@ -404,12 +327,35 @@ function getMigrationEngine({
404
327
  const docs = await Promise.all(
405
328
  oids.map((oid) => meta.getDocumentSnapshot(oid)),
406
329
  );
407
- docs.forEach((doc) => removeOidsFromAllSubObjects(doc));
330
+ // docs.forEach((doc) => removeOidsFromAllSubObjects(doc));
408
331
  return docs;
409
332
  },
410
333
  };
411
334
  return acc;
412
335
  }, {} as any);
336
+ }
337
+
338
+ function getMigrationEngine({
339
+ meta,
340
+ migration,
341
+ context,
342
+ }: {
343
+ log?: (...args: any[]) => void;
344
+ migration: Migration;
345
+ meta: Metadata;
346
+ context: Context;
347
+ }): MigrationEngine<any, any> {
348
+ function getMigrationNow() {
349
+ return meta.time.zero(migration.version);
350
+ }
351
+
352
+ const newOids = new Array<ObjectIdentifier>();
353
+
354
+ const queries = getMigrationQueries({
355
+ migration,
356
+ context,
357
+ meta,
358
+ });
413
359
  const mutations = getMigrationMutations({
414
360
  migration,
415
361
  getMigrationNow,
@@ -418,45 +364,27 @@ function getMigrationEngine({
418
364
  });
419
365
  const awaitables = new Array<Promise<any>>();
420
366
  const engine: MigrationEngine<StorageSchema, StorageSchema> = {
367
+ log: context.log,
421
368
  newOids,
422
369
  migrate: async (collection, strategy) => {
423
- const docs = await new Promise<any[]>((resolve, reject) => {
424
- const transaction = context.documentDb.transaction(
425
- collection,
426
- 'readonly',
427
- );
428
-
429
- const store = transaction.objectStore(collection);
430
- const cursorReq = store.openCursor();
431
-
432
- const documentsToMigrate: any[] = [];
433
-
434
- cursorReq.onsuccess = async (event) => {
435
- const cursor = cursorReq.result;
436
- if (cursor) {
437
- documentsToMigrate.push(cursor.value);
438
- cursor.continue();
439
- } else {
440
- resolve(documentsToMigrate);
441
- }
442
- };
443
- cursorReq.onerror = (event) => {
444
- reject(cursorReq.error);
445
- };
446
- });
370
+ const docs = await queries[collection].findAll();
447
371
 
448
372
  await Promise.all(
449
- docs.map(async (doc) => {
373
+ docs.map(async (doc: any) => {
374
+ assert(
375
+ hasOid(doc),
376
+ `Document is missing an OID: ${JSON.stringify(doc)}`,
377
+ );
450
378
  const original = cloneDeep(doc);
451
379
  // remove any indexes before computing the diff
452
- const collectionSpec = migration.oldSchema.collections[collection];
453
- const indexKeys = [
454
- ...Object.keys(collectionSpec.synthetics || {}),
455
- ...Object.keys(collectionSpec.compounds || {}),
456
- ];
457
- indexKeys.forEach((key) => {
458
- delete doc[key];
459
- });
380
+ // const collectionSpec = migration.oldSchema.collections[collection];
381
+ // const indexKeys = [
382
+ // ...Object.keys(collectionSpec.synthetics || {}),
383
+ // ...Object.keys(collectionSpec.compounds || {}),
384
+ // ];
385
+ // indexKeys.forEach((key) => {
386
+ // delete doc[key];
387
+ // });
460
388
  // @ts-ignore - excessive type resolution
461
389
  const newValue = await strategy(doc);
462
390
  if (newValue) {
@@ -470,7 +398,6 @@ function getMigrationEngine({
470
398
  original,
471
399
  newValue,
472
400
  getMigrationNow,
473
- [],
474
401
  undefined,
475
402
  [],
476
403
  {
@@ -494,6 +421,7 @@ function getMigrationEngine({
494
421
  function getVersion1MigrationEngine({
495
422
  meta,
496
423
  migration,
424
+ context,
497
425
  }: {
498
426
  context: OpenDocumentDbContext;
499
427
  migration: Migration;
@@ -520,6 +448,7 @@ function getVersion1MigrationEngine({
520
448
  meta,
521
449
  });
522
450
  const engine: MigrationEngine<StorageSchema, StorageSchema> = {
451
+ log: context.log,
523
452
  newOids,
524
453
  migrate: (collection, strategy) => {
525
454
  throw new Error(
@@ -533,59 +462,6 @@ function getVersion1MigrationEngine({
533
462
  return engine;
534
463
  }
535
464
 
536
- async function closeDatabase(db: IDBDatabase) {
537
- db.close();
538
- // FIXME: this isn't right!!!!
539
- await new Promise<void>((resolve) => resolve());
540
- }
541
-
542
- async function upgradeDatabase(
543
- indexedDb: IDBFactory,
544
- namespace: string,
545
- version: number,
546
- upgrader: (
547
- transaction: IDBTransaction,
548
- db: IDBDatabase,
549
- event: IDBVersionChangeEvent,
550
- ) => void,
551
- log?: (...args: any[]) => void,
552
- ): Promise<void> {
553
- function openAndUpgrade(resolve: () => void, reject: (err: Error) => void) {
554
- const request = indexedDb.open(
555
- [namespace, 'collections'].join('_'),
556
- version,
557
- );
558
- let wasUpgraded = false;
559
- request.onupgradeneeded = (event) => {
560
- const transaction = request.transaction!;
561
- upgrader(transaction, request.result, event);
562
- wasUpgraded = true;
563
- };
564
- request.onsuccess = (event) => {
565
- request.result.close();
566
- if (wasUpgraded) {
567
- resolve();
568
- } else {
569
- reject(
570
- new Error(
571
- 'Database was not upgraded when a version change was expected',
572
- ),
573
- );
574
- }
575
- };
576
- request.onerror = (event) => {
577
- reject(request.error || new Error('Unknown error'));
578
- };
579
- request.onblocked = (event) => {
580
- log?.('Database upgrade blocked, waiting...');
581
- // setTimeout(() => {
582
- // openAndUpgrade(resolve, reject);
583
- // }, 200);
584
- };
585
- }
586
- return new Promise(openAndUpgrade);
587
- }
588
-
589
465
  async function getAllKeys(store: IDBObjectStore) {
590
466
  return new Promise<IDBValidKey[]>((resolve, reject) => {
591
467
  const request = store.getAllKeys();
@@ -621,12 +497,3 @@ async function putView(store: IDBObjectStore, view: any) {
621
497
  };
622
498
  });
623
499
  }
624
-
625
- async function acquireLock(namespace: string, procedure: () => Promise<void>) {
626
- if (typeof navigator !== 'undefined' && navigator.locks) {
627
- await navigator.locks.request(`lo-fi_migration_${namespace}`, procedure);
628
- } else {
629
- // TODO: is there a fallback?
630
- await procedure();
631
- }
632
- }
@@ -0,0 +1,93 @@
1
+ import { Migration } from '@verdant-web/common';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { getMigrationPath } from './paths.js';
4
+
5
+ function mockMigration(from: number, to: number) {
6
+ return {
7
+ oldSchema: {
8
+ version: from,
9
+ },
10
+ newSchema: {
11
+ version: to,
12
+ },
13
+ } as any as Migration;
14
+ }
15
+
16
+ describe('migration pathfinding', () => {
17
+ it('should follow a linear chain', () => {
18
+ const migrations = [
19
+ mockMigration(0, 1),
20
+ mockMigration(1, 2),
21
+ mockMigration(2, 3),
22
+ mockMigration(3, 4),
23
+ mockMigration(4, 5),
24
+ ];
25
+ const path = getMigrationPath({
26
+ currentVersion: 0,
27
+ targetVersion: 5,
28
+ migrations,
29
+ });
30
+ expect(path).toEqual(migrations);
31
+ });
32
+ it('should choose the shortest path', () => {
33
+ const migrations = [
34
+ mockMigration(0, 1),
35
+ mockMigration(1, 2),
36
+ mockMigration(2, 3),
37
+ mockMigration(3, 4),
38
+ mockMigration(4, 5),
39
+ mockMigration(0, 5),
40
+ ];
41
+ const path = getMigrationPath({
42
+ currentVersion: 0,
43
+ targetVersion: 5,
44
+ migrations,
45
+ });
46
+ expect(path).toEqual([mockMigration(0, 5)]);
47
+ });
48
+ it('should be resilient to dead ends', () => {
49
+ const migrations = [
50
+ mockMigration(0, 1),
51
+ mockMigration(1, 2),
52
+ mockMigration(4, 5),
53
+ mockMigration(5, 6),
54
+ mockMigration(1, 4),
55
+ ];
56
+ const path = getMigrationPath({
57
+ currentVersion: 0,
58
+ targetVersion: 6,
59
+ migrations,
60
+ });
61
+ expect(path).toEqual([
62
+ mockMigration(0, 1),
63
+ mockMigration(1, 4),
64
+ mockMigration(4, 5),
65
+ mockMigration(5, 6),
66
+ ]);
67
+ });
68
+ it('should error when no path exists to target', () => {
69
+ const migrations = [
70
+ mockMigration(0, 1),
71
+ mockMigration(2, 3),
72
+ mockMigration(3, 4),
73
+ mockMigration(4, 5),
74
+ mockMigration(5, 6),
75
+ ];
76
+ expect(() => {
77
+ getMigrationPath({
78
+ currentVersion: 0,
79
+ targetVersion: 6,
80
+ migrations,
81
+ });
82
+ }).toThrow();
83
+ });
84
+ it('should return empty when versions are the same', () => {
85
+ expect(
86
+ getMigrationPath({
87
+ currentVersion: 1,
88
+ targetVersion: 1,
89
+ migrations: [],
90
+ }),
91
+ ).toEqual([]);
92
+ });
93
+ });