@zeix/cause-effect 0.15.1 → 0.16.0

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 (48) hide show
  1. package/.ai-context.md +254 -0
  2. package/.cursorrules +54 -0
  3. package/.github/copilot-instructions.md +132 -0
  4. package/CLAUDE.md +319 -0
  5. package/README.md +167 -159
  6. package/eslint.config.js +1 -1
  7. package/index.dev.js +528 -407
  8. package/index.js +1 -1
  9. package/index.ts +36 -25
  10. package/package.json +1 -1
  11. package/src/computed.ts +41 -30
  12. package/src/diff.ts +57 -44
  13. package/src/effect.ts +15 -16
  14. package/src/errors.ts +64 -0
  15. package/src/match.ts +2 -2
  16. package/src/resolve.ts +2 -2
  17. package/src/signal.ts +27 -49
  18. package/src/state.ts +27 -19
  19. package/src/store.ts +410 -209
  20. package/src/system.ts +122 -0
  21. package/src/util.ts +45 -6
  22. package/test/batch.test.ts +18 -11
  23. package/test/benchmark.test.ts +4 -4
  24. package/test/computed.test.ts +508 -72
  25. package/test/diff.test.ts +321 -4
  26. package/test/effect.test.ts +61 -61
  27. package/test/match.test.ts +38 -28
  28. package/test/resolve.test.ts +16 -16
  29. package/test/signal.test.ts +19 -147
  30. package/test/state.test.ts +212 -25
  31. package/test/store.test.ts +1370 -134
  32. package/test/util/dependency-graph.ts +1 -1
  33. package/types/index.d.ts +10 -9
  34. package/types/src/collection.d.ts +26 -0
  35. package/types/src/computed.d.ts +9 -9
  36. package/types/src/diff.d.ts +5 -3
  37. package/types/src/effect.d.ts +3 -3
  38. package/types/src/errors.d.ts +22 -0
  39. package/types/src/match.d.ts +1 -1
  40. package/types/src/resolve.d.ts +1 -1
  41. package/types/src/signal.d.ts +12 -19
  42. package/types/src/state.d.ts +5 -5
  43. package/types/src/store.d.ts +40 -36
  44. package/types/src/system.d.ts +44 -0
  45. package/types/src/util.d.ts +7 -5
  46. package/index.d.ts +0 -36
  47. package/src/scheduler.ts +0 -172
  48. package/types/test-new-effect.d.ts +0 -1
package/src/store.ts CHANGED
@@ -1,82 +1,123 @@
1
- import { diff, type UnknownRecord, type UnknownRecordOrArray } from './diff'
2
- import { effect } from './effect'
1
+ import { isComputed } from './computed'
2
+ import {
3
+ type ArrayToRecord,
4
+ diff,
5
+ type UnknownArray,
6
+ type UnknownRecord,
7
+ type UnknownRecordOrArray,
8
+ } from './diff'
9
+
10
+ import {
11
+ InvalidSignalValueError,
12
+ NullishSignalValueError,
13
+ StoreKeyExistsError,
14
+ StoreKeyRangeError,
15
+ StoreKeyReadonlyError,
16
+ } from './errors'
17
+ import { isMutableSignal, type Signal } from './signal'
18
+ import { createState, isState, type State } from './state'
3
19
  import {
4
20
  batch,
5
21
  type Cleanup,
22
+ createWatcher,
6
23
  notify,
24
+ observe,
7
25
  subscribe,
8
26
  type Watcher,
9
- } from './scheduler'
10
- import { type Signal, toMutableSignal, UNSET } from './signal'
11
- import { type State, state } from './state'
12
- import { hasMethod, isObjectOfType, validArrayIndexes } from './util'
13
-
14
- /* === Constants === */
15
-
16
- const TYPE_STORE = 'Store'
27
+ } from './system'
28
+ import {
29
+ isFunction,
30
+ isObjectOfType,
31
+ isRecord,
32
+ isSymbol,
33
+ recordToArray,
34
+ UNSET,
35
+ valueString,
36
+ } from './util'
17
37
 
