@mocanvas/store 1.0.0 → 4.0.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.
- package/ARCHITECTURE.md +421 -0
- package/BENCHMARK.md +519 -0
- package/CLEAN_ROOM.md +50 -0
- package/COMPAT.md +282 -0
- package/CUSTOM_SHAPES.md +880 -0
- package/LICENSE +110 -16
- package/MIGRATION.md +807 -0
- package/README.md +24 -4
- package/UI.md +256 -0
- package/dist/index.d.ts +484 -15
- package/dist/index.js +416 -22
- package/dist/index.js.map +1 -1
- package/package.json +12 -14
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Computed, Atom } from '@mocanvas/state';
|
|
2
|
+
export { AtomMap, AtomSet } from '@mocanvas/state';
|
|
2
3
|
|
|
3
4
|
/** Length of the unique part of a generated record id. */
|
|
4
5
|
declare const UNIQUE_ID_LENGTH = 21;
|
|
@@ -33,8 +34,13 @@ interface StoreValidator<R extends UnknownRecord> {
|
|
|
33
34
|
* Optional fast path: validate `newRecord` knowing that `knownGoodVersion`
|
|
34
35
|
* is a valid record of the same type. Implementations may skip the parts
|
|
35
36
|
* that did not change.
|
|
37
|
+
*
|
|
38
|
+
* Declared as a property that may be `undefined` rather than as an optional
|
|
39
|
+
* method, so that a validator carrying an explicit `undefined` — which is
|
|
40
|
+
* what a class with an optional constructor argument produces — still
|
|
41
|
+
* satisfies this under `exactOptionalPropertyTypes`.
|
|
36
42
|
*/
|
|
37
|
-
validateUsingKnownGoodVersion
|
|
43
|
+
validateUsingKnownGoodVersion?: ((knownGoodVersion: R, newRecord: unknown) => R) | undefined;
|
|
38
44
|
}
|
|
39
45
|
/** Keys of `R` that hold data (everything except `id` and `typeName`). */
|
|
40
46
|
type RecordDataKeys<R extends UnknownRecord> = Exclude<keyof R, "id" | "typeName">;
|
|
@@ -184,6 +190,101 @@ declare function squashRecordDiffs<R extends UnknownRecord>(diffs: readonly Reco
|
|
|
184
190
|
/** Shallow-copy a diff (entries are shared). */
|
|
185
191
|
declare function cloneRecordsDiff<R extends UnknownRecord>(diff: RecordsDiff<R>): RecordsDiff<R>;
|
|
186
192
|
|
|
193
|
+
/**
|
|
194
|
+
* The pre-sequence migration format, and the reasons a migration can fail.
|
|
195
|
+
*
|
|
196
|
+
* Before migrations were named sequences, a schema declared one integer version
|
|
197
|
+
* per record type plus one for the store, and a table of numbered up/down
|
|
198
|
+
* functions between them. mocanvas never *writes* that format — {@link
|
|
199
|
+
* SerializedSchemaV1} exists so a document saved by something that did can
|
|
200
|
+
* still be recognised and loaded, and so a schema written against the old shape
|
|
201
|
+
* can still be described.
|
|
202
|
+
*
|
|
203
|
+
* Nothing here changes what mocanvas persists. See `StoreSchema.serialize`,
|
|
204
|
+
* which always produces the v2 shape.
|
|
205
|
+
*/
|
|
206
|
+
/**
|
|
207
|
+
* The persisted schema shape used before migration sequences: a version number
|
|
208
|
+
* per record type, and one for the store as a whole.
|
|
209
|
+
*
|
|
210
|
+
* Read-only as far as mocanvas is concerned. A snapshot carrying one is treated
|
|
211
|
+
* as knowing none of today's sequences, so every retroactive sequence runs from
|
|
212
|
+
* the beginning — which is right, because none of them existed when the file
|
|
213
|
+
* was written.
|
|
214
|
+
*/
|
|
215
|
+
interface SerializedSchemaV1 {
|
|
216
|
+
schemaVersion: 1;
|
|
217
|
+
storeVersion: number;
|
|
218
|
+
recordVersions: Record<string, {
|
|
219
|
+
version: number;
|
|
220
|
+
} | {
|
|
221
|
+
version: number;
|
|
222
|
+
subTypeVersions: Record<string, number>;
|
|
223
|
+
subTypeKey: string;
|
|
224
|
+
}>;
|
|
225
|
+
}
|
|
226
|
+
/** Whether a persisted schema is in the pre-sequence format. */
|
|
227
|
+
declare function isSerializedSchemaV1(schema: {
|
|
228
|
+
schemaVersion: number;
|
|
229
|
+
}): schema is SerializedSchemaV1;
|
|
230
|
+
/** One numbered step of a {@link LegacyMigrations} table. */
|
|
231
|
+
interface LegacyMigration<Before = any, After = any> {
|
|
232
|
+
up: (oldState: Before) => After;
|
|
233
|
+
down: (newState: After) => Before;
|
|
234
|
+
}
|
|
235
|
+
/** The version bounds every legacy migration table declares. */
|
|
236
|
+
interface LegacyBaseMigrationsInfo {
|
|
237
|
+
firstVersion: number;
|
|
238
|
+
currentVersion: number;
|
|
239
|
+
migrators: {
|
|
240
|
+
[version: number]: LegacyMigration;
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* A legacy migration table: the version range it covers, the numbered steps,
|
|
245
|
+
* and optionally the sub-type split a record type used (a shape's `type`, say,
|
|
246
|
+
* each with its own version line).
|
|
247
|
+
*/
|
|
248
|
+
interface LegacyMigrations extends LegacyBaseMigrationsInfo {
|
|
249
|
+
subTypeKey?: string;
|
|
250
|
+
subTypeMigrations?: Record<string, LegacyBaseMigrationsInfo>;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* A dependency declared by a standalone migration sequence: it must run after
|
|
254
|
+
* (or before) another sequence's numbered migration, even though neither owns
|
|
255
|
+
* the other.
|
|
256
|
+
*
|
|
257
|
+
* Ordering between sequences is otherwise registration order, which is fine
|
|
258
|
+
* until one sequence's `up` reads a field another sequence is still about to
|
|
259
|
+
* add.
|
|
260
|
+
*/
|
|
261
|
+
interface StandaloneDependsOn {
|
|
262
|
+
dependsOn: readonly string[];
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Why loading a persisted snapshot failed.
|
|
266
|
+
*
|
|
267
|
+
* These are the cases worth telling apart in a UI: "this file is from a newer
|
|
268
|
+
* version of the app" is a message a user can act on, and
|
|
269
|
+
* "migrationError" is not.
|
|
270
|
+
*/
|
|
271
|
+
declare const MigrationFailureReason: {
|
|
272
|
+
/** The persisted schema names a sequence version higher than this schema knows. */
|
|
273
|
+
readonly TargetVersionTooNew: "target-version-too-new";
|
|
274
|
+
/** The persisted data is older than the oldest migration that survives. */
|
|
275
|
+
readonly TargetVersionTooOld: "target-version-too-old";
|
|
276
|
+
/** A record's type is not registered in this schema and cannot be migrated. */
|
|
277
|
+
readonly UnrecognizedType: "unrecognized-type";
|
|
278
|
+
/** A migration function threw. */
|
|
279
|
+
readonly MigrationError: "migration-error";
|
|
280
|
+
/** The persisted schema itself is malformed. */
|
|
281
|
+
readonly IncompatibleSubtype: "incompatible-subtype";
|
|
282
|
+
/** The persisted schema version is not one this store understands. */
|
|
283
|
+
readonly UnknownSchemaVersion: "unknown-schema-version";
|
|
284
|
+
};
|
|
285
|
+
/** One of the {@link MigrationFailureReason} values. */
|
|
286
|
+
type MigrationFailureReason = (typeof MigrationFailureReason)[keyof typeof MigrationFailureReason];
|
|
287
|
+
|
|
187
288
|
/** Serialized records keyed by id. */
|
|
188
289
|
type SerializedStore<R extends UnknownRecord> = Record<IdOf<R>, R>;
|
|
189
290
|
/**
|
|
@@ -196,7 +297,15 @@ interface SerializedSchemaV2 {
|
|
|
196
297
|
[sequenceId: string]: number;
|
|
197
298
|
};
|
|
198
299
|
}
|
|
199
|
-
|
|
300
|
+
/**
|
|
301
|
+
* A persisted schema, in either format mocanvas can read.
|
|
302
|
+
*
|
|
303
|
+
* Only {@link SerializedSchemaV2} is ever *written* — see
|
|
304
|
+
* `StoreSchema.serialize`. The v1 arm is here so a document saved by an older,
|
|
305
|
+
* pre-sequence writer is still loadable rather than being rejected as
|
|
306
|
+
* "unsupported schema version".
|
|
307
|
+
*/
|
|
308
|
+
type SerializedSchema = SerializedSchemaV1 | SerializedSchemaV2;
|
|
200
309
|
type MigrationId = `${string}/${number}`;
|
|
201
310
|
interface RecordMigration {
|
|
202
311
|
readonly id: MigrationId;
|
|
@@ -273,6 +382,75 @@ declare function applyRecordMigration(migration: RecordMigration, record: Unknow
|
|
|
273
382
|
/** Apply one migration (record- or store-scoped) to a whole store in place. */
|
|
274
383
|
declare function applyMigrationToStore(migration: Migration, store: SerializedStore<UnknownRecord>, direction: "up" | "down"): SerializedStore<UnknownRecord>;
|
|
275
384
|
|
|
385
|
+
/**
|
|
386
|
+
* What changed in a set: the members added and the members removed.
|
|
387
|
+
*
|
|
388
|
+
* Either side may be absent, which means "nothing on that side" — a diff with
|
|
389
|
+
* neither is a diff that says nothing happened.
|
|
390
|
+
*/
|
|
391
|
+
interface CollectionDiff<T> {
|
|
392
|
+
added?: Set<T>;
|
|
393
|
+
removed?: Set<T>;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* How one property of a record is matched.
|
|
397
|
+
*
|
|
398
|
+
* SEMANTICS-ASSUMED: three comparisons — equality, inequality and a numeric
|
|
399
|
+
* greater-than — are what an index can answer without scanning, which is the
|
|
400
|
+
* whole reason queries are data rather than functions. `gt` is deliberately
|
|
401
|
+
* numeric: the only ordered property records carry is a number.
|
|
402
|
+
*/
|
|
403
|
+
type QueryValueMatcher<T> = {
|
|
404
|
+
eq: T;
|
|
405
|
+
} | {
|
|
406
|
+
neq: T;
|
|
407
|
+
} | {
|
|
408
|
+
gt: number;
|
|
409
|
+
};
|
|
410
|
+
/**
|
|
411
|
+
* A query over the records of one type: property name to matcher, every entry
|
|
412
|
+
* of which must hold (they are ANDed).
|
|
413
|
+
*
|
|
414
|
+
* ```ts
|
|
415
|
+
* store.query.records("shape", () => ({ type: { eq: "geo" }, isLocked: { eq: false } }))
|
|
416
|
+
* ```
|
|
417
|
+
*/
|
|
418
|
+
type QueryExpression<R extends object> = {
|
|
419
|
+
[K in keyof R]?: QueryValueMatcher<R[K]>;
|
|
420
|
+
};
|
|
421
|
+
/**
|
|
422
|
+
* An index over one property of one record type: for each value that property
|
|
423
|
+
* takes, the ids of the records that hold it.
|
|
424
|
+
*/
|
|
425
|
+
type RSIndexMap<R extends UnknownRecord, Property extends keyof R & string = keyof R & string> = Map<R[Property], Set<IdOf<R>>>;
|
|
426
|
+
/** How an {@link RSIndexMap} changed: per property value, which ids joined and left. */
|
|
427
|
+
type RSIndexDiff<R extends UnknownRecord, Property extends keyof R & string = keyof R & string> = Map<R[Property], CollectionDiff<IdOf<R>>>;
|
|
428
|
+
/**
|
|
429
|
+
* A live index over one property, as a diff-carrying signal.
|
|
430
|
+
*
|
|
431
|
+
* Reading it gives the current {@link RSIndexMap}; asking it for the diffs
|
|
432
|
+
* since a past epoch gives {@link RSIndexDiff}s that describe how to get from
|
|
433
|
+
* the old map to the new one.
|
|
434
|
+
*/
|
|
435
|
+
type RSIndex<R extends UnknownRecord, Property extends keyof R & string = keyof R & string> = Computed<RSIndexMap<R, Property>, RSIndexDiff<R, Property>>;
|
|
436
|
+
/** Whether `value` satisfies `matcher`. */
|
|
437
|
+
declare function matchesQueryValue<T>(matcher: QueryValueMatcher<T>, value: T): boolean;
|
|
438
|
+
/** Whether `record` satisfies every entry of `query`. An empty query matches everything. */
|
|
439
|
+
declare function matchesQuery<R extends object>(query: QueryExpression<R>, record: R): boolean;
|
|
440
|
+
/**
|
|
441
|
+
* The property this query can be answered from an index on, or `undefined`
|
|
442
|
+
* when none of it is indexable.
|
|
443
|
+
*
|
|
444
|
+
* Only an `eq` clause narrows to a single index bucket; `neq` and `gt` still
|
|
445
|
+
* need every bucket looked at, so they are no better than a scan. The first
|
|
446
|
+
* `eq` wins — the remaining clauses are checked against the records it yields.
|
|
447
|
+
*/
|
|
448
|
+
declare function getIndexablePropertyOf<R extends object>(query: QueryExpression<R>): (keyof R & string) | undefined;
|
|
449
|
+
/** Apply a {@link CollectionDiff} to a set, in place. */
|
|
450
|
+
declare function applyCollectionDiff<T>(set: Set<T>, diff: CollectionDiff<T>): Set<T>;
|
|
451
|
+
/** Whether a {@link CollectionDiff} describes no change at all. */
|
|
452
|
+
declare function isCollectionDiffEmpty<T>(diff: CollectionDiff<T>): boolean;
|
|
453
|
+
|
|
276
454
|
type ChangeSource = "user" | "remote";
|
|
277
455
|
interface HistoryEntry<R extends UnknownRecord> {
|
|
278
456
|
changes: RecordsDiff<R>;
|
|
@@ -287,6 +465,38 @@ type RecordFromTypeName<R extends UnknownRecord, T extends string> = Extract<R,
|
|
|
287
465
|
typeName: T;
|
|
288
466
|
}>;
|
|
289
467
|
type StoreRecord<S extends Store<any, any>> = S extends Store<infer R, any> ? R : never;
|
|
468
|
+
/**
|
|
469
|
+
* Anything that owns a store: a `Store` itself, or an object holding one (an
|
|
470
|
+
* `Editor`). Written for the helpers that want to accept either without their
|
|
471
|
+
* callers having to reach for `.store`.
|
|
472
|
+
*/
|
|
473
|
+
type StoreObject<R extends UnknownRecord = UnknownRecord> = Store<R, any> | {
|
|
474
|
+
store: Store<R, any>;
|
|
475
|
+
};
|
|
476
|
+
/** The record union of whatever store a {@link StoreObject} carries. */
|
|
477
|
+
type StoreObjectRecordType<Context extends StoreObject<any>> = Context extends Store<infer R, any> ? R : Context extends {
|
|
478
|
+
store: Store<infer R, any>;
|
|
479
|
+
} ? R : never;
|
|
480
|
+
/** A validator per record type, as `StoreSchema` collects them from the record types. */
|
|
481
|
+
type StoreValidators<R extends UnknownRecord> = {
|
|
482
|
+
[TypeName in R["typeName"]]: StoreValidator<Extract<R, {
|
|
483
|
+
typeName: TypeName;
|
|
484
|
+
}>>;
|
|
485
|
+
};
|
|
486
|
+
/**
|
|
487
|
+
* A record that failed validation, with enough context to say what was being
|
|
488
|
+
* done to it at the time.
|
|
489
|
+
*
|
|
490
|
+
* Thrown rather than returned: a store that keeps going after writing an
|
|
491
|
+
* invalid record is a store whose next save produces a file nothing can load.
|
|
492
|
+
*/
|
|
493
|
+
interface StoreError {
|
|
494
|
+
error: Error;
|
|
495
|
+
phase: "initialize" | "createRecord" | "updateRecord" | "tests";
|
|
496
|
+
recordBefore?: unknown;
|
|
497
|
+
recordAfter: unknown;
|
|
498
|
+
isExistingValidationIssue: boolean;
|
|
499
|
+
}
|
|
290
500
|
interface StoreOptions<R extends UnknownRecord, Props> {
|
|
291
501
|
schema: StoreSchema<R, Props>;
|
|
292
502
|
initialData?: SerializedStore<R> | undefined;
|
|
@@ -364,19 +574,61 @@ interface TypeIndex<R extends UnknownRecord> {
|
|
|
364
574
|
readonly epoch: Atom<number>;
|
|
365
575
|
}
|
|
366
576
|
/** Reactive views over the store's records, cached per type name. */
|
|
577
|
+
/**
|
|
578
|
+
* How the records of one type are narrowed: a predicate, or a declarative
|
|
579
|
+
* {@link QueryExpression} the store can answer from an index.
|
|
580
|
+
*
|
|
581
|
+
* Prefer the query object. A predicate has to be run against every record of
|
|
582
|
+
* the type; a query with an `eq` clause is answered from an index bucket.
|
|
583
|
+
*/
|
|
584
|
+
type StoreQueryFilter<Rec extends UnknownRecord> = ((record: Rec) => boolean) | QueryExpression<Rec>;
|
|
367
585
|
declare class StoreQueries<R extends UnknownRecord> {
|
|
368
586
|
private readonly store;
|
|
369
587
|
private readonly idsCache;
|
|
370
588
|
private readonly recordsCache;
|
|
589
|
+
private readonly indexCache;
|
|
590
|
+
private readonly historyCache;
|
|
371
591
|
constructor(store: Store<R, any>);
|
|
372
|
-
/**
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
592
|
+
/**
|
|
593
|
+
* The set of ids of every record of `typeName`, optionally narrowed by a
|
|
594
|
+
* filter. Maintained incrementally.
|
|
595
|
+
*/
|
|
596
|
+
ids<T extends R["typeName"]>(typeName: T, filter?: StoreQueryFilter<RecordFromTypeName<R, T>>): Computed<ReadonlySet<IdOf<RecordFromTypeName<R, T>>>>;
|
|
597
|
+
/**
|
|
598
|
+
* Every record of `typeName`, in insertion order, optionally narrowed by a
|
|
599
|
+
* filter.
|
|
600
|
+
*
|
|
601
|
+
* A {@link QueryExpression} with an `eq` clause is answered from the index on
|
|
602
|
+
* that property, so a page with ten thousand shapes does not have to be
|
|
603
|
+
* walked to find the twelve on one frame.
|
|
604
|
+
*/
|
|
605
|
+
records<T extends R["typeName"]>(typeName: T, filter?: StoreQueryFilter<RecordFromTypeName<R, T>>): Computed<RecordFromTypeName<R, T>[]>;
|
|
606
|
+
private filteredRecords;
|
|
607
|
+
/** The first record of `typeName` matching `filter` (or the first record, when omitted). */
|
|
608
|
+
record<T extends R["typeName"]>(typeName: T, filter?: StoreQueryFilter<RecordFromTypeName<R, T>>): Computed<RecordFromTypeName<R, T> | undefined>;
|
|
378
609
|
/** Non-reactive filter over the records of `typeName`. */
|
|
379
|
-
exec<T extends R["typeName"]>(typeName: T,
|
|
610
|
+
exec<T extends R["typeName"]>(typeName: T, filter: StoreQueryFilter<RecordFromTypeName<R, T>>): RecordFromTypeName<R, T>[];
|
|
611
|
+
/**
|
|
612
|
+
* A live index from the values of one property to the ids of the records
|
|
613
|
+
* holding them.
|
|
614
|
+
*
|
|
615
|
+
* The index is cached per type and property, and it carries diffs: a
|
|
616
|
+
* dependent that already built something from it can ask
|
|
617
|
+
* `index.getDiffSince(epoch)` and patch, instead of walking the whole map
|
|
618
|
+
* again. That is what makes "every shape whose parentId is this frame" cheap
|
|
619
|
+
* enough to recompute on every pointer move.
|
|
620
|
+
*/
|
|
621
|
+
index<T extends R["typeName"], Property extends keyof RecordFromTypeName<R, T> & string>(typeName: T, property: Property): RSIndex<RecordFromTypeName<R, T>, Property>;
|
|
622
|
+
private buildIndex;
|
|
623
|
+
/**
|
|
624
|
+
* The store's history, narrowed to one record type.
|
|
625
|
+
*
|
|
626
|
+
* Its *value* is only a counter — what it is for is the diffs it carries.
|
|
627
|
+
* `filterHistory("shape").getDiffSince(epoch)` is every change to shapes
|
|
628
|
+
* since `epoch`, with changes to other record types dropped, which is how a
|
|
629
|
+
* derived collection stays incremental without re-reading the store.
|
|
630
|
+
*/
|
|
631
|
+
filterHistory<T extends R["typeName"]>(typeName: T): Computed<number, RecordsDiff<RecordFromTypeName<R, T>>>;
|
|
380
632
|
}
|
|
381
633
|
/**
|
|
382
634
|
* A reactive, transactional collection of records.
|
|
@@ -395,8 +647,16 @@ declare class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|
|
395
647
|
};
|
|
396
648
|
readonly sideEffects: StoreSideEffects<R>;
|
|
397
649
|
readonly query: StoreQueries<R>;
|
|
398
|
-
/**
|
|
399
|
-
|
|
650
|
+
/**
|
|
651
|
+
* Bumped once per completed operation that changed something.
|
|
652
|
+
*
|
|
653
|
+
* The counter itself carries no information; the diffs do. The atom keeps a
|
|
654
|
+
* bounded history of the squashed {@link RecordsDiff} of each operation, so a
|
|
655
|
+
* derived collection can ask `history.getDiffSince(epoch)` and patch itself
|
|
656
|
+
* instead of rebuilding. `store.query.filterHistory(typeName)` is the same
|
|
657
|
+
* thing narrowed to one record type.
|
|
658
|
+
*/
|
|
659
|
+
readonly history: Atom<number, RecordsDiff<R>>;
|
|
400
660
|
private readonly records;
|
|
401
661
|
private readonly typeIndexes;
|
|
402
662
|
private readonly listeners;
|
|
@@ -407,6 +667,12 @@ declare class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|
|
407
667
|
private runCallbacks;
|
|
408
668
|
private inOperationComplete;
|
|
409
669
|
private disposed;
|
|
670
|
+
/**
|
|
671
|
+
* The diff of the operation currently being committed, handed to the history
|
|
672
|
+
* atom's `computeDiff` as it is written. The atom only sees two counter
|
|
673
|
+
* values, so the diff has to be staged here for the one write that follows.
|
|
674
|
+
*/
|
|
675
|
+
private pendingHistoryDiff;
|
|
410
676
|
constructor(options: StoreOptions<R, Props>);
|
|
411
677
|
/** @internal */
|
|
412
678
|
getTypeIndex(typeName: string): TypeIndex<R>;
|
|
@@ -462,6 +728,22 @@ declare class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|
|
462
728
|
/** Plain-object snapshot of the records in `scope` (default `document`). */
|
|
463
729
|
serialize(scope?: RecordScope | "all"): SerializedStore<R>;
|
|
464
730
|
getStoreSnapshot(scope?: RecordScope | "all"): StoreSnapshot<R>;
|
|
731
|
+
/**
|
|
732
|
+
* Bring a snapshot saved by an older document up to this store's schema,
|
|
733
|
+
* without loading it.
|
|
734
|
+
*
|
|
735
|
+
* Every migration sequence the schema knows is run — including the ones
|
|
736
|
+
* `createStore` derives from the shape and binding utils, so a board saved
|
|
737
|
+
* before a prop existed is backfilled here rather than failing validation on
|
|
738
|
+
* load. The input is not mutated: the result is a new snapshot carrying this
|
|
739
|
+
* schema's serialized version, ready for {@link Store.loadStoreSnapshot} (or
|
|
740
|
+
* for a caller that wants to inspect the migrated records first).
|
|
741
|
+
*
|
|
742
|
+
* A snapshot that cannot be migrated — an unknown schema version, a sequence
|
|
743
|
+
* from a NEWER build than this one, a migration that throws — raises rather
|
|
744
|
+
* than returning half-migrated data, so a caller can fail closed on it.
|
|
745
|
+
*/
|
|
746
|
+
migrateSnapshot(snapshot: StoreSnapshot<R>): StoreSnapshot<R>;
|
|
465
747
|
/**
|
|
466
748
|
* Replace the store's contents with a snapshot (migrating it first).
|
|
467
749
|
* Existing records in `document` scope and in every scope present in the
|
|
@@ -484,6 +766,8 @@ declare class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|
|
484
766
|
private removeFromIndex;
|
|
485
767
|
private recordChange;
|
|
486
768
|
private completeOperation;
|
|
769
|
+
/** One diff describing everything the operation just committed changed. */
|
|
770
|
+
private squashPendingEntries;
|
|
487
771
|
private flushHistory;
|
|
488
772
|
private filterDiffByScope;
|
|
489
773
|
}
|
|
@@ -537,16 +821,17 @@ declare class StoreSchema<R extends UnknownRecord, Props = unknown> {
|
|
|
537
821
|
* a load/save round-trip.
|
|
538
822
|
*/
|
|
539
823
|
validateRecord(store: Store<R, any>, record: R, phase: StoreValidationPhase, recordBefore: R | undefined): R;
|
|
540
|
-
/** The current version of every sequence. */
|
|
541
|
-
serialize():
|
|
824
|
+
/** The current version of every sequence. Always the v2 shape — mocanvas never writes v1. */
|
|
825
|
+
serialize(): SerializedSchemaV2;
|
|
542
826
|
/** A schema at version 0 of every sequence (all migrations still pending). */
|
|
543
|
-
serializeEarliestVersion():
|
|
827
|
+
serializeEarliestVersion(): SerializedSchemaV2;
|
|
544
828
|
/**
|
|
545
829
|
* The migrations that must run to bring data saved under `persistedSchema`
|
|
546
830
|
* up to this schema, in order. Sequences the persisted schema knows but we
|
|
547
831
|
* do not are ignored with a warning.
|
|
548
832
|
*/
|
|
549
833
|
getMigrationsSince(persistedSchema: SerializedSchema): MigrationResult<Migration[]>;
|
|
834
|
+
private migrationsSince;
|
|
550
835
|
/**
|
|
551
836
|
* Migrate a single record. Only record-scoped migrations can be applied;
|
|
552
837
|
* encountering a store-scoped one is an error. `down` runs the migrations
|
|
@@ -604,4 +889,188 @@ declare function storeSnapshotToTldrFile(snapshot: {
|
|
|
604
889
|
schema: SerializedSchema;
|
|
605
890
|
}): string;
|
|
606
891
|
|
|
607
|
-
|
|
892
|
+
/**
|
|
893
|
+
* Standalone per-record memos.
|
|
894
|
+
*
|
|
895
|
+
* `Store.createComputedCache` already memoizes a value per record, but it is a
|
|
896
|
+
* method: the cache belongs to one store instance, and the derivation cannot see
|
|
897
|
+
* anything else. A shape util or binding util usually needs the opposite shape —
|
|
898
|
+
* one cache declared once at module scope, derived from a record *and* the editor
|
|
899
|
+
* that owns it, and shared by every store that editor drives.
|
|
900
|
+
*
|
|
901
|
+
* `createComputedCache` is that form. The context is passed in at `get` time and
|
|
902
|
+
* the underlying per-record cache is created lazily, once per context.
|
|
903
|
+
*/
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* A context a computed cache can read a store from: a store itself, or anything
|
|
907
|
+
* holding one (an `Editor`).
|
|
908
|
+
*/
|
|
909
|
+
type ComputedCacheContext = Store<any, any> | {
|
|
910
|
+
readonly store: Store<any, any>;
|
|
911
|
+
};
|
|
912
|
+
/** The handle returned by {@link createComputedCache}. */
|
|
913
|
+
interface ComputedCache<Context, R extends UnknownRecord, Result> {
|
|
914
|
+
/** The derived value for `id`, or `undefined` if no such record exists. */
|
|
915
|
+
get(context: Context, id: IdOf<R>): Result | undefined;
|
|
916
|
+
}
|
|
917
|
+
/** Options for {@link createComputedCache}. */
|
|
918
|
+
interface CreateComputedCacheOptions<Result> {
|
|
919
|
+
/**
|
|
920
|
+
* Treat two derived values as the same, so dependents are not woken when the
|
|
921
|
+
* derivation recomputes to an equivalent result.
|
|
922
|
+
*/
|
|
923
|
+
isEqual?: ((a: Result, b: Result) => boolean) | undefined;
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Declare a per-record memo, keyed by record id, recomputed only when that
|
|
927
|
+
* record changes.
|
|
928
|
+
*
|
|
929
|
+
* ```ts
|
|
930
|
+
* const bindingsCache = createComputedCache("connection bindings", (editor: Editor, shape: TLShape) =>
|
|
931
|
+
* editor.getBindingsFromShape(shape.id, "connection"),
|
|
932
|
+
* )
|
|
933
|
+
* bindingsCache.get(editor, shapeId)
|
|
934
|
+
* ```
|
|
935
|
+
*
|
|
936
|
+
* SEMANTICS-ASSUMED: `Context` is unconstrained rather than bound to
|
|
937
|
+
* {@link ComputedCacheContext}. The consumer annotates the derivation's own
|
|
938
|
+
* parameter (`(editor: Editor, shape: TLShape) => …`) and that is what `get`
|
|
939
|
+
* must accept; constraining the type parameter as well would force every caller
|
|
940
|
+
* to prove `Editor` is structurally a store holder at each call site for no
|
|
941
|
+
* added safety. The store is resolved at `get` time instead, and a context that
|
|
942
|
+
* carries none throws immediately rather than silently returning `undefined`.
|
|
943
|
+
*/
|
|
944
|
+
declare function createComputedCache<Context, R extends UnknownRecord, Result>(name: string, derive: (context: Context, record: R) => Result, options?: CreateComputedCacheOpts<Result, R>): ComputedCache<Context, R, Result>;
|
|
945
|
+
/**
|
|
946
|
+
* Options for {@link createComputedCache}, including the record-level equality
|
|
947
|
+
* that decides when a derivation is worth re-running at all.
|
|
948
|
+
*/
|
|
949
|
+
type CreateComputedCacheOpts<Result, R extends UnknownRecord = UnknownRecord> = CreateComputedCacheOptions<Result> & {
|
|
950
|
+
/**
|
|
951
|
+
* Treat two versions of the *record* as the same, so the derivation is not
|
|
952
|
+
* re-run when only parts it does not read have changed.
|
|
953
|
+
*
|
|
954
|
+
* `isEqual` compares results and can only save the dependents work;
|
|
955
|
+
* `areRecordsEqual` compares inputs and saves the derivation itself. Use it
|
|
956
|
+
* when the derivation is expensive and reads only a couple of fields —
|
|
957
|
+
* geometry from `props`, say, which should not be rebuilt because the shape
|
|
958
|
+
* moved.
|
|
959
|
+
*/
|
|
960
|
+
areRecordsEqual?: ((a: R, b: R) => boolean) | undefined;
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Two small guards the store leans on: a development-only deep freeze, and an
|
|
965
|
+
* id assertion that narrows.
|
|
966
|
+
*/
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Deep-freeze `object` in development builds, and hand it straight back in
|
|
970
|
+
* production.
|
|
971
|
+
*
|
|
972
|
+
* Records in the store are shared by reference with every consumer that read
|
|
973
|
+
* them, so mutating one in place skips the whole change pipeline: no diff, no
|
|
974
|
+
* side effects, no listeners, and an undo that silently does nothing. Freezing
|
|
975
|
+
* turns that from a bug someone finds a week later into a `TypeError` on the
|
|
976
|
+
* line that did it. The check is skipped in production because freezing every
|
|
977
|
+
* record on every write is not free.
|
|
978
|
+
*
|
|
979
|
+
* Already-frozen objects are left alone, so re-freezing a record that came out
|
|
980
|
+
* of the store costs one property read.
|
|
981
|
+
*/
|
|
982
|
+
declare function devFreeze<T>(object: T): T;
|
|
983
|
+
/**
|
|
984
|
+
* Assert that `id` belongs to `type`, narrowing it to that type's id.
|
|
985
|
+
*
|
|
986
|
+
* Record ids are branded strings, so the compiler already stops most mix-ups —
|
|
987
|
+
* but ids that arrive from outside the program (a URL, a saved file, a sync
|
|
988
|
+
* message) are plain strings that someone has to vouch for. This is the place
|
|
989
|
+
* to do that vouching: it throws with the offending id rather than letting a
|
|
990
|
+
* `page:` id be looked up as a shape and quietly returning `undefined`.
|
|
991
|
+
*
|
|
992
|
+
* ```ts
|
|
993
|
+
* assertIdType(idFromUrl, PageRecordType)
|
|
994
|
+
* editor.setCurrentPage(idFromUrl) // now typed as TLPageId
|
|
995
|
+
* ```
|
|
996
|
+
*/
|
|
997
|
+
declare function assertIdType<R extends UnknownRecord>(id: string | undefined, type: RecordType<R, any>): asserts id is IdOf<R>;
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* The synchronous storage contract a store can be backed by.
|
|
1001
|
+
*
|
|
1002
|
+
* Deliberately synchronous and deliberately tiny: it is the shape an embedded
|
|
1003
|
+
* key-value store (a SQLite table, a `Map`, an in-process test double) already
|
|
1004
|
+
* has, so a host can hand one over without writing an adapter. Anything
|
|
1005
|
+
* asynchronous — a network, IndexedDB — belongs behind a snapshot load and save
|
|
1006
|
+
* rather than behind this.
|
|
1007
|
+
*/
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Somewhere records can be read from and written to, one at a time, without
|
|
1011
|
+
* awaiting.
|
|
1012
|
+
*
|
|
1013
|
+
* Implementations must be consistent within a call: `getAll` reflects every
|
|
1014
|
+
* `set` and `delete` that has already returned.
|
|
1015
|
+
*/
|
|
1016
|
+
interface SynchronousRecordStorage<R extends UnknownRecord = UnknownRecord> {
|
|
1017
|
+
/** The record stored under `id`, or `undefined`. */
|
|
1018
|
+
get(id: IdOf<R>): R | undefined;
|
|
1019
|
+
/** Every stored record. Order is not significant. */
|
|
1020
|
+
getAll(): R[];
|
|
1021
|
+
/** Store `record` under its own id, replacing anything already there. */
|
|
1022
|
+
set(record: R): void;
|
|
1023
|
+
/** Remove the record stored under `id`. Removing an absent id is not an error. */
|
|
1024
|
+
delete(id: IdOf<R>): void;
|
|
1025
|
+
/** Remove every record. */
|
|
1026
|
+
clear(): void;
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Record storage that also remembers the schema its records were written
|
|
1030
|
+
* against, so they can be migrated when they are read back.
|
|
1031
|
+
*
|
|
1032
|
+
* Storing records without their schema is the one mistake that cannot be
|
|
1033
|
+
* recovered from later: there is no way to tell which migrations have already
|
|
1034
|
+
* run, and re-running them corrupts the data.
|
|
1035
|
+
*/
|
|
1036
|
+
interface SynchronousStorage<R extends UnknownRecord = UnknownRecord> extends SynchronousRecordStorage<R> {
|
|
1037
|
+
/** The schema the stored records were written against, or `undefined` when empty. */
|
|
1038
|
+
getSchema(): SerializedSchema | undefined;
|
|
1039
|
+
/** Record the schema the stored records are written against. */
|
|
1040
|
+
setSchema(schema: SerializedSchema): void;
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* A {@link SynchronousStorage} backed by a plain `Map`.
|
|
1044
|
+
*
|
|
1045
|
+
* Useful in tests and as the reference implementation of the contract — the
|
|
1046
|
+
* shortest correct answer to "what does a storage have to do?".
|
|
1047
|
+
*/
|
|
1048
|
+
declare function createInMemoryStorage<R extends UnknownRecord = UnknownRecord>(): SynchronousStorage<R>;
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Grapheme cluster iteration.
|
|
1052
|
+
*
|
|
1053
|
+
* "One character" as a person sees it is a grapheme cluster, not a UTF-16 code
|
|
1054
|
+
* unit and not a code point: `👩👩👧` is one, `é` written as `e` + a combining
|
|
1055
|
+
* accent is one, and a flag emoji is one. Anything that measures, truncates or
|
|
1056
|
+
* steps through text a character at a time has to walk clusters or it will cut a
|
|
1057
|
+
* family emoji in half.
|
|
1058
|
+
*/
|
|
1059
|
+
/**
|
|
1060
|
+
* Iterate the grapheme clusters of `str`.
|
|
1061
|
+
*
|
|
1062
|
+
* Uses `Intl.Segmenter` where it exists and falls back to code-point iteration
|
|
1063
|
+
* otherwise — which keeps surrogate pairs intact (so a plain emoji survives) but
|
|
1064
|
+
* cannot join a ZWJ sequence or a combining mark to its base.
|
|
1065
|
+
*
|
|
1066
|
+
* ```ts
|
|
1067
|
+
* [...iterateGraphemes("a👍🏽b")] // ["a", "👍🏽", "b"]
|
|
1068
|
+
* ```
|
|
1069
|
+
*/
|
|
1070
|
+
declare function iterateGraphemes(str: string): Generator<string, void, undefined>;
|
|
1071
|
+
/** The grapheme clusters of `str`, as an array. */
|
|
1072
|
+
declare function getGraphemes(str: string): string[];
|
|
1073
|
+
/** How many grapheme clusters `str` has — its length as a reader would count it. */
|
|
1074
|
+
declare function getGraphemeLength(str: string): number;
|
|
1075
|
+
|
|
1076
|
+
export { type BaseRecord, type ChangeSource, type CollectionDiff, type ComputedCache, type ComputedCacheContext, type CreateComputedCacheOptions, type CreateComputedCacheOpts, type EphemeralKeys, type HistoryEntry, type IdOf, type IndexKey, type LegacyBaseMigrationsInfo, type LegacyMigration, type LegacyMigrations, type Migration, MigrationFailureReason, type MigrationId, type MigrationResult, type MigrationSequence, type ParseTldrFileResult, type QueryExpression, type QueryValueMatcher, type RSIndex, type RSIndexDiff, type RSIndexMap, type RecordCreateProps, type RecordDataKeys, type RecordFromId, type RecordFromTypeName, type RecordId, type RecordMigration, type RecordScope, RecordType, type RecordTypeConfig, type RecordTypeMap, type RecordsDiff, type SerializedSchema, type SerializedSchemaV1, type SerializedSchemaV2, type SerializedStore, type StandaloneDependsOn, Store, type StoreAfterChangeHandler, type StoreAfterCreateHandler, type StoreAfterDeleteHandler, type StoreBeforeChangeHandler, type StoreBeforeCreateHandler, type StoreBeforeDeleteHandler, type StoreError, type StoreListener, type StoreListenerFilters, type StoreMigration, type StoreObject, type StoreObjectRecordType, type StoreOperationCompleteHandler, type StoreOptions, StoreQueries, type StoreQueryFilter, type StoreRecord, StoreSchema, type StoreSchemaOptions, type StoreSideEffectHandlers, StoreSideEffects, type StoreSnapshot, type StoreValidationFailure, type StoreValidationPhase, type StoreValidator, type StoreValidators, type SynchronousRecordStorage, type SynchronousStorage, TLDR_FILE_FORMAT_VERSION, type TldrFile, type TldrFileParseError, UNIQUE_ID_LENGTH, type UnknownRecord, ZERO_INDEX_KEY, ZKEY_SIGNIFICANT_DIGITS, type ZKey, applyChangeToDiff, applyCollectionDiff, applyMigrationToStore, applyRecordMigration, assertIdType, cloneRecordsDiff, compareIndexKeys, compareZKeys, createComputedCache, createEmptyRecordsDiff, createInMemoryStorage, createMigrationIds, createMigrationSequence, createRecordMigrationSequence, createRecordType, devFreeze, freezeRecord, getGraphemeLength, getGraphemes, getIndexAbove, getIndexBelow, getIndexBetween, getIndexablePropertyOf, getIndices, getIndicesAbove, getIndicesBelow, getIndicesBetween, indexKeyToZKey, isCollectionDiffEmpty, isIndexKey, isRecordLike, isRecordShallowEqual, isRecordsDiffEmpty, isSerializedSchemaV1, iterateGraphemes, matchesQuery, matchesQueryValue, parseMigrationId, parseRecordId, parseTldrFile, reverseRecordsDiff, serializeTldrFile, sortByIndex, squashRecordDiffs, squashRecordDiffsMutable, storeSnapshotToTldrFile, tldrFileToStoreSnapshot, uniqueId, validateIndexKey, zKeyToBigInt };
|