@zeix/cause-effect 0.18.2 → 0.18.4

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 CHANGED
@@ -19,7 +19,7 @@ The reactive engine is a linked graph of source and sink nodes connected by `Edg
19
19
  ### Node Types
20
20
  ```
21
21
  StateNode<T> — source-only with equality + guard (State, Sensor)
22
- MemoNode<T> — source + sink (Memo, Store, List, Collection)
22
+ MemoNode<T> — source + sink (Memo, Slot, Store, List, Collection)
23
23
  TaskNode<T> — source + sink + async (Task)
24
24
  EffectNode — sink + owner (Effect)
25
25
  Scope — owner-only (createScope)
@@ -33,6 +33,7 @@ Scope — owner-only (createScope)
33
33
  - **Store** (`createStore`): Mutable object signals with individually reactive properties via Proxy
34
34
  - **List** (`createList`): Mutable array signals with stable keys and reactive items
35
35
  - **Collection** (`createCollection`): Reactive collections — either externally-driven with watched lifecycle, or derived from List/Collection with item-level memoization
36
+ - **Slot** (`createSlot`): Stable delegation signal with swappable backing signal, designed for integration layers (property descriptors, custom elements)
36
37
  - **Effect** (`createEffect`): Side effect handlers that react to signal changes
37
38
 
38
39
  ### Key Principles
@@ -56,7 +57,8 @@ cause-effect/
56
57
  │ │ ├── effect.ts # createEffect, match — side effects
57
58
  │ │ ├── store.ts # createStore — reactive object stores
58
59
  │ │ ├── list.ts # createList — reactive arrays with stable keys
59
- │ │ └── collection.ts # createCollection — externally-driven and derived collections
60
+ │ │ ├── collection.ts # createCollection — externally-driven and derived collections
61
+ │ │ └── slot.ts # createSlot — stable delegation with swappable backing signal
60
62
  │ ├── util.ts # Utility functions and type checks
61
63
  │ └── ...
62
64
  ├── index.ts # Entry point / main export file
@@ -135,6 +137,12 @@ const feed = createCollection<{ id: string; text: string }>((applyChanges) => {
135
137
  ws.onmessage = (e) => applyChanges(JSON.parse(e.data))
136
138
  return () => ws.close()
137
139
  }, { keyConfig: item => item.id })
