@zeix/cause-effect 0.18.0 → 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 +14 -3
- package/.github/copilot-instructions.md +15 -5
- package/ARCHITECTURE.md +15 -13
- package/CHANGELOG.md +29 -0
- package/CLAUDE.md +9 -7
- package/README.md +23 -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 +13 -2
- package/src/nodes/collection.ts +166 -128
- package/src/nodes/list.ts +105 -104
- 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 +55 -60
- package/src/nodes/task.ts +31 -3
- package/test/collection.test.ts +40 -51
- 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
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
TYPE_STORE,
|
|
16
16
|
untrack,
|
|
17
17
|
} from '../graph'
|
|
18
|
-
import {
|
|
18
|
+
import { isObjectOfType, isRecord } from '../util'
|
|
19
19
|
import {
|
|
20
20
|
createList,
|
|
21
21
|
type DiffResult,
|
|
@@ -51,8 +51,8 @@ type BaseStore<T extends UnknownRecord> = {
|
|
|
51
51
|
? State<T[K] & {}>
|
|
52
52
|
: State<T[K] & {}> | undefined
|
|
53
53
|
get(): T
|
|
54
|
-
set(
|
|
55
|
-
update(fn: (
|
|
54
|
+
set(next: T): void
|
|
55
|
+
update(fn: (prev: T) => T): void
|
|
56
56
|
add<K extends keyof T & string>(key: K, value: T[K]): K
|
|
57
57
|
remove(key: string): void
|
|
58
58
|
}
|
|
@@ -70,21 +70,18 @@ type Store<T extends UnknownRecord> = BaseStore<T> & {
|
|
|
70
70
|
/* === Functions === */
|
|
71
71
|
|
|
72
72
|
/** Diff two records and return granular changes */
|
|
73
|
-
function diffRecords<T extends UnknownRecord>(
|
|
74
|
-
oldObj: T,
|
|
75
|
-
newObj: T,
|
|
76
|
-
): DiffResult {
|
|
73
|
+
function diffRecords<T extends UnknownRecord>(prev: T, next: T): DiffResult {
|
|
77
74
|
// Guard against non-objects that can't be diffed properly with Object.keys and 'in' operator
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
if (!
|
|
75
|
+
const prevValid = isRecord(prev) || Array.isArray(prev)
|
|
76
|
+
const nextValid = isRecord(next) || Array.isArray(next)
|
|
77
|
+
if (!prevValid || !nextValid) {
|
|
81
78
|
// For non-objects or non-plain objects, treat as complete change if different
|
|
82
|
-
const changed = !Object.is(
|
|
79
|
+
const changed = !Object.is(prev, next)
|
|
83
80
|
return {
|
|
84
81
|
changed,
|
|
85
|
-
add: changed &&
|
|
82
|
+
add: changed && nextValid ? next : {},
|
|
86
83
|
change: {},
|
|
87
|
-
remove: changed &&
|
|
84
|
+
remove: changed && prevValid ? prev : {},
|
|
88
85
|
}
|
|
89
86
|
}
|
|
90
87
|
|
|
@@ -95,25 +92,25 @@ function diffRecords<T extends UnknownRecord>(
|
|
|
95
92
|
const remove = {} as UnknownRecord
|
|
96
93
|
let changed = false
|
|
97
94
|
|
|
98
|
-
const
|
|
99
|
-
const
|
|
95
|
+
const prevKeys = Object.keys(prev)
|
|
96
|
+
const nextKeys = Object.keys(next)
|
|
100
97
|
|
|
101
98
|
// Pass 1: iterate new keys — find additions and changes
|
|
102
|
-
for (const key of
|
|
103
|
-
if (key in
|
|
104
|
-
if (!isEqual(
|
|
105
|
-
change[key] =
|
|
99
|
+
for (const key of nextKeys) {
|
|
100
|
+
if (key in prev) {
|
|
101
|
+
if (!isEqual(prev[key], next[key], visited)) {
|
|
102
|
+
change[key] = next[key]
|
|
106
103
|
changed = true
|
|
107
104
|
}
|
|
108
105
|
} else {
|
|
109
|
-
add[key] =
|
|
106
|
+
add[key] = next[key]
|
|
110
107
|
changed = true
|
|
111
108
|
}
|
|
112
109
|
}
|
|
113
110
|
|
|
114
111
|
// Pass 2: iterate old keys — find removals
|
|
115
|
-
for (const key of
|
|
116
|
-
if (!(key in
|
|
112
|
+
for (const key of prevKeys) {
|
|
113
|
+
if (!(key in next)) {
|
|
117
114
|
remove[key] = undefined
|
|
118
115
|
changed = true
|
|
119
116
|
}
|
|
@@ -128,7 +125,7 @@ function diffRecords<T extends UnknownRecord>(
|
|
|
128
125
|
* Properties are accessible directly via proxy.
|
|
129
126
|
*
|
|
130
127
|
* @since 0.15.0
|
|
131
|
-
* @param
|
|
128
|
+
* @param value - Initial object value of the store
|
|
132
129
|
* @param options - Optional configuration for watch lifecycle
|
|
133
130
|
* @returns A Store with reactive properties
|
|
134
131
|
*
|
|
@@ -140,10 +137,10 @@ function diffRecords<T extends UnknownRecord>(
|
|
|
140
137
|
* ```
|
|
141
138
|
*/
|
|
142
139
|
function createStore<T extends UnknownRecord>(
|
|
143
|
-
|
|
140
|
+
value: T,
|
|
144
141
|
options?: StoreOptions,
|
|
145
142
|
): Store<T> {
|
|
146
|
-
validateSignalValue(TYPE_STORE,
|
|
143
|
+
validateSignalValue(TYPE_STORE, value, isRecord)
|
|
147
144
|
|
|
148
145
|
const signals = new Map<
|
|
149
146
|
string,
|
|
@@ -152,11 +149,11 @@ function createStore<T extends UnknownRecord>(
|
|
|
152
149
|
|
|
153
150
|
// --- Internal helpers ---
|
|
154
151
|
|
|
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(
|
|
152
|
+
const addSignal = (key: string, val: unknown): void => {
|
|
153
|
+
validateSignalValue(`${TYPE_STORE} for key "${key}"`, val)
|
|
154
|
+
if (Array.isArray(val)) signals.set(key, createList(val))
|
|
155
|
+
else if (isRecord(val)) signals.set(key, createStore(val))
|
|
156
|
+
else signals.set(key, createState(val as unknown & {}))
|
|
160
157
|
}
|
|
161
158
|
|
|
162
159
|
// Build current value from child signals
|
|
@@ -174,7 +171,7 @@ function createStore<T extends UnknownRecord>(
|
|
|
174
171
|
// Mutation methods (add/remove/set) null out sources to force re-establishment.
|
|
175
172
|
const node: MemoNode<T> = {
|
|
176
173
|
fn: buildValue,
|
|
177
|
-
value
|
|
174
|
+
value,
|
|
178
175
|
flags: FLAG_DIRTY,
|
|
179
176
|
sources: null,
|
|
180
177
|
sourcesTail: null,
|
|
@@ -197,15 +194,15 @@ function createStore<T extends UnknownRecord>(
|
|
|
197
194
|
if (Object.keys(changes.change).length) {
|
|
198
195
|
batch(() => {
|
|
199
196
|
for (const key in changes.change) {
|
|
200
|
-
const
|
|
201
|
-
validateSignalValue(`${TYPE_STORE} for key "${key}"`,
|
|
197
|
+
const val = changes.change[key]
|
|
198
|
+
validateSignalValue(`${TYPE_STORE} for key "${key}"`, val)
|
|
202
199
|
const signal = signals.get(key)
|
|
203
200
|
if (signal) {
|
|
204
201
|
// Type changed (e.g. primitive → object or vice versa): replace signal
|
|
205
|
-
if (isRecord(
|
|
206
|
-
addSignal(key,
|
|
202
|
+
if (isRecord(val) !== isStore(signal)) {
|
|
203
|
+
addSignal(key, val)
|
|
207
204
|
structural = true
|
|
208
|
-
} else signal.set(
|
|
205
|
+
} else signal.set(val as never)
|
|
209
206
|
}
|
|
210
207
|
}
|
|
211
208
|
})
|
|
@@ -225,9 +222,20 @@ function createStore<T extends UnknownRecord>(
|
|
|
225
222
|
return changes.changed
|
|
226
223
|
}
|
|
227
224
|
|
|
225
|
+
const watched = options?.watched
|
|
226
|
+
const subscribe = watched
|
|
227
|
+
? () => {
|
|
228
|
+
if (activeSink) {
|
|
229
|
+
if (!node.sinks) node.stop = watched()
|
|
230
|
+
link(node, activeSink)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
: () => {
|
|
234
|
+
if (activeSink) link(node, activeSink)
|
|
235
|
+
}
|
|
236
|
+
|
|
228
237
|
// --- Initialize ---
|
|
229
|
-
for (const key of Object.keys(
|
|
230
|
-
addSignal(key, initialValue[key])
|
|
238
|
+
for (const key of Object.keys(value)) addSignal(key, value[key])
|
|
231
239
|
|
|
232
240
|
// --- Store object ---
|
|
233
241
|
const store: BaseStore<T> = {
|
|
@@ -250,11 +258,7 @@ function createStore<T extends UnknownRecord>(
|
|
|
250
258
|
},
|
|
251
259
|
|
|
252
260
|
keys() {
|
|
253
|
-
|
|
254
|
-
if (!node.sinks && options?.watched)
|
|
255
|
-
node.stop = options.watched()
|
|
256
|
-
link(node, activeSink)
|
|
257
|
-
}
|
|
261
|
+
subscribe()
|
|
258
262
|
return signals.keys()
|
|
259
263
|
},
|
|
260
264
|
|
|
@@ -270,11 +274,7 @@ function createStore<T extends UnknownRecord>(
|
|
|
270
274
|
},
|
|
271
275
|
|
|
272
276
|
get() {
|
|
273
|
-
|
|
274
|
-
if (!node.sinks && options?.watched)
|
|
275
|
-
node.stop = options.watched()
|
|
276
|
-
link(node, activeSink)
|
|
277
|
-
}
|
|
277
|
+
subscribe()
|
|
278
278
|
if (node.sources) {
|
|
279
279
|
// Fast path: edges already established, rebuild value directly
|
|
280
280
|
// from child signals using untrack to avoid creating spurious
|
|
@@ -291,16 +291,14 @@ function createStore<T extends UnknownRecord>(
|
|
|
291
291
|
return node.value
|
|
292
292
|
},
|
|
293
293
|
|
|
294
|
-
set(
|
|
294
|
+
set(next: T) {
|
|
295
295
|
// Use cached value if clean, recompute if dirty
|
|
296
|
-
const
|
|
297
|
-
node.flags & FLAG_DIRTY ? buildValue() : node.value
|
|
296
|
+
const prev = node.flags & FLAG_DIRTY ? buildValue() : node.value
|
|
298
297
|
|
|
299
|
-
const changes = diffRecords(
|
|
298
|
+
const changes = diffRecords(prev, next)
|
|
300
299
|
if (applyChanges(changes)) {
|
|
301
|
-
// Call propagate BEFORE marking dirty to ensure it doesn't early-return
|
|
302
|
-
propagate(node as unknown as SinkNode)
|
|
303
300
|
node.flags |= FLAG_DIRTY
|
|
301
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
304
302
|
if (batchDepth === 0) flush()
|
|
305
303
|
}
|
|
306
304
|
},
|
|
@@ -315,8 +313,8 @@ function createStore<T extends UnknownRecord>(
|
|
|
315
313
|
addSignal(key, value)
|
|
316
314
|
node.sources = null
|
|
317
315
|
node.sourcesTail = null
|
|
318
|
-
propagate(node as unknown as SinkNode)
|
|
319
316
|
node.flags |= FLAG_DIRTY
|
|
317
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
320
318
|
if (batchDepth === 0) flush()
|
|
321
319
|
return key
|
|
322
320
|
},
|
|
@@ -326,8 +324,8 @@ function createStore<T extends UnknownRecord>(
|
|
|
326
324
|
if (ok) {
|
|
327
325
|
node.sources = null
|
|
328
326
|
node.sourcesTail = null
|
|
329
|
-
propagate(node as unknown as SinkNode)
|
|
330
327
|
node.flags |= FLAG_DIRTY
|
|
328
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
331
329
|
if (batchDepth === 0) flush()
|
|
332
330
|
}
|
|
333
331
|
},
|
|
@@ -336,10 +334,7 @@ function createStore<T extends UnknownRecord>(
|
|
|
336
334
|
// --- Proxy ---
|
|
337
335
|
return new Proxy(store, {
|
|
338
336
|
get(target, prop) {
|
|
339
|
-
if (prop in target)
|
|
340
|
-
const value = Reflect.get(target, prop)
|
|
341
|
-
return isFunction(value) ? value.bind(target) : value
|
|
342
|
-
}
|
|
337
|
+
if (prop in target) return Reflect.get(target, prop)
|
|
343
338
|
if (typeof prop !== 'symbol')
|
|
344
339
|
return target.byKey(prop as keyof T & string)
|
|
345
340
|
},
|
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)
|