preact-sigma 5.0.0 → 6.0.0

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.
@@ -0,0 +1,162 @@
1
+ import { ReadonlySignal } from "@preact/signals";
2
+ import { Patch } from "immer";
3
+
4
+ //#region src/internal/utils.d.ts
5
+ type AnyFunction = (...args: any[]) => any;
6
+ type Cleanup = () => void;
7
+ type AnyResource = Cleanup | {
8
+ dispose(): void;
9
+ } | {
10
+ [Symbol.dispose](): void;
11
+ };
12
+ //#endregion
13
+ //#region src/internal/listener.d.ts
14
+ /** Untyped listener shape stored internally by `SigmaListenerMap`. */
15
+ type RawSigmaListener = (detail: unknown) => void;
16
+ /** Listener registry used by sigma targets and sigma states for typed event delivery. */
17
+ declare class SigmaListenerMap extends Map<string, Set<RawSigmaListener>> {
18
+ /** Delivers one event payload to the current listeners for `name`. */
19
+ emit(name: string, detail: unknown): void;
20
+ /** Adds one listener for `name`, creating the listener set on first use. */
21
+ addListener(name: string, listener: RawSigmaListener): void;
22
+ /** Removes one listener for `name` and prunes the empty listener set. */
23
+ removeListener(name: string, listener: RawSigmaListener): void;
24
+ }
25
+ /** Infers the detail parameter for a typed emit. */
26
+ type EventParameters<T> = [void] extends [T] ? [detail?: T extends void ? undefined : T] : [undefined] extends [T] ? [detail?: T] : [detail: T];
27
+ //#endregion
28
+ //#region src/internal/symbols.d.ts
29
+ declare const instanceSymbol: unique symbol;
30
+ declare const listenersSymbol: unique symbol;
31
+ declare const refSymbol: unique symbol;
32
+ declare const snapshotSymbol: unique symbol;
33
+ declare const typeSymbol: unique symbol;
34
+ //#endregion
35
+ //#region src/immer.d.ts
36
+ type PrimitiveType = number | string | boolean;
37
+ /** Object types that should never be mapped */
38
+ type AtomicObject = AbortController | Date | EventTarget | Function | Promise<any> | RegExp | Sigma<any>;
39
+ /**
40
+ * If the lib "ES2015.Collection" is not included in tsconfig.json,
41
+ * types like ReadonlyArray, WeakMap etc. fall back to `any` (specified nowhere)
42
+ * or `{}` (from the node types), in both cases entering an infinite recursion in
43
+ * pattern matching type mappings
44
+ * This type can be used to cast these types to `void` in these cases.
45
+ */
46
+ type IfAvailable<T, Fallback = void> = true | false extends (T extends never ? true : false) ? Fallback : keyof T extends never ? Fallback : T;
47
+ /**
48
+ * These should also never be mapped but must be tested after regular Map and
49
+ * Set
50
+ */
51
+ type WeakReferences = IfAvailable<WeakMap<any, any>> | IfAvailable<WeakSet<any>>;
52
+ type WritableDraft<T> = T extends any[] ? number extends T["length"] ? Draft<T[number]>[] : WritableNonArrayDraft<T> : WritableNonArrayDraft<T>;
53
+ type WritableNonArrayDraft<T> = { -readonly [K in keyof T]: T[K] extends infer V ? (V extends object ? Draft<V> : V) : never };
54
+ /**
55
+ * Convert a readonly type into a mutable type, if possible.
56
+ *
57
+ * Use this instead of `immer.Draft`
58
+ */
59
+ type Draft<T> = T extends PrimitiveType ? T : T extends AtomicObject ? T : keyof SigmaRef extends keyof T ? T : T extends ReadonlyMap<infer K, infer V> ? Map<Draft<K>, Draft<V>> : T extends ReadonlySet<infer V> ? Set<Draft<V>> : T extends WeakReferences ? T : T extends object ? WritableDraft<T> : T;
60
+ /**
61
+ * Convert a mutable type into a readonly type.
62
+ *
63
+ * Use this instead of `immer.Immutable`
64
+ */
65
+ type Immutable<T> = T extends PrimitiveType ? T : T extends AtomicObject ? T : keyof SigmaRef extends keyof T ? T : T extends ReadonlyMap<infer K, infer V> ? ReadonlyMap<Immutable<K>, Immutable<V>> : T extends ReadonlySet<infer V> ? ReadonlySet<Immutable<V>> : T extends WeakReferences ? T : T extends object ? { readonly [K in keyof T]: Immutable<T[K]> } : T;
66
+ //#endregion
67
+ //#region src/sigma.d.ts
68
+ /**
69
+ * Configures Immer auto-freezing for values published through sigma state.
70
+ *
71
+ * Auto-freezing is enabled by default, so draftable public values are deeply frozen after publish.
72
+ */
73
+ declare function setAutoFreeze(autoFreeze: boolean): void;
74
+ /** Marks object values that should keep their reference-like type in `Draft` and `Immutable` mappings. */
75
+ type SigmaRef<T extends object = {}> = T & {
76
+ [refSymbol]?: true;
77
+ };
78
+ /** Definition shape used by helper types that need both state and event maps. */
79
+ type SigmaDefinition = {
80
+ state: object;
81
+ events?: object;
82
+ };
83
+ /** Instance type for a sigma definition with state typing preserved for public helpers. */
84
+ type SigmaState<T extends SigmaDefinition> = Sigma<T["state"]> & {
85
+ [typeSymbol]: T;
86
+ };
87
+ /**
88
+ * Base class for signal-backed state models.
89
+ *
90
+ * `TState` is the source of typing for top-level state keys, subscriptions, signals, and replacement snapshots.
91
+ * Merge a same-named interface with the class when direct property reads should be typed on the instance.
92
+ */
93
+ declare abstract class Sigma<TState extends object> {
94
+ [typeSymbol]: {
95
+ state: TState;
96
+ events: unknown;
97
+ };
98
+ [snapshotSymbol]: Record<string, unknown> | undefined;
99
+ protected get [instanceSymbol](): this;
100
+ constructor(initialState: TState);
101
+ /** Optional setup hook that owns side effects and returns cleanup resources. */
102
+ onSetup?(...args: any[]): readonly AnyResource[];
103
+ /** Runs `onSetup(...)` and returns a cleanup that disposes returned resources in reverse order. */
104
+ setup(...args: Parameters<Extract<this["onSetup"], AnyFunction>>): () => void;
105
+ /**
106
+ * Publishes the current action draft.
107
+ *
108
+ * Use this before unpublished changes cross an async, event, or external-action boundary.
109
+ * A callback runs after publish in an action context.
110
+ */
111
+ commit<T = void>(callback?: (this: typeof this) => T): T | undefined;
112
+ /** Runs a synchronous setup-owned callback with action semantics from an `onSetup(...)` context. */
113
+ act(fn: (this: typeof this) => void): void;
114
+ }
115
+ /** Casts a sigma instance to its readonly public consumer view. */
116
+ declare function castProtected<T extends Sigma<any>>(instance: T): Protected<T>;
117
+ /**
118
+ * Sigma state model that can emit typed events.
119
+ *
120
+ * `TEvents` maps event names to payload types, and `TState` types reactive state.
121
+ */
122
+ declare class SigmaTarget<TEvents extends object = {}, TState extends object = {}> extends Sigma<TState> {
123
+ [typeSymbol]: {
124
+ state: TState;
125
+ events: TEvents;
126
+ };
127
+ protected [listenersSymbol]: SigmaListenerMap;
128
+ constructor(state?: TState);
129
+ /** Emits a typed event from an action after unpublished draft changes are committed. */
130
+ emit<TEvent extends string & keyof TEvents>(name: TEvent, ...[detail]: EventParameters<TEvents[TEvent]>): void;
131
+ }
132
+ /** Helpers for observing, accessing, capturing, and replacing committed sigma state. */
133
+ declare const sigma: Readonly<{
134
+ /** Subscribes to committed state publishes or to one signal-backed top-level state key. */subscribe: {
135
+ <TState extends object>(instance: Sigma<TState>, listener: (nextState: Immutable<TState>, baseState: Immutable<TState>, patches: Patch[], inversePatches: Patch[]) => void, options: {
136
+ patches: true;
137
+ }): Cleanup;
138
+ <TState extends object>(instance: Sigma<TState>, listener: (nextState: Immutable<TState>, baseState: Immutable<TState>, patches: Patch[] | undefined, inversePatches: Patch[] | undefined) => void, options: {
139
+ patches: boolean;
140
+ }): Cleanup;
141
+ <TState extends object>(instance: Sigma<TState>, listener: (nextState: Immutable<TState>, baseState: Immutable<TState>) => void): Cleanup;
142
+ <TState extends object, TKey extends Extract<keyof TState, string>>(instance: Sigma<TState>, key: TKey, listener: (value: Immutable<TState[TKey]>) => void): Cleanup;
143
+ }; /** Returns the readonly signal backing one top-level state key. */
144
+ getSignal<TState extends object, TKey extends Extract<keyof TState, string>>(instance: Sigma<TState>, key: TKey): ReadonlySignal<Immutable<TState[TKey]>>; /** Captures the current committed top-level state snapshot. */
145
+ captureState<TState extends object>(instance: Sigma<TState>): Immutable<TState>; /** Publishes a plain-object snapshot as the current committed state. */
146
+ replaceState<TState extends object>(target: Sigma<TState>, nextState: TState): void;
147
+ }>;
148
+ /** Marks a class method as a committed-state reactive read with arguments instead of an action. */
149
+ declare function query<TThis extends object, TArgs extends any[], TReturn>(method: (this: TThis, ...args: TArgs) => TReturn): (this: TThis, ...args: TArgs) => TReturn;
150
+ declare const protectedSymbol: unique symbol;
151
+ type ProtectedKey = typeof listenersSymbol | typeof snapshotSymbol | "act" | "commit" | "emit" | "onSetup";
152
+ type BrandProtected<T> = T & {
153
+ [protectedSymbol]: true;
154
+ };
155
+ /** Readonly public view returned by `castProtected(...)` and `useSigma(...)`. */
156
+ type Protected<T extends Sigma<any>> = BrandProtected<T extends {
157
+ [typeSymbol]: {
158
+ state: infer TState extends object;
159
+ };
160
+ } ? { [K in keyof T as K extends ProtectedKey ? never : K]: K extends typeof typeSymbol ? T[K] : K extends keyof TState ? Immutable<T[K]> : T[K] extends AnyFunction ? (...params: Parameters<T[K]>) => Immutable<ReturnType<T[K]>> : Immutable<T[K]> } : never>;
161
+ //#endregion
162
+ export { SigmaState as a, query as c, Draft as d, Immutable as f, SigmaRef as i, setAutoFreeze as l, Cleanup as m, Sigma as n, SigmaTarget as o, typeSymbol as p, SigmaDefinition as r, castProtected as s, Protected as t, sigma as u };
package/docs/context.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # Overview
2
2
 
