@verdant-web/store 3.11.1 → 3.12.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.
- package/dist/bundle/index.js +8 -8
- package/dist/bundle/index.js.map +4 -4
- package/dist/esm/client/Client.d.ts +9 -1
- package/dist/esm/client/Client.js +102 -23
- package/dist/esm/client/Client.js.map +1 -1
- package/dist/esm/entities/EntityStore.d.ts +6 -0
- package/dist/esm/entities/EntityStore.js +15 -4
- package/dist/esm/entities/EntityStore.js.map +1 -1
- package/dist/esm/entities/types.d.ts +1 -1
- package/dist/esm/metadata/Metadata.d.ts +3 -2
- package/dist/esm/metadata/Metadata.js +14 -5
- package/dist/esm/metadata/Metadata.js.map +1 -1
- package/dist/esm/queries/QueryCache.d.ts +1 -0
- package/dist/esm/queries/QueryCache.js +3 -0
- package/dist/esm/queries/QueryCache.js.map +1 -1
- package/dist/esm/utils/versions.d.ts +5 -0
- package/dist/esm/utils/versions.js +16 -0
- package/dist/esm/utils/versions.js.map +1 -0
- package/package.json +2 -2
- package/src/client/Client.ts +131 -27
- package/src/entities/EntityStore.ts +22 -5
- package/src/entities/types.ts +1 -1
- package/src/metadata/Metadata.ts +20 -10
- package/src/queries/QueryCache.ts +4 -0
- package/src/utils/versions.ts +23 -0
package/src/client/Client.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
debounce,
|
|
3
3
|
DocumentBaseline,
|
|
4
4
|
EventSubscriber,
|
|
5
|
+
getTimestampSchemaVersion,
|
|
5
6
|
Migration,
|
|
6
7
|
Operation,
|
|
7
8
|
} from '@verdant-web/common';
|
|
@@ -15,11 +16,16 @@ import {
|
|
|
15
16
|
deleteAllDatabases,
|
|
16
17
|
getSizeOfObjectStore,
|
|
17
18
|
} from '../idb.js';
|
|
18
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
ExportData,
|
|
21
|
+
Metadata,
|
|
22
|
+
supportLegacyExport,
|
|
23
|
+
} from '../metadata/Metadata.js';
|
|
19
24
|
import { openQueryDatabase } from '../migration/openQueryDatabase.js';
|
|
20
25
|
import { CollectionQueries } from '../queries/CollectionQueries.js';
|
|
21
26
|
import { QueryCache } from '../queries/QueryCache.js';
|
|
22
27
|
import { NoSync, ServerSync, ServerSyncOptions, Sync } from '../sync/Sync.js';
|
|
28
|
+
import { getLatestVersion } from '../utils/versions.js';
|
|
23
29
|
|
|
24
30
|
interface ClientConfig<Presence = any> {
|
|
25
31
|
syncConfig?: ServerSyncOptions<Presence>;
|
|
@@ -50,6 +56,12 @@ export class Client<Presence = any, Profile = any> extends EventSubscriber<{
|
|
|
50
56
|
* been offline for too long and reconnects.
|
|
51
57
|
*/
|
|
52
58
|
resetToServer: () => void;
|
|
59
|
+
/**
|
|
60
|
+
* These are errors that, as a developer, you should subscribe to
|
|
61
|
+
* and prompt users to contact you for resolution. Usually these errors
|
|
62
|
+
* indicate the client is in an unrecoverable state.
|
|
63
|
+
*/
|
|
64
|
+
developerError: (err: Error) => void;
|
|
53
65
|
}> {
|
|
54
66
|
readonly meta: Metadata;
|
|
55
67
|
private _entities: EntityStore;
|
|
@@ -104,14 +116,14 @@ export class Client<Presence = any, Profile = any> extends EventSubscriber<{
|
|
|
104
116
|
config: this.config.files,
|
|
105
117
|
meta: this.meta,
|
|
106
118
|
});
|
|
119
|
+
this._queryCache = new QueryCache({
|
|
120
|
+
context,
|
|
121
|
+
});
|
|
107
122
|
this._entities = new EntityStore({
|
|
108
123
|
ctx: this.context,
|
|
109
124
|
meta: this.meta,
|
|
110
125
|
files: this._fileManager,
|
|
111
126
|
});
|
|
112
|
-
this._queryCache = new QueryCache({
|
|
113
|
-
context,
|
|
114
|
-
});
|
|
115
127
|
this._documentManager = new DocumentManager(this.schema, this._entities);
|
|
116
128
|
|
|
117
129
|
const notifyFutureSeen = debounce(() => {
|
|
@@ -122,16 +134,7 @@ export class Client<Presence = any, Profile = any> extends EventSubscriber<{
|
|
|
122
134
|
this.emit('resetToServer');
|
|
123
135
|
});
|
|
124
136
|
|
|
125
|
-
this.
|
|
126
|
-
this.context.log?.(
|
|
127
|
-
'warn',
|
|
128
|
-
`Another tab has requested a version change for ${this.namespace}`,
|
|
129
|
-
);
|
|
130
|
-
this.documentDb.close();
|
|
131
|
-
if (typeof window !== 'undefined') {
|
|
132
|
-
window.location.reload();
|
|
133
|
-
}
|
|
134
|
-
});
|
|
137
|
+
this.watchForVersionChange();
|
|
135
138
|
|
|
136
139
|
this.metaDb.addEventListener('versionchange', () => {
|
|
137
140
|
this.context.log?.(
|
|
@@ -160,12 +163,81 @@ export class Client<Presence = any, Profile = any> extends EventSubscriber<{
|
|
|
160
163
|
}
|
|
161
164
|
}
|
|
162
165
|
|
|
163
|
-
private
|
|
166
|
+
private watchForVersionChange = () => {
|
|
167
|
+
this.documentDb.addEventListener('versionchange', () => {
|
|
168
|
+
this.context.log?.(
|
|
169
|
+
'warn',
|
|
170
|
+
`Another tab has requested a version change for ${this.namespace}`,
|
|
171
|
+
);
|
|
172
|
+
this.documentDb.close();
|
|
173
|
+
if (typeof window !== 'undefined') {
|
|
174
|
+
window.location.reload();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
private importingPromise = Promise.resolve();
|
|
180
|
+
private addData = async (data: {
|
|
164
181
|
operations: Operation[];
|
|
165
182
|
baselines?: DocumentBaseline[];
|
|
166
183
|
reset?: boolean;
|
|
167
184
|
}) => {
|
|
168
|
-
|
|
185
|
+
// always wait for an ongoing import to complete before handling data.
|
|
186
|
+
await this.importingPromise;
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const schemaVersion = data.reset
|
|
190
|
+
? getLatestVersion(data)
|
|
191
|
+
: this.schema.version;
|
|
192
|
+
|
|
193
|
+
if (schemaVersion < this.schema.version) {
|
|
194
|
+
/**
|
|
195
|
+
* Edge case: the server has an older version of the library
|
|
196
|
+
* than the client schema, but it wants the client to reset.
|
|
197
|
+
*
|
|
198
|
+
* This happens when a truant or new client loads up newest client
|
|
199
|
+
* code with a new schema version, but the last sync to the
|
|
200
|
+
* server was from an old version. It's particularly a problem
|
|
201
|
+
* if the new schema drops collections, since the IDB table for
|
|
202
|
+
* that collection will no longer exist, so loading in old data
|
|
203
|
+
* will result in an error.
|
|
204
|
+
*
|
|
205
|
+
* To handle this, we treat the reset data as if it were an import
|
|
206
|
+
* of exported data. The import procedure handles older
|
|
207
|
+
* schema versions by resetting the database to the imported
|
|
208
|
+
* version, then migrating up to the current version.
|
|
209
|
+
*/
|
|
210
|
+
this.context.log(
|
|
211
|
+
'warn',
|
|
212
|
+
'Incoming reset sync data is from an old schema version',
|
|
213
|
+
schemaVersion,
|
|
214
|
+
`(current ${this.schema.version})`,
|
|
215
|
+
);
|
|
216
|
+
// run through the import flow to properly handle old versions
|
|
217
|
+
return await this.import({
|
|
218
|
+
data: {
|
|
219
|
+
operations: data.operations,
|
|
220
|
+
baselines: data.baselines ?? [],
|
|
221
|
+
// keep existing
|
|
222
|
+
localReplica: undefined,
|
|
223
|
+
schemaVersion,
|
|
224
|
+
},
|
|
225
|
+
fileData: [],
|
|
226
|
+
files: [],
|
|
227
|
+
});
|
|
228
|
+
} else {
|
|
229
|
+
return await this._entities.addData(data);
|
|
230
|
+
}
|
|
231
|
+
} catch (err) {
|
|
232
|
+
this.context.log('critical', 'Sync failed', err);
|
|
233
|
+
this.emit(
|
|
234
|
+
'developerError',
|
|
235
|
+
new Error('Sync failed, see logs or cause', {
|
|
236
|
+
cause: err,
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
169
241
|
};
|
|
170
242
|
|
|
171
243
|
get documentDb() {
|
|
@@ -335,7 +407,7 @@ export class Client<Presence = any, Profile = any> extends EventSubscriber<{
|
|
|
335
407
|
};
|
|
336
408
|
|
|
337
409
|
import = async ({
|
|
338
|
-
data,
|
|
410
|
+
data: rawData,
|
|
339
411
|
fileData,
|
|
340
412
|
files,
|
|
341
413
|
}: {
|
|
@@ -343,6 +415,27 @@ export class Client<Presence = any, Profile = any> extends EventSubscriber<{
|
|
|
343
415
|
fileData: Array<Omit<ReturnedFileData, 'file'>>;
|
|
344
416
|
files: File[];
|
|
345
417
|
}) => {
|
|
418
|
+
/**
|
|
419
|
+
* Importing is a pretty involved procedure because of the possibility of
|
|
420
|
+
* importing an export from an older version of the schema. We can't add
|
|
421
|
+
* data from older schemas because the indexes may have changed or whole
|
|
422
|
+
* collections may have been since deleted, leaving no corresponding IDB
|
|
423
|
+
* tables.
|
|
424
|
+
*
|
|
425
|
+
* Since IDB doesn't allow us to go backwards, and we are resetting all
|
|
426
|
+
* data anyways, the import procedure blows away the current queryable DB
|
|
427
|
+
* and restarts from the imported schema version. It then migrates up
|
|
428
|
+
* to the latest (current) version. These migrations are added to the imported
|
|
429
|
+
* data to produce the final state.
|
|
430
|
+
*/
|
|
431
|
+
|
|
432
|
+
// register importing promise to halt other data handling
|
|
433
|
+
let resolve = () => {};
|
|
434
|
+
this.importingPromise = new Promise<void>((res) => {
|
|
435
|
+
resolve = res;
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const data = supportLegacyExport(rawData);
|
|
346
439
|
this.context.log('info', 'Importing data...');
|
|
347
440
|
// close the document DB
|
|
348
441
|
await closeDatabase(this.context.documentDb);
|
|
@@ -366,7 +459,7 @@ export class Client<Presence = any, Profile = any> extends EventSubscriber<{
|
|
|
366
459
|
await this._fileManager.importAll(importedFiles);
|
|
367
460
|
// now delete the document DB, open it to the specified version
|
|
368
461
|
// and run migrations to get it to the latest version
|
|
369
|
-
const version = data.
|
|
462
|
+
const version = data.schemaVersion;
|
|
370
463
|
const deleteReq = indexedDB.deleteDatabase(
|
|
371
464
|
[this.namespace, 'collections'].join('_'),
|
|
372
465
|
);
|
|
@@ -377,16 +470,18 @@ export class Client<Presence = any, Profile = any> extends EventSubscriber<{
|
|
|
377
470
|
// reset our context to the imported schema for now
|
|
378
471
|
const currentSchema = this.context.schema;
|
|
379
472
|
if (currentSchema.version !== version) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
// a "live" schema for that version. the client does not currently
|
|
383
|
-
// receive historical schemas, although they should be available
|
|
384
|
-
// if the CLI was used.
|
|
385
|
-
// importing from older versions is also tricky because
|
|
386
|
-
// migration shortcuts mean that versions could get marooned.
|
|
387
|
-
throw new Error(
|
|
388
|
-
`Only exports from the current schema version can be imported`,
|
|
473
|
+
const oldSchema = this.context.oldSchemas?.find(
|
|
474
|
+
(s) => s.version === version,
|
|
389
475
|
);
|
|
476
|
+
if (!oldSchema) {
|
|
477
|
+
this.emit(
|
|
478
|
+
'developerError',
|
|
479
|
+
new Error(`Could not find schema for version ${version}`),
|
|
480
|
+
);
|
|
481
|
+
throw new Error(`Could not find schema for version ${version}`);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
this.context.schema = oldSchema;
|
|
390
485
|
}
|
|
391
486
|
// now open the document DB empty at the specified version
|
|
392
487
|
// and initialize it from the meta DB
|
|
@@ -417,6 +512,15 @@ export class Client<Presence = any, Profile = any> extends EventSubscriber<{
|
|
|
417
512
|
version: currentSchema.version,
|
|
418
513
|
});
|
|
419
514
|
this.context.internalEvents.emit('documentDbChanged', this.documentDb);
|
|
515
|
+
// re-establish watcher on database
|
|
516
|
+
this.watchForVersionChange();
|
|
517
|
+
|
|
518
|
+
// finally... clear out memory cache of entities and
|
|
519
|
+
// re-run all active queries.
|
|
520
|
+
this.entities.clearCache();
|
|
521
|
+
this._queryCache.forceRefreshAll();
|
|
522
|
+
|
|
523
|
+
resolve();
|
|
420
524
|
};
|
|
421
525
|
|
|
422
526
|
/**
|
|
@@ -490,6 +490,20 @@ export class EntityStore extends Disposable {
|
|
|
490
490
|
entity: Entity,
|
|
491
491
|
opts?: { abort: AbortSignal },
|
|
492
492
|
): Promise<Entity | null> => {
|
|
493
|
+
await this.loadEntityData(entity, opts);
|
|
494
|
+
|
|
495
|
+
// only set the cache after loading.
|
|
496
|
+
// TODO: is this cache/promise stuff redundant?
|
|
497
|
+
this.cache.set(entity.oid, this.ctx.weakRef(entity));
|
|
498
|
+
this.entityFinalizationRegistry.register(entity, entity.oid);
|
|
499
|
+
|
|
500
|
+
return entity;
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
private loadEntityData = async (
|
|
504
|
+
entity: Entity,
|
|
505
|
+
opts?: { abort: AbortSignal },
|
|
506
|
+
) => {
|
|
493
507
|
const { operations, baselines } = await this.meta.getDocumentData(
|
|
494
508
|
entity.oid,
|
|
495
509
|
opts,
|
|
@@ -509,11 +523,14 @@ export class EntityStore extends Disposable {
|
|
|
509
523
|
isLocal: false,
|
|
510
524
|
});
|
|
511
525
|
|
|
512
|
-
// only set the cache after loading.
|
|
513
|
-
// TODO: is this cache/promise stuff redundant?
|
|
514
|
-
this.cache.set(entity.oid, this.ctx.weakRef(entity));
|
|
515
|
-
this.entityFinalizationRegistry.register(entity, entity.oid);
|
|
516
|
-
|
|
517
526
|
return entity;
|
|
518
527
|
};
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Drops all entities from the cache. Any entities
|
|
531
|
+
* referenced will go 'dead'...
|
|
532
|
+
*/
|
|
533
|
+
clearCache = () => {
|
|
534
|
+
this.cache.clear();
|
|
535
|
+
};
|
|
519
536
|
}
|
package/src/entities/types.ts
CHANGED
|
@@ -73,7 +73,7 @@ export interface BaseEntity<
|
|
|
73
73
|
) => void,
|
|
74
74
|
): () => void;
|
|
75
75
|
get<Key extends keyof Value>(key: Key): Value[Key];
|
|
76
|
-
getAll(): Value
|
|
76
|
+
getAll(): Readonly<Value>;
|
|
77
77
|
getSnapshot(): Snapshot;
|
|
78
78
|
getFieldSchema<FieldName extends keyof Value>(
|
|
79
79
|
key: FieldName,
|
package/src/metadata/Metadata.ts
CHANGED
|
@@ -33,8 +33,8 @@ import { Context } from '../context.js';
|
|
|
33
33
|
export interface ExportData {
|
|
34
34
|
operations: Operation[];
|
|
35
35
|
baselines: DocumentBaseline[];
|
|
36
|
-
localReplica
|
|
37
|
-
|
|
36
|
+
localReplica?: LocalReplicaInfo;
|
|
37
|
+
schemaVersion: number;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export class Metadata extends EventSubscriber<{
|
|
@@ -635,7 +635,7 @@ export class Metadata extends EventSubscriber<{
|
|
|
635
635
|
operations,
|
|
636
636
|
baselines,
|
|
637
637
|
localReplica,
|
|
638
|
-
schema,
|
|
638
|
+
schemaVersion: schema.version,
|
|
639
639
|
};
|
|
640
640
|
};
|
|
641
641
|
|
|
@@ -653,13 +653,15 @@ export class Metadata extends EventSubscriber<{
|
|
|
653
653
|
await storeRequestPromise(transaction.objectStore('baselines').clear());
|
|
654
654
|
await storeRequestPromise(transaction.objectStore('operations').clear());
|
|
655
655
|
await storeRequestPromise(transaction.objectStore('info').clear());
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
656
|
+
if (data.localReplica) {
|
|
657
|
+
await this.localReplica.update(
|
|
658
|
+
{
|
|
659
|
+
ackedLogicalTime: data.localReplica.ackedLogicalTime,
|
|
660
|
+
lastSyncedLogicalTime: data.localReplica.lastSyncedLogicalTime,
|
|
661
|
+
},
|
|
662
|
+
{ transaction },
|
|
663
|
+
);
|
|
664
|
+
}
|
|
663
665
|
};
|
|
664
666
|
|
|
665
667
|
stats = async () => {
|
|
@@ -673,3 +675,11 @@ export class Metadata extends EventSubscriber<{
|
|
|
673
675
|
};
|
|
674
676
|
};
|
|
675
677
|
}
|
|
678
|
+
|
|
679
|
+
export function supportLegacyExport(exportData: any): ExportData {
|
|
680
|
+
if (exportData.schema) {
|
|
681
|
+
exportData.schemaVersion = exportData.schema.version;
|
|
682
|
+
delete exportData.schema;
|
|
683
|
+
}
|
|
684
|
+
return exportData;
|
|
685
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DocumentBaseline,
|
|
3
|
+
getTimestampSchemaVersion,
|
|
4
|
+
Operation,
|
|
5
|
+
} from '@verdant-web/common';
|
|
6
|
+
|
|
7
|
+
export function getLatestVersion(data: {
|
|
8
|
+
operations: Operation[];
|
|
9
|
+
baselines?: DocumentBaseline[];
|
|
10
|
+
}) {
|
|
11
|
+
const timestamps = data.operations
|
|
12
|
+
.map((op) => op.timestamp)
|
|
13
|
+
.concat(data.baselines?.map((b) => b.timestamp) ?? []);
|
|
14
|
+
const latestVersion = timestamps.reduce((v, ts) => {
|
|
15
|
+
const tsVersion = getTimestampSchemaVersion(ts);
|
|
16
|
+
if (tsVersion > v) {
|
|
17
|
+
return tsVersion;
|
|
18
|
+
}
|
|
19
|
+
return v;
|
|
20
|
+
}, 0);
|
|
21
|
+
|
|
22
|
+
return latestVersion;
|
|
23
|
+
}
|