@zeix/cause-effect 0.15.0 → 0.15.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
@@ -1,4 +1,4 @@
1
- import { diff, type UnknownRecord } from './diff'
1
+ import { diff, type UnknownRecord, type UnknownRecordOrArray } from './diff'
2
2
  import { effect } from './effect'
3
3
  import {
4
4
  batch,
@@ -9,7 +9,7 @@ import {
9
9
  } from './scheduler'
10
10
  import { type Signal, toMutableSignal, UNSET } from './signal'
11
11
  import { type State, state } from './state'
12
- import { hasMethod, isObjectOfType } from './util'
12
+ import { hasMethod, isObjectOfType, validArrayIndexes } from './util'
13
13
 
14
14
  /* === Constants === */
15
15
 
@@ -17,28 +17,28 @@ const TYPE_STORE = 'Store'
17
17
 
18
18
  /* === Types === */
19
19
 
20
- interface StoreAddEvent<T extends UnknownRecord> extends CustomEvent {
20
+ interface StoreAddEvent<T extends UnknownRecordOrArray> extends CustomEvent {
21
21
  type: 'store-add'
22
22
  detail: Partial<T>
23
23
  }
24
24
 
25
- interface StoreChangeEvent<T extends UnknownRecord> extends CustomEvent {
25
+ interface StoreChangeEvent<T extends UnknownRecordOrArray> extends CustomEvent {
26
26
  type: 'store-change'
27
27
  detail: Partial<T>
28
28
  }
29
29
 
30
- interface StoreRemoveEvent<T extends UnknownRecord> extends CustomEvent {
30
+ interface StoreRemoveEvent<T extends UnknownRecordOrArray> extends CustomEvent {
31
31
  type: 'store-remove'
32
32
  detail: Partial<T>
33
33
  }
34
34
 
35
- type StoreEventMap<T extends UnknownRecord> = {
35
+ type StoreEventMap<T extends UnknownRecordOrArray> = {
36
36
  'store-add': StoreAddEvent<T>
37
37
  'store-change': StoreChangeEvent<T>
38
38
  'store-remove': StoreRemoveEvent<T>
39
39
  }
40
40
 
41
- interface StoreEventTarget<T extends UnknownRecord> extends EventTarget {
41
+ interface StoreEventTarget<T extends UnknownRecordOrArray> extends EventTarget {
42
42
  addEventListener<K extends keyof StoreEventMap<T>>(
43
43
  type: K,
44
44
  listener: (event: StoreEventMap<T>[K]) => void,
@@ -64,22 +64,17 @@ interface StoreEventTarget<T extends UnknownRecord> extends EventTarget {
64
64
  dispatchEvent(event: Event): boolean
65
65
  }
66
66
 
67
- type Store<T extends UnknownRecord = UnknownRecord> = {
68
- [K in keyof T & string]: T[K] extends UnknownRecord
69
- ? Store<T[K]>
70
- : State<T[K]>
67
+ type Store<T extends UnknownRecordOrArray = UnknownRecord> = {
68
+ [K in keyof T]: T[K] extends UnknownRecord ? Store<T[K]> : State<T[K]>
71
69
  } & StoreEventTarget<T> & {
72
70
  [Symbol.toStringTag]: 'Store'
73
- [Symbol.iterator](): IterableIterator<[string, Signal<T[keyof T]>]>
71
+ [Symbol.iterator](): IterableIterator<[keyof T, Signal<T[keyof T]>]>
74
72
 
75
- // Signal methods
76
- add<K extends keyof T & string>(key: K, value: T[K]): void
73
+ add<K extends keyof T>(key: K, value: T[K]): void
77
74
  get(): T
78
- remove<K extends keyof T & string>(key: K): void
75
+ remove<K extends keyof T>(key: K): void
79
76
  set(value: T): void
80
77
  update(updater: (value: T) => T): void
81
-
82
- // Interals signals
83
78
  size: State<number>
84
79
  }
85
80
 
@@ -88,27 +83,36 @@ type Store<T extends UnknownRecord = UnknownRecord> = {
88
83
  /**
89
84
  * Create a new store with deeply nested reactive properties
90
85
  *
86
+ * Supports both objects and arrays as initial values. Arrays are converted
87
+ * to records internally for storage but maintain their array type through
88
+ * the .get() method, which automatically converts objects with consecutive
89
+ * numeric keys back to arrays.
90
+ *
91
91
  * @since 0.15.0
92
- * @param {T} initialValue - initial object value of the store
93
- * @returns {Store<T>} - new store with reactive properties
92
+ * @param {T} initialValue - initial object or array value of the store
93
+ * @returns {Store<T>} - new store with reactive properties that preserves the original type T
94
94
  */
95
- const store = <T extends UnknownRecord>(initialValue: T): Store<T> => {
95
+ const store = <T extends UnknownRecordOrArray>(initialValue: T): Store<T> => {
96
96
  const watchers: Set<Watcher> = new Set()
97
97
  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>()
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>()
103
101
 
104
102
  // Internal state
105
103
  const size = state(0)
106
104
 
107
- // Get current record
108
- const current = () => {
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
109
113
  const record: Partial<T> = {}
110
- for (const [key, value] of signals) {
111
- record[key] = value.get()
114
+ for (const [key, signal] of signals) {
115
+ record[key] = signal.get()
112
116
  }
113
117
  return record as T
114
118
  }
@@ -118,26 +122,25 @@ const store = <T extends UnknownRecord>(initialValue: T): Store<T> => {
118
122
  eventTarget.dispatchEvent(new CustomEvent(type, { detail }))
119
123
 
120
124
  // Add nested signal and effect
121
- const addSignalAndEffect = <K extends keyof T & string>(
122
- key: K,
123
- value: T[K],
124
- ) => {
125
- const signal = toMutableSignal<T[keyof T & string]>(value)
126
- signals.set(key, signal)
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]>)
127
129
  const cleanup = effect(() => {
128
- const value = signal.get()
129
- if (value != null)
130
- emit('store-change', { [key]: value } as unknown as Partial<T>)
130
+ const currentValue = signal.get()
131
+ if (currentValue != null)
132
+ emit('store-change', { [key]: currentValue } as Partial<T>)
131
133
  })
132
- cleanups.set(key, cleanup)
134
+ cleanups.set(stringKey, cleanup)
133
135
  }
134
136
 
135
137
  // 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)
138
+ const removeProperty = <K extends keyof T>(key: K) => {
139
+ const stringKey = String(key)
140
+ signals.delete(stringKey)
141
+ const cleanup = cleanups.get(stringKey)
139
142
  if (cleanup) cleanup()
140
- cleanups.delete(key)
143
+ cleanups.delete(stringKey)
141
144
  }
142
145
 
143
146
  // Reconcile data and dispatch events
@@ -148,18 +151,18 @@ const store = <T extends UnknownRecord>(initialValue: T): Store<T> => {
148
151
  if (Object.keys(changes.add).length) {
149
152
  for (const key in changes.add) {
150
153
  const value = changes.add[key]
151
- if (value != null) addSignalAndEffect(key, value)
154
+ if (value != null) addProperty(key, value as T[keyof T])
152
155
  }
153
156
  emit('store-add', changes.add)
154
157
  }
155
158
  if (Object.keys(changes.change).length) {
156
159
  for (const key in changes.change) {
157
- const signal = signals.get(key as keyof T & string)
160
+ const signal = signals.get(key)
158
161
  const value = changes.change[key]
159
162
  if (
160
163
  signal &&
161
164
  value != null &&
162
- hasMethod<Signal<T[keyof T & string]>>(signal, 'set')
165
+ hasMethod<Signal<T[keyof T]>>(signal, 'set')
163
166
  )
164
167
  signal.set(value)
165
168
  }
@@ -167,7 +170,7 @@ const store = <T extends UnknownRecord>(initialValue: T): Store<T> => {
167
170
  }
168
171
  if (Object.keys(changes.remove).length) {
169
172
  for (const key in changes.remove) {
170
- removeSignalAndEffect(key)
173
+ removeProperty(key)
171
174
  }
172
175
  emit('store-remove', changes.remove)
173
176
  }
@@ -184,8 +187,8 @@ const store = <T extends UnknownRecord>(initialValue: T): Store<T> => {
184
187
  // Queue initial additions event to allow listeners to be added first
185
188
  setTimeout(() => {
186
189
  const initialAdditionsEvent = new CustomEvent('store-add', {
187
- detail: initialValue as Partial<T>,
188
- }) as StoreAddEvent<T>
190
+ detail: initialValue,
191
+ })
189
192
  eventTarget.dispatchEvent(initialAdditionsEvent)
190
193
  }, 0)
191
194
 
@@ -204,17 +207,12 @@ const store = <T extends UnknownRecord>(initialValue: T): Store<T> => {
204
207
  // Return proxy directly with integrated signal methods
205
208
  return new Proxy({} as Store<T>, {
206
209
  get(_target, prop) {
207
- const key = String(prop)
208
-
209
210
  // Handle signal methods and size property
210
211
  switch (prop) {
211
212
  case 'add':
212
- return <K extends keyof T & string>(
213
- k: K,
214
- v: T[K],
215
- ): void => {
213
+ return <K extends keyof T>(k: K, v: T[K]): void => {
216
214
  if (!signals.has(k)) {
217
- addSignalAndEffect(k, v)
215
+ addProperty(k, v)
218
216
  notify(watchers)
219
217
  emit('store-add', {
220
218
  [k]: v,
@@ -228,9 +226,9 @@ const store = <T extends UnknownRecord>(initialValue: T): Store<T> => {
228
226
  return current()
229
227
  }
230
228
  case 'remove':
231
- return <K extends keyof T & string>(k: K): void => {
229
+ return <K extends keyof T>(k: K): void => {
232
230
  if (signals.has(k)) {
233
- removeSignalAndEffect(k)
231
+ removeProperty(k)
234
232
  notify(watchers)
235
233
  emit('store-remove', { [k]: UNSET } as Partial<T>)
236
234
  size.set(signals.size)
@@ -267,13 +265,13 @@ const store = <T extends UnknownRecord>(initialValue: T): Store<T> => {
267
265
  if (prop === Symbol.iterator) {
268
266
  return function* () {
269
267
  for (const [key, signal] of signals) {
270
- yield [key, signal as Signal<T[keyof T]>]
268
+ yield [key, signal]
271
269
  }
272
270
  }
273
271
  }
274
272
 
275
273
  // Handle data properties - return signals
276
- return signals.get(key)
274
+ return signals.get(String(prop))
277
275
  },
278
276
  has(_target, prop) {
279
277
  const key = String(prop)
@@ -285,7 +283,7 @@ const store = <T extends UnknownRecord>(initialValue: T): Store<T> => {
285
283
  )
286
284
  },
287
285
  ownKeys() {
288
- return Array.from(signals.keys())
286
+ return Array.from(signals.keys()).map(key => String(key))
289
287
  },
290
288
  getOwnPropertyDescriptor(_target, prop) {
291
289
  const signal = signals.get(String(prop))
@@ -308,8 +306,9 @@ const store = <T extends UnknownRecord>(initialValue: T): Store<T> => {
308
306
  * @param {unknown} value - value to check
309
307
  * @returns {boolean} - true if the value is a Store instance, false otherwise
310
308
  */
311
- const isStore = <T extends UnknownRecord>(value: unknown): value is Store<T> =>
312
- isObjectOfType(value, TYPE_STORE)
309
+ const isStore = <T extends UnknownRecordOrArray>(
310
+ value: unknown,
311
+ ): value is Store<T> => isObjectOfType(value, TYPE_STORE)
313
312
 
314
313
  /* === Exports === */
315
314
 
package/src/util.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /* === Utility Functions === */
2
2
 
3
- const isNumber = /*#__PURE__*/ (value: unknown): value is number =>
4
- typeof value === 'number'
5
-
6
3
  const isString = /*#__PURE__*/ (value: unknown): value is string =>
7
4
  typeof value === 'string'
8
5
 
6
+ const isNumber = /*#__PURE__*/ (value: unknown): value is number =>
7
+ typeof value === 'number'
8
+
9
9
  const isFunction = /*#__PURE__*/ <T>(
10
10
  fn: unknown,
11
11
  ): fn is (...args: unknown[]) => T => typeof fn === 'function'
@@ -24,17 +24,16 @@ const isRecord = /*#__PURE__*/ <T extends Record<string, unknown>>(
24
24
  value: unknown,
25
25
  ): value is T => isObjectOfType(value, 'Object')
26
26
 
27
- const isPrimitive = /*#__PURE__*/ (value: unknown): boolean =>
28
- typeof value !== 'object' && !isFunction(value)
29
-
30
- const arrayToRecord = /*#__PURE__*/ <T extends unknown & {}>(
31
- array: T[],
32
- ): Record<string, T> => {
33
- const record: Record<string, T> = {}
34
- for (let i = 0; i < array.length; i++) {
35
- if (i in array) record[String(i)] = array[i]
36
- }
37
- return record
27
+ const validArrayIndexes = /*#__PURE__*/ (
28
+ keys: Array<PropertyKey>,
29
+ ): number[] | null => {
30
+ if (!keys.length) return null
31
+ const indexes = keys.map(k =>
32
+ isString(k) ? parseInt(k, 10) : isNumber(k) ? k : NaN,
33
+ )
34
+ return indexes.every(index => Number.isFinite(index) && index >= 0)
35
+ ? indexes.sort((a, b) => a - b)
36
+ : null
38
37
  }
39
38
 
40
39
  const hasMethod = /*#__PURE__*/ <
@@ -61,14 +60,13 @@ class CircularDependencyError extends Error {
61
60
  /* === Exports === */
62
61
 
63
62
  export {
64
- isNumber,
65
63
  isString,
64
+ isNumber,
66
65
  isFunction,
67
66
  isAsyncFunction,
68
67
  isObjectOfType,
69
68
  isRecord,
70
- isPrimitive,
71
- arrayToRecord,
69
+ validArrayIndexes,
72
70
  hasMethod,
73
71
  isAbortError,
74
72
  toError,