3
- `preact-sigma` builds reusable state models from one definition. A configured `SigmaType` owns top-level state, derived reads, writes, setup handlers, and typed events. Each top-level state property is exposed as a reactive public property backed by its own Preact signal, while actions use Immer-style mutation semantics to publish committed state. For event-only flows, `SigmaTarget` provides a standalone typed event hub with no managed state.
3
+ `preact-sigma` builds reusable state models as TypeScript classes. A `Sigma<TState>` subclass owns top-level state, derived reads, writes, and setup. A `SigmaTarget<TEvents, TState>` subclass adds typed events. Each top-level state key is exposed as a reactive public property backed by a Preact signal, while actions use Immer-style mutation semantics to publish committed state.
4
4
 
5
5
  # When to Use
6
6
 
7
7
  - State, derived reads, mutations, and lifecycle need to stay together.
8
- - You need multiple instances of the same model.
9
- - Public reads should stay reactive and readonly while writes stay explicit.
8
+ - You need multiple instances of the same model class.
9
+ - Public reads should stay reactive while writes stay explicit.
10
10
  - A model needs to own timers, subscriptions, listeners, nested setup, or other cleanup-aware resources.
11
11
  - Components should consume the same model shape used outside Preact.
12
12
 
@@ -19,105 +19,112 @@
19
19
 
