preact-sigma 2.2.1 → 2.2.2

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 (2) hide show
  1. package/README.md +189 -58
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,23 +1,56 @@
1
1
  # preact-sigma
2
2
 
3
- `preact-sigma` is a typed state-model builder for apps that want Preact's fine-grained reactivity, Immer-backed writes, and explicit lifecycle.
3
+ `preact-sigma` lets you define state once and reuse it as a model.
4
4
 
5
- You define a reusable state type once, then create instances wherever they make sense: inside components, in shared modules, or in plain TypeScript code. Each instance exposes readonly public state, tracked derived reads, imperative actions, and optional setup and event APIs.
5
+ It is built for Preact and TypeScript, and combines:
6
6
 
7
- ## Getting Started
7
+ - fine-grained reactive reads
8
+ - Immer-style writes
9
+ - explicit setup and cleanup
10
+ - typed events
11
+ - a constructor you can instantiate anywhere
8
12
 
9
- To add `preact-sigma` to your project:
13
+ Use it when your state has started to feel like more than "some values in a component."
14
+
15
+ Instead of spreading logic across loose signals, reducers, effects, and cleanup code, you define one model with:
16
+
17
+ - top-level state
18
+ - derived reads
19
+ - write methods
20
+ - side-effect setup
21
+ - optional events
22
+
23
+ Then you create instances wherever they make sense: inside a component, in a shared module, or in plain TypeScript.
24
+
25
+ Under the hood, each top-level state property is backed by its own Preact signal, while writes happen through actions with Immer-backed mutation semantics.
26
+
27
+ ## Why you would use it
28
+
29
+ `preact-sigma` is a good fit when you want state and behavior to live together.
30
+
31
+ It is especially useful when you need to:
32
+
33
+ - keep state, derived values, mutations, and lifecycle in one place
34
+ - create multiple instances of the same state model
35
+ - expose readonly public state while keeping writes explicit
36
+ - get fine-grained reactivity without wiring together a pile of loose signals
37
+ - own timers, subscriptions, listeners, or nested setup with clear cleanup
38
+
39
+ If a couple of plain signals are enough, use plain signals.
40
+ `preact-sigma` is for the point where state starts acting like a small system.
41
+
42
+ ## Install
10
43
 
11
44
  ```bash
12
45
  npm install preact-sigma
13
46
  ```
14
47
 
15
- ## Smallest Useful Example
48
+ ## 30-second example
16
49
 
17
50
  ```ts
18
51
  import { SigmaType } from "preact-sigma";
19
52
 
20
- const Counter = new SigmaType<{ count: number }>("Counter")
53
+ const Counter = new SigmaType<{ count: number }>()
21
54
  .defaultState({
22
55
  count: 0,
23
56
  })
@@ -40,44 +73,96 @@ console.log(counter.count); // 1
40
73
  console.log(counter.doubled); // 2
41
74
  ```
42
75
 
43
- ## What It Is
76
+ That example shows the basic shape:
77
+
78
+ - state is public and reactive
79
+ - derived values live in `computed(...)`
80
+ - writes happen in `actions(...)`
81
+ - an instance behaves like a small stateful object
82
+
83
+ ## The mental model
84
+
85
+ A sigma model is made from a few simple pieces.
86
+
87
+ ### `defaultState(...)`
88
+
89
+ Defines the top-level state for each instance.
90
+
91
+ Each top-level property becomes a reactive public property on the instance.
92
+
93
+ Use plain values for simple defaults, or zero-argument functions when each instance needs a fresh object or array.
94
+
95
+ ### `computed(...)`
96
+
97
+ Use computeds for derived values that take no arguments.
98
+
99
+ They behave like tracked getters:
100
+
101
+ ```ts
102
+ .completedCount() // no
103
+ todoList.completedCount // yes
104
+ ```
105
+
106
+ ### `queries(...)`
44
107
 
45
- At its core, `preact-sigma` lets you describe a stateful model as a constructor:
108
+ Use queries for reactive reads that need arguments.
46
109
 
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
110
+ ```ts
111
+ visibleTodos("open");
112
+ ```
113
+
114
+ Queries are for reading, not writing.
53
115
 
54
- The result feels like a small stateful object from application code, while still behaving like signal-driven state from rendering code.
116
+ ### `actions(...)`
55
117
 
