@zeix/cause-effect 0.17.3 → 0.18.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 +169 -227
- package/.cursorrules +41 -35
- package/.github/copilot-instructions.md +176 -116
- package/ARCHITECTURE.md +276 -0
- package/CHANGELOG.md +29 -0
- package/CLAUDE.md +201 -143
- package/GUIDE.md +298 -0
- package/README.md +246 -193
- package/REQUIREMENTS.md +100 -0
- package/bench/reactivity.bench.ts +577 -0
- package/context7.json +4 -0
- package/examples/events-sensor.ts +187 -0
- package/examples/selector-sensor.ts +173 -0
- package/index.dev.js +1390 -1008
- package/index.js +1 -1
- package/index.ts +60 -74
- package/package.json +5 -2
- package/skills/changelog-keeper/SKILL.md +59 -0
- package/skills/changelog-keeper/agents/openai.yaml +4 -0
- package/src/errors.ts +118 -74
- package/src/graph.ts +612 -0
- package/src/nodes/collection.ts +512 -0
- package/src/nodes/effect.ts +149 -0
- package/src/nodes/list.ts +589 -0
- package/src/nodes/memo.ts +148 -0
- package/src/nodes/sensor.ts +149 -0
- package/src/nodes/state.ts +135 -0
- package/src/nodes/store.ts +378 -0
- package/src/nodes/task.ts +174 -0
- package/src/signal.ts +112 -66
- package/src/util.ts +26 -57
- package/test/batch.test.ts +96 -62
- package/test/benchmark.test.ts +473 -487
- package/test/collection.test.ts +456 -707
- package/test/effect.test.ts +293 -696
- package/test/list.test.ts +335 -592
- package/test/memo.test.ts +574 -0
- package/test/regression.test.ts +156 -0
- package/test/scope.test.ts +191 -0
- package/test/sensor.test.ts +454 -0
- package/test/signal.test.ts +220 -213
- package/test/state.test.ts +217 -265
- package/test/store.test.ts +346 -446
- package/test/task.test.ts +529 -0
- package/test/untrack.test.ts +167 -0
- package/types/index.d.ts +13 -15
- package/types/src/errors.d.ts +73 -17
- package/types/src/graph.d.ts +218 -0
- package/types/src/nodes/collection.d.ts +69 -0
- package/types/src/nodes/effect.d.ts +48 -0
- package/types/src/nodes/list.d.ts +66 -0
- package/types/src/nodes/memo.d.ts +63 -0
- package/types/src/nodes/sensor.d.ts +81 -0
- package/types/src/nodes/state.d.ts +78 -0
- package/types/src/nodes/store.d.ts +51 -0
- package/types/src/nodes/task.d.ts +79 -0
- package/types/src/signal.d.ts +43 -29
- package/types/src/util.d.ts +9 -16
- package/archive/benchmark.ts +0 -683
- package/archive/collection.ts +0 -253
- package/archive/composite.ts +0 -85
- package/archive/computed.ts +0 -195
- package/archive/list.ts +0 -483
- package/archive/memo.ts +0 -139
- package/archive/state.ts +0 -90
- package/archive/store.ts +0 -298
- package/archive/task.ts +0 -189
- package/src/classes/collection.ts +0 -245
- package/src/classes/computed.ts +0 -349
- package/src/classes/list.ts +0 -343
- package/src/classes/ref.ts +0 -70
- package/src/classes/state.ts +0 -102
- package/src/classes/store.ts +0 -262
- package/src/diff.ts +0 -138
- package/src/effect.ts +0 -93
- package/src/match.ts +0 -45
- package/src/resolve.ts +0 -49
- package/src/system.ts +0 -257
- package/test/computed.test.ts +0 -1108
- package/test/diff.test.ts +0 -955
- package/test/match.test.ts +0 -388
- package/test/ref.test.ts +0 -353
- package/test/resolve.test.ts +0 -154
- package/types/src/classes/collection.d.ts +0 -45
- package/types/src/classes/computed.d.ts +0 -94
- package/types/src/classes/list.d.ts +0 -43
- package/types/src/classes/ref.d.ts +0 -35
- package/types/src/classes/state.d.ts +0 -49
- package/types/src/classes/store.d.ts +0 -52
- package/types/src/diff.d.ts +0 -28
- package/types/src/effect.d.ts +0 -15
- package/types/src/match.d.ts +0 -21
- package/types/src/resolve.d.ts +0 -29
- package/types/src/system.d.ts +0 -78
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CircularDependencyError,
|
|
3
|
+
DuplicateKeyError,
|
|
4
|
+
validateSignalValue,
|
|
5
|
+
} from '../errors'
|
|
6
|
+
import {
|
|
7
|
+
activeSink,
|
|
8
|
+
batch,
|
|
9
|
+
batchDepth,
|
|
10
|
+
type Cleanup,
|
|
11
|
+
FLAG_CLEAN,
|
|
12
|
+
FLAG_DIRTY,
|
|
13
|
+
flush,
|
|
14
|
+
link,
|
|
15
|
+
type MemoNode,
|
|
16
|
+
propagate,
|
|
17
|
+
refresh,
|
|
18
|
+
type SinkNode,
|
|
19
|
+
TYPE_LIST,
|
|
20
|
+
untrack,
|
|
21
|
+
} from '../graph'
|
|
22
|
+
import { isFunction, isObjectOfType, isRecord } from '../util'
|
|
23
|
+
import {
|
|
24
|
+
type Collection,
|
|
25
|
+
type CollectionSource,
|
|
26
|
+
type DeriveCollectionCallback,
|
|
27
|
+
deriveCollection,
|
|
28
|
+
} from './collection'
|
|
29
|
+
import { createState, type State } from './state'
|
|
30
|
+
|
|
31
|
+
/* === Types === */
|
|
32
|
+
|
|
33
|
+
type UnknownRecord = Record<string, unknown>
|
|
34
|
+
|
|
35
|
+
type DiffResult = {
|
|
36
|
+
changed: boolean
|
|
37
|
+
add: UnknownRecord
|
|
38
|
+
change: UnknownRecord
|
|
39
|
+
remove: UnknownRecord
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type KeyConfig<T> = string | ((item: T) => string | undefined)
|
|
43
|
+
|
|
44
|
+
type ListOptions<T extends {}> = {
|
|
45
|
+
keyConfig?: KeyConfig<T>
|
|
46
|
+
watched?: () => Cleanup
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type List<T extends {}> = {
|
|
50
|
+
readonly [Symbol.toStringTag]: 'List'
|
|
51
|
+
readonly [Symbol.isConcatSpreadable]: true
|
|
52
|
+
[Symbol.iterator](): IterableIterator<State<T>>
|
|
53
|
+
readonly length: number
|
|
54
|
+
get(): T[]
|
|
55
|
+
set(next: T[]): void
|
|
56
|
+
update(fn: (prev: T[]) => T[]): void
|
|
57
|
+
at(index: number): State<T> | undefined
|
|
58
|
+
keys(): IterableIterator<string>
|
|
59
|
+
byKey(key: string): State<T> | undefined
|
|
60
|
+
keyAt(index: number): string | undefined
|
|
61
|
+
indexOfKey(key: string): number
|
|
62
|
+
add(value: T): string
|
|
63
|
+
remove(keyOrIndex: string | number): void
|
|
64
|
+
sort(compareFn?: (a: T, b: T) => number): void
|
|
65
|
+
splice(start: number, deleteCount?: number, ...items: T[]): T[]
|
|
66
|
+
deriveCollection<R extends {}>(
|
|
67
|
+
callback: (sourceValue: T) => R,
|
|
68
|
+
): Collection<R>
|
|
69
|
+
deriveCollection<R extends {}>(
|
|
70
|
+
callback: (sourceValue: T, abort: AbortSignal) => Promise<R>,
|
|
71
|
+
): Collection<R>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* === Functions === */
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Checks if two values are equal with cycle detection
|
|
78
|
+
*
|
|
79
|
+
* @since 0.15.0
|
|
80
|
+
* @param a - First value to compare
|
|
81
|
+
* @param b - Second value to compare
|
|
82
|
+
* @param visited - Set to track visited objects for cycle detection
|
|
83
|
+
* @returns Whether the two values are equal
|
|
84
|
+
*/
|
|
85
|
+
function isEqual<T>(a: T, b: T, visited?: WeakSet<object>): boolean {
|
|
86
|
+
// Fast paths
|
|
87
|
+
if (Object.is(a, b)) return true
|
|
88
|
+
if (typeof a !== typeof b) return false
|
|
89
|
+
if (
|
|
90
|
+
a == null ||
|
|
91
|
+
typeof a !== 'object' ||
|
|
92
|
+
b == null ||
|
|
93
|
+
typeof b !== 'object'
|
|
94
|
+
)
|
|
95
|
+
return false
|
|
96
|
+
|
|
97
|
+
// Cycle detection (only allocate WeakSet when both values are objects)
|
|
98
|
+
if (!visited) visited = new WeakSet()
|
|
99
|
+
if (visited.has(a as object) || visited.has(b as object))
|
|
100
|
+
throw new CircularDependencyError('isEqual')
|
|
101
|
+
visited.add(a)
|
|
102
|
+
visited.add(b)
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const aIsArray = Array.isArray(a)
|
|
106
|
+
if (aIsArray !== Array.isArray(b)) return false
|
|
107
|
+
|
|
108
|
+
if (aIsArray) {
|
|
109
|
+
const aa = a as unknown[]
|
|
110
|
+
const ba = b as unknown[]
|
|
111
|
+
if (aa.length !== ba.length) return false
|
|
112
|
+
for (let i = 0; i < aa.length; i++)
|
|
113
|
+
if (!isEqual(aa[i], ba[i], visited)) return false
|
|
114
|
+
return true
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (isRecord(a) && isRecord(b)) {
|
|
118
|
+
const aKeys = Object.keys(a)
|
|
119
|
+
const bKeys = Object.keys(b)
|
|
120
|
+
|
|
121
|
+
if (aKeys.length !== bKeys.length) return false
|
|
122
|
+
for (const key of aKeys) {
|
|
123
|
+
if (!(key in b)) return false
|
|
124
|
+
if (!isEqual(a[key], b[key], visited)) return false
|
|
125
|
+
}
|
|
126
|
+
return true
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// For non-records/non-arrays, they are only equal if they are the same reference
|
|
130
|
+
// (which would have been caught by Object.is at the beginning)
|
|
131
|
+
return false
|
|
132
|
+
} finally {
|
|
133
|
+
visited.delete(a)
|
|
134
|
+
visited.delete(b)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Shallow equality check for string arrays */
|
|
139
|
+
function keysEqual(a: string[], b: string[]): boolean {
|
|
140
|
+
if (a.length !== b.length) return false
|
|
141
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false
|
|
142
|
+
return true
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getKeyGenerator<T extends {}>(
|
|
146
|
+
keyConfig?: KeyConfig<T>,
|
|
147
|
+
): [(item: T) => string, boolean] {
|
|
148
|
+
let keyCounter = 0
|
|
149
|
+
const contentBased = typeof keyConfig === 'function'
|
|
150
|
+
return [
|
|
151
|
+
typeof keyConfig === 'string'
|
|
152
|
+
? () => `${keyConfig}${keyCounter++}`
|
|
153
|
+
: contentBased
|
|
154
|
+
? (item: T) => keyConfig(item) || String(keyCounter++)
|
|
155
|
+
: () => String(keyCounter++),
|
|
156
|
+
contentBased,
|
|
157
|
+
]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Compares two arrays using existing keys and returns differences as a DiffResult.
|
|
162
|
+
* Avoids object conversion by working directly with arrays and keys.
|
|
163
|
+
*
|
|
164
|
+
* @since 0.18.0
|
|
165
|
+
* @param prev - The old array
|
|
166
|
+
* @param next - The new array
|
|
167
|
+
* @param prevKeys - Current keys array (may be sparse or shorter than oldArray)
|
|
168
|
+
* @param generateKey - Function to generate keys for new items
|
|
169
|
+
* @param contentBased - When true, always use generateKey (content-based keys);
|
|
170
|
+
* when false, reuse positional keys from currentKeys (synthetic keys)
|
|
171
|
+
* @returns The differences in DiffResult format plus updated keys array
|
|
172
|
+
*/
|
|
173
|
+
function diffArrays<T>(
|
|
174
|
+
prev: T[],
|
|
175
|
+
next: T[],
|
|
176
|
+
prevKeys: string[],
|
|
177
|
+
generateKey: (item: T) => string,
|
|
178
|
+
contentBased: boolean,
|
|
179
|
+
): DiffResult & { newKeys: string[] } {
|
|
180
|
+
const visited = new WeakSet()
|
|
181
|
+
const add = {} as UnknownRecord
|
|
182
|
+
const change = {} as UnknownRecord
|
|
183
|
+
const remove = {} as UnknownRecord
|
|
184
|
+
const nextKeys: string[] = []
|
|
185
|
+
let changed = false
|
|
186
|
+
|
|
187
|
+
// Build a map of old values by key for quick lookup
|
|
188
|
+
const prevByKey = new Map<string, T>()
|
|
189
|
+
for (let i = 0; i < prev.length; i++) {
|
|
190
|
+
const key = prevKeys[i]
|
|
191
|
+
if (key && prev[i]) prevByKey.set(key, prev[i])
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Track which old keys we've seen
|
|
195
|
+
const seenKeys = new Set<string>()
|
|
196
|
+
|
|
197
|
+
// Process new array and build new keys array
|
|
198
|
+
for (let i = 0; i < next.length; i++) {
|
|
199
|
+
const val = next[i]
|
|
200
|
+
if (val === undefined) continue
|
|
201
|
+
|
|
202
|
+
// Content-based keys: always derive from item; synthetic keys: reuse by position
|
|
203
|
+
const key = contentBased
|
|
204
|
+
? generateKey(val)
|
|
205
|
+
: (prevKeys[i] ?? generateKey(val))
|
|
206
|
+
|
|
207
|
+
if (seenKeys.has(key)) throw new DuplicateKeyError(TYPE_LIST, key, val)
|
|
208
|
+
|
|
209
|
+
nextKeys.push(key)
|
|
210
|
+
seenKeys.add(key)
|
|
211
|
+
|
|
212
|
+
// Check if this key existed before
|
|
213
|
+
if (!prevByKey.has(key)) {
|
|
214
|
+
add[key] = val
|
|
215
|
+
changed = true
|
|
216
|
+
} else if (!isEqual(prevByKey.get(key), val, visited)) {
|
|
217
|
+
change[key] = val
|
|
218
|
+
changed = true
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Find removed keys (existed in old but not in new)
|
|
223
|
+
for (const [key] of prevByKey) {
|
|
224
|
+
if (!seenKeys.has(key)) {
|
|
225
|
+
remove[key] = null
|
|
226
|
+
changed = true
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Detect reorder even when no values changed
|
|
231
|
+
if (!changed && !keysEqual(prevKeys, nextKeys)) changed = true
|
|
232
|
+
|
|
233
|
+
return { add, change, remove, newKeys: nextKeys, changed }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Creates a reactive list with stable keys and per-item reactivity.
|
|
238
|
+
*
|
|
239
|
+
* @since 0.18.0
|
|
240
|
+
* @param value - Initial array of items
|
|
241
|
+
* @param options - Optional configuration for key generation and watch lifecycle
|
|
242
|
+
* @returns A List signal
|
|
243
|
+
*/
|
|
244
|
+
function createList<T extends {}>(
|
|
245
|
+
value: T[],
|
|
246
|
+
options?: ListOptions<T>,
|
|
247
|
+
): List<T> {
|
|
248
|
+
validateSignalValue(TYPE_LIST, value, Array.isArray)
|
|
249
|
+
|
|
250
|
+
const signals = new Map<string, State<T>>()
|
|
251
|
+
let keys: string[] = []
|
|
252
|
+
|
|
253
|
+
const [generateKey, contentBased] = getKeyGenerator(options?.keyConfig)
|
|
254
|
+
|
|
255
|
+
// --- Internal helpers ---
|
|
256
|
+
|
|
257
|
+
// Build current value from child signals
|
|
258
|
+
const buildValue = (): T[] =>
|
|
259
|
+
keys
|
|
260
|
+
.map(key => signals.get(key)?.get())
|
|
261
|
+
.filter(v => v !== undefined) as T[]
|
|
262
|
+
|
|
263
|
+
// Structural tracking node — not a general-purpose Memo.
|
|
264
|
+
// On first get(): refresh() establishes edges from child signals.
|
|
265
|
+
// On subsequent get(): untrack(buildValue) rebuilds without re-linking.
|
|
266
|
+
// Mutation methods (add/remove/set/splice) null out sources to force re-establishment.
|
|
267
|
+
const node: MemoNode<T[]> = {
|
|
268
|
+
fn: buildValue,
|
|
269
|
+
value,
|
|
270
|
+
flags: FLAG_DIRTY,
|
|
271
|
+
sources: null,
|
|
272
|
+
sourcesTail: null,
|
|
273
|
+
sinks: null,
|
|
274
|
+
sinksTail: null,
|
|
275
|
+
equals: isEqual,
|
|
276
|
+
error: undefined,
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const toRecord = (array: T[]): Record<string, T> => {
|
|
280
|
+
const record = {} as Record<string, T>
|
|
281
|
+
for (let i = 0; i < array.length; i++) {
|
|
282
|
+
const val = array[i]
|
|
283
|
+
if (val === undefined) continue
|
|
284
|
+
let key = keys[i]
|
|
285
|
+
if (!key) {
|
|
286
|
+
key = generateKey(val)
|
|
287
|
+
keys[i] = key
|
|
288
|
+
}
|
|
289
|
+
record[key] = val
|
|
290
|
+
}
|
|
291
|
+
return record
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const applyChanges = (changes: DiffResult): boolean => {
|
|
295
|
+
let structural = false
|
|
296
|
+
|
|
297
|
+
// Additions
|
|
298
|
+
for (const key in changes.add) {
|
|
299
|
+
const val = changes.add[key] as T
|
|
300
|
+
validateSignalValue(`${TYPE_LIST} item for key "${key}"`, val)
|
|
301
|
+
signals.set(key, createState(val))
|
|
302
|
+
structural = true
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Changes
|
|
306
|
+
if (Object.keys(changes.change).length) {
|
|
307
|
+
batch(() => {
|
|
308
|
+
for (const key in changes.change) {
|
|
309
|
+
const val = changes.change[key]
|
|
310
|
+
validateSignalValue(
|
|
311
|
+
`${TYPE_LIST} item for key "${key}"`,
|
|
312
|
+
val,
|
|
313
|
+
)
|
|
314
|
+
const signal = signals.get(key)
|
|
315
|
+
if (signal) signal.set(val as T)
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Removals
|
|
321
|
+
for (const key in changes.remove) {
|
|
322
|
+
signals.delete(key)
|
|
323
|
+
const index = keys.indexOf(key)
|
|
324
|
+
if (index !== -1) keys.splice(index, 1)
|
|
325
|
+
structural = true
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (structural) {
|
|
329
|
+
node.sources = null
|
|
330
|
+
node.sourcesTail = null
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return changes.changed
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const watched = options?.watched
|
|
337
|
+
const subscribe = watched
|
|
338
|
+
? () => {
|
|
339
|
+
if (activeSink) {
|
|
340
|
+
if (!node.sinks) node.stop = watched()
|
|
341
|
+
link(node, activeSink)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
: () => {
|
|
345
|
+
if (activeSink) link(node, activeSink)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// --- Initialize ---
|
|
349
|
+
const initRecord = toRecord(value)
|
|
350
|
+
for (const key in initRecord) {
|
|
351
|
+
const val = initRecord[key]
|
|
352
|
+
validateSignalValue(`${TYPE_LIST} item for key "${key}"`, val)
|
|
353
|
+
signals.set(key, createState(val))
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Starts clean: mutation methods (add/remove/set/splice) explicitly call
|
|
357
|
+
// propagate() + invalidate edges, so refresh() on first get() is not needed.
|
|
358
|
+
node.value = value
|
|
359
|
+
node.flags = 0
|
|
360
|
+
|
|
361
|
+
// --- List object ---
|
|
362
|
+
const list: List<T> = {
|
|
363
|
+
[Symbol.toStringTag]: TYPE_LIST,
|
|
364
|
+
[Symbol.isConcatSpreadable]: true as const,
|
|
365
|
+
|
|
366
|
+
*[Symbol.iterator]() {
|
|
367
|
+
for (const key of keys) {
|
|
368
|
+
const signal = signals.get(key)
|
|
369
|
+
if (signal) yield signal
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
get length() {
|
|
374
|
+
subscribe()
|
|
375
|
+
return keys.length
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
get() {
|
|
379
|
+
subscribe()
|
|
380
|
+
if (node.sources) {
|
|
381
|
+
// Fast path: edges already established, rebuild value directly
|
|
382
|
+
if (node.flags) {
|
|
383
|
+
node.value = untrack(buildValue)
|
|
384
|
+
node.flags = FLAG_CLEAN
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
// First access: use refresh() to establish child → list edges
|
|
388
|
+
refresh(node as unknown as SinkNode)
|
|
389
|
+
if (node.error) throw node.error
|
|
390
|
+
}
|
|
391
|
+
return node.value
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
set(next: T[]) {
|
|
395
|
+
const prev = node.flags & FLAG_DIRTY ? buildValue() : node.value
|
|
396
|
+
const changes = diffArrays(
|
|
397
|
+
prev,
|
|
398
|
+
next,
|
|
399
|
+
keys,
|
|
400
|
+
generateKey,
|
|
401
|
+
contentBased,
|
|
402
|
+
)
|
|
403
|
+
if (changes.changed) {
|
|
404
|
+
keys = changes.newKeys
|
|
405
|
+
applyChanges(changes)
|
|
406
|
+
node.flags |= FLAG_DIRTY
|
|
407
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
408
|
+
if (batchDepth === 0) flush()
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
update(fn: (prev: T[]) => T[]) {
|
|
413
|
+
list.set(fn(list.get()))
|
|
414
|
+
},
|
|
415
|
+
|
|
416
|
+
at(index: number) {
|
|
417
|
+
return signals.get(keys[index])
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
keys() {
|
|
421
|
+
subscribe()
|
|
422
|
+
return keys.values()
|
|
423
|
+
},
|
|
424
|
+
|
|
425
|
+
byKey(key: string) {
|
|
426
|
+
return signals.get(key)
|
|
427
|
+
},
|
|
428
|
+
|
|
429
|
+
keyAt(index: number) {
|
|
430
|
+
return keys[index]
|
|
431
|
+
},
|
|
432
|
+
|
|
433
|
+
indexOfKey(key: string) {
|
|
434
|
+
return keys.indexOf(key)
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
add(value: T) {
|
|
438
|
+
const key = generateKey(value)
|
|
439
|
+
if (signals.has(key))
|
|
440
|
+
throw new DuplicateKeyError(TYPE_LIST, key, value)
|
|
441
|
+
if (!keys.includes(key)) keys.push(key)
|
|
442
|
+
validateSignalValue(`${TYPE_LIST} item for key "${key}"`, value)
|
|
443
|
+
signals.set(key, createState(value))
|
|
444
|
+
node.sources = null
|
|
445
|
+
node.sourcesTail = null
|
|
446
|
+
node.flags |= FLAG_DIRTY
|
|
447
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
448
|
+
if (batchDepth === 0) flush()
|
|
449
|
+
return key
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
remove(keyOrIndex: string | number) {
|
|
453
|
+
const key =
|
|
454
|
+
typeof keyOrIndex === 'number' ? keys[keyOrIndex] : keyOrIndex
|
|
455
|
+
const ok = signals.delete(key)
|
|
456
|
+
if (ok) {
|
|
457
|
+
const index =
|
|
458
|
+
typeof keyOrIndex === 'number'
|
|
459
|
+
? keyOrIndex
|
|
460
|
+
: keys.indexOf(key)
|
|
461
|
+
if (index >= 0) keys.splice(index, 1)
|
|
462
|
+
node.sources = null
|
|
463
|
+
node.sourcesTail = null
|
|
464
|
+
node.flags |= FLAG_DIRTY
|
|
465
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
466
|
+
if (batchDepth === 0) flush()
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
|
|
470
|
+
sort(compareFn?: (a: T, b: T) => number) {
|
|
471
|
+
const entries = keys
|
|
472
|
+
.map(key => [key, signals.get(key)?.get()] as [string, T])
|
|
473
|
+
.sort(
|
|
474
|
+
isFunction(compareFn)
|
|
475
|
+
? (a, b) => compareFn(a[1], b[1])
|
|
476
|
+
: (a, b) => String(a[1]).localeCompare(String(b[1])),
|
|
477
|
+
)
|
|
478
|
+
const newOrder = entries.map(([key]) => key)
|
|
479
|
+
|
|
480
|
+
if (!keysEqual(keys, newOrder)) {
|
|
481
|
+
keys = newOrder
|
|
482
|
+
node.flags |= FLAG_DIRTY
|
|
483
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
484
|
+
if (batchDepth === 0) flush()
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
|
|
488
|
+
splice(start: number, deleteCount?: number, ...items: T[]) {
|
|
489
|
+
const length = keys.length
|
|
490
|
+
const actualStart =
|
|
491
|
+
start < 0
|
|
492
|
+
? Math.max(0, length + start)
|
|
493
|
+
: Math.min(start, length)
|
|
494
|
+
const actualDeleteCount = Math.max(
|
|
495
|
+
0,
|
|
496
|
+
Math.min(
|
|
497
|
+
deleteCount ??
|
|
498
|
+
Math.max(0, length - Math.max(0, actualStart)),
|
|
499
|
+
length - actualStart,
|
|
500
|
+
),
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
const add = {} as Record<string, T>
|
|
504
|
+
const remove = {} as Record<string, T>
|
|
505
|
+
|
|
506
|
+
// Collect items to delete
|
|
507
|
+
for (let i = 0; i < actualDeleteCount; i++) {
|
|
508
|
+
const index = actualStart + i
|
|
509
|
+
const key = keys[index]
|
|
510
|
+
if (key) {
|
|
511
|
+
const signal = signals.get(key)
|
|
512
|
+
if (signal) remove[key] = signal.get() as T
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Build new key order
|
|
517
|
+
const newOrder = keys.slice(0, actualStart)
|
|
518
|
+
|
|
519
|
+
for (const item of items) {
|
|
520
|
+
const key = generateKey(item)
|
|
521
|
+
if (signals.has(key) && !(key in remove))
|
|
522
|
+
throw new DuplicateKeyError(TYPE_LIST, key, item)
|
|
523
|
+
newOrder.push(key)
|
|
524
|
+
add[key] = item
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
newOrder.push(...keys.slice(actualStart + actualDeleteCount))
|
|
528
|
+
|
|
529
|
+
const changed = !!(
|
|
530
|
+
Object.keys(add).length || Object.keys(remove).length
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
if (changed) {
|
|
534
|
+
applyChanges({
|
|
535
|
+
add,
|
|
536
|
+
change: {},
|
|
537
|
+
remove,
|
|
538
|
+
changed,
|
|
539
|
+
})
|
|
540
|
+
keys = newOrder
|
|
541
|
+
node.flags |= FLAG_DIRTY
|
|
542
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
543
|
+
if (batchDepth === 0) flush()
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return Object.values(remove)
|
|
547
|
+
},
|
|
548
|
+
|
|
549
|
+
deriveCollection<R extends {}>(
|
|
550
|
+
cb: DeriveCollectionCallback<R, T>,
|
|
551
|
+
): Collection<R> {
|
|
552
|
+
return (
|
|
553
|
+
deriveCollection as <T2 extends {}, U2 extends {}>(
|
|
554
|
+
source: CollectionSource<U2>,
|
|
555
|
+
callback: DeriveCollectionCallback<T2, U2>,
|
|
556
|
+
) => Collection<T2>
|
|
557
|
+
)(list, cb)
|
|
558
|
+
},
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return list
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Checks if a value is a List signal.
|
|
566
|
+
*
|
|
567
|
+
* @since 0.15.0
|
|
568
|
+
* @param value - The value to check
|
|
569
|
+
* @returns True if the value is a List
|
|
570
|
+
*/
|
|
571
|
+
function isList<T extends {}>(value: unknown): value is List<T> {
|
|
572
|
+
return isObjectOfType(value, TYPE_LIST)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/* === Exports === */
|
|
576
|
+
|
|
577
|
+
export {
|
|
578
|
+
type DiffResult,
|
|
579
|
+
type KeyConfig,
|
|
580
|
+
type List,
|
|
581
|
+
type ListOptions,
|
|
582
|
+
type UnknownRecord,
|
|
583
|
+
createList,
|
|
584
|
+
isEqual,
|
|
585
|
+
isList,
|
|
586
|
+
getKeyGenerator,
|
|
587
|
+
keysEqual,
|
|
588
|
+
TYPE_LIST,
|
|
589
|
+
}
|