20
20
  # Core Abstractions
21
21
 
22
- - Sigma type: the builder returned by `new SigmaType<TState, TEvents>()`. After configuration, it is also the constructor for instances.
23
- - Sigma state: an instance created from a configured sigma type.
24
- - Sigma target: a standalone typed event hub created with `new SigmaTarget<TEvents>()` when you need typed events without managed state.
25
- - State property: a top-level key from `TState`. Each one becomes a readonly reactive public property and gets its own signal.
26
- - Computed: an argument-free derived getter declared with `.computed(...)`.
27
- - Query: a reactive read that accepts arguments, declared with `.queries(...)` or built locally with `query(fn)`.
28
- - Action: a method declared with `.actions(...)` that reads and writes through sigma's draft and commit semantics.
29
- - Setup handler: a function declared with `.setup(...)` that owns side effects and cleanup resources explicitly.
30
- - Event: a typed notification emitted through `this.emit(...)` and observed through `listen(...)` or `useListener(...)`.
22
+ - Sigma class: a class that extends `Sigma<TState>` and passes its initial top-level state to `super(...)`. The `TState` argument drives helper typing for subscriptions, signals, and replacement snapshots; a same-named merged interface gives direct property reads their instance types.
23
+ - Sigma target: a class that extends `SigmaTarget<TEvents, TState>` when it also emits typed events. Use `SigmaTarget<TEvents>` for event-only targets.
24
+ - State property: a top-level key from `TState`. Each key becomes a reactive public property and has its own signal.
25
+ - Computed: an argument-free derived getter on the class prototype that reads committed state.
26
+ - Query: a reactive read method that accepts arguments, is marked with the `query` decorator, and reads committed state.
27
+ - Action: a prototype method that is not marked as a query. Actions read and write state properties through sigma's draft and commit semantics.
28
+ - Setup handler: an optional `onSetup(...)` method that owns side effects and returns cleanup resources.
29
+ - Event: a typed notification emitted with `this.emit(...)` inside an action and observed through `listen(...)` or `useListener(...)`.
30
+ - Protected view: the readonly consumer view returned by `castProtected(...)` and `useSigma(...)`.
31
31
 