140
+
141
+ // Slot for stable property delegation
142
+ const local = createState('default')
143
+ const slot = createSlot(local)
144
+ Object.defineProperty(element, 'label', slot)
145
+ slot.replace(createMemo(() => parentState.get())) // swap backing signal
138
146
  ```
139
147
 
140
148
  ### Reactivity
@@ -195,8 +203,8 @@ const processed = items.deriveCollection(item => item.toUpperCase())
195
203
  - Pure function annotations: `/*#__PURE__*/` for tree-shaking
196
204
 
197
205
  ### Naming Conventions
198
- - Factory functions: `create*` prefix (createState, createMemo, createEffect, createStore, createList, createCollection, createSensor)
199
- - Type predicates: `is*` prefix (isState, isMemo, isTask, isStore, isList, isCollection, isSensor)
206
+ - Factory functions: `create*` prefix (createState, createMemo, createEffect, createStore, createList, createCollection, createSensor, createSlot)
207
+ - Type predicates: `is*` prefix (isState, isMemo, isTask, isStore, isList, isCollection, isSensor, isSlot)
200
208
  - Type constants: `TYPE_*` format (TYPE_STATE, TYPE_STORE, TYPE_SENSOR, TYPE_COLLECTION)
201
209
  - Callback types: `*Callback` suffix (MemoCallback, TaskCallback, EffectCallback, SensorCallback, CollectionCallback, DeriveCollectionCallback)
202
210
 
@@ -20,6 +20,7 @@ Cause & Effect is a reactive state management library for JavaScript/TypeScript
20
20
  - **Store** (`createStore`): Proxy-based reactive objects with per-property State/Store signals
21
21
  - **List** (`createList`): Reactive arrays with stable keys and per-item State signals
22
22
  - **Collection** (`createCollection`): Reactive collections — either externally-driven with watched lifecycle, or derived from List/Collection with item-level memoization
23
+ - **Slot** (`createSlot`): Stable delegation signal with swappable backing signal, designed for integration layers (property descriptors, custom elements)
23
24
  - **Effect** (`createEffect`): Side effects that react to signal changes
24
25
 
25
26
  ## Key Files Structure
@@ -34,6 +35,7 @@ Cause & Effect is a reactive state management library for JavaScript/TypeScript
34
35
  - `src/nodes/store.ts` - createStore, isStore, Store type, diff, isEqual
35
36
  - `src/nodes/list.ts` - createList, isList, List type
36
37
  - `src/nodes/collection.ts` - createCollection, isCollection, Collection type, deriveCollection (internal)
38
+ - `src/nodes/slot.ts` - createSlot, isSlot, Slot type
37
39
  - `src/util.ts` - Utility functions and type checks
38
40
  - `index.ts` - Entry point / main export file
39
41
 
@@ -47,8 +49,8 @@ Cause & Effect is a reactive state management library for JavaScript/TypeScript
47
49
  - JSDoc comments for all public APIs
48
50
 
49
51
  ### Naming Conventions
50
- - Factory functions: `create*` (e.g., `createState`, `createMemo`, `createEffect`, `createStore`, `createList`, `createCollection`, `createSensor`)
51
- - Type predicates: `is*` (e.g., `isState`, `isMemo`, `isStore`, `isList`, `isCollection`, `isSensor`)
52
+ - Factory functions: `create*` (e.g., `createState`, `createMemo`, `createEffect`, `createStore`, `createList`, `createCollection`, `createSensor`, `createSlot`)
53
+ - Type predicates: `is*` (e.g., `isState`, `isMemo`, `isStore`, `isList`, `isCollection`, `isSensor`, `isSlot`)
52
54
  - Type constants: `TYPE_*` for internal type tags
53
55
  - Callback types: `*Callback` suffix (MemoCallback, TaskCallback, EffectCallback, SensorCallback, CollectionCallback, DeriveCollectionCallback)
54
56
  - Private variables: use descriptive names, no underscore prefix
@@ -148,6 +150,12 @@ const feed = createCollection<{ id: string; text: string }>((applyChanges) => {
148
150
  ws.onmessage = (e) => applyChanges(JSON.parse(e.data))
149
151
  return () => ws.close()
150
152
  }, { keyConfig: item => item.id })
153
+
154
+ // Slot for stable property delegation
155
+ const local = createState('default')
156
+ const slot = createSlot(local)
157
+ Object.defineProperty(element, 'label', slot)
158
+ slot.replace(createMemo(() => parentState.get())) // swap backing signal
151
159
  ```
152
160
 
153
161
  ### Reactivity
package/ARCHITECTURE.md CHANGED
@@ -106,14 +106,14 @@ The first four flags (`CLEAN`/`CHECK`/`DIRTY`/`RUNNING`) are used by the core gr
106
106
 
107
107
  `FLAG_RELINK` is used exclusively by composite signal types (Store, List, Collection, deriveCollection) that manage their own child signals. When a structural mutation adds or removes child signals, the node is flagged `FLAG_DIRTY | FLAG_RELINK`. On the next `get()`, the composite signal's fast path reads the flag: if `FLAG_RELINK` is set, it forces a tracked `refresh()` after rebuilding the value so that `recomputeMemo()` can call `link()` for new child signals and `trimSources()` for removed ones. This avoids the previous approach of nulling `node.sources`/`node.sourcesTail`, which orphaned edges in upstream sink lists. `FLAG_RELINK` is always cleared by `recomputeMemo()`, which assigns `node.flags = FLAG_RUNNING` (clearing all bits) at the start of recomputation.
