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.
Files changed (5) hide show
  1. package/README.md +119 -460
  2. package/dist/index.d.mts +207 -193
  3. package/dist/index.mjs +593 -265
  4. package/llms.txt +343 -292
  5. package/package.json +1 -1
package/llms.txt CHANGED
@@ -2,366 +2,417 @@
2
2
 
3
3
  ## Glossary
4
4
 
5
- - `base state`: The private mutable state owned by one managed state definition. It may be any non-function value, including a primitive.
6
- - `public state`: The immutable data exposed on a managed state instance. It is derived from returned signals, returned top-level lenses, returned handles, and returned nested managed states.
7
- - `managed state`: An instance created by `defineManagedState()` or returned by `useManagedState()`. It exposes immutable public data, public methods, subscriptions, events, and disposal.
8
- - `manager class`: The class returned by `defineManagedState()`. Best practice is to name it with a `Manager` suffix.
9
- - `constructor`: The function passed to `defineManagedState()` or `useManagedState()`. It receives a typed `StateHandle`.
10
- - `handle`: The first constructor parameter. Its type must be `StateHandle<...>`. It is the private control surface for reading, writing, owning resources, and emitting.
11
- - `lens`: A constructor-local object for one top-level property of an object-shaped base state. A lens has `get()` and `set()`.
12
- - `signal`: A `ReadonlySignal` from `@preact/signals`.
13
- - `signal-backed public property`: A public property backed by a returned signal, returned lens, or returned handle. Keyed `get`, `peek`, and `subscribe` target only these properties.
14
- - `derived value`: A value computed from state rather than stored directly.
15
- - `public action`: A returned method intended to change state. Returned methods are action-wrapped by default.
16
- - `query method`: A returned method wrapped with `query()`. Query methods are tracked reads and are not action-wrapped.
17
- - `event map`: A type mapping event names to zero-or-one-argument tuples, for example `{ saved: []; selected: [{ id: string }] }`.
18
- - `cleanup`: A function of type `() => void`.
19
- - `disposable`: An object with a `[Symbol.dispose]()` method.
20
- - `owned resource`: A cleanup function or disposable registered through `handle.own()`.
21
- - `object-shaped state`: A base state that is a plain object. It is not a function, array, `Map`, or `Set`.
22
- - `top-level property`: A direct property on an object-shaped base state, such as `query` in `{ query: string }`.
23
- - `composition`: Returning one managed state instance from another managed state constructor so the nested instance is exposed unchanged as a public property.
24
- - `tracked read`: A read that participates in Signals tracking.
25
- - `untracked read`: A read that does not participate in Signals tracking.
26
- - `unsubscribe`: The cleanup function returned by `.on()` and `.subscribe()`.
27
-
28
- ## Purpose
29
-
30
- `preact-sigma` is a small state-management layer built on top of `@preact/signals` and `immer`.
31
-
32
- It is designed to:
33
-
34
- - keep mutable implementation details private
35
- - expose immutable public data
36
- - express state transitions through public methods
37
- - support derived reactive values
38
- - support typed custom events
39
- - support composition of nested managed states
40
- - support instance-owned resource cleanup
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
- All runtime functions must be imported directly from `preact-sigma`.
49
+ Import runtime APIs from `preact-sigma`.
45
50
 