56
- ## What You Can Do With It
118
+ Actions are where state changes happen.
57
119
 
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.
120
+ Outside an action, public state is readonly. Inside an action, you write with normal mutation syntax and sigma handles the draft/update flow for you.
59
121
 
60
- With it, you can:
122
+ ```ts
123
+ .actions({
124
+ rename(title: string) {
125
+ this.title = title;
126
+ },
127
+ })
128
+ ```
61
129
 
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(...)`
130
+ ### `setup(...)`
70
131
 
71
- ## Why This Shape Exists
132
+ Setup is where side effects belong.
72
133
 
73
- This package exists to keep stateful logic cohesive without giving up signal-level reactivity.
134
+ Use it for things like:
74
135
 
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.
136
+ - timers
137
+ - event listeners
138
+ - subscriptions
139
+ - nested model setup
140
+ - storage sync
76
141
 
77
- ## Big Picture Example
142
+ Setup is explicit. A new instance does not automatically run setup. When setup does run, it returns one cleanup function that tears down everything that instance owns.
143
+
144
+ ### Events
145
+
146
+ Use events when the model needs to notify the outside world without exposing mutable internals.
147
+
148
+ Emit inside actions or setup:
78
149
 
79
150
  ```ts
80
- import { computed, SigmaType } from "preact-sigma";
151
+ this.emit("saved", { count: 3 });
152
+ ```
153
+
154
+ Listen from the outside:
155
+
156
+ ```ts
157
+ const stop = instance.on("saved", ({ count }) => {
158
+ console.log(count);
159
+ });
160
+ ```
161
+
162
+ ## A more realistic example
163
+
164
+ ```ts
165
+ import { SigmaType } from "preact-sigma";
81
166
 