32
32
  # Data Flow / Lifecycle
33
33
 
34
- 1. Define a sigma type with `new SigmaType<TState, TEvents>()`. Let later builder methods infer names and types from the objects you pass to them.
35
- 2. Add `defaultState(...)` for top-level public state and optional per-instance initializers.
36
- 3. Add `computed(...)`, `queries(...)`, and `actions(...)` for derived reads and writes.
37
- 4. Instantiate the configured type. Constructor input shallowly overrides `defaultState(...)`.
34
+ 1. Define a class that extends `Sigma<TState>` or `SigmaTarget<TEvents, TState>`.
35
+ 2. Define the state as a named type, pass it to `Sigma<TState>`, then merge `interface Model extends ModelState {}` after the class so direct property reads are typed.
36
+ 3. Add getters for computed values, `@query` methods for argument-based reactive reads, and ordinary methods for actions.
37
+ 4. Instantiate the class. Constructor input can be merged with defaults before `super(...)` when instances need partial overrides.
38
38
  5. Read state, computeds, and queries reactively from the public instance.
39
- 6. Mutate state inside actions. Sync nested actions on the same instance share one draft. Boundaries like `await`, `emit(...)`, or separate action invocations may require `this.commit()` before the boundary.
40
- 7. Run `setup(...)` explicitly when the instance should start owning side effects. `useSigma(...)` does this automatically for component-owned instances that define setup.
39
+ 6. Mutate state inside actions. Synchronous actions publish automatically when they return, and sync nested actions on the same instance share one draft. Computeds and queries still read the last committed state while an action has unpublished draft changes. Call `this.commit()` when derived reads or unpublished changes must cross a boundary, such as before an `await`, before the action promise resolves, before `emit(...)`, or before invoking another instance's action.
40
+ 7. Run `setup(...)` explicitly when the instance should start owning side effects. `useSigma(...)` does this automatically for component-owned instances that define `onSetup(...)`.
41
41
  8. Dispose the cleanup returned from `setup(...)` when the owned resources should stop.
42
42
 
43
43
  # Common Tasks -> Recommended APIs
44
44
 
