@tldraw/store 4.1.0-next.542f014c3fac → 4.1.0-next.74327a60f18a
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-cjs/index.d.ts +1884 -153
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/AtomMap.js +241 -1
- package/dist-cjs/lib/AtomMap.js.map +2 -2
- package/dist-cjs/lib/BaseRecord.js.map +2 -2
- package/dist-cjs/lib/ImmutableMap.js +141 -0
- package/dist-cjs/lib/ImmutableMap.js.map +2 -2
- package/dist-cjs/lib/IncrementalSetConstructor.js +45 -5
- package/dist-cjs/lib/IncrementalSetConstructor.js.map +2 -2
- package/dist-cjs/lib/RecordType.js +116 -21
- package/dist-cjs/lib/RecordType.js.map +2 -2
- package/dist-cjs/lib/RecordsDiff.js.map +2 -2
- package/dist-cjs/lib/Store.js +233 -39
- package/dist-cjs/lib/Store.js.map +2 -2
- package/dist-cjs/lib/StoreQueries.js +135 -22
- package/dist-cjs/lib/StoreQueries.js.map +2 -2
- package/dist-cjs/lib/StoreSchema.js +207 -2
- package/dist-cjs/lib/StoreSchema.js.map +2 -2
- package/dist-cjs/lib/StoreSideEffects.js +102 -10
- package/dist-cjs/lib/StoreSideEffects.js.map +2 -2
- package/dist-cjs/lib/executeQuery.js.map +2 -2
- package/dist-cjs/lib/migrate.js.map +2 -2
- package/dist-cjs/lib/setUtils.js.map +2 -2
- package/dist-esm/index.d.mts +1884 -153
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/AtomMap.mjs +241 -1
- package/dist-esm/lib/AtomMap.mjs.map +2 -2
- package/dist-esm/lib/BaseRecord.mjs.map +2 -2
- package/dist-esm/lib/ImmutableMap.mjs +141 -0
- package/dist-esm/lib/ImmutableMap.mjs.map +2 -2
- package/dist-esm/lib/IncrementalSetConstructor.mjs +45 -5
- package/dist-esm/lib/IncrementalSetConstructor.mjs.map +2 -2
- package/dist-esm/lib/RecordType.mjs +116 -21
- package/dist-esm/lib/RecordType.mjs.map +2 -2
- package/dist-esm/lib/RecordsDiff.mjs.map +2 -2
- package/dist-esm/lib/Store.mjs +233 -39
- package/dist-esm/lib/Store.mjs.map +2 -2
- package/dist-esm/lib/StoreQueries.mjs +135 -22
- package/dist-esm/lib/StoreQueries.mjs.map +2 -2
- package/dist-esm/lib/StoreSchema.mjs +207 -2
- package/dist-esm/lib/StoreSchema.mjs.map +2 -2
- package/dist-esm/lib/StoreSideEffects.mjs +102 -10
- package/dist-esm/lib/StoreSideEffects.mjs.map +2 -2
- package/dist-esm/lib/executeQuery.mjs.map +2 -2
- package/dist-esm/lib/migrate.mjs.map +2 -2
- package/dist-esm/lib/setUtils.mjs.map +2 -2
- package/package.json +3 -3
- package/src/lib/AtomMap.ts +241 -1
- package/src/lib/BaseRecord.test.ts +44 -0
- package/src/lib/BaseRecord.ts +118 -4
- package/src/lib/ImmutableMap.test.ts +103 -0
- package/src/lib/ImmutableMap.ts +212 -0
- package/src/lib/IncrementalSetConstructor.test.ts +111 -0
- package/src/lib/IncrementalSetConstructor.ts +63 -6
- package/src/lib/RecordType.ts +149 -25
- package/src/lib/RecordsDiff.test.ts +144 -0
- package/src/lib/RecordsDiff.ts +145 -10
- package/src/lib/Store.test.ts +827 -0
- package/src/lib/Store.ts +533 -67
- package/src/lib/StoreQueries.test.ts +627 -0
- package/src/lib/StoreQueries.ts +194 -27
- package/src/lib/StoreSchema.test.ts +226 -0
- package/src/lib/StoreSchema.ts +386 -8
- package/src/lib/StoreSideEffects.test.ts +239 -19
- package/src/lib/StoreSideEffects.ts +266 -19
- package/src/lib/devFreeze.test.ts +137 -0
- package/src/lib/executeQuery.test.ts +481 -0
- package/src/lib/executeQuery.ts +80 -2
- package/src/lib/migrate.test.ts +400 -0
- package/src/lib/migrate.ts +187 -14
- package/src/lib/setUtils.test.ts +105 -0
- package/src/lib/setUtils.ts +44 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/RecordsDiff.ts"],
|
|
4
|
-
"sourcesContent": ["import { objectMapEntries } from '@tldraw/utils'\nimport { IdOf, UnknownRecord } from './BaseRecord'\n\n/**\n * A diff describing the changes to
|
|
5
|
-
"mappings": "AAAA,SAAS,wBAAwB;
|
|
4
|
+
"sourcesContent": ["import { objectMapEntries } from '@tldraw/utils'\nimport { IdOf, UnknownRecord } from './BaseRecord'\n\n/**\n * A diff describing the changes to records, containing collections of records that were added,\n * updated, or removed. This is the fundamental data structure used throughout the store system\n * to track and communicate changes.\n *\n * @example\n * ```ts\n * const diff: RecordsDiff<Book> = {\n * added: {\n * 'book:1': { id: 'book:1', typeName: 'book', title: 'New Book' }\n * },\n * updated: {\n * 'book:2': [\n * { id: 'book:2', typeName: 'book', title: 'Old Title' }, // from\n * { id: 'book:2', typeName: 'book', title: 'New Title' } // to\n * ]\n * },\n * removed: {\n * 'book:3': { id: 'book:3', typeName: 'book', title: 'Deleted Book' }\n * }\n * }\n * ```\n *\n * @public\n */\nexport interface RecordsDiff<R extends UnknownRecord> {\n\t/** Records that were created, keyed by their ID */\n\tadded: Record<IdOf<R>, R>\n\t/** Records that were modified, keyed by their ID. Each entry contains [from, to] tuple */\n\tupdated: Record<IdOf<R>, [from: R, to: R]>\n\t/** Records that were deleted, keyed by their ID */\n\tremoved: Record<IdOf<R>, R>\n}\n\n/**\n * Creates an empty RecordsDiff with no added, updated, or removed records.\n * This is useful as a starting point when building diffs programmatically.\n *\n * @returns An empty RecordsDiff with all collections initialized to empty objects\n * @example\n * ```ts\n * const emptyDiff = createEmptyRecordsDiff<Book>()\n * // Result: { added: {}, updated: {}, removed: {} }\n * ```\n *\n * @internal\n */\nexport function createEmptyRecordsDiff<R extends UnknownRecord>(): RecordsDiff<R> {\n\treturn { added: {}, updated: {}, removed: {} } as RecordsDiff<R>\n}\n\n/**\n * Creates the inverse of a RecordsDiff, effectively reversing all changes.\n * Added records become removed, removed records become added, and updated records\n * have their from/to values swapped. This is useful for implementing undo operations.\n *\n * @param diff - The diff to reverse\n * @returns A new RecordsDiff that represents the inverse of the input diff\n * @example\n * ```ts\n * const originalDiff: RecordsDiff<Book> = {\n * added: { 'book:1': newBook },\n * updated: { 'book:2': [oldBook, updatedBook] },\n * removed: { 'book:3': deletedBook }\n * }\n *\n * const reversedDiff = reverseRecordsDiff(originalDiff)\n * // Result: {\n * // added: { 'book:3': deletedBook },\n * // updated: { 'book:2': [updatedBook, oldBook] },\n * // removed: { 'book:1': newBook }\n * // }\n * ```\n *\n * @public\n */\nexport function reverseRecordsDiff(diff: RecordsDiff<any>) {\n\tconst result: RecordsDiff<any> = { added: diff.removed, removed: diff.added, updated: {} }\n\tfor (const [from, to] of Object.values(diff.updated)) {\n\t\tresult.updated[from.id] = [to, from]\n\t}\n\treturn result\n}\n\n/**\n * Checks whether a RecordsDiff contains any changes. A diff is considered empty\n * if it has no added, updated, or removed records.\n *\n * @param diff - The diff to check\n * @returns True if the diff contains no changes, false otherwise\n * @example\n * ```ts\n * const emptyDiff = createEmptyRecordsDiff<Book>()\n * console.log(isRecordsDiffEmpty(emptyDiff)) // true\n *\n * const nonEmptyDiff: RecordsDiff<Book> = {\n * added: { 'book:1': someBook },\n * updated: {},\n * removed: {}\n * }\n * console.log(isRecordsDiffEmpty(nonEmptyDiff)) // false\n * ```\n *\n * @public\n */\nexport function isRecordsDiffEmpty<T extends UnknownRecord>(diff: RecordsDiff<T>) {\n\treturn (\n\t\tObject.keys(diff.added).length === 0 &&\n\t\tObject.keys(diff.updated).length === 0 &&\n\t\tObject.keys(diff.removed).length === 0\n\t)\n}\n\n/**\n * Combines multiple RecordsDiff objects into a single consolidated diff.\n * This function intelligently merges changes, handling cases where the same record\n * is modified multiple times across different diffs. For example, if a record is\n * added in one diff and then updated in another, the result will show it as added\n * with the final state.\n *\n * @param diffs - An array of diffs to combine into a single diff\n * @param options - Configuration options for the squashing operation\n * - mutateFirstDiff - If true, modifies the first diff in place instead of creating a new one\n * @returns A single diff that represents the cumulative effect of all input diffs\n * @example\n * ```ts\n * const diff1: RecordsDiff<Book> = {\n * added: { 'book:1': { id: 'book:1', title: 'New Book' } },\n * updated: {},\n * removed: {}\n * }\n *\n * const diff2: RecordsDiff<Book> = {\n * added: {},\n * updated: { 'book:1': [{ id: 'book:1', title: 'New Book' }, { id: 'book:1', title: 'Updated Title' }] },\n * removed: {}\n * }\n *\n * const squashed = squashRecordDiffs([diff1, diff2])\n * // Result: {\n * // added: { 'book:1': { id: 'book:1', title: 'Updated Title' } },\n * // updated: {},\n * // removed: {}\n * // }\n * ```\n *\n * @public\n */\nexport function squashRecordDiffs<T extends UnknownRecord>(\n\tdiffs: RecordsDiff<T>[],\n\toptions?: {\n\t\tmutateFirstDiff?: boolean\n\t}\n): RecordsDiff<T> {\n\tconst result = options?.mutateFirstDiff\n\t\t? diffs[0]\n\t\t: ({ added: {}, removed: {}, updated: {} } as RecordsDiff<T>)\n\n\tsquashRecordDiffsMutable(result, options?.mutateFirstDiff ? diffs.slice(1) : diffs)\n\treturn result\n}\n\n/**\n * Applies an array of diffs to a target diff by mutating the target in-place.\n * This is the core implementation used by squashRecordDiffs. It handles complex\n * scenarios where records move between added/updated/removed states across multiple diffs.\n *\n * The function processes each diff sequentially, applying the following logic:\n * - Added records: If the record was previously removed, convert to an update; otherwise add it\n * - Updated records: Chain updates together, preserving the original 'from' state\n * - Removed records: If the record was added in this sequence, cancel both operations\n *\n * @param target - The diff to modify in-place (will be mutated)\n * @param diffs - Array of diffs to apply to the target\n * @example\n * ```ts\n * const targetDiff: RecordsDiff<Book> = {\n * added: {},\n * updated: {},\n * removed: { 'book:1': oldBook }\n * }\n *\n * const newDiffs = [{\n * added: { 'book:1': newBook },\n * updated: {},\n * removed: {}\n * }]\n *\n * squashRecordDiffsMutable(targetDiff, newDiffs)\n * // targetDiff is now: {\n * // added: {},\n * // updated: { 'book:1': [oldBook, newBook] },\n * // removed: {}\n * // }\n * ```\n *\n * @internal\n */\nexport function squashRecordDiffsMutable<T extends UnknownRecord>(\n\ttarget: RecordsDiff<T>,\n\tdiffs: RecordsDiff<T>[]\n): void {\n\tfor (const diff of diffs) {\n\t\tfor (const [id, value] of objectMapEntries(diff.added)) {\n\t\t\tif (target.removed[id]) {\n\t\t\t\tconst original = target.removed[id]\n\t\t\t\tdelete target.removed[id]\n\t\t\t\tif (original !== value) {\n\t\t\t\t\ttarget.updated[id] = [original, value]\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttarget.added[id] = value\n\t\t\t}\n\t\t}\n\n\t\tfor (const [id, [_from, to]] of objectMapEntries(diff.updated)) {\n\t\t\tif (target.added[id]) {\n\t\t\t\ttarget.added[id] = to\n\t\t\t\tdelete target.updated[id]\n\t\t\t\tdelete target.removed[id]\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif (target.updated[id]) {\n\t\t\t\ttarget.updated[id] = [target.updated[id][0], to]\n\t\t\t\tdelete target.removed[id]\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttarget.updated[id] = diff.updated[id]\n\t\t\tdelete target.removed[id]\n\t\t}\n\n\t\tfor (const [id, value] of objectMapEntries(diff.removed)) {\n\t\t\t// the same record was added in this diff sequence, just drop it\n\t\t\tif (target.added[id]) {\n\t\t\t\tdelete target.added[id]\n\t\t\t} else if (target.updated[id]) {\n\t\t\t\ttarget.removed[id] = target.updated[id][0]\n\t\t\t\tdelete target.updated[id]\n\t\t\t} else {\n\t\t\t\ttarget.removed[id] = value\n\t\t\t}\n\t\t}\n\t}\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,wBAAwB;AAkD1B,SAAS,yBAAkE;AACjF,SAAO,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC,EAAE;AAC9C;AA2BO,SAAS,mBAAmB,MAAwB;AAC1D,QAAM,SAA2B,EAAE,OAAO,KAAK,SAAS,SAAS,KAAK,OAAO,SAAS,CAAC,EAAE;AACzF,aAAW,CAAC,MAAM,EAAE,KAAK,OAAO,OAAO,KAAK,OAAO,GAAG;AACrD,WAAO,QAAQ,KAAK,EAAE,IAAI,CAAC,IAAI,IAAI;AAAA,EACpC;AACA,SAAO;AACR;AAuBO,SAAS,mBAA4C,MAAsB;AACjF,SACC,OAAO,KAAK,KAAK,KAAK,EAAE,WAAW,KACnC,OAAO,KAAK,KAAK,OAAO,EAAE,WAAW,KACrC,OAAO,KAAK,KAAK,OAAO,EAAE,WAAW;AAEvC;AAqCO,SAAS,kBACf,OACA,SAGiB;AACjB,QAAM,SAAS,SAAS,kBACrB,MAAM,CAAC,IACN,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC,EAAE;AAE1C,2BAAyB,QAAQ,SAAS,kBAAkB,MAAM,MAAM,CAAC,IAAI,KAAK;AAClF,SAAO;AACR;AAsCO,SAAS,yBACf,QACA,OACO;AACP,aAAW,QAAQ,OAAO;AACzB,eAAW,CAAC,IAAI,KAAK,KAAK,iBAAiB,KAAK,KAAK,GAAG;AACvD,UAAI,OAAO,QAAQ,EAAE,GAAG;AACvB,cAAM,WAAW,OAAO,QAAQ,EAAE;AAClC,eAAO,OAAO,QAAQ,EAAE;AACxB,YAAI,aAAa,OAAO;AACvB,iBAAO,QAAQ,EAAE,IAAI,CAAC,UAAU,KAAK;AAAA,QACtC;AAAA,MACD,OAAO;AACN,eAAO,MAAM,EAAE,IAAI;AAAA,MACpB;AAAA,IACD;AAEA,eAAW,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,iBAAiB,KAAK,OAAO,GAAG;AAC/D,UAAI,OAAO,MAAM,EAAE,GAAG;AACrB,eAAO,MAAM,EAAE,IAAI;AACnB,eAAO,OAAO,QAAQ,EAAE;AACxB,eAAO,OAAO,QAAQ,EAAE;AACxB;AAAA,MACD;AACA,UAAI,OAAO,QAAQ,EAAE,GAAG;AACvB,eAAO,QAAQ,EAAE,IAAI,CAAC,OAAO,QAAQ,EAAE,EAAE,CAAC,GAAG,EAAE;AAC/C,eAAO,OAAO,QAAQ,EAAE;AACxB;AAAA,MACD;AAEA,aAAO,QAAQ,EAAE,IAAI,KAAK,QAAQ,EAAE;AACpC,aAAO,OAAO,QAAQ,EAAE;AAAA,IACzB;AAEA,eAAW,CAAC,IAAI,KAAK,KAAK,iBAAiB,KAAK,OAAO,GAAG;AAEzD,UAAI,OAAO,MAAM,EAAE,GAAG;AACrB,eAAO,OAAO,MAAM,EAAE;AAAA,MACvB,WAAW,OAAO,QAAQ,EAAE,GAAG;AAC9B,eAAO,QAAQ,EAAE,IAAI,OAAO,QAAQ,EAAE,EAAE,CAAC;AACzC,eAAO,OAAO,QAAQ,EAAE;AAAA,MACzB,OAAO;AACN,eAAO,QAAQ,EAAE,IAAI;AAAA,MACtB;AAAA,IACD;AAAA,EACD;AACD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist-esm/lib/Store.mjs
CHANGED
|
@@ -18,7 +18,9 @@ import { StoreSideEffects } from "./StoreSideEffects.mjs";
|
|
|
18
18
|
import { devFreeze } from "./devFreeze.mjs";
|
|
19
19
|
class Store {
|
|
20
20
|
/**
|
|
21
|
-
* The
|
|
21
|
+
* The unique identifier of the store instance.
|
|
22
|
+
*
|
|
23
|
+
* @public
|
|
22
24
|
*/
|
|
23
25
|
id;
|
|
24
26
|
/**
|
|
@@ -38,7 +40,19 @@ class Store {
|
|
|
38
40
|
historyLength: 1e3
|
|
39
41
|
});
|
|
40
42
|
/**
|
|
41
|
-
*
|
|
43
|
+
* Reactive queries and indexes for efficiently accessing store data.
|
|
44
|
+
* Provides methods for filtering, indexing, and subscribing to subsets of records.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* // Create an index by a property
|
|
49
|
+
* const booksByAuthor = store.query.index('book', 'author')
|
|
50
|
+
*
|
|
51
|
+
* // Get records matching criteria
|
|
52
|
+
* const inStockBooks = store.query.records('book', () => ({
|
|
53
|
+
* inStock: { eq: true }
|
|
54
|
+
* }))
|
|
55
|
+
* ```
|
|
42
56
|
*
|
|
43
57
|
* @public
|
|
44
58
|
* @readonly
|
|
@@ -70,10 +84,53 @@ class Store {
|
|
|
70
84
|
*/
|
|
71
85
|
cancelHistoryReactor() {
|
|
72
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* The schema that defines the structure and validation rules for records in this store.
|
|
89
|
+
*
|
|
90
|
+
* @public
|
|
91
|
+
*/
|
|
73
92
|
schema;
|
|
93
|
+
/**
|
|
94
|
+
* Custom properties associated with this store instance.
|
|
95
|
+
*
|
|
96
|
+
* @public
|
|
97
|
+
*/
|
|
74
98
|
props;
|
|
99
|
+
/**
|
|
100
|
+
* A mapping of record scopes to the set of record type names that belong to each scope.
|
|
101
|
+
* Used to filter records by their persistence and synchronization behavior.
|
|
102
|
+
*
|
|
103
|
+
* @public
|
|
104
|
+
*/
|
|
75
105
|
scopedTypes;
|
|
106
|
+
/**
|
|
107
|
+
* Side effects manager that handles lifecycle events for record operations.
|
|
108
|
+
* Allows registration of callbacks for create, update, delete, and validation events.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* store.sideEffects.registerAfterCreateHandler('book', (book) => {
|
|
113
|
+
* console.log('Book created:', book.title)
|
|
114
|
+
* })
|
|
115
|
+
* ```
|
|
116
|
+
*
|
|
117
|
+
* @public
|
|
118
|
+
*/
|
|
76
119
|
sideEffects = new StoreSideEffects(this);
|
|
120
|
+
/**
|
|
121
|
+
* Creates a new Store instance.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* const store = new Store({
|
|
126
|
+
* schema: StoreSchema.create({ book: Book }),
|
|
127
|
+
* props: { appName: 'MyLibrary' },
|
|
128
|
+
* initialData: savedData
|
|
129
|
+
* })
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* @param config - Configuration object for the store
|
|
133
|
+
*/
|
|
77
134
|
constructor(config) {
|
|
78
135
|
const { initialData, schema, id } = config;
|
|
79
136
|
this.id = id ?? uniqueId();
|
|
@@ -182,10 +239,21 @@ class Store {
|
|
|
182
239
|
this.allRecords().forEach((record) => this.schema.validateRecord(this, record, phase, null));
|
|
183
240
|
}
|
|
184
241
|
/**
|
|
185
|
-
* Add
|
|
242
|
+
* Add or update records in the store. If a record with the same ID already exists, it will be updated.
|
|
243
|
+
* Otherwise, a new record will be created.
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```ts
|
|
247
|
+
* // Add new records
|
|
248
|
+
* const book = Book.create({ title: 'Lathe Of Heaven', author: 'Le Guin' })
|
|
249
|
+
* store.put([book])
|
|
186
250
|
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
251
|
+
* // Update existing record
|
|
252
|
+
* store.put([{ ...book, title: 'The Lathe of Heaven' }])
|
|
253
|
+
* ```
|
|
254
|
+
*
|
|
255
|
+
* @param records - The records to add or update
|
|
256
|
+
* @param phaseOverride - Override the validation phase (used internally)
|
|
189
257
|
* @public
|
|
190
258
|
*/
|
|
191
259
|
put(records, phaseOverride) {
|
|
@@ -236,9 +304,18 @@ class Store {
|
|
|
236
304
|
});
|
|
237
305
|
}
|
|
238
306
|
/**
|
|
239
|
-
* Remove
|
|
307
|
+
* Remove records from the store by their IDs.
|
|
308
|
+
*
|
|
309
|
+
* @example
|
|
310
|
+
* ```ts
|
|
311
|
+
* // Remove a single record
|
|
312
|
+
* store.remove([book.id])
|
|
313
|
+
*
|
|
314
|
+
* // Remove multiple records
|
|
315
|
+
* store.remove([book1.id, book2.id, book3.id])
|
|
316
|
+
* ```
|
|
240
317
|
*
|
|
241
|
-
* @param ids - The
|
|
318
|
+
* @param ids - The IDs of the records to remove
|
|
242
319
|
* @public
|
|
243
320
|
*/
|
|
244
321
|
remove(ids) {
|
|
@@ -265,28 +342,56 @@ class Store {
|
|
|
265
342
|
});
|
|
266
343
|
}
|
|
267
344
|
/**
|
|
268
|
-
* Get
|
|
345
|
+
* Get a record by its ID. This creates a reactive subscription to the record.
|
|
269
346
|
*
|
|
270
|
-
* @
|
|
347
|
+
* @example
|
|
348
|
+
* ```ts
|
|
349
|
+
* const book = store.get(bookId)
|
|
350
|
+
* if (book) {
|
|
351
|
+
* console.log(book.title)
|
|
352
|
+
* }
|
|
353
|
+
* ```
|
|
354
|
+
*
|
|
355
|
+
* @param id - The ID of the record to get
|
|
356
|
+
* @returns The record if it exists, undefined otherwise
|
|
271
357
|
* @public
|
|
272
358
|
*/
|
|
273
359
|
get(id) {
|
|
274
360
|
return this.records.get(id);
|
|
275
361
|
}
|
|
276
362
|
/**
|
|
277
|
-
* Get
|
|
363
|
+
* Get a record by its ID without creating a reactive subscription.
|
|
364
|
+
* Use this when you need to access a record but don't want reactive updates.
|
|
278
365
|
*
|
|
279
|
-
* @
|
|
366
|
+
* @example
|
|
367
|
+
* ```ts
|
|
368
|
+
* // Won't trigger reactive updates when this record changes
|
|
369
|
+
* const book = store.unsafeGetWithoutCapture(bookId)
|
|
370
|
+
* ```
|
|
371
|
+
*
|
|
372
|
+
* @param id - The ID of the record to get
|
|
373
|
+
* @returns The record if it exists, undefined otherwise
|
|
280
374
|
* @public
|
|
281
375
|
*/
|
|
282
376
|
unsafeGetWithoutCapture(id) {
|
|
283
377
|
return this.records.__unsafe__getWithoutCapture(id);
|
|
284
378
|
}
|
|
285
379
|
/**
|
|
286
|
-
*
|
|
380
|
+
* Serialize the store's records to a plain JavaScript object.
|
|
381
|
+
* Only includes records matching the specified scope.
|
|
382
|
+
*
|
|
383
|
+
* @example
|
|
384
|
+
* ```ts
|
|
385
|
+
* // Serialize only document records (default)
|
|
386
|
+
* const documentData = store.serialize('document')
|
|
287
387
|
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
388
|
+
* // Serialize all records
|
|
389
|
+
* const allData = store.serialize('all')
|
|
390
|
+
* ```
|
|
391
|
+
*
|
|
392
|
+
* @param scope - The scope of records to serialize. Defaults to 'document'
|
|
393
|
+
* @returns The serialized store data
|
|
394
|
+
* @public
|
|
290
395
|
*/
|
|
291
396
|
serialize(scope = "document") {
|
|
292
397
|
const result = {};
|
|
@@ -299,14 +404,20 @@ class Store {
|
|
|
299
404
|
}
|
|
300
405
|
/**
|
|
301
406
|
* Get a serialized snapshot of the store and its schema.
|
|
407
|
+
* This includes both the data and schema information needed for proper migration.
|
|
302
408
|
*
|
|
409
|
+
* @example
|
|
303
410
|
* ```ts
|
|
304
411
|
* const snapshot = store.getStoreSnapshot()
|
|
305
|
-
*
|
|
306
|
-
* ```
|
|
412
|
+
* localStorage.setItem('myApp', JSON.stringify(snapshot))
|
|
307
413
|
*
|
|
308
|
-
*
|
|
414
|
+
* // Later...
|
|
415
|
+
* const saved = JSON.parse(localStorage.getItem('myApp'))
|
|
416
|
+
* store.loadStoreSnapshot(saved)
|
|
417
|
+
* ```
|
|
309
418
|
*
|
|
419
|
+
* @param scope - The scope of records to serialize. Defaults to 'document'
|
|
420
|
+
* @returns A snapshot containing both store data and schema information
|
|
310
421
|
* @public
|
|
311
422
|
*/
|
|
312
423
|
getStoreSnapshot(scope = "document") {
|
|
@@ -316,14 +427,18 @@ class Store {
|
|
|
316
427
|
};
|
|
317
428
|
}
|
|
318
429
|
/**
|
|
319
|
-
* Migrate a serialized snapshot
|
|
430
|
+
* Migrate a serialized snapshot to the current schema version.
|
|
431
|
+
* This applies any necessary migrations to bring old data up to date.
|
|
320
432
|
*
|
|
433
|
+
* @example
|
|
321
434
|
* ```ts
|
|
322
|
-
* const
|
|
323
|
-
* store.migrateSnapshot(
|
|
435
|
+
* const oldSnapshot = JSON.parse(localStorage.getItem('myApp'))
|
|
436
|
+
* const migratedSnapshot = store.migrateSnapshot(oldSnapshot)
|
|
324
437
|
* ```
|
|
325
438
|
*
|
|
326
|
-
* @param snapshot - The snapshot to
|
|
439
|
+
* @param snapshot - The snapshot to migrate
|
|
440
|
+
* @returns The migrated snapshot with current schema version
|
|
441
|
+
* @throws Error if migration fails
|
|
327
442
|
* @public
|
|
328
443
|
*/
|
|
329
444
|
migrateSnapshot(snapshot) {
|
|
@@ -337,14 +452,17 @@ class Store {
|
|
|
337
452
|
};
|
|
338
453
|
}
|
|
339
454
|
/**
|
|
340
|
-
* Load a serialized snapshot.
|
|
455
|
+
* Load a serialized snapshot into the store, replacing all current data.
|
|
456
|
+
* The snapshot will be automatically migrated to the current schema version if needed.
|
|
341
457
|
*
|
|
458
|
+
* @example
|
|
342
459
|
* ```ts
|
|
343
|
-
* const snapshot =
|
|
460
|
+
* const snapshot = JSON.parse(localStorage.getItem('myApp'))
|
|
344
461
|
* store.loadStoreSnapshot(snapshot)
|
|
345
462
|
* ```
|
|
346
463
|
*
|
|
347
|
-
* @param snapshot - The snapshot to load
|
|
464
|
+
* @param snapshot - The snapshot to load
|
|
465
|
+
* @throws Error if migration fails or snapshot is invalid
|
|
348
466
|
* @public
|
|
349
467
|
*/
|
|
350
468
|
loadStoreSnapshot(snapshot) {
|
|
@@ -365,16 +483,28 @@ class Store {
|
|
|
365
483
|
}
|
|
366
484
|
}
|
|
367
485
|
/**
|
|
368
|
-
* Get an array of all
|
|
486
|
+
* Get an array of all records in the store.
|
|
369
487
|
*
|
|
370
|
-
* @
|
|
488
|
+
* @example
|
|
489
|
+
* ```ts
|
|
490
|
+
* const allRecords = store.allRecords()
|
|
491
|
+
* const books = allRecords.filter(r => r.typeName === 'book')
|
|
492
|
+
* ```
|
|
493
|
+
*
|
|
494
|
+
* @returns An array containing all records in the store
|
|
371
495
|
* @public
|
|
372
496
|
*/
|
|
373
497
|
allRecords() {
|
|
374
498
|
return Array.from(this.records.values());
|
|
375
499
|
}
|
|
376
500
|
/**
|
|
377
|
-
*
|
|
501
|
+
* Remove all records from the store.
|
|
502
|
+
*
|
|
503
|
+
* @example
|
|
504
|
+
* ```ts
|
|
505
|
+
* store.clear()
|
|
506
|
+
* console.log(store.allRecords().length) // 0
|
|
507
|
+
* ```
|
|
378
508
|
*
|
|
379
509
|
* @public
|
|
380
510
|
*/
|
|
@@ -382,11 +512,20 @@ class Store {
|
|
|
382
512
|
this.remove(Array.from(this.records.keys()));
|
|
383
513
|
}
|
|
384
514
|
/**
|
|
385
|
-
* Update a record. To update multiple records at once,
|
|
386
|
-
* `TypedStore` class.
|
|
515
|
+
* Update a single record using an updater function. To update multiple records at once,
|
|
516
|
+
* use the `update` method of the `TypedStore` class.
|
|
387
517
|
*
|
|
388
|
-
* @
|
|
389
|
-
*
|
|
518
|
+
* @example
|
|
519
|
+
* ```ts
|
|
520
|
+
* store.update(book.id, (book) => ({
|
|
521
|
+
* ...book,
|
|
522
|
+
* title: 'Updated Title'
|
|
523
|
+
* }))
|
|
524
|
+
* ```
|
|
525
|
+
*
|
|
526
|
+
* @param id - The ID of the record to update
|
|
527
|
+
* @param updater - A function that receives the current record and returns the updated record
|
|
528
|
+
* @public
|
|
390
529
|
*/
|
|
391
530
|
update(id, updater) {
|
|
392
531
|
const existing = this.unsafeGetWithoutCapture(id);
|
|
@@ -397,20 +536,47 @@ class Store {
|
|
|
397
536
|
this.put([updater(existing)]);
|
|
398
537
|
}
|
|
399
538
|
/**
|
|
400
|
-
*
|
|
539
|
+
* Check whether a record with the given ID exists in the store.
|
|
401
540
|
*
|
|
402
|
-
* @
|
|
541
|
+
* @example
|
|
542
|
+
* ```ts
|
|
543
|
+
* if (store.has(bookId)) {
|
|
544
|
+
* console.log('Book exists!')
|
|
545
|
+
* }
|
|
546
|
+
* ```
|
|
547
|
+
*
|
|
548
|
+
* @param id - The ID of the record to check
|
|
549
|
+
* @returns True if the record exists, false otherwise
|
|
403
550
|
* @public
|
|
404
551
|
*/
|
|
405
552
|
has(id) {
|
|
406
553
|
return this.records.has(id);
|
|
407
554
|
}
|
|
408
555
|
/**
|
|
409
|
-
* Add a
|
|
556
|
+
* Add a listener that will be called when the store changes.
|
|
557
|
+
* Returns a function to remove the listener.
|
|
558
|
+
*
|
|
559
|
+
* @example
|
|
560
|
+
* ```ts
|
|
561
|
+
* const removeListener = store.listen((entry) => {
|
|
562
|
+
* console.log('Changes:', entry.changes)
|
|
563
|
+
* console.log('Source:', entry.source)
|
|
564
|
+
* })
|
|
565
|
+
*
|
|
566
|
+
* // Listen only to user changes to document records
|
|
567
|
+
* const removeDocumentListener = store.listen(
|
|
568
|
+
* (entry) => console.log('Document changed:', entry),
|
|
569
|
+
* { source: 'user', scope: 'document' }
|
|
570
|
+
* )
|
|
410
571
|
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
572
|
+
* // Later, remove the listener
|
|
573
|
+
* removeListener()
|
|
574
|
+
* ```
|
|
575
|
+
*
|
|
576
|
+
* @param onHistory - The listener function to call when changes occur
|
|
577
|
+
* @param filters - Optional filters to control when the listener is called
|
|
578
|
+
* @returns A function that removes the listener when called
|
|
579
|
+
* @public
|
|
414
580
|
*/
|
|
415
581
|
listen(onHistory, filters) {
|
|
416
582
|
this._flushHistory();
|
|
@@ -435,9 +601,19 @@ class Store {
|
|
|
435
601
|
}
|
|
436
602
|
isMergingRemoteChanges = false;
|
|
437
603
|
/**
|
|
438
|
-
* Merge changes from a remote source
|
|
604
|
+
* Merge changes from a remote source. Changes made within the provided function
|
|
605
|
+
* will be marked with source 'remote' instead of 'user'.
|
|
606
|
+
*
|
|
607
|
+
* @example
|
|
608
|
+
* ```ts
|
|
609
|
+
* // Changes from sync/collaboration
|
|
610
|
+
* store.mergeRemoteChanges(() => {
|
|
611
|
+
* store.put(remoteRecords)
|
|
612
|
+
* store.remove(deletedIds)
|
|
613
|
+
* })
|
|
614
|
+
* ```
|
|
439
615
|
*
|
|
440
|
-
* @param fn - A function that
|
|
616
|
+
* @param fn - A function that applies the remote changes
|
|
441
617
|
* @public
|
|
442
618
|
*/
|
|
443
619
|
mergeRemoteChanges(fn) {
|
|
@@ -666,26 +842,44 @@ function squashHistoryEntries(entries) {
|
|
|
666
842
|
class HistoryAccumulator {
|
|
667
843
|
_history = [];
|
|
668
844
|
_interceptors = /* @__PURE__ */ new Set();
|
|
845
|
+
/**
|
|
846
|
+
* Add an interceptor that will be called for each history entry.
|
|
847
|
+
* Returns a function to remove the interceptor.
|
|
848
|
+
*/
|
|
669
849
|
addInterceptor(fn) {
|
|
670
850
|
this._interceptors.add(fn);
|
|
671
851
|
return () => {
|
|
672
852
|
this._interceptors.delete(fn);
|
|
673
853
|
};
|
|
674
854
|
}
|
|
855
|
+
/**
|
|
856
|
+
* Add a history entry to the accumulator.
|
|
857
|
+
* Calls all registered interceptors with the entry.
|
|
858
|
+
*/
|
|
675
859
|
add(entry) {
|
|
676
860
|
this._history.push(entry);
|
|
677
861
|
for (const interceptor of this._interceptors) {
|
|
678
862
|
interceptor(entry);
|
|
679
863
|
}
|
|
680
864
|
}
|
|
865
|
+
/**
|
|
866
|
+
* Flush all accumulated history entries, squashing adjacent entries from the same source.
|
|
867
|
+
* Clears the internal history buffer.
|
|
868
|
+
*/
|
|
681
869
|
flush() {
|
|
682
870
|
const history = squashHistoryEntries(this._history);
|
|
683
871
|
this._history = [];
|
|
684
872
|
return history;
|
|
685
873
|
}
|
|
874
|
+
/**
|
|
875
|
+
* Clear all accumulated history entries without flushing.
|
|
876
|
+
*/
|
|
686
877
|
clear() {
|
|
687
878
|
this._history = [];
|
|
688
879
|
}
|
|
880
|
+
/**
|
|
881
|
+
* Check if there are any accumulated history entries.
|
|
882
|
+
*/
|
|
689
883
|
hasChanges() {
|
|
690
884
|
return this._history.length > 0;
|
|
691
885
|
}
|