@zeix/cause-effect 1.0.0 → 1.0.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/.github/copilot-instructions.md +5 -1
- package/.zed/settings.json +24 -1
- package/ARCHITECTURE.md +27 -1
- package/CHANGELOG.md +21 -0
- package/CLAUDE.md +1 -1
- package/GUIDE.md +2 -5
- package/README.md +45 -3
- package/REQUIREMENTS.md +3 -3
- package/eslint.config.js +2 -1
- package/index.dev.js +14 -0
- package/index.js +1 -1
- package/index.ts +1 -1
- package/package.json +5 -4
- package/skills/cause-effect/SKILL.md +69 -0
- package/skills/cause-effect/agents/openai.yaml +4 -0
- package/skills/cause-effect/references/api-facts.md +179 -0
- package/skills/cause-effect/references/error-classes.md +153 -0
- package/skills/cause-effect/references/non-obvious-behaviors.md +195 -0
- package/skills/cause-effect/references/signal-types.md +292 -0
- package/skills/cause-effect/workflows/answer-question.md +54 -0
- package/skills/cause-effect/workflows/debug.md +71 -0
- package/skills/cause-effect/workflows/use-api.md +63 -0
- package/skills/cause-effect-dev/SKILL.md +61 -100
- package/skills/cause-effect-dev/references/api-facts.md +96 -0
- package/skills/cause-effect-dev/references/error-classes.md +97 -0
- package/skills/cause-effect-dev/references/internal-types.md +54 -0
- package/skills/cause-effect-dev/references/non-obvious-behaviors.md +162 -0
- package/skills/cause-effect-dev/references/source-map.md +45 -0
- package/skills/cause-effect-dev/workflows/answer-question.md +55 -0
- package/skills/cause-effect-dev/workflows/fix-bug.md +63 -0
- package/skills/cause-effect-dev/workflows/implement-feature.md +46 -0
- package/skills/cause-effect-dev/workflows/write-tests.md +64 -0
- package/skills/changelog-keeper/SKILL.md +47 -37
- package/skills/tech-writer/SKILL.md +94 -0
- package/skills/tech-writer/references/document-map.md +199 -0
- package/skills/tech-writer/references/tone-guide.md +189 -0
- package/skills/tech-writer/workflows/consistency-review.md +98 -0
- package/skills/tech-writer/workflows/update-after-change.md +65 -0
- package/skills/tech-writer/workflows/update-agent-docs.md +77 -0
- package/skills/tech-writer/workflows/update-architecture.md +61 -0
- package/skills/tech-writer/workflows/update-jsdoc.md +72 -0
- package/skills/tech-writer/workflows/update-public-api.md +59 -0
- package/skills/tech-writer/workflows/update-requirements.md +80 -0
- package/src/graph.ts +2 -0
- package/src/nodes/collection.ts +38 -0
- package/src/nodes/effect.ts +13 -1
- package/src/nodes/list.ts +41 -2
- package/src/nodes/memo.ts +0 -1
- package/src/nodes/sensor.ts +10 -4
- package/src/nodes/store.ts +11 -0
- package/src/signal.ts +6 -0
- package/test/list.test.ts +121 -0
- package/tsconfig.json +9 -0
- package/types/index.d.ts +1 -1
- package/types/src/graph.d.ts +2 -0
- package/types/src/nodes/collection.d.ts +38 -0
- package/types/src/nodes/effect.d.ts +13 -1
- package/types/src/nodes/list.d.ts +30 -2
- package/types/src/nodes/memo.d.ts +0 -1
- package/types/src/nodes/sensor.d.ts +10 -4
- package/types/src/nodes/store.d.ts +11 -0
- package/types/src/signal.d.ts +6 -0
package/src/nodes/list.ts
CHANGED
|
@@ -40,13 +40,33 @@ type DiffResult = {
|
|
|
40
40
|
remove: UnknownRecord
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Key generation strategy for `createList` items.
|
|
45
|
+
* A string value is used as a prefix for auto-incremented keys (`prefix0`, `prefix1`, …).
|
|
46
|
+
* A function receives each item and returns a stable string key, or `undefined` to fall back to auto-increment.
|
|
47
|
+
*
|
|
48
|
+
* @template T - The type of items in the list
|
|
49
|
+
*/
|
|
43
50
|
type KeyConfig<T> = string | ((item: T) => string | undefined)
|
|
44
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Configuration options for `createList`.
|
|
54
|
+
*
|
|
55
|
+
* @template T - The type of items in the list
|
|
56
|
+
*/
|
|
45
57
|
type ListOptions<T extends {}> = {
|
|
58
|
+
/** Key generation strategy. A string prefix or a function `(item) => string | undefined`. Defaults to auto-increment. */
|
|
46
59
|
keyConfig?: KeyConfig<T>
|
|
60
|
+
/** Lifecycle callback invoked when the list gains its first downstream subscriber. Must return a cleanup function. */
|
|
47
61
|
watched?: () => Cleanup
|
|
48
62
|
}
|
|
49
63
|
|
|
64
|
+
/**
|
|
65
|
+
* A reactive ordered array with stable keys and per-item reactivity.
|
|
66
|
+
* Each item is a `State<T>` signal; structural changes (add/remove/sort) propagate reactively.
|
|
67
|
+
*
|
|
68
|
+
* @template T - The type of items in the list
|
|
69
|
+
*/
|
|
50
70
|
type List<T extends {}> = {
|
|
51
71
|
readonly [Symbol.toStringTag]: 'List'
|
|
52
72
|
readonly [Symbol.isConcatSpreadable]: true
|
|
@@ -62,6 +82,13 @@ type List<T extends {}> = {
|
|
|
62
82
|
indexOfKey(key: string): number
|
|
63
83
|
add(value: T): string
|
|
64
84
|
remove(keyOrIndex: string | number): void
|
|
85
|
+
/**
|
|
86
|
+
* Updates an existing item by key, propagating to all subscribers.
|
|
87
|
+
* No-op if the key does not exist or the value is reference-equal to the current value.
|
|
88
|
+
* @param key - Stable key of the item to update
|
|
89
|
+
* @param value - New value for the item
|
|
90
|
+
*/
|
|
91
|
+
replace(key: string, value: T): void
|
|
65
92
|
sort(compareFn?: (a: T, b: T) => number): void
|
|
66
93
|
splice(start: number, deleteCount?: number, ...items: T[]): T[]
|
|
67
94
|
deriveCollection<R extends {}>(
|
|
@@ -240,8 +267,9 @@ function diffArrays<T>(
|
|
|
240
267
|
*
|
|
241
268
|
* @since 0.18.0
|
|
242
269
|
* @param value - Initial array of items
|
|
243
|
-
* @param options -
|
|
244
|
-
* @
|
|
270
|
+
* @param options.keyConfig - Key generation strategy: string prefix or `(item) => string | undefined`. Defaults to auto-increment.
|
|
271
|
+
* @param options.watched - Lifecycle callback invoked on first subscriber; must return a cleanup function called on last unsubscribe.
|
|
272
|
+
* @returns A `List` signal with reactive per-item `State` signals
|
|
245
273
|
*/
|
|
246
274
|
function createList<T extends {}>(
|
|
247
275
|
value: T[],
|
|
@@ -474,6 +502,17 @@ function createList<T extends {}>(
|
|
|
474
502
|
}
|
|
475
503
|
},
|
|
476
504
|
|
|
505
|
+
replace(key: string, value: T) {
|
|
506
|
+
const signal = signals.get(key)
|
|
507
|
+
if (!signal) return
|
|
508
|
+
validateSignalValue(`${TYPE_LIST} item for key "${key}"`, value)
|
|
509
|
+
if (signal.get() === value) return
|
|
510
|
+
signal.set(value)
|
|
511
|
+
node.flags |= FLAG_DIRTY
|
|
512
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
513
|
+
if (batchDepth === 0) flush()
|
|
514
|
+
},
|
|
515
|
+
|
|
477
516
|
sort(compareFn?: (a: T, b: T) => number) {
|
|
478
517
|
const entries = keys
|
|
479
518
|
.map(key => [key, signals.get(key)?.get()] as [string, T])
|
package/src/nodes/memo.ts
CHANGED
|
@@ -36,7 +36,6 @@ type Memo<T extends {}> = {
|
|
|
36
36
|
* Recomputes if dependencies have changed since last access.
|
|
37
37
|
* When called inside another reactive context, creates a dependency.
|
|
38
38
|
* @returns The computed value
|
|
39
|
-
* @throws UnsetSignalValueError If the memo value is still unset when read.
|
|
40
39
|
*/
|
|
41
40
|
get(): T
|
|
42
41
|
}
|
package/src/nodes/sensor.ts
CHANGED
|
@@ -35,11 +35,9 @@ type Sensor<T extends {}> = {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
38
|
+
* Configuration options for `createSensor`.
|
|
39
39
|
*
|
|
40
|
-
* @template T - The type of value
|
|
41
|
-
* @param set - A function to set the observed value
|
|
42
|
-
* @returns A cleanup function when the sensor stops being watched
|
|
40
|
+
* @template T - The type of value produced by the sensor
|
|
43
41
|
*/
|
|
44
42
|
type SensorOptions<T extends {}> = SignalOptions<T> & {
|
|
45
43
|
/**
|
|
@@ -49,6 +47,14 @@ type SensorOptions<T extends {}> = SignalOptions<T> & {
|
|
|
49
47
|
value?: T
|
|
50
48
|
}
|
|
51
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Setup callback for `createSensor`. Invoked when the sensor gains its first downstream
|
|
52
|
+
* subscriber; receives a `set` function to push new values into the graph.
|
|
53
|
+
*
|
|
54
|
+
* @template T - The type of value produced by the sensor
|
|
55
|
+
* @param set - Updates the sensor value and propagates the change to subscribers
|
|
56
|
+
* @returns A cleanup function invoked when the sensor loses all subscribers
|
|
57
|
+
*/
|
|
52
58
|
type SensorCallback<T extends {}> = (set: (next: T) => void) => Cleanup
|
|
53
59
|
|
|
54
60
|
/* === Exported Functions === */
|
package/src/nodes/store.ts
CHANGED
|
@@ -28,7 +28,11 @@ import { createState, type State } from './state'
|
|
|
28
28
|
|
|
29
29
|
/* === Types === */
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Configuration options for `createStore`.
|
|
33
|
+
*/
|
|
31
34
|
type StoreOptions = {
|
|
35
|
+
/** Invoked when the store gains its first downstream subscriber; returns a cleanup called when the last one unsubscribes. */
|
|
32
36
|
watched?: () => Cleanup
|
|
33
37
|
}
|
|
34
38
|
|
|
@@ -58,6 +62,13 @@ type BaseStore<T extends UnknownRecord> = {
|
|
|
58
62
|
remove(key: string): void
|
|
59
63
|
}
|
|
60
64
|
|
|
65
|
+
/**
|
|
66
|
+
* A reactive object with per-property reactivity.
|
|
67
|
+
* Each property is wrapped as a `State`, nested `Store`, or `List` signal, accessible directly via proxy.
|
|
68
|
+
* Updating one property only re-runs effects that read that property.
|
|
69
|
+
*
|
|
70
|
+
* @template T - The plain-object type whose properties become reactive signals
|
|
71
|
+
*/
|
|
61
72
|
type Store<T extends UnknownRecord> = BaseStore<T> & {
|
|
62
73
|
[K in keyof T]: T[K] extends readonly (infer U extends {})[]
|
|
63
74
|
? List<U>
|
package/src/signal.ts
CHANGED
|
@@ -22,6 +22,12 @@ import { isAsyncFunction, isFunction, isRecord, isUniformArray } from './util'
|
|
|
22
22
|
|
|
23
23
|
/* === Types === */
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* A readable and writable signal — the type union of `State`, `Store`, and `List`.
|
|
27
|
+
* Use as a parameter type for generic code that accepts any writable signal.
|
|
28
|
+
*
|
|
29
|
+
* @template T - The type of value held by the signal
|
|
30
|
+
*/
|
|
25
31
|
type MutableSignal<T extends {}> = {
|
|
26
32
|
get(): T
|
|
27
33
|
set(value: T): void
|
package/test/list.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import {
|
|
3
|
+
batch,
|
|
3
4
|
createEffect,
|
|
4
5
|
createList,
|
|
5
6
|
createMemo,
|
|
@@ -209,6 +210,126 @@ describe('List', () => {
|
|
|
209
210
|
})
|
|
210
211
|
})
|
|
211
212
|
|
|
213
|
+
describe('replace', () => {
|
|
214
|
+
test('should update the item signal value', () => {
|
|
215
|
+
const list = createList(['a', 'b', 'c'])
|
|
216
|
+
// biome-ignore lint/style/noNonNullAssertion: index is within bounds
|
|
217
|
+
const key = list.keyAt(1)!
|
|
218
|
+
list.replace(key, 'B')
|
|
219
|
+
expect(list.byKey(key)?.get()).toBe('B')
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
test('structural subscriber via keys() re-runs after replace(); byKey().set() does NOT', () => {
|
|
223
|
+
const list = createList(['x', 'y'])
|
|
224
|
+
// biome-ignore lint/style/noNonNullAssertion: index is within bounds
|
|
225
|
+
const key = list.keyAt(0)!
|
|
226
|
+
let effectCount = 0
|
|
227
|
+
|
|
228
|
+
// This effect subscribes structurally via keys() only — no list.get() call
|
|
229
|
+
createEffect(() => {
|
|
230
|
+
void [...list.keys()]
|
|
231
|
+
effectCount++
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
expect(effectCount).toBe(1)
|
|
235
|
+
|
|
236
|
+
// replace() propagates through node.sinks — structural subscriber re-runs
|
|
237
|
+
list.replace(key, 'X')
|
|
238
|
+
expect(effectCount).toBe(2)
|
|
239
|
+
|
|
240
|
+
// byKey().set() does NOT propagate through node.sinks — structural subscriber does NOT re-run
|
|
241
|
+
list.byKey(key)?.set('XX')
|
|
242
|
+
expect(effectCount).toBe(2)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
test('direct subscriber via byKey().get() re-runs after replace()', () => {
|
|
246
|
+
const list = createList(['a', 'b'])
|
|
247
|
+
// biome-ignore lint/style/noNonNullAssertion: index is within bounds
|
|
248
|
+
const key = list.keyAt(0)!
|
|
249
|
+
let lastValue = ''
|
|
250
|
+
let effectCount = 0
|
|
251
|
+
|
|
252
|
+
createEffect(() => {
|
|
253
|
+
// biome-ignore lint/style/noNonNullAssertion: key is valid
|
|
254
|
+
lastValue = list.byKey(key)!.get()
|
|
255
|
+
effectCount++
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
expect(effectCount).toBe(1)
|
|
259
|
+
expect(lastValue).toBe('a')
|
|
260
|
+
|
|
261
|
+
list.replace(key, 'A')
|
|
262
|
+
expect(effectCount).toBe(2)
|
|
263
|
+
expect(lastValue).toBe('A')
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
test('no-op on equal value (same reference)', () => {
|
|
267
|
+
const obj = { id: 1 }
|
|
268
|
+
const list = createList([obj])
|
|
269
|
+
// biome-ignore lint/style/noNonNullAssertion: index is within bounds
|
|
270
|
+
const key = list.keyAt(0)!
|
|
271
|
+
let effectCount = 0
|
|
272
|
+
|
|
273
|
+
createEffect(() => {
|
|
274
|
+
list.get()
|
|
275
|
+
effectCount++
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
expect(effectCount).toBe(1)
|
|
279
|
+
|
|
280
|
+
list.replace(key, obj)
|
|
281
|
+
expect(effectCount).toBe(1)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test('no-op on missing key — does not throw and does not trigger effects', () => {
|
|
285
|
+
const list = createList([1, 2, 3])
|
|
286
|
+
let effectCount = 0
|
|
287
|
+
|
|
288
|
+
createEffect(() => {
|
|
289
|
+
list.get()
|
|
290
|
+
effectCount++
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
expect(effectCount).toBe(1)
|
|
294
|
+
expect(() => list.replace('nonexistent', 99)).not.toThrow()
|
|
295
|
+
expect(effectCount).toBe(1)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
test('batch compatibility — effects run only once inside batch()', () => {
|
|
299
|
+
const list = createList(['a', 'b', 'c'])
|
|
300
|
+
// biome-ignore lint/style/noNonNullAssertion: index is within bounds
|
|
301
|
+
const key0 = list.keyAt(0)!
|
|
302
|
+
// biome-ignore lint/style/noNonNullAssertion: index is within bounds
|
|
303
|
+
const key1 = list.keyAt(1)!
|
|
304
|
+
let effectCount = 0
|
|
305
|
+
|
|
306
|
+
createEffect(() => {
|
|
307
|
+
list.get()
|
|
308
|
+
effectCount++
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
expect(effectCount).toBe(1)
|
|
312
|
+
|
|
313
|
+
batch(() => {
|
|
314
|
+
list.replace(key0, 'A')
|
|
315
|
+
list.replace(key1, 'B')
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
expect(effectCount).toBe(2)
|
|
319
|
+
expect(list.get()).toEqual(['A', 'B', 'c'])
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
test('signal identity preserved — byKey() returns same signal before and after replace()', () => {
|
|
323
|
+
const list = createList([10, 20])
|
|
324
|
+
// biome-ignore lint/style/noNonNullAssertion: index is within bounds
|
|
325
|
+
const key = list.keyAt(0)!
|
|
326
|
+
const signalBefore = list.byKey(key)
|
|
327
|
+
list.replace(key, 99)
|
|
328
|
+
const signalAfter = list.byKey(key)
|
|
329
|
+
expect(signalBefore).toBe(signalAfter)
|
|
330
|
+
})
|
|
331
|
+
})
|
|
332
|
+
|
|
212
333
|
describe('sort', () => {
|
|
213
334
|
test('should sort with default string comparison', () => {
|
|
214
335
|
const list = createList([3, 1, 2])
|
package/tsconfig.json
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
"moduleResolution": "bundler",
|
|
13
13
|
"allowImportingTsExtensions": true,
|
|
14
14
|
"verbatimModuleSyntax": true,
|
|
15
|
+
"erasableSyntaxOnly": true,
|
|
16
|
+
"isolatedModules": true,
|
|
17
|
+
"resolveJsonModule": true,
|
|
18
|
+
"types": ["bun-types"],
|
|
15
19
|
|
|
16
20
|
// Editor-only mode - no emit
|
|
17
21
|
"noEmit": true,
|
|
@@ -24,11 +28,16 @@
|
|
|
24
28
|
"useUnknownInCatchVariables": true,
|
|
25
29
|
"noUncheckedSideEffectImports": true,
|
|
26
30
|
"noFallthroughCasesInSwitch": true,
|
|
31
|
+
"forceConsistentCasingInFileNames": true,
|
|
27
32
|
|
|
28
33
|
// Some stricter flags (disabled by default)
|
|
29
34
|
"noUnusedLocals": false,
|
|
30
35
|
"noUnusedParameters": false,
|
|
31
36
|
"noPropertyAccessFromIndexSignature": false,
|
|
37
|
+
|
|
38
|
+
/* Performance */
|
|
39
|
+
"incremental": true,
|
|
40
|
+
"tsBuildInfoFile": "./.tsbuildinfo",
|
|
32
41
|
},
|
|
33
42
|
"include": ["./**/*.ts"],
|
|
34
43
|
"exclude": ["node_modules", "types", "index.js"],
|
package/types/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @name Cause & Effect
|
|
3
|
-
* @version 1.0.
|
|
3
|
+
* @version 1.0.2
|
|
4
4
|
* @author Esther Brunner
|
|
5
5
|
*/
|
|
6
6
|
export { CircularDependencyError, type Guard, InvalidCallbackError, InvalidSignalValueError, NullishSignalValueError, ReadonlySignalError, RequiredOwnerError, UnsetSignalValueError, } from './src/errors';
|
package/types/src/graph.d.ts
CHANGED
|
@@ -226,6 +226,8 @@ declare function createScope(fn: () => MaybeCleanup): Cleanup;
|
|
|
226
226
|
* reactive graph.
|
|
227
227
|
*
|
|
228
228
|
* @since 0.18.5
|
|
229
|
+
* @param fn - The function to execute without an active owner
|
|
230
|
+
* @returns The return value of `fn`
|
|
229
231
|
*/
|
|
230
232
|
declare function unown<T>(fn: () => T): T;
|
|
231
233
|
export { type Cleanup, type ComputedOptions, type EffectCallback, type EffectNode, type MaybeCleanup, type MemoCallback, type MemoNode, type Scope, type Signal, type SignalOptions, type SinkNode, type StateNode, type TaskCallback, type TaskNode, activeOwner, activeSink, batch, batchDepth, createScope, DEFAULT_EQUALITY, SKIP_EQUALITY, FLAG_CHECK, FLAG_CLEAN, FLAG_DIRTY, FLAG_RELINK, flush, link, propagate, refresh, registerCleanup, runCleanup, runEffect, setState, trimSources, TYPE_COLLECTION, TYPE_LIST, TYPE_MEMO, TYPE_SENSOR, TYPE_STATE, TYPE_SLOT, TYPE_STORE, TYPE_TASK, unlink, unown, untrack, };
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import { type Cleanup, type Signal } from '../graph';
|
|
2
2
|
import { type KeyConfig, type List } from './list';
|
|
3
3
|
type CollectionSource<T extends {}> = List<T> | Collection<T>;
|
|
4
|
+
/**
|
|
5
|
+
* Transformation callback for `deriveCollection` — sync or async.
|
|
6
|
+
* Sync callbacks produce a `Memo<T>` per item; async callbacks produce a `Task<T>`
|
|
7
|
+
* with automatic cancellation when the source item changes.
|
|
8
|
+
*
|
|
9
|
+
* @template T - The type of derived items
|
|
10
|
+
* @template U - The type of source items
|
|
11
|
+
*/
|
|
4
12
|
type DeriveCollectionCallback<T extends {}, U extends {}> = ((sourceValue: U) => T) | ((sourceValue: U, abort: AbortSignal) => Promise<T>);
|
|
13
|
+
/**
|
|
14
|
+
* A read-only reactive keyed collection with per-item reactivity.
|
|
15
|
+
* Created by `createCollection` (externally driven) or via `.deriveCollection()` on a `List` or `Collection`.
|
|
16
|
+
*
|
|
17
|
+
* @template T - The type of items in the collection
|
|
18
|
+
*/
|
|
5
19
|
type Collection<T extends {}> = {
|
|
6
20
|
readonly [Symbol.toStringTag]: 'Collection';
|
|
7
21
|
readonly [Symbol.isConcatSpreadable]: true;
|
|
@@ -16,16 +30,40 @@ type Collection<T extends {}> = {
|
|
|
16
30
|
deriveCollection<R extends {}>(callback: (sourceValue: T, abort: AbortSignal) => Promise<R>): Collection<R>;
|
|
17
31
|
readonly length: number;
|
|
18
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* Granular mutation descriptor passed to the `applyChanges` callback inside a `CollectionCallback`.
|
|
35
|
+
*
|
|
36
|
+
* @template T - The type of items in the collection
|
|
37
|
+
*/
|
|
19
38
|
type CollectionChanges<T> = {
|
|
39
|
+
/** Items to add. Each item is assigned a new key via the configured `keyConfig`. */
|
|
20
40
|
add?: T[];
|
|
41
|
+
/** Items whose values have changed. Matched to existing entries by key. */
|
|
21
42
|
change?: T[];
|
|
43
|
+
/** Items to remove. Matched to existing entries by key. */
|
|
22
44
|
remove?: T[];
|
|
23
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* Configuration options for `createCollection`.
|
|
48
|
+
*
|
|
49
|
+
* @template T - The type of items in the collection
|
|
50
|
+
*/
|
|
24
51
|
type CollectionOptions<T extends {}> = {
|
|
52
|
+
/** Initial items. Defaults to `[]`. */
|
|
25
53
|
value?: T[];
|
|
54
|
+
/** Key generation strategy. See `KeyConfig`. Defaults to auto-increment. */
|
|
26
55
|
keyConfig?: KeyConfig<T>;
|
|
56
|
+
/** Factory for per-item signals. Defaults to `createState`. */
|
|
27
57
|
createItem?: (value: T) => Signal<T>;
|
|
28
58
|
};
|
|
59
|
+
/**
|
|
60
|
+
* Setup callback for `createCollection`. Invoked when the collection gains its first downstream
|
|
61
|
+
* subscriber; receives an `applyChanges` function to push granular mutations into the graph.
|
|
62
|
+
*
|
|
63
|
+
* @template T - The type of items in the collection
|
|
64
|
+
* @param apply - Call with a `CollectionChanges` object to add, update, or remove items
|
|
65
|
+
* @returns A cleanup function invoked when the collection loses all subscribers
|
|
66
|
+
*/
|
|
29
67
|
type CollectionCallback<T extends {}> = (apply: (changes: CollectionChanges<T>) => void) => Cleanup;
|
|
30
68
|
/**
|
|
31
69
|
* Creates a derived Collection from a List or another Collection with item-level memoization.
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { type Cleanup, type EffectCallback, type MaybeCleanup, type Signal } from '../graph';
|
|
2
|
+
/** A value that is either synchronous or a `Promise` — used for handler return types in `match()`. */
|
|
2
3
|
type MaybePromise<T> = T | Promise<T>;
|
|
4
|
+
/**
|
|
5
|
+
* Handlers for all states of one or more signals passed to `match()`.
|
|
6
|
+
*
|
|
7
|
+
* @template T - Tuple of `Signal` types being matched
|
|
8
|
+
*/
|
|
3
9
|
type MatchHandlers<T extends readonly Signal<unknown & {}>[]> = {
|
|
10
|
+
/** Called when all signals have a value. Receives a tuple of resolved values. */
|
|
4
11
|
ok: (values: {
|
|
5
12
|
[K in keyof T]: T[K] extends Signal<infer V> ? V : never;
|
|
6
13
|
}) => MaybePromise<MaybeCleanup>;
|
|
14
|
+
/** Called when one or more signals hold an error. Defaults to `console.error`. */
|
|
7
15
|
err?: (errors: readonly Error[]) => MaybePromise<MaybeCleanup>;
|
|
16
|
+
/** Called when one or more signals are unset (pending). */
|
|
8
17
|
nil?: () => MaybePromise<MaybeCleanup>;
|
|
9
18
|
};
|
|
10
19
|
/**
|
|
@@ -38,10 +47,13 @@ type MatchHandlers<T extends readonly Signal<unknown & {}>[]> = {
|
|
|
38
47
|
*/
|
|
39
48
|
declare function createEffect(fn: EffectCallback): Cleanup;
|
|
40
49
|
/**
|
|
41
|
-
*
|
|
50
|
+
* Reads one or more signals and dispatches to the appropriate handler based on their state.
|
|
42
51
|
* Must be called within an active owner (effect or scope) so async cleanup can be registered.
|
|
43
52
|
*
|
|
44
53
|
* @since 0.15.0
|
|
54
|
+
* @param signals - Tuple of signals to read; all must have a value for `ok` to run.
|
|
55
|
+
* @param handlers - Object with an `ok` branch and optional `err` and `nil` branches.
|
|
56
|
+
* @returns An optional cleanup function if the active handler returns one.
|
|
45
57
|
* @throws RequiredOwnerError If called without an active owner.
|
|
46
58
|
*/
|
|
47
59
|
declare function match<T extends readonly Signal<unknown & {}>[]>(signals: readonly [...T], handlers: MatchHandlers<T>): MaybeCleanup;
|
|
@@ -8,11 +8,31 @@ type DiffResult = {
|
|
|
8
8
|
change: UnknownRecord;
|
|
9
9
|
remove: UnknownRecord;
|
|
10
10
|
};
|
|
11
|
+
/**
|
|
12
|
+
* Key generation strategy for `createList` items.
|
|
13
|
+
* A string value is used as a prefix for auto-incremented keys (`prefix0`, `prefix1`, …).
|
|
14
|
+
* A function receives each item and returns a stable string key, or `undefined` to fall back to auto-increment.
|
|
15
|
+
*
|
|
16
|
+
* @template T - The type of items in the list
|
|
17
|
+
*/
|
|
11
18
|
type KeyConfig<T> = string | ((item: T) => string | undefined);
|
|
19
|
+
/**
|
|
20
|
+
* Configuration options for `createList`.
|
|
21
|
+
*
|
|
22
|
+
* @template T - The type of items in the list
|
|
23
|
+
*/
|
|
12
24
|
type ListOptions<T extends {}> = {
|
|
25
|
+
/** Key generation strategy. A string prefix or a function `(item) => string | undefined`. Defaults to auto-increment. */
|
|
13
26
|
keyConfig?: KeyConfig<T>;
|
|
27
|
+
/** Lifecycle callback invoked when the list gains its first downstream subscriber. Must return a cleanup function. */
|
|
14
28
|
watched?: () => Cleanup;
|
|
15
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* A reactive ordered array with stable keys and per-item reactivity.
|
|
32
|
+
* Each item is a `State<T>` signal; structural changes (add/remove/sort) propagate reactively.
|
|
33
|
+
*
|
|
34
|
+
* @template T - The type of items in the list
|
|
35
|
+
*/
|
|
16
36
|
type List<T extends {}> = {
|
|
17
37
|
readonly [Symbol.toStringTag]: 'List';
|
|
18
38
|
readonly [Symbol.isConcatSpreadable]: true;
|
|
@@ -28,6 +48,13 @@ type List<T extends {}> = {
|
|
|
28
48
|
indexOfKey(key: string): number;
|
|
29
49
|
add(value: T): string;
|
|
30
50
|
remove(keyOrIndex: string | number): void;
|
|
51
|
+
/**
|
|
52
|
+
* Updates an existing item by key, propagating to all subscribers.
|
|
53
|
+
* No-op if the key does not exist or the value is reference-equal to the current value.
|
|
54
|
+
* @param key - Stable key of the item to update
|
|
55
|
+
* @param value - New value for the item
|
|
56
|
+
*/
|
|
57
|
+
replace(key: string, value: T): void;
|
|
31
58
|
sort(compareFn?: (a: T, b: T) => number): void;
|
|
32
59
|
splice(start: number, deleteCount?: number, ...items: T[]): T[];
|
|
33
60
|
deriveCollection<R extends {}>(callback: (sourceValue: T) => R): Collection<R>;
|
|
@@ -51,8 +78,9 @@ declare function getKeyGenerator<T extends {}>(keyConfig?: KeyConfig<T>): [(item
|
|
|
51
78
|
*
|
|
52
79
|
* @since 0.18.0
|
|
53
80
|
* @param value - Initial array of items
|
|
54
|
-
* @param options -
|
|
55
|
-
* @
|
|
81
|
+
* @param options.keyConfig - Key generation strategy: string prefix or `(item) => string | undefined`. Defaults to auto-increment.
|
|
82
|
+
* @param options.watched - Lifecycle callback invoked on first subscriber; must return a cleanup function called on last unsubscribe.
|
|
83
|
+
* @returns A `List` signal with reactive per-item `State` signals
|
|
56
84
|
*/
|
|
57
85
|
declare function createList<T extends {}>(value: T[], options?: ListOptions<T>): List<T>;
|
|
58
86
|
/**
|
|
@@ -12,7 +12,6 @@ type Memo<T extends {}> = {
|
|
|
12
12
|
* Recomputes if dependencies have changed since last access.
|
|
13
13
|
* When called inside another reactive context, creates a dependency.
|
|
14
14
|
* @returns The computed value
|
|
15
|
-
* @throws UnsetSignalValueError If the memo value is still unset when read.
|
|
16
15
|
*/
|
|
17
16
|
get(): T;
|
|
18
17
|
};
|
|
@@ -15,11 +15,9 @@ type Sensor<T extends {}> = {
|
|
|
15
15
|
get(): T;
|
|
16
16
|
};
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
18
|
+
* Configuration options for `createSensor`.
|
|
19
19
|
*
|
|
20
|
-
* @template T - The type of value
|
|
21
|
-
* @param set - A function to set the observed value
|
|
22
|
-
* @returns A cleanup function when the sensor stops being watched
|
|
20
|
+
* @template T - The type of value produced by the sensor
|
|
23
21
|
*/
|
|
24
22
|
type SensorOptions<T extends {}> = SignalOptions<T> & {
|
|
25
23
|
/**
|
|
@@ -28,6 +26,14 @@ type SensorOptions<T extends {}> = SignalOptions<T> & {
|
|
|
28
26
|
*/
|
|
29
27
|
value?: T;
|
|
30
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* Setup callback for `createSensor`. Invoked when the sensor gains its first downstream
|
|
31
|
+
* subscriber; receives a `set` function to push new values into the graph.
|
|
32
|
+
*
|
|
33
|
+
* @template T - The type of value produced by the sensor
|
|
34
|
+
* @param set - Updates the sensor value and propagates the change to subscribers
|
|
35
|
+
* @returns A cleanup function invoked when the sensor loses all subscribers
|
|
36
|
+
*/
|
|
31
37
|
type SensorCallback<T extends {}> = (set: (next: T) => void) => Cleanup;
|
|
32
38
|
/**
|
|
33
39
|
* Creates a sensor that tracks external input and updates a state value as long as it is active.
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { type Cleanup, TYPE_STORE } from '../graph';
|
|
2
2
|
import { type List, type UnknownRecord } from './list';
|
|
3
3
|
import { type State } from './state';
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for `createStore`.
|
|
6
|
+
*/
|
|
4
7
|
type StoreOptions = {
|
|
8
|
+
/** Invoked when the store gains its first downstream subscriber; returns a cleanup called when the last one unsubscribes. */
|
|
5
9
|
watched?: () => Cleanup;
|
|
6
10
|
};
|
|
7
11
|
type BaseStore<T extends UnknownRecord> = {
|
|
@@ -19,6 +23,13 @@ type BaseStore<T extends UnknownRecord> = {
|
|
|
19
23
|
add<K extends keyof T & string>(key: K, value: T[K]): K;
|
|
20
24
|
remove(key: string): void;
|
|
21
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* A reactive object with per-property reactivity.
|
|
28
|
+
* Each property is wrapped as a `State`, nested `Store`, or `List` signal, accessible directly via proxy.
|
|
29
|
+
* Updating one property only re-runs effects that read that property.
|
|
30
|
+
*
|
|
31
|
+
* @template T - The plain-object type whose properties become reactive signals
|
|
32
|
+
*/
|
|
22
33
|
type Store<T extends UnknownRecord> = BaseStore<T> & {
|
|
23
34
|
[K in keyof T]: T[K] extends readonly (infer U extends {})[] ? List<U> : T[K] extends UnknownRecord ? Store<T[K]> : T[K] extends unknown & {} ? State<T[K] & {}> : State<T[K] & {}> | undefined;
|
|
24
35
|
};
|
package/types/src/signal.d.ts
CHANGED
|
@@ -4,6 +4,12 @@ import { type Memo } from './nodes/memo';
|
|
|
4
4
|
import { type State } from './nodes/state';
|
|
5
5
|
import { type Store } from './nodes/store';
|
|
6
6
|
import { type Task } from './nodes/task';
|
|
7
|
+
/**
|
|
8
|
+
* A readable and writable signal — the type union of `State`, `Store`, and `List`.
|
|
9
|
+
* Use as a parameter type for generic code that accepts any writable signal.
|
|
10
|
+
*
|
|
11
|
+
* @template T - The type of value held by the signal
|
|
12
|
+
*/
|
|
7
13
|
type MutableSignal<T extends {}> = {
|
|
8
14
|
get(): T;
|
|
9
15
|
set(value: T): void;
|