45
- - Define reusable model state: `new SigmaType<TState, TEvents>().defaultState(...)`
46
- - Derive an argument-free value: `.computed(...)`
47
- - Derive a reactive read with arguments: `.queries(...)`
48
- - Keep a tracked helper local to one consumer module: `query(fn)`
49
- - Mutate state and emit typed notifications: `.actions(...)`
50
- - Publish before `await`, `emit(...)`, or another action boundary: `this.commit()`
51
- - React to committed state changes: `sigma.subscribe(instance, handler)` or `sigma.subscribe(instance, key, handler)`
52
- - Read one top-level state property or computed as a `ReadonlySignal`: `sigma.getSignal(instance, key)`
53
- - Own timers, listeners, subscriptions, or nested setup: `.setup(...)`
54
- - Use a sigma state inside a component: `useSigma(...)`
55
- - Subscribe to sigma or DOM events in a component: `useListener(...)`
56
- - Create a standalone typed event hub with no managed state: `new SigmaTarget<TEvents>()`, `hub.emit(...)`, and `listen(hub, ...)`
57
- - Subscribe outside components: `listen(instance, ...)`
58
- - Read or restore committed top-level state: `sigma.getState(...)` and `sigma.replaceState(...)`
45
+ - Define reusable model state: `class Model extends Sigma<TState>`.
46
+ - Define reusable model state with events: `class Model extends SigmaTarget<TEvents, TState>`.
47
+ - Merge partial constructor input with defaults: `mergeDefaults(initial, defaults)`.
48
+ - Derive an argument-free value: a class getter.
49
+ - Derive a reactive read with arguments: an `@query` class method.
50
+ - Mutate state and emit typed notifications: ordinary class methods plus `this.emit(...)`.
51
+ - Publish unpublished changes before `await`, `emit(...)`, promise resolution, or another instance's action: `this.commit()`.
52
+ - React to committed state changes: `sigma.subscribe(instance, handler)` or `sigma.subscribe(instance, key, handler)`.
53
+ - Read one top-level state property as a `ReadonlySignal`: `sigma.getSignal(instance, key)`.
54
+ - Own timers, listeners, subscriptions, or nested setup: `onSetup(...)` plus `setup(...)`.
55
+ - Use a sigma instance inside a component: `useSigma(...)`.
56
+ - Cast an instance to its readonly consumer view outside a component: `castProtected(instance)`.
57
+ - Subscribe to sigma or DOM events in a component: `useListener(...)`.
58
+ - Subscribe outside components: `listen(instance, ...)`.
59
+ - Read or restore committed top-level state: `sigma.captureState(...)` and `sigma.replaceState(...)`.
59
60
 
60
61
  # Recommended Patterns
61
62
 
62
- - Put explicit type arguments on `new SigmaType<TState, TEvents>()` and let later builder methods infer from the objects you pass.
63
+ - Put the state shape in a named `State` type, pass it to `Sigma<TState>` or `SigmaTarget<TEvents, TState>`, then merge a same-named interface with the class for direct property typing.
63
64
  - Keep frequently read values as separate top-level state properties. Each top-level key gets its own signal.
64
- - Use `.computed(...)` for argument-free derived reads.
65
- - Use `.queries(...)` for tracked reads with arguments.
66
- - Keep one-off calculations local until they become reusable model behavior.
67
- - Use ordinary actions for routine writes. Reserve `sigma.getState(...)` and `sigma.replaceState(...)` for replay, reset, or undo-like flows on committed top-level state.
68
- - Prefer `listen(...)` for external event subscriptions. It works with sigma states, `SigmaTarget`, and DOM targets.
69
- - Put owned side effects in `.setup(...)`.
70
- - Use `sigma.subscribe(this, ...)` inside `.setup(...)` when a setup-owned side effect should react to future committed publishes. Return that cleanup so the subscription stops with setup.
65
+ - Use getters for argument-free derived reads.
66
+ - Use `@query` for tracked reads with arguments.
67
+ - Derive directly from state properties inside an action when the calculation needs unpublished draft values.
68
+ - Use ordinary actions for routine writes. Reserve `sigma.captureState(...)` and `sigma.replaceState(...)` for replay, reset, restore, or undo-like flows on committed top-level state.
69
+ - Emit directly from actions that have no unpublished draft changes. After mutating state, publish first with `this.commit(); this.emit(...)`.
70
+ - Prefer `listen(...)` for external event subscriptions. It works with sigma targets and DOM targets.
71
+ - Put owned side effects in `onSetup(...)`.
72
+ - Use `sigma.subscribe(this, ...)` inside `onSetup(...)` when a setup-owned side effect should react to future committed publishes. Return that cleanup so the subscription stops with setup.
71
73
  ```ts
72
- .setup(function () {
74
+ onSetup() {
73
75
  return [
74
- sigma.subscribe(this, (change) => {
75
- console.log(change.newState);
76
+ sigma.subscribe(this, (nextState, baseState) => {
77
+ console.log(baseState, nextState);
76
78
  }),
77
79
  ];
78
- })
80
+ }
79
81
  ```
80
82
  - Use `this.act(function () { ... })` for setup-owned callbacks that need action semantics.
81
83
 
82
84
  # Patterns to Avoid
83
85
 
84
86
  - Reaching for `sigma.getSignal(instance, key)` when direct property reads already cover the use case.
