@zeix/cause-effect 0.16.1 → 0.17.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/.ai-context.md +85 -21
- package/.cursorrules +11 -5
- package/.github/copilot-instructions.md +64 -13
- package/CLAUDE.md +143 -163
- package/LICENSE +1 -1
- package/README.md +248 -333
- package/archive/benchmark.ts +688 -0
- package/archive/collection.ts +312 -0
- package/{src → archive}/computed.ts +21 -21
- package/archive/list.ts +551 -0
- package/archive/memo.ts +139 -0
- package/{src → archive}/state.ts +13 -11
- package/archive/store.ts +368 -0
- package/archive/task.ts +194 -0
- package/eslint.config.js +1 -0
- package/index.dev.js +938 -509
- package/index.js +1 -1
- package/index.ts +50 -23
- package/package.json +1 -1
- package/src/classes/collection.ts +282 -0
- package/src/classes/composite.ts +176 -0
- package/src/classes/computed.ts +333 -0
- package/src/classes/list.ts +305 -0
- package/src/classes/ref.ts +68 -0
- package/src/classes/state.ts +98 -0
- package/src/classes/store.ts +210 -0
- package/src/diff.ts +26 -53
- package/src/effect.ts +9 -9
- package/src/errors.ts +71 -25
- package/src/match.ts +5 -12
- package/src/resolve.ts +3 -2
- package/src/signal.ts +58 -41
- package/src/system.ts +79 -42
- package/src/util.ts +16 -34
- package/test/batch.test.ts +15 -17
- package/test/benchmark.test.ts +4 -4
- package/test/collection.test.ts +853 -0
- package/test/computed.test.ts +138 -130
- package/test/diff.test.ts +2 -2
- package/test/effect.test.ts +36 -35
- package/test/list.test.ts +754 -0
- package/test/match.test.ts +25 -25
- package/test/ref.test.ts +227 -0
- package/test/resolve.test.ts +17 -19
- package/test/signal.test.ts +70 -119
- package/test/state.test.ts +44 -44
- package/test/store.test.ts +253 -929
- package/types/index.d.ts +12 -9
- package/types/src/classes/collection.d.ts +46 -0
- package/types/src/classes/composite.d.ts +15 -0
- package/types/src/classes/computed.d.ts +97 -0
- package/types/src/classes/list.d.ts +41 -0
- package/types/src/classes/ref.d.ts +39 -0
- package/types/src/classes/state.d.ts +52 -0
- package/types/src/classes/store.d.ts +51 -0
- package/types/src/diff.d.ts +8 -12
- package/types/src/errors.d.ts +17 -11
- package/types/src/signal.d.ts +27 -14
- package/types/src/system.d.ts +41 -20
- package/types/src/util.d.ts +6 -4
- package/src/store.ts +0 -474
- package/types/src/collection.d.ts +0 -26
- package/types/src/computed.d.ts +0 -33
- package/types/src/scheduler.d.ts +0 -55
- package/types/src/state.d.ts +0 -24
- package/types/src/store.d.ts +0 -65
package/archive/list.ts
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import { type DiffResult, diff, isEqual, type UnknownArray } from '../src/diff'
|
|
2
|
+
import {
|
|
3
|
+
DuplicateKeyError,
|
|
4
|
+
InvalidSignalValueError,
|
|
5
|
+
NullishSignalValueError,
|
|
6
|
+
ReadonlySignalError,
|
|
7
|
+
} from '../src/errors'
|
|
8
|
+
import { isMutableSignal, type MutableSignal } from '../src/signal'
|
|
9
|
+
import {
|
|
10
|
+
batchSignalWrites,
|
|
11
|
+
type Cleanup,
|
|
12
|
+
createWatcher,
|
|
13
|
+
emitNotification,
|
|
14
|
+
type Listener,
|
|
15
|
+
type Listeners,
|
|
16
|
+
type Notifications,
|
|
17
|
+
notifyWatchers,
|
|
18
|
+
subscribeActiveWatcher,
|
|
19
|
+
trackSignalReads,
|
|
20
|
+
type Watcher,
|
|
21
|
+
} from '../src/system'
|
|
22
|
+
import {
|
|
23
|
+
isFunction,
|
|
24
|
+
isNumber,
|
|
25
|
+
isObjectOfType,
|
|
26
|
+
isRecord,
|
|
27
|
+
isString,
|
|
28
|
+
isSymbol,
|
|
29
|
+
UNSET,
|
|
30
|
+
} from '../src/util'
|
|
31
|
+
import {
|
|
32
|
+
type Collection,
|
|
33
|
+
type CollectionCallback,
|
|
34
|
+
createCollection,
|
|
35
|
+
} from './collection'
|
|
36
|
+
import { isComputed } from './computed'
|
|
37
|
+
import { createState, isState } from './state'
|
|
38
|
+
import { createStore, isStore } from './store'
|
|
39
|
+
|
|
40
|
+
/* === Types === */
|
|
41
|
+
|
|
42
|
+
type ArrayToRecord<T extends UnknownArray> = {
|
|
43
|
+
[key: string]: T extends Array<infer U extends {}> ? U : never
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type KeyConfig<T> = string | ((item: T) => string)
|
|
47
|
+
|
|
48
|
+
type List<T extends {}> = {
|
|
49
|
+
readonly [Symbol.toStringTag]: 'List'
|
|
50
|
+
[Symbol.iterator](): IterableIterator<MutableSignal<T>>
|
|
51
|
+
readonly [Symbol.isConcatSpreadable]: boolean
|
|
52
|
+
[n: number]: MutableSignal<T>
|
|
53
|
+
readonly length: number
|
|
54
|
+
add(value: T): string
|
|
55
|
+
byKey(key: string): MutableSignal<T> | undefined
|
|
56
|
+
deriveCollection<U extends {}>(
|
|
57
|
+
callback: CollectionCallback<U, T extends UnknownArray ? T : never>,
|
|
58
|
+
): Collection<U>
|
|
59
|
+
get(): T
|
|
60
|
+
keyAt(index: number): string | undefined
|
|
61
|
+
indexOfKey(key: string): number
|
|
62
|
+
set(value: T): void
|
|
63
|
+
update(fn: (value: T) => T): void
|
|
64
|
+
sort<U = T>(compareFn?: (a: U, b: U) => number): void
|
|
65
|
+
splice(start: number, deleteCount?: number, ...items: T[]): T[]
|
|
66
|
+
on<K extends keyof Notifications>(type: K, listener: Listener<K>): Cleanup
|
|
67
|
+
remove(index: number): void
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* === Constants === */
|
|
71
|
+
|
|
72
|
+
const TYPE_LIST = 'List' as const
|
|
73
|
+
|
|
74
|
+
/* === Functions === */
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create a new list with deeply nested reactive list items
|
|
78
|
+
*
|
|
79
|
+
* @since 0.16.2
|
|
80
|
+
* @param {T} initialValue - Initial array of the list
|
|
81
|
+
* @param {KeyConfig<T>} keyConfig - Optional key configuration:
|
|
82
|
+
* - string: used as prefix for auto-incrementing IDs (e.g., "item" → "item0", "item1")
|
|
83
|
+
* - function: computes key from array item at creation time
|
|
84
|
+
* @returns {List<T>} - New list with reactive items of type T
|
|
85
|
+
*/
|
|
86
|
+
const createList = <T extends {}>(
|
|
87
|
+
initialValue: T[],
|
|
88
|
+
keyConfig?: KeyConfig<T>,
|
|
89
|
+
): List<T> => {
|
|
90
|
+
if (initialValue == null) throw new NullishSignalValueError('store')
|
|
91
|
+
|
|
92
|
+
const watchers = new Set<Watcher>()
|
|
93
|
+
const listeners: Listeners = {
|
|
94
|
+
add: new Set<Listener<'add'>>(),
|
|
95
|
+
change: new Set<Listener<'change'>>(),
|
|
96
|
+
remove: new Set<Listener<'remove'>>(),
|
|
97
|
+
sort: new Set<Listener<'sort'>>(),
|
|
98
|
+
}
|
|
99
|
+
const signals = new Map<string, MutableSignal<T>>()
|
|
100
|
+
const ownWatchers = new Map<string, Watcher>()
|
|
101
|
+
|
|
102
|
+
// Stable key support for lists
|
|
103
|
+
let keyCounter = 0
|
|
104
|
+
let order: string[] = []
|
|
105
|
+
|
|
106
|
+
// Get signal by key or index
|
|
107
|
+
const getSignal = (prop: string): MutableSignal<T> | undefined => {
|
|
108
|
+
let key = prop
|
|
109
|
+
const index = Number(prop)
|
|
110
|
+
if (Number.isInteger(index) && index >= 0) key = order[index] ?? prop
|
|
111
|
+
return signals.get(key)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Generate stable key for array items
|
|
115
|
+
const generateKey = (item: T): string => {
|
|
116
|
+
const id = keyCounter++
|
|
117
|
+
return isString(keyConfig)
|
|
118
|
+
? `${keyConfig}${id}`
|
|
119
|
+
: isFunction<string>(keyConfig)
|
|
120
|
+
? keyConfig(item)
|
|
121
|
+
: String(id)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Convert array to record with stable keys
|
|
125
|
+
const arrayToRecord = (array: T[]): ArrayToRecord<T[]> => {
|
|
126
|
+
const record = {} as Record<string, T>
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < array.length; i++) {
|
|
129
|
+
const value = array[i]
|
|
130
|
+
if (value === undefined) continue // Skip sparse array positions
|
|
131
|
+
|
|
132
|
+
let key = order[i]
|
|
133
|
+
if (!key) {
|
|
134
|
+
key = generateKey(value)
|
|
135
|
+
order[i] = key
|
|
136
|
+
}
|
|
137
|
+
record[key] = value
|
|
138
|
+
}
|
|
139
|
+
return record
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Get current record
|
|
143
|
+
const current = (): T[] =>
|
|
144
|
+
order
|
|
145
|
+
.map(key => signals.get(key)?.get())
|
|
146
|
+
.filter(v => v !== undefined) as T[]
|
|
147
|
+
|
|
148
|
+
// Validate input
|
|
149
|
+
const isValidValue = <T>(
|
|
150
|
+
key: string,
|
|
151
|
+
value: T,
|
|
152
|
+
): value is NonNullable<T> => {
|
|
153
|
+
if (value == null)
|
|
154
|
+
throw new NullishSignalValueError(`store for key "${key}"`)
|
|
155
|
+
if (value === UNSET) return true
|
|
156
|
+
if (isSymbol(value) || isFunction(value) || isComputed(value))
|
|
157
|
+
throw new InvalidSignalValueError(`store for key "${key}"`, value)
|
|
158
|
+
return true
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Add own watcher for nested signal
|
|
162
|
+
const addOwnWatcher = (key: string, signal: MutableSignal<T>) => {
|
|
163
|
+
const watcher = createWatcher(() => {
|
|
164
|
+
trackSignalReads(watcher, () => {
|
|
165
|
+
signal.get() // Subscribe to the signal
|
|
166
|
+
emitNotification(listeners.change, [key])
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
ownWatchers.set(key, watcher)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Add nested signal and own watcher
|
|
173
|
+
const addProperty = <K extends keyof T & string>(
|
|
174
|
+
key: K,
|
|
175
|
+
value: T[K],
|
|
176
|
+
single = false,
|
|
177
|
+
): boolean => {
|
|
178
|
+
if (!isValidValue(key, value)) return false
|
|
179
|
+
|
|
180
|
+
// Create signal for key
|
|
181
|
+
// @ts-expect-error ignore
|
|
182
|
+
const signal: MutableSignal<T[K] & {}> =
|
|
183
|
+
isState(value) || isStore(value)
|
|
184
|
+
? (value as unknown as MutableSignal<T[K] & {}>)
|
|
185
|
+
: isRecord(value) || Array.isArray(value)
|
|
186
|
+
? createStore(value)
|
|
187
|
+
: createState(value)
|
|
188
|
+
|
|
189
|
+
// Set internal states
|
|
190
|
+
// @ts-expect-error ignore
|
|
191
|
+
signals.set(key, signal)
|
|
192
|
+
if (!order.includes(key)) order.push(key)
|
|
193
|
+
// @ts-expect-error ignore
|
|
194
|
+
if (listeners.change.size) addOwnWatcher(key, signal)
|
|
195
|
+
|
|
196
|
+
if (single) {
|
|
197
|
+
notifyWatchers(watchers)
|
|
198
|
+
emitNotification(listeners.add, [key])
|
|
199
|
+
}
|
|
200
|
+
return true
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Remove nested signal and effect
|
|
204
|
+
const removeProperty = (key: string, single = false) => {
|
|
205
|
+
// Remove signal for key
|
|
206
|
+
const ok = signals.delete(key)
|
|
207
|
+
if (!ok) return
|
|
208
|
+
|
|
209
|
+
// Clean up internal states
|
|
210
|
+
const index = order.indexOf(key)
|
|
211
|
+
if (index >= 0) order.splice(index, 1)
|
|
212
|
+
const watcher = ownWatchers.get(key)
|
|
213
|
+
if (watcher) {
|
|
214
|
+
watcher.stop()
|
|
215
|
+
ownWatchers.delete(key)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (single) {
|
|
219
|
+
order = order.filter(() => true) // Compact array
|
|
220
|
+
notifyWatchers(watchers)
|
|
221
|
+
emitNotification(listeners.remove, [key])
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Commit batched changes and emit notifications
|
|
226
|
+
const batchChanges = (changes: DiffResult, initialRun?: boolean) => {
|
|
227
|
+
// Additions
|
|
228
|
+
if (Object.keys(changes.add).length) {
|
|
229
|
+
for (const key in changes.add)
|
|
230
|
+
// @ts-expect-error ignore
|
|
231
|
+
addProperty(key, changes.add[key] as T, false)
|
|
232
|
+
|
|
233
|
+
// Queue initial additions event to allow listeners to be added first
|
|
234
|
+
if (initialRun)
|
|
235
|
+
setTimeout(() => {
|
|
236
|
+
emitNotification(listeners.add, Object.keys(changes.add))
|
|
237
|
+
}, 0)
|
|
238
|
+
else emitNotification(listeners.add, Object.keys(changes.add))
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Changes
|
|
242
|
+
if (Object.keys(changes.change).length) {
|
|
243
|
+
batchSignalWrites(() => {
|
|
244
|
+
for (const key in changes.change) {
|
|
245
|
+
const value = changes.change[key] as T
|
|
246
|
+
if (!isValidValue(key, value)) continue
|
|
247
|
+
|
|
248
|
+
const signal = signals.get(key)
|
|
249
|
+
if (isMutableSignal(signal)) signal.set(value)
|
|
250
|
+
else throw new ReadonlySignalError(key, value)
|
|
251
|
+
}
|
|
252
|
+
emitNotification(listeners.change, Object.keys(changes.change))
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Removals
|
|
257
|
+
if (Object.keys(changes.remove).length) {
|
|
258
|
+
for (const key in changes.remove) removeProperty(key)
|
|
259
|
+
order = order.filter(() => true)
|
|
260
|
+
emitNotification(listeners.remove, Object.keys(changes.remove))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return changes.changed
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Reconcile data and dispatch events
|
|
267
|
+
const reconcile = (
|
|
268
|
+
oldValue: T[],
|
|
269
|
+
newValue: T[],
|
|
270
|
+
initialRun?: boolean,
|
|
271
|
+
): boolean =>
|
|
272
|
+
batchChanges(
|
|
273
|
+
diff(arrayToRecord(oldValue), arrayToRecord(newValue)),
|
|
274
|
+
initialRun,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
// Initialize data
|
|
278
|
+
reconcile([] as T[], initialValue, true)
|
|
279
|
+
|
|
280
|
+
// Methods and Properties
|
|
281
|
+
const prototype: Record<PropertyKey, unknown> = {}
|
|
282
|
+
Object.defineProperties(prototype, {
|
|
283
|
+
[Symbol.toStringTag]: {
|
|
284
|
+
value: TYPE_LIST,
|
|
285
|
+
},
|
|
286
|
+
[Symbol.isConcatSpreadable]: {
|
|
287
|
+
value: true,
|
|
288
|
+
},
|
|
289
|
+
[Symbol.iterator]: {
|
|
290
|
+
value: function* () {
|
|
291
|
+
for (const key of order) {
|
|
292
|
+
const signal = signals.get(key)
|
|
293
|
+
if (signal) yield signal
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
length: {
|
|
298
|
+
get(): number {
|
|
299
|
+
subscribeActiveWatcher(watchers)
|
|
300
|
+
return signals.size
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
order: {
|
|
304
|
+
get(): string[] {
|
|
305
|
+
subscribeActiveWatcher(watchers)
|
|
306
|
+
return order
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
at: {
|
|
310
|
+
value(index: number): MutableSignal<T> | undefined {
|
|
311
|
+
return signals.get(order[index])
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
byKey: {
|
|
315
|
+
value: (key: string): MutableSignal<T> | undefined => {
|
|
316
|
+
return getSignal(key)
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
deriveCollection: {
|
|
320
|
+
value: <U extends {}>(
|
|
321
|
+
callback: CollectionCallback<U, T>,
|
|
322
|
+
): Collection<U> => {
|
|
323
|
+
const collection = createCollection(list, callback)
|
|
324
|
+
return collection
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
keyAt: {
|
|
328
|
+
value(index: number): string | undefined {
|
|
329
|
+
return order[index]
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
indexOfKey: {
|
|
333
|
+
value(key: string): number {
|
|
334
|
+
return order.indexOf(key)
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
get: {
|
|
338
|
+
value: (): T[] => {
|
|
339
|
+
subscribeActiveWatcher(watchers)
|
|
340
|
+
return current()
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
set: {
|
|
344
|
+
value: (newValue: T[]): void => {
|
|
345
|
+
if (reconcile(current(), newValue)) {
|
|
346
|
+
notifyWatchers(watchers)
|
|
347
|
+
if (UNSET === newValue) watchers.clear()
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
update: {
|
|
352
|
+
value: (fn: (oldValue: T[]) => T[]): void => {
|
|
353
|
+
const oldValue = current()
|
|
354
|
+
const newValue = fn(oldValue)
|
|
355
|
+
if (reconcile(oldValue, newValue)) {
|
|
356
|
+
notifyWatchers(watchers)
|
|
357
|
+
if (UNSET === newValue) watchers.clear()
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
add: {
|
|
362
|
+
value: (value: T): string => {
|
|
363
|
+
const key = generateKey(value)
|
|
364
|
+
if (!signals.has(key)) {
|
|
365
|
+
// @ts-expect-error ignore
|
|
366
|
+
addProperty(key, value, true)
|
|
367
|
+
return key
|
|
368
|
+
} else throw new DuplicateKeyError('store', key, value)
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
remove: {
|
|
372
|
+
value: (keyOrIndex: string | number): void => {
|
|
373
|
+
const key = isNumber(keyOrIndex)
|
|
374
|
+
? order[keyOrIndex]
|
|
375
|
+
: keyOrIndex
|
|
376
|
+
if (key && signals.has(key)) removeProperty(key, true)
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
sort: {
|
|
380
|
+
value: (
|
|
381
|
+
compareFn?: <
|
|
382
|
+
U = T extends UnknownArray
|
|
383
|
+
? T
|
|
384
|
+
: T[Extract<keyof T, string>],
|
|
385
|
+
>(
|
|
386
|
+
a: U,
|
|
387
|
+
b: U,
|
|
388
|
+
) => number,
|
|
389
|
+
): void => {
|
|
390
|
+
const entries = order
|
|
391
|
+
.map(key => [key, signals.get(key)?.get()] as [string, T])
|
|
392
|
+
.sort(
|
|
393
|
+
compareFn
|
|
394
|
+
? (a, b) => compareFn(a[1], b[1])
|
|
395
|
+
: (a, b) =>
|
|
396
|
+
String(a[1]).localeCompare(String(b[1])),
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
// Set new order
|
|
400
|
+
const newOrder = entries.map(([key]) => key)
|
|
401
|
+
if (!isEqual(newOrder, order)) {
|
|
402
|
+
order = newOrder
|
|
403
|
+
notifyWatchers(watchers)
|
|
404
|
+
emitNotification(listeners.sort, order)
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
splice: {
|
|
409
|
+
value: (
|
|
410
|
+
start: number,
|
|
411
|
+
deleteCount?: number,
|
|
412
|
+
...items: T[]
|
|
413
|
+
): T[] => {
|
|
414
|
+
// Normalize start and deleteCount
|
|
415
|
+
const length = signals.size
|
|
416
|
+
const actualStart =
|
|
417
|
+
start < 0
|
|
418
|
+
? Math.max(0, length + start)
|
|
419
|
+
: Math.min(start, length)
|
|
420
|
+
const actualDeleteCount = Math.max(
|
|
421
|
+
0,
|
|
422
|
+
Math.min(
|
|
423
|
+
deleteCount ??
|
|
424
|
+
Math.max(0, length - Math.max(0, actualStart)),
|
|
425
|
+
length - actualStart,
|
|
426
|
+
),
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
const add = {} as Record<string, T>
|
|
430
|
+
const remove = {} as Record<string, T>
|
|
431
|
+
|
|
432
|
+
// Collect items to delete and their keys
|
|
433
|
+
for (let i = 0; i < actualDeleteCount; i++) {
|
|
434
|
+
const index = actualStart + i
|
|
435
|
+
const key = order[index]
|
|
436
|
+
if (key) {
|
|
437
|
+
const signal = signals.get(key)
|
|
438
|
+
if (signal) remove[key] = signal.get() as T
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Build new order: items before splice point
|
|
443
|
+
const newOrder = order.slice(0, actualStart)
|
|
444
|
+
|
|
445
|
+
// Add new items
|
|
446
|
+
for (const item of items) {
|
|
447
|
+
const key = generateKey(item)
|
|
448
|
+
newOrder.push(key)
|
|
449
|
+
add[key] = item as T
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Add items after splice point
|
|
453
|
+
newOrder.push(...order.slice(actualStart + actualDeleteCount))
|
|
454
|
+
|
|
455
|
+
// Update the order array
|
|
456
|
+
order = newOrder.filter(() => true) // Compact array
|
|
457
|
+
|
|
458
|
+
const changed = !!(
|
|
459
|
+
Object.keys(add).length || Object.keys(remove).length
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
if (changed)
|
|
463
|
+
batchChanges({
|
|
464
|
+
add,
|
|
465
|
+
change: {} as Record<string, T>,
|
|
466
|
+
remove,
|
|
467
|
+
changed,
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
notifyWatchers(watchers)
|
|
471
|
+
|
|
472
|
+
return Object.values(remove) as T[]
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
on: {
|
|
476
|
+
value: <K extends keyof Notifications>(
|
|
477
|
+
type: K,
|
|
478
|
+
listener: Listener<K>,
|
|
479
|
+
): Cleanup => {
|
|
480
|
+
listeners[type].add(listener)
|
|
481
|
+
if (type === 'change' && !ownWatchers.size) {
|
|
482
|
+
for (const [key, signal] of signals)
|
|
483
|
+
addOwnWatcher(key, signal)
|
|
484
|
+
}
|
|
485
|
+
return () => {
|
|
486
|
+
listeners[type].delete(listener)
|
|
487
|
+
if (type === 'change' && !listeners.change.size) {
|
|
488
|
+
if (ownWatchers.size) {
|
|
489
|
+
for (const watcher of ownWatchers.values())
|
|
490
|
+
watcher.stop()
|
|
491
|
+
ownWatchers.clear()
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
// Return proxy directly with integrated signal methods
|
|
500
|
+
const list = new Proxy(prototype as List<T>, {
|
|
501
|
+
get(target, prop) {
|
|
502
|
+
if (prop in target) return Reflect.get(target, prop)
|
|
503
|
+
if (!isSymbol(prop)) return getSignal(prop)
|
|
504
|
+
},
|
|
505
|
+
has(target, prop) {
|
|
506
|
+
if (prop in target) return true
|
|
507
|
+
return signals.has(String(prop))
|
|
508
|
+
},
|
|
509
|
+
ownKeys(target) {
|
|
510
|
+
const staticKeys = Reflect.ownKeys(target)
|
|
511
|
+
return [...new Set([...order, ...staticKeys])]
|
|
512
|
+
},
|
|
513
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
514
|
+
if (prop in target)
|
|
515
|
+
return Reflect.getOwnPropertyDescriptor(target, prop)
|
|
516
|
+
if (isSymbol(prop)) return undefined
|
|
517
|
+
|
|
518
|
+
const signal = getSignal(prop)
|
|
519
|
+
return signal
|
|
520
|
+
? {
|
|
521
|
+
enumerable: true,
|
|
522
|
+
configurable: true,
|
|
523
|
+
writable: true,
|
|
524
|
+
value: signal,
|
|
525
|
+
}
|
|
526
|
+
: undefined
|
|
527
|
+
},
|
|
528
|
+
})
|
|
529
|
+
return list
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Check if the provided value is a List instance
|
|
534
|
+
*
|
|
535
|
+
* @since 0.15.0
|
|
536
|
+
* @param {unknown} value - value to check
|
|
537
|
+
* @returns {boolean} - true if the value is a List instance, false otherwise
|
|
538
|
+
*/
|
|
539
|
+
const isList = <T extends {}>(value: unknown): value is List<T> =>
|
|
540
|
+
isObjectOfType(value, TYPE_LIST)
|
|
541
|
+
|
|
542
|
+
/* === Exports === */
|
|
543
|
+
|
|
544
|
+
export {
|
|
545
|
+
TYPE_LIST,
|
|
546
|
+
isList,
|
|
547
|
+
createList,
|
|
548
|
+
type ArrayToRecord,
|
|
549
|
+
type List,
|
|
550
|
+
type KeyConfig,
|
|
551
|
+
}
|
package/archive/memo.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CircularDependencyError,
|
|
3
|
+
createError,
|
|
4
|
+
InvalidCallbackError,
|
|
5
|
+
NullishSignalValueError,
|
|
6
|
+
} from '../src/errors'
|
|
7
|
+
import {
|
|
8
|
+
createWatcher,
|
|
9
|
+
flushPendingReactions,
|
|
10
|
+
notifyWatchers,
|
|
11
|
+
subscribeActiveWatcher,
|
|
12
|
+
trackSignalReads,
|
|
13
|
+
type Watcher,
|
|
14
|
+
} from '../src/system'
|
|
15
|
+
import { isObjectOfType, isSyncFunction, UNSET } from '../src/util'
|
|
16
|
+
|
|
17
|
+
/* === Types === */
|
|
18
|
+
|
|
19
|
+
type Memo<T extends {}> = {
|
|
20
|
+
readonly [Symbol.toStringTag]: 'Memo'
|
|
21
|
+
get(): T
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type MemoCallback<T extends {} & { then?: undefined }> = (oldValue: T) => T
|
|
25
|
+
|
|
26
|
+
/* === Constants === */
|
|
27
|
+
|
|
28
|
+
const TYPE_MEMO = 'Memo' as const
|
|
29
|
+
|
|
30
|
+
/* === Functions === */
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a derived signal from existing signals
|
|
34
|
+
*
|
|
35
|
+
* @since 0.9.0
|
|
36
|
+
* @param {MemoCallback<T>} callback - Computation callback function
|
|
37
|
+
* @returns {Memo<T>} - Computed signal
|
|
38
|
+
*/
|
|
39
|
+
const createMemo = <T extends {}>(
|
|
40
|
+
callback: MemoCallback<T>,
|
|
41
|
+
initialValue: T = UNSET,
|
|
42
|
+
): Memo<T> => {
|
|
43
|
+
if (!isMemoCallback(callback))
|
|
44
|
+
throw new InvalidCallbackError('memo', callback)
|
|
45
|
+
if (initialValue == null) throw new NullishSignalValueError('memo')
|
|
46
|
+
|
|
47
|
+
const watchers: Set<Watcher> = new Set()
|
|
48
|
+
|
|
49
|
+
// Internal state
|
|
50
|
+
let value: T = initialValue
|
|
51
|
+
let error: Error | undefined
|
|
52
|
+
let dirty = true
|
|
53
|
+
let computing = false
|
|
54
|
+
|
|
55
|
+
// Own watcher: called when notified from sources (push)
|
|
56
|
+
const watcher = createWatcher(() => {
|
|
57
|
+
dirty = true
|
|
58
|
+
if (watchers.size) notifyWatchers(watchers)
|
|
59
|
+
else watcher.stop()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Called when requested by dependencies (pull)
|
|
63
|
+
const compute = () =>
|
|
64
|
+
trackSignalReads(watcher, () => {
|
|
65
|
+
if (computing) throw new CircularDependencyError('memo')
|
|
66
|
+
let result: T
|
|
67
|
+
computing = true
|
|
68
|
+
try {
|
|
69
|
+
result = callback(value)
|
|
70
|
+
} catch (e) {
|
|
71
|
+
// Err track
|
|
72
|
+
value = UNSET
|
|
73
|
+
error = createError(e)
|
|
74
|
+
computing = false
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (null == result || UNSET === result) {
|
|
79
|
+
// Nil track
|
|
80
|
+
value = UNSET
|
|
81
|
+
error = undefined
|
|
82
|
+
} else {
|
|
83
|
+
// Ok track
|
|
84
|
+
value = result
|
|
85
|
+
error = undefined
|
|
86
|
+
dirty = false
|
|
87
|
+
}
|
|
88
|
+
computing = false
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const memo: Record<PropertyKey, unknown> = {}
|
|
92
|
+
Object.defineProperties(memo, {
|
|
93
|
+
[Symbol.toStringTag]: {
|
|
94
|
+
value: TYPE_MEMO,
|
|
95
|
+
},
|
|
96
|
+
get: {
|
|
97
|
+
value: (): T => {
|
|
98
|
+
subscribeActiveWatcher(watchers)
|
|
99
|
+
flushPendingReactions()
|
|
100
|
+
if (dirty) compute()
|
|
101
|
+
if (error) throw error
|
|
102
|
+
return value
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
return memo as Memo<T>
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if a value is a memoized signal
|
|
111
|
+
*
|
|
112
|
+
* @since 0.9.0
|
|
113
|
+
* @param {unknown} value - Value to check
|
|
114
|
+
* @returns {boolean} - True if value is a memo signal, false otherwise
|
|
115
|
+
*/
|
|
116
|
+
const isMemo = /*#__PURE__*/ <T extends {}>(value: unknown): value is Memo<T> =>
|
|
117
|
+
isObjectOfType(value, TYPE_MEMO)
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if the provided value is a callback that may be used as input for toSignal() to derive a computed state
|
|
121
|
+
*
|
|
122
|
+
* @since 0.12.0
|
|
123
|
+
* @param {unknown} value - Value to check
|
|
124
|
+
* @returns {boolean} - True if value is a sync callback, false otherwise
|
|
125
|
+
*/
|
|
126
|
+
const isMemoCallback = /*#__PURE__*/ <T extends {} & { then?: undefined }>(
|
|
127
|
+
value: unknown,
|
|
128
|
+
): value is MemoCallback<T> => isSyncFunction(value) && value.length < 2
|
|
129
|
+
|
|
130
|
+
/* === Exports === */
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
TYPE_MEMO,
|
|
134
|
+
createMemo,
|
|
135
|
+
isMemo,
|
|
136
|
+
isMemoCallback,
|
|
137
|
+
type Memo,
|
|
138
|
+
type MemoCallback,
|
|
139
|
+
}
|