@verdant-web/store 4.0.0 → 4.1.0-alpha.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 (171) hide show
  1. package/LICENSE +21 -650
  2. package/dist/bundle/index.js +11 -11
  3. package/dist/bundle/index.js.map +4 -4
  4. package/dist/esm/__tests__/fixtures/testStorage.d.ts +1 -2
  5. package/dist/esm/__tests__/fixtures/testStorage.js +3 -5
  6. package/dist/esm/__tests__/fixtures/testStorage.js.map +1 -1
  7. package/dist/esm/client/Client.d.ts +6 -2
  8. package/dist/esm/client/Client.js +18 -6
  9. package/dist/esm/client/Client.js.map +1 -1
  10. package/dist/esm/client/ClientDescriptor.d.ts +7 -5
  11. package/dist/esm/client/ClientDescriptor.js +18 -4
  12. package/dist/esm/client/ClientDescriptor.js.map +1 -1
  13. package/dist/esm/context/ShutdownHandler.d.ts +8 -0
  14. package/dist/esm/context/ShutdownHandler.js +24 -0
  15. package/dist/esm/context/ShutdownHandler.js.map +1 -0
  16. package/dist/esm/context/context.d.ts +15 -4
  17. package/dist/esm/entities/EntityStore.js +6 -3
  18. package/dist/esm/entities/EntityStore.js.map +1 -1
  19. package/dist/esm/files/EntityFile.d.ts +1 -0
  20. package/dist/esm/files/EntityFile.js +16 -11
  21. package/dist/esm/files/EntityFile.js.map +1 -1
  22. package/dist/esm/files/FileManager.d.ts +1 -3
  23. package/dist/esm/files/FileManager.js +12 -10
  24. package/dist/esm/files/FileManager.js.map +1 -1
  25. package/dist/esm/index.d.ts +4 -5
  26. package/dist/esm/index.js +2 -3
  27. package/dist/esm/index.js.map +1 -1
  28. package/dist/esm/internal.d.ts +6 -0
  29. package/dist/esm/internal.js +5 -0
  30. package/dist/esm/internal.js.map +1 -0
  31. package/dist/esm/persistence/MessageCreator.d.ts +3 -1
  32. package/dist/esm/persistence/MessageCreator.js +58 -55
  33. package/dist/esm/persistence/MessageCreator.js.map +1 -1
  34. package/dist/esm/persistence/PersistenceFiles.d.ts +8 -21
  35. package/dist/esm/persistence/PersistenceFiles.js +44 -30
  36. package/dist/esm/persistence/PersistenceFiles.js.map +1 -1
  37. package/dist/esm/persistence/PersistenceMetadata.d.ts +12 -11
  38. package/dist/esm/persistence/PersistenceMetadata.js +201 -137
  39. package/dist/esm/persistence/PersistenceMetadata.js.map +1 -1
  40. package/dist/esm/persistence/PersistenceQueries.d.ts +10 -11
  41. package/dist/esm/persistence/PersistenceQueries.js +33 -5
  42. package/dist/esm/persistence/PersistenceQueries.js.map +1 -1
  43. package/dist/esm/persistence/PersistenceRebaser.d.ts +5 -9
  44. package/dist/esm/persistence/PersistenceRebaser.js +63 -47
  45. package/dist/esm/persistence/PersistenceRebaser.js.map +1 -1
  46. package/dist/esm/persistence/idb/IdbService.d.ts +0 -1
  47. package/dist/esm/persistence/idb/IdbService.js +28 -16
  48. package/dist/esm/persistence/idb/IdbService.js.map +1 -1
  49. package/dist/esm/persistence/idb/files/IdbPersistenceFileDb.d.ts +11 -31
  50. package/dist/esm/persistence/idb/files/IdbPersistenceFileDb.js +31 -36
  51. package/dist/esm/persistence/idb/files/IdbPersistenceFileDb.js.map +1 -1
  52. package/dist/esm/persistence/idb/idbPersistence.d.ts +17 -9
  53. package/dist/esm/persistence/idb/idbPersistence.js +80 -39
  54. package/dist/esm/persistence/idb/idbPersistence.js.map +1 -1
  55. package/dist/esm/persistence/idb/metadata/IdbMetadataDb.d.ts +7 -10
  56. package/dist/esm/persistence/idb/metadata/IdbMetadataDb.js +45 -71
  57. package/dist/esm/persistence/idb/metadata/IdbMetadataDb.js.map +1 -1
  58. package/dist/esm/persistence/idb/metadata/openMetadataDatabase.d.ts +1 -12
  59. package/dist/esm/persistence/idb/metadata/openMetadataDatabase.js +3 -56
  60. package/dist/esm/persistence/idb/metadata/openMetadataDatabase.js.map +1 -1
  61. package/dist/esm/persistence/idb/queries/{IdbQueryDb.d.ts → IdbDocumentDb.d.ts} +7 -13
  62. package/dist/esm/persistence/idb/queries/{IdbQueryDb.js → IdbDocumentDb.js} +15 -32
  63. package/dist/esm/persistence/idb/queries/IdbDocumentDb.js.map +1 -0
  64. package/dist/esm/persistence/idb/queries/migration/db.d.ts +3 -5
  65. package/dist/esm/persistence/idb/queries/migration/db.js +13 -28
  66. package/dist/esm/persistence/idb/queries/migration/db.js.map +1 -1
  67. package/dist/esm/persistence/idb/util.d.ts +8 -4
  68. package/dist/esm/persistence/idb/util.js +64 -21
  69. package/dist/esm/persistence/idb/util.js.map +1 -1
  70. package/dist/esm/persistence/interfaces.d.ts +68 -75
  71. package/dist/esm/persistence/{idb/queries/migration → migration}/engine.d.ts +4 -7
  72. package/dist/esm/persistence/{idb/queries/migration → migration}/engine.js +18 -10
  73. package/dist/esm/persistence/migration/engine.js.map +1 -0
  74. package/dist/esm/persistence/migration/finalize.d.ts +9 -0
  75. package/dist/esm/persistence/migration/finalize.js +75 -0
  76. package/dist/esm/persistence/migration/finalize.js.map +1 -0
  77. package/dist/esm/persistence/migration/migrate.d.ts +12 -0
  78. package/dist/esm/persistence/migration/migrate.js +89 -0
  79. package/dist/esm/persistence/migration/migrate.js.map +1 -0
  80. package/dist/esm/persistence/migration/paths.js.map +1 -0
  81. package/dist/esm/persistence/migration/paths.test.js.map +1 -0
  82. package/dist/esm/persistence/migration/types.d.ts +3 -0
  83. package/dist/esm/persistence/migration/types.js.map +1 -0
  84. package/dist/esm/persistence/persistence.js +25 -15
  85. package/dist/esm/persistence/persistence.js.map +1 -1
  86. package/dist/esm/queries/FindAllQuery.js +1 -1
  87. package/dist/esm/queries/FindAllQuery.js.map +1 -1
  88. package/dist/esm/queries/FindInfiniteQuery.js +2 -2
  89. package/dist/esm/queries/FindInfiniteQuery.js.map +1 -1
  90. package/dist/esm/queries/FindOneQuery.js +1 -1
  91. package/dist/esm/queries/FindOneQuery.js.map +1 -1
  92. package/dist/esm/queries/FindPageQuery.js +1 -1
  93. package/dist/esm/queries/FindPageQuery.js.map +1 -1
  94. package/dist/esm/sync/FileSync.js +3 -3
  95. package/dist/esm/sync/FileSync.js.map +1 -1
  96. package/dist/esm/sync/PushPullSync.d.ts +2 -3
  97. package/dist/esm/sync/PushPullSync.js +4 -2
  98. package/dist/esm/sync/PushPullSync.js.map +1 -1
  99. package/dist/esm/sync/ServerSyncEndpointProvider.d.ts +3 -7
  100. package/dist/esm/sync/ServerSyncEndpointProvider.js +3 -2
  101. package/dist/esm/sync/ServerSyncEndpointProvider.js.map +1 -1
  102. package/dist/esm/sync/Sync.d.ts +6 -1
  103. package/dist/esm/sync/Sync.js +12 -4
  104. package/dist/esm/sync/Sync.js.map +1 -1
  105. package/dist/esm/sync/WebSocketSync.js +10 -4
  106. package/dist/esm/sync/WebSocketSync.js.map +1 -1
  107. package/package.json +6 -2
  108. package/src/__tests__/fixtures/testStorage.ts +6 -6
  109. package/src/client/Client.ts +26 -8
  110. package/src/client/ClientDescriptor.ts +27 -9
  111. package/src/context/ShutdownHandler.ts +26 -0
  112. package/src/context/context.ts +16 -4
  113. package/src/entities/EntityStore.ts +9 -3
  114. package/src/files/EntityFile.ts +11 -6
  115. package/src/files/FileManager.ts +13 -10
  116. package/src/index.ts +8 -9
  117. package/src/internal.ts +27 -0
  118. package/src/persistence/MessageCreator.ts +79 -73
  119. package/src/persistence/PersistenceFiles.ts +57 -31
  120. package/src/persistence/PersistenceMetadata.ts +287 -195
  121. package/src/persistence/PersistenceQueries.ts +45 -9
  122. package/src/persistence/PersistenceRebaser.ts +105 -70
  123. package/src/persistence/idb/IdbService.ts +40 -22
  124. package/src/persistence/idb/files/IdbPersistenceFileDb.ts +30 -62
  125. package/src/persistence/idb/idbPersistence.ts +123 -47
  126. package/src/persistence/idb/metadata/IdbMetadataDb.ts +75 -97
  127. package/src/persistence/idb/metadata/openMetadataDatabase.ts +2 -96
  128. package/src/persistence/idb/queries/{IdbQueryDb.ts → IdbDocumentDb.ts} +17 -57
  129. package/src/persistence/idb/queries/migration/db.ts +20 -39
  130. package/src/persistence/idb/util.ts +84 -21
  131. package/src/persistence/interfaces.ts +89 -90
  132. package/src/persistence/{idb/queries/migration → migration}/engine.ts +30 -15
  133. package/src/persistence/migration/finalize.ts +126 -0
  134. package/src/persistence/migration/migrate.ts +169 -0
  135. package/src/persistence/migration/types.ts +4 -0
  136. package/src/persistence/persistence.ts +37 -14
  137. package/src/queries/FindAllQuery.ts +1 -1
  138. package/src/queries/FindInfiniteQuery.ts +2 -2
  139. package/src/queries/FindOneQuery.ts +1 -1
  140. package/src/queries/FindPageQuery.ts +1 -1
  141. package/src/sync/FileSync.ts +21 -15
  142. package/src/sync/PushPullSync.ts +3 -4
  143. package/src/sync/ServerSyncEndpointProvider.ts +6 -8
  144. package/src/sync/Sync.ts +20 -7
  145. package/src/sync/WebSocketSync.ts +10 -4
  146. package/dist/esm/client/constants.d.ts +0 -1
  147. package/dist/esm/client/constants.js +0 -2
  148. package/dist/esm/client/constants.js.map +0 -1
  149. package/dist/esm/persistence/idb/queries/IdbQueryDb.js.map +0 -1
  150. package/dist/esm/persistence/idb/queries/migration/engine.js.map +0 -1
  151. package/dist/esm/persistence/idb/queries/migration/migrations.d.ts +0 -15
  152. package/dist/esm/persistence/idb/queries/migration/migrations.js +0 -243
  153. package/dist/esm/persistence/idb/queries/migration/migrations.js.map +0 -1
  154. package/dist/esm/persistence/idb/queries/migration/openQueryDatabase.d.ts +0 -8
  155. package/dist/esm/persistence/idb/queries/migration/openQueryDatabase.js +0 -24
  156. package/dist/esm/persistence/idb/queries/migration/openQueryDatabase.js.map +0 -1
  157. package/dist/esm/persistence/idb/queries/migration/paths.js.map +0 -1
  158. package/dist/esm/persistence/idb/queries/migration/paths.test.js.map +0 -1
  159. package/dist/esm/persistence/idb/queries/migration/types.d.ts +0 -6
  160. package/dist/esm/persistence/idb/queries/migration/types.js.map +0 -1
  161. package/src/client/constants.ts +0 -1
  162. package/src/persistence/idb/queries/migration/migrations.ts +0 -345
  163. package/src/persistence/idb/queries/migration/openQueryDatabase.ts +0 -54
  164. package/src/persistence/idb/queries/migration/types.ts +0 -8
  165. /package/dist/esm/persistence/{idb/queries/migration → migration}/paths.d.ts +0 -0
  166. /package/dist/esm/persistence/{idb/queries/migration → migration}/paths.js +0 -0
  167. /package/dist/esm/persistence/{idb/queries/migration → migration}/paths.test.d.ts +0 -0
  168. /package/dist/esm/persistence/{idb/queries/migration → migration}/paths.test.js +0 -0
  169. /package/dist/esm/persistence/{idb/queries/migration → migration}/types.js +0 -0
  170. /package/src/persistence/{idb/queries/migration → migration}/paths.test.ts +0 -0
  171. /package/src/persistence/{idb/queries/migration → migration}/paths.ts +0 -0
