@tldraw/store 4.1.0-canary.8351372cc72b → 4.1.0-canary.8659480a0467
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 +1880 -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 +1880 -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 +144 -9
- 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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAiC;
|
|
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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAiC;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,SAAK,+BAAiB,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,SAAK,+BAAiB,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,SAAK,+BAAiB,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-cjs/lib/Store.js
CHANGED
|
@@ -31,7 +31,9 @@ var import_StoreSideEffects = require("./StoreSideEffects");
|
|
|
31
31
|
var import_devFreeze = require("./devFreeze");
|
|
32
32
|
class Store {
|
|
33
33
|
/**
|
|
34
|
-
* The
|
|
34
|
+
* The unique identifier of the store instance.
|
|
35
|
+
*
|
|
36
|
+
* @public
|
|
35
37
|
*/
|
|
36
38
|
id;
|
|
37
39
|
/**
|
|
@@ -51,7 +53,19 @@ class Store {
|
|
|
51
53
|
historyLength: 1e3
|
|
52
54
|
});
|
|
53
55
|
/**
|
|
54
|
-
*
|
|
56
|
+
* Reactive queries and indexes for efficiently accessing store data.
|
|
57
|
+
* Provides methods for filtering, indexing, and subscribing to subsets of records.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* // Create an index by a property
|
|
62
|
+
* const booksByAuthor = store.query.index('book', 'author')
|
|
63
|
+
*
|
|
64
|
+
* // Get records matching criteria
|
|
65
|
+
* const inStockBooks = store.query.records('book', () => ({
|
|
66
|
+
* inStock: { eq: true }
|
|
67
|
+
* }))
|
|
68
|
+
* ```
|
|
55
69
|
*
|
|
56
70
|
* @public
|
|
57
71
|
* @readonly
|
|
@@ -83,10 +97,53 @@ class Store {
|
|
|
83
97
|
*/
|
|
84
98
|
cancelHistoryReactor() {
|
|
85
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* The schema that defines the structure and validation rules for records in this store.
|
|
102
|
+
*
|
|
103
|
+
* @public
|
|
104
|
+
*/
|
|
86
105
|
schema;
|
|
106
|
+
/**
|
|
107
|
+
* Custom properties associated with this store instance.
|
|
108
|
+
*
|
|
109
|
+
* @public
|
|
110
|
+
*/
|
|
87
111
|
props;
|
|
112
|
+
/**
|
|
113
|
+
* A mapping of record scopes to the set of record type names that belong to each scope.
|
|
114
|
+
* Used to filter records by their persistence and synchronization behavior.
|
|
115
|
+
*
|
|
116
|
+
* @public
|
|
117
|
+
*/
|
|
88
118
|
scopedTypes;
|
|
119
|
+
/**
|
|
120
|
+
* Side effects manager that handles lifecycle events for record operations.
|
|
121
|
+
* Allows registration of callbacks for create, update, delete, and validation events.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* store.sideEffects.registerAfterCreateHandler('book', (book) => {
|
|
126
|
+
* console.log('Book created:', book.title)
|
|
127
|
+
* })
|
|
128
|
+
* ```
|
|
129
|
+
*
|
|
130
|
+
* @public
|
|
131
|
+
*/
|
|
89
132
|
sideEffects = new import_StoreSideEffects.StoreSideEffects(this);
|
|
133
|
+
/**
|
|
134
|
+
* Creates a new Store instance.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```ts
|
|
138
|
+
* const store = new Store({
|
|
139
|
+
* schema: StoreSchema.create({ book: Book }),
|
|
140
|
+
* props: { appName: 'MyLibrary' },
|
|
141
|
+
* initialData: savedData
|
|
142
|
+
* })
|
|
143
|
+
* ```
|
|
144
|
+
*
|
|
145
|
+
* @param config - Configuration object for the store
|
|
146
|
+
*/
|
|
90
147
|
constructor(config) {
|
|
91
148
|
const { initialData, schema, id } = config;
|
|
92
149
|
this.id = id ?? (0, import_utils.uniqueId)();
|
|
@@ -195,10 +252,21 @@ class Store {
|
|
|
195
252
|
this.allRecords().forEach((record) => this.schema.validateRecord(this, record, phase, null));
|
|
196
253
|
}
|
|
197
254
|
/**
|
|
198
|
-
* Add
|
|
255
|
+
* Add or update records in the store. If a record with the same ID already exists, it will be updated.
|
|
256
|
+
* Otherwise, a new record will be created.
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* ```ts
|
|
260
|
+
* // Add new records
|
|
261
|
+
* const book = Book.create({ title: 'Lathe Of Heaven', author: 'Le Guin' })
|
|
262
|
+
* store.put([book])
|
|
199
263
|
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
264
|
+
* // Update existing record
|
|
265
|
+
* store.put([{ ...book, title: 'The Lathe of Heaven' }])
|
|
266
|
+
* ```
|
|
267
|
+
*
|
|
268
|
+
* @param records - The records to add or update
|
|
269
|
+
* @param phaseOverride - Override the validation phase (used internally)
|
|
202
270
|
* @public
|
|
203
271
|
*/
|
|
204
272
|
put(records, phaseOverride) {
|
|
@@ -249,9 +317,18 @@ class Store {
|
|
|
249
317
|
});
|
|
250
318
|
}
|
|
251
319
|
/**
|
|
252
|
-
* Remove
|
|
320
|
+
* Remove records from the store by their IDs.
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* ```ts
|
|
324
|
+
* // Remove a single record
|
|
325
|
+
* store.remove([book.id])
|
|
326
|
+
*
|
|
327
|
+
* // Remove multiple records
|
|
328
|
+
* store.remove([book1.id, book2.id, book3.id])
|
|
329
|
+
* ```
|
|
253
330
|
*
|
|
254
|
-
* @param ids - The
|
|
331
|
+
* @param ids - The IDs of the records to remove
|
|
255
332
|
* @public
|
|
256
333
|
*/
|
|
257
334
|
remove(ids) {
|
|
@@ -278,28 +355,56 @@ class Store {
|
|
|
278
355
|
});
|
|
279
356
|
}
|
|
280
357
|
/**
|
|
281
|
-
* Get
|
|
358
|
+
* Get a record by its ID. This creates a reactive subscription to the record.
|
|
282
359
|
*
|
|
283
|
-
* @
|
|
360
|
+
* @example
|
|
361
|
+
* ```ts
|
|
362
|
+
* const book = store.get(bookId)
|
|
363
|
+
* if (book) {
|
|
364
|
+
* console.log(book.title)
|
|
365
|
+
* }
|
|
366
|
+
* ```
|
|
367
|
+
*
|
|
368
|
+
* @param id - The ID of the record to get
|
|
369
|
+
* @returns The record if it exists, undefined otherwise
|
|
284
370
|
* @public
|
|
285
371
|
*/
|
|
286
372
|
get(id) {
|
|
287
373
|
return this.records.get(id);
|
|
288
374
|
}
|
|
289
375
|
/**
|
|
290
|
-
* Get
|
|
376
|
+
* Get a record by its ID without creating a reactive subscription.
|
|
377
|
+
* Use this when you need to access a record but don't want reactive updates.
|
|
291
378
|
*
|
|
292
|
-
* @
|
|
379
|
+
* @example
|
|
380
|
+
* ```ts
|
|
381
|
+
* // Won't trigger reactive updates when this record changes
|
|
382
|
+
* const book = store.unsafeGetWithoutCapture(bookId)
|
|
383
|
+
* ```
|
|
384
|
+
*
|
|
385
|
+
* @param id - The ID of the record to get
|
|
386
|
+
* @returns The record if it exists, undefined otherwise
|
|
293
387
|
* @public
|
|
294
388
|
*/
|
|
295
389
|
unsafeGetWithoutCapture(id) {
|
|
296
390
|
return this.records.__unsafe__getWithoutCapture(id);
|
|
297
391
|
}
|
|
298
392
|
/**
|
|
299
|
-
*
|
|
393
|
+
* Serialize the store's records to a plain JavaScript object.
|
|
394
|
+
* Only includes records matching the specified scope.
|
|
395
|
+
*
|
|
396
|
+
* @example
|
|
397
|
+
* ```ts
|
|
398
|
+
* // Serialize only document records (default)
|
|
399
|
+
* const documentData = store.serialize('document')
|
|
300
400
|
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
401
|
+
* // Serialize all records
|
|
402
|
+
* const allData = store.serialize('all')
|
|
403
|
+
* ```
|
|
404
|
+
*
|
|
405
|
+
* @param scope - The scope of records to serialize. Defaults to 'document'
|
|
406
|
+
* @returns The serialized store data
|
|
407
|
+
* @public
|
|
303
408
|
*/
|
|
304
409
|
serialize(scope = "document") {
|
|
305
410
|
const result = {};
|
|
@@ -312,14 +417,20 @@ class Store {
|
|
|
312
417
|
}
|
|
313
418
|
/**
|
|
314
419
|
* Get a serialized snapshot of the store and its schema.
|
|
420
|
+
* This includes both the data and schema information needed for proper migration.
|
|
315
421
|
*
|
|
422
|
+
* @example
|
|
316
423
|
* ```ts
|
|
317
424
|
* const snapshot = store.getStoreSnapshot()
|
|
318
|
-
*
|
|
319
|
-
* ```
|
|
425
|
+
* localStorage.setItem('myApp', JSON.stringify(snapshot))
|
|
320
426
|
*
|
|
321
|
-
*
|
|
427
|
+
* // Later...
|
|
428
|
+
* const saved = JSON.parse(localStorage.getItem('myApp'))
|
|
429
|
+
* store.loadStoreSnapshot(saved)
|
|
430
|
+
* ```
|
|
322
431
|
*
|
|
432
|
+
* @param scope - The scope of records to serialize. Defaults to 'document'
|
|
433
|
+
* @returns A snapshot containing both store data and schema information
|
|
323
434
|
* @public
|
|
324
435
|
*/
|
|
325
436
|
getStoreSnapshot(scope = "document") {
|
|
@@ -329,14 +440,18 @@ class Store {
|
|
|
329
440
|
};
|
|
330
441
|
}
|
|
331
442
|
/**
|
|
332
|
-
* Migrate a serialized snapshot
|
|
443
|
+
* Migrate a serialized snapshot to the current schema version.
|
|
444
|
+
* This applies any necessary migrations to bring old data up to date.
|
|
333
445
|
*
|
|
446
|
+
* @example
|
|
334
447
|
* ```ts
|
|
335
|
-
* const
|
|
336
|
-
* store.migrateSnapshot(
|
|
448
|
+
* const oldSnapshot = JSON.parse(localStorage.getItem('myApp'))
|
|
449
|
+
* const migratedSnapshot = store.migrateSnapshot(oldSnapshot)
|
|
337
450
|
* ```
|
|
338
451
|
*
|
|
339
|
-
* @param snapshot - The snapshot to
|
|
452
|
+
* @param snapshot - The snapshot to migrate
|
|
453
|
+
* @returns The migrated snapshot with current schema version
|
|
454
|
+
* @throws Error if migration fails
|
|
340
455
|
* @public
|
|
341
456
|
*/
|
|
342
457
|
migrateSnapshot(snapshot) {
|
|
@@ -350,14 +465,17 @@ class Store {
|
|
|
350
465
|
};
|
|
351
466
|
}
|
|
352
467
|
/**
|
|
353
|
-
* Load a serialized snapshot.
|
|
468
|
+
* Load a serialized snapshot into the store, replacing all current data.
|
|
469
|
+
* The snapshot will be automatically migrated to the current schema version if needed.
|
|
354
470
|
*
|
|
471
|
+
* @example
|
|
355
472
|
* ```ts
|
|
356
|
-
* const snapshot =
|
|
473
|
+
* const snapshot = JSON.parse(localStorage.getItem('myApp'))
|
|
357
474
|
* store.loadStoreSnapshot(snapshot)
|
|
358
475
|
* ```
|
|
359
476
|
*
|
|
360
|
-
* @param snapshot - The snapshot to load
|
|
477
|
+
* @param snapshot - The snapshot to load
|
|
478
|
+
* @throws Error if migration fails or snapshot is invalid
|
|
361
479
|
* @public
|
|
362
480
|
*/
|
|
363
481
|
loadStoreSnapshot(snapshot) {
|
|
@@ -378,16 +496,28 @@ class Store {
|
|
|
378
496
|
}
|
|
379
497
|
}
|
|
380
498
|
/**
|
|
381
|
-
* Get an array of all
|
|
499
|
+
* Get an array of all records in the store.
|
|
382
500
|
*
|
|
383
|
-
* @
|
|
501
|
+
* @example
|
|
502
|
+
* ```ts
|
|
503
|
+
* const allRecords = store.allRecords()
|
|
504
|
+
* const books = allRecords.filter(r => r.typeName === 'book')
|
|
505
|
+
* ```
|
|
506
|
+
*
|
|
507
|
+
* @returns An array containing all records in the store
|
|
384
508
|
* @public
|
|
385
509
|
*/
|
|
386
510
|
allRecords() {
|
|
387
511
|
return Array.from(this.records.values());
|
|
388
512
|
}
|
|
389
513
|
/**
|
|
390
|
-
*
|
|
514
|
+
* Remove all records from the store.
|
|
515
|
+
*
|
|
516
|
+
* @example
|
|
517
|
+
* ```ts
|
|
518
|
+
* store.clear()
|
|
519
|
+
* console.log(store.allRecords().length) // 0
|
|
520
|
+
* ```
|
|
391
521
|
*
|
|
392
522
|
* @public
|
|
393
523
|
*/
|
|
@@ -395,11 +525,20 @@ class Store {
|
|
|
395
525
|
this.remove(Array.from(this.records.keys()));
|
|
396
526
|
}
|
|
397
527
|
/**
|
|
398
|
-
* Update a record. To update multiple records at once,
|
|
399
|
-
* `TypedStore` class.
|
|
528
|
+
* Update a single record using an updater function. To update multiple records at once,
|
|
529
|
+
* use the `update` method of the `TypedStore` class.
|
|
400
530
|
*
|
|
401
|
-
* @
|
|
402
|
-
*
|
|
531
|
+
* @example
|
|
532
|
+
* ```ts
|
|
533
|
+
* store.update(book.id, (book) => ({
|
|
534
|
+
* ...book,
|
|
535
|
+
* title: 'Updated Title'
|
|
536
|
+
* }))
|
|
537
|
+
* ```
|
|
538
|
+
*
|
|
539
|
+
* @param id - The ID of the record to update
|
|
540
|
+
* @param updater - A function that receives the current record and returns the updated record
|
|
541
|
+
* @public
|
|
403
542
|
*/
|
|
404
543
|
update(id, updater) {
|
|
405
544
|
const existing = this.unsafeGetWithoutCapture(id);
|
|
@@ -410,20 +549,47 @@ class Store {
|
|
|
410
549
|
this.put([updater(existing)]);
|
|
411
550
|
}
|
|
412
551
|
/**
|
|
413
|
-
*
|
|
552
|
+
* Check whether a record with the given ID exists in the store.
|
|
414
553
|
*
|
|
415
|
-
* @
|
|
554
|
+
* @example
|
|
555
|
+
* ```ts
|
|
556
|
+
* if (store.has(bookId)) {
|
|
557
|
+
* console.log('Book exists!')
|
|
558
|
+
* }
|
|
559
|
+
* ```
|
|
560
|
+
*
|
|
561
|
+
* @param id - The ID of the record to check
|
|
562
|
+
* @returns True if the record exists, false otherwise
|
|
416
563
|
* @public
|
|
417
564
|
*/
|
|
418
565
|
has(id) {
|
|
419
566
|
return this.records.has(id);
|
|
420
567
|
}
|
|
421
568
|
/**
|
|
422
|
-
* Add a
|
|
569
|
+
* Add a listener that will be called when the store changes.
|
|
570
|
+
* Returns a function to remove the listener.
|
|
571
|
+
*
|
|
572
|
+
* @example
|
|
573
|
+
* ```ts
|
|
574
|
+
* const removeListener = store.listen((entry) => {
|
|
575
|
+
* console.log('Changes:', entry.changes)
|
|
576
|
+
* console.log('Source:', entry.source)
|
|
577
|
+
* })
|
|
578
|
+
*
|
|
579
|
+
* // Listen only to user changes to document records
|
|
580
|
+
* const removeDocumentListener = store.listen(
|
|
581
|
+
* (entry) => console.log('Document changed:', entry),
|
|
582
|
+
* { source: 'user', scope: 'document' }
|
|
583
|
+
* )
|
|
423
584
|
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
585
|
+
* // Later, remove the listener
|
|
586
|
+
* removeListener()
|
|
587
|
+
* ```
|
|
588
|
+
*
|
|
589
|
+
* @param onHistory - The listener function to call when changes occur
|
|
590
|
+
* @param filters - Optional filters to control when the listener is called
|
|
591
|
+
* @returns A function that removes the listener when called
|
|
592
|
+
* @public
|
|
427
593
|
*/
|
|
428
594
|
listen(onHistory, filters) {
|
|
429
595
|
this._flushHistory();
|
|
@@ -448,9 +614,19 @@ class Store {
|
|
|
448
614
|
}
|
|
449
615
|
isMergingRemoteChanges = false;
|
|
450
616
|
/**
|
|
451
|
-
* Merge changes from a remote source
|
|
617
|
+
* Merge changes from a remote source. Changes made within the provided function
|
|
618
|
+
* will be marked with source 'remote' instead of 'user'.
|
|
619
|
+
*
|
|
620
|
+
* @example
|
|
621
|
+
* ```ts
|
|
622
|
+
* // Changes from sync/collaboration
|
|
623
|
+
* store.mergeRemoteChanges(() => {
|
|
624
|
+
* store.put(remoteRecords)
|
|
625
|
+
* store.remove(deletedIds)
|
|
626
|
+
* })
|
|
627
|
+
* ```
|
|
452
628
|
*
|
|
453
|
-
* @param fn - A function that
|
|
629
|
+
* @param fn - A function that applies the remote changes
|
|
454
630
|
* @public
|
|
455
631
|
*/
|
|
456
632
|
mergeRemoteChanges(fn) {
|
|
@@ -679,26 +855,44 @@ function squashHistoryEntries(entries) {
|
|
|
679
855
|
class HistoryAccumulator {
|
|
680
856
|
_history = [];
|
|
681
857
|
_interceptors = /* @__PURE__ */ new Set();
|
|
858
|
+
/**
|
|
859
|
+
* Add an interceptor that will be called for each history entry.
|
|
860
|
+
* Returns a function to remove the interceptor.
|
|
861
|
+
*/
|
|
682
862
|
addInterceptor(fn) {
|
|
683
863
|
this._interceptors.add(fn);
|
|
684
864
|
return () => {
|
|
685
865
|
this._interceptors.delete(fn);
|
|
686
866
|
};
|
|
687
867
|
}
|
|
868
|
+
/**
|
|
869
|
+
* Add a history entry to the accumulator.
|
|
870
|
+
* Calls all registered interceptors with the entry.
|
|
871
|
+
*/
|
|
688
872
|
add(entry) {
|
|
689
873
|
this._history.push(entry);
|
|
690
874
|
for (const interceptor of this._interceptors) {
|
|
691
875
|
interceptor(entry);
|
|
692
876
|
}
|
|
693
877
|
}
|
|
878
|
+
/**
|
|
879
|
+
* Flush all accumulated history entries, squashing adjacent entries from the same source.
|
|
880
|
+
* Clears the internal history buffer.
|
|
881
|
+
*/
|
|
694
882
|
flush() {
|
|
695
883
|
const history = squashHistoryEntries(this._history);
|
|
696
884
|
this._history = [];
|
|
697
885
|
return history;
|
|
698
886
|
}
|
|
887
|
+
/**
|
|
888
|
+
* Clear all accumulated history entries without flushing.
|
|
889
|
+
*/
|
|
699
890
|
clear() {
|
|
700
891
|
this._history = [];
|
|
701
892
|
}
|
|
893
|
+
/**
|
|
894
|
+
* Check if there are any accumulated history entries.
|
|
895
|
+
*/
|
|
702
896
|
hasChanges() {
|
|
703
897
|
return this._history.length > 0;
|
|
704
898
|
}
|