@zeix/cause-effect 0.15.1 → 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, type UnknownRecordOrArray } 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,76 +21,133 @@ 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, validArrayIndexes } 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 UnknownRecordOrArray> extends CustomEvent {
21
- type: 'store-add'
22
- detail: Partial<T>
23
- }
24
-
25
- interface StoreChangeEvent<T extends UnknownRecordOrArray> extends CustomEvent {
26
- type: 'store-change'
27
- detail: Partial<T>
28
- }
29
-
30
- interface StoreRemoveEvent<T extends UnknownRecordOrArray> extends CustomEvent {
31
- type: 'store-remove'
32
- detail: Partial<T>
33
- }
38
+ type ArrayItem<T> = T extends readonly (infer U extends {})[] ? U : never
34
39
 
35
- type StoreEventMap<T extends UnknownRecordOrArray> = {
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 UnknownRecordOrArray> 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 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
- }
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>
80
+ ? Store<T[K]>
81
+ : State<T[K]>
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'
80
151
 
81
152
  /* === Functions === */
82
153
 
@@ -92,87 +163,165 @@ type Store<T extends UnknownRecordOrArray = UnknownRecord> = {
92
163
  * @param {T} initialValue - initial object or array value of the store
93
164
  * @returns {Store<T>} - new store with reactive properties that preserves the original type T
94
165
  */
95
- const store = <T extends UnknownRecordOrArray>(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<keyof T, Store<T[keyof T]> | State<T[keyof T]>> =
99
- new Map()
100
- const cleanups = new Map<keyof T, 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)
101
176
 
102
177
  // Internal state
103
178
  const size = state(0)
104
179
 
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> = {}
180
+ // Get current record
181
+ const current = () => {
182
+ const record: Record<string, unknown> = {}
114
183
  for (const [key, signal] of signals) {
115
184
  record[key] = signal.get()
116
185
  }
117
- return record as T
186
+ return record
118
187
  }
119
188
 
120
189
  // Emit event
121
- const emit = (type: keyof StoreEventMap<T>, detail: Partial<T>) =>
190
+ const emit = <R>(type: keyof StoreEventMap<T>, detail: R) =>
122
191
  eventTarget.dispatchEvent(new CustomEvent(type, { detail }))
123
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
+
124
216
  // 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]>)
217
+ const addProperty = <K extends Extract<keyof T, string>>(
218
+ key: K,
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
232
+ signals.set(key, signal)
129
233
  const cleanup = effect(() => {
130
234
  const currentValue = signal.get()
131
235
  if (currentValue != null)
132
- emit('store-change', { [key]: currentValue } as Partial<T>)
236
+ emit(STORE_EVENT_CHANGE, {
237
+ [key]: currentValue,
238
+ } as unknown as Partial<T>)
133
239
  })
134
- cleanups.set(stringKey, cleanup)
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
135
250
  }
136
251
 
137
252
  // 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)
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
144
272
  }
145
273
 
146
274
  // Reconcile data and dispatch events
147
- const reconcile = (oldValue: T, newValue: T): boolean => {
148
- 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
+ )
149
284
 
150
285
  batch(() => {
286
+ // Additions
151
287
  if (Object.keys(changes.add).length) {
152
288
  for (const key in changes.add) {
153
- const value = changes.add[key]
154
- if (value != null) addProperty(key, value as T[keyof T])
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>)
155
303
  }
156
- emit('store-add', changes.add)
157
304
  }
305
+
306
+ // Changes
158
307
  if (Object.keys(changes.change).length) {
159
308
  for (const key in changes.change) {
160
- const signal = signals.get(key)
161
309
  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)
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))
168
316
  }
169
- emit('store-change', changes.change)
317
+ emit(STORE_EVENT_CHANGE, changes.change as Partial<T>)
170
318
  }
319
+
320
+ // Removals
171
321
  if (Object.keys(changes.remove).length) {
172
- for (const key in changes.remove) {
173
- removeProperty(key)
174
- }
175
- 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>)
176
325
  }
177
326
 
178
327
  size.set(signals.size)
@@ -181,112 +330,185 @@ const store = <T extends UnknownRecordOrArray>(initialValue: T): Store<T> => {
181
330
  return changes.changed
182
331
  }
183
332
 
184
- // Initialize data
185
- reconcile({} as T, initialValue)
333
+ // Initialize data - convert arrays to records for internal storage
334
+ reconcile({} as T, initialValue, true)
186
335
 
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
- ]
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
+ }
206
436
 
207
437
  // Return proxy directly with integrated signal methods
208
438
  return new Proxy({} as Store<T>, {
209
439
  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()
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
+ }
242
453
  }
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()
454
+ : function* () {
455
+ for (const [key, signal] of signals)
456
+ yield [key, signal]
251
457
  }
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
- }
458
+ if (isSymbol(prop)) return undefined
262
459
 
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
- }
460
+ // Methods and Properties
461
+ if (prop in s) return s[prop]
462
+ if (prop === 'length' && isArrayLike) {
463
+ subscribe(watchers)
464
+ return size.get()
271
465
  }
272
466
 
273
- // Handle data properties - return signals
274
- return signals.get(String(prop))
467
+ // Signals
468
+ return signals.get(prop as Extract<keyof T, string>)
275
469
  },
276
470
  has(_target, prop) {
277
- const key = String(prop)
471
+ const stringProp = String(prop)
278
472
  return (
279
- signals.has(key) ||
280
- storeProps.includes(key) ||
473
+ (stringProp &&
474
+ signals.has(stringProp as Extract<keyof T, string>)) ||
475
+ Object.keys(s).includes(stringProp) ||
281
476
  prop === Symbol.toStringTag ||
282
- prop === Symbol.iterator
477
+ prop === Symbol.iterator ||
478
+ prop === Symbol.isConcatSpreadable ||
479
+ (prop === 'length' && isArrayLike)
283
480
  )
284
481
  },
285
482
  ownKeys() {
286
- return Array.from(signals.keys()).map(key => String(key))
483
+ return isArrayLike
484
+ ? getSortedIndexes()
485
+ .map(key => String(key))
486
+ .concat(['length'])
487
+ : Array.from(signals.keys()).map(key => String(key))
287
488
  },
288
489
  getOwnPropertyDescriptor(_target, prop) {
289
- 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>)
290
512
  return signal
291
513
  ? {
292
514
  enumerable: true,
@@ -320,5 +542,6 @@ export {
320
542
  type StoreAddEvent,
321
543
  type StoreChangeEvent,
322
544
  type StoreRemoveEvent,
545
+ type StoreSortEvent,
323
546
  type StoreEventMap,
324
547
  }