@mocanvas/store 1.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/LICENSE +21 -0
- package/README.md +37 -0
- package/dist/index.d.ts +607 -0
- package/dist/index.js +1261 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Symbio Digital
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @mocanvas/store
|
|
2
|
+
|
|
3
|
+
The document model behind [mocanvas](https://github.com/SYMBIO/mocanvas): a
|
|
4
|
+
reactive record store with a typed schema, migrations, record diffs and
|
|
5
|
+
`.tldr` file IO. Built on `@mocanvas/state`; no React.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @mocanvas/store
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Use
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createRecordType, Store, StoreSchema, type BaseRecord, type RecordId } from "@mocanvas/store"
|
|
17
|
+
|
|
18
|
+
interface Book extends BaseRecord<"book", RecordId<Book>> {
|
|
19
|
+
title: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const BookRecord = createRecordType<Book>("book", { scope: "document" })
|
|
23
|
+
const store = new Store({ schema: StoreSchema.create({ book: BookRecord }), props: {} })
|
|
24
|
+
|
|
25
|
+
store.listen(({ changes }) => console.log(changes))
|
|
26
|
+
store.put([BookRecord.create({ title: "Dune" })])
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Records are reactive: `store.query` exposes signals you can read from
|
|
30
|
+
`@mocanvas/state` computeds and React components.
|
|
31
|
+
|
|
32
|
+
ESM only. See the [repository](https://github.com/SYMBIO/mocanvas) for the
|
|
33
|
+
rest of the packages.
|
|
34
|
+
|
|
35
|
+
## License
|
|
36
|
+
|
|
37
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
import { Computed, Atom } from '@mocanvas/state';
|
|
2
|
+
|
|
3
|
+
/** Length of the unique part of a generated record id. */
|
|
4
|
+
declare const UNIQUE_ID_LENGTH = 21;
|
|
5
|
+
/** Generate a URL-safe unique id (21 chars by default). */
|
|
6
|
+
declare function uniqueId(size?: number): string;
|
|
7
|
+
/**
|
|
8
|
+
* A branded string id. The brand carries the record type so that ids of
|
|
9
|
+
* different record types cannot be mixed up at compile time.
|
|
10
|
+
*/
|
|
11
|
+
type RecordId<R extends UnknownRecord> = string & {
|
|
12
|
+
__type__: R;
|
|
13
|
+
};
|
|
14
|
+
/** Every record has an id and a type name. */
|
|
15
|
+
interface BaseRecord<TypeName extends string, Id extends RecordId<UnknownRecord>> {
|
|
16
|
+
readonly id: Id;
|
|
17
|
+
readonly typeName: TypeName;
|
|
18
|
+
}
|
|
19
|
+
type UnknownRecord = BaseRecord<string, RecordId<UnknownRecord>>;
|
|
20
|
+
type IdOf<R extends UnknownRecord> = R["id"];
|
|
21
|
+
/** Extract the record type an id refers to. */
|
|
22
|
+
type RecordFromId<K extends RecordId<UnknownRecord>> = K extends RecordId<infer R> ? R : never;
|
|
23
|
+
/**
|
|
24
|
+
* Where a record lives:
|
|
25
|
+
* - `document`: persisted, shared between collaborators (shapes, pages, ...)
|
|
26
|
+
* - `session`: persisted locally only (camera, current page, ...)
|
|
27
|
+
* - `presence`: shared but never persisted (cursors, ...)
|
|
28
|
+
*/
|
|
29
|
+
type RecordScope = "document" | "session" | "presence";
|
|
30
|
+
interface StoreValidator<R extends UnknownRecord> {
|
|
31
|
+
validate(record: unknown): R;
|
|
32
|
+
/**
|
|
33
|
+
* Optional fast path: validate `newRecord` knowing that `knownGoodVersion`
|
|
34
|
+
* is a valid record of the same type. Implementations may skip the parts
|
|
35
|
+
* that did not change.
|
|
36
|
+
*/
|
|
37
|
+
validateUsingKnownGoodVersion?(knownGoodVersion: R, newRecord: unknown): R;
|
|
38
|
+
}
|
|
39
|
+
/** Keys of `R` that hold data (everything except `id` and `typeName`). */
|
|
40
|
+
type RecordDataKeys<R extends UnknownRecord> = Exclude<keyof R, "id" | "typeName">;
|
|
41
|
+
type EphemeralKeys<R extends UnknownRecord> = {
|
|
42
|
+
readonly [K in RecordDataKeys<R>]: boolean;
|
|
43
|
+
};
|
|
44
|
+
interface RecordTypeConfig<R extends UnknownRecord> {
|
|
45
|
+
readonly scope: RecordScope;
|
|
46
|
+
readonly validator?: StoreValidator<R> | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Keys whose changes should not be considered "real" document changes,
|
|
49
|
+
* e.g. transient UI flags. Used by `Store.applyDiff({ ignoreEphemeralKeys })`.
|
|
50
|
+
*/
|
|
51
|
+
readonly ephemeralKeys?: EphemeralKeys<R> | undefined;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Properties the caller must pass to `create()`: the record's data keys minus
|
|
55
|
+
* whatever `withDefaultProperties` provides. `id` is always optional.
|
|
56
|
+
*/
|
|
57
|
+
type RecordCreateProps<R extends UnknownRecord, RequiredProps extends keyof R> = Pick<R, RequiredProps> & Partial<Omit<R, RequiredProps | "typeName">>;
|
|
58
|
+
/**
|
|
59
|
+
* Describes one kind of record in the store: how to make ids, defaults,
|
|
60
|
+
* validation, and where the record lives (its scope).
|
|
61
|
+
*/
|
|
62
|
+
declare class RecordType<R extends UnknownRecord, RequiredProps extends keyof R = RecordDataKeys<R>> {
|
|
63
|
+
private readonly config;
|
|
64
|
+
readonly typeName: R["typeName"];
|
|
65
|
+
readonly scope: RecordScope;
|
|
66
|
+
readonly validator: StoreValidator<R> | undefined;
|
|
67
|
+
readonly ephemeralKeys: EphemeralKeys<R> | undefined;
|
|
68
|
+
readonly ephemeralKeySet: ReadonlySet<string>;
|
|
69
|
+
constructor(typeName: R["typeName"], config: RecordTypeConfig<R> & {
|
|
70
|
+
readonly createDefaultProperties: () => Partial<Omit<R, "id" | "typeName">>;
|
|
71
|
+
});
|
|
72
|
+
/** Create a new record with defaults applied. A fresh id is generated when none is given. */
|
|
73
|
+
create(properties: RecordCreateProps<R, RequiredProps>): R;
|
|
74
|
+
/** Shallow-clone a record (props/meta are shared). */
|
|
75
|
+
clone(record: R): R;
|
|
76
|
+
/** Make an id of this type: `${typeName}:${uniquePart}`. */
|
|
77
|
+
createId(customUniquePart?: string): IdOf<R>;
|
|
78
|
+
/** Recover the unique part of an id of this type. */
|
|
79
|
+
parseId(id: IdOf<R>): string;
|
|
80
|
+
isId(id?: string): id is IdOf<R>;
|
|
81
|
+
isInstance(record?: unknown): record is R;
|
|
82
|
+
/**
|
|
83
|
+
* Return a new RecordType whose `create()` fills in the given defaults, so
|
|
84
|
+
* those properties become optional for callers.
|
|
85
|
+
*/
|
|
86
|
+
withDefaultProperties<DefaultProps extends RecordDataKeys<R>>(createDefaultProperties: () => Pick<R, DefaultProps>): RecordType<R, Exclude<RequiredProps, DefaultProps>>;
|
|
87
|
+
/** Run the validator (if any). Throws on invalid input. */
|
|
88
|
+
validate(record: unknown, recordBefore?: R): R;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Define a record type.
|
|
92
|
+
*
|
|
93
|
+
* ```ts
|
|
94
|
+
* const Book = createRecordType<Book>('book', { scope: 'document' })
|
|
95
|
+
* .withDefaultProperties(() => ({ inStock: true }))
|
|
96
|
+
* const b = Book.create({ title: 'Dune' }) // -> { id: 'book:...', typeName: 'book', title, inStock }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
declare function createRecordType<R extends UnknownRecord>(typeName: R["typeName"], config: RecordTypeConfig<R>): RecordType<R, RecordDataKeys<R>>;
|
|
100
|
+
/** Split any `${typeName}:${unique}` id into its parts. */
|
|
101
|
+
declare function parseRecordId(id: string): {
|
|
102
|
+
typeName: string;
|
|
103
|
+
uniquePart: string;
|
|
104
|
+
};
|
|
105
|
+
/** Assert that `value` looks like a record: an object with string `id` and `typeName`. */
|
|
106
|
+
declare function isRecordLike(value: unknown): value is UnknownRecord;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A fractional index: an order key that sorts lexicographically (plain string
|
|
110
|
+
* comparison) and between which new keys can always be generated.
|
|
111
|
+
*/
|
|
112
|
+
type IndexKey = string & {
|
|
113
|
+
__brand: "indexKey";
|
|
114
|
+
};
|
|
115
|
+
/** The conventional first key. */
|
|
116
|
+
declare const ZERO_INDEX_KEY: IndexKey;
|
|
117
|
+
/** Generate a key strictly between `below` and `above`; either may be omitted. Throws when `below >= above`. */
|
|
118
|
+
declare function getIndexBetween(below?: IndexKey | undefined, above?: IndexKey | undefined): IndexKey;
|
|
119
|
+
/** Generate a key strictly above `below` (or a first key when omitted). */
|
|
120
|
+
declare function getIndexAbove(below?: IndexKey | undefined): IndexKey;
|
|
121
|
+
/** Generate a key strictly below `above` (or a first key when omitted). */
|
|
122
|
+
declare function getIndexBelow(above?: IndexKey | undefined): IndexKey;
|
|
123
|
+
/** Generate `n` sorted keys strictly between `below` and `above`. */
|
|
124
|
+
declare function getIndicesBetween(below: IndexKey | undefined, above: IndexKey | undefined, n: number): IndexKey[];
|
|
125
|
+
/** Generate `n` sorted keys strictly above `below`. */
|
|
126
|
+
declare function getIndicesAbove(below: IndexKey | undefined, n: number): IndexKey[];
|
|
127
|
+
/** Generate `n` sorted keys strictly below `above`. */
|
|
128
|
+
declare function getIndicesBelow(above: IndexKey | undefined, n: number): IndexKey[];
|
|
129
|
+
/**
|
|
130
|
+
* Generate `n` sorted keys, the first of which is `start` (default `a0`).
|
|
131
|
+
* Useful when creating `n` items at once.
|
|
132
|
+
*/
|
|
133
|
+
declare function getIndices(n: number, start?: IndexKey): IndexKey[];
|
|
134
|
+
/** Return a sorted copy of `items` ordered by their `index` (stable). */
|
|
135
|
+
declare function sortByIndex<T extends {
|
|
136
|
+
index: IndexKey;
|
|
137
|
+
}>(items: readonly T[]): T[];
|
|
138
|
+
/** Compare two keys: negative, zero, or positive. */
|
|
139
|
+
declare function compareIndexKeys(a: IndexKey, b: IndexKey): number;
|
|
140
|
+
/**
|
|
141
|
+
* Throws if `key` is not a well-formed fractional index key: a head marker,
|
|
142
|
+
* enough base-62 integer digits for that head, then an optional fraction of
|
|
143
|
+
* base-62 digits that does not end in `0`.
|
|
144
|
+
* Narrows the type on success.
|
|
145
|
+
*/
|
|
146
|
+
declare function validateIndexKey(key: string): asserts key is IndexKey;
|
|
147
|
+
/** Non-throwing variant of `validateIndexKey`. */
|
|
148
|
+
declare function isIndexKey(key: unknown): key is IndexKey;
|
|
149
|
+
|
|
150
|
+
/** Number of base-62 digits (after the head marker) that affect the zkey. */
|
|
151
|
+
declare const ZKEY_SIGNIFICANT_DIGITS = 10;
|
|
152
|
+
type ZKey = readonly [lo: number, hi: number];
|
|
153
|
+
declare function indexKeyToZKey(key: IndexKey): [lo: number, hi: number];
|
|
154
|
+
/** Recombine a zkey into a BigInt (mostly for tests and debugging). */
|
|
155
|
+
declare function zKeyToBigInt([lo, hi]: ZKey): bigint;
|
|
156
|
+
/** Compare two zkeys as unsigned 64-bit integers. */
|
|
157
|
+
declare function compareZKeys(a: ZKey, b: ZKey): number;
|
|
158
|
+
|
|
159
|
+
/** A set of changes to a store's records. */
|
|
160
|
+
interface RecordsDiff<R extends UnknownRecord> {
|
|
161
|
+
added: Record<IdOf<R>, R>;
|
|
162
|
+
updated: Record<IdOf<R>, [from: R, to: R]>;
|
|
163
|
+
removed: Record<IdOf<R>, R>;
|
|
164
|
+
}
|
|
165
|
+
declare function createEmptyRecordsDiff<R extends UnknownRecord>(): RecordsDiff<R>;
|
|
166
|
+
declare function isRecordsDiffEmpty<R extends UnknownRecord>(diff: RecordsDiff<R>): boolean;
|
|
167
|
+
/** Produce the diff that undoes `diff`. */
|
|
168
|
+
declare function reverseRecordsDiff<R extends UnknownRecord>(diff: RecordsDiff<R>): RecordsDiff<R>;
|
|
169
|
+
/**
|
|
170
|
+
* Record that `id` went from `before` to `after` in `target`, collapsing with
|
|
171
|
+
* any change already recorded for the same id:
|
|
172
|
+
*
|
|
173
|
+
* added + updated -> added (latest)
|
|
174
|
+
* added + removed -> (nothing)
|
|
175
|
+
* updated + updated -> updated [original from, latest to]
|
|
176
|
+
* updated + removed -> removed (original from)
|
|
177
|
+
* removed + added -> updated [removed, added] (or nothing if identical)
|
|
178
|
+
*/
|
|
179
|
+
declare function applyChangeToDiff<R extends UnknownRecord>(target: RecordsDiff<R>, id: IdOf<R>, before: R | undefined, after: R | undefined): void;
|
|
180
|
+
/** Merge `diff` into `target` in place (see `applyChangeToDiff` for the rules). */
|
|
181
|
+
declare function squashRecordDiffsMutable<R extends UnknownRecord>(target: RecordsDiff<R>, diff: RecordsDiff<R>): void;
|
|
182
|
+
/** Squash a sequence of diffs into one equivalent diff (does not mutate inputs). */
|
|
183
|
+
declare function squashRecordDiffs<R extends UnknownRecord>(diffs: readonly RecordsDiff<R>[]): RecordsDiff<R>;
|
|
184
|
+
/** Shallow-copy a diff (entries are shared). */
|
|
185
|
+
declare function cloneRecordsDiff<R extends UnknownRecord>(diff: RecordsDiff<R>): RecordsDiff<R>;
|
|
186
|
+
|
|
187
|
+
/** Serialized records keyed by id. */
|
|
188
|
+
type SerializedStore<R extends UnknownRecord> = Record<IdOf<R>, R>;
|
|
189
|
+
/**
|
|
190
|
+
* Persisted description of a schema: for each migration sequence, how many
|
|
191
|
+
* migrations had been applied when the data was saved.
|
|
192
|
+
*/
|
|
193
|
+
interface SerializedSchemaV2 {
|
|
194
|
+
schemaVersion: 2;
|
|
195
|
+
sequences: {
|
|
196
|
+
[sequenceId: string]: number;
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
type SerializedSchema = SerializedSchemaV2;
|
|
200
|
+
type MigrationId = `${string}/${number}`;
|
|
201
|
+
interface RecordMigration {
|
|
202
|
+
readonly id: MigrationId;
|
|
203
|
+
readonly scope: "record";
|
|
204
|
+
/** Only records for which this returns true are migrated. Defaults to all records. */
|
|
205
|
+
readonly filter?: ((record: UnknownRecord) => boolean) | undefined;
|
|
206
|
+
/** Mutate the record in place, or return a replacement. */
|
|
207
|
+
readonly up: (record: UnknownRecord) => void | UnknownRecord;
|
|
208
|
+
readonly down?: ((record: UnknownRecord) => void | UnknownRecord) | undefined;
|
|
209
|
+
}
|
|
210
|
+
interface StoreMigration {
|
|
211
|
+
readonly id: MigrationId;
|
|
212
|
+
readonly scope: "store";
|
|
213
|
+
/** Mutate the store in place, or return a replacement. */
|
|
214
|
+
readonly up: (store: SerializedStore<UnknownRecord>) => void | SerializedStore<UnknownRecord>;
|
|
215
|
+
readonly down?: ((store: SerializedStore<UnknownRecord>) => void | SerializedStore<UnknownRecord>) | undefined;
|
|
216
|
+
}
|
|
217
|
+
type Migration = RecordMigration | StoreMigration;
|
|
218
|
+
interface MigrationSequence {
|
|
219
|
+
readonly sequenceId: string;
|
|
220
|
+
/**
|
|
221
|
+
* When data is loaded that has never seen this sequence, should every
|
|
222
|
+
* migration be applied (`true`, the default) or should the data be assumed
|
|
223
|
+
* already up to date (`false`)? Use `false` for sequences added to a type
|
|
224
|
+
* that already existed before the sequence did.
|
|
225
|
+
*/
|
|
226
|
+
readonly retroactive: boolean;
|
|
227
|
+
readonly sequence: readonly Migration[];
|
|
228
|
+
}
|
|
229
|
+
type MigrationResult<T> = {
|
|
230
|
+
type: "success";
|
|
231
|
+
value: T;
|
|
232
|
+
} | {
|
|
233
|
+
type: "error";
|
|
234
|
+
reason: string;
|
|
235
|
+
};
|
|
236
|
+
/**
|
|
237
|
+
* Build the ids of a sequence's migrations from friendly names:
|
|
238
|
+
*
|
|
239
|
+
* ```ts
|
|
240
|
+
* const Versions = createMigrationIds('com.example.shape.box', { AddColor: 1, AddSize: 2 })
|
|
241
|
+
* // Versions.AddColor === 'com.example.shape.box/1'
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
declare function createMigrationIds<const ID extends string, const Versions extends Record<string, number>>(sequenceId: ID, versions: Versions): {
|
|
245
|
+
readonly [K in keyof Versions]: `${ID}/${Versions[K]}`;
|
|
246
|
+
};
|
|
247
|
+
declare function parseMigrationId(id: string): {
|
|
248
|
+
sequenceId: string;
|
|
249
|
+
version: number;
|
|
250
|
+
};
|
|
251
|
+
/**
|
|
252
|
+
* Create a validated migration sequence. Migration ids must be
|
|
253
|
+
* `${sequenceId}/1`, `${sequenceId}/2`, ... in order.
|
|
254
|
+
*/
|
|
255
|
+
declare function createMigrationSequence(options: {
|
|
256
|
+
sequenceId: string;
|
|
257
|
+
retroactive?: boolean | undefined;
|
|
258
|
+
sequence: readonly Migration[];
|
|
259
|
+
}): MigrationSequence;
|
|
260
|
+
/**
|
|
261
|
+
* Convenience for the common case: a sequence of record-scoped migrations
|
|
262
|
+
* that all apply to one record type (optionally narrowed further by `filter`).
|
|
263
|
+
*/
|
|
264
|
+
declare function createRecordMigrationSequence(options: {
|
|
265
|
+
sequenceId: string;
|
|
266
|
+
recordType: string;
|
|
267
|
+
retroactive?: boolean | undefined;
|
|
268
|
+
filter?: ((record: UnknownRecord) => boolean) | undefined;
|
|
269
|
+
sequence: readonly Omit<RecordMigration, "scope" | "filter">[];
|
|
270
|
+
}): MigrationSequence;
|
|
271
|
+
/** Apply one record migration to a single record, honoring its filter. */
|
|
272
|
+
declare function applyRecordMigration(migration: RecordMigration, record: UnknownRecord, direction: "up" | "down"): UnknownRecord;
|
|
273
|
+
/** Apply one migration (record- or store-scoped) to a whole store in place. */
|
|
274
|
+
declare function applyMigrationToStore(migration: Migration, store: SerializedStore<UnknownRecord>, direction: "up" | "down"): SerializedStore<UnknownRecord>;
|
|
275
|
+
|
|
276
|
+
type ChangeSource = "user" | "remote";
|
|
277
|
+
interface HistoryEntry<R extends UnknownRecord> {
|
|
278
|
+
changes: RecordsDiff<R>;
|
|
279
|
+
source: ChangeSource;
|
|
280
|
+
}
|
|
281
|
+
type StoreListener<R extends UnknownRecord> = (entry: HistoryEntry<R>) => void;
|
|
282
|
+
interface StoreListenerFilters {
|
|
283
|
+
source: ChangeSource | "all";
|
|
284
|
+
scope: RecordScope | "all";
|
|
285
|
+
}
|
|
286
|
+
type RecordFromTypeName<R extends UnknownRecord, T extends string> = Extract<R, {
|
|
287
|
+
typeName: T;
|
|
288
|
+
}>;
|
|
289
|
+
type StoreRecord<S extends Store<any, any>> = S extends Store<infer R, any> ? R : never;
|
|
290
|
+
interface StoreOptions<R extends UnknownRecord, Props> {
|
|
291
|
+
schema: StoreSchema<R, Props>;
|
|
292
|
+
initialData?: SerializedStore<R> | undefined;
|
|
293
|
+
props: Props;
|
|
294
|
+
id?: string | undefined;
|
|
295
|
+
}
|
|
296
|
+
type StoreBeforeCreateHandler<R extends UnknownRecord> = (record: R, source: ChangeSource) => R;
|
|
297
|
+
type StoreAfterCreateHandler<R extends UnknownRecord> = (record: R, source: ChangeSource) => void;
|
|
298
|
+
type StoreBeforeChangeHandler<R extends UnknownRecord> = (prev: R, next: R, source: ChangeSource) => R;
|
|
299
|
+
type StoreAfterChangeHandler<R extends UnknownRecord> = (prev: R, next: R, source: ChangeSource) => void;
|
|
300
|
+
/** Return `false` to veto the deletion. */
|
|
301
|
+
type StoreBeforeDeleteHandler<R extends UnknownRecord> = (record: R, source: ChangeSource) => void | false;
|
|
302
|
+
type StoreAfterDeleteHandler<R extends UnknownRecord> = (record: R, source: ChangeSource) => void;
|
|
303
|
+
type StoreOperationCompleteHandler = (source: ChangeSource) => void;
|
|
304
|
+
interface StoreSideEffectHandlers<R extends UnknownRecord> {
|
|
305
|
+
beforeCreate?: StoreBeforeCreateHandler<R> | undefined;
|
|
306
|
+
afterCreate?: StoreAfterCreateHandler<R> | undefined;
|
|
307
|
+
beforeChange?: StoreBeforeChangeHandler<R> | undefined;
|
|
308
|
+
afterChange?: StoreAfterChangeHandler<R> | undefined;
|
|
309
|
+
beforeDelete?: StoreBeforeDeleteHandler<R> | undefined;
|
|
310
|
+
afterDelete?: StoreAfterDeleteHandler<R> | undefined;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Hooks that run around record writes. `before*` handlers may replace the
|
|
314
|
+
* record being written (or veto a delete); `after*` handlers observe.
|
|
315
|
+
* `operationComplete` handlers run once when the outermost operation ends,
|
|
316
|
+
* before history listeners are notified.
|
|
317
|
+
*/
|
|
318
|
+
declare class StoreSideEffects<R extends UnknownRecord> {
|
|
319
|
+
private readonly byType;
|
|
320
|
+
private readonly operationComplete;
|
|
321
|
+
private enabled;
|
|
322
|
+
isEnabled(): boolean;
|
|
323
|
+
setIsEnabled(enabled: boolean): void;
|
|
324
|
+
private sets;
|
|
325
|
+
private add;
|
|
326
|
+
/** Register several handlers for several types at once. Returns a disposer for all of them. */
|
|
327
|
+
register(handlers: {
|
|
328
|
+
[T in R["typeName"]]?: StoreSideEffectHandlers<RecordFromTypeName<R, T>>;
|
|
329
|
+
}): () => void;
|
|
330
|
+
registerBeforeCreateHandler<T extends R["typeName"]>(typeName: T, handler: StoreBeforeCreateHandler<RecordFromTypeName<R, T>>): () => void;
|
|
331
|
+
registerAfterCreateHandler<T extends R["typeName"]>(typeName: T, handler: StoreAfterCreateHandler<RecordFromTypeName<R, T>>): () => void;
|
|
332
|
+
registerBeforeChangeHandler<T extends R["typeName"]>(typeName: T, handler: StoreBeforeChangeHandler<RecordFromTypeName<R, T>>): () => void;
|
|
333
|
+
registerAfterChangeHandler<T extends R["typeName"]>(typeName: T, handler: StoreAfterChangeHandler<RecordFromTypeName<R, T>>): () => void;
|
|
334
|
+
registerBeforeDeleteHandler<T extends R["typeName"]>(typeName: T, handler: StoreBeforeDeleteHandler<RecordFromTypeName<R, T>>): () => void;
|
|
335
|
+
registerAfterDeleteHandler<T extends R["typeName"]>(typeName: T, handler: StoreAfterDeleteHandler<RecordFromTypeName<R, T>>): () => void;
|
|
336
|
+
registerOperationCompleteHandler(handler: StoreOperationCompleteHandler): () => void;
|
|
337
|
+
/** @internal */
|
|
338
|
+
handleBeforeCreate(record: R, source: ChangeSource): R;
|
|
339
|
+
/** @internal */
|
|
340
|
+
handleAfterCreate(record: R, source: ChangeSource): void;
|
|
341
|
+
/** @internal */
|
|
342
|
+
handleBeforeChange(prev: R, next: R, source: ChangeSource): R;
|
|
343
|
+
/** @internal */
|
|
344
|
+
handleAfterChange(prev: R, next: R, source: ChangeSource): void;
|
|
345
|
+
/** @internal Returns false when a handler vetoed the delete. */
|
|
346
|
+
handleBeforeDelete(record: R, source: ChangeSource): boolean;
|
|
347
|
+
/** @internal */
|
|
348
|
+
handleAfterDelete(record: R, source: ChangeSource): void;
|
|
349
|
+
/** @internal */
|
|
350
|
+
handleOperationComplete(source: ChangeSource): void;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Records are considered unchanged when every top-level value is identical,
|
|
354
|
+
* with `props` and `meta` compared one level deeper. Cheap enough to run on
|
|
355
|
+
* every `put`, and avoids no-op history entries.
|
|
356
|
+
*/
|
|
357
|
+
declare function isRecordShallowEqual(a: UnknownRecord, b: UnknownRecord): boolean;
|
|
358
|
+
/** Freeze a record and its `props` / `meta` bags (one level). */
|
|
359
|
+
declare function freezeRecord<R extends UnknownRecord>(record: R): R;
|
|
360
|
+
interface TypeIndex<R extends UnknownRecord> {
|
|
361
|
+
/** Mutated in place on every add/remove: O(1) per record. */
|
|
362
|
+
readonly live: Set<IdOf<R>>;
|
|
363
|
+
/** Bumped whenever `live` changes; the reactive handle on membership. */
|
|
364
|
+
readonly epoch: Atom<number>;
|
|
365
|
+
}
|
|
366
|
+
/** Reactive views over the store's records, cached per type name. */
|
|
367
|
+
declare class StoreQueries<R extends UnknownRecord> {
|
|
368
|
+
private readonly store;
|
|
369
|
+
private readonly idsCache;
|
|
370
|
+
private readonly recordsCache;
|
|
371
|
+
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>;
|
|
378
|
+
/** 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>[];
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* A reactive, transactional collection of records.
|
|
383
|
+
*
|
|
384
|
+
* - one atom per record, so consumers subscribe to exactly what they read
|
|
385
|
+
* - per-type id sets maintained incrementally
|
|
386
|
+
* - writes are batched; listeners receive one squashed diff per outermost operation
|
|
387
|
+
* - records are frozen on write
|
|
388
|
+
*/
|
|
389
|
+
declare class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|
390
|
+
readonly id: string;
|
|
391
|
+
readonly schema: StoreSchema<R, Props>;
|
|
392
|
+
readonly props: Props;
|
|
393
|
+
readonly scopedTypes: {
|
|
394
|
+
readonly [S in RecordScope]: ReadonlySet<string>;
|
|
395
|
+
};
|
|
396
|
+
readonly sideEffects: StoreSideEffects<R>;
|
|
397
|
+
readonly query: StoreQueries<R>;
|
|
398
|
+
/** Bumped once per completed operation that changed something. */
|
|
399
|
+
readonly history: Atom<number>;
|
|
400
|
+
private readonly records;
|
|
401
|
+
private readonly typeIndexes;
|
|
402
|
+
private readonly listeners;
|
|
403
|
+
private pendingEntries;
|
|
404
|
+
private readonly extractStack;
|
|
405
|
+
private depth;
|
|
406
|
+
private source;
|
|
407
|
+
private runCallbacks;
|
|
408
|
+
private inOperationComplete;
|
|
409
|
+
private disposed;
|
|
410
|
+
constructor(options: StoreOptions<R, Props>);
|
|
411
|
+
/** @internal */
|
|
412
|
+
getTypeIndex(typeName: string): TypeIndex<R>;
|
|
413
|
+
/** Get a record (reactive: subscribes to the record, or to its type's membership when absent). */
|
|
414
|
+
get<K extends IdOf<R>>(id: K): RecordFromId<K> | undefined;
|
|
415
|
+
/** Get a record without registering a reactive dependency. */
|
|
416
|
+
unsafeGetWithoutCapture<K extends IdOf<R>>(id: K): RecordFromId<K> | undefined;
|
|
417
|
+
has<K extends IdOf<R>>(id: K): boolean;
|
|
418
|
+
/** All records (reactive over every record and every type's membership). */
|
|
419
|
+
allRecords(): R[];
|
|
420
|
+
/** Scope of a record type; unknown types are `document`. */
|
|
421
|
+
getScope(typeName: string): RecordScope;
|
|
422
|
+
/**
|
|
423
|
+
* Insert or update records. Records are validated, passed through `before*`
|
|
424
|
+
* side effects, frozen, and written. `after*` side effects run once every
|
|
425
|
+
* record in the call has been written.
|
|
426
|
+
*/
|
|
427
|
+
put(records: readonly R[], phaseOverride?: StoreValidationPhase): void;
|
|
428
|
+
/** Remove records by id. Missing ids are ignored. `beforeDelete` handlers may veto. */
|
|
429
|
+
remove(ids: readonly IdOf<R>[]): void;
|
|
430
|
+
/** Remove every record. */
|
|
431
|
+
clear(): void;
|
|
432
|
+
/**
|
|
433
|
+
* Update one record with a function. No-op when the record does not exist.
|
|
434
|
+
*/
|
|
435
|
+
update<K extends IdOf<R>>(id: K, updater: (record: RecordFromId<K>) => RecordFromId<K>): void;
|
|
436
|
+
/**
|
|
437
|
+
* Run `fn` as one operation: side effects' `operationComplete` handlers run
|
|
438
|
+
* once at the end, and listeners get a single squashed history entry.
|
|
439
|
+
*/
|
|
440
|
+
atomic<T>(fn: () => T, options?: {
|
|
441
|
+
source?: ChangeSource | undefined;
|
|
442
|
+
runCallbacks?: boolean | undefined;
|
|
443
|
+
}): T;
|
|
444
|
+
/** Changes made inside `fn` are reported to listeners with source `remote`. */
|
|
445
|
+
mergeRemoteChanges(fn: () => void): void;
|
|
446
|
+
/** Run `fn` and return the squashed diff of everything it changed. Listeners are still notified. */
|
|
447
|
+
extractingChanges(fn: () => void): RecordsDiff<R>;
|
|
448
|
+
/**
|
|
449
|
+
* Apply a diff (e.g. from `extractingChanges` or `reverseRecordsDiff`).
|
|
450
|
+
* With `ignoreEphemeralKeys`, ephemeral keys of updated records keep their
|
|
451
|
+
* current store values instead of the diff's.
|
|
452
|
+
*/
|
|
453
|
+
applyDiff(diff: RecordsDiff<R>, options?: {
|
|
454
|
+
runCallbacks?: boolean | undefined;
|
|
455
|
+
ignoreEphemeralKeys?: boolean | undefined;
|
|
456
|
+
}): void;
|
|
457
|
+
/**
|
|
458
|
+
* Subscribe to history entries. Called after each outermost operation with
|
|
459
|
+
* the squashed changes, filtered by source and record scope.
|
|
460
|
+
*/
|
|
461
|
+
listen(onHistory: StoreListener<R>, filters?: Partial<StoreListenerFilters>): () => void;
|
|
462
|
+
/** Plain-object snapshot of the records in `scope` (default `document`). */
|
|
463
|
+
serialize(scope?: RecordScope | "all"): SerializedStore<R>;
|
|
464
|
+
getStoreSnapshot(scope?: RecordScope | "all"): StoreSnapshot<R>;
|
|
465
|
+
/**
|
|
466
|
+
* Replace the store's contents with a snapshot (migrating it first).
|
|
467
|
+
* Existing records in `document` scope and in every scope present in the
|
|
468
|
+
* snapshot are removed unless the snapshot contains them; other scopes are
|
|
469
|
+
* left alone. Side effects do not run; listeners are notified.
|
|
470
|
+
*/
|
|
471
|
+
loadStoreSnapshot(snapshot: StoreSnapshot<R>): void;
|
|
472
|
+
/**
|
|
473
|
+
* A per-record derived value, recomputed only when that record changes.
|
|
474
|
+
* Entries are dropped automatically when records are removed.
|
|
475
|
+
*/
|
|
476
|
+
createComputedCache<T, K extends IdOf<R> = IdOf<R>>(name: string, derive: (record: RecordFromId<K>) => T, options?: {
|
|
477
|
+
isEqual?: ((a: T, b: T) => boolean) | undefined;
|
|
478
|
+
}): {
|
|
479
|
+
get(id: K): T | undefined;
|
|
480
|
+
};
|
|
481
|
+
isDisposed(): boolean;
|
|
482
|
+
dispose(): void;
|
|
483
|
+
private addToIndex;
|
|
484
|
+
private removeFromIndex;
|
|
485
|
+
private recordChange;
|
|
486
|
+
private completeOperation;
|
|
487
|
+
private flushHistory;
|
|
488
|
+
private filterDiffByScope;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
type StoreValidationPhase = "initialize" | "createRecord" | "updateRecord" | "tests";
|
|
492
|
+
type RecordTypeMap<R extends UnknownRecord> = {
|
|
493
|
+
readonly [TypeName in R["typeName"]]: RecordType<Extract<R, {
|
|
494
|
+
typeName: TypeName;
|
|
495
|
+
}>, any>;
|
|
496
|
+
};
|
|
497
|
+
interface StoreValidationFailure<R extends UnknownRecord> {
|
|
498
|
+
error: unknown;
|
|
499
|
+
store: Store<R, any>;
|
|
500
|
+
record: R;
|
|
501
|
+
phase: StoreValidationPhase;
|
|
502
|
+
recordBefore: R | null;
|
|
503
|
+
}
|
|
504
|
+
interface StoreSchemaOptions<R extends UnknownRecord, Props> {
|
|
505
|
+
readonly migrations?: readonly MigrationSequence[] | undefined;
|
|
506
|
+
/**
|
|
507
|
+
* Called when a record fails validation. Return a repaired record to keep
|
|
508
|
+
* going, or throw to abort the operation. When omitted the error is thrown.
|
|
509
|
+
*/
|
|
510
|
+
readonly onValidationFailure?: ((data: StoreValidationFailure<R>) => R) | undefined;
|
|
511
|
+
/** Reserved for store-level integrity checks; unused by the schema itself. */
|
|
512
|
+
readonly createIntegrityChecker?: ((store: Store<R, Props>) => void) | undefined;
|
|
513
|
+
}
|
|
514
|
+
interface StoreSnapshot<R extends UnknownRecord> {
|
|
515
|
+
store: SerializedStore<R>;
|
|
516
|
+
schema: SerializedSchema;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* The set of record types a store holds plus the migrations that bring
|
|
520
|
+
* persisted data up to date.
|
|
521
|
+
*/
|
|
522
|
+
declare class StoreSchema<R extends UnknownRecord, Props = unknown> {
|
|
523
|
+
readonly types: RecordTypeMap<R>;
|
|
524
|
+
private readonly options;
|
|
525
|
+
static create<R extends UnknownRecord, Props = unknown>(types: RecordTypeMap<R>, options?: StoreSchemaOptions<R, Props>): StoreSchema<R, Props>;
|
|
526
|
+
readonly migrations: Readonly<Record<string, MigrationSequence>>;
|
|
527
|
+
/** All migrations in application order (sequence registration order, then version). */
|
|
528
|
+
readonly sortedMigrations: readonly Migration[];
|
|
529
|
+
private readonly typeByName;
|
|
530
|
+
private constructor();
|
|
531
|
+
getType(typeName: string): RecordType<R, any> | undefined;
|
|
532
|
+
/** Scope of a record type; unknown types are treated as `document`. */
|
|
533
|
+
getScope(typeName: string): RecordScope;
|
|
534
|
+
/**
|
|
535
|
+
* Validate a record, delegating to its record type's validator. Records of
|
|
536
|
+
* unknown types are passed through untouched so that foreign data survives
|
|
537
|
+
* a load/save round-trip.
|
|
538
|
+
*/
|
|
539
|
+
validateRecord(store: Store<R, any>, record: R, phase: StoreValidationPhase, recordBefore: R | undefined): R;
|
|
540
|
+
/** The current version of every sequence. */
|
|
541
|
+
serialize(): SerializedSchema;
|
|
542
|
+
/** A schema at version 0 of every sequence (all migrations still pending). */
|
|
543
|
+
serializeEarliestVersion(): SerializedSchema;
|
|
544
|
+
/**
|
|
545
|
+
* The migrations that must run to bring data saved under `persistedSchema`
|
|
546
|
+
* up to this schema, in order. Sequences the persisted schema knows but we
|
|
547
|
+
* do not are ignored with a warning.
|
|
548
|
+
*/
|
|
549
|
+
getMigrationsSince(persistedSchema: SerializedSchema): MigrationResult<Migration[]>;
|
|
550
|
+
/**
|
|
551
|
+
* Migrate a single record. Only record-scoped migrations can be applied;
|
|
552
|
+
* encountering a store-scoped one is an error. `down` runs the migrations
|
|
553
|
+
* in reverse (from this schema to `persistedSchema`).
|
|
554
|
+
*/
|
|
555
|
+
migratePersistedRecord(record: UnknownRecord, persistedSchema: SerializedSchema, direction?: "up" | "down"): MigrationResult<UnknownRecord>;
|
|
556
|
+
/**
|
|
557
|
+
* Bring a whole persisted store up to date. The input is not mutated.
|
|
558
|
+
* Records of types this schema does not know are preserved as-is.
|
|
559
|
+
*/
|
|
560
|
+
migrateStoreSnapshot(snapshot: StoreSnapshot<R>): MigrationResult<SerializedStore<R>>;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* `.tldr` file envelope. Schema-agnostic: this module does not know or care
|
|
565
|
+
* what record types the file holds.
|
|
566
|
+
*/
|
|
567
|
+
declare const TLDR_FILE_FORMAT_VERSION = 1;
|
|
568
|
+
interface TldrFile {
|
|
569
|
+
tldrawFileFormatVersion: number;
|
|
570
|
+
schema: SerializedSchema;
|
|
571
|
+
records: UnknownRecord[];
|
|
572
|
+
}
|
|
573
|
+
type TldrFileParseError = "notATldrFile" | "v1File" | "invalidRecords" | "futureVersion";
|
|
574
|
+
type ParseTldrFileResult = {
|
|
575
|
+
ok: true;
|
|
576
|
+
schema: SerializedSchema;
|
|
577
|
+
records: UnknownRecord[];
|
|
578
|
+
} | {
|
|
579
|
+
ok: false;
|
|
580
|
+
error: TldrFileParseError;
|
|
581
|
+
cause?: unknown;
|
|
582
|
+
};
|
|
583
|
+
/**
|
|
584
|
+
* Parse a `.tldr` file. Accepts either the JSON text or the already-parsed
|
|
585
|
+
* value. Never throws.
|
|
586
|
+
*/
|
|
587
|
+
declare function parseTldrFile(json: unknown): ParseTldrFileResult;
|
|
588
|
+
/**
|
|
589
|
+
* Serialize records and their schema into the `.tldr` envelope
|
|
590
|
+
* (pretty-printed, stable key order). Records are written in the given order.
|
|
591
|
+
*/
|
|
592
|
+
declare function serializeTldrFile(schema: SerializedSchema, records: readonly UnknownRecord[]): string;
|
|
593
|
+
/** Convert a parsed file into a `{ store, schema }` snapshot. */
|
|
594
|
+
declare function tldrFileToStoreSnapshot(file: {
|
|
595
|
+
schema: SerializedSchema;
|
|
596
|
+
records: readonly UnknownRecord[];
|
|
597
|
+
}): {
|
|
598
|
+
store: SerializedStore<UnknownRecord>;
|
|
599
|
+
schema: SerializedSchema;
|
|
600
|
+
};
|
|
601
|
+
/** Convert a `{ store, schema }` snapshot into `.tldr` text. Records are ordered by id. */
|
|
602
|
+
declare function storeSnapshotToTldrFile(snapshot: {
|
|
603
|
+
store: SerializedStore<UnknownRecord>;
|
|
604
|
+
schema: SerializedSchema;
|
|
605
|
+
}): string;
|
|
606
|
+
|
|
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 };
|