46
51
  ```typescript
47
52
  import {
48
- defineManagedState,
49
- useManagedState,
50
- useSubscribe,
51
- useEventTarget,
52
- isManagedState,
53
- query,
54
- computed,
53
+ SigmaType,
54
+ action,
55
55
  batch,
56
- untracked
57
- } from 'preact-sigma';
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
- ## Public Type Exports
61
-
62
- - `ManagedState`
63
- - `StateConstructor`
64
- - `StateHandle`
65
- - `Lens`
66
- - `SubscribeTarget`
67
-
68
- ## Core Rules
69
-
70
- - The first constructor parameter must be explicitly typed as `StateHandle<...>`.
71
- - The constructor should be side-effect free.
72
- - The constructor may return only:
73
- - methods
74
- - signals
75
- - top-level lenses from the provided handle
76
- - the provided handle itself
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 { defineManagedState, query, type StateHandle } from 'preact-sigma';
90
-
91
- type CounterState = { count: number; name: string };
92
- type CounterEvents = { maxReached: [] };
93
-
94
- export const useCounterManager = defineManagedState(
95
- (handle: StateHandle<CounterState, CounterEvents>, initialName: string) => {
96
- // Top-level lenses
97
- const { count, name } = handle;
98
-
99
- return {
100
- // Expose lenses as tracked getter properties
101
- count,
102
- name,
103
-
104
- // Mutating Action (uses Immer producer under the hood)
105
- increment() {
106
- handle.set(draft => {
107
- draft.count += 1;
108
- });
109
-
110
- if (handle.peek().count >= 10) {
111
- handle.emit('maxReached');
112
- }
113
- },
114
-
115
- // Tracked read using query()
116
- isEven: query(() => count.get() % 2 === 0),
117
- };
118
- },
119
- { count: 0, name: 'Default' } // Initial State
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
- ### Using in a Component
124
- ```tsx
125
- import { useCounterManager } from './counter-manager';
126
-
127
- export function Counter() {
128
- const manager = useCounterManager("My Counter");
129
-
130
- // Tracked reads in JSX (no .value needed!)
131
- return (
132
- <div>
133
- <p>{manager.name}: {manager.count}</p>
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
- ## `defineManagedState()`
154
+ ## `SigmaType`
142
155
 
143
- Use `defineManagedState(constructor, initialState)` to create a reusable managed-state class.
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
- - `initialState` is the initial base state
148
- - constructor parameters after the handle become runtime constructor parameters for the class
149
- - internal state and event types are inferred from the typed `StateHandle` parameter
150
- - the resulting instance exposes immutable public data plus public methods
151
- - the resulting instance owns any resources registered through `handle.own()`
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
- ## `useManagedState()`
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
- Use `useManagedState(constructor, initialState)` to create a managed state directly inside a component.
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
- - follows the same constructor rules as `defineManagedState()`
160
- - accepts either a concrete initial state or a lazy initializer function
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
- ## `StateHandle`
203
+ ## `computed`
164
204
 
165
- `StateHandle<TState, TEvents>` is the constructor-local control surface.
205
+ Computeds are added with `.computed({ ... })`.
166
206
 
167
- Methods:
207
+ Behavior:
168
208
 
169
- - `get()`: tracked read of the current immutable base state
170
- - `peek()`: untracked read of the current immutable base state
171
- - `set(next)`: replace the base state or update it with an Immer producer
172
- - `own(resources)`: attach cleanup functions or disposables to the managed state instance
173
- - `emit(name, arg?)`: emit a typed custom event with zero or one argument
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
- For object-shaped base state only:
215
+ ## `queries`
176
216
 
177
- - each top-level property is available as a `Lens`
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
- ## `Lens`
219
+ Behavior:
181
220
 
182
- `Lens<T>` exists only on object-shaped `StateHandle`s.
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
- Methods:
231
+ ## `actions`
185
232
 
186
- - `get()`: tracked read of that top-level property
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
- Important:
235
+ Behavior:
190
236
 
191
- - lenses are constructor-local unless returned
192
- - returning one lens exposes one reactive public property
193
- - spreading an object-shaped handle exposes all current top-level lenses at once
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
- ## `ManagedState`
264
+ Behavior:
196
265
 
197
- A managed state instance exposes:
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
- - immutable public data as normal properties
200
- - public methods returned by the constructor
201
- - subscription methods
202
- - event subscription methods
203
- - disposal methods
278
+ ## `setup`
204
279
 
205
- Read APIs:
280
+ Setup is added with `.setup(fn)`.
206
281
 
207
- - `get(key)`: return the underlying signal for one exposed signal-backed public property
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
- Subscription APIs:
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
- - `subscribe(key, listener)`: subscribe to the current and future values of one exposed signal-backed public property
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
- Event API:
296
+ - cleanup functions
297
+ - objects with `[Symbol.dispose]()`
298
+ - `AbortController`
219
299
 
220
- - `on(name, listener)`: subscribe to one custom event
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
- Disposal API:
302
+ Cleanup runs in reverse order. If multiple cleanup steps throw, cleanup rethrows an `AggregateError`.
225
303
 
226
- - `dispose()`: dispose the managed state instance and its owned resources
227
- - `[Symbol.dispose]()` does the same thing as `dispose()`
304
+ ## Events
228
305
 
229
- ## `query()`
306
+ Events are emitted from actions or setup through `this.emit(name, payload?)`.
307
+
308
+ Behavior:
230
309
 
231
- `query(fn)` marks a returned method as a tracked public read.
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
- - wraps the method body in `computed()`
236
- - lets reads inside the method participate in Signals tracking after the method is exposed publicly
237
- - query functions read from closed-over handles or signals and do not use an instance receiver
238
- - skips the default `action()` wrapping step
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
- Use `query()` for public methods that conceptually answer a question.
328
+ ## `query(fn)`
241
329
 
242
- Do not use `query()` for ordinary mutating actions.
330
+ `query(fn)` creates a standalone tracked query helper.
243
331
 
244
- ## `computed`
332
+ Behavior:
245
333
 
246
- `computed` is re-exported from `@preact/signals`.
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
- Use it when a public derived value should be memoized and reactive.
342
+ ## `setAutoFreeze`
249
343
 
250
- ## `batch`
344
+ `setAutoFreeze(autoFreeze)` controls whether sigma deep-freezes published public state at runtime.
251
345
 
252
- `batch` is re-exported from `@preact/signals`.
346
+ Behavior:
253
347
 
254
- Use it when multiple state updates should be grouped into one reactive batch.
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
- ## `untracked`
353
+ ## `snapshot`
257
354
 
258
- `untracked` is re-exported from `@preact/signals`.
355
+ `snapshot(instance)` returns a shallow snapshot of an instance's committed public state.
259
356
 
260
- Use it when code must read reactive values without subscribing to them.
357
+ Behavior:
261
358
 
262
- ## `useSubscribe()`
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
- Use `useSubscribe(target, listener)` inside a component.
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
- - accepts any subscribable target, including a managed state or a Preact signal
269
- - keeps the listener fresh automatically
270
- - calls the listener immediately with the current value, then with future updates
271
- - does not take a dependency array
272
- - accepts `null` to disable the subscription temporarily
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
- ## `useEventTarget()`
383
+ ## `useSigma`
275
384
 
276
- Use `useEventTarget(target, name, listener)` inside a component.
385
+ `useSigma(create, setupParams?)` creates one sigma-state instance for a component and manages setup cleanup.
277
386
 
278
387
  Behavior:
279
388
 
280
- - accepts either a DOM-style `EventTarget` or a managed state
281
- - keeps the listener fresh automatically
282
- - does not take a dependency array
283
- - accepts `null` to disable the subscription temporarily
284
- - for managed-state events, the listener receives the emitted argument directly, or no argument at all
285
-
286
- ## `isManagedState()`
287
-
288
- Use `isManagedState(value)` to check whether a value is a managed-state instance.
289
-
290
- ## Supported Patterns
291
-
292
- - primitive base state
293
- - plain-object base state
294
- - Immer producer updates
295
- - lifecycle-owned cleanup via `handle.own()`
296
- - disposal via `dispose()` or `[Symbol.dispose]()`
297
- - reactive derived properties via returned signals
298
- - tracked read methods via `query()`
299
- - public exposure of one top-level property via a returned lens
300
- - public exposure of all top-level properties via `...handle`
301
- - public exposure of the whole base state via the returned handle
302
- - nested managed-state composition
303
- - keyed and whole-state signal access
304
- - keyed and whole-state snapshot access
305
- - keyed and whole-state subscriptions
306
- - typed custom events with zero or one argument
307
- - component-local managed state via `useManagedState`
308
- - hook-based subscriptions via `useSubscribe`
309
- - hook-based event subscriptions via `useEventTarget`
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.