@zeix/cause-effect 0.18.0 → 0.18.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/.ai-context.md +14 -3
- package/.github/copilot-instructions.md +15 -5
- package/ARCHITECTURE.md +43 -19
- package/CHANGELOG.md +43 -0
- package/CLAUDE.md +33 -7
- package/GUIDE.md +1 -1
- package/README.md +36 -5
- package/context7.json +4 -0
- package/examples/events-sensor.ts +187 -0
- package/examples/selector-sensor.ts +173 -0
- package/index.dev.js +276 -222
- package/index.js +1 -1
- package/index.ts +4 -2
- package/package.json +2 -2
- package/skills/changelog-keeper/SKILL.md +59 -0
- package/skills/changelog-keeper/agents/openai.yaml +4 -0
- package/src/graph.ts +28 -5
- package/src/nodes/collection.ts +185 -133
- package/src/nodes/list.ts +121 -116
- package/src/nodes/memo.ts +31 -3
- package/src/nodes/sensor.ts +27 -17
- package/src/nodes/state.ts +2 -2
- package/src/nodes/store.ts +71 -72
- package/src/nodes/task.ts +31 -3
- package/test/collection.test.ts +40 -51
- package/test/list.test.ts +192 -0
- package/test/memo.test.ts +194 -0
- package/test/task.test.ts +134 -0
- package/types/index.d.ts +5 -5
- package/types/src/graph.d.ts +12 -2
- package/types/src/nodes/collection.d.ts +12 -7
- package/types/src/nodes/list.d.ts +12 -11
- package/types/src/nodes/memo.d.ts +6 -0
- package/types/src/nodes/sensor.d.ts +15 -9
- package/types/src/nodes/store.d.ts +4 -4
- package/types/src/nodes/task.d.ts +6 -0
- package/COLLECTION_REFACTORING.md +0 -161
package/src/nodes/store.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type Cleanup,
|
|
7
7
|
FLAG_CLEAN,
|
|
8
8
|
FLAG_DIRTY,
|
|
9
|
+
FLAG_RELINK,
|
|
9
10
|
flush,
|
|
10
11
|
link,
|
|
11
12
|
type MemoNode,
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
TYPE_STORE,
|
|
16
17
|
untrack,
|
|
17
18
|
} from '../graph'
|
|
18
|
-
import {
|
|
19
|
+
import { isObjectOfType, isRecord } from '../util'
|
|
19
20
|
import {
|
|
20
21
|
createList,
|
|
21
22
|
type DiffResult,
|
|
@@ -51,8 +52,8 @@ type BaseStore<T extends UnknownRecord> = {
|
|
|
51
52
|
? State<T[K] & {}>
|
|
52
53
|
: State<T[K] & {}> | undefined
|
|
53
54
|
get(): T
|
|
54
|
-
set(
|
|
55
|
-
update(fn: (
|
|
55
|
+
set(next: T): void
|
|
56
|
+
update(fn: (prev: T) => T): void
|
|
56
57
|
add<K extends keyof T & string>(key: K, value: T[K]): K
|
|
57
58
|
remove(key: string): void
|
|
58
59
|
}
|
|
@@ -70,21 +71,18 @@ type Store<T extends UnknownRecord> = BaseStore<T> & {
|
|
|
70
71
|
/* === Functions === */
|
|
71
72
|
|
|
72
73
|
/** Diff two records and return granular changes */
|
|
73
|
-
function diffRecords<T extends UnknownRecord>(
|
|
74
|
-
oldObj: T,
|
|
75
|
-
newObj: T,
|
|
76
|
-
): DiffResult {
|
|
74
|
+
function diffRecords<T extends UnknownRecord>(prev: T, next: T): DiffResult {
|
|
77
75
|
// Guard against non-objects that can't be diffed properly with Object.keys and 'in' operator
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
if (!
|
|
76
|
+
const prevValid = isRecord(prev) || Array.isArray(prev)
|
|
77
|
+
const nextValid = isRecord(next) || Array.isArray(next)
|
|
78
|
+
if (!prevValid || !nextValid) {
|
|
81
79
|
// For non-objects or non-plain objects, treat as complete change if different
|
|
82
|
-
const changed = !Object.is(
|
|
80
|
+
const changed = !Object.is(prev, next)
|
|
83
81
|
return {
|
|
84
82
|
changed,
|
|
85
|
-
add: changed &&
|
|
83
|
+
add: changed && nextValid ? next : {},
|
|
86
84
|
change: {},
|
|
87
|
-
remove: changed &&
|
|
85
|
+
remove: changed && prevValid ? prev : {},
|
|
88
86
|
}
|
|
89
87
|
}
|
|
90
88
|
|
|
@@ -95,25 +93,25 @@ function diffRecords<T extends UnknownRecord>(
|
|
|
95
93
|
const remove = {} as UnknownRecord
|
|
96
94
|
let changed = false
|
|
97
95
|
|
|
98
|
-
const
|
|
99
|
-
const
|
|
96
|
+
const prevKeys = Object.keys(prev)
|
|
97
|
+
const nextKeys = Object.keys(next)
|
|
100
98
|
|
|
101
99
|
// Pass 1: iterate new keys — find additions and changes
|
|
102
|
-
for (const key of
|
|
103
|
-
if (key in
|
|
104
|
-
if (!isEqual(
|
|
105
|
-
change[key] =
|
|
100
|
+
for (const key of nextKeys) {
|
|
101
|
+
if (key in prev) {
|
|
102
|
+
if (!isEqual(prev[key], next[key], visited)) {
|
|
103
|
+
change[key] = next[key]
|
|
106
104
|
changed = true
|
|
107
105
|
}
|
|
108
106
|
} else {
|
|
109
|
-
add[key] =
|
|
107
|
+
add[key] = next[key]
|
|
110
108
|
changed = true
|
|
111
109
|
}
|
|
112
110
|
}
|
|
113
111
|
|
|
114
112
|
// Pass 2: iterate old keys — find removals
|
|
115
|
-
for (const key of
|
|
116
|
-
if (!(key in
|
|
113
|
+
for (const key of prevKeys) {
|
|
114
|
+
if (!(key in next)) {
|
|
117
115
|
remove[key] = undefined
|
|
118
116
|
changed = true
|
|
119
117
|
}
|
|
@@ -128,7 +126,7 @@ function diffRecords<T extends UnknownRecord>(
|
|
|
128
126
|
* Properties are accessible directly via proxy.
|
|
129
127
|
*
|
|
130
128
|
* @since 0.15.0
|
|
131
|
-
* @param
|
|
129
|
+
* @param value - Initial object value of the store
|
|
132
130
|
* @param options - Optional configuration for watch lifecycle
|
|
133
131
|
* @returns A Store with reactive properties
|
|
134
132
|
*
|
|
@@ -140,10 +138,10 @@ function diffRecords<T extends UnknownRecord>(
|
|
|
140
138
|
* ```
|
|
141
139
|
*/
|
|
142
140
|
function createStore<T extends UnknownRecord>(
|
|
143
|
-
|
|
141
|
+
value: T,
|
|
144
142
|
options?: StoreOptions,
|
|
145
143
|
): Store<T> {
|
|
146
|
-
validateSignalValue(TYPE_STORE,
|
|
144
|
+
validateSignalValue(TYPE_STORE, value, isRecord)
|
|
147
145
|
|
|
148
146
|
const signals = new Map<
|
|
149
147
|
string,
|
|
@@ -152,11 +150,11 @@ function createStore<T extends UnknownRecord>(
|
|
|
152
150
|
|
|
153
151
|
// --- Internal helpers ---
|
|
154
152
|
|
|
155
|
-
const addSignal = (key: string,
|
|
156
|
-
validateSignalValue(`${TYPE_STORE} for key "${key}"`,
|
|
157
|
-
if (Array.isArray(
|
|
158
|
-
else if (isRecord(
|
|
159
|
-
else signals.set(key, createState(
|
|
153
|
+
const addSignal = (key: string, val: unknown): void => {
|
|
154
|
+
validateSignalValue(`${TYPE_STORE} for key "${key}"`, val)
|
|
155
|
+
if (Array.isArray(val)) signals.set(key, createList(val))
|
|
156
|
+
else if (isRecord(val)) signals.set(key, createStore(val))
|
|
157
|
+
else signals.set(key, createState(val as unknown & {}))
|
|
160
158
|
}
|
|
161
159
|
|
|
162
160
|
// Build current value from child signals
|
|
@@ -171,10 +169,10 @@ function createStore<T extends UnknownRecord>(
|
|
|
171
169
|
// Structural tracking node — not a general-purpose Memo.
|
|
172
170
|
// On first get(): refresh() establishes edges from child signals.
|
|
173
171
|
// On subsequent get(): untrack(buildValue) rebuilds without re-linking.
|
|
174
|
-
// Mutation methods
|
|
172
|
+
// Mutation methods set FLAG_RELINK to force re-establishment on next read.
|
|
175
173
|
const node: MemoNode<T> = {
|
|
176
174
|
fn: buildValue,
|
|
177
|
-
value
|
|
175
|
+
value,
|
|
178
176
|
flags: FLAG_DIRTY,
|
|
179
177
|
sources: null,
|
|
180
178
|
sourcesTail: null,
|
|
@@ -197,15 +195,15 @@ function createStore<T extends UnknownRecord>(
|
|
|
197
195
|
if (Object.keys(changes.change).length) {
|
|
198
196
|
batch(() => {
|
|
199
197
|
for (const key in changes.change) {
|
|
200
|
-
const
|
|
201
|
-
validateSignalValue(`${TYPE_STORE} for key "${key}"`,
|
|
198
|
+
const val = changes.change[key]
|
|
199
|
+
validateSignalValue(`${TYPE_STORE} for key "${key}"`, val)
|
|
202
200
|
const signal = signals.get(key)
|
|
203
201
|
if (signal) {
|
|
204
202
|
// Type changed (e.g. primitive → object or vice versa): replace signal
|
|
205
|
-
if (isRecord(
|
|
206
|
-
addSignal(key,
|
|
203
|
+
if (isRecord(val) !== isStore(signal)) {
|
|
204
|
+
addSignal(key, val)
|
|
207
205
|
structural = true
|
|
208
|
-
} else signal.set(
|
|
206
|
+
} else signal.set(val as never)
|
|
209
207
|
}
|
|
210
208
|
}
|
|
211
209
|
})
|
|
@@ -217,17 +215,25 @@ function createStore<T extends UnknownRecord>(
|
|
|
217
215
|
structural = true
|
|
218
216
|
}
|
|
219
217
|
|
|
220
|
-
if (structural)
|
|
221
|
-
node.sources = null
|
|
222
|
-
node.sourcesTail = null
|
|
223
|
-
}
|
|
218
|
+
if (structural) node.flags |= FLAG_RELINK
|
|
224
219
|
|
|
225
220
|
return changes.changed
|
|
226
221
|
}
|
|
227
222
|
|
|
223
|
+
const watched = options?.watched
|
|
224
|
+
const subscribe = watched
|
|
225
|
+
? () => {
|
|
226
|
+
if (activeSink) {
|
|
227
|
+
if (!node.sinks) node.stop = watched()
|
|
228
|
+
link(node, activeSink)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
: () => {
|
|
232
|
+
if (activeSink) link(node, activeSink)
|
|
233
|
+
}
|
|
234
|
+
|
|
228
235
|
// --- Initialize ---
|
|
229
|
-
for (const key of Object.keys(
|
|
230
|
-
addSignal(key, initialValue[key])
|
|
236
|
+
for (const key of Object.keys(value)) addSignal(key, value[key])
|
|
231
237
|
|
|
232
238
|
// --- Store object ---
|
|
233
239
|
const store: BaseStore<T> = {
|
|
@@ -250,11 +256,7 @@ function createStore<T extends UnknownRecord>(
|
|
|
250
256
|
},
|
|
251
257
|
|
|
252
258
|
keys() {
|
|
253
|
-
|
|
254
|
-
if (!node.sinks && options?.watched)
|
|
255
|
-
node.stop = options.watched()
|
|
256
|
-
link(node, activeSink)
|
|
257
|
-
}
|
|
259
|
+
subscribe()
|
|
258
260
|
return signals.keys()
|
|
259
261
|
},
|
|
260
262
|
|
|
@@ -270,18 +272,24 @@ function createStore<T extends UnknownRecord>(
|
|
|
270
272
|
},
|
|
271
273
|
|
|
272
274
|
get() {
|
|
273
|
-
|
|
274
|
-
if (!node.sinks && options?.watched)
|
|
275
|
-
node.stop = options.watched()
|
|
276
|
-
link(node, activeSink)
|
|
277
|
-
}
|
|
275
|
+
subscribe()
|
|
278
276
|
if (node.sources) {
|
|
279
277
|
// Fast path: edges already established, rebuild value directly
|
|
280
278
|
// from child signals using untrack to avoid creating spurious
|
|
281
279
|
// edges to the current effect/memo consumer
|
|
282
280
|
if (node.flags) {
|
|
281
|
+
const relink = node.flags & FLAG_RELINK
|
|
283
282
|
node.value = untrack(buildValue)
|
|
284
|
-
|
|
283
|
+
if (relink) {
|
|
284
|
+
// Structural mutation added/removed child signals —
|
|
285
|
+
// tracked recompute so link() adds new edges and
|
|
286
|
+
// trimSources() removes stale ones without orphaning.
|
|
287
|
+
node.flags = FLAG_DIRTY
|
|
288
|
+
refresh(node as unknown as SinkNode)
|
|
289
|
+
if (node.error) throw node.error
|
|
290
|
+
} else {
|
|
291
|
+
node.flags = FLAG_CLEAN
|
|
292
|
+
}
|
|
285
293
|
}
|
|
286
294
|
} else {
|
|
287
295
|
// First access: use refresh() to establish child → store edges
|
|
@@ -291,16 +299,14 @@ function createStore<T extends UnknownRecord>(
|
|
|
291
299
|
return node.value
|
|
292
300
|
},
|
|
293
301
|
|
|
294
|
-
set(
|
|
302
|
+
set(next: T) {
|
|
295
303
|
// Use cached value if clean, recompute if dirty
|
|
296
|
-
const
|
|
297
|
-
node.flags & FLAG_DIRTY ? buildValue() : node.value
|
|
304
|
+
const prev = node.flags & FLAG_DIRTY ? buildValue() : node.value
|
|
298
305
|
|
|
299
|
-
const changes = diffRecords(
|
|
306
|
+
const changes = diffRecords(prev, next)
|
|
300
307
|
if (applyChanges(changes)) {
|
|
301
|
-
// Call propagate BEFORE marking dirty to ensure it doesn't early-return
|
|
302
|
-
propagate(node as unknown as SinkNode)
|
|
303
308
|
node.flags |= FLAG_DIRTY
|
|
309
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
304
310
|
if (batchDepth === 0) flush()
|
|
305
311
|
}
|
|
306
312
|
},
|
|
@@ -313,10 +319,8 @@ function createStore<T extends UnknownRecord>(
|
|
|
313
319
|
if (signals.has(key))
|
|
314
320
|
throw new DuplicateKeyError(TYPE_STORE, key, value)
|
|
315
321
|
addSignal(key, value)
|
|
316
|
-
node.
|
|
317
|
-
node.
|
|
318
|
-
propagate(node as unknown as SinkNode)
|
|
319
|
-
node.flags |= FLAG_DIRTY
|
|
322
|
+
node.flags |= FLAG_DIRTY | FLAG_RELINK
|
|
323
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
320
324
|
if (batchDepth === 0) flush()
|
|
321
325
|
return key
|
|
322
326
|
},
|
|
@@ -324,10 +328,8 @@ function createStore<T extends UnknownRecord>(
|
|
|
324
328
|
remove(key: string) {
|
|
325
329
|
const ok = signals.delete(key)
|
|
326
330
|
if (ok) {
|
|
327
|
-
node.
|
|
328
|
-
node.
|
|
329
|
-
propagate(node as unknown as SinkNode)
|
|
330
|
-
node.flags |= FLAG_DIRTY
|
|
331
|
+
node.flags |= FLAG_DIRTY | FLAG_RELINK
|
|
332
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
331
333
|
if (batchDepth === 0) flush()
|
|
332
334
|
}
|
|
333
335
|
},
|
|
@@ -336,10 +338,7 @@ function createStore<T extends UnknownRecord>(
|
|
|
336
338
|
// --- Proxy ---
|
|
337
339
|
return new Proxy(store, {
|
|
338
340
|
get(target, prop) {
|
|
339
|
-
if (prop in target)
|
|
340
|
-
const value = Reflect.get(target, prop)
|
|
341
|
-
return isFunction(value) ? value.bind(target) : value
|
|
342
|
-
}
|
|
341
|
+
if (prop in target) return Reflect.get(target, prop)
|
|
343
342
|
if (typeof prop !== 'symbol')
|
|
344
343
|
return target.byKey(prop as keyof T & string)
|
|
345
344
|
},
|
package/src/nodes/task.ts
CHANGED
|
@@ -5,10 +5,13 @@ import {
|
|
|
5
5
|
} from '../errors'
|
|
6
6
|
import {
|
|
7
7
|
activeSink,
|
|
8
|
+
batchDepth,
|
|
8
9
|
type ComputedOptions,
|
|
9
|
-
|
|
10
|
+
DEFAULT_EQUALITY,
|
|
10
11
|
FLAG_DIRTY,
|
|
12
|
+
flush,
|
|
11
13
|
link,
|
|
14
|
+
propagate,
|
|
12
15
|
refresh,
|
|
13
16
|
type SinkNode,
|
|
14
17
|
type TaskCallback,
|
|
@@ -62,6 +65,12 @@ type Task<T extends {}> = {
|
|
|
62
65
|
* @template T - The type of value resolved by the task
|
|
63
66
|
* @param fn - The async computation function that receives the previous value and an AbortSignal
|
|
64
67
|
* @param options - Optional configuration for the task
|
|
68
|
+
* @param options.value - Optional initial value for reducer patterns
|
|
69
|
+
* @param options.equals - Optional equality function. Defaults to strict equality (`===`)
|
|
70
|
+
* @param options.guard - Optional type guard to validate values
|
|
71
|
+
* @param options.watched - Optional callback invoked when the task is first watched by an effect.
|
|
72
|
+
* Receives an `invalidate` function to mark the task dirty and trigger re-execution.
|
|
73
|
+
* Must return a cleanup function called when no effects are watching.
|
|
65
74
|
* @returns A Task object with get(), isPending(), and abort() methods
|
|
66
75
|
*
|
|
67
76
|
* @example
|
|
@@ -108,15 +117,34 @@ function createTask<T extends {}>(
|
|
|
108
117
|
sinks: null,
|
|
109
118
|
sinksTail: null,
|
|
110
119
|
flags: FLAG_DIRTY,
|
|
111
|
-
equals: options?.equals ??
|
|
120
|
+
equals: options?.equals ?? DEFAULT_EQUALITY,
|
|
112
121
|
controller: undefined,
|
|
113
122
|
error: undefined,
|
|
123
|
+
stop: undefined,
|
|
114
124
|
}
|
|
115
125
|
|
|
126
|
+
const watched = options?.watched
|
|
127
|
+
const subscribe = watched
|
|
128
|
+
? () => {
|
|
129
|
+
if (activeSink) {
|
|
130
|
+
if (!node.sinks)
|
|
131
|
+
node.stop = watched(() => {
|
|
132
|
+
node.flags |= FLAG_DIRTY
|
|
133
|
+
for (let e = node.sinks; e; e = e.nextSink)
|
|
134
|
+
propagate(e.sink)
|
|
135
|
+
if (batchDepth === 0) flush()
|
|
136
|
+
})
|
|
137
|
+
link(node, activeSink)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
: () => {
|
|
141
|
+
if (activeSink) link(node, activeSink)
|
|
142
|
+
}
|
|
143
|
+
|
|
116
144
|
return {
|
|
117
145
|
[Symbol.toStringTag]: TYPE_TASK,
|
|
118
146
|
get(): T {
|
|
119
|
-
|
|
147
|
+
subscribe()
|
|
120
148
|
refresh(node as unknown as SinkNode)
|
|
121
149
|
if (node.error) throw node.error
|
|
122
150
|
validateReadValue(TYPE_TASK, node.value)
|
package/test/collection.test.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import {
|
|
3
3
|
batch,
|
|
4
|
+
type CollectionChanges,
|
|
4
5
|
createCollection,
|
|
5
6
|
createEffect,
|
|
6
7
|
createList,
|
|
7
8
|
createScope,
|
|
8
9
|
createState,
|
|
9
|
-
type DiffResult,
|
|
10
10
|
isCollection,
|
|
11
11
|
isList,
|
|
12
12
|
} from '../index.ts'
|
|
@@ -88,7 +88,7 @@ describe('Collection', () => {
|
|
|
88
88
|
let guardCalled = false
|
|
89
89
|
const col = createCollection(() => () => {}, {
|
|
90
90
|
value: [5, 10],
|
|
91
|
-
createItem:
|
|
91
|
+
createItem: value =>
|
|
92
92
|
createState(value, {
|
|
93
93
|
guard: (v): v is number => {
|
|
94
94
|
guardCalled = true
|
|
@@ -174,7 +174,9 @@ describe('Collection', () => {
|
|
|
174
174
|
|
|
175
175
|
describe('applyChanges', () => {
|
|
176
176
|
test('should add items', () => {
|
|
177
|
-
let apply:
|
|
177
|
+
let apply:
|
|
178
|
+
| ((changes: CollectionChanges<number>) => void)
|
|
179
|
+
| undefined
|
|
178
180
|
const col = createCollection<number>(applyChanges => {
|
|
179
181
|
apply = applyChanges
|
|
180
182
|
return () => {}
|
|
@@ -190,12 +192,7 @@ describe('Collection', () => {
|
|
|
190
192
|
expect(values).toEqual([[]])
|
|
191
193
|
|
|
192
194
|
// biome-ignore lint/style/noNonNullAssertion: test
|
|
193
|
-
apply!({
|
|
194
|
-
changed: true,
|
|
195
|
-
add: { a: 1, b: 2 },
|
|
196
|
-
change: {},
|
|
197
|
-
remove: {},
|
|
198
|
-
})
|
|
195
|
+
apply!({ add: [1, 2] })
|
|
199
196
|
|
|
200
197
|
expect(values.length).toBe(2)
|
|
201
198
|
expect(values[1]).toEqual([1, 2])
|
|
@@ -205,7 +202,11 @@ describe('Collection', () => {
|
|
|
205
202
|
})
|
|
206
203
|
|
|
207
204
|
test('should change item values', () => {
|
|
208
|
-
let apply:
|
|
205
|
+
let apply:
|
|
206
|
+
| ((
|
|
207
|
+
changes: CollectionChanges<{ id: string; val: number }>,
|
|
208
|
+
) => void)
|
|
209
|
+
| undefined
|
|
209
210
|
const col = createCollection(
|
|
210
211
|
applyChanges => {
|
|
211
212
|
apply = applyChanges
|
|
@@ -227,12 +228,7 @@ describe('Collection', () => {
|
|
|
227
228
|
expect(values[0]).toEqual([{ id: 'x', val: 1 }])
|
|
228
229
|
|
|
229
230
|
// biome-ignore lint/style/noNonNullAssertion: test
|
|
230
|
-
apply!({
|
|
231
|
-
changed: true,
|
|
232
|
-
add: {},
|
|
233
|
-
change: { x: { id: 'x', val: 42 } },
|
|
234
|
-
remove: {},
|
|
235
|
-
})
|
|
231
|
+
apply!({ change: [{ id: 'x', val: 42 }] })
|
|
236
232
|
|
|
237
233
|
expect(values.length).toBe(2)
|
|
238
234
|
expect(values[1]).toEqual([{ id: 'x', val: 42 }])
|
|
@@ -241,7 +237,11 @@ describe('Collection', () => {
|
|
|
241
237
|
})
|
|
242
238
|
|
|
243
239
|
test('should remove items', () => {
|
|
244
|
-
let apply:
|
|
240
|
+
let apply:
|
|
241
|
+
| ((
|
|
242
|
+
changes: CollectionChanges<{ id: string; v: number }>,
|
|
243
|
+
) => void)
|
|
244
|
+
| undefined
|
|
245
245
|
const col = createCollection(
|
|
246
246
|
applyChanges => {
|
|
247
247
|
apply = applyChanges
|
|
@@ -271,12 +271,7 @@ describe('Collection', () => {
|
|
|
271
271
|
])
|
|
272
272
|
|
|
273
273
|
// biome-ignore lint/style/noNonNullAssertion: test
|
|
274
|
-
apply!({
|
|
275
|
-
changed: true,
|
|
276
|
-
add: {},
|
|
277
|
-
change: {},
|
|
278
|
-
remove: { b: null },
|
|
279
|
-
})
|
|
274
|
+
apply!({ remove: [{ id: 'b', v: 2 }] })
|
|
280
275
|
|
|
281
276
|
expect(values.length).toBe(2)
|
|
282
277
|
expect(values[1]).toEqual([
|
|
@@ -289,7 +284,11 @@ describe('Collection', () => {
|
|
|
289
284
|
})
|
|
290
285
|
|
|
291
286
|
test('should handle mixed add/change/remove', () => {
|
|
292
|
-
let apply:
|
|
287
|
+
let apply:
|
|
288
|
+
| ((
|
|
289
|
+
changes: CollectionChanges<{ id: string; v: number }>,
|
|
290
|
+
) => void)
|
|
291
|
+
| undefined
|
|
293
292
|
const col = createCollection(
|
|
294
293
|
applyChanges => {
|
|
295
294
|
apply = applyChanges
|
|
@@ -313,10 +312,9 @@ describe('Collection', () => {
|
|
|
313
312
|
|
|
314
313
|
// biome-ignore lint/style/noNonNullAssertion: test
|
|
315
314
|
apply!({
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
remove: { b: null },
|
|
315
|
+
add: [{ id: 'c', v: 3 }],
|
|
316
|
+
change: [{ id: 'a', v: 10 }],
|
|
317
|
+
remove: [{ id: 'b', v: 2 }],
|
|
320
318
|
})
|
|
321
319
|
|
|
322
320
|
expect(values.length).toBe(2)
|
|
@@ -328,8 +326,10 @@ describe('Collection', () => {
|
|
|
328
326
|
dispose()
|
|
329
327
|
})
|
|
330
328
|
|
|
331
|
-
test('should skip when
|
|
332
|
-
let apply:
|
|
329
|
+
test('should skip when no changes provided', () => {
|
|
330
|
+
let apply:
|
|
331
|
+
| ((changes: CollectionChanges<number>) => void)
|
|
332
|
+
| undefined
|
|
333
333
|
const col = createCollection(
|
|
334
334
|
applyChanges => {
|
|
335
335
|
apply = applyChanges
|
|
@@ -349,7 +349,7 @@ describe('Collection', () => {
|
|
|
349
349
|
expect(callCount).toBe(1)
|
|
350
350
|
|
|
351
351
|
// biome-ignore lint/style/noNonNullAssertion: test
|
|
352
|
-
apply!({
|
|
352
|
+
apply!({})
|
|
353
353
|
|
|
354
354
|
expect(callCount).toBe(1)
|
|
355
355
|
|
|
@@ -357,7 +357,9 @@ describe('Collection', () => {
|
|
|
357
357
|
})
|
|
358
358
|
|
|
359
359
|
test('should trigger effects on structural changes', () => {
|
|
360
|
-
let apply:
|
|
360
|
+
let apply:
|
|
361
|
+
| ((changes: CollectionChanges<string>) => void)
|
|
362
|
+
| undefined
|
|
361
363
|
const col = createCollection<string>(applyChanges => {
|
|
362
364
|
apply = applyChanges
|
|
363
365
|
return () => {}
|
|
@@ -374,12 +376,7 @@ describe('Collection', () => {
|
|
|
374
376
|
expect(effectCount).toBe(1)
|
|
375
377
|
|
|
376
378
|
// biome-ignore lint/style/noNonNullAssertion: test
|
|
377
|
-
apply!({
|
|
378
|
-
changed: true,
|
|
379
|
-
add: { a: 'hello' },
|
|
380
|
-
change: {},
|
|
381
|
-
remove: {},
|
|
382
|
-
})
|
|
379
|
+
apply!({ add: ['hello'] })
|
|
383
380
|
|
|
384
381
|
expect(effectCount).toBe(2)
|
|
385
382
|
expect(col.length).toBe(1)
|
|
@@ -388,7 +385,9 @@ describe('Collection', () => {
|
|
|
388
385
|
})
|
|
389
386
|
|
|
390
387
|
test('should batch multiple calls', () => {
|
|
391
|
-
let apply:
|
|
388
|
+
let apply:
|
|
389
|
+
| ((changes: CollectionChanges<number>) => void)
|
|
390
|
+
| undefined
|
|
392
391
|
const col = createCollection<number>(applyChanges => {
|
|
393
392
|
apply = applyChanges
|
|
394
393
|
return () => {}
|
|
@@ -406,19 +405,9 @@ describe('Collection', () => {
|
|
|
406
405
|
|
|
407
406
|
batch(() => {
|
|
408
407
|
// biome-ignore lint/style/noNonNullAssertion: test
|
|
409
|
-
apply!({
|
|
410
|
-
changed: true,
|
|
411
|
-
add: { a: 1 },
|
|
412
|
-
change: {},
|
|
413
|
-
remove: {},
|
|
414
|
-
})
|
|
408
|
+
apply!({ add: [1] })
|
|
415
409
|
// biome-ignore lint/style/noNonNullAssertion: test
|
|
416
|
-
apply!({
|
|
417
|
-
changed: true,
|
|
418
|
-
add: { b: 2 },
|
|
419
|
-
change: {},
|
|
420
|
-
remove: {},
|
|
421
|
-
})
|
|
410
|
+
apply!({ add: [2] })
|
|
422
411
|
})
|
|
423
412
|
|
|
424
413
|
expect(effectCount).toBe(2)
|