18
38
  /* === Types === */
19
39
 
20
- interface StoreAddEvent<T extends UnknownRecordOrArray> extends CustomEvent {
21
- type: 'store-add'
22
- detail: Partial<T>
23
- }
40
+ type ArrayItem<T> = T extends readonly (infer U extends {})[] ? U : never
24
41
 
25
- interface StoreChangeEvent<T extends UnknownRecordOrArray> extends CustomEvent {
26
- type: 'store-change'
27
- detail: Partial<T>
42
+ type StoreChanges<T> = {
43
+ add: Partial<T>
44
+ change: Partial<T>
45
+ remove: Partial<T>
46
+ sort: string[]
28
47
  }
29
48
 
30
- interface StoreRemoveEvent<T extends UnknownRecordOrArray> extends CustomEvent {
31
- type: 'store-remove'
32
- detail: Partial<T>
49
+ type StoreListeners<T> = {
50
+ [K in keyof StoreChanges<T>]: Set<(change: StoreChanges<T>[K]) => void>
33
51
  }
34
52
 
35
- type StoreEventMap<T extends UnknownRecordOrArray> = {
36
- 'store-add': StoreAddEvent<T>
37
- 'store-change': StoreChangeEvent<T>
38
- 'store-remove': StoreRemoveEvent<T>
53
+ interface BaseStore {
54
+ readonly [Symbol.toStringTag]: 'Store'
55
+ readonly size: State<number>
39
56
  }
40
57
 
41
- interface StoreEventTarget<T extends UnknownRecordOrArray> extends EventTarget {
42
- addEventListener<K extends keyof StoreEventMap<T>>(
43
- type: K,
44
- listener: (event: StoreEventMap<T>[K]) => void,
45
- options?: boolean | AddEventListenerOptions,
58
+ type RecordStore<T extends UnknownRecord> = BaseStore & {
59
+ [K in keyof T]: T[K] extends readonly unknown[] | Record<string, unknown>
60
+ ? Store<T[K]>
61
+ : State<T[K]>
62
+ } & {
63
+ [Symbol.iterator](): IterableIterator<
64
+ [
65
+ Extract<keyof T, string>,
66
+ T[Extract<keyof T, string>] extends
67
+ | readonly unknown[]
68
+ | Record<string, unknown>
69
+ ? Store<T[Extract<keyof T, string>]>
70
+ : State<T[Extract<keyof T, string>]>,
71
+ ]
72
+ >
73
+ add<K extends Extract<keyof T, string>>(key: K, value: T[K]): void
74
+ get(): T
75
+ set(value: T): void
76
+ update(fn: (value: T) => T): void
77
+ sort<U = T[Extract<keyof T, string>]>(
78
+ compareFn?: (a: U, b: U) => number,
46
79
  ): void
47
- addEventListener(
48
- type: string,
49
- listener: EventListenerOrEventListenerObject,
50
- options?: boolean | AddEventListenerOptions,
51
- ): void
52
-
53
- removeEventListener<K extends keyof StoreEventMap<T>>(
80
+ on<K extends keyof StoreChanges<T>>(
54
81
  type: K,
55
- listener: (event: StoreEventMap<T>[K]) => void,
56
- options?: boolean | EventListenerOptions,
57
- ): void
58
- removeEventListener(
59
- type: string,
60
- listener: EventListenerOrEventListenerObject,
61
- options?: boolean | EventListenerOptions,
62
- ): void
82
+ listener: (change: StoreChanges<T>[K]) => void,
83
+ ): Cleanup
84
+ remove<K extends Extract<keyof T, string>>(key: K): void
85
+ }
63
86
 
64
- dispatchEvent(event: Event): boolean
87
+ type ArrayStore<T extends UnknownArray> = BaseStore & {
88
+ [Symbol.iterator](): IterableIterator<
89
+ ArrayItem<T> extends readonly unknown[] | Record<string, unknown>
90
+ ? Store<ArrayItem<T>>
91
+ : State<ArrayItem<T>>
92
+ >
93
+ readonly [Symbol.isConcatSpreadable]: boolean
94
+ [n: number]: ArrayItem<T> extends
95
+ | readonly unknown[]
96
+ | Record<string, unknown>
97
+ ? Store<ArrayItem<T>>
98
+ : State<ArrayItem<T>>
99
+ add(value: ArrayItem<T>): void
100
+ get(): T
101
+ set(value: T): void
102
+ update(fn: (value: T) => T): void
103
+ sort<U = ArrayItem<T>>(compareFn?: (a: U, b: U) => number): void
104
+ on<K extends keyof StoreChanges<T>>(
105
+ type: K,
106
+ listener: (change: StoreChanges<T>[K]) => void,
107
+ ): Cleanup
108
+ remove(index: number): void
109
+ readonly length: number
65
110
  }
66
111
 
67
- type Store<T extends UnknownRecordOrArray = UnknownRecord> = {
68
- [K in keyof T]: T[K] extends UnknownRecord ? Store<T[K]> : State<T[K]>
69
- } & StoreEventTarget<T> & {
70
- [Symbol.toStringTag]: 'Store'
71
- [Symbol.iterator](): IterableIterator<[keyof T, Signal<T[keyof T]>]>
72
-
73
- add<K extends keyof T>(key: K, value: T[K]): void
74
- get(): T
75
- remove<K extends keyof T>(key: K): void
76
- set(value: T): void
77
- update(updater: (value: T) => T): void
78
- size: State<number>
79
- }
112
+ type Store<T extends UnknownRecord | UnknownArray> = T extends UnknownRecord
113
+ ? RecordStore<T>
114
+ : T extends UnknownArray
115
+ ? ArrayStore<T>
116
+ : never
117
+
118
+ /* === Constants === */
119
+
120
+ const TYPE_STORE = 'Store'
80
121
 
81
122
  /* === Functions === */
82
123
 
@@ -92,87 +133,178 @@ type Store<T extends UnknownRecordOrArray = UnknownRecord> = {
92
133
  * @param {T} initialValue - initial object or array value of the store
93
134
  * @returns {Store<T>} - new store with reactive properties that preserves the original type T
94
135
  */
95
- const store = <T extends UnknownRecordOrArray>(initialValue: T): Store<T> => {
96
- const watchers: Set<Watcher> = new Set()
97
- const eventTarget = new EventTarget()
98
- const signals: Map<keyof T, Store<T[keyof T]> | State<T[keyof T]>> =
99
- new Map()
100
- const cleanups = new Map<keyof T, Cleanup>()
136
+ const createStore = <T extends UnknownRecord | UnknownArray>(
137
+ initialValue: T,
138
+ ): Store<T> => {
139
+ if (initialValue == null) throw new NullishSignalValueError('store')
140
+
141
+ const watchers = new Set<Watcher>()
142
+ 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>(),
146
+ sort: new Set<(change: string[]) => void>(),
147
+ }
148
+ const signals = new Map<string, Signal<T[Extract<keyof T, string>] & {}>>()
149
+ const signalWatchers = new Map<string, Watcher>()
150
+
151
+ // Determine if this is an array-like store at creation time
152
+ const isArrayLike = Array.isArray(initialValue)
101
153
 
102
154
  // Internal state
103
- const size = state(0)
104
-
105
- // Get current record or array
106
- const current = (): T => {
107
- const keys = Array.from(signals.keys())
108
- const arrayIndexes = validArrayIndexes(keys)
109
- if (arrayIndexes)
110
- return arrayIndexes.map(index =>
111
- signals.get(String(index))?.get(),
112
- ) as unknown as T
113
- const record: Partial<T> = {}
155
+ const size = createState(0)
156
+
157
+ // Get current record
158
+ const current = () => {
159
+ const record: Record<string, unknown> = {}
114
160
  for (const [key, signal] of signals) {
115
161
  record[key] = signal.get()
116
162
  }
117
- return record as T
163
+ return record
118
164
  }
119
165
 
120
- // Emit event
121
- const emit = (type: keyof StoreEventMap<T>, detail: Partial<T>) =>
122
- eventTarget.dispatchEvent(new CustomEvent(type, { detail }))
166
+ // Emit change notifications
167
+ const emit = <K extends keyof StoreChanges<T>>(
168
+ key: K,
169
+ changes: StoreChanges<T>[K],
170
+ ) => {
171
+ Object.freeze(changes)
172
+ for (const listener of listeners[key]) {
173
+ listener(changes)
174
+ }
175
+ }
176
+
177
+ // Get sorted indexes
178
+ const getSortedIndexes = () =>
179
+ Array.from(signals.keys())
180
+ .map(k => Number(k))
181
+ .filter(n => Number.isInteger(n))
182
+ .sort((a, b) => a - b)
183
+
184
+ // Validate input
185
+ const isValidValue = <T>(
186
+ key: string,
187
+ value: T,
188
+ ): value is NonNullable<T> => {
189
+ if (value == null)
190
+ throw new NullishSignalValueError(`store for key "${key}"`)
191
+ if (value === UNSET) return true
192
+ if (isSymbol(value) || isFunction(value) || isComputed(value))
193
+ throw new InvalidSignalValueError(
194
+ `store for key "${key}"`,
195
+ valueString(value),
196
+ )
197
+ return true
198
+ }
123
199
 
124
200
  // Add nested signal and effect
125
- const addProperty = <K extends keyof T>(key: K, value: T[K]) => {
126
- const stringKey = String(key)
127
- const signal = toMutableSignal(value)
128
- signals.set(stringKey, signal as Store<T[keyof T]> | State<T[keyof T]>)
129
- const cleanup = effect(() => {
130
- const currentValue = signal.get()
131
- if (currentValue != null)
132
- emit('store-change', { [key]: currentValue } as Partial<T>)
133
- })
134
- cleanups.set(stringKey, cleanup)
201
+ const addProperty = <K extends Extract<keyof T, string>>(
202
+ key: K,
203
+ value: T[K] | ArrayItem<T>,
204
+ single = false,
205
+ ): boolean => {
206
+ if (!isValidValue(key, value)) return false
207
+ const signal =
208
+ isState(value) || isStore(value)
209
+ ? value
210
+ : isRecord(value) || Array.isArray(value)
211
+ ? createStore(value)
212
+ : createState(value)
213
+ // @ts-expect-error non-matching signal types
214
+ signals.set(key, signal)
215
+ const watcher = createWatcher(() =>
216
+ observe(() => {
217
+ emit('change', {
218
+ [key]: signal.get(),
219
+ } as unknown as Partial<T>)
220
+ }, watcher),
221
+ )
222
+ watcher()
223
+ signalWatchers.set(key, watcher)
224
+
225
+ if (single) {
226
+ size.set(signals.size)
227
+ notify(watchers)
228
+ emit('add', {
229
+ [key]: value,
230
+ } as unknown as Partial<T>)
231
+ }
232
+ return true
135
233
  }
136
234
 
137
235
  // Remove nested signal and effect
138
- const removeProperty = <K extends keyof T>(key: K) => {
139
- const stringKey = String(key)
140
- signals.delete(stringKey)
141
- const cleanup = cleanups.get(stringKey)
142
- if (cleanup) cleanup()
143
- cleanups.delete(stringKey)
236
+ const removeProperty = <K extends Extract<keyof T, string>>(
237
+ key: K,
238
+ single = false,
239
+ ) => {
240
+ const ok = signals.delete(key)
241
+ if (ok) {
242
+ const watcher = signalWatchers.get(key)
243
+ if (watcher) watcher.cleanup()
244
+ signalWatchers.delete(key)
245
+ }
246
+
247
+ if (single) {
248
+ size.set(signals.size)
249
+ notify(watchers)
250
+ emit('remove', {
251
+ [key]: UNSET,
252
+ } as unknown as Partial<T>)
253
+ }
254
+ return ok
144
255
  }
145
256
 
146
257
  // Reconcile data and dispatch events
147
- const reconcile = (oldValue: T, newValue: T): boolean => {
148
- const changes = diff(oldValue, newValue)
258
+ const reconcile = (
259
+ oldValue: T,
260
+ newValue: T,
261
+ initialRun?: boolean,
262
+ ): boolean => {
263
+ const changes = diff(
264
+ oldValue as T extends UnknownArray ? ArrayToRecord<T> : T,
265
+ newValue as T extends UnknownArray ? ArrayToRecord<T> : T,
266
+ )
149
267
 
150
268
  batch(() => {
269
+ // Additions
151
270
  if (Object.keys(changes.add).length) {
152
271
  for (const key in changes.add) {
153
- const value = changes.add[key]
154
- if (value != null) addProperty(key, value as T[keyof T])
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
+ }
278
+
279
+ // Queue initial additions event to allow listeners to be added first
280
+ if (initialRun) {
281
+ setTimeout(() => {
282
+ emit('add', changes.add as Partial<T>)
283
+ }, 0)
284
+ } else {
285
+ emit('add', changes.add as Partial<T>)
155
286
  }
156
- emit('store-add', changes.add)
157
287
  }
288
+
289
+ // Changes
158
290
  if (Object.keys(changes.change).length) {
159
291
  for (const key in changes.change) {
160
- const signal = signals.get(key)
161
292
  const value = changes.change[key]
162
- if (
163
- signal &&
164
- value != null &&
165
- hasMethod<Signal<T[keyof T]>>(signal, 'set')
166
- )
167
- signal.set(value)
293
+ if (!isValidValue(key, value)) continue
294
+ const signal = signals.get(key as Extract<keyof T, string>)
295
+ if (isMutableSignal(signal))
296
+ signal.set(value as T[Extract<keyof T, string>] & {})
297
+ else
298
+ throw new StoreKeyReadonlyError(key, valueString(value))
168
299
  }
169
- emit('store-change', changes.change)
300
+ emit('change', changes.change as Partial<T>)
170
301
  }
302
+
303
+ // Removals
171
304
  if (Object.keys(changes.remove).length) {
172
- for (const key in changes.remove) {
173
- removeProperty(key)
174
- }
175
- emit('store-remove', changes.remove)
305
+ for (const key in changes.remove)
306
+ removeProperty(key as Extract<keyof T, string>)
307
+ emit('remove', changes.remove as Partial<T>)
176
308
  }
177
309
 
178
310
  size.set(signals.size)
@@ -181,112 +313,190 @@ const store = <T extends UnknownRecordOrArray>(initialValue: T): Store<T> => {
181
313
  return changes.changed
182
314
  }
183
315
 
184
- // Initialize data
185
- reconcile({} as T, initialValue)
316
+ // Initialize data - convert arrays to records for internal storage
317
+ reconcile({} as T, initialValue, true)
186
318
 
187
- // Queue initial additions event to allow listeners to be added first
188
- setTimeout(() => {
189
- const initialAdditionsEvent = new CustomEvent('store-add', {
190
- detail: initialValue,
191
- })
192
- eventTarget.dispatchEvent(initialAdditionsEvent)
193
- }, 0)
194
-
195
- const storeProps = [
196
- 'add',
197
- 'get',
198
- 'remove',
199
- 'set',
200
- 'update',
201
- 'addEventListener',
202
- 'removeEventListener',
203
- 'dispatchEvent',
204
- 'size',
205
- ]
319
+ // 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)
326
+ }
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
334
+ },
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)
350
+ }
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
+ }
359
+ },
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)) {
364
+ notify(watchers)
365
+ if (UNSET === newValue) watchers.clear()
366
+ }
367
+ },
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)
413
+ },
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)
420
+ },
421
+ size,
422
+ }
206
423
 
