preact-sigma 2.2.1 → 2.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -159
- package/dist/index.d.mts +87 -22
- package/dist/index.mjs +34 -12
- package/docs/context.md +102 -0
- package/examples/async-commit.ts +42 -0
- package/examples/basic-counter.ts +23 -0
- package/examples/command-palette.tsx +211 -0
- package/examples/observe-and-restore.ts +27 -0
- package/examples/setup-act.ts +34 -0
- package/examples/signal-access.ts +28 -0
- package/package.json +3 -2
- package/llms.txt +0 -437
package/README.md
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
# preact-sigma
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Purpose
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`preact-sigma` is a typed state-model builder for Preact and TypeScript. It keeps top-level public state reactive, derived reads local to the model, writes explicit through actions, and side effects owned by explicit setup.
|
|
6
6
|
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
To add `preact-sigma` to your project:
|
|
7
|
+
## Installation
|
|
10
8
|
|
|
11
9
|
```bash
|
|
12
10
|
npm install preact-sigma
|
|
13
11
|
```
|
|
14
12
|
|
|
15
|
-
##
|
|
13
|
+
## Quick Example
|
|
16
14
|
|
|
17
15
|
```ts
|
|
18
16
|
import { SigmaType } from "preact-sigma";
|
|
@@ -40,157 +38,10 @@ console.log(counter.count); // 1
|
|
|
40
38
|
console.log(counter.doubled); // 2
|
|
41
39
|
```
|
|
42
40
|
|
|
43
|
-
##
|
|
44
|
-
|
|
45
|
-
At its core, `preact-sigma` lets you describe a stateful model as a constructor:
|
|
46
|
-
|
|
47
|
-
- top-level state stays reactive through one signal per state property
|
|
48
|
-
- computed values become tracked getters
|
|
49
|
-
- queries become tracked methods, including queries with arguments
|
|
50
|
-
- actions batch reads and writes through Immer drafts
|
|
51
|
-
- setup handlers own side effects and cleanup
|
|
52
|
-
- typed events let instances notify the outside world without exposing mutable internals
|
|
53
|
-
|
|
54
|
-
The result feels like a small stateful object from application code, while still behaving like signal-driven state from rendering code.
|
|
55
|
-
|
|
56
|
-
## What You Can Do With It
|
|
57
|
-
|
|
58
|
-
`preact-sigma` is useful when you want state logic to live in one reusable unit instead of being split across loose signals, reducers, and effect cleanup code.
|
|
59
|
-
|
|
60
|
-
With it, you can:
|
|
61
|
-
|
|
62
|
-
- model domain state as reusable constructors instead of one-off store objects
|
|
63
|
-
- read public state directly while keeping writes inside typed action methods
|
|
64
|
-
- derive reactive values with computed getters and parameterized queries
|
|
65
|
-
- publish state changes from synchronous or async actions
|
|
66
|
-
- observe committed state changes and optional Immer patches
|
|
67
|
-
- snapshot committed top-level state and replace committed state for undo-like flows
|
|
68
|
-
- manage timers, listeners, nested state setup, and teardown through explicit cleanup
|
|
69
|
-
- use the same model inside Preact components with `useSigma(...)` and `useListener(...)`
|
|
70
|
-
|
|
71
|
-
## Why This Shape Exists
|
|
72
|
-
|
|
73
|
-
This package exists to keep stateful logic cohesive without giving up signal-level reactivity.
|
|
74
|
-
|
|
75
|
-
It is a good fit when plain signals start to sprawl across modules, but heavier store abstractions feel too opaque or too tied to component structure. `preact-sigma` keeps the "model object" ergonomics of a class-like API, while preserving readonly public reads, explicit write boundaries, and explicit ownership of side effects.
|
|
76
|
-
|
|
77
|
-
## Big Picture Example
|
|
78
|
-
|
|
79
|
-
```ts
|
|
80
|
-
import { computed, SigmaType } from "preact-sigma";
|
|
81
|
-
|
|
82
|
-
type Todo = {
|
|
83
|
-
id: string;
|
|
84
|
-
title: string;
|
|
85
|
-
done: boolean;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const TodoList = new SigmaType<
|
|
89
|
-
{ draft: string; todos: Todo[]; saving: boolean },
|
|
90
|
-
{ saved: { count: number } }
|
|
91
|
-
>("TodoList")
|
|
92
|
-
.defaultState({
|
|
93
|
-
draft: "",
|
|
94
|
-
todos: [],
|
|
95
|
-
saving: false,
|
|
96
|
-
})
|
|
97
|
-
.computed({
|
|
98
|
-
// Computeds are tracked getters with no arguments.
|
|
99
|
-
remainingCount() {
|
|
100
|
-
return this.todos.filter((todo) => !todo.done).length;
|
|
101
|
-
},
|
|
102
|
-
})
|
|
103
|
-
.queries({
|
|
104
|
-
// Queries stay reactive at the call site and can accept arguments.
|
|
105
|
-
visibleTodos(filter: "all" | "open" | "done") {
|
|
106
|
-
return this.todos.filter((todo) => {
|
|
107
|
-
if (filter === "open") return !todo.done;
|
|
108
|
-
if (filter === "done") return todo.done;
|
|
109
|
-
return true;
|
|
110
|
-
});
|
|
111
|
-
},
|
|
112
|
-
})
|
|
113
|
-
.actions({
|
|
114
|
-
// Public state is readonly, so writes live in actions.
|
|
115
|
-
setDraft(draft: string) {
|
|
116
|
-
this.draft = draft;
|
|
117
|
-
},
|
|
118
|
-
addTodo() {
|
|
119
|
-
if (!this.draft.trim()) return;
|
|
120
|
-
|
|
121
|
-
this.todos.push({
|
|
122
|
-
id: crypto.randomUUID(),
|
|
123
|
-
title: this.draft,
|
|
124
|
-
done: false,
|
|
125
|
-
});
|
|
126
|
-
this.draft = "";
|
|
127
|
-
},
|
|
128
|
-
toggleTodo(id: string) {
|
|
129
|
-
const todo = this.todos.find((todo) => todo.id === id);
|
|
130
|
-
if (todo) todo.done = !todo.done;
|
|
131
|
-
},
|
|
132
|
-
async save() {
|
|
133
|
-
this.saving = true;
|
|
134
|
-
this.commit(); // Publish the loading state before awaiting.
|
|
135
|
-
|
|
136
|
-
await fetch("/api/todos", {
|
|
137
|
-
method: "POST",
|
|
138
|
-
body: JSON.stringify(this.todos),
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
this.saving = false;
|
|
142
|
-
this.commit(); // Publish post-await writes explicitly.
|
|
143
|
-
this.emit("saved", { count: this.todos.length });
|
|
144
|
-
},
|
|
145
|
-
})
|
|
146
|
-
.setup(function (storageKey: string) {
|
|
147
|
-
// Setup is explicit and returns cleanup resources.
|
|
148
|
-
const interval = window.setInterval(() => {
|
|
149
|
-
localStorage.setItem(storageKey, JSON.stringify(this.todos));
|
|
150
|
-
}, 1000);
|
|
151
|
-
|
|
152
|
-
return [() => window.clearInterval(interval)];
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
const todoList = new TodoList();
|
|
156
|
-
|
|
157
|
-
// setup(...) returns one cleanup function for everything this instance owns.
|
|
158
|
-
const cleanup = todoList.setup("todos-demo");
|
|
159
|
-
|
|
160
|
-
// Queries are reactive where they are read.
|
|
161
|
-
const firstOpenTitle = computed(() => {
|
|
162
|
-
return todoList.visibleTodos("open")[0]?.title ?? "Nothing open";
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// Events are typed and unsubscribe cleanly.
|
|
166
|
-
const stop = todoList.on("saved", ({ count }) => {
|
|
167
|
-
console.log(`Saved ${count} todos`);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
todoList.setDraft("Write the README");
|
|
171
|
-
todoList.addTodo();
|
|
172
|
-
await todoList.save();
|
|
173
|
-
|
|
174
|
-
console.log(todoList.remainingCount);
|
|
175
|
-
console.log(firstOpenTitle.value);
|
|
176
|
-
|
|
177
|
-
stop();
|
|
178
|
-
cleanup();
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
In Preact, the same constructor can be used with `useSigma(() => new TodoList(), ["todos-demo"])` so the component owns one instance and `setup(...)` cleanup runs automatically. Use `useListener(...)` when you want component-scoped event subscriptions with automatic teardown.
|
|
182
|
-
|
|
183
|
-
Cleanup resources can be returned as functions, `AbortController`, objects with `dispose()`, or objects with `Symbol.dispose`.
|
|
184
|
-
|
|
185
|
-
Inside setup, `this` exposes the public instance plus `emit(...)` and `act(fn)`. Use `this.act(function () { ... })` when setup needs one synchronous anonymous action with normal draft, `commit()`, and `emit(...)` semantics, whether that work happens immediately in the setup body or later from a setup-owned callback, but should not become a public action method.
|
|
186
|
-
|
|
187
|
-
## Constructor and Defaults
|
|
188
|
-
|
|
189
|
-
- `defaultState` values may be plain values or zero-argument initializer functions. Use initializer functions when each instance needs a fresh object, array, or class instance.
|
|
190
|
-
- Constructor input shallowly overrides `defaultState`, so `new TodoList({ draft: "ready" })` replaces only the top-level keys you pass.
|
|
191
|
-
|
|
192
|
-
## More Docs
|
|
41
|
+
## Documentation Map
|
|
193
42
|
|
|
194
|
-
-
|
|
195
|
-
-
|
|
196
|
-
-
|
|
43
|
+
- Concepts, lifecycle, invariants, and API selection live in [`docs/context.md`](./docs/context.md).
|
|
44
|
+
- Quick-start usage lives in [`examples/basic-counter.ts`](./examples/basic-counter.ts).
|
|
45
|
+
- An advanced end-to-end example lives in [`examples/command-palette.tsx`](./examples/command-palette.tsx).
|
|
46
|
+
- Focused examples for non-obvious APIs live in [`examples/async-commit.ts`](./examples/async-commit.ts), [`examples/observe-and-restore.ts`](./examples/observe-and-restore.ts), [`examples/signal-access.ts`](./examples/signal-access.ts), and [`examples/setup-act.ts`](./examples/setup-act.ts).
|
|
47
|
+
- Exact exported signatures live in `dist/index.d.mts` after `pnpm build`.
|
package/dist/index.d.mts
CHANGED
|
@@ -59,7 +59,7 @@ type SigmaRef<T = unknown> = T & SigmaRefBrand;
|
|
|
59
59
|
type AnyEvents = Record<string, object | void>;
|
|
60
60
|
/** The top-level state object shape used by sigma types. */
|
|
61
61
|
type AnyState = Record<string, unknown>;
|
|
62
|
-
/** The object accepted by `.defaultState(...)
|
|
62
|
+
/** The object accepted by `.defaultState(...)`, where each property may be a value or a zero-argument initializer. */
|
|
63
63
|
type AnyDefaultState<TState extends AnyState> = { [K in keyof TState]?: DefaultStateValue<TState[K]> };
|
|
64
64
|
/** A cleanup resource supported by `.setup(...)`, including function, `dispose()`, and `Symbol.dispose` cleanup. */
|
|
65
65
|
type AnyResource = Cleanup | Disposable | DisposableLike | AbortController;
|
|
@@ -68,16 +68,16 @@ type ComputedContext<TState extends AnyState, TComputeds extends object> = Immut
|
|
|
68
68
|
type QueryMethods<TQueries extends object | undefined> = [undefined] extends [TQueries] ? never : { [K in keyof TQueries]: TQueries[K] extends AnyFunction ? (...args: Parameters<TQueries[K]>) => ReturnType<TQueries[K]> : never };
|
|
69
69
|
type ActionMethods<TActions extends object | undefined> = [undefined] extends [TActions] ? never : { [K in keyof TActions]: TActions[K] extends AnyFunction ? (...args: Parameters<TActions[K]>) => ReturnType<TActions[K]> : never };
|
|
70
70
|
type EventMethods<TEvents extends AnyEvents | undefined> = [undefined] extends [TEvents] ? never : {
|
|
71
|
-
readonly [sigmaEventsBrand]: TEvents;
|
|
71
|
+
readonly [sigmaEventsBrand]: TEvents; /** Registers a typed event listener and returns an unsubscribe function. */
|
|
72
72
|
on<TEvent extends string & keyof TEvents>(name: TEvent, listener: [TEvents[TEvent]] extends [void] ? () => void : (payload: TEvents[TEvent]) => void): Cleanup;
|
|
73
73
|
};
|
|
74
74
|
type SetupMethods<TSetupArgs extends any[] | undefined> = [TSetupArgs] extends [undefined] ? never : {
|
|
75
|
-
setup(...args: Extract<TSetupArgs, any[]>): Cleanup;
|
|
75
|
+
/** Runs every registered setup handler and returns one cleanup function for the active setup. */setup(...args: Extract<TSetupArgs, any[]>): Cleanup;
|
|
76
76
|
};
|
|
77
77
|
type ReadonlyContext<TState extends AnyState, TComputeds extends object, TQueries extends object> = Immutable<TState> & ComputedValues<TComputeds> & QueryMethods<TQueries>;
|
|
78
78
|
type Emit<TEvents extends AnyEvents> = <TEvent extends string & keyof TEvents>(name: TEvent, ...args: [TEvents[TEvent]] extends [void] ? [] : [payload: TEvents[TEvent]]) => void;
|
|
79
79
|
type ActionContext<TState extends AnyState, TEvents extends AnyEvents, TComputeds extends object, TQueries extends object, TActions extends object> = Draft<TState> & ComputedValues<TComputeds> & QueryMethods<TQueries> & ActionMethods<TActions> & {
|
|
80
|
-
/** Publishes the current action draft immediately so later boundaries use committed state. */commit(): void;
|
|
80
|
+
/** Publishes the current action draft immediately so later boundaries use committed state. */commit(): void; /** Emits a typed event from the current action. */
|
|
81
81
|
emit: Emit<TEvents>;
|
|
82
82
|
};
|
|
83
83
|
type DefinitionEvents<T extends SigmaDefinition> = T["events"] extends AnyEvents ? T["events"] : {};
|
|
@@ -94,7 +94,7 @@ type AnySigmaStateWithEvents<TEvents extends AnyEvents> = AnySigmaState & {
|
|
|
94
94
|
};
|
|
95
95
|
/** Options accepted by `.observe(...)`. */
|
|
96
96
|
type SigmaObserveOptions = {
|
|
97
|
-
patches?: boolean;
|
|
97
|
+
/** Includes Immer patches and inverse patches on the delivered change object. */patches?: boolean;
|
|
98
98
|
};
|
|
99
99
|
/** The change object delivered to `.observe(...)` listeners. */
|
|
100
100
|
type SigmaObserveChange<TState extends AnyState, TWithPatches extends boolean = false> = {
|
|
@@ -113,15 +113,16 @@ type SigmaDefinition = {
|
|
|
113
113
|
setupArgs?: any[];
|
|
114
114
|
};
|
|
115
115
|
interface SignalAccessors<T extends object> {
|
|
116
|
+
/** Returns the underlying signal for a top-level state property or computed. */
|
|
116
117
|
get<K extends keyof T>(key: K): ReadonlySignal<T[K]>;
|
|
117
118
|
}
|
|
118
119
|
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
|
|
119
120
|
type Simplify<T> = {} & { [K in keyof T]: T[K] };
|
|
120
121
|
type MapSigmaDefinition<T extends SigmaDefinition> = keyof T extends infer K ? K extends "state" ? Immutable<T[K]> & SignalAccessors<Immutable<T[K]>> : K extends "computeds" ? ComputedValues<T[K]> & SignalAccessors<ComputedValues<T[K]>> : K extends "queries" ? QueryMethods<T[K]> : K extends "actions" ? ActionMethods<T[K]> : K extends "events" ? EventMethods<T[K]> : K extends "setupArgs" ? SetupMethods<T[K]> : never : never;
|
|
121
|
-
/** The public instance shape produced by a configured sigma type. */
|
|
122
|
+
/** The public instance shape produced by a configured sigma type, including signal access inferred from the definition. */
|
|
122
123
|
type SigmaState<T extends SigmaDefinition = SigmaDefinition> = AnySigmaState & Simplify<UnionToIntersection<MapSigmaDefinition<T>>>;
|
|
123
124
|
type SetupContext<T extends SigmaDefinition> = SigmaState<T> & {
|
|
124
|
-
act<TResult>(fn: (this: ActionContext<T["state"], DefinitionEvents<T>, DefinitionComputeds<T>, DefinitionQueries<T>, DefinitionActions<T>>) => TResult): TResult;
|
|
125
|
+
/** Runs a synchronous anonymous action from setup so reads and writes use normal action semantics. */act<TResult>(fn: (this: ActionContext<T["state"], DefinitionEvents<T>, DefinitionComputeds<T>, DefinitionQueries<T>, DefinitionActions<T>>) => TResult): TResult; /** Emits a typed event from setup. */
|
|
125
126
|
emit: T["events"] extends object ? Emit<T["events"]> : never;
|
|
126
127
|
};
|
|
127
128
|
type MergeObjects<TLeft extends object, TRight> = [TRight] extends [object] ? Extract<Simplify<Omit<TLeft, keyof TRight> & TRight>, TLeft> : TLeft;
|
|
@@ -136,39 +137,56 @@ type InferSetupArgs<T extends AnySigmaState> = T extends {
|
|
|
136
137
|
} ? TArgs : never;
|
|
137
138
|
//#endregion
|
|
138
139
|
//#region src/internal/runtime.d.ts
|
|
139
|
-
/** Controls whether sigma deep-freezes published public state. Auto-freezing starts enabled. */
|
|
140
|
+
/** Controls whether sigma deep-freezes published public state. Auto-freezing starts enabled and the setting is shared across instances. */
|
|
140
141
|
declare function setAutoFreeze(autoFreeze: boolean): void;
|
|
141
142
|
/**
|
|
142
143
|
* Returns a shallow snapshot of an instance's committed public state.
|
|
143
144
|
*
|
|
144
145
|
* The snapshot includes one own property for each top-level state key and reads
|
|
145
|
-
* the current committed value for that key.
|
|
146
|
-
* instance's sigma-state
|
|
146
|
+
* the current committed value for that key. Nested sigma states remain as
|
|
147
|
+
* referenced values. Its type is inferred from the instance's sigma-state
|
|
148
|
+
* definition.
|
|
147
149
|
*/
|
|
148
150
|
declare function snapshot<T extends AnySigmaState>(publicInstance: T): T extends SigmaState<infer TDefinition> ? Immutable<TDefinition["state"]> : never;
|
|
149
151
|
/**
|
|
150
152
|
* Replaces an instance's committed public state from a snapshot object.
|
|
151
153
|
*
|
|
152
154
|
* The replacement snapshot must be a plain object with exactly the instance's
|
|
153
|
-
* top-level state keys.
|
|
154
|
-
*
|
|
155
|
+
* top-level state keys. It updates committed state outside action semantics and
|
|
156
|
+
* notifies observers when the committed state changes. Its type is inferred
|
|
157
|
+
* from the instance's sigma-state definition.
|
|
155
158
|
*/
|
|
156
159
|
declare function replaceState<T extends AnySigmaState>(publicInstance: T, nextState: T extends SigmaState<infer TDefinition> ? Immutable<TDefinition["state"]> : never): void;
|
|
157
160
|
//#endregion
|
|
158
161
|
//#region src/framework.d.ts
|
|
159
|
-
/** Checks whether a value is a sigma
|
|
162
|
+
/** Checks whether a value is an instance created by a configured sigma type. */
|
|
160
163
|
declare function isSigmaState(value: unknown): value is AnySigmaState;
|
|
161
|
-
/**
|
|
164
|
+
/**
|
|
165
|
+
* Creates a standalone tracked query helper with the same signature as `fn`.
|
|
166
|
+
*
|
|
167
|
+
* Each call is reactive at the call site and does not memoize results across
|
|
168
|
+
* invocations, which makes `query(fn)` a good fit for local tracked helpers
|
|
169
|
+
* that do not need to live on the sigma-state instance.
|
|
170
|
+
*/
|
|
162
171
|
declare function query<TArgs extends any[], TResult>(fn: (this: void, ...args: TArgs) => TResult): typeof fn;
|
|
163
172
|
/**
|
|
164
|
-
* Builds sigma-state constructors by accumulating default state, computeds,
|
|
165
|
-
* observers, actions, and setup handlers.
|
|
173
|
+
* Builds sigma-state constructors by accumulating default state, computeds,
|
|
174
|
+
* queries, observers, actions, and setup handlers.
|
|
175
|
+
*
|
|
176
|
+
* State and event inference starts from `new SigmaType<TState, TEvents>()`.
|
|
177
|
+
* Later builder methods infer names and types from the objects you pass to them.
|
|
166
178
|
*/
|
|
167
179
|
declare class SigmaType<TState extends AnyState, TEvents extends AnyEvents = {}, TDefaults extends AnyDefaultState<TState> = {}, TComputeds extends object = {}, TQueries extends object = {}, TActions extends object = {}, TSetupArgs extends any[] = never> extends Function {
|
|
168
180
|
constructor(name?: string);
|
|
169
181
|
}
|
|
170
182
|
/** The constructor shape exposed by a configured sigma type. */
|
|
171
183
|
interface SigmaType<TState extends AnyState, TEvents extends AnyEvents, TDefaults extends AnyDefaultState<TState>, TComputeds extends object, TQueries extends object, TActions extends object, TSetupArgs extends any[]> {
|
|
184
|
+
/**
|
|
185
|
+
* Creates a sigma-state instance.
|
|
186
|
+
*
|
|
187
|
+
* Constructor input shallowly overrides `defaultState(...)`. Required keys are
|
|
188
|
+
* inferred from whichever state properties still do not have defaults.
|
|
189
|
+
*/
|
|
172
190
|
new (...args: InitialStateInput<TState, TDefaults>): SigmaState<Extract<OmitEmpty<{
|
|
173
191
|
state: TState;
|
|
174
192
|
events: TEvents;
|
|
@@ -177,7 +195,12 @@ interface SigmaType<TState extends AnyState, TEvents extends AnyEvents, TDefault
|
|
|
177
195
|
actions: TActions;
|
|
178
196
|
setupArgs: TSetupArgs;
|
|
179
197
|
}>, SigmaDefinition>>;
|
|
180
|
-
/**
|
|
198
|
+
/**
|
|
199
|
+
* Type-only access to the configured instance shape.
|
|
200
|
+
*
|
|
201
|
+
* This property does not exist at runtime. Its type is inferred from the
|
|
202
|
+
* generics on `new SigmaType<TState, TEvents>()` plus the later builder inputs.
|
|
203
|
+
*/
|
|
181
204
|
get Instance(): SigmaState<Extract<OmitEmpty<{
|
|
182
205
|
state: TState;
|
|
183
206
|
events: TEvents;
|
|
@@ -186,9 +209,34 @@ interface SigmaType<TState extends AnyState, TEvents extends AnyEvents, TDefault
|
|
|
186
209
|
actions: TActions;
|
|
187
210
|
setupArgs: TSetupArgs;
|
|
188
211
|
}>, SigmaDefinition>>;
|
|
212
|
+
/**
|
|
213
|
+
* Adds top-level public state and default values to the builder.
|
|
214
|
+
*
|
|
215
|
+
* Each property becomes a reactive public state property on instances. Use a
|
|
216
|
+
* zero-argument function when each instance needs a fresh object or array.
|
|
217
|
+
*/
|
|
189
218
|
defaultState<TNextDefaults extends AnyDefaultState<TState>>(defaultState: TNextDefaults): SigmaType<TState, TEvents, MergeObjects<TDefaults, TNextDefaults>, TComputeds, TQueries, TActions, TSetupArgs>;
|
|
219
|
+
/**
|
|
220
|
+
* Adds reactive getter properties for derived values that take no arguments.
|
|
221
|
+
*
|
|
222
|
+
* Computed names and return types are inferred from the object you pass.
|
|
223
|
+
* `this` exposes readonly state plus computeds that are already on the builder.
|
|
224
|
+
*/
|
|
190
225
|
computed<TNextComputeds extends object>(computeds: TNextComputeds & ThisType<ComputedContext<TState, MergeObjects<TComputeds, TNextComputeds>>>): SigmaType<TState, TEvents, TDefaults, MergeObjects<TComputeds, TNextComputeds>, TQueries, TActions, TSetupArgs>;
|
|
226
|
+
/**
|
|
227
|
+
* Adds reactive read methods that accept arguments.
|
|
228
|
+
*
|
|
229
|
+
* Query names, parameters, and return types are inferred from the object you
|
|
230
|
+
* pass. Each call tracks reactively at the call site and does not memoize
|
|
231
|
+
* results across invocations.
|
|
232
|
+
*/
|
|
191
233
|
queries<TNextQueries extends object>(queries: TNextQueries & ThisType<ReadonlyContext<TState, TComputeds, MergeObjects<TQueries, TNextQueries>>>): SigmaType<TState, TEvents, TDefaults, TComputeds, MergeObjects<TQueries, TNextQueries>, TActions, TSetupArgs>;
|
|
234
|
+
/**
|
|
235
|
+
* Adds a committed-state observer.
|
|
236
|
+
*
|
|
237
|
+
* Observers run after successful publishes and can opt into Immer patches
|
|
238
|
+
* with `{ patches: true }`.
|
|
239
|
+
*/
|
|
192
240
|
observe(listener: (this: ReadonlyContext<TState, TComputeds, TQueries>, change: SigmaObserveChange<TState>) => void, options?: SigmaObserveOptions & {
|
|
193
241
|
patches?: false | undefined;
|
|
194
242
|
}): this;
|
|
@@ -207,6 +255,12 @@ interface SigmaType<TState extends AnyState, TEvents extends AnyEvents, TDefault
|
|
|
207
255
|
* returns a promise, sigma throws so async boundaries stay explicit.
|
|
208
256
|
*/
|
|
209
257
|
actions<TNextActions extends object>(actions: TNextActions & ThisType<ActionContext<TState, TEvents, TComputeds, TQueries, MergeObjects<TActions, TNextActions>>>): SigmaType<TState, TEvents, TDefaults, TComputeds, TQueries, MergeObjects<TActions, TNextActions>, TSetupArgs>;
|
|
258
|
+
/**
|
|
259
|
+
* Adds an explicit setup handler for side effects and owned resources.
|
|
260
|
+
*
|
|
261
|
+
* Every registered handler runs when `instance.setup(...)` is called, and the
|
|
262
|
+
* setup argument list is inferred from the first handler you add.
|
|
263
|
+
*/
|
|
210
264
|
setup<TNextSetupArgs extends ([TSetupArgs] extends [never] ? any[] : NonNullable<TSetupArgs>)>(setup: (this: SetupContext<{
|
|
211
265
|
state: TState;
|
|
212
266
|
events: TEvents;
|
|
@@ -224,21 +278,32 @@ type TypedEventListener<TEventMap, TEvent extends string, TCurrentTarget extends
|
|
|
224
278
|
}) => void) & {
|
|
225
279
|
__eventType?: string extends TEvent ? keyof TEventMap : TEvent;
|
|
226
280
|
};
|
|
227
|
-
/** Infers the
|
|
281
|
+
/** Infers the event names accepted by `listen(...)` or `useListener(...)` for a target. */
|
|
228
282
|
type InferEventType<TTarget extends EventTarget> = (InferListener<TTarget> extends {
|
|
229
283
|
__eventType?: infer TEvent;
|
|
230
284
|
} ? string & TEvent : never) | (string & {});
|
|
231
|
-
/** Infers the listener
|
|
285
|
+
/** Infers the listener callback shape for a target and event name. Sigma states receive payloads directly, while DOM targets receive typed events. */
|
|
232
286
|
type InferListener<TTarget extends EventTarget, TEvent extends string = string> = TTarget extends AnySigmaStateWithEvents<infer TEvents> ? TEvent extends keyof TEvents ? (event: TEvents[TEvent]) => void : never : TTarget extends Window ? TypedEventListener<WindowEventMap, TEvent, TTarget> : TTarget extends Document ? TypedEventListener<DocumentEventMap, TEvent, TTarget> : TTarget extends HTMLBodyElement ? TypedEventListener<HTMLBodyElementEventMap, TEvent, TTarget> : TTarget extends HTMLMediaElement ? TypedEventListener<HTMLMediaElementEventMap, TEvent, TTarget> : TTarget extends HTMLElement ? TypedEventListener<HTMLElementEventMap, TEvent, TTarget> : TTarget extends SVGSVGElement ? TypedEventListener<SVGSVGElementEventMap, TEvent, TTarget> : TTarget extends SVGElement ? TypedEventListener<SVGElementEventMap, TEvent, TTarget> : (event: Event & {
|
|
233
287
|
readonly currentTarget: TTarget;
|
|
234
288
|
}) => void;
|
|
235
|
-
/** Adds
|
|
289
|
+
/** Adds a listener to a sigma state or DOM target and returns a cleanup function that removes it. */
|
|
236
290
|
declare function listen<TTarget extends EventTarget, TEvent extends InferEventType<TTarget>>(target: TTarget, name: TEvent, fn: InferListener<TTarget, TEvent>): () => void;
|
|
237
291
|
//#endregion
|
|
238
292
|
//#region src/hooks.d.ts
|
|
239
|
-
/**
|
|
293
|
+
/**
|
|
294
|
+
* Creates one sigma-state instance for a component.
|
|
295
|
+
*
|
|
296
|
+
* `create()` runs once per mounted component instance. When the sigma state
|
|
297
|
+
* defines setup, `useSigma(...)` also runs `setup(...setupArgs)` in an effect
|
|
298
|
+
* and cleans it up when the setup arguments change or the component unmounts.
|
|
299
|
+
*/
|
|
240
300
|
declare function useSigma<T extends AnySigmaState>(create: () => T, setupArgs?: InferSetupArgs<T>): T;
|
|
241
|
-
/**
|
|
301
|
+
/**
|
|
302
|
+
* Attaches an event listener in a component and cleans it up automatically.
|
|
303
|
+
*
|
|
304
|
+
* Passing `null` disables the listener. The latest callback is used without
|
|
305
|
+
* forcing the effect to resubscribe on every render.
|
|
306
|
+
*/
|
|
242
307
|
declare function useListener<TTarget extends EventTarget | AnySigmaState, TEvent extends InferEventType<TTarget>>(target: TTarget | null, name: TEvent, listener: InferListener<TTarget, TEvent>): void;
|
|
243
308
|
//#endregion
|
|
244
309
|
export { type AnyDefaultState, type AnyEvents, type AnyResource, type AnySigmaState, type AnySigmaStateWithEvents, type AnyState, InferEventType, InferListener, type InferSetupArgs, type SigmaObserveChange, type SigmaObserveOptions, type SigmaRef, type SigmaState, SigmaType, action, batch, computed, effect, freeze, immerable, isSigmaState, listen, query, replaceState, setAutoFreeze, snapshot, untracked, useListener, useSigma };
|
package/dist/index.mjs
CHANGED
|
@@ -198,7 +198,7 @@ function getSigmaInternals(context) {
|
|
|
198
198
|
function isAutoFreeze() {
|
|
199
199
|
return autoFreezeEnabled;
|
|
200
200
|
}
|
|
201
|
-
/** Controls whether sigma deep-freezes published public state. Auto-freezing starts enabled. */
|
|
201
|
+
/** Controls whether sigma deep-freezes published public state. Auto-freezing starts enabled and the setting is shared across instances. */
|
|
202
202
|
function setAutoFreeze(autoFreeze) {
|
|
203
203
|
autoFreezeEnabled = autoFreeze;
|
|
204
204
|
immer.setAutoFreeze(autoFreeze);
|
|
@@ -446,8 +446,9 @@ function publishState(instance, finalized) {
|
|
|
446
446
|
* Returns a shallow snapshot of an instance's committed public state.
|
|
447
447
|
*
|
|
448
448
|
* The snapshot includes one own property for each top-level state key and reads
|
|
449
|
-
* the current committed value for that key.
|
|
450
|
-
* instance's sigma-state
|
|
449
|
+
* the current committed value for that key. Nested sigma states remain as
|
|
450
|
+
* referenced values. Its type is inferred from the instance's sigma-state
|
|
451
|
+
* definition.
|
|
451
452
|
*/
|
|
452
453
|
function snapshot(publicInstance) {
|
|
453
454
|
return snapshotState(getSigmaInternals(publicInstance));
|
|
@@ -456,8 +457,9 @@ function snapshot(publicInstance) {
|
|
|
456
457
|
* Replaces an instance's committed public state from a snapshot object.
|
|
457
458
|
*
|
|
458
459
|
* The replacement snapshot must be a plain object with exactly the instance's
|
|
459
|
-
* top-level state keys.
|
|
460
|
-
*
|
|
460
|
+
* top-level state keys. It updates committed state outside action semantics and
|
|
461
|
+
* notifies observers when the committed state changes. Its type is inferred
|
|
462
|
+
* from the instance's sigma-state definition.
|
|
461
463
|
*/
|
|
462
464
|
function replaceState(publicInstance, nextState) {
|
|
463
465
|
const instance = getSigmaInternals(publicInstance);
|
|
@@ -537,17 +539,26 @@ var Sigma = class extends EventTarget {
|
|
|
537
539
|
Object.defineProperty(Sigma.prototype, sigmaStateBrand, { value: true });
|
|
538
540
|
//#endregion
|
|
539
541
|
//#region src/framework.ts
|
|
540
|
-
/** Checks whether a value is a sigma
|
|
542
|
+
/** Checks whether a value is an instance created by a configured sigma type. */
|
|
541
543
|
function isSigmaState(value) {
|
|
542
544
|
return Boolean(value && typeof value === "object" && value[sigmaStateBrand]);
|
|
543
545
|
}
|
|
544
|
-
/**
|
|
546
|
+
/**
|
|
547
|
+
* Creates a standalone tracked query helper with the same signature as `fn`.
|
|
548
|
+
*
|
|
549
|
+
* Each call is reactive at the call site and does not memoize results across
|
|
550
|
+
* invocations, which makes `query(fn)` a good fit for local tracked helpers
|
|
551
|
+
* that do not need to live on the sigma-state instance.
|
|
552
|
+
*/
|
|
545
553
|
function query(fn) {
|
|
546
554
|
return ((...args) => computed$1(() => fn(...args)).value);
|
|
547
555
|
}
|
|
548
556
|
/**
|
|
549
|
-
* Builds sigma-state constructors by accumulating default state, computeds,
|
|
550
|
-
* observers, actions, and setup handlers.
|
|
557
|
+
* Builds sigma-state constructors by accumulating default state, computeds,
|
|
558
|
+
* queries, observers, actions, and setup handlers.
|
|
559
|
+
*
|
|
560
|
+
* State and event inference starts from `new SigmaType<TState, TEvents>()`.
|
|
561
|
+
* Later builder methods infer names and types from the objects you pass to them.
|
|
551
562
|
*/
|
|
552
563
|
var SigmaType = class extends Function {
|
|
553
564
|
constructor(name = "Sigma") {
|
|
@@ -618,7 +629,7 @@ var SigmaType = class extends Function {
|
|
|
618
629
|
};
|
|
619
630
|
//#endregion
|
|
620
631
|
//#region src/listener.ts
|
|
621
|
-
/** Adds
|
|
632
|
+
/** Adds a listener to a sigma state or DOM target and returns a cleanup function that removes it. */
|
|
622
633
|
function listen(target, name, fn) {
|
|
623
634
|
const listener = isSigmaState(target) ? (event) => fn(event.detail) : fn;
|
|
624
635
|
target.addEventListener(name, listener);
|
|
@@ -628,7 +639,13 @@ function listen(target, name, fn) {
|
|
|
628
639
|
}
|
|
629
640
|
//#endregion
|
|
630
641
|
//#region src/hooks.ts
|
|
631
|
-
/**
|
|
642
|
+
/**
|
|
643
|
+
* Creates one sigma-state instance for a component.
|
|
644
|
+
*
|
|
645
|
+
* `create()` runs once per mounted component instance. When the sigma state
|
|
646
|
+
* defines setup, `useSigma(...)` also runs `setup(...setupArgs)` in an effect
|
|
647
|
+
* and cleans it up when the setup arguments change or the component unmounts.
|
|
648
|
+
*/
|
|
632
649
|
function useSigma(create, setupArgs) {
|
|
633
650
|
const sigmaState = useState(create)[0];
|
|
634
651
|
if (shouldSetup(sigmaState)) {
|
|
@@ -637,7 +654,12 @@ function useSigma(create, setupArgs) {
|
|
|
637
654
|
}
|
|
638
655
|
return sigmaState;
|
|
639
656
|
}
|
|
640
|
-
/**
|
|
657
|
+
/**
|
|
658
|
+
* Attaches an event listener in a component and cleans it up automatically.
|
|
659
|
+
*
|
|
660
|
+
* Passing `null` disables the listener. The latest callback is used without
|
|
661
|
+
* forcing the effect to resubscribe on every render.
|
|
662
|
+
*/
|
|
641
663
|
function useListener(target, name, listener) {
|
|
642
664
|
const listenerRef = useRef(listener);
|
|
643
665
|
listenerRef.current = listener;
|
package/docs/context.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Overview
|
|
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.
|
|
4
|
+
|
|
5
|
+
# When to Use
|
|
6
|
+
|
|
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.
|
|
10
|
+
- A model needs to own timers, subscriptions, listeners, nested setup, or other cleanup-aware resources.
|
|
11
|
+
- Components should consume the same model shape used outside Preact.
|
|
12
|
+
|
|
13
|
+
# When Not to Use
|
|
14
|
+
|
|
15
|
+
- A few plain signals already cover the state without extra coordination.
|
|
16
|
+
- You want side effects to start implicitly during construction.
|
|
17
|
+
- The main problem is remote caching, normalization, or cross-app store tooling rather than local state behavior.
|
|
18
|
+
- You need ad hoc mutable objects with no benefit from typed actions, setup, or signal-backed reads.
|
|
19
|
+
|
|
20
|
+
# Core Abstractions
|
|
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
|
+
- State property: a top-level key from `TState`. Each one becomes a readonly reactive public property and gets its own signal.
|
|
25
|
+
- Computed: an argument-free derived getter declared with `.computed(...)`.
|
|
26
|
+
- Query: a reactive read that accepts arguments, declared with `.queries(...)` or built locally with `query(fn)`.
|
|
27
|
+
- Action: a method declared with `.actions(...)` that reads and writes through sigma's draft and commit semantics.
|
|
28
|
+
- Setup handler: a function declared with `.setup(...)` that owns side effects and cleanup resources explicitly.
|
|
29
|
+
- Event: a typed notification emitted through `this.emit(...)` and observed through `.on(...)`, `listen(...)`, or `useListener(...)`.
|
|
30
|
+
|
|
31
|
+
# Data Flow / Lifecycle
|
|
32
|
+
|
|
33
|
+
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.
|
|
34
|
+
2. Add `defaultState(...)` for top-level public state and optional per-instance initializers.
|
|
35
|
+
3. Add `computed(...)`, `queries(...)`, and `actions(...)` for derived reads and writes.
|
|
36
|
+
4. Instantiate the configured type. Constructor input shallowly overrides `defaultState(...)`.
|
|
37
|
+
5. Read state, computeds, and queries reactively from the public instance.
|
|
38
|
+
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.
|
|
39
|
+
7. Run `setup(...)` explicitly when the instance should start owning side effects. `useSigma(...)` does this automatically for component-owned instances that define setup.
|
|
40
|
+
8. Dispose the cleanup returned from `setup(...)` when the owned resources should stop.
|
|
41
|
+
|
|
42
|
+
# Common Tasks -> Recommended APIs
|
|
43
|
+
|
|
44
|
+
- Define reusable model state: `new SigmaType<TState, TEvents>().defaultState(...)`
|
|
45
|
+
- Derive an argument-free value: `.computed(...)`
|
|
46
|
+
- Derive a reactive read with arguments: `.queries(...)`
|
|
47
|
+
- Keep a tracked helper local to one consumer module: `query(fn)`
|
|
48
|
+
- Mutate state and emit typed notifications: `.actions(...)`
|
|
49
|
+
- Publish before `await`, `emit(...)`, or another action boundary: `this.commit()`
|
|
50
|
+
- React to committed state changes: `.observe(...)`
|
|
51
|
+
- Own timers, listeners, subscriptions, or nested setup: `.setup(...)`
|
|
52
|
+
- Use a sigma state inside a component: `useSigma(...)`
|
|
53
|
+
- Subscribe to sigma or DOM events in a component: `useListener(...)`
|
|
54
|
+
- Subscribe outside components: `.on(...)` or `listen(...)`
|
|
55
|
+
- Read or restore committed top-level state: `snapshot(...)` and `replaceState(...)`
|
|
56
|
+
|
|
57
|
+
# Practical Guidelines
|
|
58
|
+
|
|
59
|
+
- Put explicit type arguments on `new SigmaType<TState, TEvents>()` and let later builder methods infer from the objects you pass.
|
|
60
|
+
- Keep frequently read values as separate top-level state properties. Each top-level key gets its own signal.
|
|
61
|
+
- Use `.computed(...)` for argument-free derived reads.
|
|
62
|
+
- Use `.queries(...)` for tracked reads with arguments.
|
|
63
|
+
- Keep one-off calculations local until they become reusable model behavior.
|
|
64
|
+
- Reach for `instance.get(key)` only when code specifically needs the underlying `ReadonlySignal`.
|
|
65
|
+
- Treat `emit(...)`, `await`, and any action call other than a same-instance synchronous nested action call as draft boundaries. Call `this.commit()` only when pending changes need to become public before one of those boundaries.
|
|
66
|
+
- Use ordinary actions for routine writes. Reserve `snapshot(...)` and `replaceState(...)` for replay, reset, or undo-like flows on committed top-level state.
|
|
67
|
+
- Put owned side effects in `.setup(...)`.
|
|
68
|
+
- Use `this.act(function () { ... })` for setup-owned callbacks that need action semantics.
|
|
69
|
+
- Call Immer's `enablePatches()` before relying on `.observe(..., { patches: true })`.
|
|
70
|
+
|
|
71
|
+
# Invariants and Constraints
|
|
72
|
+
|
|
73
|
+
- Sigma only tracks top-level state properties. Each top-level key gets its own signal.
|
|
74
|
+
- Public state is readonly outside actions and `this.act(...)` inside setup.
|
|
75
|
+
- Duplicate names across state properties, computeds, queries, and actions are rejected at runtime. Reserved public names include `act`, `emit`, `get`, `on`, and `setup`.
|
|
76
|
+
- Query calls are reactive at the call site but do not memoize across invocations.
|
|
77
|
+
- Setup handlers return arrays of cleanup resources, and cleanup runs in reverse order.
|
|
78
|
+
- `replaceState(...)` works on committed top-level state and requires the exact state-key shape.
|
|
79
|
+
- Published draftable public state is deep-frozen by default. `setAutoFreeze(false)` disables that behavior globally.
|
|
80
|
+
|
|
81
|
+
# Error Model
|
|
82
|
+
|
|
83
|
+
- Crossing an action boundary with unpublished changes throws until `this.commit()` publishes them. Async actions also reject when they finish with unpublished changes.
|
|
84
|
+
- If another invocation crosses a boundary while unpublished changes still exist, sigma warns and discards those changes before continuing.
|
|
85
|
+
- Calling `setup(...)` on a sigma state without registered setup handlers throws.
|
|
86
|
+
- Cleanup rethrows an `AggregateError` when more than one cleanup resource fails.
|
|
87
|
+
- `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.
|
|
88
|
+
|
|
89
|
+
# Terminology
|
|
90
|
+
|
|
91
|
+
- Draft boundary: a point where sigma cannot keep reusing the current unpublished draft.
|
|
92
|
+
- Committed state: the published top-level public state visible outside the current action draft.
|
|
93
|
+
- Signal access: reading the underlying `ReadonlySignal` for a top-level state key or computed through `instance.get(key)`.
|
|
94
|
+
- Cleanup resource: a cleanup function, `AbortController`, object with `dispose()`, or object with `[Symbol.dispose]()`.
|
|
95
|
+
- 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.
|
|
96
|
+
|
|
97
|
+
# Non-Goals
|
|
98
|
+
|
|
99
|
+
- Replacing every plain-signal use case with a builder abstraction.
|
|
100
|
+
- Hiding lifecycle behind implicit setup or constructor side effects.
|
|
101
|
+
- Memoizing every query call or turning queries into a global cache.
|
|
102
|
+
- 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,42 @@
|
|
|
1
|
+
import { SigmaType } from "preact-sigma";
|
|
2
|
+
|
|
3
|
+
const SaveIndicator = new SigmaType<
|
|
4
|
+
{
|
|
5
|
+
savedCount: number;
|
|
6
|
+
saving: boolean;
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
saved: {
|
|
10
|
+
count: number;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
>("SaveIndicator")
|
|
14
|
+
.defaultState({
|
|
15
|
+
savedCount: 0,
|
|
16
|
+
saving: false,
|
|
17
|
+
})
|
|
18
|
+
.actions({
|
|
19
|
+
async save() {
|
|
20
|
+
this.saving = true;
|
|
21
|
+
this.commit(); // Publish before the async boundary.
|
|
22
|
+
|
|
23
|
+
await Promise.resolve();
|
|
24
|
+
|
|
25
|
+
this.savedCount += 1;
|
|
26
|
+
this.saving = false;
|
|
27
|
+
this.commit(); // Publish before emitting the event boundary.
|
|
28
|
+
|
|
29
|
+
this.emit("saved", { count: this.savedCount });
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const indicator = new SaveIndicator();
|
|
34
|
+
|
|
35
|
+
indicator.on("saved", ({ count }) => {
|
|
36
|
+
console.log(`Saved ${count} times`);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await indicator.save();
|
|
40
|
+
|
|
41
|
+
console.log(indicator.saving); // false
|
|
42
|
+
console.log(indicator.savedCount); // 1
|