108
108
 
109
- ### The `propagate(node)` Function
109
+ ### The `propagate(node, newFlag?)` Function
110
110
 
111
- When a source value changes, `propagate()` walks its sink list:
111
+ When a source value changes, `propagate()` walks its sink list. The `newFlag` parameter defaults to `FLAG_DIRTY` but callers may pass `FLAG_CHECK` for speculative invalidation (e.g., watched callbacks where the source value may not have actually changed).
112
112
 
113
- - **Memo/Task sinks** (have `sinks` field): Flagged `DIRTY`. Their own sinks are recursively flagged `CHECK`. If the node has an in-flight `AbortController`, it is aborted immediately.
114
- - **Effect sinks** (no `sinks` field): Flagged `DIRTY` and pushed onto the `queuedEffects` array for later execution.
113
+ - **Memo/Task sinks** (have `sinks` field): Flagged with `newFlag` (typically `DIRTY`). Their own sinks are recursively flagged `CHECK`. If the node has an in-flight `AbortController`, it is aborted immediately. Short-circuits if the node already carries an equal or higher flag.
114
+ - **Effect sinks** (no `sinks` field): Flagged with `newFlag` and pushed onto the `queuedEffects` array. An effect is only enqueued once — subsequent propagations escalate the flag (e.g., `CHECK` → `DIRTY`) without re-enqueuing. The flag is assigned (not OR'd) to clear `FLAG_RUNNING`, preserving the existing pattern where a state update inside a running effect triggers a re-run.
115
115
 
116
- The two-level flagging (`DIRTY` for direct dependents, `CHECK` for transitive) avoids unnecessary recomputation. A `CHECK` node only recomputes if, upon inspection during `refresh()`, one of its sources turns out to have actually changed.
116
+ The two-level flagging (`DIRTY` for direct dependents, `CHECK` for transitive) avoids unnecessary recomputation. A `CHECK` node only recomputes if, upon inspection during `refresh()`, one of its sources turns out to have actually changed. This applies equally to memo, task, and effect nodes.
117
117
 
118
118
  ### The `refresh(node)` Function
119
119
 
@@ -141,7 +141,7 @@ If `FLAG_RUNNING` is encountered, a `CircularDependencyError` is thrown.
141
141
 
142
142
  ### The `flush()` Function
143
143
 
144
- `flush()` iterates over `queuedEffects`, calling `refresh()` on each effect that is still `DIRTY`. A `flushing` guard prevents re-entrant flushes. Effects that were enqueued during the flush (due to async resolution or nested state changes) are processed in the same pass, since `flush()` reads the array length dynamically.
144
+ `flush()` iterates over `queuedEffects`, calling `refresh()` on each effect that is still `DIRTY` or `CHECK`. A `flushing` guard prevents re-entrant flushes. Effects that were enqueued during the flush (due to async resolution or nested state changes) are processed in the same pass, since `flush()` reads the array length dynamically. Effects with only `FLAG_CHECK` enter `refresh()`, which walks their sources — if no source value actually changed, the effect is cleaned without running.
145
145
 
146
146
  ### Effect Lifecycle
147
147
 
@@ -178,7 +178,7 @@ Creates an ownership scope without an effect. The scope becomes `activeOwner` du
178
178
 
179
179
  ### State (`src/nodes/state.ts`)
180
180
 
181
- **Graph node**: `StateNode<T>` (source only)
181
+ **Graph node**: `MemoNode<T>` (source + sink, single delegated dependency)
182
182
 
183
183
  A mutable value container. The simplest signal type — `get()` links and returns the value, `set()` validates, calls `setState()`, which propagates changes to dependents.
184
184
 
@@ -209,7 +209,7 @@ The `error` field preserves thrown errors: if `fn` throws, the error is stored a
209
209
 
210
210
  **Reducer pattern**: The `prev` parameter enables state accumulation across recomputations without writable state.
211
211
 
212
- **Watched lifecycle**: An optional `watched` callback in options provides lazy external invalidation. The callback receives an `invalidate` function and is invoked on first sink attachment. Calling `invalidate()` marks the node `FLAG_DIRTY`, propagates to sinks, and flushes triggering re-evaluation of the memo's `fn` without changing any tracked dependency. The returned cleanup is stored as `node.stop` and called when the last sink detaches. This enables patterns like DOM observation (MutationObserver) where the memo re-derives its value in response to external events.
212
+ **Watched lifecycle**: An optional `watched` callback in options provides lazy external invalidation. The callback receives an `invalidate` function and is invoked on first sink attachment. Calling `invalidate()` calls `propagate(node)` on the memo itself, which marks it `FLAG_DIRTY` and propagates `FLAG_CHECK` to downstream sinks, then flushes. During flush, downstream effects verify the memo via `refresh()` — if the memo's `equals` function determines the recomputed value is unchanged, the effect is cleaned without running. The returned cleanup is stored as `node.stop` and called when the last sink detaches. This enables patterns like DOM observation (MutationObserver) where a memo re-derives its value in response to external events, with the `equals` check respected at every level of the graph.
213
213
 
214
214
  ### Task (`src/nodes/task.ts`)
215
215
 
@@ -221,7 +221,7 @@ During dependency tracking, only the synchronous preamble of `fn` is tracked (be
221
221
 
222
222
  `isPending()` returns `true` while a computation is in flight. `abort()` cancels the current computation manually. Errors are preserved like Memo, but old values are retained on errors (the last successful result remains accessible).
223
223
 
224
- **Watched lifecycle**: Same pattern as Memo — an optional `watched` callback receives `invalidate` and is invoked on first sink attachment. Calling `invalidate()` marks the node dirty and triggers re-execution, which aborts any in-flight computation via the existing `AbortController` mechanism before starting a new one.
224
+ **Watched lifecycle**: Same pattern as Memo — an optional `watched` callback receives `invalidate` and is invoked on first sink attachment. Calling `invalidate()` calls `propagate(node)` on the task itself, which marks it dirty, aborts any in-flight computation eagerly via the `AbortController`, and propagates `FLAG_CHECK` to downstream sinks. Effects only re-run if the task's resolved value actually changes.
225
225
 
226
226
  ### Effect (`src/nodes/effect.ts`)
227
227
 
@@ -231,6 +231,18 @@ A side-effecting computation that runs immediately and re-runs when dependencies
231
231
 
232
232
  Effects double as owners: they have a `cleanup` field and become `activeOwner` during execution. Child effects and scopes created during execution are automatically disposed when the parent effect re-runs or is disposed.
233
233
 
234
+ ### Slot (`src/nodes/slot.ts`)
235
+
236
+ **Graph node**: `MemoNode<T>` (source + sink)
237
+
238
+ A stable reactive source that delegates reads and writes to a swappable backing signal. Designed for integration layers (e.g. custom element systems) where a property position must switch its backing signal — from a local `State` to a parent-controlled `Memo`, for example — without breaking existing subscribers.
239
+
240
+ The slot object doubles as a property descriptor: its `get`, `set`, `configurable`, and `enumerable` fields can be passed directly to `Object.defineProperty()`. Control methods (`replace()`, `current()`) live on the same object but are ignored by the property definition; integration code should retain the slot reference for later `replace()` calls.
241
+
242
+ **Graph behavior**: Sinks link to the slot (stable across replacements). The slot links upstream to exactly one delegated signal at a time. On `replace(nextSignal)`, the slot updates its internal reference, flags sinks dirty via `propagate()`, and flushes. Re-running sinks call `slot.get()`, which triggers `refresh()` — dependency tracking (`link` + `trimSources`) re-subscribes to the new backing signal and drops stale edges to the old one. Setter calls forward to the delegated signal when writable; `ReadonlySignalError` is thrown otherwise.
243
+
244
+ Options mirror State: optional `guard` and `equals`. Type-level replacement follows `replace<U extends T>(next)` — narrowing is allowed, widening is not.
245
+
234
246
  ### Store (`src/nodes/store.ts`)
235
247
 
236
248
  **Graph node**: `MemoNode<T>` (source + sink, used for structural reactivity)
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.18.4
4
+
5
+ ### Fixed
6
+
7
+ - **Watched `invalidate()` now respects `equals` at every graph level**: Previously, calling `invalidate()` from a Memo or Task `watched` callback propagated `FLAG_DIRTY` directly to effect sinks, causing unconditional re-runs even when the recomputed value was unchanged. Now `invalidate()` delegates to `propagate(node)`, which marks the node itself `FLAG_DIRTY` and propagates `FLAG_CHECK` to downstream sinks. During flush, effects verify their sources via `refresh()` — if the memo's `equals` function determines the value is unchanged, the effect is cleaned without running. This eliminates unnecessary effect executions for watched memos with custom equality or stable return values.
8
+
9
+ ### Changed
10
+
11
+ - **`propagate()` supports `FLAG_CHECK` for effect nodes**: The effect branch of `propagate()` now respects the `newFlag` parameter instead of unconditionally setting `FLAG_DIRTY`. Effects are enqueued only on first notification; subsequent propagations escalate the flag (e.g., `CHECK` → `DIRTY`) without re-enqueuing.
12
+ - **`flush()` processes `FLAG_CHECK` effects**: The flush loop now calls `refresh()` on effects with either `FLAG_DIRTY` or `FLAG_CHECK`, enabling the check-sources-first path for effects.
13
+ - **Task `invalidate()` aborts eagerly**: Task watched callbacks now abort in-flight computations immediately during `propagate()` rather than deferring to `recomputeTask()`, consistent with the normal dependency-change path.
14
+
15
+ ## 0.18.3
16
+
17
+ ### Added
18
+
19
+ - **Slot signal (`createSlot`, `isSlot`)**: A stable reactive source that delegates reads and writes to a swappable backing signal. Designed for integration layers (e.g. custom element systems) where a property position must switch its backing signal — from a local writable `State` to a parent-controlled `Memo` — without breaking existing subscribers. The slot object doubles as a property descriptor for `Object.defineProperty()`. `replace(nextSignal)` swaps the backing signal and invalidates downstream subscribers; `current()` returns the currently delegated signal. Options mirror State: optional `guard` and `equals`.
20
+
21
+ ### Fixed
22
+
23
+ - **`match()` now preserves tuple types**: The `ok` handler correctly receives per-position types (e.g., `[number, string]`) instead of a widened union (e.g., `(number | string)[]`). The `signals` parameter and `MatchHandlers` type now use `readonly [...T]` to preserve tuple inference.
24
+
3
25
  ## 0.18.2
4
26
 
5
27
  ### Fixed
package/CLAUDE.md CHANGED
@@ -20,6 +20,7 @@ Think of signals as **observable cells** in a spreadsheet:
20
20
  - **Store signals** are structured tables where individual columns (properties) are reactive
21
21
  - **List signals** are tables with stable row IDs that survive sorting and reordering
22
22
  - **Collection signals** are externally-fed reactive tables with a watched lifecycle (WebSocket, SSE), or derived tables from Lists/Collections via `.deriveCollection()`
23
+ - **Slot signals** are stable cell references that delegate to a swappable backing cell — used by integration layers to swap a property's backing signal without breaking subscribers
23
24
  - **Effects** are event handlers that trigger side effects when cells change
24
25
 
25
26
  ## Architectural Deep Dive
@@ -37,7 +38,7 @@ The core of reactivity lies in the linked graph system (`src/graph.ts`):
37
38
 
38
39
  ```
39
40
  StateNode<T> — source-only with equality + guard (State, Sensor)
40
- MemoNode<T> — source + sink (Memo, Store, List, Collection)
41
+ MemoNode<T> — source + sink (Memo, Slot, Store, List, Collection)
41
42
  TaskNode<T> — source + sink + async (Task)
42
43
  EffectNode — sink + owner (Effect)
43
44
  Scope — owner-only (createScope)
@@ -70,6 +71,14 @@ type Task<T extends {}> = {
70
71
  abort(): void
71
72
  }
72
73
 
74
+ // Slot delegates to a swappable backing signal
75
+ type Slot<T extends {}> = {
76
+ get(): T
77
+ set(next: T): void
78
+ replace<U extends T>(next: Signal<U>): void
79
+ current(): Signal<T>
80
+ }
81
+
73
82
  // Collection interface
74
83
  type Collection<T extends {}> = {
75
84
  get(): T[]
@@ -117,7 +126,7 @@ The generic constraint `T extends {}` is crucial - it excludes `null` and `undef
117
126
 
118
127
  Computed signals implement smart memoization:
119
128
  - **Dependency Tracking**: Automatically tracks which signals are accessed during computation via `link()`
120
- - **Stale Detection**: Flag-based dirty checking (CLEAN, CHECK, DIRTY) — only recalculates when dependencies actually change
129
+ - **Stale Detection**: Flag-based dirty checking (CLEAN, CHECK, DIRTY) — only recalculates when dependencies actually change. The `equals` check is respected at every graph level: when a memo recomputes to the same value, downstream memos *and* effects are cleaned without running.
121
130
  - **Async Support**: `createTask` handles Promise-based computations with automatic AbortController cancellation
122
131
  - **Error Handling**: Preserves error states and prevents cascade failures
123
132
  - **Reducer Capabilities**: Access to previous value enables state accumulation and transitions
@@ -265,7 +274,7 @@ const processed = todoList
265
274
  **Memo (`createMemo`)**:
266
275
  - Synchronous derived computations with memoization
267
276
  - Reducer pattern with previous value access
268
- - Optional `watched(invalidate)` callback for lazy external invalidation (e.g., MutationObserver)
277
+ - Optional `watched(invalidate)` callback for lazy external invalidation (e.g., MutationObserver). Calling `invalidate()` marks the memo dirty and propagates `FLAG_CHECK` to sinks — effects only re-run if the memo's `equals` check determines the value actually changed.
269
278
 
270
279
  ```typescript
271
280
  const doubled = createMemo(() => count.get() * 2)
@@ -286,7 +295,7 @@ const runningTotal = createMemo(prev => prev + currentValue.get(), { value: 0 })
286
295
 
287
296
  **Task (`createTask`)**:
288
297
  - Async computations with automatic cancellation
289
- - Optional `watched(invalidate)` callback for lazy external invalidation
298
+ - Optional `watched(invalidate)` callback for lazy external invalidation. Calling `invalidate()` eagerly aborts any in-flight computation and propagates `FLAG_CHECK` to sinks.
290
299
 
291
300
  ```typescript
292
301
  const userData = createTask(async (prev, abort) => {
@@ -297,6 +306,21 @@ const userData = createTask(async (prev, abort) => {
297
306
  })
298
307
  ```
299
308
 
309
+ **Slot (`createSlot`)**:
310
+ - Property positions that must swap their backing signal without breaking subscribers
311
+ - Integration layers (custom elements, component systems) using `Object.defineProperty()`
312
+ - The slot object is a valid property descriptor (`get`, `set`, `configurable`, `enumerable`)
313
+
314
+ ```typescript
315
+ const local = createState('default')
316
+ const slot = createSlot(local)
317
+ Object.defineProperty(element, 'label', slot)
318
+
319
+ // Swap backing signal — subscribers re-run automatically
320
+ const parentLabel = createMemo(() => parentState.get())
321
+ slot.replace(parentLabel)
322
+ ```
323
+
300
324
  ### Error Handling Strategies
301
325
 
302
326
  The library provides several layers of error handling:
package/GUIDE.md CHANGED
@@ -296,3 +296,25 @@ const windowSize = createSensor((set) => {
296
296
  ```
297
297
 
298
298
  The start callback runs lazily — only when an effect first reads the sensor. When no effects are watching, the cleanup runs automatically. When an effect reads it again, the start callback runs again. No manual setup/teardown.
299
+
300
+ ### Slot: stable property delegation
301
+
302
+ If you are building a component system, you often need to expose signals as object properties via `Object.defineProperty()`. The challenge arises when a property must switch its backing signal — for example, from a local writable `State` to a parent-controlled read-only `Memo` — without breaking existing subscribers.
303
+
304
+ `createSlot()` solves this by providing a stable reactive source that delegates to a swappable backing signal. The slot object itself is a valid property descriptor:
305
+
306
+ ```ts
307
+ import { createState, createMemo, createSlot, createEffect } from '@zeix/cause-effect'
308
+
309
+ const local = createState('default')
310
+ const slot = createSlot(local)
311
+ Object.defineProperty(element, 'label', slot)
312
+
313
+ createEffect(() => console.log(element.label)) // logs: "default"
314
+
315
+ // Parent provides a derived value — swap without breaking the effect
316
+ const parentLabel = createMemo(() => `Parent: ${parentState.get()}`)
317
+ slot.replace(parentLabel) // effect re-runs with new value
318
+ ```
319
+
320
+ Setter calls forward to the current backing signal when it is writable. If the backing signal is read-only (e.g. a Memo), setting throws `ReadonlySignalError`. The `replace()` and `current()` methods are on the slot object but not on the installed property — keep the slot reference for later control.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Cause & Effect
2
2
 
3
- Version 0.18.2
3
+ Version 0.18.4
4
4
 
5
5
  **Cause & Effect** is a reactive state management primitives library for TypeScript. It provides the foundational building blocks for managing complex, dynamic, composite, and asynchronous state — correctly and performantly — in a unified signal graph.
6
6
 
@@ -27,6 +27,7 @@ Every signal type participates in the same dependency graph with the same propag
27
27
  | **Store** | Reactive object (keyed properties, proxy-based) | `createStore()` |
28
28
  | **List** | Reactive array (keyed items, stable identity) | `createList()` |
29
29
  | **Collection** | Reactive collection (external source or derived, item-level memoization) | `createCollection()` |
30
+ | **Slot** | Stable delegation for integration layers (swappable backing signal) | `createSlot()` |
30
31
  | **Effect** | Side-effect sink (terminal) | `createEffect()` |
31
32
 
32
33
  ## Design Principles
@@ -322,6 +323,33 @@ const processed = users
322
323
  .deriveCollection(user => user.active ? `Active: ${user.name}` : `Inactive: ${user.name}`)
323
324
  ```
324
325
 
326
+ ### Slot
327
+
328
+ A stable reactive source that delegates to a swappable backing signal. Designed for integration layers (e.g. custom element systems) where a property must switch its backing signal without breaking subscribers. The slot object doubles as a property descriptor for `Object.defineProperty()`:
329
+
330
+ ```js
331
+ import { createState, createMemo, createSlot, createEffect } from '@zeix/cause-effect'
332
+
333
+ const local = createState(1)
334
+ const slot = createSlot(local)
335
+
336
+ // Use as a property descriptor
337
+ const target = {}
338
+ Object.defineProperty(target, 'value', slot)
339
+
340
+ createEffect(() => console.log(target.value)) // logs: 1
341
+
342
+ // Swap the backing signal — subscribers re-run automatically
343
+ const derived = createMemo(() => 42)
344
+ slot.replace(derived) // logs: 42
345
+
346
+ // Write through to the current backing signal
347
+ slot.replace(local)
348
+ target.value = 10 // sets local to 10
349
+ ```
350
+
351
+ `replace()` and `current()` are available on the slot object but are not installed on the property — keep the slot reference for later control. Setting via the property forwards to the delegated signal; throws `ReadonlySignalError` if the current backing signal is read-only.
352
+
325
353
  ### Effect
326
354
 
327
355
  A side-effect sink that runs whenever the signals it reads change. Effects are terminal — they consume values but produce none. The returned function disposes the effect:
@@ -418,6 +446,10 @@ Does the data come from *outside* the reactive system?
418
446
  └─ Is it derived / read-only transformation of a `List` or `Collection`?
419
447
  └─ Yes → `.deriveCollection()`
420
448
  (memoized + supports async mapping + chaining)
449
+
450
+ Do you need a *stable property position* that can swap its backing signal?
451
+ └─ Yes → `createSlot(existingSignal)`
452
+ (integration layers, custom elements, property descriptors)
421
453
  ```
422
454
 
423
455
  ## Advanced Usage
package/REQUIREMENTS.md CHANGED
@@ -32,7 +32,7 @@ All signals enforce `T extends {}`, excluding `null` and `undefined` at the type
32
32
  Every signal type participates in the same dependency graph with the same propagation, batching, and cleanup semantics. Composite signals (Store, List, Collection) and async signals (Task) are first-class citizens, not afterthoughts. The goal is that all state which is derivable can be derived.
33
33
 
34
34
  ### Minimal Surface, Maximum Coverage
35
- The library ships 8 signal types — each justified by a distinct role in the graph and a distinct data structure it manages:
35
+ The library ships 9 signal types — each justified by a distinct role in the graph and a distinct data structure it manages:
36
36
 
37
37
  | Type | Role | Data Structure |
38
38
  |------|------|----------------|
@@ -41,6 +41,7 @@ The library ships 8 signal types — each justified by a distinct role in the gr
41
41
  | **Memo** | Synchronous derivation | Single value (memoized) |
42
42
  | **Task** | Asynchronous derivation | Single value (memoized, cancellable) |
43
43
  | **Effect** | Side-effect sink | None (terminal) |
44
+ | **Slot** | Stable delegation (integration layer) | Single value (swappable backing signal) |
44
45
  | **Store** | Reactive object | Keyed properties (proxy-based) |
45
46
  | **List** | Reactive array | Keyed items (stable identity) |
46
47
  | **Collection** | Reactive collection (external source or derived) | Keyed items (lazy lifecycle, item-level memoization) |
@@ -63,7 +64,7 @@ The library uses no browser-specific APIs in its core. Environment-specific beha
63
64
  | Usage | Target |
64
65
  |-------|--------|
65
66
  | Core signals only (State, Memo, Task, Effect) | Below 5 kB gzipped |
66
- | Full library (all 8 signal types + utilities) | Below 10 kB gzipped |
67
+ | Full library (all 9 signal types + utilities) | Below 10 kB gzipped |
67
68
 
68
69
  The library must remain tree-shakable: importing only what you use should not pull in unrelated signal types.
69
70
 
@@ -79,7 +80,7 @@ The following are explicitly out of scope and will not be added to the library:
79
80
  - **Persistence**: No serialization, no local storage, no database integration. State enters and leaves the graph through signals; how it is stored is not this library's concern.
80
81
  - **Framework-specific bindings**: No React hooks, no Vue composables, no Angular decorators. Consuming libraries build their own integrations.
81
82
  - **DevTools protocol**: Debugging is straightforward by design — attaching an effect to any signal reveals its current value and update behavior. A dedicated debugging protocol adds complexity without proportional value.
82
- - **Additional signal types**: The 8 signal types are considered complete. New types would only be considered if major Web Platform changes shift the optimal way to achieve the library's existing goals.
83
+ - **Additional signal types**: The 9 signal types are considered complete. New types would only be considered if major Web Platform changes shift the optimal way to achieve the library's existing goals.
83
84
 
84
85
  ## Stability
85
86