@tldraw/store 4.1.0-canary.e4499a57ef5b → 4.1.0-canary.f2f81cd6fe2c
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/AtomMap.ts
CHANGED
|
@@ -9,6 +9,21 @@ import { emptyMap, ImmutableMap } from './ImmutableMap'
|
|
|
9
9
|
export class AtomMap<K, V> implements Map<K, V> {
|
|
10
10
|
private atoms: Atom<ImmutableMap<K, Atom<V | UNINITIALIZED>>>
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Creates a new AtomMap instance.
|
|
14
|
+
*
|
|
15
|
+
* name - A unique name for this map, used for atom identification
|
|
16
|
+
* entries - Optional initial entries to populate the map with
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* // Create an empty map
|
|
20
|
+
* const map = new AtomMap('userMap')
|
|
21
|
+
*
|
|
22
|
+
* // Create a map with initial data
|
|
23
|
+
* const initialData: [string, number][] = [['a', 1], ['b', 2]]
|
|
24
|
+
* const mapWithData = new AtomMap('numbersMap', initialData)
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
12
27
|
constructor(
|
|
13
28
|
private readonly name: string,
|
|
14
29
|
entries?: Iterable<readonly [K, V]>
|
|
@@ -24,7 +39,13 @@ export class AtomMap<K, V> implements Map<K, V> {
|
|
|
24
39
|
this.atoms = atom(`${name}:atoms`, atoms)
|
|
25
40
|
}
|
|
26
41
|
|
|
27
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Retrieves the underlying atom for a given key.
|
|
44
|
+
*
|
|
45
|
+
* @param key - The key to retrieve the atom for
|
|
46
|
+
* @returns The atom containing the value, or undefined if the key doesn't exist
|
|
47
|
+
* @internal
|
|
48
|
+
*/
|
|
28
49
|
getAtom(key: K): Atom<V | UNINITIALIZED> | undefined {
|
|
29
50
|
const valueAtom = this.atoms.__unsafe__getWithoutCapture().get(key)
|
|
30
51
|
if (!valueAtom) {
|
|
@@ -35,12 +56,39 @@ export class AtomMap<K, V> implements Map<K, V> {
|
|
|
35
56
|
return valueAtom
|
|
36
57
|
}
|
|
37
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Gets the value associated with a key. Returns undefined if the key doesn't exist.
|
|
61
|
+
* This method is reactive and will cause reactive contexts to update when the value changes.
|
|
62
|
+
*
|
|
63
|
+
* @param key - The key to retrieve the value for
|
|
64
|
+
* @returns The value associated with the key, or undefined if not found
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* const map = new AtomMap('myMap')
|
|
68
|
+
* map.set('name', 'Alice')
|
|
69
|
+
* console.log(map.get('name')) // 'Alice'
|
|
70
|
+
* console.log(map.get('missing')) // undefined
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
38
73
|
get(key: K): V | undefined {
|
|
39
74
|
const value = this.getAtom(key)?.get()
|
|
40
75
|
assert(value !== UNINITIALIZED)
|
|
41
76
|
return value
|
|
42
77
|
}
|
|
43
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Gets the value associated with a key without creating reactive dependencies.
|
|
81
|
+
* This method will not cause reactive contexts to update when the value changes.
|
|
82
|
+
*
|
|
83
|
+
* @param key - The key to retrieve the value for
|
|
84
|
+
* @returns The value associated with the key, or undefined if not found
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* const map = new AtomMap('myMap')
|
|
88
|
+
* map.set('count', 42)
|
|
89
|
+
* const value = map.__unsafe__getWithoutCapture('count') // No reactive subscription
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
44
92
|
__unsafe__getWithoutCapture(key: K): V | undefined {
|
|
45
93
|
const valueAtom = this.atoms.__unsafe__getWithoutCapture().get(key)
|
|
46
94
|
if (!valueAtom) return undefined
|
|
@@ -49,6 +97,20 @@ export class AtomMap<K, V> implements Map<K, V> {
|
|
|
49
97
|
return value
|
|
50
98
|
}
|
|
51
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Checks whether a key exists in the map.
|
|
102
|
+
* This method is reactive and will cause reactive contexts to update when keys are added or removed.
|
|
103
|
+
*
|
|
104
|
+
* @param key - The key to check for
|
|
105
|
+
* @returns True if the key exists in the map, false otherwise
|
|
106
|
+
* @example
|
|
107
|
+
* ```ts
|
|
108
|
+
* const map = new AtomMap('myMap')
|
|
109
|
+
* console.log(map.has('name')) // false
|
|
110
|
+
* map.set('name', 'Alice')
|
|
111
|
+
* console.log(map.has('name')) // true
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
52
114
|
has(key: K): boolean {
|
|
53
115
|
const valueAtom = this.getAtom(key)
|
|
54
116
|
if (!valueAtom) {
|
|
@@ -57,6 +119,19 @@ export class AtomMap<K, V> implements Map<K, V> {
|
|
|
57
119
|
return valueAtom.get() !== UNINITIALIZED
|
|
58
120
|
}
|
|
59
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Checks whether a key exists in the map without creating reactive dependencies.
|
|
124
|
+
* This method will not cause reactive contexts to update when keys are added or removed.
|
|
125
|
+
*
|
|
126
|
+
* @param key - The key to check for
|
|
127
|
+
* @returns True if the key exists in the map, false otherwise
|
|
128
|
+
* @example
|
|
129
|
+
* ```ts
|
|
130
|
+
* const map = new AtomMap('myMap')
|
|
131
|
+
* map.set('active', true)
|
|
132
|
+
* const exists = map.__unsafe__hasWithoutCapture('active') // No reactive subscription
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
60
135
|
__unsafe__hasWithoutCapture(key: K): boolean {
|
|
61
136
|
const valueAtom = this.atoms.__unsafe__getWithoutCapture().get(key)
|
|
62
137
|
if (!valueAtom) return false
|
|
@@ -64,6 +139,19 @@ export class AtomMap<K, V> implements Map<K, V> {
|
|
|
64
139
|
return true
|
|
65
140
|
}
|
|
66
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Sets a value for the given key. If the key already exists, its value is updated.
|
|
144
|
+
* If the key doesn't exist, a new entry is created.
|
|
145
|
+
*
|
|
146
|
+
* @param key - The key to set the value for
|
|
147
|
+
* @param value - The value to associate with the key
|
|
148
|
+
* @returns This AtomMap instance for method chaining
|
|
149
|
+
* @example
|
|
150
|
+
* ```ts
|
|
151
|
+
* const map = new AtomMap('myMap')
|
|
152
|
+
* map.set('name', 'Alice').set('age', 30)
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
67
155
|
set(key: K, value: V) {
|
|
68
156
|
const existingAtom = this.atoms.__unsafe__getWithoutCapture().get(key)
|
|
69
157
|
if (existingAtom) {
|
|
@@ -76,6 +164,19 @@ export class AtomMap<K, V> implements Map<K, V> {
|
|
|
76
164
|
return this
|
|
77
165
|
}
|
|
78
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Updates an existing value using an updater function.
|
|
169
|
+
*
|
|
170
|
+
* @param key - The key of the value to update
|
|
171
|
+
* @param updater - A function that receives the current value and returns the new value
|
|
172
|
+
* @throws Error if the key doesn't exist in the map
|
|
173
|
+
* @example
|
|
174
|
+
* ```ts
|
|
175
|
+
* const map = new AtomMap('myMap')
|
|
176
|
+
* map.set('count', 5)
|
|
177
|
+
* map.update('count', count => count + 1) // count is now 6
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
79
180
|
update(key: K, updater: (value: V) => V) {
|
|
80
181
|
const valueAtom = this.atoms.__unsafe__getWithoutCapture().get(key)
|
|
81
182
|
if (!valueAtom) {
|
|
@@ -86,6 +187,19 @@ export class AtomMap<K, V> implements Map<K, V> {
|
|
|
86
187
|
valueAtom.set(updater(value))
|
|
87
188
|
}
|
|
88
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Removes a key-value pair from the map.
|
|
192
|
+
*
|
|
193
|
+
* @param key - The key to remove
|
|
194
|
+
* @returns True if the key existed and was removed, false if it didn't exist
|
|
195
|
+
* @example
|
|
196
|
+
* ```ts
|
|
197
|
+
* const map = new AtomMap('myMap')
|
|
198
|
+
* map.set('temp', 'value')
|
|
199
|
+
* console.log(map.delete('temp')) // true
|
|
200
|
+
* console.log(map.delete('missing')) // false
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
89
203
|
delete(key: K) {
|
|
90
204
|
const valueAtom = this.atoms.__unsafe__getWithoutCapture().get(key)
|
|
91
205
|
if (!valueAtom) {
|
|
@@ -101,6 +215,19 @@ export class AtomMap<K, V> implements Map<K, V> {
|
|
|
101
215
|
return true
|
|
102
216
|
}
|
|
103
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Removes multiple key-value pairs from the map in a single transaction.
|
|
220
|
+
*
|
|
221
|
+
* @param keys - An iterable of keys to remove
|
|
222
|
+
* @returns An array of [key, value] pairs that were actually deleted
|
|
223
|
+
* @example
|
|
224
|
+
* ```ts
|
|
225
|
+
* const map = new AtomMap('myMap')
|
|
226
|
+
* map.set('a', 1).set('b', 2).set('c', 3)
|
|
227
|
+
* const deleted = map.deleteMany(['a', 'c', 'missing'])
|
|
228
|
+
* console.log(deleted) // [['a', 1], ['c', 3]]
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
104
231
|
deleteMany(keys: Iterable<K>): [K, V][] {
|
|
105
232
|
return transact(() => {
|
|
106
233
|
const deleted: [K, V][] = []
|
|
@@ -126,6 +253,17 @@ export class AtomMap<K, V> implements Map<K, V> {
|
|
|
126
253
|
})
|
|
127
254
|
}
|
|
128
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Removes all key-value pairs from the map.
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```ts
|
|
261
|
+
* const map = new AtomMap('myMap')
|
|
262
|
+
* map.set('a', 1).set('b', 2)
|
|
263
|
+
* map.clear()
|
|
264
|
+
* console.log(map.size) // 0
|
|
265
|
+
* ```
|
|
266
|
+
*/
|
|
129
267
|
clear() {
|
|
130
268
|
return transact(() => {
|
|
131
269
|
for (const valueAtom of this.atoms.__unsafe__getWithoutCapture().values()) {
|
|
@@ -135,6 +273,20 @@ export class AtomMap<K, V> implements Map<K, V> {
|
|
|
135
273
|
})
|
|
136
274
|
}
|
|
137
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Returns an iterator that yields [key, value] pairs for each entry in the map.
|
|
278
|
+
* This method is reactive and will cause reactive contexts to update when entries change.
|
|
279
|
+
*
|
|
280
|
+
* @returns A generator that yields [key, value] tuples
|
|
281
|
+
* @example
|
|
282
|
+
* ```ts
|
|
283
|
+
* const map = new AtomMap('myMap')
|
|
284
|
+
* map.set('a', 1).set('b', 2)
|
|
285
|
+
* for (const [key, value] of map.entries()) {
|
|
286
|
+
* console.log(`${key}: ${value}`)
|
|
287
|
+
* }
|
|
288
|
+
* ```
|
|
289
|
+
*/
|
|
138
290
|
*entries(): Generator<[K, V], undefined, unknown> {
|
|
139
291
|
for (const [key, valueAtom] of this.atoms.get()) {
|
|
140
292
|
const value = valueAtom.get()
|
|
@@ -143,12 +295,40 @@ export class AtomMap<K, V> implements Map<K, V> {
|
|
|
143
295
|
}
|
|
144
296
|
}
|
|
145
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Returns an iterator that yields all keys in the map.
|
|
300
|
+
* This method is reactive and will cause reactive contexts to update when keys change.
|
|
301
|
+
*
|
|
302
|
+
* @returns A generator that yields keys
|
|
303
|
+
* @example
|
|
304
|
+
* ```ts
|
|
305
|
+
* const map = new AtomMap('myMap')
|
|
306
|
+
* map.set('name', 'Alice').set('age', 30)
|
|
307
|
+
* for (const key of map.keys()) {
|
|
308
|
+
* console.log(key) // 'name', 'age'
|
|
309
|
+
* }
|
|
310
|
+
* ```
|
|
311
|
+
*/
|
|
146
312
|
*keys(): Generator<K, undefined, unknown> {
|
|
147
313
|
for (const key of this.atoms.get().keys()) {
|
|
148
314
|
yield key
|
|
149
315
|
}
|
|
150
316
|
}
|
|
151
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Returns an iterator that yields all values in the map.
|
|
320
|
+
* This method is reactive and will cause reactive contexts to update when values change.
|
|
321
|
+
*
|
|
322
|
+
* @returns A generator that yields values
|
|
323
|
+
* @example
|
|
324
|
+
* ```ts
|
|
325
|
+
* const map = new AtomMap('myMap')
|
|
326
|
+
* map.set('name', 'Alice').set('age', 30)
|
|
327
|
+
* for (const value of map.values()) {
|
|
328
|
+
* console.log(value) // 'Alice', 30
|
|
329
|
+
* }
|
|
330
|
+
* ```
|
|
331
|
+
*/
|
|
152
332
|
*values(): Generator<V, undefined, unknown> {
|
|
153
333
|
for (const valueAtom of this.atoms.get().values()) {
|
|
154
334
|
const value = valueAtom.get()
|
|
@@ -157,20 +337,80 @@ export class AtomMap<K, V> implements Map<K, V> {
|
|
|
157
337
|
}
|
|
158
338
|
}
|
|
159
339
|
|
|
340
|
+
/**
|
|
341
|
+
* The number of key-value pairs in the map.
|
|
342
|
+
* This property is reactive and will cause reactive contexts to update when the size changes.
|
|
343
|
+
*
|
|
344
|
+
* @returns The number of entries in the map
|
|
345
|
+
* @example
|
|
346
|
+
* ```ts
|
|
347
|
+
* const map = new AtomMap('myMap')
|
|
348
|
+
* console.log(map.size) // 0
|
|
349
|
+
* map.set('a', 1)
|
|
350
|
+
* console.log(map.size) // 1
|
|
351
|
+
* ```
|
|
352
|
+
*/
|
|
160
353
|
// eslint-disable-next-line no-restricted-syntax
|
|
161
354
|
get size() {
|
|
162
355
|
return this.atoms.get().size
|
|
163
356
|
}
|
|
164
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Executes a provided function once for each key-value pair in the map.
|
|
360
|
+
* This method is reactive and will cause reactive contexts to update when entries change.
|
|
361
|
+
*
|
|
362
|
+
* @param callbackfn - Function to execute for each entry
|
|
363
|
+
* - value - The value of the current entry
|
|
364
|
+
* - key - The key of the current entry
|
|
365
|
+
* - map - The AtomMap being traversed
|
|
366
|
+
* @param thisArg - Value to use as `this` when executing the callback
|
|
367
|
+
* @example
|
|
368
|
+
* ```ts
|
|
369
|
+
* const map = new AtomMap('myMap')
|
|
370
|
+
* map.set('a', 1).set('b', 2)
|
|
371
|
+
* map.forEach((value, key) => {
|
|
372
|
+
* console.log(`${key} = ${value}`)
|
|
373
|
+
* })
|
|
374
|
+
* ```
|
|
375
|
+
*/
|
|
165
376
|
forEach(callbackfn: (value: V, key: K, map: AtomMap<K, V>) => void, thisArg?: any): void {
|
|
166
377
|
for (const [key, value] of this.entries()) {
|
|
167
378
|
callbackfn.call(thisArg, value, key, this)
|
|
168
379
|
}
|
|
169
380
|
}
|
|
170
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Returns the default iterator for the map, which is the same as entries().
|
|
384
|
+
* This allows the map to be used in for...of loops and other iterable contexts.
|
|
385
|
+
*
|
|
386
|
+
* @returns The same iterator as entries()
|
|
387
|
+
* @example
|
|
388
|
+
* ```ts
|
|
389
|
+
* const map = new AtomMap('myMap')
|
|
390
|
+
* map.set('a', 1).set('b', 2)
|
|
391
|
+
*
|
|
392
|
+
* // These are equivalent:
|
|
393
|
+
* for (const [key, value] of map) {
|
|
394
|
+
* console.log(`${key}: ${value}`)
|
|
395
|
+
* }
|
|
396
|
+
*
|
|
397
|
+
* for (const [key, value] of map.entries()) {
|
|
398
|
+
* console.log(`${key}: ${value}`)
|
|
399
|
+
* }
|
|
400
|
+
* ```
|
|
401
|
+
*/
|
|
171
402
|
[Symbol.iterator]() {
|
|
172
403
|
return this.entries()
|
|
173
404
|
}
|
|
174
405
|
|
|
406
|
+
/**
|
|
407
|
+
* The string tag used by Object.prototype.toString for this class.
|
|
408
|
+
*
|
|
409
|
+
* @example
|
|
410
|
+
* ```ts
|
|
411
|
+
* const map = new AtomMap('myMap')
|
|
412
|
+
* console.log(Object.prototype.toString.call(map)) // '[object AtomMap]'
|
|
413
|
+
* ```
|
|
414
|
+
*/
|
|
175
415
|
[Symbol.toStringTag] = 'AtomMap'
|
|
176
416
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { isRecord } from './BaseRecord'
|
|
3
|
+
|
|
4
|
+
describe('isRecord function', () => {
|
|
5
|
+
it('should return true for valid records', () => {
|
|
6
|
+
const validRecord = {
|
|
7
|
+
id: 'book:123',
|
|
8
|
+
typeName: 'book',
|
|
9
|
+
title: '1984',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
expect(isRecord(validRecord)).toBe(true)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should return false for null and undefined', () => {
|
|
16
|
+
expect(isRecord(null)).toBe(false)
|
|
17
|
+
expect(isRecord(undefined)).toBe(false)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('should return false for primitive types', () => {
|
|
21
|
+
expect(isRecord('string')).toBe(false)
|
|
22
|
+
expect(isRecord(42)).toBe(false)
|
|
23
|
+
expect(isRecord(true)).toBe(false)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should return false for objects missing required properties', () => {
|
|
27
|
+
expect(isRecord({})).toBe(false)
|
|
28
|
+
expect(isRecord({ id: 'test' })).toBe(false)
|
|
29
|
+
expect(isRecord({ typeName: 'test' })).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should work as type guard', () => {
|
|
33
|
+
const unknownValue: unknown = {
|
|
34
|
+
id: 'book:123',
|
|
35
|
+
typeName: 'book',
|
|
36
|
+
title: '1984',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (isRecord(unknownValue)) {
|
|
40
|
+
expect(unknownValue.id).toBe('book:123')
|
|
41
|
+
expect(unknownValue.typeName).toBe('book')
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
})
|
package/src/lib/BaseRecord.ts
CHANGED
|
@@ -1,11 +1,78 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* A branded string type that represents a unique identifier for a record.
|
|
3
|
+
* The brand ensures type safety by preventing mixing of IDs between different record types.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* // Define a Book record
|
|
8
|
+
* interface Book extends BaseRecord<'book', RecordId<Book>> {
|
|
9
|
+
* title: string
|
|
10
|
+
* author: string
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* const bookId: RecordId<Book> = 'book:abc123' as RecordId<Book>
|
|
14
|
+
* const authorId: RecordId<Author> = 'author:xyz789' as RecordId<Author>
|
|
15
|
+
*
|
|
16
|
+
* // TypeScript prevents mixing different record ID types
|
|
17
|
+
* // bookId = authorId // Type error!
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
2
22
|
export type RecordId<R extends UnknownRecord> = string & { __type__: R }
|
|
3
23
|
|
|
4
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Utility type that extracts the ID type from a record type.
|
|
26
|
+
* This is useful when you need to work with record IDs without having the full record type.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* interface Book extends BaseRecord<'book', RecordId<Book>> {
|
|
31
|
+
* title: string
|
|
32
|
+
* author: string
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* // Extract the ID type from the Book record
|
|
36
|
+
* type BookId = IdOf<Book> // RecordId<Book>
|
|
37
|
+
*
|
|
38
|
+
* function findBook(id: IdOf<Book>): Book | undefined {
|
|
39
|
+
* return store.get(id)
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @public
|
|
44
|
+
*/
|
|
5
45
|
export type IdOf<R extends UnknownRecord> = R['id']
|
|
6
46
|
|
|
7
47
|
/**
|
|
8
|
-
* The base record that all records must extend.
|
|
48
|
+
* The base record interface that all records in the store must extend.
|
|
49
|
+
* This interface provides the fundamental structure required for all records: a unique ID and a type name.
|
|
50
|
+
* The type parameters ensure type safety and prevent mixing of different record types.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* // Define a Book record that extends BaseRecord
|
|
55
|
+
* interface Book extends BaseRecord<'book', RecordId<Book>> {
|
|
56
|
+
* title: string
|
|
57
|
+
* author: string
|
|
58
|
+
* publishedYear: number
|
|
59
|
+
* }
|
|
60
|
+
*
|
|
61
|
+
* // Define an Author record
|
|
62
|
+
* interface Author extends BaseRecord<'author', RecordId<Author>> {
|
|
63
|
+
* name: string
|
|
64
|
+
* birthYear: number
|
|
65
|
+
* }
|
|
66
|
+
*
|
|
67
|
+
* // Usage with RecordType
|
|
68
|
+
* const Book = createRecordType<Book>('book', { scope: 'document' })
|
|
69
|
+
* const book = Book.create({
|
|
70
|
+
* title: '1984',
|
|
71
|
+
* author: 'George Orwell',
|
|
72
|
+
* publishedYear: 1949
|
|
73
|
+
* })
|
|
74
|
+
* // Results in: { id: 'book:abc123', typeName: 'book', title: '1984', ... }
|
|
75
|
+
* ```
|
|
9
76
|
*
|
|
10
77
|
* @public
|
|
11
78
|
*/
|
|
@@ -14,9 +81,56 @@ export interface BaseRecord<TypeName extends string, Id extends RecordId<Unknown
|
|
|
14
81
|
readonly typeName: TypeName
|
|
15
82
|
}
|
|
16
83
|
|
|
17
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* A generic type representing any record that extends BaseRecord.
|
|
86
|
+
* This is useful for type constraints when you need to work with records of unknown types,
|
|
87
|
+
* but still want to ensure they follow the BaseRecord structure.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* // Function that works with any type of record
|
|
92
|
+
* function logRecord(record: UnknownRecord): void {
|
|
93
|
+
* console.log(`Record ${record.id} of type ${record.typeName}`)
|
|
94
|
+
* }
|
|
95
|
+
*
|
|
96
|
+
* // Can be used with any record type
|
|
97
|
+
* const book: Book = { id: 'book:123' as RecordId<Book>, typeName: 'book', title: '1984' }
|
|
98
|
+
* const author: Author = { id: 'author:456' as RecordId<Author>, typeName: 'author', name: 'Orwell' }
|
|
99
|
+
*
|
|
100
|
+
* logRecord(book) // "Record book:123 of type book"
|
|
101
|
+
* logRecord(author) // "Record author:456 of type author"
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* @public
|
|
105
|
+
*/
|
|
18
106
|
export type UnknownRecord = BaseRecord<string, RecordId<UnknownRecord>>
|
|
19
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Type guard function that checks if an unknown value is a valid record.
|
|
110
|
+
* A valid record must be an object with both `id` and `typeName` properties.
|
|
111
|
+
*
|
|
112
|
+
* @param record - The unknown value to check
|
|
113
|
+
* @returns `true` if the value is a valid UnknownRecord, `false` otherwise
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* const maybeRecord: unknown = { id: 'book:123', typeName: 'book', title: '1984' }
|
|
118
|
+
* const notARecord: unknown = { title: '1984', author: 'Orwell' }
|
|
119
|
+
* const nullValue: unknown = null
|
|
120
|
+
*
|
|
121
|
+
* if (isRecord(maybeRecord)) {
|
|
122
|
+
* // TypeScript now knows maybeRecord is UnknownRecord
|
|
123
|
+
* console.log(maybeRecord.id) // 'book:123'
|
|
124
|
+
* console.log(maybeRecord.typeName) // 'book'
|
|
125
|
+
* }
|
|
126
|
+
*
|
|
127
|
+
* console.log(isRecord(maybeRecord)) // true
|
|
128
|
+
* console.log(isRecord(notARecord)) // false (missing id and typeName)
|
|
129
|
+
* console.log(isRecord(nullValue)) // false
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* @public
|
|
133
|
+
*/
|
|
20
134
|
export function isRecord(record: unknown): record is UnknownRecord {
|
|
21
135
|
return typeof record === 'object' && record !== null && 'id' in record && 'typeName' in record
|
|
22
136
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { ImmutableMap, emptyMap } from './ImmutableMap'
|
|
3
|
+
|
|
4
|
+
describe('ImmutableMap', () => {
|
|
5
|
+
describe('constructor', () => {
|
|
6
|
+
it('should handle duplicate keys by keeping last value', () => {
|
|
7
|
+
const pairs: Array<[string, number]> = [
|
|
8
|
+
['a', 1],
|
|
9
|
+
['a', 2],
|
|
10
|
+
['a', 3],
|
|
11
|
+
]
|
|
12
|
+
const map = new ImmutableMap(pairs)
|
|
13
|
+
expect(map.size).toBe(1)
|
|
14
|
+
expect(map.get('a')).toBe(3)
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('get', () => {
|
|
19
|
+
it('should handle object keys', () => {
|
|
20
|
+
const objKey = { id: 1 }
|
|
21
|
+
const map = new ImmutableMap([[objKey, 'object']])
|
|
22
|
+
expect(map.get(objKey)).toBe('object')
|
|
23
|
+
expect(map.get({ id: 1 })).toBeUndefined() // Different object
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('set', () => {
|
|
28
|
+
it('should create new map with updated value', () => {
|
|
29
|
+
const map = new ImmutableMap([['key', 'value']])
|
|
30
|
+
const newMap = map.set('key', 'newValue')
|
|
31
|
+
|
|
32
|
+
expect(newMap.get('key')).toBe('newValue')
|
|
33
|
+
expect(map.get('key')).toBe('value') // Original unchanged
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should handle different object keys correctly', () => {
|
|
37
|
+
const obj1 = { id: 1 }
|
|
38
|
+
const obj2 = { id: 2 }
|
|
39
|
+
|
|
40
|
+
let map = new ImmutableMap()
|
|
41
|
+
map = map.set(obj1, 'first')
|
|
42
|
+
map = map.set(obj2, 'second')
|
|
43
|
+
|
|
44
|
+
expect(map.size).toBe(2)
|
|
45
|
+
expect(map.get(obj1)).toBe('first')
|
|
46
|
+
expect(map.get(obj2)).toBe('second')
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('delete', () => {
|
|
51
|
+
it('should remove existing keys', () => {
|
|
52
|
+
const map = new ImmutableMap([
|
|
53
|
+
['a', 1],
|
|
54
|
+
['b', 2],
|
|
55
|
+
])
|
|
56
|
+
const newMap = map.delete('a')
|
|
57
|
+
|
|
58
|
+
expect(newMap.size).toBe(1)
|
|
59
|
+
expect(newMap.get('a')).toBeUndefined()
|
|
60
|
+
expect(newMap.get('b')).toBe(2)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('deleteAll', () => {
|
|
65
|
+
it('should remove multiple keys', () => {
|
|
66
|
+
const map = new ImmutableMap([
|
|
67
|
+
['a', 1],
|
|
68
|
+
['b', 2],
|
|
69
|
+
['c', 3],
|
|
70
|
+
])
|
|
71
|
+
const newMap = map.deleteAll(['a', 'c'])
|
|
72
|
+
|
|
73
|
+
expect(newMap.size).toBe(1)
|
|
74
|
+
expect(newMap.get('a')).toBeUndefined()
|
|
75
|
+
expect(newMap.get('b')).toBe(2)
|
|
76
|
+
expect(newMap.get('c')).toBeUndefined()
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('withMutations', () => {
|
|
81
|
+
it('should allow efficient batch operations', () => {
|
|
82
|
+
const map = new ImmutableMap([['a', 1]])
|
|
83
|
+
const newMap = map.withMutations((mutable) => {
|
|
84
|
+
mutable.set('b', 2)
|
|
85
|
+
mutable.set('c', 3)
|
|
86
|
+
mutable.delete('a')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
expect(newMap.size).toBe(2)
|
|
90
|
+
expect(newMap.get('a')).toBeUndefined()
|
|
91
|
+
expect(newMap.get('b')).toBe(2)
|
|
92
|
+
expect(newMap.get('c')).toBe(3)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('emptyMap', () => {
|
|
98
|
+
it('should create empty map with zero size', () => {
|
|
99
|
+
const empty = emptyMap()
|
|
100
|
+
expect(empty.size).toBe(0)
|
|
101
|
+
expect(empty.get('anything')).toBeUndefined()
|
|
102
|
+
})
|
|
103
|
+
})
|