@@ -14,7 +14,7 @@ import {
14
14
  AuthorizationKey,
15
15
  } from '@verdant-web/common';
16
16
  import { OpenDocumentDbContext } from './types.js';
17
- import { IdbQueryDb } from '../IdbQueryDb.js';
17
+ import { PersistenceDocumentDb, PersistenceNamespace } from '../interfaces.js';
18
18
 
19
19
  function getMigrationMutations({
20
20
  migration,
@@ -61,14 +61,12 @@ function getMigrationMutations({
61
61
  function getMigrationQueries({
62
62
  migration,
63
63
  context,
64
- queryDb,
64
+ documents,
65
65
  }: {
66
66
  migration: Migration<any>;
67
67
  context: OpenDocumentDbContext;
68
- queryDb: IDBDatabase;
68
+ documents: PersistenceDocumentDb;
69
69
  }) {
70
- const queries = new IdbQueryDb(queryDb, context);
71
-
72
70
  return migration.oldCollections.reduce((acc, collectionName) => {
73
71
  acc[collectionName] = {
74
72
  get: async (id: string) => {
@@ -80,7 +78,7 @@ function getMigrationQueries({
80
78
  return doc;
81
79
  },
82
80
  findOne: async (filter: CollectionFilter) => {
83
- const oid = await queries.findOneOid({
81
+ const oid = await documents.findOneOid({
84
82
  collection: collectionName,
85
83
  index: filter,
86
84
  });
@@ -92,7 +90,7 @@ function getMigrationQueries({
92
90
  return doc;
93
91
  },
94
92
  findAll: async (filter: CollectionFilter) => {
95
- const { result: oids } = await queries.findAllOids({
93
+ const { result: oids } = await documents.findAllOids({
96
94
  collection: collectionName,
97
95
  index: filter,
98
96
  });
@@ -111,27 +109,36 @@ function getMigrationQueries({
111
109
  }, {} as any);
112
110
  }
113
111
 
114
- export function getMigrationEngine({
112
+ export async function getMigrationEngine({
115
113
  migration,
116
114
  context,
117
- queryDb,
115
+ ns,
118
116
  }: {
119
117
  log?: (...args: any[]) => void;
120
118
  migration: Migration;
121
119
  context: OpenDocumentDbContext;
122
- queryDb: IDBDatabase;
123
- }): MigrationEngine {
120
+ ns: PersistenceNamespace;
121
+ }): Promise<MigrationEngine> {
122
+ const migrationContext = {
123
+ ...context,
124
+ schema: migration.oldSchema,
125
+ };
126
+ if (migration.oldSchema.version === 0) {
127
+ return getInitialMigrationEngine({ migration, context: migrationContext });
128
+ }
129
+
124
130
  const newOids = new Array<ObjectIdentifier>();
125
131
 
132
+ const documents = await ns.openDocuments(migrationContext);
126
133
  const queries = getMigrationQueries({
127
134
  migration,
128
- context,
129
- queryDb,
135
+ context: migrationContext,
136
+ documents,
130
137
  });
131
138
  const mutations = getMigrationMutations({
132
139
  migration,
133
140
  newOids,
134
- ctx: context,
141
+ ctx: migrationContext,
135
142
  });
136
143
  const deleteCollection = async (collection: string) => {
137
144
  await context.meta.deleteCollection(collection);
@@ -143,6 +150,10 @@ export function getMigrationEngine({
143
150
  deleteCollection,
144
151
  migrate: async (collection, strategy) => {
145
152
  const docs = await queries[collection].findAll();
153
+ context.log(
154
+ 'debug',
155
+ `Migrating ${docs.length} documents in ${collection}`,
156
+ );
146
157
 
147
158
  await Promise.all(
148
159
  docs.filter(Boolean).map(async (doc: any) => {
@@ -190,11 +201,14 @@ export function getMigrationEngine({
190
201
  queries,
191
202
  mutations,
192
203
  awaitables,
204
+ close: async () => {
205
+ await documents.close();
206
+ },
193
207
  };
194
208
  return engine;
195
209
  }
196
210
 
197
- export function getInitialMigrationEngine({
211
+ function getInitialMigrationEngine({
198
212
  migration,
199
213
  context,
200
214
  }: {
@@ -232,6 +246,7 @@ export function getInitialMigrationEngine({
232
246
  queries,
233
247
  mutations,
234
248
  awaitables: [],
249
+ close: () => Promise.resolve(),
235
250
  };
236
251
  return engine;
237
252
  }
@@ -0,0 +1,126 @@
1
+ import {
2
+ decomposeOid,
3
+ getOidRoot,
4
+ Migration,
5
+ MigrationEngine,
6
+ } from '@verdant-web/common';
7
+ import { ClientOperation, PersistenceDocumentDb } from '../interfaces.js';
8
+ import { OpenDocumentDbContext } from './types.js';
9
+
10
+ export async function finalizeMigration({
11
+ ctx,
12
+ documents,
13
+ migration,
14
+ engine,
15
+ }: {
16
+ ctx: OpenDocumentDbContext;
17
+ documents: PersistenceDocumentDb;
18
+ migration: Migration<any>;
19
+ engine: MigrationEngine;
20
+ }) {
21
+ /**
22
+ * In cases where operations from the future have been
23
+ * received by this client, we may have created entire
24
+ * documents in metadata which were not written to storage
25
+ * because all of their operations were in the future (
26
+ * i.e. in the next version). We have to find those documents
27
+ * and also write their snapshots to storage, because they
28
+ * won't be present in storage already to 'refresh,' so
29
+ * if we don't analyze metadata for 'future' operations like
30
+ * this, we won't know they exist.
31
+ *
32
+ * This led to behavior where the metadata would be properly
33
+ * synced, but after upgrading the app and migrating, items
34
+ * would be missing from findAll and findOne queries.
35
+ */
36
+ const docsWithUnappliedMigrations = await getDocsWithUnappliedMigrations({
37
+ currentVersion: migration.oldSchema.version,
38
+ newVersion: migration.newSchema.version,
39
+ ctx,
40
+ });
41
+
42
+ // once the schema is ready, we can write back the migrated documents
43
+
44
+ for (const collection of migration.allCollections) {
45
+ // map the keys to OIDs
46
+ const { result: oids } = await documents.findAllOids({
47
+ collection,
48
+ });
49
+ oids.push(
50
+ ...engine.newOids.filter((oid) => {
51
+ return decomposeOid(oid).collection === collection;
52
+ }),
53
+ ...docsWithUnappliedMigrations.filter((oid) => {
54
+ return decomposeOid(oid).collection === collection;
55
+ }),
56
+ );
57
+
58
+ const snapshots = await Promise.all(
59
+ oids.map(async (oid) => {
60
+ try {
61
+ const snap = await ctx.meta.getDocumentSnapshot(oid);
62
+ return [oid, snap];
63
+ } catch (e) {
64
+ // this seems to happen with baselines/ops which are not fully
65
+ // cleaned up after deletion?
66
+ ctx.log(
67
+ 'error',
68
+ 'Could not regenerate snapshot during migration for oid',
69
+ oid,
70
+ 'this document will not be preserved',
71
+ e,
72
+ );
73
+ return null;
74
+ }
75
+ }),
76
+ );
77
+
78
+ const views: [string, any][] = snapshots.filter(
79
+ (s: any): s is [string, any] => !!s,
80
+ );
81
+
82
+ // now we can write the documents back
83
+ await documents.saveEntities(
84
+ views.map(([oid, snapshot]) => ({
85
+ oid,
86
+ getSnapshot() {
87
+ return snapshot;
88
+ },
89
+ })),
90
+ {
91
+ collections: [collection],
92
+ },
93
+ );
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Gets a list of root OIDs for all documents which had operations stored already
99
+ * that were not applied to their queryable snapshots because they were in the
100
+ * future. These documents need to be refreshed in storage.
101
+ */
102
+ async function getDocsWithUnappliedMigrations({
103
+ currentVersion,
104
+ newVersion: _,
105
+ ctx,
106
+ }: {
107
+ currentVersion: number;
108
+ newVersion: number;
109
+ ctx: OpenDocumentDbContext;
110
+ }) {
111
+ // scan for all operations in metadata after the current version.
112
+ // this could be more efficient if also filtering below or equal newVersion but
113
+ // that seems so unlikely in practice...
114
+ const unappliedOperations: ClientOperation[] = [];
115
+ await ctx.meta.iterateAllOperations(
116
+ (op) => {
117
+ unappliedOperations.push(op);
118
+ },
119
+ {
120
+ from: ctx.time.zeroWithVersion(currentVersion + 1),
121
+ },
122
+ );
123
+ return Array.from(
124
+ new Set(unappliedOperations.map((op) => getOidRoot(op.oid))),
125
+ );
126
+ }
@@ -0,0 +1,169 @@
1
+ import { Migration } from '@verdant-web/common';
2
+ import { getMigrationPath } from './paths.js';
3
+ import { OpenDocumentDbContext } from './types.js';
4
+ import { getMigrationEngine } from './engine.js';
5
+ import { finalizeMigration } from './finalize.js';
6
+ import { PersistenceNamespace } from '../interfaces.js';
7
+ import { ShutdownHandler } from '../../context/ShutdownHandler.js';
8
+
9
+ export async function migrate({
10
+ context,
11
+ version,
12
+ }: {
13
+ context: OpenDocumentDbContext;
14
+ version: number;
15
+ }) {
16
+ const ns = await context.persistence.openNamespace(
17
+ context.namespace,
18
+ context,
19
+ );
20
+ await acquireLock(context.namespace, async () => {
21
+ const currentVersion = await context.persistence.getNamespaceVersion(
22
+ context.namespace,
23
+ );
24
+
25
+ context.log(
26
+ 'debug',
27
+ 'Opening index database',
28
+ context.namespace,
29
+ 'Current database version:',
30
+ currentVersion,
31
+ 'target version:',
32
+ version,
33
+ context.schema.wip ? '(wip)' : '',
34
+ );
35
+
36
+ const toRun = getMigrationPath({
37
+ currentVersion,
38
+ targetVersion: version,
39
+ migrations: context.migrations,
40
+ });
41
+
42
+ if (toRun.length > 0) {
43
+ context.log(
44
+ 'debug',
45
+ 'Migrations to run:',
46
+ toRun.map((m) => m.version),
47
+ );
48
+ await runMigrations({ context, ns, toRun });
49
+ }
50
+ });
51
+ }
52
+
53
+ async function acquireLock(namespace: string, procedure: () => Promise<void>) {
54
+ if (typeof navigator !== 'undefined' && navigator.locks) {
55
+ await navigator.locks.request(`verdant_migration_${namespace}`, procedure);
56
+ } else {
57
+ // TODO: is there a fallback?
58
+ await procedure();
59
+ }
60
+ }
61
+
62
+ export async function runMigrations({
63
+ context,
64
+ toRun,
65
+ ns,
66
+ }: {
67
+ context: OpenDocumentDbContext;
68
+ toRun: Migration<any>[];
69
+ ns: PersistenceNamespace;
70
+ }) {
71
+ // disable rebasing for the duration of migrations
72
+ context.pauseRebasing = true;
73
+ // now the fun part
74
+ for (const migration of toRun) {
75
+ context.log(
76
+ 'info',
77
+ `🚀 Running migration v${migration.oldSchema.version} -> v${migration.newSchema.version}`,
78
+ );
79
+ const migrationContext = {
80
+ ...context,
81
+ schema: migration.oldSchema,
82
+ shutdownHandler: new ShutdownHandler(),
83
+ };
84
+ // this will only write to our metadata store via operations!
85
+ const engine = await getMigrationEngine({
86
+ migration,
87
+ context: migrationContext,
88
+ ns,
89
+ });
90
+ try {
91
+ context.log(
92
+ 'debug',
93
+ 'Migrating data',
94
+ migrationContext.namespace,
95
+ 'from version',
96
+ migration.oldSchema.version,
97
+ 'to version',
98
+ migration.newSchema.version,
99
+ );
100
+ await migration.migrate(engine);
101
+ // wait on any out-of-band async operations to complete
102
+ await Promise.all(engine.awaitables);
103
+ } catch (err) {
104
+ context.log(
105
+ 'critical',
106
+ `Migration failed (${migration.oldSchema.version} -> ${migration.newSchema.version})`,
107
+ err,
108
+ );
109
+ if (err instanceof Error) {
110
+ throw err;
111
+ } else {
112
+ throw new Error('Unknown error during migration');
113
+ }
114
+ }
115
+
116
+ await engine.close();
117
+
118
+ migrationContext.log(
119
+ 'debug',
120
+ 'Upgrading database',
121
+ migrationContext.namespace,
122
+ 'from version',
123
+ migrationContext.schema.version,
124
+ 'to version',
125
+ migration.newSchema.version,
126
+ );
127
+
128
+ await ns.applyMigration(migrationContext, migration);
129
+
130
+ // switch to the new schema
131
+ migrationContext.schema = migration.newSchema;
132
+ const upgradedDocuments = await ns.openDocuments(migrationContext);
133
+
134
+ await finalizeMigration({
135
+ ctx: migrationContext,
136
+ migration,
137
+ engine,
138
+ documents: upgradedDocuments,
139
+ });
140
+ await upgradedDocuments.close();
141
+
142
+ migrationContext.log(
143
+ 'debug',
144
+ `Migration of ${migrationContext.namespace} complete.`,
145
+ );
146
+ migrationContext.log(
147
+ 'info',
148
+ `
149
+ ⬆️ v${migration.newSchema.version} Migration complete. Here's the rundown:
150
+ - Added collections: ${migration.addedCollections.join(', ')}
151
+ - Removed collections: ${migration.removedCollections.join(', ')}
152
+ - Changed collections: ${migration.changedCollections.join(', ')}
153
+ - New indexes: ${Object.keys(migration.addedIndexes)
154
+ .map((col) =>
155
+ migration.addedIndexes[col].map((i) => `${col}.${i.name}`),
156
+ )
157
+ .flatMap((i) => i)
158
+ .join(', ')}
159
+ - Removed indexes: ${Object.keys(migration.removedIndexes)
160
+ .map((col) =>
161
+ migration.removedIndexes[col].map((i) => `${col}.${i.name}`),
162
+ )
163
+ .flatMap((i) => i)
164
+ .join(', ')}
165
+ `,
166
+ );
167
+ }
168
+ context.pauseRebasing = false;
169
+ }
@@ -0,0 +1,4 @@
1
+ import { Context } from '../../context/context.js';
2
+
3
+ /** During migration, only a partial context is available */
4
+ export type OpenDocumentDbContext = Omit<Context, 'documents' | 'files'>;
@@ -4,7 +4,9 @@ import { getWipNamespace } from '../utils/wip.js';
4
4
  import { ExportedData } from './interfaces.js';
5
5
  import { PersistenceFiles } from './PersistenceFiles.js';
6
6
  import { PersistenceMetadata } from './PersistenceMetadata.js';
7
- import { PersistenceQueries } from './PersistenceQueries.js';
7
+ import { PersistenceDocuments } from './PersistenceQueries.js';
8
+ import { migrate } from './migration/migrate.js';
9
+ import { ShutdownHandler } from '../context/ShutdownHandler.js';
8
10
 
9
11
  export async function initializePersistence(
10
12
  ctx: InitialContext,
@@ -27,6 +29,7 @@ export async function initializePersistence(
27
29
 
28
30
  if (currentVersion === 0) {
29
31
  // there is no existing data. nothing to copy.
32
+ context.log('debug', 'No existing data to copy to WIP namespace');
30
33
  } else {
31
34
  const currentSchema = ctx.oldSchemas?.find(
32
35
  (s) => s.version === currentVersion,
@@ -45,6 +48,9 @@ export async function initializePersistence(
45
48
  await context.persistence.copyNamespace(
46
49
  context.originalNamespace,
47
50
  context.namespace,
51
+ // needs to be the original schema; the copy should be of the original
52
+ // data and schema structure; the WIP schema migration application happens
53
+ // below.
48
54
  {
49
55
  ...context,
50
56
  schema: currentSchema,
@@ -54,21 +60,32 @@ export async function initializePersistence(
54
60
  }
55
61
  }
56
62
 
63
+ const namespace = await ctx.persistence.openNamespace(
64
+ context.namespace,
65
+ context,
66
+ );
67
+
57
68
  context.log('info', 'Opening persistence metadata');
58
69
  context.meta = new PersistenceMetadata(
59
- await ctx.persistence.openMetadata(ctx),
70
+ await namespace.openMetadata(ctx),
60
71
  ctx,
61
72
  );
62
73
 
63
74
  context.log('info', 'Opening persistence files');
64
75
  context.files = new PersistenceFiles(
65
- await ctx.persistence.openFiles(context),
76
+ await namespace.openFiles(context),
66
77
  context,
67
78
  );
68
79
 
69
- context.log('info', 'Opening persistence queries');
70
- context.queries = new PersistenceQueries(
71
- await ctx.persistence.openQueries(context),
80
+ context.log('info', 'Migrating document database');
81
+ await migrate({
82
+ context,
83
+ version: ctx.schema.version,
84
+ });
85
+
86
+ context.log('info', 'Opening persistence documents');
87
+ context.documents = new PersistenceDocuments(
88
+ await namespace.openDocuments(context),
72
89
  context,
73
90
  );
74
91
 
@@ -120,6 +137,7 @@ export async function importPersistence(
120
137
  disableRebasing: true,
121
138
  },
122
139
  },
140
+ persistenceShutdownHandler: new ShutdownHandler(),
123
141
  });
124
142
  // load imported data into persistence
125
143
  await importedContext.meta.resetFrom(exportedData.data);
@@ -140,40 +158,42 @@ export async function importPersistence(
140
158
  };
141
159
  }),
142
160
  );
143
- await importedContext.queries.saveEntities(toSave);
161
+ await importedContext.documents.saveEntities(toSave);
144
162
  await importedContext.files.import(exportedData);
145
163
 
146
164
  ctx.log('debug', 'Imported data into temporary namespace', importedNamespace);
147
165
 
148
166
  // shut down the imported databases
149
- await importedContext.queries.dispose();
150
- await importedContext.meta.dispose();
151
- await importedContext.files.dispose();
167
+ await importedContext.persistenceShutdownHandler.shutdown();
152
168
 
153
169
  if (exportedSchema.version !== ctx.schema.version) {
154
170
  // an upgrade of the imported data is needed ; it's an older version
155
171
  // of the schema.
156
- ctx.log('debug', 'Shut down imported databases');
157
172
 
158
173
  // upgrade the imported data to the latest schema
159
174
  const currentSchema = ctx.schema;
160
175
  const upgradedContext = await initializePersistence({
161
176
  ...importedContext,
177
+ persistenceShutdownHandler: new ShutdownHandler(),
162
178
  schema: currentSchema,
163
179
  });
164
180
 
165
181
  ctx.log('debug', 'Upgraded imported data to current schema');
166
182
 
167
- await upgradedContext.queries.dispose();
168
- await upgradedContext.meta.dispose();
169
- await upgradedContext.files.dispose();
183
+ await upgradedContext.persistenceShutdownHandler.shutdown();
170
184
 
171
185
  ctx.log('debug', 'Shut down upgraded databases');
172
186
  }
173
187
 
188
+ // shut down the persistence layer
189
+ await ctx.persistenceShutdownHandler.shutdown();
190
+
174
191
  // copy the imported data into the current namespace
175
192
  await ctx.persistence.copyNamespace(importedNamespace, ctx.namespace, ctx);
176
193
 
194
+ // restart the persistence layer
195
+ await initializePersistence(ctx);
196
+
177
197
  // verify integrity -- this can only be done if imported data was same
178
198
  // version as current schema, because migrations could add or remove
179
199
  // operations. still, it's a good sanity check.
@@ -220,4 +240,7 @@ export async function importPersistence(
220
240
 
221
241
  ctx.internalEvents.emit('persistenceReset');
222
242
  ctx.log('info', 'Data imported successfully');
243
+
244
+ // reset to allow future shutdowns.
245
+ ctx.persistenceShutdownHandler.reset();
223
246
  }
@@ -23,7 +23,7 @@ export class FindAllQuery<T> extends BaseQuery<T[]> {
23
23
  }
24
24
 
25
25
  protected run = async () => {
26
- const { result: oids } = await this.context.queries.findAllOids({
26
+ const { result: oids } = await this.context.documents.findAllOids({
27
27
  collection: this.collection,
28
28
  index: this.index,
29
29
  });
@@ -37,7 +37,7 @@ export class FindInfiniteQuery<T> extends BaseQuery<T[]> {
37
37
  }
38
38
 
39
39
  protected run = async () => {
40
- const { result, hasNextPage } = await this.context.queries.findAllOids({
40
+ const { result, hasNextPage } = await this.context.documents.findAllOids({
41
41
  collection: this.collection,
42
42
  limit: this._pageSize * this._upToPage,
43
43
  offset: 0,
@@ -48,7 +48,7 @@ export class FindInfiniteQuery<T> extends BaseQuery<T[]> {
48
48
  };
49
49
 
50
50
  public loadMore = async () => {
51
- const { result, hasNextPage } = await this.context.queries.findAllOids({
51
+ const { result, hasNextPage } = await this.context.documents.findAllOids({
52
52
  collection: this.collection,
53
53
  limit: this._pageSize,
54
54
  offset: this._pageSize * this._upToPage,
@@ -23,7 +23,7 @@ export class FindOneQuery<T> extends BaseQuery<T | null> {
23
23
  }
24
24
 
25
25
  protected run = async () => {
26
- const oid = await this.context.queries.findOneOid({
26
+ const oid = await this.context.documents.findOneOid({
27
27
  collection: this.collection,
28
28
  index: this.index,
29
29
  });
@@ -48,7 +48,7 @@ export class FindPageQuery<T> extends BaseQuery<T[]> {
48
48
  }
49
49
 
50
50
  protected run = async () => {
51
- const { result, hasNextPage } = await this.context.queries.findAllOids({
51
+ const { result, hasNextPage } = await this.context.documents.findAllOids({
52
52
  collection: this.collection,
53
53
  index: this.index,
54
54
  limit: this._pageSize,
@@ -42,7 +42,7 @@ export class FileSync extends Disposable {
42
42
  this.ctx.log('debug', 'Uploading file', data.id, data.name);
43
43
  try {
44
44
  await this.uploadFile(data);
45
- this.ctx.internalEvents.emit(`fileUploaded:${data.id}`);
45
+ this.ctx.internalEvents.emit(`fileUploaded:${data.id}`, data);
46
46
  } catch (e) {
47
47
  this.ctx.log('error', 'File upload failed', e);
48
48
  }
@@ -70,14 +70,17 @@ export class FileSync extends Disposable {
70
70
  formData.append('file', file);
71
71
 
72
72
  try {
73
- const response = await fetch(fileEndpoint + `/${data.id}`, {
74
- method: 'POST',
75
- body: formData,
76
- credentials: 'include',
77
- headers: {
78
- Authorization: `Bearer ${token}`,
73
+ const response = await this.ctx.environment.fetch(
74
+ fileEndpoint + `/${data.id}`,
75
+ {
76
+ method: 'POST',
77
+ body: formData,
78
+ credentials: 'include',
79
+ headers: {
80
+ Authorization: `Bearer ${token}`,
81
+ },
79
82
  },
80
- });
83
+ );
81
84
 
82
85
  if (response.ok) {
83
86
  this.ctx.log('info', 'File upload successful');
@@ -132,14 +135,17 @@ export class FileSync extends Disposable {
132
135
  await this.endpointProvider.getEndpoints();
133
136
 
134
137
  try {
135
- const response = await fetch(fileEndpoint + `/${id}`, {
136
- method: 'GET',
137
- credentials: 'include',
138
- headers: {
139
- 'Content-Type': 'application/json',
140
- Authorization: `Bearer ${token}`,
138
+ const response = await this.ctx.environment.fetch(
139
+ fileEndpoint + `/${id}`,
140
+ {
141
+ method: 'GET',
142
+ credentials: 'include',
143
+ headers: {
144
+ 'Content-Type': 'application/json',
145
+ Authorization: `Bearer ${token}`,
146
+ },
141
147
  },
142
- });
148
+ );
143
149
 
144
150
  if (response.ok) {
145
151
  const data = await response.json();
@@ -20,7 +20,9 @@ export class PushPullSync
20
20
  readonly presence: PresenceManager;
21
21
  private endpointProvider;
22
22
  private heartbeat;
23
- private fetch;
23
+ private get fetch() {
24
+ return this.ctx.environment.fetch;
25
+ }
24
26
 
25
27
  readonly mode = 'pull';
26
28
  private ctx;
@@ -33,20 +35,17 @@ export class PushPullSync
33
35
  endpointProvider,
34
36
  presence,
35
37
  interval = 15 * 1000,
36
- fetch = window.fetch.bind(window),
37
38
  ctx,
38
39
  }: {
39
40
  endpointProvider: ServerSyncEndpointProvider;
40
41
  presence: PresenceManager;
41
42
  interval?: number;
42
- fetch?: typeof window.fetch;
43
43
  ctx: Context;
44
44
  }) {
45
45
  super();
46
46
  this.ctx = ctx;
47
47
  this.presence = presence;
48
48
  this.endpointProvider = endpointProvider;
49
- this.fetch = fetch;
50
49
 
51
50
  this.heartbeat = new Heartbeat({
52
51
  interval,