@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.
Files changed (72) hide show
  1. package/dist-cjs/index.d.ts +1884 -153
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/lib/AtomMap.js +241 -1
  4. package/dist-cjs/lib/AtomMap.js.map +2 -2
  5. package/dist-cjs/lib/BaseRecord.js.map +2 -2
  6. package/dist-cjs/lib/ImmutableMap.js +141 -0
  7. package/dist-cjs/lib/ImmutableMap.js.map +2 -2
  8. package/dist-cjs/lib/IncrementalSetConstructor.js +45 -5
  9. package/dist-cjs/lib/IncrementalSetConstructor.js.map +2 -2
  10. package/dist-cjs/lib/RecordType.js +116 -21
  11. package/dist-cjs/lib/RecordType.js.map +2 -2
  12. package/dist-cjs/lib/RecordsDiff.js.map +2 -2
  13. package/dist-cjs/lib/Store.js +233 -39
  14. package/dist-cjs/lib/Store.js.map +2 -2
  15. package/dist-cjs/lib/StoreQueries.js +135 -22
  16. package/dist-cjs/lib/StoreQueries.js.map +2 -2
  17. package/dist-cjs/lib/StoreSchema.js +207 -2
  18. package/dist-cjs/lib/StoreSchema.js.map +2 -2
  19. package/dist-cjs/lib/StoreSideEffects.js +102 -10
  20. package/dist-cjs/lib/StoreSideEffects.js.map +2 -2
  21. package/dist-cjs/lib/executeQuery.js.map +2 -2
  22. package/dist-cjs/lib/migrate.js.map +2 -2
  23. package/dist-cjs/lib/setUtils.js.map +2 -2
  24. package/dist-esm/index.d.mts +1884 -153
  25. package/dist-esm/index.mjs +1 -1
  26. package/dist-esm/lib/AtomMap.mjs +241 -1
  27. package/dist-esm/lib/AtomMap.mjs.map +2 -2
  28. package/dist-esm/lib/BaseRecord.mjs.map +2 -2
  29. package/dist-esm/lib/ImmutableMap.mjs +141 -0
  30. package/dist-esm/lib/ImmutableMap.mjs.map +2 -2
  31. package/dist-esm/lib/IncrementalSetConstructor.mjs +45 -5
  32. package/dist-esm/lib/IncrementalSetConstructor.mjs.map +2 -2
  33. package/dist-esm/lib/RecordType.mjs +116 -21
  34. package/dist-esm/lib/RecordType.mjs.map +2 -2
  35. package/dist-esm/lib/RecordsDiff.mjs.map +2 -2
  36. package/dist-esm/lib/Store.mjs +233 -39
  37. package/dist-esm/lib/Store.mjs.map +2 -2
  38. package/dist-esm/lib/StoreQueries.mjs +135 -22
  39. package/dist-esm/lib/StoreQueries.mjs.map +2 -2
  40. package/dist-esm/lib/StoreSchema.mjs +207 -2
  41. package/dist-esm/lib/StoreSchema.mjs.map +2 -2
  42. package/dist-esm/lib/StoreSideEffects.mjs +102 -10
  43. package/dist-esm/lib/StoreSideEffects.mjs.map +2 -2
  44. package/dist-esm/lib/executeQuery.mjs.map +2 -2
  45. package/dist-esm/lib/migrate.mjs.map +2 -2
  46. package/dist-esm/lib/setUtils.mjs.map +2 -2
  47. package/package.json +3 -3
  48. package/src/lib/AtomMap.ts +241 -1
  49. package/src/lib/BaseRecord.test.ts +44 -0
  50. package/src/lib/BaseRecord.ts +118 -4
  51. package/src/lib/ImmutableMap.test.ts +103 -0
  52. package/src/lib/ImmutableMap.ts +212 -0
  53. package/src/lib/IncrementalSetConstructor.test.ts +111 -0
  54. package/src/lib/IncrementalSetConstructor.ts +63 -6
  55. package/src/lib/RecordType.ts +149 -25
  56. package/src/lib/RecordsDiff.test.ts +144 -0
  57. package/src/lib/RecordsDiff.ts +145 -10
  58. package/src/lib/Store.test.ts +827 -0
  59. package/src/lib/Store.ts +533 -67
  60. package/src/lib/StoreQueries.test.ts +627 -0
  61. package/src/lib/StoreQueries.ts +194 -27
  62. package/src/lib/StoreSchema.test.ts +226 -0
  63. package/src/lib/StoreSchema.ts +386 -8
  64. package/src/lib/StoreSideEffects.test.ts +239 -19
  65. package/src/lib/StoreSideEffects.ts +266 -19
  66. package/src/lib/devFreeze.test.ts +137 -0
  67. package/src/lib/executeQuery.test.ts +481 -0
  68. package/src/lib/executeQuery.ts +80 -2
  69. package/src/lib/migrate.test.ts +400 -0
  70. package/src/lib/migrate.ts +187 -14
  71. package/src/lib/setUtils.test.ts +105 -0
  72. package/src/lib/setUtils.ts +44 -4
@@ -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
- /** @internal */
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
+ })
@@ -1,11 +1,78 @@
1
- /** @public */
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
- /** @public */
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
- /** @public */
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
+ })