@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 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
@@ -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
 
@@ -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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.18.3
4
+
5
+ ### Added
6
+
7
+ - **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`.
8
+
9
+ ### Fixed
10
+
11
+ - **`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.
12
+
3
13
  ## 0.18.2
4
14
 
5
15
  ### 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[]
@@ -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.3
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