85
- - Crossing `emit(...)`, `await`, or another action boundary with unpublished changes when those changes need to stay visible afterward. Publish them first with `this.commit()`.
87
+ - Crossing `emit(...)`, `await`, promise resolution, or another instance's action with unpublished changes. Publish them first with `this.commit()`.
86
88
  - Starting side effects during construction instead of through explicit `setup(...)`.
87
- - Encoding storage, hydration, or migration policy directly into `SigmaType` definitions.
89
+ - Encoding storage, hydration, or migration policy directly into model classes.
90
+ - Relying on computeds or queries to observe unpublished draft changes inside actions.
88
91
  - Treating query calls as memoized across invocations.
89
92
  - Relying on patch payloads without enabling Immer patches first.
90
93
 
91
94
  # Invariants and Constraints
92
95
 
93
- - Sigma only tracks top-level state properties. Each top-level key gets its own signal.
94
- - Public state is readonly outside actions and `this.act(...)` inside setup.
95
- - Duplicate names across state properties, computeds, queries, and actions are rejected at runtime. Reserved public names include `act`, `commit`, `emit`, and `setup`.
96
+ - Sigma tracks top-level state properties. Each top-level key gets its own signal.
97
+ - Protected consumer views expose immutable state and callable actions.
98
+ - Published draftable public state is deep-frozen by default. `setAutoFreeze(false)` disables that behavior globally.
99
+ - Computeds and queries read committed state, including when called inside actions.
96
100
  - Query calls are reactive at the call site but do not memoize across invocations.
97
101
  - Setup handlers return arrays of cleanup resources, and cleanup runs in reverse order.
98
102
  - Call Immer's `enablePatches()` before relying on `sigma.subscribe(instance, handler, { patches: true })`.
99
- - `sigma.replaceState(...)` works on committed top-level state and requires the exact state-key shape.
100
- - Published draftable public state is deep-frozen by default. `setAutoFreeze(false)` disables that behavior globally.
103
+ - `sigma.replaceState(...)` works on committed top-level state and requires a plain object snapshot.
104
+ - `SigmaTarget.emit(...)` runs from an action and requires no active unpublished draft. It does not need a `commit(...)` callback.
101
105
 
102
106
  # Error Model
103
107
 
104
108
  - Crossing an action boundary with unpublished changes throws until `this.commit()` publishes them. Async actions also reject when they finish with unpublished changes.
105
- - If another invocation crosses a boundary while unpublished changes still exist, sigma warns and discards those changes before continuing.
106
- - Calling `setup(...)` on a sigma state without registered setup handlers throws.
107
- - Cleanup rethrows an `AggregateError` when more than one cleanup resource fails.
108
- - `sigma.replaceState(...)` throws when the replacement value is not a plain object, has the wrong top-level keys, or runs while an action still owns unpublished changes.
109
+ - Calling `commit(...)` outside an action throws.
110
+ - Calling `act(...)` outside an `onSetup(...)` setup context throws.
111
+ - Calling `emit(...)` outside an action or before committing the active draft throws.
112
+ - Calling an action from a computed or query throws.
113
+ - Returning an active draft from an action throws.
114
+ - `sigma.replaceState(...)` throws when the replacement value is not a plain object or when an action still owns unpublished changes.
115
+ - Starting an action on another sigma instance while the current instance has an active action context throws.
109
116
 
110
117
  # Terminology
111
118
 
112
119
  - Draft boundary: a point where sigma cannot keep reusing the current unpublished draft.
113
120
  - Committed state: the published top-level public state visible outside the current action draft.
114
- - Signal access: reading the underlying `ReadonlySignal` for a top-level state key or computed through `sigma.getSignal(instance, key)`.
115
- - Cleanup resource: a cleanup function, `AbortController`, object with `dispose()`, or object with `[Symbol.dispose]()`.
121
+ - Signal access: reading the underlying `ReadonlySignal` for a top-level state key through `sigma.getSignal(instance, key)`.
122
+ - Cleanup resource: a cleanup function, object with `dispose()`, or object with `[Symbol.dispose]()`.
116
123
  - Nested sigma state: a sigma-state instance stored in top-level state as a value; it stays usable as a value rather than exposing its internals through parent actions.
117
124
 
118
125
  # Non-Goals
119
126
 
120
- - Replacing every plain-signal use case with a builder abstraction.
127
+ - Replacing every plain-signal use case with a class abstraction.
121
128
  - Hiding lifecycle behind implicit setup or constructor side effects.
122
129
  - Memoizing every query call or turning queries into a global cache.
123
130
  - Acting as a large tutorial framework or hand-maintained API reference. Exact signatures come from declaration output, and factual behavior lives beside source.
