@tldraw/store 4.1.0-canary.e653ec63c99b → 4.1.0-canary.e87046ba1a0c
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
package/src/lib/Store.ts
CHANGED
|
@@ -20,105 +20,328 @@ import { SerializedSchema, StoreSchema } from './StoreSchema'
|
|
|
20
20
|
import { StoreSideEffects } from './StoreSideEffects'
|
|
21
21
|
import { devFreeze } from './devFreeze'
|
|
22
22
|
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Extracts the record type from a record ID type.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* type BookId = RecordId<Book>
|
|
29
|
+
* type BookType = RecordFromId<BookId> // Book
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @public
|
|
33
|
+
*/
|
|
24
34
|
export type RecordFromId<K extends RecordId<UnknownRecord>> =
|
|
25
35
|
K extends RecordId<infer R> ? R : never
|
|
26
36
|
|
|
27
37
|
/**
|
|
28
38
|
* A diff describing the changes to a collection.
|
|
29
39
|
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* const diff: CollectionDiff<string> = {
|
|
43
|
+
* added: new Set(['newItem']),
|
|
44
|
+
* removed: new Set(['oldItem'])
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
30
48
|
* @public
|
|
31
49
|
*/
|
|
32
50
|
export interface CollectionDiff<T> {
|
|
51
|
+
/** Items that were added to the collection */
|
|
33
52
|
added?: Set<T>
|
|
53
|
+
/** Items that were removed from the collection */
|
|
34
54
|
removed?: Set<T>
|
|
35
55
|
}
|
|
36
56
|
|
|
37
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* The source of a change to the store.
|
|
59
|
+
* - `'user'` - Changes originating from local user actions
|
|
60
|
+
* - `'remote'` - Changes originating from remote synchronization
|
|
61
|
+
*
|
|
62
|
+
* @public
|
|
63
|
+
*/
|
|
38
64
|
export type ChangeSource = 'user' | 'remote'
|
|
39
65
|
|
|
40
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Filters for store listeners to control which changes trigger the listener.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* const filters: StoreListenerFilters = {
|
|
72
|
+
* source: 'user', // Only listen to user changes
|
|
73
|
+
* scope: 'document' // Only listen to document-scoped records
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* @public
|
|
78
|
+
*/
|
|
41
79
|
export interface StoreListenerFilters {
|
|
80
|
+
/** Filter by the source of changes */
|
|
42
81
|
source: ChangeSource | 'all'
|
|
82
|
+
/** Filter by the scope of records */
|
|
43
83
|
scope: RecordScope | 'all'
|
|
44
84
|
}
|
|
45
85
|
|
|
46
86
|
/**
|
|
47
87
|
* An entry containing changes that originated either by user actions or remote changes.
|
|
88
|
+
* History entries are used to track and replay changes to the store.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* const entry: HistoryEntry<Book> = {
|
|
93
|
+
* changes: {
|
|
94
|
+
* added: { 'book:123': bookRecord },
|
|
95
|
+
* updated: {},
|
|
96
|
+
* removed: {}
|
|
97
|
+
* },
|
|
98
|
+
* source: 'user'
|
|
99
|
+
* }
|
|
100
|
+
* ```
|
|
48
101
|
*
|
|
49
102
|
* @public
|
|
50
103
|
*/
|
|
51
104
|
export interface HistoryEntry<R extends UnknownRecord = UnknownRecord> {
|
|
105
|
+
/** The changes that occurred in this history entry */
|
|
52
106
|
changes: RecordsDiff<R>
|
|
107
|
+
/** The source of these changes */
|
|
53
108
|
source: ChangeSource
|
|
54
109
|
}
|
|
55
110
|
|
|
56
111
|
/**
|
|
57
112
|
* A function that will be called when the history changes.
|
|
58
113
|
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```ts
|
|
116
|
+
* const listener: StoreListener<Book> = (entry) => {
|
|
117
|
+
* console.log('Changes:', entry.changes)
|
|
118
|
+
* console.log('Source:', entry.source)
|
|
119
|
+
* }
|
|
120
|
+
*
|
|
121
|
+
* store.listen(listener)
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* @param entry - The history entry containing the changes
|
|
125
|
+
*
|
|
59
126
|
* @public
|
|
60
127
|
*/
|
|
61
128
|
export type StoreListener<R extends UnknownRecord> = (entry: HistoryEntry<R>) => void
|
|
62
129
|
|
|
63
130
|
/**
|
|
64
|
-
* A
|
|
131
|
+
* A computed cache that stores derived data for records.
|
|
132
|
+
* The cache automatically updates when underlying records change and cleans up when records are deleted.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* const expensiveCache = store.createComputedCache(
|
|
137
|
+
* 'expensive',
|
|
138
|
+
* (book: Book) => performExpensiveCalculation(book)
|
|
139
|
+
* )
|
|
140
|
+
*
|
|
141
|
+
* const result = expensiveCache.get(bookId)
|
|
142
|
+
* ```
|
|
65
143
|
*
|
|
66
144
|
* @public
|
|
67
145
|
*/
|
|
68
146
|
export interface ComputedCache<Data, R extends UnknownRecord> {
|
|
147
|
+
/**
|
|
148
|
+
* Get the cached data for a record by its ID.
|
|
149
|
+
*
|
|
150
|
+
* @param id - The ID of the record
|
|
151
|
+
* @returns The cached data or undefined if the record doesn't exist
|
|
152
|
+
*/
|
|
69
153
|
get(id: IdOf<R>): Data | undefined
|
|
70
154
|
}
|
|
71
155
|
|
|
72
|
-
/**
|
|
156
|
+
/**
|
|
157
|
+
* Options for creating a computed cache.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```ts
|
|
161
|
+
* const options: CreateComputedCacheOpts<string[], Book> = {
|
|
162
|
+
* areRecordsEqual: (a, b) => a.title === b.title,
|
|
163
|
+
* areResultsEqual: (a, b) => JSON.stringify(a) === JSON.stringify(b)
|
|
164
|
+
* }
|
|
165
|
+
* ```
|
|
166
|
+
*
|
|
167
|
+
* @public
|
|
168
|
+
*/
|
|
73
169
|
export interface CreateComputedCacheOpts<Data, R extends UnknownRecord> {
|
|
170
|
+
/** Custom equality function for comparing records */
|
|
74
171
|
areRecordsEqual?(a: R, b: R): boolean
|
|
172
|
+
/** Custom equality function for comparing results */
|
|
75
173
|
areResultsEqual?(a: Data, b: Data): boolean
|
|
76
174
|
}
|
|
77
175
|
|
|
78
176
|
/**
|
|
79
177
|
* A serialized snapshot of the record store's values.
|
|
178
|
+
* This is a plain JavaScript object that can be saved to storage or transmitted over the network.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```ts
|
|
182
|
+
* const serialized: SerializedStore<Book> = {
|
|
183
|
+
* 'book:123': { id: 'book:123', typeName: 'book', title: 'The Lathe of Heaven' },
|
|
184
|
+
* 'book:456': { id: 'book:456', typeName: 'book', title: 'The Left Hand of Darkness' }
|
|
185
|
+
* }
|
|
186
|
+
* ```
|
|
80
187
|
*
|
|
81
188
|
* @public
|
|
82
189
|
*/
|
|
83
190
|
export type SerializedStore<R extends UnknownRecord> = Record<IdOf<R>, R>
|
|
84
191
|
|
|
85
|
-
/**
|
|
192
|
+
/**
|
|
193
|
+
* A snapshot of the store including both data and schema information.
|
|
194
|
+
* This enables proper migration when loading data from different schema versions.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```ts
|
|
198
|
+
* const snapshot = store.getStoreSnapshot()
|
|
199
|
+
* // Later...
|
|
200
|
+
* store.loadStoreSnapshot(snapshot)
|
|
201
|
+
* ```
|
|
202
|
+
*
|
|
203
|
+
* @public
|
|
204
|
+
*/
|
|
86
205
|
export interface StoreSnapshot<R extends UnknownRecord> {
|
|
206
|
+
/** The serialized store data */
|
|
87
207
|
store: SerializedStore<R>
|
|
208
|
+
/** The serialized schema information */
|
|
88
209
|
schema: SerializedSchema
|
|
89
210
|
}
|
|
90
211
|
|
|
91
|
-
/**
|
|
212
|
+
/**
|
|
213
|
+
* A validator for store records that ensures data integrity.
|
|
214
|
+
* Validators are called when records are created or updated.
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```ts
|
|
218
|
+
* const bookValidator: StoreValidator<Book> = {
|
|
219
|
+
* validate(record: unknown): Book {
|
|
220
|
+
* // Validate and return the record
|
|
221
|
+
* if (typeof record !== 'object' || !record.title) {
|
|
222
|
+
* throw new Error('Invalid book')
|
|
223
|
+
* }
|
|
224
|
+
* return record as Book
|
|
225
|
+
* }
|
|
226
|
+
* }
|
|
227
|
+
* ```
|
|
228
|
+
*
|
|
229
|
+
* @public
|
|
230
|
+
*/
|
|
92
231
|
export interface StoreValidator<R extends UnknownRecord> {
|
|
232
|
+
/**
|
|
233
|
+
* Validate a record.
|
|
234
|
+
*
|
|
235
|
+
* @param record - The record to validate
|
|
236
|
+
* @returns The validated record
|
|
237
|
+
* @throws When validation fails
|
|
238
|
+
*/
|
|
93
239
|
validate(record: unknown): R
|
|
240
|
+
/**
|
|
241
|
+
* Validate a record using a known good version for reference.
|
|
242
|
+
*
|
|
243
|
+
* @param knownGoodVersion - A known valid version of the record
|
|
244
|
+
* @param record - The record to validate
|
|
245
|
+
* @returns The validated record
|
|
246
|
+
*/
|
|
94
247
|
validateUsingKnownGoodVersion?(knownGoodVersion: R, record: unknown): R
|
|
95
248
|
}
|
|
96
249
|
|
|
97
|
-
/**
|
|
250
|
+
/**
|
|
251
|
+
* A map of validators for each record type in the store.
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* ```ts
|
|
255
|
+
* const validators: StoreValidators<Book | Author> = {
|
|
256
|
+
* book: bookValidator,
|
|
257
|
+
* author: authorValidator
|
|
258
|
+
* }
|
|
259
|
+
* ```
|
|
260
|
+
*
|
|
261
|
+
* @public
|
|
262
|
+
*/
|
|
98
263
|
export type StoreValidators<R extends UnknownRecord> = {
|
|
99
264
|
[K in R['typeName']]: StoreValidator<Extract<R, { typeName: K }>>
|
|
100
265
|
}
|
|
101
266
|
|
|
102
|
-
/**
|
|
267
|
+
/**
|
|
268
|
+
* Information about an error that occurred in the store.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```ts
|
|
272
|
+
* const error: StoreError = {
|
|
273
|
+
* error: new Error('Validation failed'),
|
|
274
|
+
* phase: 'updateRecord',
|
|
275
|
+
* recordBefore: oldRecord,
|
|
276
|
+
* recordAfter: newRecord,
|
|
277
|
+
* isExistingValidationIssue: false
|
|
278
|
+
* }
|
|
279
|
+
* ```
|
|
280
|
+
*
|
|
281
|
+
* @public
|
|
282
|
+
*/
|
|
103
283
|
export interface StoreError {
|
|
284
|
+
/** The error that occurred */
|
|
104
285
|
error: Error
|
|
286
|
+
/** The phase during which the error occurred */
|
|
105
287
|
phase: 'initialize' | 'createRecord' | 'updateRecord' | 'tests'
|
|
288
|
+
/** The record state before the operation (if applicable) */
|
|
106
289
|
recordBefore?: unknown
|
|
290
|
+
/** The record state after the operation */
|
|
107
291
|
recordAfter: unknown
|
|
292
|
+
/** Whether this is an existing validation issue */
|
|
108
293
|
isExistingValidationIssue: boolean
|
|
109
294
|
}
|
|
110
295
|
|
|
111
|
-
/**
|
|
296
|
+
/**
|
|
297
|
+
* Extract the record type from a Store type.
|
|
298
|
+
* Used internally for type inference.
|
|
299
|
+
*
|
|
300
|
+
* @internal
|
|
301
|
+
*/
|
|
112
302
|
export type StoreRecord<S extends Store<any>> = S extends Store<infer R> ? R : never
|
|
113
303
|
|
|
114
304
|
/**
|
|
115
|
-
* A store of records.
|
|
305
|
+
* A reactive store that manages collections of typed records.
|
|
306
|
+
*
|
|
307
|
+
* The Store is the central container for your application's data, providing:
|
|
308
|
+
* - Reactive state management with automatic updates
|
|
309
|
+
* - Type-safe record operations
|
|
310
|
+
* - History tracking and change notifications
|
|
311
|
+
* - Schema validation and migrations
|
|
312
|
+
* - Side effects and business logic hooks
|
|
313
|
+
* - Efficient querying and indexing
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* ```ts
|
|
317
|
+
* // Create a store with schema
|
|
318
|
+
* const schema = StoreSchema.create({
|
|
319
|
+
* book: Book,
|
|
320
|
+
* author: Author
|
|
321
|
+
* })
|
|
322
|
+
*
|
|
323
|
+
* const store = new Store({
|
|
324
|
+
* schema,
|
|
325
|
+
* props: {}
|
|
326
|
+
* })
|
|
327
|
+
*
|
|
328
|
+
* // Add records
|
|
329
|
+
* const book = Book.create({ title: 'The Lathe of Heaven', author: 'Le Guin' })
|
|
330
|
+
* store.put([book])
|
|
331
|
+
*
|
|
332
|
+
* // Listen to changes
|
|
333
|
+
* store.listen((entry) => {
|
|
334
|
+
* console.log('Changes:', entry.changes)
|
|
335
|
+
* })
|
|
336
|
+
* ```
|
|
116
337
|
*
|
|
117
338
|
* @public
|
|
118
339
|
*/
|
|
119
340
|
export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|
120
341
|
/**
|
|
121
|
-
* The
|
|
342
|
+
* The unique identifier of the store instance.
|
|
343
|
+
*
|
|
344
|
+
* @public
|
|
122
345
|
*/
|
|
123
346
|
public readonly id: string
|
|
124
347
|
/**
|
|
@@ -140,7 +363,19 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|
|
140
363
|
})
|
|
141
364
|
|
|
142
365
|
/**
|
|
143
|
-
*
|
|
366
|
+
* Reactive queries and indexes for efficiently accessing store data.
|
|
367
|
+
* Provides methods for filtering, indexing, and subscribing to subsets of records.
|
|
368
|
+
*
|
|
369
|
+
* @example
|
|
370
|
+
* ```ts
|
|
371
|
+
* // Create an index by a property
|
|
372
|
+
* const booksByAuthor = store.query.index('book', 'author')
|
|
373
|
+
*
|
|
374
|
+
* // Get records matching criteria
|
|
375
|
+
* const inStockBooks = store.query.records('book', () => ({
|
|
376
|
+
* inStock: { eq: true }
|
|
377
|
+
* }))
|
|
378
|
+
* ```
|
|
144
379
|
*
|
|
145
380
|
* @public
|
|
146
381
|
* @readonly
|
|
@@ -178,23 +413,65 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|
|
178
413
|
/* noop */
|
|
179
414
|
}
|
|
180
415
|
|
|
416
|
+
/**
|
|
417
|
+
* The schema that defines the structure and validation rules for records in this store.
|
|
418
|
+
*
|
|
419
|
+
* @public
|
|
420
|
+
*/
|
|
181
421
|
readonly schema: StoreSchema<R, Props>
|
|
182
422
|
|
|
423
|
+
/**
|
|
424
|
+
* Custom properties associated with this store instance.
|
|
425
|
+
*
|
|
426
|
+
* @public
|
|
427
|
+
*/
|
|
183
428
|
readonly props: Props
|
|
184
429
|
|
|
430
|
+
/**
|
|
431
|
+
* A mapping of record scopes to the set of record type names that belong to each scope.
|
|
432
|
+
* Used to filter records by their persistence and synchronization behavior.
|
|
433
|
+
*
|
|
434
|
+
* @public
|
|
435
|
+
*/
|
|
185
436
|
public readonly scopedTypes: { readonly [K in RecordScope]: ReadonlySet<R['typeName']> }
|
|
186
437
|
|
|
438
|
+
/**
|
|
439
|
+
* Side effects manager that handles lifecycle events for record operations.
|
|
440
|
+
* Allows registration of callbacks for create, update, delete, and validation events.
|
|
441
|
+
*
|
|
442
|
+
* @example
|
|
443
|
+
* ```ts
|
|
444
|
+
* store.sideEffects.registerAfterCreateHandler('book', (book) => {
|
|
445
|
+
* console.log('Book created:', book.title)
|
|
446
|
+
* })
|
|
447
|
+
* ```
|
|
448
|
+
*
|
|
449
|
+
* @public
|
|
450
|
+
*/
|
|
187
451
|
public readonly sideEffects = new StoreSideEffects<R>(this)
|
|
188
452
|
|
|
453
|
+
/**
|
|
454
|
+
* Creates a new Store instance.
|
|
455
|
+
*
|
|
456
|
+
* @example
|
|
457
|
+
* ```ts
|
|
458
|
+
* const store = new Store({
|
|
459
|
+
* schema: StoreSchema.create({ book: Book }),
|
|
460
|
+
* props: { appName: 'MyLibrary' },
|
|
461
|
+
* initialData: savedData
|
|
462
|
+
* })
|
|
463
|
+
* ```
|
|
464
|
+
*
|
|
465
|
+
* @param config - Configuration object for the store
|
|
466
|
+
*/
|
|
189
467
|
constructor(config: {
|
|
468
|
+
/** Optional unique identifier for the store */
|
|
190
469
|
id?: string
|
|
191
|
-
/** The store's initial data
|
|
470
|
+
/** The store's initial data to populate on creation */
|
|
192
471
|
initialData?: SerializedStore<R>
|
|
193
|
-
/**
|
|
194
|
-
* A map of validators for each record type. A record's validator will be called when the record
|
|
195
|
-
* is created or updated. It should throw an error if the record is invalid.
|
|
196
|
-
*/
|
|
472
|
+
/** The schema defining record types, validation, and migrations */
|
|
197
473
|
schema: StoreSchema<R, Props>
|
|
474
|
+
/** Custom properties for the store instance */
|
|
198
475
|
props: Props
|
|
199
476
|
}) {
|
|
200
477
|
const { initialData, schema, id } = config
|
|
@@ -327,10 +604,21 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|
|
327
604
|
}
|
|
328
605
|
|
|
329
606
|
/**
|
|
330
|
-
* Add
|
|
607
|
+
* Add or update records in the store. If a record with the same ID already exists, it will be updated.
|
|
608
|
+
* Otherwise, a new record will be created.
|
|
331
609
|
*
|
|
332
|
-
* @
|
|
333
|
-
*
|
|
610
|
+
* @example
|
|
611
|
+
* ```ts
|
|
612
|
+
* // Add new records
|
|
613
|
+
* const book = Book.create({ title: 'Lathe Of Heaven', author: 'Le Guin' })
|
|
614
|
+
* store.put([book])
|
|
615
|
+
*
|
|
616
|
+
* // Update existing record
|
|
617
|
+
* store.put([{ ...book, title: 'The Lathe of Heaven' }])
|
|
618
|
+
* ```
|
|
619
|
+
*
|
|
620
|
+
* @param records - The records to add or update
|
|
621
|
+
* @param phaseOverride - Override the validation phase (used internally)
|
|
334
622
|
* @public
|
|
335
623
|
*/
|
|
336
624
|
put(records: R[], phaseOverride?: 'initialize'): void {
|
|
@@ -411,9 +699,18 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|
|
411
699
|
}
|
|
412
700
|
|
|
413
701
|
/**
|
|
414
|
-
* Remove
|
|
702
|
+
* Remove records from the store by their IDs.
|
|
703
|
+
*
|
|
704
|
+
* @example
|
|
705
|
+
* ```ts
|
|
706
|
+
* // Remove a single record
|
|
707
|
+
* store.remove([book.id])
|
|
708
|
+
*
|
|
709
|
+
* // Remove multiple records
|
|
710
|
+
* store.remove([book1.id, book2.id, book3.id])
|
|
711
|
+
* ```
|
|
415
712
|
*
|
|
416
|
-
* @param ids - The
|
|
713
|
+
* @param ids - The IDs of the records to remove
|
|
417
714
|
* @public
|
|
418
715
|
*/
|
|
419
716
|
remove(ids: IdOf<R>[]): void {
|
|
@@ -447,9 +744,18 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|
|
447
744
|
}
|
|
448
745
|
|
|
449
746
|
/**
|
|
450
|
-
* Get
|
|
747
|
+
* Get a record by its ID. This creates a reactive subscription to the record.
|
|
451
748
|
*
|
|
452
|
-
* @
|
|
749
|
+
* @example
|
|
750
|
+
* ```ts
|
|
751
|
+
* const book = store.get(bookId)
|
|
752
|
+
* if (book) {
|
|
753
|
+
* console.log(book.title)
|
|
754
|
+
* }
|
|
755
|
+
* ```
|
|
756
|
+
*
|
|
757
|
+
* @param id - The ID of the record to get
|
|
758
|
+
* @returns The record if it exists, undefined otherwise
|
|
453
759
|
* @public
|
|
454
760
|
*/
|
|
455
761
|
get<K extends IdOf<R>>(id: K): RecordFromId<K> | undefined {
|
|
@@ -457,9 +763,17 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|
|
457
763
|
}
|
|
458
764
|
|
|
459
765
|
/**
|
|
460
|
-
* Get
|
|
766
|
+
* Get a record by its ID without creating a reactive subscription.
|
|
767
|
+
* Use this when you need to access a record but don't want reactive updates.
|
|
461
768
|
*
|
|
462
|
-
* @
|
|
769
|
+
* @example
|
|
770
|
+
* ```ts
|
|
771
|
+
* // Won't trigger reactive updates when this record changes
|
|
772
|
+
* const book = store.unsafeGetWithoutCapture(bookId)
|
|
773
|
+
* ```
|
|
774
|
+
*
|
|
775
|
+
* @param id - The ID of the record to get
|
|
776
|
+
* @returns The record if it exists, undefined otherwise
|
|
463
777
|
* @public
|
|
464
778
|
*/
|
|
465
779
|
unsafeGetWithoutCapture<K extends IdOf<R>>(id: K): RecordFromId<K> | undefined {
|
|
@@ -467,10 +781,21 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|
|
467
781
|
}
|
|
468
782
|
|
|
469
783
|
/**
|
|
470
|
-
*
|
|
784
|
+
* Serialize the store's records to a plain JavaScript object.
|
|
785
|
+
* Only includes records matching the specified scope.
|
|
471
786
|
*
|
|
472
|
-
* @
|
|
473
|
-
*
|
|
787
|
+
* @example
|
|
788
|
+
* ```ts
|
|
789
|
+
* // Serialize only document records (default)
|
|
790
|
+
* const documentData = store.serialize('document')
|
|
791
|
+
*
|
|
792
|
+
* // Serialize all records
|
|
793
|
+
* const allData = store.serialize('all')
|
|
794
|
+
* ```
|
|
795
|
+
*
|
|
796
|
+
* @param scope - The scope of records to serialize. Defaults to 'document'
|
|
797
|
+
* @returns The serialized store data
|
|
798
|
+
* @public
|
|
474
799
|
*/
|
|
475
800
|
serialize(scope: RecordScope | 'all' = 'document'): SerializedStore<R> {
|
|
476
801
|
const result = {} as SerializedStore<R>
|
|
@@ -484,14 +809,20 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|
|
484
809
|
|
|
485
810
|
/**
|
|
486
811
|
* Get a serialized snapshot of the store and its schema.
|
|
812
|
+
* This includes both the data and schema information needed for proper migration.
|
|
487
813
|
*
|
|
814
|
+
* @example
|
|
488
815
|
* ```ts
|
|
489
816
|
* const snapshot = store.getStoreSnapshot()
|
|
490
|
-
*
|
|
491
|
-
* ```
|
|
817
|
+
* localStorage.setItem('myApp', JSON.stringify(snapshot))
|
|
492
818
|
*
|
|
493
|
-
*
|
|
819
|
+
* // Later...
|
|
820
|
+
* const saved = JSON.parse(localStorage.getItem('myApp'))
|
|
821
|
+
* store.loadStoreSnapshot(saved)
|
|
822
|
+
* ```
|
|
494
823
|
*
|
|
824
|
+
* @param scope - The scope of records to serialize. Defaults to 'document'
|
|
825
|
+
* @returns A snapshot containing both store data and schema information
|
|
495
826
|
* @public
|
|
496
827
|
*/
|
|
497
828
|
getStoreSnapshot(scope: RecordScope | 'all' = 'document'): StoreSnapshot<R> {
|
|
@@ -502,14 +833,18 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|
|
502
833
|
}
|
|
503
834
|
|
|
504
835
|
/**
|
|
505
|
-
* Migrate a serialized snapshot
|
|
836
|
+
* Migrate a serialized snapshot to the current schema version.
|
|
837
|
+
* This applies any necessary migrations to bring old data up to date.
|
|
506
838
|
*
|
|
839
|
+
* @example
|
|
507
840
|
* ```ts
|
|
508
|
-
* const
|
|
509
|
-
* store.migrateSnapshot(
|
|
841
|
+
* const oldSnapshot = JSON.parse(localStorage.getItem('myApp'))
|
|
842
|
+
* const migratedSnapshot = store.migrateSnapshot(oldSnapshot)
|
|
510
843
|
* ```
|
|
511
844
|
*
|
|
512
|
-
* @param snapshot - The snapshot to
|
|
845
|
+
* @param snapshot - The snapshot to migrate
|
|
846
|
+
* @returns The migrated snapshot with current schema version
|
|
847
|
+
* @throws Error if migration fails
|
|
513
848
|
* @public
|
|
514
849
|
*/
|
|
515
850
|
migrateSnapshot(snapshot: StoreSnapshot<R>): StoreSnapshot<R> {
|
|
@@ -526,14 +861,17 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|
|
526
861
|
}
|
|
527
862
|
|
|
528
863
|
/**
|
|
529
|
-
* Load a serialized snapshot.
|
|
864
|
+
* Load a serialized snapshot into the store, replacing all current data.
|
|
865
|
+
* The snapshot will be automatically migrated to the current schema version if needed.
|
|
530
866
|
*
|
|
867
|
+
* @example
|
|
531
868
|
* ```ts
|
|
532
|
-
* const snapshot =
|
|
869
|
+
* const snapshot = JSON.parse(localStorage.getItem('myApp'))
|
|
533
870
|
* store.loadStoreSnapshot(snapshot)
|
|
534
871
|
* ```
|
|
535
872
|
*
|
|
536
|
-
* @param snapshot - The snapshot to load
|
|
873
|
+
* @param snapshot - The snapshot to load
|
|
874
|
+
* @throws Error if migration fails or snapshot is invalid
|
|
537
875
|
* @public
|
|
538
876
|
*/
|
|
539
877
|
loadStoreSnapshot(snapshot: StoreSnapshot<R>): void {
|
|
@@ -557,9 +895,15 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|
|
557
895
|
}
|
|
558
896
|
|
|
559
897
|
/**
|
|
560
|
-
* Get an array of all
|
|
898
|
+
* Get an array of all records in the store.
|
|
899
|
+
*
|
|
900
|
+
* @example
|
|
901
|
+
* ```ts
|
|
902
|
+
* const allRecords = store.allRecords()
|
|
903
|
+
* const books = allRecords.filter(r => r.typeName === 'book')
|
|
904
|
+
* ```
|
|
561
905
|
*
|
|
562
|
-
* @returns An array
|
|
906
|
+
* @returns An array containing all records in the store
|
|
563
907
|
* @public
|
|
564
908
|
*/
|
|
565
909
|
allRecords(): R[] {
|
|
@@ -567,7 +911,13 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|
|
567
911
|
}
|
|
568
912
|
|
|
569
913
|
/**
|
|
570
|
-
*
|
|
914
|
+
* Remove all records from the store.
|
|
915
|
+
*
|
|
916
|
+
* @example
|
|
917
|
+
* ```ts
|
|
918
|
+
* store.clear()
|
|
919
|
+
* console.log(store.allRecords().length) // 0
|
|
920
|
+
* ```
|
|
571
921
|
*
|
|
572
922
|
* @public
|
|
573
923
|
*/
|
|
@@ -576,11 +926,20 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|
|
576
926
|
}
|
|
577
927
|
|
|
578
928
|
/**
|
|
579
|
-
* Update a record. To update multiple records at once,
|
|
580
|
-
* `TypedStore` class.
|
|
929
|
+
* Update a single record using an updater function. To update multiple records at once,
|
|
930
|
+
* use the `update` method of the `TypedStore` class.
|
|
581
931
|
*
|
|
582
|
-
* @
|
|
583
|
-
*
|
|
932
|
+
* @example
|
|
933
|
+
* ```ts
|
|
934
|
+
* store.update(book.id, (book) => ({
|
|
935
|
+
* ...book,
|
|
936
|
+
* title: 'Updated Title'
|
|
937
|
+
* }))
|
|
938
|
+
* ```
|
|
939
|
+
*
|
|
940
|
+
* @param id - The ID of the record to update
|
|
941
|
+
* @param updater - A function that receives the current record and returns the updated record
|
|
942
|
+
* @public
|
|
584
943
|
*/
|
|
585
944
|
update<K extends IdOf<R>>(id: K, updater: (record: RecordFromId<K>) => RecordFromId<K>) {
|
|
586
945
|
const existing = this.unsafeGetWithoutCapture(id)
|
|
@@ -593,9 +952,17 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|
|
593
952
|
}
|
|
594
953
|
|
|
595
954
|
/**
|
|
596
|
-
*
|
|
955
|
+
* Check whether a record with the given ID exists in the store.
|
|
956
|
+
*
|
|
957
|
+
* @example
|
|
958
|
+
* ```ts
|
|
959
|
+
* if (store.has(bookId)) {
|
|
960
|
+
* console.log('Book exists!')
|
|
961
|
+
* }
|
|
962
|
+
* ```
|
|
597
963
|
*
|
|
598
|
-
* @param id - The
|
|
964
|
+
* @param id - The ID of the record to check
|
|
965
|
+
* @returns True if the record exists, false otherwise
|
|
599
966
|
* @public
|
|
600
967
|
*/
|
|
601
968
|
has<K extends IdOf<R>>(id: K): boolean {
|
|
@@ -603,11 +970,30 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|
|
603
970
|
}
|
|
604
971
|
|
|
605
972
|
/**
|
|
606
|
-
* Add a
|
|
973
|
+
* Add a listener that will be called when the store changes.
|
|
974
|
+
* Returns a function to remove the listener.
|
|
975
|
+
*
|
|
976
|
+
* @example
|
|
977
|
+
* ```ts
|
|
978
|
+
* const removeListener = store.listen((entry) => {
|
|
979
|
+
* console.log('Changes:', entry.changes)
|
|
980
|
+
* console.log('Source:', entry.source)
|
|
981
|
+
* })
|
|
982
|
+
*
|
|
983
|
+
* // Listen only to user changes to document records
|
|
984
|
+
* const removeDocumentListener = store.listen(
|
|
985
|
+
* (entry) => console.log('Document changed:', entry),
|
|
986
|
+
* { source: 'user', scope: 'document' }
|
|
987
|
+
* )
|
|
988
|
+
*
|
|
989
|
+
* // Later, remove the listener
|
|
990
|
+
* removeListener()
|
|
991
|
+
* ```
|
|
607
992
|
*
|
|
608
|
-
* @param onHistory - The listener to call when
|
|
609
|
-
* @param filters -
|
|
610
|
-
* @returns A function
|
|
993
|
+
* @param onHistory - The listener function to call when changes occur
|
|
994
|
+
* @param filters - Optional filters to control when the listener is called
|
|
995
|
+
* @returns A function that removes the listener when called
|
|
996
|
+
* @public
|
|
611
997
|
*/
|
|
612
998
|
listen(onHistory: StoreListener<R>, filters?: Partial<StoreListenerFilters>) {
|
|
613
999
|
// flush history so that this listener's history starts from exactly now
|
|
@@ -640,9 +1026,19 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|
|
640
1026
|
private isMergingRemoteChanges = false
|
|
641
1027
|
|
|
642
1028
|
/**
|
|
643
|
-
* Merge changes from a remote source
|
|
1029
|
+
* Merge changes from a remote source. Changes made within the provided function
|
|
1030
|
+
* will be marked with source 'remote' instead of 'user'.
|
|
1031
|
+
*
|
|
1032
|
+
* @example
|
|
1033
|
+
* ```ts
|
|
1034
|
+
* // Changes from sync/collaboration
|
|
1035
|
+
* store.mergeRemoteChanges(() => {
|
|
1036
|
+
* store.put(remoteRecords)
|
|
1037
|
+
* store.remove(deletedIds)
|
|
1038
|
+
* })
|
|
1039
|
+
* ```
|
|
644
1040
|
*
|
|
645
|
-
* @param fn - A function that
|
|
1041
|
+
* @param fn - A function that applies the remote changes
|
|
646
1042
|
* @public
|
|
647
1043
|
*/
|
|
648
1044
|
mergeRemoteChanges(fn: () => void) {
|
|
@@ -888,12 +1284,25 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
|
|
888
1284
|
}
|
|
889
1285
|
|
|
890
1286
|
/**
|
|
891
|
-
* Collect
|
|
892
|
-
*
|
|
893
|
-
* with adjacent entries of the same source squashed into a single entry.
|
|
1287
|
+
* Collect and squash history entries by their adjacent sources.
|
|
1288
|
+
* Adjacent entries from the same source are combined into a single entry.
|
|
894
1289
|
*
|
|
895
|
-
*
|
|
896
|
-
*
|
|
1290
|
+
* For example: [user, user, remote, remote, user] becomes [user, remote, user]
|
|
1291
|
+
*
|
|
1292
|
+
* @example
|
|
1293
|
+
* ```ts
|
|
1294
|
+
* const entries = [
|
|
1295
|
+
* { source: 'user', changes: userChanges1 },
|
|
1296
|
+
* { source: 'user', changes: userChanges2 },
|
|
1297
|
+
* { source: 'remote', changes: remoteChanges }
|
|
1298
|
+
* ]
|
|
1299
|
+
*
|
|
1300
|
+
* const squashed = squashHistoryEntries(entries)
|
|
1301
|
+
* // Results in 2 entries: combined user changes + remote changes
|
|
1302
|
+
* ```
|
|
1303
|
+
*
|
|
1304
|
+
* @param entries - The array of history entries to squash
|
|
1305
|
+
* @returns An array of squashed history entries
|
|
897
1306
|
* @public
|
|
898
1307
|
*/
|
|
899
1308
|
function squashHistoryEntries<T extends UnknownRecord>(
|
|
@@ -924,11 +1333,21 @@ function squashHistoryEntries<T extends UnknownRecord>(
|
|
|
924
1333
|
)
|
|
925
1334
|
}
|
|
926
1335
|
|
|
1336
|
+
/**
|
|
1337
|
+
* Internal class that accumulates history entries before they are flushed to listeners.
|
|
1338
|
+
* Handles batching and squashing of adjacent entries from the same source.
|
|
1339
|
+
*
|
|
1340
|
+
* @internal
|
|
1341
|
+
*/
|
|
927
1342
|
class HistoryAccumulator<T extends UnknownRecord> {
|
|
928
1343
|
private _history: HistoryEntry<T>[] = []
|
|
929
1344
|
|
|
930
1345
|
private _interceptors: Set<(entry: HistoryEntry<T>) => void> = new Set()
|
|
931
1346
|
|
|
1347
|
+
/**
|
|
1348
|
+
* Add an interceptor that will be called for each history entry.
|
|
1349
|
+
* Returns a function to remove the interceptor.
|
|
1350
|
+
*/
|
|
932
1351
|
addInterceptor(fn: (entry: HistoryEntry<T>) => void) {
|
|
933
1352
|
this._interceptors.add(fn)
|
|
934
1353
|
return () => {
|
|
@@ -936,6 +1355,10 @@ class HistoryAccumulator<T extends UnknownRecord> {
|
|
|
936
1355
|
}
|
|
937
1356
|
}
|
|
938
1357
|
|
|
1358
|
+
/**
|
|
1359
|
+
* Add a history entry to the accumulator.
|
|
1360
|
+
* Calls all registered interceptors with the entry.
|
|
1361
|
+
*/
|
|
939
1362
|
add(entry: HistoryEntry<T>) {
|
|
940
1363
|
this._history.push(entry)
|
|
941
1364
|
for (const interceptor of this._interceptors) {
|
|
@@ -943,39 +1366,82 @@ class HistoryAccumulator<T extends UnknownRecord> {
|
|
|
943
1366
|
}
|
|
944
1367
|
}
|
|
945
1368
|
|
|
1369
|
+
/**
|
|
1370
|
+
* Flush all accumulated history entries, squashing adjacent entries from the same source.
|
|
1371
|
+
* Clears the internal history buffer.
|
|
1372
|
+
*/
|
|
946
1373
|
flush() {
|
|
947
1374
|
const history = squashHistoryEntries(this._history)
|
|
948
1375
|
this._history = []
|
|
949
1376
|
return history
|
|
950
1377
|
}
|
|
951
1378
|
|
|
1379
|
+
/**
|
|
1380
|
+
* Clear all accumulated history entries without flushing.
|
|
1381
|
+
*/
|
|
952
1382
|
clear() {
|
|
953
1383
|
this._history = []
|
|
954
1384
|
}
|
|
955
1385
|
|
|
1386
|
+
/**
|
|
1387
|
+
* Check if there are any accumulated history entries.
|
|
1388
|
+
*/
|
|
956
1389
|
hasChanges() {
|
|
957
1390
|
return this._history.length > 0
|
|
958
1391
|
}
|
|
959
1392
|
}
|
|
960
1393
|
|
|
961
|
-
/**
|
|
1394
|
+
/**
|
|
1395
|
+
* A store or an object containing a store.
|
|
1396
|
+
* This type is used for APIs that can accept either a store directly or an object with a store property.
|
|
1397
|
+
*
|
|
1398
|
+
* @example
|
|
1399
|
+
* ```ts
|
|
1400
|
+
* function useStore(storeOrObject: StoreObject<MyRecord>) {
|
|
1401
|
+
* const store = storeOrObject instanceof Store ? storeOrObject : storeOrObject.store
|
|
1402
|
+
* return store
|
|
1403
|
+
* }
|
|
1404
|
+
* ```
|
|
1405
|
+
*
|
|
1406
|
+
* @public
|
|
1407
|
+
*/
|
|
962
1408
|
export type StoreObject<R extends UnknownRecord> = Store<R> | { store: Store<R> }
|
|
963
|
-
/**
|
|
1409
|
+
/**
|
|
1410
|
+
* Extract the record type from a StoreObject.
|
|
1411
|
+
*
|
|
1412
|
+
* @example
|
|
1413
|
+
* ```ts
|
|
1414
|
+
* type MyStoreObject = { store: Store<Book | Author> }
|
|
1415
|
+
* type Records = StoreObjectRecordType<MyStoreObject> // Book | Author
|
|
1416
|
+
* ```
|
|
1417
|
+
*
|
|
1418
|
+
* @public
|
|
1419
|
+
*/
|
|
964
1420
|
export type StoreObjectRecordType<Context extends StoreObject<any>> =
|
|
965
1421
|
Context extends Store<infer R> ? R : Context extends { store: Store<infer R> } ? R : never
|
|
966
1422
|
|
|
967
1423
|
/**
|
|
968
|
-
*
|
|
1424
|
+
* Create a computed cache that works with any StoreObject (store or object containing a store).
|
|
1425
|
+
* This is a standalone version of Store.createComputedCache that can work with multiple store instances.
|
|
969
1426
|
*
|
|
970
1427
|
* @example
|
|
971
1428
|
* ```ts
|
|
972
|
-
* const
|
|
973
|
-
*
|
|
974
|
-
* })
|
|
1429
|
+
* const expensiveCache = createComputedCache(
|
|
1430
|
+
* 'expensiveData',
|
|
1431
|
+
* (context: { store: Store<Book> }, book: Book) => {
|
|
1432
|
+
* return performExpensiveCalculation(book)
|
|
1433
|
+
* }
|
|
1434
|
+
* )
|
|
975
1435
|
*
|
|
976
|
-
*
|
|
1436
|
+
* // Use with different store instances
|
|
1437
|
+
* const result1 = expensiveCache.get(storeObject1, bookId)
|
|
1438
|
+
* const result2 = expensiveCache.get(storeObject2, bookId)
|
|
977
1439
|
* ```
|
|
978
1440
|
*
|
|
1441
|
+
* @param name - A unique name for the cache (used for debugging)
|
|
1442
|
+
* @param derive - Function that derives a value from the context and record
|
|
1443
|
+
* @param opts - Optional configuration for equality checks
|
|
1444
|
+
* @returns A cache that can be used with multiple store instances
|
|
979
1445
|
* @public
|
|
980
1446
|
*/
|
|
981
1447
|
export function createComputedCache<
|