@zeix/cause-effect 0.18.1 → 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.
@@ -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.1
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';
@@ -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, };
@@ -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 };