preact-sigma 5.0.0 → 6.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.
@@ -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
package/docs/persist.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- `preact-sigma/persist` persists and restores committed top-level sigma state without moving storage, scheduling, or migration policy into `SigmaType`.
5
+ `preact-sigma/persist` persists and restores committed top-level sigma state without moving storage, scheduling, or migration policy into `Sigma` classes.
6
6
 
7
7
  The module builds on the core committed-state helpers:
8
8
 
9
- - `sigma.getState(instance)` reads the current committed snapshot.
9
+ - `sigma.captureState(instance)` reads the current committed snapshot.
10
10
  - `sigma.replaceState(instance, nextState)` restores a committed snapshot.
11
11
  - `sigma.subscribe(instance, handler)` observes future committed publishes.
12
12
 
@@ -15,52 +15,53 @@ Use the persist module when those primitives are the right boundary, but you do
15
15
  ## When to Use
16
16
 
17
17
  - State should survive reloads, navigation, or app restarts.
18
- - Persistence needs to stay instance-specific instead of becoming part of the model definition.
18
+ - Persistence needs to stay instance-specific instead of becoming part of the model class.
19
19
  - Storage may be synchronous or asynchronous.
20
20
  - Stored payloads need versioning, migration, or partial persistence.
21
21
  - Restore and future persistence should share one small lifecycle helper.
22
22
 
23
23
  ## When Not to Use
24
24
 
25
- - A one-off snapshot or replay flow is enough. Use `sigma.getState(...)` and `sigma.replaceState(...)` directly.
25
+ - A one-off snapshot or replay flow is enough. Use `sigma.captureState(...)` and `sigma.replaceState(...)` directly.
26
26
  - The data is really a remote cache, normalization layer, or conflict-resolution problem.
27
27
  - You need unpublished drafts, computeds, queries, setup resources, or emitted events persisted.
28
28
  - The model should start side effects before async restore completes. Sequence that explicitly outside `useSigma(...)`.
29
29
 
30
30
  ## Core Pieces
31
31
 