82
167
  type Todo = {
83
168
  id: string;
@@ -88,20 +173,18 @@ type Todo = {
88
173
  const TodoList = new SigmaType<
89
174
  { draft: string; todos: Todo[]; saving: boolean },
90
175
  { saved: { count: number } }
91
- >("TodoList")
176
+ >()
92
177
  .defaultState({
93
178
  draft: "",
94
179
  todos: [],
95
180
  saving: false,
96
181
  })
97
182
  .computed({
98
- // Computeds are tracked getters with no arguments.
99
183
  remainingCount() {
100
184
  return this.todos.filter((todo) => !todo.done).length;
101
185
  },
102
186
  })
103
187
  .queries({
104
- // Queries stay reactive at the call site and can accept arguments.
105
188
  visibleTodos(filter: "all" | "open" | "done") {
106
189
  return this.todos.filter((todo) => {
107
190
  if (filter === "open") return !todo.done;
@@ -111,7 +194,6 @@ const TodoList = new SigmaType<
111
194
  },
112
195
  })
113
196
  .actions({
114
- // Public state is readonly, so writes live in actions.
115
197
  setDraft(draft: string) {
116
198
  this.draft = draft;
117
199
  },
@@ -123,6 +205,7 @@ const TodoList = new SigmaType<
123
205
  title: this.draft,
124
206
  done: false,
125
207
  });
208
+
126
209
  this.draft = "";
127
210
  },
128
211
  toggleTodo(id: string) {
@@ -131,7 +214,7 @@ const TodoList = new SigmaType<
131
214
  },
132
215
  async save() {
133
216
  this.saving = true;
134
- this.commit(); // Publish the loading state before awaiting.
217
+ this.commit(); // publish "saving" before awaiting
135
218
 
136
219
  await fetch("/api/todos", {
137
220
  method: "POST",
@@ -139,12 +222,11 @@ const TodoList = new SigmaType<
139
222
  });
140
223
 
141
224
  this.saving = false;
142
- this.commit(); // Publish post-await writes explicitly.
225
+ this.commit(); // publish before emitting
143
226
  this.emit("saved", { count: this.todos.length });
144
227
  },
145
228
  })
146
229
  .setup(function (storageKey: string) {
147
- // Setup is explicit and returns cleanup resources.
148
230
  const interval = window.setInterval(() => {
149
231
  localStorage.setItem(storageKey, JSON.stringify(this.todos));
150
232
  }, 1000);
@@ -153,44 +235,93 @@ const TodoList = new SigmaType<
153
235
  });
154
236
 
155
237
  const todoList = new TodoList();
156
-
157
- // setup(...) returns one cleanup function for everything this instance owns.
158
238
  const cleanup = todoList.setup("todos-demo");
159
239
 
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
240
  const stop = todoList.on("saved", ({ count }) => {
167
241
  console.log(`Saved ${count} todos`);
168
242
  });
169
243
 
170
- todoList.setDraft("Write the README");
244
+ todoList.setDraft("Rewrite the README");
171
245
  todoList.addTodo();
172
- await todoList.save();
173
246
 
174
247
  console.log(todoList.remainingCount);
175
- console.log(firstOpenTitle.value);
248
+ console.log(todoList.visibleTodos("open"));
249
+
250
+ await todoList.save();
176
251
 
177
252
  stop();
178
253
  cleanup();
179
254
  ```
180
255
 
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.
256
+ ## The one rule to remember about actions
257
+
258
+ For normal synchronous actions, mutate state and return. You usually do **not** need `this.commit()`.
259
+
260
+ Use `this.commit()` when you have unpublished changes and the action is about to cross a boundary like:
261
+
262
+ - `await`
263
+ - `emit(...)`
264
+ - another action boundary that should not keep using the current draft
265
+
266
+ In practice, that means:
267
+
268
+ - sync action with no boundary: mutate and return
269
+ - async action before `await`: `commit()` if you want those changes published first
270
+ - action before `emit(...)`: `commit()` if there are pending changes
271
+
272
+ That rule is the main thing to learn beyond the basic API.
273
+
274
+ ## In Preact
275
+
276
+ `preact-sigma` works outside components, but it also has a nice component story.
277
+
278
+ Use `useSigma(...)` when the component should own one instance:
279
+
280
+ ```ts
281
+ import { useSigma } from "preact-sigma";
282
+
283
+ const todoList = useSigma(() => new TodoList(), ["todos-demo"]);
284
+ ```
285
+
286
+ If the model defines setup handlers, `useSigma(...)` runs setup for that component-owned instance and cleans it up automatically when setup params change or the component unmounts.
287
+
288
+ Use `useListener(...)` for component-scoped event subscriptions:
289
+
290
+ ```ts
291
+ import { useListener } from "preact-sigma";
292
+
293
+ useListener(todoList, "saved", ({ count }) => {
294
+ console.log(`Saved ${count} todos`);
295
+ });
296
+ ```
297
+
298
+ ## What you get out of the box
299
+
300
+ Beyond the core model API, `preact-sigma` also includes:
301
+
302
+ - `observe(...)` for reacting to committed state changes
303
+ - optional Immer patch delivery in observers
304
+ - `snapshot(...)` and `replaceState(...)` for restore/undo-like flows
305
+ - `get(key)` when you need direct signal access for a state key or computed
306
+
307
+ ## Why this shape exists
308
+
309
+ `preact-sigma` exists for the space between two extremes:
182
310
 
183
- Cleanup resources can be returned as functions, `AbortController`, objects with `dispose()`, or objects with `Symbol.dispose`.
311
+ - **too small for a big store abstraction**
312
+ - **too stateful for a handful of loose signals**
184
313
 
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.
314
+ It keeps the ergonomics of working with a model object, while preserving:
186
315
 
187
- ## Constructor and Defaults
316
+ - readonly public reads
317
+ - explicit write boundaries
318
+ - fine-grained reactivity
319
+ - explicit ownership of side effects
188
320
 
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.
321
+ That makes it useful for app state that has real behavior, not just values.
191
322
 
192
- ## More Docs
323
+ ## More docs
193
324
 
194
- - [llms.txt](./llms.txt) provides the exhaustive machine-oriented API guide and terminology.
325
+ - [`llms.txt`](./llms.txt) contains the exhaustive API and behavior reference.
195
326
  - Companion skills are available via `npx skills add alloc/preact-sigma`.
196
- - The `preact-sigma` skill packages the procedural guidance and agent-oriented workflow for this library.
327
+ - The `preact-sigma` skill packages procedural guidance and agent-oriented workflow for the library.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "preact-sigma",
3
- "version": "2.2.1",
3
+ "version": "2.2.2",
4
4
  "keywords": [],
5
5
  "license": "MIT",
6
6
  "author": "Alec Larson",