207
424
  // Return proxy directly with integrated signal methods
208
425
  return new Proxy({} as Store<T>, {
209
426
  get(_target, prop) {
210
- // Handle signal methods and size property
211
- switch (prop) {
212
- case 'add':
213
- return <K extends keyof T>(k: K, v: T[K]): void => {
214
- if (!signals.has(k)) {
215
- addProperty(k, v)
216
- notify(watchers)
217
- emit('store-add', {
218
- [k]: v,
219
- } as unknown as Partial<T>)
220
- size.set(signals.size)
221
- }
222
- }
223
- case 'get':
224
- return (): T => {
225
- subscribe(watchers)
226
- return current()
227
- }
228
- case 'remove':
229
- return <K extends keyof T>(k: K): void => {
230
- if (signals.has(k)) {
231
- removeProperty(k)
232
- notify(watchers)
233
- emit('store-remove', { [k]: UNSET } as Partial<T>)
234
- size.set(signals.size)
235
- }
236
- }
237
- case 'set':
238
- return (v: T): void => {
239
- if (reconcile(current(), v)) {
240
- notify(watchers)
241
- if (UNSET === v) watchers.clear()
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
+ }
242
440
  }
243
- }
244
- case 'update':
245
- return (fn: (v: T) => T): void => {
246
- const oldValue = current()
247
- const newValue = fn(oldValue)
248
- if (reconcile(oldValue, newValue)) {
249
- notify(watchers)
250
- if (UNSET === newValue) watchers.clear()
441
+ : function* () {
442
+ for (const [key, signal] of signals)
443
+ yield [key, signal]
251
444
  }
252
- }
253
- case 'addEventListener':
254
- return eventTarget.addEventListener.bind(eventTarget)
255
- case 'removeEventListener':
256
- return eventTarget.removeEventListener.bind(eventTarget)
257
- case 'dispatchEvent':
258
- return eventTarget.dispatchEvent.bind(eventTarget)
259
- case 'size':
260
- return size
261
- }
445
+ if (isSymbol(prop)) return undefined
262
446
 
263
- // Handle symbol properties
264
- if (prop === Symbol.toStringTag) return TYPE_STORE
265
- if (prop === Symbol.iterator) {
266
- return function* () {
267
- for (const [key, signal] of signals) {
268
- yield [key, signal]
269
- }
270
- }
447
+ // Methods and Properties
448
+ if (prop in store) return store[prop]
449
+ if (prop === 'length' && isArrayLike) {
450
+ subscribe(watchers)
451
+ return size.get()
271
452
  }
272
453
 
273
- // Handle data properties - return signals
274
- return signals.get(String(prop))
454
+ // Signals
455
+ return signals.get(prop as Extract<keyof T, string>)
275
456
  },
276
457
  has(_target, prop) {
277
- const key = String(prop)
458
+ const stringProp = String(prop)
278
459
  return (
279
- signals.has(key) ||
280
- storeProps.includes(key) ||
460
+ (stringProp &&
461
+ signals.has(stringProp as Extract<keyof T, string>)) ||
462
+ Object.keys(store).includes(stringProp) ||
281
463
  prop === Symbol.toStringTag ||
282
- prop === Symbol.iterator
464
+ prop === Symbol.iterator ||
465
+ prop === Symbol.isConcatSpreadable ||
466
+ (prop === 'length' && isArrayLike)
283
467
  )
284
468
  },
285
469
  ownKeys() {
286
- return Array.from(signals.keys()).map(key => String(key))
470
+ return isArrayLike
471
+ ? getSortedIndexes()
472
+ .map(key => String(key))
473
+ .concat(['length'])
474
+ : Array.from(signals.keys()).map(key => String(key))
287
475
  },
288
476
  getOwnPropertyDescriptor(_target, prop) {
289
- const signal = signals.get(String(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])
498
+
499
+ const signal = signals.get(prop as Extract<keyof T, string>)
290
500
  return signal
291
501
  ? {
292
502
  enumerable: true,
@@ -312,13 +522,4 @@ const isStore = <T extends UnknownRecordOrArray>(
312
522
 
313
523
  /* === Exports === */
314
524
 
315
- export {
316
- TYPE_STORE,
317
- isStore,
318
- store,
319
- type Store,
320
- type StoreAddEvent,
321
- type StoreChangeEvent,
322
- type StoreRemoveEvent,
323
- type StoreEventMap,
324
- }
525
+ export { TYPE_STORE, isStore, createStore, type Store, type StoreChanges }