@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/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?(knownGoodVersion: R, newRecord: unknown): R;
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
- type SerializedSchema = SerializedSchemaV2;
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
- /** The set of ids of every record of `typeName`. Maintained incrementally. */
373
- ids<T extends R["typeName"]>(typeName: T): Computed<ReadonlySet<IdOf<RecordFromTypeName<R, T>>>>;
374
- /** Every record of `typeName`, in insertion order. */
375
- records<T extends R["typeName"]>(typeName: T): Computed<RecordFromTypeName<R, T>[]>;
376
- /** The first record of `typeName` matching `predicate` (or the first record, when omitted). */
377
- record<T extends R["typeName"]>(typeName: T, predicate?: (record: RecordFromTypeName<R, T>) => boolean): Computed<RecordFromTypeName<R, T> | undefined>;
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, predicate: (record: RecordFromTypeName<R, T>) => boolean): RecordFromTypeName<R, 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
- /** Bumped once per completed operation that changed something. */
399
- readonly history: Atom<number>;
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(): SerializedSchema;
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(): SerializedSchema;
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
- export { type BaseRecord, type ChangeSource, type EphemeralKeys, type HistoryEntry, type IdOf, type IndexKey, type Migration, type MigrationId, type MigrationResult, type MigrationSequence, type ParseTldrFileResult, type RecordCreateProps, type RecordDataKeys, type RecordFromId, type RecordFromTypeName, type RecordId, type RecordMigration, type RecordScope, RecordType, type RecordTypeConfig, type RecordTypeMap, type RecordsDiff, type SerializedSchema, type SerializedSchemaV2, type SerializedStore, Store, type StoreAfterChangeHandler, type StoreAfterCreateHandler, type StoreAfterDeleteHandler, type StoreBeforeChangeHandler, type StoreBeforeCreateHandler, type StoreBeforeDeleteHandler, type StoreListener, type StoreListenerFilters, type StoreMigration, type StoreOperationCompleteHandler, type StoreOptions, StoreQueries, type StoreRecord, StoreSchema, type StoreSchemaOptions, type StoreSideEffectHandlers, StoreSideEffects, type StoreSnapshot, type StoreValidationFailure, type StoreValidationPhase, type StoreValidator, TLDR_FILE_FORMAT_VERSION, type TldrFile, type TldrFileParseError, UNIQUE_ID_LENGTH, type UnknownRecord, ZERO_INDEX_KEY, ZKEY_SIGNIFICANT_DIGITS, type ZKey, applyChangeToDiff, applyMigrationToStore, applyRecordMigration, cloneRecordsDiff, compareIndexKeys, compareZKeys, createEmptyRecordsDiff, createMigrationIds, createMigrationSequence, createRecordMigrationSequence, createRecordType, freezeRecord, getIndexAbove, getIndexBelow, getIndexBetween, getIndices, getIndicesAbove, getIndicesBelow, getIndicesBetween, indexKeyToZKey, isIndexKey, isRecordLike, isRecordShallowEqual, isRecordsDiffEmpty, parseMigrationId, parseRecordId, parseTldrFile, reverseRecordsDiff, serializeTldrFile, sortByIndex, squashRecordDiffs, squashRecordDiffsMutable, storeSnapshotToTldrFile, tldrFileToStoreSnapshot, uniqueId, validateIndexKey, zKeyToBigInt };
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 };