@@ -0,0 +1,273 @@
1
+ # Migrating from v5 to v6
2
+
3
+ v6 replaces the `SigmaType` builder with class-based models. The runtime contract is still centered on top-level reactive state, derived reads, explicit actions, setup-owned side effects, typed events, and committed snapshots.
4
+
5
+ ## Model Definitions
6
+
7
+ Define a class that extends `Sigma<TState>` instead of building a configured `SigmaType`.
8
+
9
+ Before:
10
+
11
+ ```ts
12
+ import { SigmaType } from "preact-sigma";
13
+
14
+ const Counter = new SigmaType<{ count: number }>("Counter")
15
+ .defaultState({
16
+ count: 0,
17
+ })
18
+ .computed({
19
+ doubled() {
20
+ return this.count * 2;
21
+ },
22
+ })
23
+ .actions({
24
+ increment() {
25
+ this.count += 1;
26
+ },
27
+ });
28
+ ```
29
+
30
+ After:
31
+
32
+ ```ts
33
+ import { Sigma } from "preact-sigma";
34
+
35
+ type CounterState = {
36
+ count: number;
37
+ };
38
+
39
+ class Counter extends Sigma<CounterState> {
40
+ constructor() {
41
+ super({
42
+ count: 0,
43
+ });
44
+ }
45
+
46
+ get doubled() {
47
+ return this.count * 2;
48
+ }
49
+
50
+ increment() {
51
+ this.count += 1;
52
+ }
53
+ }
54
+
55
+ interface Counter extends CounterState {}
56
+ ```
57
+
58
+ `TState` drives helper typing for subscriptions, signals, and replacement snapshots. The same-named merged interface gives direct state property reads their instance types.
59
+
60
+ ## Constructor Defaults
61
+
62
+ Constructor input is ordinary TypeScript now. Use `mergeDefaults(...)` when an instance accepts partial initial state.
63
+
64
+ ```ts
65
+ import { mergeDefaults, Sigma } from "preact-sigma";
66
+
67
+ type SearchState = {
68
+ draft: string;
69
+ page: number;
70
+ };
71
+
72
+ class Search extends Sigma<SearchState> {
73
+ static defaultState: SearchState = {
74
+ draft: "",
75
+ page: 1,
76
+ };
77
+
78
+ constructor(initialState?: Partial<SearchState>) {
79
+ super(mergeDefaults(initialState, Search.defaultState));
80
+ }
81
+ }
82
+
83
+ interface Search extends SearchState {}
84
+ ```
85
+
86
+ ## Computeds, Queries, and Actions
87
+
88
+ Use class getters for argument-free computed reads. Use ordinary prototype methods for actions. Computeds and queries read committed state, including when called inside actions.
89
+
90
+ Argument-based reactive reads are class methods marked with `@query`.
91
+
92
+ ```ts
93
+ import { query, Sigma } from "preact-sigma";
94
+
95
+ type TodoListState = {
96
+ draft: string;
97
+ };
98
+
99
+ class TodoList extends Sigma<TodoListState> {
100
+ constructor() {
101
+ super({ draft: "" });
102
+ }
103
+
104
+ @query
105
+ canAdd(minLength: number) {
106
+ return this.draft.trim().length >= minLength;
107
+ }
108
+
109
+ setDraft(draft: string) {
110
+ this.draft = draft;
111
+ }
112
+ }
113
+
114
+ interface TodoList extends TodoListState {}
115
+ ```
116
+
117
+ ## Events
118
+
119
+ `SigmaTarget` now takes event types first. Use `SigmaTarget<TEvents>` for event-only targets and `SigmaTarget<TEvents, TState>` for targets that also own state.
120
+
121
+ ```ts
122
+ import { listen, SigmaTarget } from "preact-sigma";
123
+
124
+ type NotificationEvents = {
125
+ saved: {
126
+ id: string;
127
+ };
128
+ };
129
+
130
+ class Notifications extends SigmaTarget<NotificationEvents> {
131
+ saved(id: string) {
132
+ this.emit("saved", { id });
133
+ }
134
+ }
135
+
136
+ const notifications = new Notifications();
137
+
138
+ const stop = listen(notifications, "saved", ({ id }) => {
139
+ console.log(id);
140
+ });
141
+ ```
142
+
143
+ `emit(...)` runs inside actions. If an action mutates state before emitting, publish first with `this.commit()`.
144
+
145
+ ## Commit Boundaries
146
+
147
+ Synchronous actions publish automatically when they return. Call `this.commit()` only when unpublished changes cross a boundary:
148
+
149
+ - before `await`
150
+ - before an async action promise resolves
151
+ - before `emit(...)`
152
+ - before invoking another instance's action
153
+
154
+ ```ts
155
+ type SaveIndicatorState = {
156
+ savedCount: number;
157
+ saving: boolean;
158
+ };
159
+
160
+ type SaveIndicatorEvents = {
161
+ saved: {
162
+ count: number;
163
+ };
164
+ };
165
+
166
+ class SaveIndicator extends SigmaTarget<SaveIndicatorEvents, SaveIndicatorState> {
167
+ constructor() {
168
+ super({
169
+ savedCount: 0,
170
+ saving: false,
171
+ });
172
+ }
173
+
174
+ async save() {
175
+ this.saving = true;
176
+ this.commit();
177
+
178
+ await Promise.resolve();
179
+
180
+ this.savedCount += 1;
181
+ this.saving = false;
182
+ this.commit();
183
+
184
+ this.emit("saved", { count: this.savedCount });
185
+ }
186
+ }
187
+
188
+ interface SaveIndicator extends SaveIndicatorState {}
189
+ ```
190
+
191
+ ## Setup
192
+
193
+ Replace builder setup with an `onSetup(...)` method. Call `setup(...)` manually outside Preact, or use `useSigma(...)` for component-owned instances.
194
+
195
+ ```ts
196
+ import { listen, Sigma } from "preact-sigma";
197
+
198
+ type ClickTrackerState = {
199
+ clicks: number;
200
+ };
201
+
202
+ class ClickTracker extends Sigma<ClickTrackerState> {
203
+ constructor() {
204
+ super({ clicks: 0 });
205
+ }
206
+
207
+ onSetup(target: EventTarget) {
208
+ return [
209
+ listen(target, "click", () => {
210
+ this.act(function () {
211
+ this.clicks += 1;
212
+ });
213
+ }),
214
+ ];
215
+ }
216
+ }
217
+
218
+ interface ClickTracker extends ClickTrackerState {}
219
+ ```
220
+
221
+ ## Protected Views
222
+
223
+ The instance method `protect()` is gone. Use `castProtected(instance)` outside components, and use `useSigma(...)` inside components.
224
+
225
+ ```ts
226
+ import { castProtected } from "preact-sigma";
227
+
228
+ const publicCounter = castProtected(new Counter());
229
+ ```
230
+
231
+ Protected sigma targets keep their event metadata, so `useListener(...)` works directly with the value returned by `useSigma(...)`.
232
+
233
+ ```tsx
234
+ const palette = useSigma(() => new CommandPalette());
235
+
236
+ useListener(palette, "ran", (command) => {
237
+ console.log(command.title);
238
+ });
239
+ ```
240
+
241
+ ## Committed State Helpers
242
+
243
+ Replace `sigma.getState(...)` with `sigma.captureState(...)`.
244
+
245
+ ```ts
246
+ const saved = sigma.captureState(todoList);
247
+
248
+ todoList.add("Ship release");
249
+ sigma.replaceState(todoList, saved);
250
+ ```
251
+
252
+ Use `sigma.subscribe(instance, listener)` for committed state publishes and `sigma.subscribe(instance, key, listener)` for one top-level state key.
253
+
254
+ ## Persistence
255
+
256
+ The `preact-sigma/persist` helpers are named around restore, persist, and hydrate flows:
257
+
258
+ - `restore(instance, options)`
259
+ - `restoreSync(instance, options)`
260
+ - `persist(instance, options)`
261
+ - `hydrate(instance, options)`
262
+ - `hydrateSync(instance, options)`
263
+
264
+ Use `pick: ["key"]` options for selected top-level state keys instead of a separate pick codec helper.
265
+
266
+ See [`../persist.md`](../persist.md) for persistence-specific guidance.
267
+
268
+ ## More References
269
+
270
+ - [`../context.md`](../context.md): concepts, lifecycle, invariants, and API selection
271
+ - [`../../examples/basic-counter.ts`](../../examples/basic-counter.ts): minimal class model
272
+ - [`../../examples/command-palette.tsx`](../../examples/command-palette.tsx): component usage, setup, events, nested state, and custom helper objects
273
+ - `dist/index.d.mts` and `dist/persist.d.mts` after `pnpm build`: exact exported signatures