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.
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
 
@@ -1,6 +1,6 @@
1
1
  import { useState } from "preact/hooks";
2
2
 
3
- import { listen, query, SigmaType, useListener, useSigma } from "preact-sigma";
3
+ import { listen, query, Sigma, SigmaTarget, useListener, useSigma } from "preact-sigma";
4
4
 
5
5
  type Command = {
6
6
  id: string;
@@ -9,18 +9,18 @@ type Command = {
9
9
  };
10
10
 
11
11
  class UsageLedger {
12
- counts = new Map<string, number>();
12
+ #counts = new Map<string, number>();
13
13
 
14
14
  get(id: string) {
15
- return this.counts.get(id) ?? 0;
15
+ return this.#counts.get(id) ?? 0;
16
16
  }
17
17
 
18
18
  increment(id: string) {
19
- this.counts.set(id, this.get(id) + 1);
19
+ this.#counts.set(id, this.get(id) + 1);
20
20
  }
21
21
  }
22
22
 
23
- const matchesText = query((command: Command, draft: string) => {
23
+ function matchesText(command: Command, draft: string) {
24
24
  const needle = draft.trim().toLowerCase();
25
25
  if (!needle) {
26
26
  return true;
@@ -30,101 +30,115 @@ const matchesText = query((command: Command, draft: string) => {
30
30
  command.title.toLowerCase().includes(needle) ||
31
31
  command.keywords.some((keyword) => keyword.toLowerCase().includes(needle))
32
32
  );
33
- });
33
+ }
34
34
 
35
- const SearchHistory = new SigmaType<{
35
+ type SearchHistoryState = {
36
36
  items: string[];
37
- }>("SearchHistory")
38
- .defaultState({
39
- items: [],
40
- })
41
- .actions({
42
- remember(query: string) {
43
- const value = query.trim();
44
- if (!value) {
45
- return;
46
- }
47
-
48
- this.items = [value, ...this.items.filter((item) => item !== value)].slice(0, 5);
49
- },
50
- });
37
+ };
51
38
 
52
- interface SearchHistory extends InstanceType<typeof SearchHistory> {}
53
-
54
- const CommandPalette = new SigmaType<
55
- {
56
- commands: Command[];
57
- cursor: number;
58
- draft: string;
59
- history: SearchHistory;
60
- usage: UsageLedger;
61
- },
62
- {
63
- ran: Command;
39
+ class SearchHistory extends Sigma<SearchHistoryState> {
40
+ constructor() {
41
+ super({
42
+ items: [],
43
+ });
64
44
  }
65
- >("CommandPalette")
66
- .defaultState({
67
- commands: [
68
- { id: "inbox", title: "Open inbox", keywords: ["mail", "messages", "triage"] },
69
- { id: "capture", title: "Capture note", keywords: ["write", "quick", "idea"] },
70
- { id: "focus", title: "Start focus timer", keywords: ["pomodoro", "deep work"] },
71
- { id: "theme", title: "Toggle theme", keywords: ["appearance", "dark", "light"] },
72
- ],
73
- cursor: 0,
74
- draft: "",
75
- history: () => new SearchHistory(),
76
- usage: () => new UsageLedger(),
77
- })
78
- .computed({
79
- visibleCommands() {
80
- return this.commands.filter((command) => matchesText(command, this.draft));
81
- },
82
- activeCommand() {
83
- return this.visibleCommands[this.cursor] ?? null;
84
- },
85
- })
86
- .queries({
87
- canRun() {
88
- return this.activeCommand !== null;
89
- },
90
- usageCount(id: string) {
91
- return this.usage.get(id);
92
- },
93
- })
94
- .actions({
95
- setDraft(draft: string) {
96
- this.draft = draft;
97
- this.cursor = 0;
98
- },
99
- move(step: number) {
100
- if (this.visibleCommands.length === 0) {
101
- this.cursor = 0;
102
- return;
103
- }
104
-
105
- const lastIndex = this.visibleCommands.length - 1;
106
- this.cursor = Math.max(0, Math.min(lastIndex, this.cursor + step));
107
- },
108
- seedDraftFromHistory() {
109
- const latest = this.history.items[0];
110
- if (latest) {
111
- this.setDraft(latest);
112
- }
113
- },
114
- runActive() {
115
- const command = this.activeCommand;
116
- if (!command || !this.canRun()) {
117
- return;
118
- }
119
-
120
- this.history.remember(this.draft || command.title);
121
- this.usage.increment(command.id);
122
- this.emit("ran", command);
123
- this.draft = "";
45
+
46
+ remember(query: string) {
47
+ const value = query.trim();
48
+ if (!value) {
49
+ return;
50
+ }
51
+
52
+ this.items = [value, ...this.items.filter((item) => item !== value)].slice(0, 5);
53
+ }
54
+ }
55
+
56
+ interface SearchHistory extends SearchHistoryState {}
57
+
58
+ type CommandPaletteState = {
59
+ commands: Command[];
60
+ cursor: number;
61
+ draft: string;
62
+ history: SearchHistory;
63
+ usage: UsageLedger;
64
+ };
65
+
66
+ type CommandPaletteEvents = {
67
+ ran: Command;
68
+ };
69
+
70
+ class CommandPalette extends SigmaTarget<CommandPaletteEvents, CommandPaletteState> {
71
+ constructor() {
72
+ super({
73
+ commands: [
74
+ { id: "inbox", title: "Open inbox", keywords: ["mail", "messages", "triage"] },
75
+ { id: "capture", title: "Capture note", keywords: ["write", "quick", "idea"] },
76
+ { id: "focus", title: "Start focus timer", keywords: ["pomodoro", "deep work"] },
77
+ { id: "theme", title: "Toggle theme", keywords: ["appearance", "dark", "light"] },
78
+ ],
79
+ cursor: 0,
80
+ draft: "",
81
+ history: new SearchHistory(),
82
+ usage: new UsageLedger(),
83
+ });
84
+ }
85
+
86
+ get visibleCommands() {
87
+ return this.commands.filter((command) => matchesText(command, this.draft));
88
+ }
89
+
90
+ get activeCommand() {
91
+ return this.visibleCommands[this.cursor] ?? null;
92
+ }
93
+
94
+ get canRun() {
95
+ return this.activeCommand !== null;
96
+ }
97
+
98
+ @query
99
+ usageCount(id: string) {
100
+ return this.usage.get(id);
101
+ }
102
+
103
+ setDraft(draft: string) {
104
+ this.draft = draft;
105
+ this.cursor = 0;
106
+ }
107
+
108
+ move(step: number) {
109
+ if (this.visibleCommands.length === 0) {
124
110
  this.cursor = 0;
125
- },
126
- })
127
- .setup(function () {
111
+ return;
112
+ }
113
+
114
+ const lastIndex = this.visibleCommands.length - 1;
115
+ this.cursor = Math.max(0, Math.min(lastIndex, this.cursor + step));
116
+ }
117
+
118
+ seedDraftFromHistory() {
119
+ const latest = this.history.items[0];
120
+ if (latest) {
121
+ this.setDraft(latest);
122
+ }
123
+ }
124
+
125
+ runActive() {
126
+ const command = this.activeCommand;
127
+ if (!command) {
128
+ return;
129
+ }
130
+
131
+ const search = this.draft || command.title;
132
+ this.usage.increment(command.id);
133
+ this.draft = "";
134
+ this.cursor = 0;
135
+ this.commit();
136
+
137
+ this.history.remember(search);
138
+ this.emit("ran", command);
139
+ }
140
+
141
+ onSetup() {
128
142
  return [
129
143
  listen(window, "keydown", (event) => {
130
144
  if ((event.metaKey || event.ctrlKey) && event.key === "k") {
@@ -144,9 +158,10 @@ const CommandPalette = new SigmaType<
144
158
  }
145
159
  }),
146
160
  ];
147
- });
161
+ }
162
+ }
148
163
 
149
- interface CommandPalette extends InstanceType<typeof CommandPalette> {}
164
+ interface CommandPalette extends CommandPaletteState {}
150
165
 
151
166
  export function CommandPaletteExample() {
152
167
  const palette = useSigma(() => new CommandPalette());
@@ -158,16 +173,11 @@ export function CommandPaletteExample() {
158
173
 
159
174
  return (
160
175
  <section>
161
- <p>
162
- <strong>Command palette</strong>: setup-owned keyboard shortcuts, computed getters, tracked
163
- queries with args, typed events, nested sigma state, and a mutable custom class instance.
164
- </p>
165
-
166
176
  <label>
167
177
  Search
168
178
  <input
169
179
  value={palette.draft}
170
- onInput={(event) => palette.setDraft((event.currentTarget as HTMLInputElement).value)}
180
+ onInput={(event) => palette.setDraft(event.currentTarget.value)}
171
181
  placeholder="Try: note, timer, inbox"
172
182
  />
173
183
  </label>
@@ -179,7 +189,7 @@ export function CommandPaletteExample() {
179
189
  <button type="button" onClick={() => palette.move(1)}>
180
190
  Down
181
191
  </button>
182
- <button type="button" onClick={() => palette.runActive()} disabled={!palette.canRun()}>
192
+ <button type="button" onClick={() => palette.runActive()} disabled={!palette.canRun}>
183
193
  Run
184
194
  </button>
185
195
  </div>
@@ -1,29 +1,35 @@
1
- import { sigma, SigmaType } from "preact-sigma";
1
+ import { sigma, Sigma } from "preact-sigma";
2
2
 
3
- const TodoList = new SigmaType<{
3
+ type TodoListState = {
4
4
  todos: string[];
5
- }>("TodoList")
6
- .defaultState({
7
- todos: [],
8
- })
9
- .actions({
10
- add(title: string) {
11
- this.todos.push(title);
12
- },
13
- });
5
+ };
6
+
7
+ class TodoList extends Sigma<TodoListState> {
8
+ constructor() {
9
+ super({
10
+ todos: [],
11
+ });
12
+ }
13
+
14
+ add(title: string) {
15
+ this.todos.push(title);
16
+ }
17
+ }
18
+
19
+ interface TodoList extends TodoListState {}
14
20
 
15
21
  const todoList = new TodoList();
16
- const stop = sigma.subscribe(todoList, (change) => {
17
- console.log(`${change.oldState.todos.length} -> ${change.newState.todos.length}`);
22
+ const stop = sigma.subscribe(todoList, (nextState, baseState) => {
23
+ console.log(`${baseState.todos.length} -> ${nextState.todos.length}`);
18
24
  });
19
25
 
20
26
  todoList.add("Write docs");
21
27
 
22
- const saved = sigma.getState(todoList);
28
+ const saved = sigma.captureState(todoList);
23
29
 
24
30
  todoList.add("Ship release");
25
31
  sigma.replaceState(todoList, saved);
26
32
 
27
- console.log(sigma.getState(todoList).todos); // ["Write docs"]
33
+ console.log(sigma.captureState(todoList).todos); // ["Write docs"]
28
34
 
29
35
  stop();
@@ -1,29 +1,32 @@
1
- import { SigmaType } from "preact-sigma";
2
- import {
3
- bindPersistenceSync,
4
- pickStateCodec,
5
- type PersistRecord,
6
- type SyncPersistStore,
7
- } from "preact-sigma/persist";
1
+ import { Sigma } from "preact-sigma";
2
+ import { hydrateSync, type PersistRecord, type SyncPersistStore } from "preact-sigma/persist";
8
3
 
9
- const Search = new SigmaType<{
4
+ type SearchState = {
10
5
  draft: string;
11
6
  page: number;
12
- }>("Search")
13
- .defaultState({
14
- draft: "",
15
- page: 1,
16
- })
17
- .actions({
18
- nextPage() {
19
- this.page += 1;
20
- },
21
- setDraft(draft: string) {
22
- this.draft = draft;
23
- },
24
- });
7
+ };
8
+
9
+ class Search extends Sigma<SearchState> {
10
+ constructor(initialState: Partial<SearchState> = {}) {
11
+ super({
12
+ draft: "",
13
+ page: 1,
14
+ ...initialState,
15
+ });
16
+ }
17
+
18
+ nextPage() {
19
+ this.page += 1;
20
+ }
21
+
22
+ setDraft(draft: string) {
23
+ this.draft = draft;
24
+ }
25
+ }
26
+
27
+ interface Search extends SearchState {}
25
28
 
26
- const records = new Map<string, PersistRecord<{ draft: string }>>([
29
+ const records = new Map<string, PersistRecord<Pick<SearchState, "draft">>>([
27
30
  [
28
31
  "search",
29
32
  {
@@ -34,22 +37,22 @@ const records = new Map<string, PersistRecord<{ draft: string }>>([
34
37
  ],
35
38
  ]);
36
39
 
37
- const store: SyncPersistStore<PersistRecord<{ draft: string }>> = {
38
- read(key) {
40
+ const store: SyncPersistStore<Pick<SearchState, "draft">> = {
41
+ get(key) {
39
42
  return records.get(key);
40
43
  },
41
- write(key, record) {
42
- records.set(key, record);
44
+ set(key, record) {
45
+ return records.set(key, record);
43
46
  },
44
- remove(key) {
45
- records.delete(key);
47
+ delete(key) {
48
+ return records.delete(key);
46
49
  },
47
50
  };
48
51
 
49
52
  const search = new Search({ page: 3 });
50
- const persistence = bindPersistenceSync(search, {
51
- codec: pickStateCodec(["draft"]),
53
+ const persistence = hydrateSync(search, {
52
54
  key: "search",
55
+ pick: ["draft"],
53
56
  store,
54
57
  });
55
58
 
@@ -1,14 +1,19 @@
1
- import { listen, SigmaType } from "preact-sigma";
1
+ import { listen, Sigma } from "preact-sigma";
2
2
 
3
- const ClickTracker = new SigmaType<{
3
+ type ClickTrackerState = {
4
4
  clicks: number;
5
5
  status: "idle" | "ready";
6
- }>("ClickTracker")
7
- .defaultState({
8
- clicks: 0,
9
- status: "idle",
10
- })
11
- .setup(function (target: EventTarget) {
6
+ };
7
+
8
+ class ClickTracker extends Sigma<ClickTrackerState> {
9
+ constructor() {
10
+ super({
11
+ clicks: 0,
12
+ status: "idle",
13
+ });
14
+ }
15
+
16
+ onSetup(target: EventTarget) {
12
17
  this.act(function () {
13
18
  this.status = "ready";
14
19
  });
@@ -20,7 +25,10 @@ const ClickTracker = new SigmaType<{
20
25
  });
21
26
  }),
22
27
  ];
23
- });
28
+ }
29
+ }
30
+
31
+ interface ClickTracker extends ClickTrackerState {}
24
32
 
25
33
  const target = new EventTarget();
26
34
  const tracker = new ClickTracker();