@zeix/cause-effect 0.15.0 → 0.15.2

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
@@ -1,5 +1,19 @@
1
- import { diff, type UnknownRecord } from './diff'
1
+ import { isComputed } from './computed'
2
+ import {
3
+ type ArrayToRecord,
4
+ diff,
5
+ type UnknownArray,
6
+ type UnknownRecord,
7
+ type UnknownRecordOrArray,
8
+ } from './diff'
2
9
  import { effect } from './effect'
10
+ import {
11
+ InvalidSignalValueError,
12
+ NullishSignalValueError,
13
+ StoreKeyExistsError,
14
+ StoreKeyRangeError,
15
+ StoreKeyReadonlyError,
16
+ } from './errors'
3
17
  import {
4
18
  batch,
5
19
  type Cleanup,
@@ -7,169 +21,307 @@ import {
7
21
  subscribe,
8
22
  type Watcher,
9
23
  } from './scheduler'
10
- import { type Signal, toMutableSignal, UNSET } from './signal'
11
- import { type State, state } from './state'
12
- import { hasMethod, isObjectOfType } from './util'
13
-
14
- /* === Constants === */
15
-
16
- const TYPE_STORE = 'Store'
24
+ import { isMutableSignal, type Signal } from './signal'
25
+ import { isState, type State, state } from './state'
26
+ import {
27
+ isFunction,
28
+ isObjectOfType,
29
+ isRecord,
30
+ isSymbol,
31
+ recordToArray,
32
+ UNSET,
33
+ valueString,
34
+ } from './util'
17
35
 
18
36
  /* === Types === */
19
37
 
20
- interface StoreAddEvent<T extends UnknownRecord> extends CustomEvent {
21
- type: 'store-add'
22
- detail: Partial<T>
23
- }
38
+ type ArrayItem<T> = T extends readonly (infer U extends {})[] ? U : never
24
39
 
25
- interface StoreChangeEvent<T extends UnknownRecord> extends CustomEvent {
26
- type: 'store-change'
27
- detail: Partial<T>
28
- }
29
-
30
- interface StoreRemoveEvent<T extends UnknownRecord> extends CustomEvent {
31
- type: 'store-remove'
32
- detail: Partial<T>
33
- }
34
-
35
- type StoreEventMap<T extends UnknownRecord> = {
40
+ type StoreEventMap<T extends UnknownRecord | UnknownArray> = {
36
41
  'store-add': StoreAddEvent<T>
37
42
  'store-change': StoreChangeEvent<T>
38
43
  'store-remove': StoreRemoveEvent<T>
44
+ 'store-sort': StoreSortEvent
39
45
  }
40
46
 
41
- interface StoreEventTarget<T extends UnknownRecord> extends EventTarget {
47
+ interface StoreEventTarget<T extends UnknownRecord | UnknownArray>
48
+ extends EventTarget {
42
49
  addEventListener<K extends keyof StoreEventMap<T>>(
43
50
  type: K,
44
51
  listener: (event: StoreEventMap<T>[K]) => void,
45
52
  options?: boolean | AddEventListenerOptions,
46
53
  ): void
47
- addEventListener(
48
- type: string,
49
- listener: EventListenerOrEventListenerObject,
50
- options?: boolean | AddEventListenerOptions,
51
- ): void
52
54
 
53
55
  removeEventListener<K extends keyof StoreEventMap<T>>(
54
56
  type: K,
55
57
  listener: (event: StoreEventMap<T>[K]) => void,
56
58
  options?: boolean | EventListenerOptions,
57
59
  ): void
58
- removeEventListener(
59
- type: string,
60
- listener: EventListenerOrEventListenerObject,
61
- options?: boolean | EventListenerOptions,
62
- ): void
63
60
 
64
61
  dispatchEvent(event: Event): boolean
65
62
  }
66
63
 
67
- type Store<T extends UnknownRecord = UnknownRecord> = {
68
- [K in keyof T & string]: T[K] extends UnknownRecord
64
+ interface BaseStore<T extends UnknownRecord | UnknownArray>
65
+ extends StoreEventTarget<T> {
66
+ readonly [Symbol.toStringTag]: 'Store'
67
+ get(): T
68
+ set(value: T): void
69
+ update(fn: (value: T) => T): void
70
+ sort<
71
+ U = T extends UnknownArray ? ArrayItem<T> : T[Extract<keyof T, string>],
72
+ >(
73
+ compareFn?: (a: U, b: U) => number,
74
+ ): void
75
+ readonly size: State<number>
76
+ }
77
+
78
+ type RecordStore<T extends UnknownRecord> = BaseStore<T> & {
79
+ [K in keyof T]: T[K] extends readonly unknown[] | Record<string, unknown>
69
80
  ? Store<T[K]>
70
81
  : State<T[K]>
71
- } & StoreEventTarget<T> & {
72
- [Symbol.toStringTag]: 'Store'
73
- [Symbol.iterator](): IterableIterator<[string, Signal<T[keyof T]>]>
74
-
75
- // Signal methods
76
- add<K extends keyof T & string>(key: K, value: T[K]): void
77
- get(): T
78
- remove<K extends keyof T & string>(key: K): void
79
- set(value: T): void
80
- update(updater: (value: T) => T): void
81
-
82
- // Interals signals
83
- size: State<number>
84
- }
82
+ } & {
83
+ add<K extends Extract<keyof T, string>>(key: K, value: T[K]): void
84
+ remove<K extends Extract<keyof T, string>>(key: K): void
85
+ [Symbol.iterator](): IterableIterator<
86
+ [
87
+ Extract<keyof T, string>,
88
+ T[Extract<keyof T, string>] extends
89
+ | readonly unknown[]
90
+ | Record<string, unknown>
91
+ ? Store<T[Extract<keyof T, string>]>
92
+ : State<T[Extract<keyof T, string>]>,
93
+ ]
94
+ >
95
+ }
96
+
97
+ type ArrayStore<T extends UnknownArray> = BaseStore<T> & {
98
+ readonly length: number
99
+ [n: number]: ArrayItem<T> extends
100
+ | readonly unknown[]
101
+ | Record<string, unknown>
102
+ ? Store<ArrayItem<T>>
103
+ : State<ArrayItem<T>>
104
+ add(value: ArrayItem<T>): void
105
+ remove(index: number): void
106
+ [Symbol.iterator](): IterableIterator<
107
+ ArrayItem<T> extends readonly unknown[] | Record<string, unknown>
108
+ ? Store<ArrayItem<T>>
109
+ : State<ArrayItem<T>>
110
+ >
111
+ readonly [Symbol.isConcatSpreadable]: boolean
112
+ }
113
+
114
+ interface StoreAddEvent<T extends UnknownRecord | UnknownArray>
115
+ extends CustomEvent {
116
+ type: 'store-add'
117
+ detail: Partial<T>
118
+ }
119
+
120
+ interface StoreChangeEvent<T extends UnknownRecord | UnknownArray>
121
+ extends CustomEvent {
122
+ type: 'store-change'
123
+ detail: Partial<T>
124
+ }
125
+
126
+ interface StoreRemoveEvent<T extends UnknownRecord | UnknownArray>
127
+ extends CustomEvent {
128
+ type: 'store-remove'
129
+ detail: Partial<T>
130
+ }
131
+
132
+ interface StoreSortEvent extends CustomEvent {
133
+ type: 'store-sort'
134
+ detail: string[]
135
+ }
136
+
137
+ type Store<T> = T extends UnknownRecord
138
+ ? RecordStore<T>
139
+ : T extends UnknownArray
140
+ ? ArrayStore<T>
141
+ : never
142
+
143
+ /* === Constants === */
144
+
145
+ const TYPE_STORE = 'Store'
146
+
147
+ const STORE_EVENT_ADD = 'store-add'
148
+ const STORE_EVENT_CHANGE = 'store-change'
149
+ const STORE_EVENT_REMOVE = 'store-remove'
150
+ const STORE_EVENT_SORT = 'store-sort'
85
151
 
86
152
  /* === Functions === */
87
153
 
88
154
  /**
89
155
  * Create a new store with deeply nested reactive properties
90
156
  *
157
+ * Supports both objects and arrays as initial values. Arrays are converted
158
+ * to records internally for storage but maintain their array type through
159
+ * the .get() method, which automatically converts objects with consecutive
160
+ * numeric keys back to arrays.
161
+ *
91
162
  * @since 0.15.0
92
- * @param {T} initialValue - initial object value of the store
93
- * @returns {Store<T>} - new store with reactive properties
163
+ * @param {T} initialValue - initial object or array value of the store
164
+ * @returns {Store<T>} - new store with reactive properties that preserves the original type T
94
165
  */
95
- const store = <T extends UnknownRecord>(initialValue: T): Store<T> => {
96
- const watchers: Set<Watcher> = new Set()
166
+ const store = <T extends UnknownRecord | UnknownArray>(
167
+ initialValue: T,
168
+ ): Store<T> => {
169
+ const watchers = new Set<Watcher>()
97
170
  const eventTarget = new EventTarget()
98
- const signals: Map<
99
- keyof T & string,
100
- Store<T[keyof T & string]> | State<T[keyof T & string]>
101
- > = new Map()
102
- const cleanups = new Map<keyof T & string, Cleanup>()
171
+ const signals = new Map<string, Signal<T[Extract<keyof T, string>] & {}>>()
172
+ const cleanups = new Map<string, Cleanup>()
173
+
174
+ // Determine if this is an array-like store at creation time
175
+ const isArrayLike = Array.isArray(initialValue)
103
176
 
104
177
  // Internal state
105
178
  const size = state(0)
106
179
 
107
180
  // Get current record
108
181
  const current = () => {
109
- const record: Partial<T> = {}
110
- for (const [key, value] of signals) {
111
- record[key] = value.get()
182
+ const record: Record<string, unknown> = {}
183
+ for (const [key, signal] of signals) {
184
+ record[key] = signal.get()
112
185
  }
113
- return record as T
186
+ return record
114
187
  }
115
188
 
116
189
  // Emit event
117
- const emit = (type: keyof StoreEventMap<T>, detail: Partial<T>) =>
190
+ const emit = <R>(type: keyof StoreEventMap<T>, detail: R) =>
118
191
  eventTarget.dispatchEvent(new CustomEvent(type, { detail }))
119
192
 
193
+ // Get sorted indexes
194
+ const getSortedIndexes = () =>
195
+ Array.from(signals.keys())
196
+ .map(k => Number(k))
197
+ .filter(n => Number.isInteger(n))
198
+ .sort((a, b) => a - b)
199
+
200
+ // Validate input
201
+ const isValidValue = <T>(
202
+ key: string,
203
+ value: T,
204
+ ): value is NonNullable<T> => {
205
+ if (value == null)
206
+ throw new NullishSignalValueError(`store for key "${key}"`)
207
+ if (value === UNSET) return true
208
+ if (isSymbol(value) || isFunction(value) || isComputed(value))
209
+ throw new InvalidSignalValueError(
210
+ `store for key "${key}"`,
211
+ valueString(value),
212
+ )
213
+ return true
214
+ }
215
+
120
216
  // Add nested signal and effect
121
- const addSignalAndEffect = <K extends keyof T & string>(
217
+ const addProperty = <K extends Extract<keyof T, string>>(
122
218
  key: K,
123
- value: T[K],
124
- ) => {
125
- const signal = toMutableSignal<T[keyof T & string]>(value)
219
+ value: T[K] | ArrayItem<T>,
220
+ single = false,
221
+ ): boolean => {
222
+ if (!isValidValue(key, value)) return false
223
+ const signal =
224
+ isState(value) || isStore(value)
225
+ ? value
226
+ : isRecord(value)
227
+ ? store(value)
228
+ : Array.isArray(value)
229
+ ? store(value)
230
+ : state(value)
231
+ // @ts-expect-error non-matching signal types
126
232
  signals.set(key, signal)
127
233
  const cleanup = effect(() => {
128
- const value = signal.get()
129
- if (value != null)
130
- emit('store-change', { [key]: value } as unknown as Partial<T>)
234
+ const currentValue = signal.get()
235
+ if (currentValue != null)
236
+ emit(STORE_EVENT_CHANGE, {
237
+ [key]: currentValue,
238
+ } as unknown as Partial<T>)
131
239
  })
132
240
  cleanups.set(key, cleanup)
241
+
242
+ if (single) {
243
+ size.set(signals.size)
244
+ notify(watchers)
245
+ emit(STORE_EVENT_ADD, {
246
+ [key]: value,
247
+ } as unknown as Partial<T>)
248
+ }
249
+ return true
133
250
  }
134
251
 
135
252
  // Remove nested signal and effect
136
- const removeSignalAndEffect = <K extends keyof T & string>(key: K) => {
137
- signals.delete(key)
138
- const cleanup = cleanups.get(key)
139
- if (cleanup) cleanup()
140
- cleanups.delete(key)
253
+ const removeProperty = <K extends Extract<keyof T, string>>(
254
+ key: K,
255
+ single = false,
256
+ ) => {
257
+ const ok = signals.delete(key)
258
+ if (ok) {
259
+ const cleanup = cleanups.get(key)
260
+ if (cleanup) cleanup()
261
+ cleanups.delete(key)
262
+ }
263
+
264
+ if (single) {
265
+ size.set(signals.size)
266
+ notify(watchers)
267
+ emit(STORE_EVENT_REMOVE, {
268
+ [key]: UNSET,
269
+ } as unknown as Partial<T>)
270
+ }
271
+ return ok
141
272
  }
142
273
 
143
274
  // Reconcile data and dispatch events
144
- const reconcile = (oldValue: T, newValue: T): boolean => {
145
- const changes = diff(oldValue, newValue)
275
+ const reconcile = (
276
+ oldValue: T,
277
+ newValue: T,
278
+ initialRun?: boolean,
279
+ ): boolean => {
280
+ const changes = diff(
281
+ oldValue as T extends UnknownArray ? ArrayToRecord<T> : T,
282
+ newValue as T extends UnknownArray ? ArrayToRecord<T> : T,
283
+ )
146
284
 
147
285
  batch(() => {
286
+ // Additions
148
287
  if (Object.keys(changes.add).length) {
149
288
  for (const key in changes.add) {
150
- const value = changes.add[key]
151
- if (value != null) addSignalAndEffect(key, value)
289
+ const value = changes.add[key] ?? UNSET
290
+ addProperty(
291
+ key as Extract<keyof T, string>,
292
+ value as T[Extract<keyof T, string>] & {},
293
+ )
294
+ }
295
+
296
+ // Queue initial additions event to allow listeners to be added first
297
+ if (initialRun) {
298
+ setTimeout(() => {
299
+ emit(STORE_EVENT_ADD, changes.add as Partial<T>)
300
+ }, 0)
301
+ } else {
302
+ emit<Partial<T>>(STORE_EVENT_ADD, changes.add as Partial<T>)
152
303
  }
153
- emit('store-add', changes.add)
154
304
  }
305
+
306
+ // Changes
155
307
  if (Object.keys(changes.change).length) {
156
308
  for (const key in changes.change) {
157
- const signal = signals.get(key as keyof T & string)
158
309
  const value = changes.change[key]
159
- if (
160
- signal &&
161
- value != null &&
162
- hasMethod<Signal<T[keyof T & string]>>(signal, 'set')
163
- )
164
- signal.set(value)
310
+ if (!isValidValue(key, value)) continue
311
+ const signal = signals.get(key as Extract<keyof T, string>)
312
+ if (isMutableSignal(signal))
313
+ signal.set(value as T[Extract<keyof T, string>] & {})
314
+ else
315
+ throw new StoreKeyReadonlyError(key, valueString(value))
165
316
  }
166
- emit('store-change', changes.change)
317
+ emit(STORE_EVENT_CHANGE, changes.change as Partial<T>)
167
318
  }
319
+
320
+ // Removals
168
321
  if (Object.keys(changes.remove).length) {
169
- for (const key in changes.remove) {
170
- removeSignalAndEffect(key)
171
- }
172
- emit('store-remove', changes.remove)
322
+ for (const key in changes.remove)
323
+ removeProperty(key as Extract<keyof T, string>)
324
+ emit(STORE_EVENT_REMOVE, changes.remove as Partial<T>)
173
325
  }
174
326
 
175
327
  size.set(signals.size)
@@ -178,117 +330,185 @@ const store = <T extends UnknownRecord>(initialValue: T): Store<T> => {
178
330
  return changes.changed
179
331
  }
180
332
 
181
- // Initialize data
182
- reconcile({} as T, initialValue)
183
-
184
- // Queue initial additions event to allow listeners to be added first
185
- setTimeout(() => {
186
- const initialAdditionsEvent = new CustomEvent('store-add', {
187
- detail: initialValue as Partial<T>,
188
- }) as StoreAddEvent<T>
189
- eventTarget.dispatchEvent(initialAdditionsEvent)
190
- }, 0)
191
-
192
- const storeProps = [
193
- 'add',
194
- 'get',
195
- 'remove',
196
- 'set',
197
- 'update',
198
- 'addEventListener',
199
- 'removeEventListener',
200
- 'dispatchEvent',
201
- 'size',
202
- ]
333
+ // Initialize data - convert arrays to records for internal storage
334
+ reconcile({} as T, initialValue, true)
335
+
336
+ // Methods and Properties
337
+ const s: Record<string, unknown> = {
338
+ add: isArrayLike
339
+ ? (v: ArrayItem<T>): void => {
340
+ const nextIndex = signals.size
341
+ const key = String(nextIndex) as Extract<keyof T, string>
342
+ addProperty(key, v, true)
343
+ }
344
+ : <K extends Extract<keyof T, string>>(k: K, v: T[K]): void => {
345
+ if (!signals.has(k)) addProperty(k, v, true)
346
+ else throw new StoreKeyExistsError(k, valueString(v))
347
+ },
348
+ get: (): T => {
349
+ subscribe(watchers)
350
+ return recordToArray(current()) as T
351
+ },
352
+ remove: isArrayLike
353
+ ? (index: number): void => {
354
+ const currentArray = recordToArray(current()) as T
355
+ const currentLength = signals.size
356
+ if (
357
+ !Array.isArray(currentArray) ||
358
+ index <= -currentLength ||
359
+ index >= currentLength
360
+ )
361
+ throw new StoreKeyRangeError(index)
362
+ const newArray = [...currentArray]
363
+ newArray.splice(index, 1)
364
+
365
+ if (reconcile(currentArray, newArray as unknown as T))
366
+ notify(watchers)
367
+ }
368
+ : <K extends Extract<keyof T, string>>(k: K): void => {
369
+ if (signals.has(k)) removeProperty(k, true)
370
+ },
371
+ set: (v: T): void => {
372
+ if (reconcile(current() as T, v)) {
373
+ notify(watchers)
374
+ if (UNSET === v) watchers.clear()
375
+ }
376
+ },
377
+ update: (fn: (v: T) => T): void => {
378
+ const oldValue = current()
379
+ const newValue = fn(recordToArray(oldValue) as T)
380
+ if (reconcile(oldValue as T, newValue)) {
381
+ notify(watchers)
382
+ if (UNSET === newValue) watchers.clear()
383
+ }
384
+ },
385
+ sort: (
386
+ compareFn?: <
387
+ U = T extends UnknownArray
388
+ ? ArrayItem<T>
389
+ : T[Extract<keyof T, string>],
390
+ >(
391
+ a: U,
392
+ b: U,
393
+ ) => number,
394
+ ): void => {
395
+ // Get all entries as [key, value] pairs
396
+ const entries = Array.from(signals.entries())
397
+ .map(
398
+ ([key, signal]) =>
399
+ [key, signal.get()] as [
400
+ string,
401
+ T[Extract<keyof T, string>],
402
+ ],
403
+ )
404
+ .sort(
405
+ compareFn
406
+ ? (a, b) => compareFn(a[1], b[1])
407
+ : (a, b) => String(a[1]).localeCompare(String(b[1])),
408
+ )
409
+
410
+ // Create array of original keys in their new sorted order
411
+ const newOrder: string[] = entries.map(([key]) => String(key))
412
+ const newSignals = new Map<
413
+ string,
414
+ Signal<T[Extract<keyof T, string>] & {}>
415
+ >()
416
+
417
+ entries.forEach(([key], newIndex) => {
418
+ const oldKey = String(key)
419
+ const newKey = isArrayLike ? String(newIndex) : String(key)
420
+
421
+ const signal = signals.get(oldKey)
422
+ if (signal) newSignals.set(newKey, signal)
423
+ })
424
+
425
+ // Replace signals map
426
+ signals.clear()
427
+ newSignals.forEach((signal, key) => signals.set(key, signal))
428
+ notify(watchers)
429
+ emit(STORE_EVENT_SORT, newOrder)
430
+ },
431
+ addEventListener: eventTarget.addEventListener.bind(eventTarget),
432
+ removeEventListener: eventTarget.removeEventListener.bind(eventTarget),
433
+ dispatchEvent: eventTarget.dispatchEvent.bind(eventTarget),
434
+ size,
435
+ }
203
436
 
204
437
  // Return proxy directly with integrated signal methods
205
438
  return new Proxy({} as Store<T>, {
206
439
  get(_target, prop) {
207
- const key = String(prop)
208
-
209
- // Handle signal methods and size property
210
- switch (prop) {
211
- case 'add':
212
- return <K extends keyof T & string>(
213
- k: K,
214
- v: T[K],
215
- ): void => {
216
- if (!signals.has(k)) {
217
- addSignalAndEffect(k, v)
218
- notify(watchers)
219
- emit('store-add', {
220
- [k]: v,
221
- } as unknown as Partial<T>)
222
- size.set(signals.size)
223
- }
224
- }
225
- case 'get':
226
- return (): T => {
227
- subscribe(watchers)
228
- return current()
229
- }
230
- case 'remove':
231
- return <K extends keyof T & string>(k: K): void => {
232
- if (signals.has(k)) {
233
- removeSignalAndEffect(k)
234
- notify(watchers)
235
- emit('store-remove', { [k]: UNSET } as Partial<T>)
236
- size.set(signals.size)
237
- }
238
- }
239
- case 'set':
240
- return (v: T): void => {
241
- if (reconcile(current(), v)) {
242
- notify(watchers)
243
- if (UNSET === v) watchers.clear()
440
+ // Symbols
441
+ if (prop === Symbol.toStringTag) return TYPE_STORE
442
+ if (prop === Symbol.isConcatSpreadable) return isArrayLike
443
+ if (prop === Symbol.iterator)
444
+ return isArrayLike
445
+ ? function* () {
446
+ const indexes = getSortedIndexes()
447
+ for (const index of indexes) {
448
+ const signal = signals.get(
449
+ String(index) as Extract<keyof T, string>,
450
+ )
451
+ if (signal) yield signal
452
+ }
244
453
  }
245
- }
246
- case 'update':
247
- return (fn: (v: T) => T): void => {
248
- const oldValue = current()
249
- const newValue = fn(oldValue)
250
- if (reconcile(oldValue, newValue)) {
251
- notify(watchers)
252
- if (UNSET === newValue) watchers.clear()
454
+ : function* () {
455
+ for (const [key, signal] of signals)
456
+ yield [key, signal]
253
457
  }
254
- }
255
- case 'addEventListener':
256
- return eventTarget.addEventListener.bind(eventTarget)
257
- case 'removeEventListener':
258
- return eventTarget.removeEventListener.bind(eventTarget)
259
- case 'dispatchEvent':
260
- return eventTarget.dispatchEvent.bind(eventTarget)
261
- case 'size':
262
- return size
263
- }
458
+ if (isSymbol(prop)) return undefined
264
459
 
265
- // Handle symbol properties
266
- if (prop === Symbol.toStringTag) return TYPE_STORE
267
- if (prop === Symbol.iterator) {
268
- return function* () {
269
- for (const [key, signal] of signals) {
270
- yield [key, signal as Signal<T[keyof T]>]
271
- }
272
- }
460
+ // Methods and Properties
461
+ if (prop in s) return s[prop]
462
+ if (prop === 'length' && isArrayLike) {
463
+ subscribe(watchers)
464
+ return size.get()
273
465
  }
274
466
 
275
- // Handle data properties - return signals
276
- return signals.get(key)
467
+ // Signals
468
+ return signals.get(prop as Extract<keyof T, string>)
277
469
  },
278
470
  has(_target, prop) {
279
- const key = String(prop)
471
+ const stringProp = String(prop)
280
472
  return (
281
- signals.has(key) ||
282
- storeProps.includes(key) ||
473
+ (stringProp &&
474
+ signals.has(stringProp as Extract<keyof T, string>)) ||
475
+ Object.keys(s).includes(stringProp) ||
283
476
  prop === Symbol.toStringTag ||
284
- prop === Symbol.iterator
477
+ prop === Symbol.iterator ||
478
+ prop === Symbol.isConcatSpreadable ||
479
+ (prop === 'length' && isArrayLike)
285
480
  )
286
481
  },
287
482
  ownKeys() {
288
- return Array.from(signals.keys())
483
+ return isArrayLike
484
+ ? getSortedIndexes()
485
+ .map(key => String(key))
486
+ .concat(['length'])
487
+ : Array.from(signals.keys()).map(key => String(key))
289
488
  },
290
489
  getOwnPropertyDescriptor(_target, prop) {
291
- const signal = signals.get(String(prop))
490
+ const nonEnumerable = <T>(value: T) => ({
491
+ enumerable: false,
492
+ configurable: true,
493
+ writable: false,
494
+ value,
495
+ })
496
+
497
+ if (prop === 'length' && isArrayLike)
498
+ return {
499
+ enumerable: true,
500
+ configurable: true,
501
+ writable: false,
502
+ value: size.get(),
503
+ }
504
+ if (prop === Symbol.isConcatSpreadable)
505
+ return nonEnumerable(isArrayLike)
506
+ if (prop === Symbol.toStringTag) return nonEnumerable(TYPE_STORE)
507
+ if (isSymbol(prop)) return undefined
508
+
509
+ if (Object.keys(s).includes(prop)) return nonEnumerable(s[prop])
510
+
511
+ const signal = signals.get(prop as Extract<keyof T, string>)
292
512
  return signal
293
513
  ? {
294
514
  enumerable: true,
@@ -308,8 +528,9 @@ const store = <T extends UnknownRecord>(initialValue: T): Store<T> => {
308
528
  * @param {unknown} value - value to check
309
529
  * @returns {boolean} - true if the value is a Store instance, false otherwise
310
530
  */
311
- const isStore = <T extends UnknownRecord>(value: unknown): value is Store<T> =>
312
- isObjectOfType(value, TYPE_STORE)
531
+ const isStore = <T extends UnknownRecordOrArray>(
532
+ value: unknown,
533
+ ): value is Store<T> => isObjectOfType(value, TYPE_STORE)
313
534
 
314
535
  /* === Exports === */
315
536
 
@@ -321,5 +542,6 @@ export {
321
542
  type StoreAddEvent,
322
543
  type StoreChangeEvent,
323
544
  type StoreRemoveEvent,
545
+ type StoreSortEvent,
324
546
  type StoreEventMap,
325
547
  }