32
- - Store: owns `read`, `write`, and `remove` for persisted records.
32
+ - Store: owns `get`, `set`, and `delete` for persisted records. These names match [Keyv](https://github.com/jaredwray/keyv) and `Map`.
33
33
  - Codec: owns payload shape, versioning, and migration logic between stored data and a full committed snapshot.
34
- - Helper: owns restore sequencing, subscription lifecycle, and write scheduling for one sigma-state instance.
34
+ - Pick options: persist selected top-level keys without writing a custom codec.
35
+ - Helper: owns restore sequencing, subscription lifecycle, and write scheduling for one sigma instance.
35
36
 
36
37
  ## Common Tasks -> Recommended APIs
37
38
 
38
- - Restore once through an async store: `restoreState(instance, options)`
39
- - Restore once through a sync store: `restoreStateSync(instance, options)`
40
- - Persist future committed changes only: `persistState(instance, options)`
41
- - Restore first, then persist future changes: `bindPersistence(instance, options)` or `bindPersistenceSync(instance, options)`
42
- - Persist only selected top-level keys while restoring the full state shape: `pickStateCodec(keys)`
39
+ - Restore once through an async store: `restore(instance, options)`
40
+ - Restore once through a sync store: `restoreSync(instance, options)`
41
+ - Persist future committed changes only: `persist(instance, options)`
42
+ - Restore first, then persist future changes: `hydrate(instance, options)` or `hydrateSync(instance, options)`
43
+ - Persist only selected top-level keys while restoring the full state shape: pass `pick: ["key"]`
43
44
 
44
45
  ## Scheduling and Lifecycle
45
46
 
46
47
  - Persistence helpers only read and write committed snapshots. Unpublished drafts never reach storage.
47
- - `persistState(...)` defaults to `"microtask"` scheduling so multiple same-turn publishes can coalesce into one write.
48
+ - `persist(...)` defaults to `"microtask"` scheduling so multiple same-turn publishes can coalesce into one write.
48
49
  - `writeInitial` defaults to `false`, which prevents a new binding from overwriting an older record before restore runs.
49
50
  - `flush()` waits for scheduled or active writes to finish.
50
51
  - `clear()` removes the stored record and keeps the binding usable for later writes.
51
- - `stop()` unsubscribes the binding and waits for any in-flight write to settle.
52
- - `bindPersistence(...)` starts future persistence only after restore resolves successfully.
52
+ - `stop()` unsubscribes the binding, cancels unwritten scheduled state, and waits for any active write to settle.
53
+ - `hydrate(...)` starts future persistence only after restore resolves successfully.
53
54
 
54
55
  ## Constraints
55
56
 
56
- - `sigma.replaceState(...)` still requires a plain object with the exact top-level state-key shape.
57
- - Partial persistence codecs must reconstruct a full replacement snapshot before restore finishes.
57
+ - `sigma.replaceState(...)` requires a plain object replacement snapshot. In supported TypeScript usage, pass the class's full `TState` shape.
58
+ - Custom partial persistence codecs should reconstruct a full replacement snapshot before restore finishes.
58
59
  - Nested sigma-state values are stored only if the chosen codec and payload format support them explicitly.
59
- - Async restore failures reject through `restoreState(...)` or the `restored` promise from `bindPersistence(...)`.
60
+ - Async restore failures reject through `restore(...)` or the `restored` promise from `hydrate(...)`.
60
61
  - Background write failures route through `onWriteError(...)` without automatically stopping persistence.
61
62
 
62
63
  ## Example Routes
63
64
 
64
- - [`examples/persist-search-draft.ts`](../examples/persist-search-draft.ts): sync restore-first persistence with `bindPersistenceSync(...)` and `pickStateCodec(...)`
65
+ - [`examples/persist-search-draft.ts`](../examples/persist-search-draft.ts): sync restore-first persistence with `hydrateSync(...)` and `pick`
65
66
  - [`examples/observe-and-restore.ts`](../examples/observe-and-restore.ts): direct snapshot and restore without the persist subpath
66
67
  - [`dist/persist.d.mts`](../dist/persist.d.mts): exact exported signatures for the persist module
@@ -1,38 +1,43 @@
1
- import { listen, 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
- };
1
+ import { listen, SigmaTarget } from "preact-sigma";
2
+
3
+ type SaveIndicatorState = {
4
+ savedCount: number;
5
+ saving: boolean;
6
+ };
7
+
8
+ type SaveIndicatorEvents = {
9
+ saved: {
10
+ count: number;
11
+ };
12
+ };
13
+
14
+ class SaveIndicator extends SigmaTarget<SaveIndicatorEvents, SaveIndicatorState> {
15
+ constructor() {
16
+ super({
17
+ savedCount: 0,
18
+ saving: false,
19
+ });
12
20
  }
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
- });
21
+
22
+ async save() {
23
+ this.saving = true;
24
+ this.commit(); // Publish before the async boundary.
25
+
26
+ await Promise.resolve();
27
+
28
+ this.savedCount += 1;
29
+ this.saving = false;
30
+ this.commit(); // Publish before emitting the event boundary.
31
+
32
+ this.emit("saved", { count: this.savedCount });
33
+ }
34
+ }
35
+
36
+ interface SaveIndicator extends SaveIndicatorState {}
32
37
 
33
38
  const indicator = new SaveIndicator();
34
39
 
35
- listen(indicator, "saved", ({ count }) => {
40
+ const stop = listen(indicator, "saved", ({ count }) => {
36
41
  console.log(`Saved ${count} times`);
37
42
  });
38
43
 
@@ -40,3 +45,5 @@ await indicator.save();
40
45
 
41
46
  console.log(indicator.saving); // false
42
47
  console.log(indicator.savedCount); // 1
48
+
49
+ stop();
@@ -1,19 +1,24 @@
1
- import { SigmaType } from "preact-sigma";
2
-
3
- const Counter = new SigmaType<{ count: number }>("Counter")
4
- .defaultState({
5
- count: 0,
6
- })
7
- .computed({
8
- doubled() {
9
- return this.count * 2;
10
- },
11
- })
12
- .actions({
13
- increment() {
14
- this.count += 1;
15
- },
16
- });
1
+ import { Sigma } from "preact-sigma";
2
+
3
+ type CounterState = { count: number };
4
+
5
+ class Counter extends Sigma<CounterState> {
6
+ constructor() {
7
+ super({
8
+ count: 0,
9
+ });
10
+ }
11
+
12
+ get doubled() {
13
+ return this.count * 2;
14
+ }
15
+
16
+ increment() {
17
+ this.count += 1;
18
+ }
19
+ }
20
+
21
+ interface Counter extends CounterState {}
17
22
 
18
23
  const counter = new Counter();
19
24