preact-sigma 1.0.1 → 2.0.1
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/README.md +119 -460
- package/dist/index.d.mts +207 -193
- package/dist/index.mjs +593 -265
- package/llms.txt +343 -292
- package/package.json +1 -1
package/llms.txt
CHANGED
|
@@ -2,366 +2,417 @@
|
|
|
2
2
|
|
|
3
3
|
## Glossary
|
|
4
4
|
|
|
5
|
-
- `
|
|
6
|
-
- `
|
|
7
|
-
- `
|
|
8
|
-
- `
|
|
9
|
-
- `
|
|
10
|
-
- `
|
|
11
|
-
- `
|
|
12
|
-
- `
|
|
13
|
-
- `
|
|
14
|
-
- `
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- `
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- `
|
|
26
|
-
- `
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
5
|
+
- `sigma type`: The builder returned by `new SigmaType<...>()`. It is also the constructor for sigma-state instances after configuration.
|
|
6
|
+
- `sigma state`: An instance created from a configured sigma type.
|
|
7
|
+
- `state property`: A top-level property from `TState`, such as `draft` in `{ draft: string }`.
|
|
8
|
+
- `computed`: A tracked getter declared with `.computed({ ... })`.
|
|
9
|
+
- `query`: A tracked method declared with `.queries({ ... })` or created with `query(fn)`.
|
|
10
|
+
- `action`: A method declared with `.actions({ ... })` that reads and writes through one Immer draft for one synchronous call.
|
|
11
|
+
- `draft boundary`: Any point where sigma cannot keep reusing the current draft.
|
|
12
|
+
- `setup handler`: A function declared with `.setup(fn)` that returns an array of cleanup resources.
|
|
13
|
+
- `cleanup resource`: A cleanup function, an `AbortController`, or an object with `[Symbol.dispose]()`.
|
|
14
|
+
- `signal access`: Reading the underlying `ReadonlySignal` for a state property or computed through `instance.get(key)`.
|
|
15
|
+
|
|
16
|
+
## Navigation
|
|
17
|
+
|
|
18
|
+
- For state shape, inference, and instance shape, read `Start Here`, `Inference`, `SigmaType`, and `Public Instance Shape`.
|
|
19
|
+
- For mutation semantics, read `Critical Rules`, `actions`, `immerable`, and `setAutoFreeze`.
|
|
20
|
+
- For side effects and events, read `setup`, `Events`, `listen`, `useListener`, and `useSigma`.
|
|
21
|
+
- For committed-state utilities, read `observe`, `snapshot`, and `replaceState`.
|
|
22
|
+
|
|
23
|
+
## Start Here
|
|
24
|
+
|
|
25
|
+
- `preact-sigma` is a class-builder state API built on top of `@preact/signals` and `immer`.
|
|
26
|
+
- Use `new SigmaType<TState, TEvents>()` to build reusable constructors for sigma states.
|
|
27
|
+
- Each top-level state property gets its own signal and readonly public property.
|
|
28
|
+
- Use `computed` for argument-free derived state.
|
|
29
|
+
- Use `.queries({ ... })` for reactive reads that accept parameters.
|
|
30
|
+
- Put writes in `.actions({ ... })`.
|
|
31
|
+
- `setup` is explicit and returns cleanup resources.
|
|
32
|
+
- `observe` sees committed state changes after successful publishes.
|
|
33
|
+
- `snapshot` and `replaceState` operate on committed top-level state outside action semantics.
|
|
34
|
+
|
|
35
|
+
## Critical Rules
|
|
36
|
+
|
|
37
|
+
- Prefer explicit type arguments only on `new SigmaType<TState, TEvents>()`. Let builder methods infer from their inputs.
|
|
38
|
+
- `emit()` is a draft boundary.
|
|
39
|
+
- Any action call is a draft boundary unless it is a same-instance sync nested action call.
|
|
40
|
+
- That means cross-instance action calls and all async action calls are draft boundaries.
|
|
41
|
+
- `await` inside an async action is also a draft boundary.
|
|
42
|
+
- `this.commit()` is only needed when the current action has unpublished draft changes and is about to cross a draft boundary.
|
|
43
|
+
- A synchronous action does not need `this.commit()` when it finishes without crossing a draft boundary.
|
|
44
|
+
- Successful publishes deep-freeze draftable public state while auto-freezing is enabled.
|
|
45
|
+
- Only values that Immer considers draftable participate in that freeze path. Custom class instances without a true `[immerable]` property stay outside it.
|
|
41
46
|
|
|
42
47
|
## Runtime Exports
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
Import runtime APIs from `preact-sigma`.
|
|
45
50
|
|
|
46
51
|
```typescript
|
|
47
52
|
import {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
useSubscribe,
|
|
51
|
-
useEventTarget,
|
|
52
|
-
isManagedState,
|
|
53
|
-
query,
|
|
54
|
-
computed,
|
|
53
|
+
SigmaType,
|
|
54
|
+
action,
|
|
55
55
|
batch,
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
computed,
|
|
57
|
+
effect,
|
|
58
|
+
freeze,
|
|
59
|
+
immerable,
|
|
60
|
+
isSigmaState,
|
|
61
|
+
listen,
|
|
62
|
+
query,
|
|
63
|
+
replaceState,
|
|
64
|
+
setAutoFreeze,
|
|
65
|
+
snapshot,
|
|
66
|
+
untracked,
|
|
67
|
+
useListener,
|
|
68
|
+
useSigma,
|
|
69
|
+
} from "preact-sigma";
|
|
58
70
|
```
|
|
59
71
|
|
|
60
|
-
##
|
|
61
|
-
|
|
62
|
-
- `
|
|
63
|
-
- `
|
|
64
|
-
- `
|
|
65
|
-
- `
|
|
66
|
-
- `
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
- a managed state instance
|
|
78
|
-
- Event payloads may have zero or one argument only.
|
|
79
|
-
- Returned methods are action-wrapped automatically unless wrapped with `query()`.
|
|
80
|
-
- Returned signals and returned top-level lenses become tracked getter properties on the public instance.
|
|
81
|
-
- Returning the handle exposes the full base state as one reactive immutable public property.
|
|
82
|
-
- Returning a managed state instance exposes that nested managed state unchanged.
|
|
83
|
-
- Keyed `get`, `peek`, and `subscribe` apply only to signal-backed public properties, not to composed managed-state properties.
|
|
84
|
-
|
|
85
|
-
## Core Examples
|
|
86
|
-
|
|
87
|
-
### Defining a Manager Class
|
|
72
|
+
## Type Exports
|
|
73
|
+
|
|
74
|
+
- `AnyDefaultState`: Describes the object accepted by `.defaultState(...)`.
|
|
75
|
+
- `AnyEvents`: Describes an event map from event names to payload objects or `void`.
|
|
76
|
+
- `AnyResource`: Describes a supported setup cleanup resource.
|
|
77
|
+
- `AnySigmaState`: Describes the public shape shared by all sigma-state instances.
|
|
78
|
+
- `AnySigmaStateWithEvents`: Describes a sigma-state instance with a typed event map.
|
|
79
|
+
- `AnyState`: Describes the top-level state object for a sigma type.
|
|
80
|
+
- `InferEventType`: Infers the supported event names for a target used with `listen(...)` or `useListener(...)`.
|
|
81
|
+
- `InferListener`: Infers the listener signature for a target and event name.
|
|
82
|
+
- `InferSetupArgs`: Infers the `setup(...)` argument list for a sigma-state instance.
|
|
83
|
+
- `SigmaObserveChange`: Describes the object received by `.observe(...)` listeners.
|
|
84
|
+
- `SigmaObserveOptions`: Describes the options object accepted by `.observe(...)`.
|
|
85
|
+
- `SigmaState`: Describes the public instance shape produced by a configured sigma type.
|
|
86
|
+
|
|
87
|
+
## Builder Example
|
|
88
|
+
|
|
88
89
|
```typescript
|
|
89
|
-
import {
|
|
90
|
-
|
|
91
|
-
type
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
)
|
|
90
|
+
import { SigmaType } from "preact-sigma";
|
|
91
|
+
|
|
92
|
+
type Todo = {
|
|
93
|
+
id: string;
|
|
94
|
+
title: string;
|
|
95
|
+
completed: boolean;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
type TodoListState = {
|
|
99
|
+
draft: string;
|
|
100
|
+
todos: Todo[];
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
type TodoListEvents = {
|
|
104
|
+
added: Todo;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const TodoList = new SigmaType<TodoListState, TodoListEvents>()
|
|
108
|
+
.defaultState({
|
|
109
|
+
draft: "",
|
|
110
|
+
todos: [],
|
|
111
|
+
})
|
|
112
|
+
.computed({
|
|
113
|
+
completedCount() {
|
|
114
|
+
return this.todos.filter((todo) => todo.completed).length;
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
.queries({
|
|
118
|
+
canAddTodo() {
|
|
119
|
+
return this.draft.trim().length > 0;
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
.actions({
|
|
123
|
+
setDraft(draft: string) {
|
|
124
|
+
this.draft = draft;
|
|
125
|
+
},
|
|
126
|
+
addTodo() {
|
|
127
|
+
const todo = {
|
|
128
|
+
id: crypto.randomUUID(),
|
|
129
|
+
title: this.draft,
|
|
130
|
+
completed: false,
|
|
131
|
+
};
|
|
132
|
+
this.todos.push(todo);
|
|
133
|
+
this.draft = "";
|
|
134
|
+
this.commit();
|
|
135
|
+
this.emit("added", todo);
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const todoList = new TodoList();
|
|
121
140
|
```
|
|
122
141
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
<p>Is Even: {manager.isEven() ? 'Yes' : 'No'}</p>
|
|
135
|
-
<button onClick={() => manager.increment()}>+1</button>
|
|
136
|
-
</div>
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
```
|
|
142
|
+
## Inference
|
|
143
|
+
|
|
144
|
+
- `TState` and `TEvents` come from `new SigmaType<TState, TEvents>()`.
|
|
145
|
+
- `defaultState` comes from `.defaultState(...)`, where each property may be either a value or a zero-argument initializer that returns the value.
|
|
146
|
+
- Public computed names and return types come from `.computed(...)`.
|
|
147
|
+
- Public query names, parameter types, and return types come from `.queries(...)`.
|
|
148
|
+
- `observe(change)` types come from `.observe(...)`, and `patches` and `inversePatches` are present when that call uses `{ patches: true }`.
|
|
149
|
+
- Public action names, parameter types, and return types come from `.actions(...)`.
|
|
150
|
+
- `setup(...args)` argument types come from the first `.setup(...)` call and later setup calls reuse that same argument list.
|
|
151
|
+
- `get(key)` signal types come from `TState` and from the return types declared by `.computed(...)`.
|
|
152
|
+
- Do not pass explicit type arguments to the builder methods. Let inference come from each method input.
|
|
140
153
|
|
|
141
|
-
## `
|
|
154
|
+
## `SigmaType`
|
|
142
155
|
|
|
143
|
-
|
|
156
|
+
`new SigmaType<TState, TEvents>()` creates a mutable, reusable sigma-state builder that is also the constructor for sigma-state instances after configuration.
|
|
144
157
|
|
|
145
158
|
Behavior:
|
|
146
159
|
|
|
147
|
-
- `
|
|
148
|
-
-
|
|
149
|
-
-
|
|
150
|
-
- the
|
|
151
|
-
-
|
|
160
|
+
- `.defaultState(...)`, `.setup(...)`, `.computed(...)`, `.queries(...)`, `.observe(...)`, and `.actions(...)` all mutate the same builder and return it.
|
|
161
|
+
- Those builder methods are additive and may be called in any order.
|
|
162
|
+
- Builder method typing only exposes state helpers that existed when that builder call happened.
|
|
163
|
+
- Runtime contexts use the full accumulated builder, including definitions added by later builder calls.
|
|
164
|
+
- `defaultState` is optional and must be a plain object when provided.
|
|
165
|
+
- Function-valued `defaultState` properties act as per-instance initializers, and their return values become the state values for that instance.
|
|
166
|
+
- Constructor input must be a plain object when provided.
|
|
167
|
+
- Constructor input shallowly overrides `defaultState`.
|
|
168
|
+
- If every required state property is covered by `defaultState`, constructor input is optional.
|
|
169
|
+
- Duplicate names across state properties, computeds, queries, and actions are rejected at runtime.
|
|
170
|
+
- Reserved public names are `get`, `setup`, `on`, and `emit`.
|
|
171
|
+
|
|
172
|
+
## Public Instance Shape
|
|
173
|
+
|
|
174
|
+
A sigma-state instance exposes:
|
|
152
175
|
|
|
153
|
-
|
|
176
|
+
- one readonly enumerable own property for every state property
|
|
177
|
+
- one tracked non-enumerable getter for every computed
|
|
178
|
+
- one method for every query
|
|
179
|
+
- one method for every action
|
|
180
|
+
- `get(key): ReadonlySignal<...>` for state-property and computed keys
|
|
181
|
+
- `setup(...args): () => void` when the builder has at least one setup handler
|
|
182
|
+
- `on(name, listener): () => void`
|
|
183
|
+
- `Object.keys(instance)` includes only top-level state properties
|
|
154
184
|
|
|
155
|
-
|
|
185
|
+
## Reactivity Model
|
|
186
|
+
|
|
187
|
+
- each top-level state property is backed by its own Preact signal
|
|
188
|
+
- public state reads are reactive
|
|
189
|
+
- signal access is reactive, so reading `.value` tracks like any other Preact signal read
|
|
190
|
+
- computed getters are reactive and lazily memoized
|
|
191
|
+
- queries are reactive at the call site, including queries with arguments
|
|
192
|
+
- query calls are not memoized across invocations; each call uses a fresh `computed(...)` wrapper and does not retain that signal
|
|
193
|
+
|
|
194
|
+
## `get(key)`
|
|
195
|
+
|
|
196
|
+
`instance.get(key)` returns the underlying `ReadonlySignal` for one top-level state property or computed.
|
|
156
197
|
|
|
157
198
|
Behavior:
|
|
158
199
|
|
|
159
|
-
-
|
|
160
|
-
-
|
|
161
|
-
- returns one stable managed state instance for the component
|
|
200
|
+
- state-property keys return that property's signal
|
|
201
|
+
- computed keys return that computed getter's signal
|
|
162
202
|
|
|
163
|
-
## `
|
|
203
|
+
## `computed`
|
|
164
204
|
|
|
165
|
-
|
|
205
|
+
Computeds are added with `.computed({ ... })`.
|
|
166
206
|
|
|
167
|
-
|
|
207
|
+
Behavior:
|
|
168
208
|
|
|
169
|
-
-
|
|
170
|
-
-
|
|
171
|
-
- `
|
|
172
|
-
-
|
|
173
|
-
-
|
|
209
|
+
- each computed is exposed as a tracked getter property
|
|
210
|
+
- computed getters are non-enumerable on the public instance
|
|
211
|
+
- `this` inside a computed exposes readonly state plus other computeds
|
|
212
|
+
- computeds do not receive query or action methods on `this`
|
|
213
|
+
- computeds cannot accept arguments
|
|
174
214
|
|
|
175
|
-
|
|
215
|
+
## `queries`
|
|
176
216
|
|
|
177
|
-
|
|
178
|
-
- spreading the handle into the returned constructor object exposes all current top-level lenses as tracked public properties
|
|
217
|
+
Queries are added with `.queries({ ... })`.
|
|
179
218
|
|
|
180
|
-
|
|
219
|
+
Behavior:
|
|
181
220
|
|
|
182
|
-
|
|
221
|
+
- queries may accept arbitrary parameters
|
|
222
|
+
- `this` inside a query exposes readonly state, computeds, and other queries
|
|
223
|
+
- queries do not receive action methods on `this`
|
|
224
|
+
- when a query runs inside an action, it reads from the current draft-aware state
|
|
225
|
+
- query results are reactive at the call site but are not memoized across calls
|
|
226
|
+
- prefer `.queries({ ... })` for commonly needed instance methods
|
|
227
|
+
- not every calculation belongs in `.queries({ ... })`; keeping a calculation local to the module that uses it is often clearer until it becomes a common use case
|
|
228
|
+
- query wrappers are shared across instances
|
|
229
|
+
- query typing only exposes computeds and queries that were already present when its `.queries(...)` call happened
|
|
183
230
|
|
|
184
|
-
|
|
231
|
+
## `actions`
|
|
185
232
|
|
|
186
|
-
|
|
187
|
-
- `set(next)`: replace that property or update it with an Immer producer for that property
|
|
233
|
+
Actions are added with `.actions({ ... })`.
|
|
188
234
|
|
|
189
|
-
|
|
235
|
+
Behavior:
|
|
190
236
|
|
|
191
|
-
-
|
|
192
|
-
-
|
|
193
|
-
-
|
|
237
|
+
- actions create drafts lazily when reads or writes need draft-backed mutation semantics
|
|
238
|
+
- actions may call other actions, queries, and computeds
|
|
239
|
+
- same-instance sync nested action calls reuse the current draft
|
|
240
|
+
- any other action call starts a different invocation and is a draft boundary
|
|
241
|
+
- `emit()` is a draft boundary
|
|
242
|
+
- `await` inside an async action is a draft boundary
|
|
243
|
+
- `this.commit()` publishes the current draft immediately
|
|
244
|
+
- `this.commit()` is only needed when the current action has unpublished draft changes and is about to cross a draft boundary
|
|
245
|
+
- a synchronous action does not need `this.commit()` when it finishes without crossing a draft boundary
|
|
246
|
+
- declared async actions publish their initial synchronous draft on return
|
|
247
|
+
- after an async action resumes from `await`, top-level reads of draftable state and state writes may open a hidden draft for that async invocation
|
|
248
|
+
- non-async actions must stay synchronous; if one returns a promise, sigma throws
|
|
249
|
+
- if an async action reaches `await` or `return` with unpublished changes, the action promise rejects when it settles
|
|
250
|
+
- if an action crosses a boundary while it owns unpublished changes, sigma throws until `this.commit()` publishes them
|
|
251
|
+
- if a different invocation crosses a boundary while unpublished changes still exist, sigma warns and discards them before continuing
|
|
252
|
+
- successful publishes deep-freeze draftable public state and write it back to per-property signals while auto-freezing is enabled
|
|
253
|
+
- custom classes participate in Immer drafting only when the class opts into drafting with `[immerable] = true`
|
|
254
|
+
- actions can emit typed events with `this.emit(...)`
|
|
255
|
+
- action wrappers are shared across instances
|
|
256
|
+
- action typing only exposes computeds, queries, and actions that were already present when its `.actions(...)` call happened
|
|
257
|
+
|
|
258
|
+
Nested sigma states stored in state stay usable as values. Actions do not proxy direct mutation into a nested sigma state's internals.
|
|
259
|
+
|
|
260
|
+
## `observe`
|
|
261
|
+
|
|
262
|
+
Observers are added with `.observe(listener, options?)`.
|
|
194
263
|
|
|
195
|
-
|
|
264
|
+
Behavior:
|
|
196
265
|
|
|
197
|
-
|
|
266
|
+
- each observer runs after a successful action commit that changes base state
|
|
267
|
+
- observers do not run for actions that leave base state unchanged
|
|
268
|
+
- `change.newState` is the committed base-state snapshot for that action
|
|
269
|
+
- `change.oldState` is the base-state snapshot from before that action started
|
|
270
|
+
- `this` inside an observer exposes readonly state, computeds, and queries
|
|
271
|
+
- observers do not receive action methods or `emit(...)` on `this`
|
|
272
|
+
- same-instance sync nested action calls produce one observer notification after the outer action commits
|
|
273
|
+
- patch generation is opt-in with `{ patches: true }`
|
|
274
|
+
- when patch generation is enabled, `change.patches` and `change.inversePatches` come from Immer
|
|
275
|
+
- applications are responsible for calling `enablePatches()` before using observer patch generation
|
|
276
|
+
- observer typing only exposes computeds and queries that were already present when that `.observe(...)` call happened
|
|
198
277
|
|
|
199
|
-
|
|
200
|
-
- public methods returned by the constructor
|
|
201
|
-
- subscription methods
|
|
202
|
-
- event subscription methods
|
|
203
|
-
- disposal methods
|
|
278
|
+
## `setup`
|
|
204
279
|
|
|
205
|
-
|
|
280
|
+
Setup is added with `.setup(fn)`.
|
|
206
281
|
|
|
207
|
-
|
|
208
|
-
- `get()`: return the underlying signal for the whole public state
|
|
209
|
-
- `peek(key)`: untracked read of one exposed signal-backed public property
|
|
210
|
-
- `peek()`: untracked read of the whole public state
|
|
282
|
+
Behavior:
|
|
211
283
|
|
|
212
|
-
|
|
284
|
+
- setup is explicit; a new instance does not run setup automatically
|
|
285
|
+
- each `.setup(...)` call adds another setup handler
|
|
286
|
+
- `useSigma(...)` calls `.setup(...)` for component-owned instances that define setup
|
|
287
|
+
- calling `.setup(...)` again cleans up the previous setup first
|
|
288
|
+
- one `.setup(...)` call runs every registered setup handler in definition order
|
|
289
|
+
- the public `.setup(...)` method always returns one cleanup function
|
|
290
|
+
- `this` inside a setup handler exposes the public instance plus `emit(...)`
|
|
291
|
+
- each setup handler returns an array of cleanup resources
|
|
292
|
+
- setup typing only exposes computeds, queries, and actions that were already present when that `.setup(...)` call happened
|
|
213
293
|
|
|
214
|
-
|
|
215
|
-
- `subscribe(listener)`: subscribe to the current and future whole public state
|
|
216
|
-
- both forms return an unsubscribe function
|
|
294
|
+
Supported cleanup resources:
|
|
217
295
|
|
|
218
|
-
|
|
296
|
+
- cleanup functions
|
|
297
|
+
- objects with `[Symbol.dispose]()`
|
|
298
|
+
- `AbortController`
|
|
219
299
|
|
|
220
|
-
|
|
221
|
-
- listener receives the emitted argument directly, or no argument at all
|
|
222
|
-
- returns an unsubscribe function
|
|
300
|
+
When a parent setup wants to own a nested sigma state's setup, call the child sigma state's `setup(...)` method and return that cleanup function.
|
|
223
301
|
|
|
224
|
-
|
|
302
|
+
Cleanup runs in reverse order. If multiple cleanup steps throw, cleanup rethrows an `AggregateError`.
|
|
225
303
|
|
|
226
|
-
|
|
227
|
-
- `[Symbol.dispose]()` does the same thing as `dispose()`
|
|
304
|
+
## Events
|
|
228
305
|
|
|
229
|
-
|
|
306
|
+
Events are emitted from actions or setup through `this.emit(name, payload?)`.
|
|
307
|
+
|
|
308
|
+
Behavior:
|
|
230
309
|
|
|
231
|
-
|
|
310
|
+
- the event map controls allowed event names and payload types
|
|
311
|
+
- `void` events emit no payload
|
|
312
|
+
- object events emit one payload object
|
|
313
|
+
- `.on(name, listener)` returns an unsubscribe function
|
|
314
|
+
- listeners receive the payload directly, or no argument for `void` events
|
|
315
|
+
|
|
316
|
+
## `immerable`
|
|
317
|
+
|
|
318
|
+
`immerable` is re-exported from Immer so custom classes can opt into drafting with `[immerable] = true`.
|
|
232
319
|
|
|
233
320
|
Behavior:
|
|
234
321
|
|
|
235
|
-
-
|
|
236
|
-
-
|
|
237
|
-
-
|
|
238
|
-
-
|
|
322
|
+
- unmarked custom class instances stay outside Immer drafting
|
|
323
|
+
- marking a class with `[immerable] = true` makes that class participate in Immer drafting
|
|
324
|
+
- sigma only freezes published values that Immer considers draftable
|
|
325
|
+
- custom class instances without a true `[immerable]` property stay outside that freeze path
|
|
326
|
+
- plain objects, arrays, `Map`, and `Set` already participate in normal Immer drafting without extra markers
|
|
239
327
|
|
|
240
|
-
|
|
328
|
+
## `query(fn)`
|
|
241
329
|
|
|
242
|
-
|
|
330
|
+
`query(fn)` creates a standalone tracked query helper.
|
|
243
331
|
|
|
244
|
-
|
|
332
|
+
Behavior:
|
|
245
333
|
|
|
246
|
-
|
|
334
|
+
- it returns a function with the same parameter and return types as `fn`
|
|
335
|
+
- it evaluates `fn` inside a signal `computed(...)`
|
|
336
|
+
- its calls are reactive but not memoized across invocations
|
|
337
|
+
- it does not use an instance receiver
|
|
338
|
+
- prefer `.queries({ ... })` for commonly needed instance methods
|
|
339
|
+
- use `query(fn)` when a tracked helper is large, rarely needed, or better kept local to a consumer module for tree-shaking
|
|
340
|
+
- query helpers do not need to live on the sigma state or in the same module as it
|
|
247
341
|
|
|
248
|
-
|
|
342
|
+
## `setAutoFreeze`
|
|
249
343
|
|
|
250
|
-
|
|
344
|
+
`setAutoFreeze(autoFreeze)` controls whether sigma deep-freezes published public state at runtime.
|
|
251
345
|
|
|
252
|
-
|
|
346
|
+
Behavior:
|
|
253
347
|
|
|
254
|
-
|
|
348
|
+
- auto-freezing starts enabled
|
|
349
|
+
- `setAutoFreeze(false)` leaves later published draftable public state unfrozen
|
|
350
|
+
- `setAutoFreeze(true)` restores deep freezing for later published draftable state
|
|
351
|
+
- the setting is shared across sigma state instances
|
|
255
352
|
|
|
256
|
-
## `
|
|
353
|
+
## `snapshot`
|
|
257
354
|
|
|
258
|
-
`
|
|
355
|
+
`snapshot(instance)` returns a shallow snapshot of an instance's committed public state.
|
|
259
356
|
|
|
260
|
-
|
|
357
|
+
Behavior:
|
|
261
358
|
|
|
262
|
-
|
|
359
|
+
- the snapshot includes one own property for each top-level state key
|
|
360
|
+
- each value comes from the current committed public state
|
|
361
|
+
- the snapshot does not include computeds, queries, actions, events, or setup helpers
|
|
362
|
+
- nested sigma states remain as referenced values; `snapshot(...)` does not recurse into their internal state
|
|
363
|
+
- the return type is inferred from the instance's sigma-state definition
|
|
263
364
|
|
|
264
|
-
|
|
365
|
+
## `replaceState`
|
|
366
|
+
|
|
367
|
+
`replaceState(instance, snapshot)` replaces an instance's committed public state from a snapshot object.
|
|
265
368
|
|
|
266
369
|
Behavior:
|
|
267
370
|
|
|
268
|
-
-
|
|
269
|
-
-
|
|
270
|
-
-
|
|
271
|
-
-
|
|
272
|
-
-
|
|
371
|
+
- the replacement snapshot must be a plain object with exactly the instance's top-level state keys
|
|
372
|
+
- it updates the committed public state without going through an action method
|
|
373
|
+
- it notifies observers when the committed state changes
|
|
374
|
+
- when observer patch generation is enabled, `replaceState(...)` also delivers patches and inverse patches
|
|
375
|
+
- it throws if an action still owns unpublished changes
|
|
376
|
+
- the snapshot parameter type is inferred from the instance's sigma-state definition
|
|
377
|
+
|
|
378
|
+
## Passthrough Exports
|
|
379
|
+
|
|
380
|
+
- `action`, `batch`, `computed`, `effect`, and `untracked` are re-exported from `@preact/signals`.
|
|
381
|
+
- `freeze` is re-exported from `immer`. A frozen object cannot be mutated through Immer drafts, including inside sigma actions.
|
|
273
382
|
|
|
274
|
-
## `
|
|
383
|
+
## `useSigma`
|
|
275
384
|
|
|
276
|
-
|
|
385
|
+
`useSigma(create, setupParams?)` creates one sigma-state instance for a component and manages setup cleanup.
|
|
277
386
|
|
|
278
387
|
Behavior:
|
|
279
388
|
|
|
280
|
-
-
|
|
281
|
-
-
|
|
282
|
-
-
|
|
283
|
-
-
|
|
284
|
-
-
|
|
285
|
-
|
|
286
|
-
## `
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
-
|
|
293
|
-
-
|
|
294
|
-
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
-
|
|
303
|
-
-
|
|
304
|
-
-
|
|
305
|
-
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
- async actions (await promises inside returned methods, then call `handle.set()` or `lens.set()`)
|
|
311
|
-
|
|
312
|
-
## Unsupported Or Disallowed Patterns
|
|
313
|
-
|
|
314
|
-
- function-valued base state
|
|
315
|
-
- returning arbitrary plain objects from the constructor unless each returned property is one of the supported public shapes
|
|
316
|
-
- multi-argument event payloads outside a single object payload
|
|
317
|
-
- relying on untyped constructor parameters for state or event inference
|
|
318
|
-
- using arrays, `Map`, or `Set` as object-shaped state for lens generation
|
|
319
|
-
|
|
320
|
-
## Best Practices
|
|
321
|
-
|
|
322
|
-
### Inference And Naming
|
|
323
|
-
|
|
324
|
-
- Explicitly type the first constructor parameter as `StateHandle<...>`.
|
|
325
|
-
- Prefer a `${ModelName}State` alias near the constructor, even for simple state.
|
|
326
|
-
- Prefer a `${ModelName}Events` alias when events exist.
|
|
327
|
-
- Name the class returned by `defineManagedState()` with a `Manager` suffix.
|
|
328
|
-
- Name the handle like an instance of the state model, not a generic word like `state` or `value`.
|
|
329
|
-
|
|
330
|
-
### State Shape And Exposure
|
|
331
|
-
|
|
332
|
-
- Use top-level lenses for constructor-local reads and writes to top-level object fields.
|
|
333
|
-
- Return one top-level lens when one public field should stay reactive.
|
|
334
|
-
- Spread an object-shaped handle when the public managed state should mirror the base state's top-level shape.
|
|
335
|
-
- Return the full handle only when the whole base state should be one public property.
|
|
336
|
-
- Use `handle.own()` when the managed-state instance should control external cleanup.
|
|
337
|
-
|
|
338
|
-
### Derivations
|
|
339
|
-
|
|
340
|
-
- Prefer plain external derivation functions for ordinary derived values so unused helpers can be tree-shaken.
|
|
341
|
-
- Use `computed(() => derive(handle.get()))` only when a derived value needs memoized reactive reads for performance.
|
|
342
|
-
- Use `query()` for tracked public read methods.
|
|
343
|
-
|
|
344
|
-
### Public API Design
|
|
345
|
-
|
|
346
|
-
- Keep public actions domain-specific.
|
|
347
|
-
- Prefer verbs like `save`, `submit`, `open`, `close`, and `rename` over low-level mutation names.
|
|
348
|
-
- Avoid unnecessary binding. Public methods work through closure over the typed handle, not through `this`.
|
|
349
|
-
- Keep nested features as separate managed states when they already have a clean public API.
|
|
350
|
-
- Prefer explicit disposal when a managed state owns external resources.
|
|
351
|
-
|
|
352
|
-
### Events
|
|
353
|
-
|
|
354
|
-
- Keep events domain-specific.
|
|
355
|
-
- Do not use generic event names like `changed` or `updated` for ordinary state observation.
|
|
356
|
-
- When you need multiple event fields, wrap them in one object payload.
|
|
357
|
-
|
|
358
|
-
## Example Interpretation Rules For Agents
|
|
359
|
-
|
|
360
|
-
- If a snippet assigns `const stop = thing.on(...)`, `stop` is an unsubscribe function.
|
|
361
|
-
- If a snippet assigns `const stop = thing.subscribe(...)`, `stop` is an unsubscribe function.
|
|
362
|
-
- If a snippet calls `handle.own([...])`, those resources are released by `instance.dispose()` or `instance[Symbol.dispose]()`.
|
|
363
|
-
- If a returned method is wrapped with `query()`, treat it as a tracked read, not a mutating action.
|
|
364
|
-
- If a constructor returns `...handle`, treat each top-level object property as a public tracked property.
|
|
365
|
-
- If a constructor returns `field: handle.someField`, treat `field` as a public tracked property whose value is the lens's current value, not as the lens object itself.
|
|
366
|
-
- NEVER append `.value` when reading exposed signal-backed public properties on a manager instance (e.g., use `manager.count`, NOT `manager.count.value`). They are exposed as native getters.
|
|
367
|
-
- When calling `handle.set(draft => ...)` or `lens.set(draft => ...)`, treat it exactly like an Immer producer. Mutate the `draft` directly. Do not return a completely new object from the callback unless you intend to entirely overwrite that state.
|
|
389
|
+
- calls `create()` once per mounted component instance
|
|
390
|
+
- returns the same sigma-state instance for the component lifetime
|
|
391
|
+
- if the sigma state defines setup, calls `sigmaState.setup(...setupParams)` in an effect
|
|
392
|
+
- reruns setup when `setupParams` change
|
|
393
|
+
- the cleanup returned by `setup(...)` runs when `setupParams` change or when the component unmounts
|
|
394
|
+
|
|
395
|
+
## `listen`
|
|
396
|
+
|
|
397
|
+
`listen(target, name, listener)` adds an event listener and returns a cleanup function.
|
|
398
|
+
|
|
399
|
+
Behavior:
|
|
400
|
+
|
|
401
|
+
- it subscribes with `addEventListener(...)` and returns a cleanup function that removes that listener
|
|
402
|
+
- for sigma-state targets, the listener receives the typed payload directly
|
|
403
|
+
- for DOM targets, the listener receives the typed DOM event object
|
|
404
|
+
|
|
405
|
+
## `useListener`
|
|
406
|
+
|
|
407
|
+
`useListener(target, name, listener)` attaches an event listener inside a component.
|
|
408
|
+
|
|
409
|
+
Behavior:
|
|
410
|
+
|
|
411
|
+
- subscribes in `useEffect`
|
|
412
|
+
- unsubscribes automatically when `target` or `name` changes or when the component unmounts
|
|
413
|
+
- keeps the latest listener callback without requiring it in the effect dependency list
|
|
414
|
+
- passing `null` disables the listener
|
|
415
|
+
|
|
416
|
+
## `isSigmaState`
|
|
417
|
+
|
|
418
|
+
`isSigmaState(value)` checks whether a value is a sigma-state instance.
|