@zeix/cause-effect 0.18.2 → 0.18.3
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 +12 -4
- package/.github/copilot-instructions.md +10 -2
- package/ARCHITECTURE.md +13 -1
- package/CHANGELOG.md +10 -0
- package/CLAUDE.md +25 -1
- package/GUIDE.md +22 -0
- package/README.md +33 -1
- package/REQUIREMENTS.md +4 -3
- package/index.dev.js +170 -71
- package/index.js +1 -1
- package/index.ts +3 -1
- package/package.json +1 -1
- package/src/errors.ts +13 -0
- package/src/graph.ts +2 -0
- package/src/nodes/collection.ts +0 -4
- package/src/nodes/effect.ts +3 -3
- package/src/nodes/slot.ts +134 -0
- package/src/signal.ts +2 -0
- package/test/effect.test.ts +17 -0
- package/test/signal.test.ts +2 -0
- package/test/slot.test.ts +118 -0
- package/types/index.d.ts +3 -2
- package/types/src/errors.d.ts +9 -1
- package/types/src/graph.d.ts +3 -1
- package/types/src/nodes/effect.d.ts +2 -2
- package/types/src/nodes/slot.d.ts +53 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { ReadonlySignalError, validateSignalValue } from '../errors'
|
|
2
|
+
import {
|
|
3
|
+
activeSink,
|
|
4
|
+
batchDepth,
|
|
5
|
+
DEFAULT_EQUALITY,
|
|
6
|
+
FLAG_DIRTY,
|
|
7
|
+
flush,
|
|
8
|
+
link,
|
|
9
|
+
type MemoNode,
|
|
10
|
+
propagate,
|
|
11
|
+
refresh,
|
|
12
|
+
type Signal,
|
|
13
|
+
type SignalOptions,
|
|
14
|
+
type SinkNode,
|
|
15
|
+
TYPE_SLOT,
|
|
16
|
+
} from '../graph'
|
|
17
|
+
import { isMutableSignal, isSignal } from '../signal'
|
|
18
|
+
import { isObjectOfType } from '../util'
|
|
19
|
+
|
|
20
|
+
/* === Types === */
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A signal that delegates its value to a swappable backing signal.
|
|
24
|
+
*
|
|
25
|
+
* Slots provide a stable reactive source at a fixed position (e.g. an object property)
|
|
26
|
+
* while allowing the backing signal to be replaced without breaking subscribers.
|
|
27
|
+
* The object shape is compatible with `Object.defineProperty()` descriptors:
|
|
28
|
+
* `get`, `set`, `configurable`, and `enumerable` are used by the property definition;
|
|
29
|
+
* `replace()` and `current()` are kept on the slot object for integration-layer control.
|
|
30
|
+
*
|
|
31
|
+
* @template T - The type of value held by the delegated signal.
|
|
32
|
+
*/
|
|
33
|
+
type Slot<T extends {}> = {
|
|
34
|
+
readonly [Symbol.toStringTag]: 'Slot'
|
|
35
|
+
/** Descriptor field: allows the property to be redefined or deleted. */
|
|
36
|
+
configurable: true
|
|
37
|
+
/** Descriptor field: the property shows up during enumeration. */
|
|
38
|
+
enumerable: true
|
|
39
|
+
/** Reads the current value from the delegated signal, tracking dependencies. */
|
|
40
|
+
get(): T
|
|
41
|
+
/** Writes a value to the delegated signal. Throws `ReadonlySignalError` if the delegated signal is read-only. */
|
|
42
|
+
set(next: T): void
|
|
43
|
+
/** Swaps the backing signal, invalidating all downstream subscribers. Narrowing (`U extends T`) is allowed. */
|
|
44
|
+
replace<U extends T>(next: Signal<U>): void
|
|
45
|
+
/** Returns the currently delegated signal. */
|
|
46
|
+
current(): Signal<T>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* === Exported Functions === */
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Creates a slot signal that delegates its value to a swappable backing signal.
|
|
53
|
+
*
|
|
54
|
+
* A slot acts as a stable reactive source that can be used as a property descriptor
|
|
55
|
+
* via `Object.defineProperty(target, key, slot)`. Subscribers link to the slot itself,
|
|
56
|
+
* so replacing the backing signal with `replace()` invalidates them without breaking
|
|
57
|
+
* existing edges. Setter calls forward to the current backing signal when it is writable.
|
|
58
|
+
*
|
|
59
|
+
* @since 0.18.3
|
|
60
|
+
* @template T - The type of value held by the delegated signal.
|
|
61
|
+
* @param initialSignal - The initial signal to delegate to.
|
|
62
|
+
* @param options - Optional configuration for the slot.
|
|
63
|
+
* @param options.equals - Custom equality function. Defaults to strict equality (`===`).
|
|
64
|
+
* @param options.guard - Type guard to validate values passed to `set()`.
|
|
65
|
+
* @returns A `Slot<T>` object usable both as a property descriptor and as a reactive signal.
|
|
66
|
+
*/
|
|
67
|
+
function createSlot<T extends {}>(
|
|
68
|
+
initialSignal: Signal<T>,
|
|
69
|
+
options?: SignalOptions<T>,
|
|
70
|
+
): Slot<T> {
|
|
71
|
+
validateSignalValue(TYPE_SLOT, initialSignal, isSignal)
|
|
72
|
+
|
|
73
|
+
let delegated = initialSignal as Signal<T>
|
|
74
|
+
const guard = options?.guard
|
|
75
|
+
|
|
76
|
+
const node: MemoNode<T> = {
|
|
77
|
+
fn: () => delegated.get(),
|
|
78
|
+
value: undefined as unknown as T,
|
|
79
|
+
flags: FLAG_DIRTY,
|
|
80
|
+
sources: null,
|
|
81
|
+
sourcesTail: null,
|
|
82
|
+
sinks: null,
|
|
83
|
+
sinksTail: null,
|
|
84
|
+
equals: options?.equals ?? DEFAULT_EQUALITY,
|
|
85
|
+
error: undefined,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const get = (): T => {
|
|
89
|
+
if (activeSink) link(node, activeSink)
|
|
90
|
+
refresh(node as unknown as SinkNode)
|
|
91
|
+
if (node.error) throw node.error
|
|
92
|
+
return node.value
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const set = (next: T): void => {
|
|
96
|
+
if (!isMutableSignal(delegated))
|
|
97
|
+
throw new ReadonlySignalError(TYPE_SLOT)
|
|
98
|
+
validateSignalValue(TYPE_SLOT, next, guard)
|
|
99
|
+
|
|
100
|
+
delegated.set(next)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const replace = <U extends T>(next: Signal<U>): void => {
|
|
104
|
+
validateSignalValue(TYPE_SLOT, next, isSignal)
|
|
105
|
+
|
|
106
|
+
delegated = next
|
|
107
|
+
node.flags |= FLAG_DIRTY
|
|
108
|
+
for (let e = node.sinks; e; e = e.nextSink) propagate(e.sink)
|
|
109
|
+
if (batchDepth === 0) flush()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
[Symbol.toStringTag]: TYPE_SLOT,
|
|
114
|
+
configurable: true,
|
|
115
|
+
enumerable: true,
|
|
116
|
+
get,
|
|
117
|
+
set,
|
|
118
|
+
replace,
|
|
119
|
+
current: () => delegated,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Checks if a value is a Slot signal.
|
|
125
|
+
*
|
|
126
|
+
* @since 0.18.3
|
|
127
|
+
* @param value - The value to check
|
|
128
|
+
* @returns True if the value is a Slot
|
|
129
|
+
*/
|
|
130
|
+
function isSlot<T extends {} = unknown & {}>(value: unknown): value is Slot<T> {
|
|
131
|
+
return isObjectOfType(value, TYPE_SLOT)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export { createSlot, isSlot, type Slot }
|
package/src/signal.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
TYPE_LIST,
|
|
9
9
|
TYPE_MEMO,
|
|
10
10
|
TYPE_SENSOR,
|
|
11
|
+
TYPE_SLOT,
|
|
11
12
|
TYPE_STATE,
|
|
12
13
|
TYPE_STORE,
|
|
13
14
|
TYPE_TASK,
|
|
@@ -122,6 +123,7 @@ function isSignal<T extends {}>(value: unknown): value is Signal<T> {
|
|
|
122
123
|
TYPE_MEMO,
|
|
123
124
|
TYPE_TASK,
|
|
124
125
|
TYPE_SENSOR,
|
|
126
|
+
TYPE_SLOT,
|
|
125
127
|
TYPE_LIST,
|
|
126
128
|
TYPE_COLLECTION,
|
|
127
129
|
TYPE_STORE,
|
package/test/effect.test.ts
CHANGED
|
@@ -249,6 +249,23 @@ describe('match', () => {
|
|
|
249
249
|
}
|
|
250
250
|
})
|
|
251
251
|
|
|
252
|
+
test('should preserve tuple types in ok handler', () => {
|
|
253
|
+
const a = createState(1)
|
|
254
|
+
const b = createState('hello')
|
|
255
|
+
createEffect(() =>
|
|
256
|
+
match([a, b], {
|
|
257
|
+
ok: ([aVal, bVal]) => {
|
|
258
|
+
// If tuple types are preserved, aVal is number and bVal is string
|
|
259
|
+
// If widened, both would be string | number
|
|
260
|
+
const num: number = aVal
|
|
261
|
+
const str: string = bVal
|
|
262
|
+
expect(num).toBe(1)
|
|
263
|
+
expect(str).toBe('hello')
|
|
264
|
+
},
|
|
265
|
+
}),
|
|
266
|
+
)
|
|
267
|
+
})
|
|
268
|
+
|
|
252
269
|
test('should throw RequiredOwnerError when called outside an owner', () => {
|
|
253
270
|
expect(() => match([], { ok: () => {} })).toThrow(RequiredOwnerError)
|
|
254
271
|
})
|
package/test/signal.test.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
createMutableSignal,
|
|
7
7
|
createScope,
|
|
8
8
|
createSignal,
|
|
9
|
+
createSlot,
|
|
9
10
|
createState,
|
|
10
11
|
createStore,
|
|
11
12
|
createTask,
|
|
@@ -229,6 +230,7 @@ describe('isSignal', () => {
|
|
|
229
230
|
expect(isSignal(createTask(async () => 42))).toBe(true)
|
|
230
231
|
expect(isSignal(createStore({ a: 1 }))).toBe(true)
|
|
231
232
|
expect(isSignal(createList([1, 2, 3]))).toBe(true)
|
|
233
|
+
expect(isSignal(createSlot(createState(1)))).toBe(true)
|
|
232
234
|
})
|
|
233
235
|
cleanup()
|
|
234
236
|
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
batch,
|
|
4
|
+
createEffect,
|
|
5
|
+
createMemo,
|
|
6
|
+
createSlot,
|
|
7
|
+
createState,
|
|
8
|
+
} from '../index.ts'
|
|
9
|
+
import { InvalidSignalValueError, NullishSignalValueError } from '../src/errors'
|
|
10
|
+
|
|
11
|
+
describe('Slot', () => {
|
|
12
|
+
test('should replace delegated signal and re-subscribe sinks', () => {
|
|
13
|
+
const local = createState(1)
|
|
14
|
+
const parent = createState(10)
|
|
15
|
+
const derived = createMemo(() => parent.get())
|
|
16
|
+
const slot = createSlot(local)
|
|
17
|
+
|
|
18
|
+
const target = {}
|
|
19
|
+
Object.defineProperty(target, 'value', slot)
|
|
20
|
+
|
|
21
|
+
let runs = 0
|
|
22
|
+
let seen = 0
|
|
23
|
+
createEffect(() => {
|
|
24
|
+
seen = (target as { value: number }).value
|
|
25
|
+
runs++
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
expect(runs).toBe(1)
|
|
29
|
+
expect(seen).toBe(1)
|
|
30
|
+
|
|
31
|
+
slot.replace(derived)
|
|
32
|
+
expect(runs).toBe(2)
|
|
33
|
+
expect(seen).toBe(10)
|
|
34
|
+
|
|
35
|
+
// Old delegated signal should no longer trigger downstream sinks
|
|
36
|
+
local.set(2)
|
|
37
|
+
expect(runs).toBe(2)
|
|
38
|
+
|
|
39
|
+
parent.set(11)
|
|
40
|
+
expect(runs).toBe(3)
|
|
41
|
+
expect(seen).toBe(11)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('should forward property set to writable delegated signal', () => {
|
|
45
|
+
const source = createState(2)
|
|
46
|
+
const slot = createSlot(source)
|
|
47
|
+
const target = {}
|
|
48
|
+
Object.defineProperty(target, 'value', slot)
|
|
49
|
+
;(target as { value: number }).value = 3
|
|
50
|
+
|
|
51
|
+
expect(source.get()).toBe(3)
|
|
52
|
+
expect((target as { value: number }).value).toBe(3)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('should throw on set when delegated signal is read-only', () => {
|
|
56
|
+
const source = createState(2)
|
|
57
|
+
const readonly = createMemo(() => source.get() * 2)
|
|
58
|
+
const slot = createSlot(source)
|
|
59
|
+
const target = {}
|
|
60
|
+
Object.defineProperty(target, 'value', slot)
|
|
61
|
+
slot.replace(readonly)
|
|
62
|
+
|
|
63
|
+
expect(() => {
|
|
64
|
+
;(target as { value: number }).value = 7
|
|
65
|
+
}).toThrow('[Slot] Signal is read-only')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('should keep replace handle outside property descriptor', () => {
|
|
69
|
+
const source = createState(1)
|
|
70
|
+
const slot = createSlot(source)
|
|
71
|
+
const target = {}
|
|
72
|
+
Object.defineProperty(target, 'value', slot)
|
|
73
|
+
|
|
74
|
+
const descriptor = Object.getOwnPropertyDescriptor(target, 'value')
|
|
75
|
+
expect(descriptor).toBeDefined()
|
|
76
|
+
expect(typeof descriptor?.get).toBe('function')
|
|
77
|
+
expect(typeof descriptor?.set).toBe('function')
|
|
78
|
+
expect((descriptor as unknown as { replace?: unknown }).replace).toBe(
|
|
79
|
+
undefined,
|
|
80
|
+
)
|
|
81
|
+
expect(typeof slot.replace).toBe('function')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('should batch multiple replacements into one downstream rerun', () => {
|
|
85
|
+
const a = createState(1)
|
|
86
|
+
const b = createState(2)
|
|
87
|
+
const c = createState(3)
|
|
88
|
+
const slot = createSlot(a)
|
|
89
|
+
const target = {}
|
|
90
|
+
Object.defineProperty(target, 'value', slot)
|
|
91
|
+
|
|
92
|
+
let runs = 0
|
|
93
|
+
createEffect(() => {
|
|
94
|
+
void (target as { value: number }).value
|
|
95
|
+
runs++
|
|
96
|
+
})
|
|
97
|
+
expect(runs).toBe(1)
|
|
98
|
+
|
|
99
|
+
batch(() => {
|
|
100
|
+
slot.replace(b)
|
|
101
|
+
slot.replace(c)
|
|
102
|
+
})
|
|
103
|
+
expect(runs).toBe(2)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('should validate initial signal and replacement signal', () => {
|
|
107
|
+
expect(() => {
|
|
108
|
+
// @ts-expect-error: deliberate error test
|
|
109
|
+
createSlot(null)
|
|
110
|
+
}).toThrow(NullishSignalValueError)
|
|
111
|
+
|
|
112
|
+
const slot = createSlot(createState(1))
|
|
113
|
+
expect(() => {
|
|
114
|
+
// @ts-expect-error: deliberate error test
|
|
115
|
+
slot.replace(42)
|
|
116
|
+
}).toThrow(InvalidSignalValueError)
|
|
117
|
+
})
|
|
118
|
+
})
|
package/types/index.d.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @name Cause & Effect
|
|
3
|
-
* @version 0.18.
|
|
3
|
+
* @version 0.18.3
|
|
4
4
|
* @author Esther Brunner
|
|
5
5
|
*/
|
|
6
|
-
export { CircularDependencyError, type Guard, InvalidCallbackError, InvalidSignalValueError, NullishSignalValueError, RequiredOwnerError, UnsetSignalValueError, } from './src/errors';
|
|
6
|
+
export { CircularDependencyError, type Guard, InvalidCallbackError, InvalidSignalValueError, NullishSignalValueError, ReadonlySignalError, RequiredOwnerError, UnsetSignalValueError, } from './src/errors';
|
|
7
7
|
export { batch, type Cleanup, type ComputedOptions, createScope, type EffectCallback, type MaybeCleanup, type MemoCallback, type Signal, type SignalOptions, SKIP_EQUALITY, type TaskCallback, untrack, } from './src/graph';
|
|
8
8
|
export { type Collection, type CollectionCallback, type CollectionChanges, type CollectionOptions, createCollection, type DeriveCollectionCallback, isCollection, } from './src/nodes/collection';
|
|
9
9
|
export { createEffect, type MatchHandlers, type MaybePromise, match, } from './src/nodes/effect';
|
|
10
10
|
export { createList, isEqual, isList, type KeyConfig, type List, type ListOptions, } from './src/nodes/list';
|
|
11
11
|
export { createMemo, isMemo, type Memo } from './src/nodes/memo';
|
|
12
12
|
export { createSensor, isSensor, type Sensor, type SensorCallback, type SensorOptions, } from './src/nodes/sensor';
|
|
13
|
+
export { createSlot, isSlot, type Slot } from './src/nodes/slot';
|
|
13
14
|
export { createState, isState, type State, type UpdateCallback, } from './src/nodes/state';
|
|
14
15
|
export { createStore, isStore, type Store, type StoreOptions, } from './src/nodes/store';
|
|
15
16
|
export { createTask, isTask, type Task } from './src/nodes/task';
|
package/types/src/errors.d.ts
CHANGED
|
@@ -64,6 +64,14 @@ declare class InvalidCallbackError extends TypeError {
|
|
|
64
64
|
*/
|
|
65
65
|
constructor(where: string, value: unknown);
|
|
66
66
|
}
|
|
67
|
+
declare class ReadonlySignalError extends Error {
|
|
68
|
+
/**
|
|
69
|
+
* Constructs a new ReadonlySignalError.
|
|
70
|
+
*
|
|
71
|
+
* @param where - The location where the error occurred.
|
|
72
|
+
*/
|
|
73
|
+
constructor(where: string);
|
|
74
|
+
}
|
|
67
75
|
/**
|
|
68
76
|
* Error thrown when an API requiring an owner is called without one.
|
|
69
77
|
*/
|
|
@@ -82,4 +90,4 @@ declare function validateSignalValue<T extends {}>(where: string, value: unknown
|
|
|
82
90
|
declare function validateReadValue<T extends {}>(where: string, value: T | null | undefined): asserts value is T;
|
|
83
91
|
declare function validateCallback(where: string, value: unknown): asserts value is (...args: unknown[]) => unknown;
|
|
84
92
|
declare function validateCallback<T>(where: string, value: unknown, guard: (value: unknown) => value is T): asserts value is T;
|
|
85
|
-
export { type Guard, CircularDependencyError, NullishSignalValueError, InvalidSignalValueError, UnsetSignalValueError, InvalidCallbackError, RequiredOwnerError, DuplicateKeyError, validateSignalValue, validateReadValue, validateCallback, };
|
|
93
|
+
export { type Guard, CircularDependencyError, NullishSignalValueError, InvalidSignalValueError, UnsetSignalValueError, InvalidCallbackError, ReadonlySignalError, RequiredOwnerError, DuplicateKeyError, validateSignalValue, validateReadValue, validateCallback, };
|
package/types/src/graph.d.ts
CHANGED
|
@@ -117,8 +117,10 @@ declare const TYPE_SENSOR = "Sensor";
|
|
|
117
117
|
declare const TYPE_LIST = "List";
|
|
118
118
|
declare const TYPE_COLLECTION = "Collection";
|
|
119
119
|
declare const TYPE_STORE = "Store";
|
|
120
|
+
declare const TYPE_SLOT = "Slot";
|
|
120
121
|
declare const FLAG_CLEAN = 0;
|
|
121
122
|
declare const FLAG_DIRTY: number;
|
|
123
|
+
declare const FLAG_RELINK: number;
|
|
122
124
|
declare let activeSink: SinkNode | null;
|
|
123
125
|
declare let activeOwner: OwnerNode | null;
|
|
124
126
|
declare let batchDepth: number;
|
|
@@ -215,4 +217,4 @@ declare function untrack<T>(fn: () => T): T;
|
|
|
215
217
|
* ```
|
|
216
218
|
*/
|
|
217
219
|
declare function createScope(fn: () => MaybeCleanup): Cleanup;
|
|
218
|
-
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_CLEAN, FLAG_DIRTY, flush, link, propagate, refresh, registerCleanup, runCleanup, runEffect, setState, trimSources, TYPE_COLLECTION, TYPE_LIST, TYPE_MEMO, TYPE_SENSOR, TYPE_STATE, TYPE_STORE, TYPE_TASK, unlink, untrack, };
|
|
220
|
+
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_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, untrack, };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Cleanup, type EffectCallback, type MaybeCleanup, type Signal } from '../graph';
|
|
2
2
|
type MaybePromise<T> = T | Promise<T>;
|
|
3
|
-
type MatchHandlers<T extends Signal<unknown & {}>[]> = {
|
|
3
|
+
type MatchHandlers<T extends readonly Signal<unknown & {}>[]> = {
|
|
4
4
|
ok: (values: {
|
|
5
5
|
[K in keyof T]: T[K] extends Signal<infer V> ? V : never;
|
|
6
6
|
}) => MaybePromise<MaybeCleanup>;
|
|
@@ -44,5 +44,5 @@ declare function createEffect(fn: EffectCallback): Cleanup;
|
|
|
44
44
|
* @since 0.15.0
|
|
45
45
|
* @throws RequiredOwnerError If called without an active owner.
|
|
46
46
|
*/
|
|
47
|
-
declare function match<T extends Signal<unknown & {}>[]>(signals: T, handlers: MatchHandlers<T>): MaybeCleanup;
|
|
47
|
+
declare function match<T extends readonly Signal<unknown & {}>[]>(signals: readonly [...T], handlers: MatchHandlers<T>): MaybeCleanup;
|
|
48
48
|
export { type MaybePromise, type MatchHandlers, createEffect, match };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type Signal, type SignalOptions } from '../graph';
|
|
2
|
+
/**
|
|
3
|
+
* A signal that delegates its value to a swappable backing signal.
|
|
4
|
+
*
|
|
5
|
+
* Slots provide a stable reactive source at a fixed position (e.g. an object property)
|
|
6
|
+
* while allowing the backing signal to be replaced without breaking subscribers.
|
|
7
|
+
* The object shape is compatible with `Object.defineProperty()` descriptors:
|
|
8
|
+
* `get`, `set`, `configurable`, and `enumerable` are used by the property definition;
|
|
9
|
+
* `replace()` and `current()` are kept on the slot object for integration-layer control.
|
|
10
|
+
*
|
|
11
|
+
* @template T - The type of value held by the delegated signal.
|
|
12
|
+
*/
|
|
13
|
+
type Slot<T extends {}> = {
|
|
14
|
+
readonly [Symbol.toStringTag]: 'Slot';
|
|
15
|
+
/** Descriptor field: allows the property to be redefined or deleted. */
|
|
16
|
+
configurable: true;
|
|
17
|
+
/** Descriptor field: the property shows up during enumeration. */
|
|
18
|
+
enumerable: true;
|
|
19
|
+
/** Reads the current value from the delegated signal, tracking dependencies. */
|
|
20
|
+
get(): T;
|
|
21
|
+
/** Writes a value to the delegated signal. Throws `ReadonlySignalError` if the delegated signal is read-only. */
|
|
22
|
+
set(next: T): void;
|
|
23
|
+
/** Swaps the backing signal, invalidating all downstream subscribers. Narrowing (`U extends T`) is allowed. */
|
|
24
|
+
replace<U extends T>(next: Signal<U>): void;
|
|
25
|
+
/** Returns the currently delegated signal. */
|
|
26
|
+
current(): Signal<T>;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Creates a slot signal that delegates its value to a swappable backing signal.
|
|
30
|
+
*
|
|
31
|
+
* A slot acts as a stable reactive source that can be used as a property descriptor
|
|
32
|
+
* via `Object.defineProperty(target, key, slot)`. Subscribers link to the slot itself,
|
|
33
|
+
* so replacing the backing signal with `replace()` invalidates them without breaking
|
|
34
|
+
* existing edges. Setter calls forward to the current backing signal when it is writable.
|
|
35
|
+
*
|
|
36
|
+
* @since 0.18.3
|
|
37
|
+
* @template T - The type of value held by the delegated signal.
|
|
38
|
+
* @param initialSignal - The initial signal to delegate to.
|
|
39
|
+
* @param options - Optional configuration for the slot.
|
|
40
|
+
* @param options.equals - Custom equality function. Defaults to strict equality (`===`).
|
|
41
|
+
* @param options.guard - Type guard to validate values passed to `set()`.
|
|
42
|
+
* @returns A `Slot<T>` object usable both as a property descriptor and as a reactive signal.
|
|
43
|
+
*/
|
|
44
|
+
declare function createSlot<T extends {}>(initialSignal: Signal<T>, options?: SignalOptions<T>): Slot<T>;
|
|
45
|
+
/**
|
|
46
|
+
* Checks if a value is a Slot signal.
|
|
47
|
+
*
|
|
48
|
+
* @since 0.18.3
|
|
49
|
+
* @param value - The value to check
|
|
50
|
+
* @returns True if the value is a Slot
|
|
51
|
+
*/
|
|
52
|
+
declare function isSlot<T extends {} = unknown & {}>(value: unknown): value is Slot<T>;
|
|
53
|
+
export { createSlot, isSlot, type Slot };
|