@zeix/cause-effect 0.16.0 → 0.16.1

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/src/store.ts CHANGED
@@ -2,11 +2,10 @@ import { isComputed } from './computed'
2
2
  import {
3
3
  type ArrayToRecord,
4
4
  diff,
5
+ type PartialRecord,
5
6
  type UnknownArray,
6
7
  type UnknownRecord,
7
- type UnknownRecordOrArray,
8
8
  } from './diff'
9
-
10
9
  import {
11
10
  InvalidSignalValueError,
12
11
  NullishSignalValueError,
@@ -40,9 +39,9 @@ import {
40
39
  type ArrayItem<T> = T extends readonly (infer U extends {})[] ? U : never
41
40
 
42
41
  type StoreChanges<T> = {
43
- add: Partial<T>
44
- change: Partial<T>
45
- remove: Partial<T>
42
+ add: PartialRecord<T>
43
+ change: PartialRecord<T>
44
+ remove: PartialRecord<T>
46
45
  sort: string[]
47
46
  }
48
47
 
@@ -52,7 +51,7 @@ type StoreListeners<T> = {
52
51
 
53
52
  interface BaseStore {
54
53
  readonly [Symbol.toStringTag]: 'Store'
55
- readonly size: State<number>
54
+ readonly length: number
56
55
  }
57
56
 
58
57
  type RecordStore<T extends UnknownRecord> = BaseStore & {
@@ -106,7 +105,6 @@ type ArrayStore<T extends UnknownArray> = BaseStore & {
106
105
  listener: (change: StoreChanges<T>[K]) => void,
107
106
  ): Cleanup
108
107
  remove(index: number): void
109
- readonly length: number
110
108
  }
111
109
 
112
110
  type Store<T extends UnknownRecord | UnknownArray> = T extends UnknownRecord
@@ -140,9 +138,9 @@ const createStore = <T extends UnknownRecord | UnknownArray>(
140
138
 
141
139
  const watchers = new Set<Watcher>()
142
140
  const listeners: StoreListeners<T> = {
143
- add: new Set<(change: Partial<T>) => void>(),
144
- change: new Set<(change: Partial<T>) => void>(),
145
- remove: new Set<(change: Partial<T>) => void>(),
141
+ add: new Set<(change: PartialRecord<T>) => void>(),
142
+ change: new Set<(change: PartialRecord<T>) => void>(),
143
+ remove: new Set<(change: PartialRecord<T>) => void>(),
146
144
  sort: new Set<(change: string[]) => void>(),
147
145
  }
148
146
  const signals = new Map<string, Signal<T[Extract<keyof T, string>] & {}>>()
@@ -151,15 +149,10 @@ const createStore = <T extends UnknownRecord | UnknownArray>(
151
149
  // Determine if this is an array-like store at creation time
152
150
  const isArrayLike = Array.isArray(initialValue)
153
151
 
154
- // Internal state
155
- const size = createState(0)
156
-
157
152
  // Get current record
158
153
  const current = () => {
159
154
  const record: Record<string, unknown> = {}
160
- for (const [key, signal] of signals) {
161
- record[key] = signal.get()
162
- }
155
+ for (const [key, signal] of signals) record[key] = signal.get()
163
156
  return record
164
157
  }
165
158
 
@@ -169,9 +162,7 @@ const createStore = <T extends UnknownRecord | UnknownArray>(
169
162
  changes: StoreChanges<T>[K],
170
163
  ) => {
171
164
  Object.freeze(changes)
172
- for (const listener of listeners[key]) {
173
- listener(changes)
174
- }
165
+ for (const listener of listeners[key]) listener(changes)
175
166
  }
176
167
 
177
168
  // Get sorted indexes
@@ -198,9 +189,9 @@ const createStore = <T extends UnknownRecord | UnknownArray>(
198
189
  }
199
190
 
200
191
  // Add nested signal and effect
201
- const addProperty = <K extends Extract<keyof T, string>>(
202
- key: K,
203
- value: T[K] | ArrayItem<T>,
192
+ const addProperty = (
193
+ key: string,
194
+ value: ArrayItem<T> | T[keyof T],
204
195
  single = false,
205
196
  ): boolean => {
206
197
  if (!isValidValue(key, value)) return false
@@ -214,29 +205,21 @@ const createStore = <T extends UnknownRecord | UnknownArray>(
214
205
  signals.set(key, signal)
215
206
  const watcher = createWatcher(() =>
216
207
  observe(() => {
217
- emit('change', {
218
- [key]: signal.get(),
219
- } as unknown as Partial<T>)
208
+ emit('change', { [key]: signal.get() } as PartialRecord<T>)
220
209
  }, watcher),
221
210
  )
222
211
  watcher()
223
212
  signalWatchers.set(key, watcher)
224
213
 
225
214
  if (single) {
226
- size.set(signals.size)
227
215
  notify(watchers)
228
- emit('add', {
229
- [key]: value,
230
- } as unknown as Partial<T>)
216
+ emit('add', { [key]: value } as PartialRecord<T>)
231
217
  }
232
218
  return true
233
219
  }
234
220
 
235
221
  // Remove nested signal and effect
236
- const removeProperty = <K extends Extract<keyof T, string>>(
237
- key: K,
238
- single = false,
239
- ) => {
222
+ const removeProperty = (key: string, single = false) => {
240
223
  const ok = signals.delete(key)
241
224
  if (ok) {
242
225
  const watcher = signalWatchers.get(key)
@@ -245,11 +228,8 @@ const createStore = <T extends UnknownRecord | UnknownArray>(
245
228
  }
246
229
 
247
230
  if (single) {
248
- size.set(signals.size)
249
231
  notify(watchers)
250
- emit('remove', {
251
- [key]: UNSET,
252
- } as unknown as Partial<T>)
232
+ emit('remove', { [key]: UNSET } as PartialRecord<T>)
253
233
  }
254
234
  return ok
255
235
  }
@@ -268,21 +248,16 @@ const createStore = <T extends UnknownRecord | UnknownArray>(
268
248
  batch(() => {
269
249
  // Additions
270
250
  if (Object.keys(changes.add).length) {
271
- for (const key in changes.add) {
272
- const value = changes.add[key] ?? UNSET
273
- addProperty(
274
- key as Extract<keyof T, string>,
275
- value as T[Extract<keyof T, string>] & {},
276
- )
277
- }
251
+ for (const key in changes.add)
252
+ addProperty(key, changes.add[key] ?? UNSET)
278
253
 
279
254
  // Queue initial additions event to allow listeners to be added first
280
255
  if (initialRun) {
281
256
  setTimeout(() => {
282
- emit('add', changes.add as Partial<T>)
257
+ emit('add', changes.add)
283
258
  }, 0)
284
259
  } else {
285
- emit('add', changes.add as Partial<T>)
260
+ emit('add', changes.add)
286
261
  }
287
262
  }
288
263
 
@@ -292,211 +267,185 @@ const createStore = <T extends UnknownRecord | UnknownArray>(
292
267
  const value = changes.change[key]
293
268
  if (!isValidValue(key, value)) continue
294
269
  const signal = signals.get(key as Extract<keyof T, string>)
295
- if (isMutableSignal(signal))
296
- signal.set(value as T[Extract<keyof T, string>] & {})
270
+ if (isMutableSignal(signal)) signal.set(value)
297
271
  else
298
272
  throw new StoreKeyReadonlyError(key, valueString(value))
299
273
  }
300
- emit('change', changes.change as Partial<T>)
274
+ emit('change', changes.change)
301
275
  }
302
276
 
303
277
  // Removals
304
278
  if (Object.keys(changes.remove).length) {
305
- for (const key in changes.remove)
306
- removeProperty(key as Extract<keyof T, string>)
307
- emit('remove', changes.remove as Partial<T>)
279
+ for (const key in changes.remove) removeProperty(key)
280
+ emit('remove', changes.remove)
308
281
  }
309
-
310
- size.set(signals.size)
311
282
  })
312
283
 
313
284
  return changes.changed
314
285
  }
315
286
 
316
- // Initialize data - convert arrays to records for internal storage
287
+ // Initialize data
317
288
  reconcile({} as T, initialValue, true)
318
289
 
319
290
  // Methods and Properties
320
- const store: Record<string, unknown> = {
321
- add: isArrayLike
322
- ? (v: ArrayItem<T>): void => {
323
- const nextIndex = signals.size
324
- const key = String(nextIndex) as Extract<keyof T, string>
325
- addProperty(key, v, true)
291
+ const store: Record<PropertyKey, unknown> = {}
292
+ Object.defineProperties(store, {
293
+ [Symbol.toStringTag]: {
294
+ value: TYPE_STORE,
295
+ },
296
+ [Symbol.isConcatSpreadable]: {
297
+ value: isArrayLike,
298
+ },
299
+ [Symbol.iterator]: {
300
+ value: isArrayLike
301
+ ? function* () {
302
+ const indexes = getSortedIndexes()
303
+ for (const index of indexes) {
304
+ const signal = signals.get(String(index))
305
+ if (signal) yield signal
306
+ }
307
+ }
308
+ : function* () {
309
+ for (const [key, signal] of signals) yield [key, signal]
310
+ },
311
+ },
312
+ add: {
313
+ value: isArrayLike
314
+ ? (v: ArrayItem<T>): void => {
315
+ addProperty(String(signals.size), v, true)
316
+ }
317
+ : <K extends Extract<keyof T, string>>(k: K, v: T[K]): void => {
318
+ if (!signals.has(k)) addProperty(k, v, true)
319
+ else throw new StoreKeyExistsError(k, valueString(v))
320
+ },
321
+ },
322
+ get: {
323
+ value: (): T => {
324
+ subscribe(watchers)
325
+ return recordToArray(current()) as T
326
+ },
327
+ },
328
+ remove: {
329
+ value: isArrayLike
330
+ ? (index: number): void => {
331
+ const currentArray = recordToArray(current()) as T
332
+ const currentLength = signals.size
333
+ if (
334
+ !Array.isArray(currentArray) ||
335
+ index <= -currentLength ||
336
+ index >= currentLength
337
+ )
338
+ throw new StoreKeyRangeError(index)
339
+ const newArray = [...currentArray]
340
+ newArray.splice(index, 1)
341
+
342
+ if (reconcile(currentArray, newArray as unknown as T))
343
+ notify(watchers)
344
+ }
345
+ : (k: string): void => {
346
+ if (signals.has(k)) removeProperty(k, true)
347
+ },
348
+ },
349
+ set: {
350
+ value: (v: T): void => {
351
+ if (reconcile(current() as T, v)) {
352
+ notify(watchers)
353
+ if (UNSET === v) watchers.clear()
326
354
  }
327
- : <K extends Extract<keyof T, string>>(k: K, v: T[K]): void => {
328
- if (!signals.has(k)) addProperty(k, v, true)
329
- else throw new StoreKeyExistsError(k, valueString(v))
330
- },
331
- get: (): T => {
332
- subscribe(watchers)
333
- return recordToArray(current()) as T
355
+ },
334
356
  },
335
- remove: isArrayLike
336
- ? (index: number): void => {
337
- const currentArray = recordToArray(current()) as T
338
- const currentLength = signals.size
339
- if (
340
- !Array.isArray(currentArray) ||
341
- index <= -currentLength ||
342
- index >= currentLength
343
- )
344
- throw new StoreKeyRangeError(index)
345
- const newArray = [...currentArray]
346
- newArray.splice(index, 1)
347
-
348
- if (reconcile(currentArray, newArray as unknown as T))
349
- notify(watchers)
357
+ update: {
358
+ value: (fn: (v: T) => T): void => {
359
+ const oldValue = current()
360
+ const newValue = fn(recordToArray(oldValue) as T)
361
+ if (reconcile(oldValue as T, newValue)) {
362
+ notify(watchers)
363
+ if (UNSET === newValue) watchers.clear()
350
364
  }
351
- : <K extends Extract<keyof T, string>>(k: K): void => {
352
- if (signals.has(k)) removeProperty(k, true)
353
- },
354
- set: (v: T): void => {
355
- if (reconcile(current() as T, v)) {
356
- notify(watchers)
357
- if (UNSET === v) watchers.clear()
358
- }
365
+ },
359
366
  },
360
- update: (fn: (v: T) => T): void => {
361
- const oldValue = current()
362
- const newValue = fn(recordToArray(oldValue) as T)
363
- if (reconcile(oldValue as T, newValue)) {
367
+ sort: {
368
+ value: (
369
+ compareFn?: <
370
+ U = T extends UnknownArray
371
+ ? ArrayItem<T>
372
+ : T[Extract<keyof T, string>],
373
+ >(
374
+ a: U,
375
+ b: U,
376
+ ) => number,
377
+ ): void => {
378
+ // Get all entries as [key, value] pairs
379
+ const entries = Array.from(signals.entries())
380
+ .map(([key, signal]) => [key, signal.get()])
381
+ .sort(
382
+ compareFn
383
+ ? (a, b) => compareFn(a[1], b[1])
384
+ : (a, b) =>
385
+ String(a[1]).localeCompare(String(b[1])),
386
+ )
387
+
388
+ // Create array of original keys in their new sorted order
389
+ const newOrder: string[] = entries.map(([key]) => String(key))
390
+ const newSignals = new Map<
391
+ string,
392
+ Signal<T[Extract<keyof T, string>] & {}>
393
+ >()
394
+
395
+ entries.forEach(([key], newIndex) => {
396
+ const oldKey = String(key)
397
+ const newKey = isArrayLike ? String(newIndex) : String(key)
398
+ const signal = signals.get(oldKey)
399
+ if (signal) newSignals.set(newKey, signal)
400
+ })
401
+
402
+ // Replace signals map
403
+ signals.clear()
404
+ newSignals.forEach((signal, key) => signals.set(key, signal))
364
405
  notify(watchers)
365
- if (UNSET === newValue) watchers.clear()
366
- }
406
+ emit('sort', newOrder)
407
+ },
367
408
  },
368
- sort: (
369
- compareFn?: <
370
- U = T extends UnknownArray
371
- ? ArrayItem<T>
372
- : T[Extract<keyof T, string>],
373
- >(
374
- a: U,
375
- b: U,
376
- ) => number,
377
- ): void => {
378
- // Get all entries as [key, value] pairs
379
- const entries = Array.from(signals.entries())
380
- .map(
381
- ([key, signal]) =>
382
- [key, signal.get()] as [
383
- string,
384
- T[Extract<keyof T, string>],
385
- ],
386
- )
387
- .sort(
388
- compareFn
389
- ? (a, b) => compareFn(a[1], b[1])
390
- : (a, b) => String(a[1]).localeCompare(String(b[1])),
391
- )
392
-
393
- // Create array of original keys in their new sorted order
394
- const newOrder: string[] = entries.map(([key]) => String(key))
395
- const newSignals = new Map<
396
- string,
397
- Signal<T[Extract<keyof T, string>] & {}>
398
- >()
399
-
400
- entries.forEach(([key], newIndex) => {
401
- const oldKey = String(key)
402
- const newKey = isArrayLike ? String(newIndex) : String(key)
403
-
404
- const signal = signals.get(oldKey)
405
- if (signal) newSignals.set(newKey, signal)
406
- })
407
-
408
- // Replace signals map
409
- signals.clear()
410
- newSignals.forEach((signal, key) => signals.set(key, signal))
411
- notify(watchers)
412
- emit('sort', newOrder)
409
+ on: {
410
+ value: <K extends keyof StoreChanges<T>>(
411
+ type: K,
412
+ listener: (change: StoreChanges<T>[K]) => void,
413
+ ): Cleanup => {
414
+ listeners[type].add(listener)
415
+ return () => listeners[type].delete(listener)
416
+ },
413
417
  },
414
- on: <K extends keyof StoreChanges<T>>(
415
- type: K,
416
- listener: (change: StoreChanges<T>[K]) => void,
417
- ): Cleanup => {
418
- listeners[type].add(listener)
419
- return () => listeners[type].delete(listener)
418
+ length: {
419
+ get(): number {
420
+ subscribe(watchers)
421
+ return signals.size
422
+ },
420
423
  },
421
- size,
422
- }
424
+ })
423
425
 
424
426
  // Return proxy directly with integrated signal methods
425
- return new Proxy({} as Store<T>, {
426
- get(_target, prop) {
427
- // Symbols
428
- if (prop === Symbol.toStringTag) return TYPE_STORE
429
- if (prop === Symbol.isConcatSpreadable) return isArrayLike
430
- if (prop === Symbol.iterator)
431
- return isArrayLike
432
- ? function* () {
433
- const indexes = getSortedIndexes()
434
- for (const index of indexes) {
435
- const signal = signals.get(
436
- String(index) as Extract<keyof T, string>,
437
- )
438
- if (signal) yield signal
439
- }
440
- }
441
- : function* () {
442
- for (const [key, signal] of signals)
443
- yield [key, signal]
444
- }
427
+ return new Proxy(store as Store<T>, {
428
+ get(target, prop) {
429
+ if (prop in target) return Reflect.get(target, prop)
445
430
  if (isSymbol(prop)) return undefined
446
-
447
- // Methods and Properties
448
- if (prop in store) return store[prop]
449
- if (prop === 'length' && isArrayLike) {
450
- subscribe(watchers)
451
- return size.get()
452
- }
453
-
454
- // Signals
455
- return signals.get(prop as Extract<keyof T, string>)
431
+ return signals.get(prop)
456
432
  },
457
- has(_target, prop) {
458
- const stringProp = String(prop)
459
- return (
460
- (stringProp &&
461
- signals.has(stringProp as Extract<keyof T, string>)) ||
462
- Object.keys(store).includes(stringProp) ||
463
- prop === Symbol.toStringTag ||
464
- prop === Symbol.iterator ||
465
- prop === Symbol.isConcatSpreadable ||
466
- (prop === 'length' && isArrayLike)
467
- )
433
+ has(target, prop) {
434
+ if (prop in target) return true
435
+ return signals.has(String(prop))
468
436
  },
469
- ownKeys() {
470
- return isArrayLike
471
- ? getSortedIndexes()
472
- .map(key => String(key))
473
- .concat(['length'])
474
- : Array.from(signals.keys()).map(key => String(key))
437
+ ownKeys(target) {
438
+ const staticKeys = Reflect.ownKeys(target)
439
+ const signalKeys = isArrayLike
440
+ ? getSortedIndexes().map(key => String(key))
441
+ : Array.from(signals.keys())
442
+ return [...new Set([...signalKeys, ...staticKeys])]
475
443
  },
476
- getOwnPropertyDescriptor(_target, prop) {
477
- const nonEnumerable = <T>(value: T) => ({
478
- enumerable: false,
479
- configurable: true,
480
- writable: false,
481
- value,
482
- })
483
-
484
- if (prop === 'length' && isArrayLike)
485
- return {
486
- enumerable: true,
487
- configurable: true,
488
- writable: false,
489
- value: size.get(),
490
- }
491
- if (prop === Symbol.isConcatSpreadable)
492
- return nonEnumerable(isArrayLike)
493
- if (prop === Symbol.toStringTag) return nonEnumerable(TYPE_STORE)
494
- if (isSymbol(prop)) return undefined
495
-
496
- if (Object.keys(store).includes(prop))
497
- return nonEnumerable(store[prop])
444
+ getOwnPropertyDescriptor(target, prop) {
445
+ if (prop in target)
446
+ return Reflect.getOwnPropertyDescriptor(target, prop)
498
447
 
499
- const signal = signals.get(prop as Extract<keyof T, string>)
448
+ const signal = signals.get(String(prop))
500
449
  return signal
501
450
  ? {
502
451
  enumerable: true,
@@ -516,7 +465,7 @@ const createStore = <T extends UnknownRecord | UnknownArray>(
516
465
  * @param {unknown} value - value to check
517
466
  * @returns {boolean} - true if the value is a Store instance, false otherwise
518
467
  */
519
- const isStore = <T extends UnknownRecordOrArray>(
468
+ const isStore = <T extends UnknownRecord | UnknownArray>(
520
469
  value: unknown,
521
470
  ): value is Store<T> => isObjectOfType(value, TYPE_STORE)
522
471
 
package/src/util.ts CHANGED
@@ -66,9 +66,7 @@ const toError = /*#__PURE__*/ (reason: unknown): Error =>
66
66
 
67
67
  const arrayToRecord = /*#__PURE__*/ <T>(array: T[]): Record<string, T> => {
68
68
  const record: Record<string, T> = {}
69
- for (let i = 0; i < array.length; i++) {
70
- record[String(i)] = array[i]
71
- }
69
+ for (let i = 0; i < array.length; i++) record[String(i)] = array[i]
72
70
  return record
73
71
  }
74
72
 
@@ -79,9 +77,7 @@ const recordToArray = /*#__PURE__*/ <T>(
79
77
  if (indexes === null) return record
80
78
 
81
79
  const array: T[] = []
82
- for (const index of indexes) {
83
- array.push(record[String(index)])
84
- }
80
+ for (const index of indexes) array.push(record[String(index)])
85
81
  return array
86
82
  }
87
83
 
@@ -6,7 +6,7 @@ import {
6
6
  createState,
7
7
  match,
8
8
  resolve,
9
- } from '../'
9
+ } from '..'
10
10
 
11
11
  /* === Tests === */
12
12
 
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, mock, test } from 'bun:test'
2
- import { batch, createComputed, createEffect, createState } from '../'
2
+ import { batch, createComputed, createEffect, createState } from '..'
3
3
  import { Counter, makeGraph, runGraph } from './util/dependency-graph'
4
4
  import type { Computed, ReactiveFramework } from './util/reactive-framework'
5
5
 
@@ -8,7 +8,7 @@ import {
8
8
  match,
9
9
  resolve,
10
10
  UNSET,
11
- } from '../'
11
+ } from '..'
12
12
 
13
13
  /* === Utility Functions === */
14
14
 
@@ -7,7 +7,7 @@ import {
7
7
  match,
8
8
  resolve,
9
9
  UNSET,
10
- } from '../'
10
+ } from '..'
11
11
 
12
12
  /* === Utility Functions === */
13
13
 
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { createComputed, createState, match, resolve, UNSET } from '../'
2
+ import { createComputed, createState, match, resolve, UNSET } from '..'
3
3
 
4
4
  /* === Tests === */
5
5
 
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { createComputed, resolve, createState, UNSET } from '../'
2
+ import { createComputed, createState, resolve, UNSET } from '..'
3
3
 
4
4
  /* === Tests === */
5
5
 
@@ -34,13 +34,13 @@ describe('toSignal', () => {
34
34
  expect(typedResult).toBeDefined()
35
35
  })
36
36
 
37
- test('converts empty array to Store<Record<string, never>>', () => {
37
+ test('converts empty array to ArrayStore<never[]>', () => {
38
38
  const result = toSignal([])
39
39
 
40
40
  // Runtime behavior
41
41
  expect(isStore(result)).toBe(true)
42
42
  expect(result.length).toBe(0)
43
- expect(Object.keys(result).length).toBe(1) // length property
43
+ expect(Object.keys(result).length).toBe(0)
44
44
  })
45
45
 
46
46
  test('converts record to Store<T>', () => {
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { createState, isComputed, isState } from '../'
2
+ import { createState, isComputed, isState } from '..'
3
3
 
4
4
  /* === Tests === */
5
5