@verdant-web/store 2.2.0 → 2.3.1

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 (119) hide show
  1. package/dist/cjs/client/Client.js +4 -16
  2. package/dist/cjs/client/Client.js.map +1 -1
  3. package/dist/cjs/client/ClientDescriptor.d.ts +1 -0
  4. package/dist/cjs/client/ClientDescriptor.js +7 -3
  5. package/dist/cjs/client/ClientDescriptor.js.map +1 -1
  6. package/dist/cjs/files/EntityFile.d.ts +5 -1
  7. package/dist/cjs/files/EntityFile.js +6 -1
  8. package/dist/cjs/files/EntityFile.js.map +1 -1
  9. package/dist/cjs/files/FileManager.js +9 -2
  10. package/dist/cjs/files/FileManager.js.map +1 -1
  11. package/dist/cjs/files/FileStorage.d.ts +2 -1
  12. package/dist/cjs/files/FileStorage.js +13 -2
  13. package/dist/cjs/files/FileStorage.js.map +1 -1
  14. package/dist/cjs/files/utils.js +1 -1
  15. package/dist/cjs/files/utils.js.map +1 -1
  16. package/dist/cjs/idb.d.ts +1 -0
  17. package/dist/cjs/idb.js +17 -1
  18. package/dist/cjs/idb.js.map +1 -1
  19. package/dist/cjs/index.d.ts +1 -0
  20. package/dist/cjs/index.js +3 -1
  21. package/dist/cjs/index.js.map +1 -1
  22. package/dist/cjs/metadata/Metadata.d.ts +4 -0
  23. package/dist/cjs/metadata/Metadata.js +9 -2
  24. package/dist/cjs/metadata/Metadata.js.map +1 -1
  25. package/dist/cjs/migration/db.d.ts +8 -0
  26. package/dist/cjs/migration/db.js +109 -0
  27. package/dist/cjs/migration/db.js.map +1 -0
  28. package/dist/cjs/migration/errors.d.ts +5 -0
  29. package/dist/cjs/migration/errors.js +12 -0
  30. package/dist/cjs/migration/errors.js.map +1 -0
  31. package/dist/cjs/{openDocumentDatabase.d.ts → migration/openDatabase.d.ts} +2 -2
  32. package/dist/cjs/{openDocumentDatabase.js → migration/openDatabase.js} +47 -150
  33. package/dist/cjs/migration/openDatabase.js.map +1 -0
  34. package/dist/cjs/migration/paths.d.ts +6 -0
  35. package/dist/cjs/migration/paths.js +53 -0
  36. package/dist/cjs/migration/paths.js.map +1 -0
  37. package/dist/cjs/migration/paths.test.d.ts +1 -0
  38. package/dist/cjs/migration/paths.test.js +91 -0
  39. package/dist/cjs/migration/paths.test.js.map +1 -0
  40. package/dist/cjs/reactives/DocumentFamiliyCache.d.ts +5 -1
  41. package/dist/cjs/reactives/DocumentFamiliyCache.js +11 -7
  42. package/dist/cjs/reactives/DocumentFamiliyCache.js.map +1 -1
  43. package/dist/cjs/sync/Heartbeat.js +1 -1
  44. package/dist/cjs/sync/Heartbeat.js.map +1 -1
  45. package/dist/cjs/sync/PresenceManager.js +3 -0
  46. package/dist/cjs/sync/PresenceManager.js.map +1 -1
  47. package/dist/cjs/sync/WebSocketSync.js +6 -0
  48. package/dist/cjs/sync/WebSocketSync.js.map +1 -1
  49. package/dist/esm/client/Client.js +3 -15
  50. package/dist/esm/client/Client.js.map +1 -1
  51. package/dist/esm/client/ClientDescriptor.d.ts +1 -0
  52. package/dist/esm/client/ClientDescriptor.js +6 -2
  53. package/dist/esm/client/ClientDescriptor.js.map +1 -1
  54. package/dist/esm/files/EntityFile.d.ts +5 -1
  55. package/dist/esm/files/EntityFile.js +6 -1
  56. package/dist/esm/files/EntityFile.js.map +1 -1
  57. package/dist/esm/files/FileManager.js +9 -2
  58. package/dist/esm/files/FileManager.js.map +1 -1
  59. package/dist/esm/files/FileStorage.d.ts +2 -1
  60. package/dist/esm/files/FileStorage.js +13 -2
  61. package/dist/esm/files/FileStorage.js.map +1 -1
  62. package/dist/esm/files/utils.js +1 -1
  63. package/dist/esm/files/utils.js.map +1 -1
  64. package/dist/esm/idb.d.ts +1 -0
  65. package/dist/esm/idb.js +15 -0
  66. package/dist/esm/idb.js.map +1 -1
  67. package/dist/esm/index.d.ts +1 -0
  68. package/dist/esm/index.js +1 -0
  69. package/dist/esm/index.js.map +1 -1
  70. package/dist/esm/metadata/Metadata.d.ts +4 -0
  71. package/dist/esm/metadata/Metadata.js +9 -2
  72. package/dist/esm/metadata/Metadata.js.map +1 -1
  73. package/dist/esm/migration/db.d.ts +8 -0
  74. package/dist/esm/migration/db.js +101 -0
  75. package/dist/esm/migration/db.js.map +1 -0
  76. package/dist/esm/migration/errors.d.ts +5 -0
  77. package/dist/esm/migration/errors.js +8 -0
  78. package/dist/esm/migration/errors.js.map +1 -0
  79. package/dist/esm/{openDocumentDatabase.d.ts → migration/openDatabase.d.ts} +2 -2
  80. package/dist/esm/{openDocumentDatabase.js → migration/openDatabase.js} +39 -142
  81. package/dist/esm/migration/openDatabase.js.map +1 -0
  82. package/dist/esm/migration/paths.d.ts +6 -0
  83. package/dist/esm/migration/paths.js +49 -0
  84. package/dist/esm/migration/paths.js.map +1 -0
  85. package/dist/esm/migration/paths.test.d.ts +1 -0
  86. package/dist/esm/migration/paths.test.js +89 -0
  87. package/dist/esm/migration/paths.test.js.map +1 -0
  88. package/dist/esm/reactives/DocumentFamiliyCache.d.ts +5 -1
  89. package/dist/esm/reactives/DocumentFamiliyCache.js +11 -7
  90. package/dist/esm/reactives/DocumentFamiliyCache.js.map +1 -1
  91. package/dist/esm/sync/Heartbeat.js +1 -1
  92. package/dist/esm/sync/Heartbeat.js.map +1 -1
  93. package/dist/esm/sync/PresenceManager.js +3 -0
  94. package/dist/esm/sync/PresenceManager.js.map +1 -1
  95. package/dist/esm/sync/WebSocketSync.js +6 -0
  96. package/dist/esm/sync/WebSocketSync.js.map +1 -1
  97. package/dist/tsconfig-cjs.tsbuildinfo +1 -1
  98. package/dist/tsconfig.tsbuildinfo +1 -1
  99. package/package.json +2 -2
  100. package/src/client/Client.ts +7 -17
  101. package/src/client/ClientDescriptor.ts +7 -3
  102. package/src/files/EntityFile.ts +14 -1
  103. package/src/files/FileManager.ts +8 -2
  104. package/src/files/FileStorage.ts +18 -2
  105. package/src/files/utils.ts +1 -1
  106. package/src/idb.ts +19 -0
  107. package/src/index.ts +1 -0
  108. package/src/metadata/Metadata.ts +9 -2
  109. package/src/migration/db.ts +147 -0
  110. package/src/migration/errors.ts +7 -0
  111. package/src/{openDocumentDatabase.ts → migration/openDatabase.ts} +69 -201
  112. package/src/migration/paths.test.ts +93 -0
  113. package/src/migration/paths.ts +73 -0
  114. package/src/reactives/DocumentFamiliyCache.ts +31 -9
  115. package/src/sync/Heartbeat.ts +1 -1
  116. package/src/sync/PresenceManager.ts +3 -0
  117. package/src/sync/WebSocketSync.ts +9 -0
  118. package/dist/cjs/openDocumentDatabase.js.map +0 -1
  119. 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 () => {
@@ -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,
@@ -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
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) {
@@ -493,6 +421,7 @@ function getMigrationEngine({
493
421
  function getVersion1MigrationEngine({
494
422
  meta,
495
423
  migration,
424
+ context,
496
425
  }: {
497
426
  context: OpenDocumentDbContext;
498
427
  migration: Migration;
@@ -519,6 +448,7 @@ function getVersion1MigrationEngine({
519
448
  meta,
520
449
  });
521
450
  const engine: MigrationEngine<StorageSchema, StorageSchema> = {
451
+ log: context.log,
522
452
  newOids,
523
453
  migrate: (collection, strategy) => {
524
454
  throw new Error(
@@ -532,59 +462,6 @@ function getVersion1MigrationEngine({
532
462
  return engine;
533
463
  }
534
464
 
535
- async function closeDatabase(db: IDBDatabase) {
536
- db.close();
537
- // FIXME: this isn't right!!!!
538
- await new Promise<void>((resolve) => resolve());
539
- }
540
-
541
- async function upgradeDatabase(
542
- indexedDb: IDBFactory,
543
- namespace: string,
544
- version: number,
545
- upgrader: (
546
- transaction: IDBTransaction,
547
- db: IDBDatabase,
548
- event: IDBVersionChangeEvent,
549
- ) => void,
550
- log?: (...args: any[]) => void,
551
- ): Promise<void> {
552
- function openAndUpgrade(resolve: () => void, reject: (err: Error) => void) {
553
- const request = indexedDb.open(
554
- [namespace, 'collections'].join('_'),
555
- version,
556
- );
557
- let wasUpgraded = false;
558
- request.onupgradeneeded = (event) => {
559
- const transaction = request.transaction!;
560
- upgrader(transaction, request.result, event);
561
- wasUpgraded = true;
562
- };
563
- request.onsuccess = (event) => {
564
- request.result.close();
565
- if (wasUpgraded) {
566
- resolve();
567
- } else {
568
- reject(
569
- new Error(
570
- 'Database was not upgraded when a version change was expected',
571
- ),
572
- );
573
- }
574
- };
575
- request.onerror = (event) => {
576
- reject(request.error || new Error('Unknown error'));
577
- };
578
- request.onblocked = (event) => {
579
- log?.('Database upgrade blocked, waiting...');
580
- // setTimeout(() => {
581
- // openAndUpgrade(resolve, reject);
582
- // }, 200);
583
- };
584
- }
585
- return new Promise(openAndUpgrade);
586
- }
587
-
588
465
  async function getAllKeys(store: IDBObjectStore) {
589
466
  return new Promise<IDBValidKey[]>((resolve, reject) => {
590
467
  const request = store.getAllKeys();
@@ -620,12 +497,3 @@ async function putView(store: IDBObjectStore, view: any) {
620
497
  };
621
498
  });
622
499
  }
623
-
624
- async function acquireLock(namespace: string, procedure: () => Promise<void>) {
625
- if (typeof navigator !== 'undefined' && navigator.locks) {
626
- await navigator.locks.request(`lo-fi_migration_${namespace}`, procedure);
627
- } else {
628
- // TODO: is there a fallback?
629
- await procedure();
630
- }
631
- }
@@ -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
+ });
@@ -0,0 +1,73 @@
1
+ import { Migration } from '@verdant-web/common';
2
+ import { MigrationPathError } from './errors.js';
3
+
4
+ export function getMigrationPath({
5
+ currentVersion,
6
+ targetVersion,
7
+ migrations,
8
+ }: {
9
+ currentVersion: number;
10
+ targetVersion: number;
11
+ migrations: Migration[];
12
+ }) {
13
+ const path = getNextPathStep({
14
+ currentVersion,
15
+ targetVersion,
16
+ migrations,
17
+ });
18
+ if (!path) {
19
+ throw new MigrationPathError(
20
+ `No migration path found from ${currentVersion} to ${targetVersion}! This is a bug. If you're seeing this, contact the developer and provide them with the full contents of this message.`,
21
+ );
22
+ }
23
+ return path;
24
+ }
25
+
26
+ function getNextPathStep({
27
+ currentVersion,
28
+ targetVersion,
29
+ migrations,
30
+ }: {
31
+ currentVersion: number;
32
+ targetVersion: number;
33
+ migrations: Migration[];
34
+ }): Migration[] | null {
35
+ if (currentVersion === targetVersion) {
36
+ return [];
37
+ }
38
+
39
+ const fromHere = migrations
40
+ .filter((m) => m.oldSchema.version === currentVersion)
41
+ .sort((a, b) => b.newSchema.version - a.newSchema.version);
42
+
43
+ // keep trying next steps, starting from the largest step,
44
+ // until we find one that leads to the target version down the line
45
+ while (fromHere.length > 0) {
46
+ const next = fromHere.shift()!;
47
+ // this one goes too far (probably never relevant, but still)
48
+ if (next.newSchema.version > targetVersion) {
49
+ return null;
50
+ }
51
+ // exact match - we're done, return the path
52
+ if (next.newSchema.version === targetVersion) {
53
+ return [next];
54
+ }
55
+ // look ahead a down the line. do we reach the target? if so,
56
+ // we choose this path.
57
+ const nextPath = getNextPathStep({
58
+ currentVersion: next.newSchema.version,
59
+ targetVersion,
60
+ migrations,
61
+ });
62
+ if (nextPath) {
63
+ return [next, ...nextPath];
64
+ }
65
+
66
+ // otherwise, try the next one with a smaller increment
67
+ }
68
+
69
+ // no paths from here match at all! if another layer is calling this one,
70
+ // it will fallback to its next longest step. otherwise there may
71
+ // be no paths at all...
72
+ return null;
73
+ }