preact-sigma 2.1.3 → 2.2.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.
- package/README.md +39 -12
- package/dist/index.d.mts +10 -2
- package/dist/index.mjs +51 -30
- package/llms.txt +10 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,10 +12,33 @@ To add `preact-sigma` to your project:
|
|
|
12
12
|
npm install preact-sigma
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
## Smallest Useful Example
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
```ts
|
|
18
|
+
import { SigmaType } from "preact-sigma";
|
|
19
|
+
|
|
20
|
+
const Counter = new SigmaType<{ count: number }>("Counter")
|
|
21
|
+
.defaultState({
|
|
22
|
+
count: 0,
|
|
23
|
+
})
|
|
24
|
+
.computed({
|
|
25
|
+
doubled() {
|
|
26
|
+
return this.count * 2;
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
.actions({
|
|
30
|
+
increment() {
|
|
31
|
+
this.count += 1;
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const counter = new Counter();
|
|
36
|
+
|
|
37
|
+
counter.increment();
|
|
38
|
+
|
|
39
|
+
console.log(counter.count); // 1
|
|
40
|
+
console.log(counter.doubled); // 2
|
|
41
|
+
```
|
|
19
42
|
|
|
20
43
|
## What It Is
|
|
21
44
|
|
|
@@ -157,13 +180,17 @@ cleanup();
|
|
|
157
180
|
|
|
158
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.
|
|
159
182
|
|
|
160
|
-
|
|
183
|
+
Cleanup resources can be returned as functions, `AbortController`, objects with `dispose()`, or objects with `Symbol.dispose`.
|
|
161
184
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
-
|
|
167
|
-
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
193
|
+
|
|
194
|
+
- [llms.txt](./llms.txt) provides the exhaustive machine-oriented API guide and terminology.
|
|
195
|
+
- 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.
|
package/dist/index.d.mts
CHANGED
|
@@ -47,6 +47,9 @@ type DefaultStateValue<TValue> = TValue | DefaultStateInitializer<TValue>;
|
|
|
47
47
|
type Disposable = {
|
|
48
48
|
[Symbol.dispose](): void;
|
|
49
49
|
};
|
|
50
|
+
type DisposableLike = {
|
|
51
|
+
dispose(): void;
|
|
52
|
+
};
|
|
50
53
|
interface SigmaRefBrand {
|
|
51
54
|
[sigmaRefBrand]?: true;
|
|
52
55
|
}
|
|
@@ -58,8 +61,8 @@ type AnyEvents = Record<string, object | void>;
|
|
|
58
61
|
type AnyState = Record<string, unknown>;
|
|
59
62
|
/** The object accepted by `.defaultState(...)`. */
|
|
60
63
|
type AnyDefaultState<TState extends AnyState> = { [K in keyof TState]?: DefaultStateValue<TState[K]> };
|
|
61
|
-
/** A cleanup resource supported by `.setup(...)
|
|
62
|
-
type AnyResource = Cleanup | Disposable | AbortController;
|
|
64
|
+
/** A cleanup resource supported by `.setup(...)`, including function, `dispose()`, and `Symbol.dispose` cleanup. */
|
|
65
|
+
type AnyResource = Cleanup | Disposable | DisposableLike | AbortController;
|
|
63
66
|
type ComputedValues<TComputeds extends object | undefined> = [undefined] extends [TComputeds] ? never : { readonly [K in keyof TComputeds]: TComputeds[K] extends AnyFunction ? Immutable<ReturnType<TComputeds[K]>> : never };
|
|
64
67
|
type ComputedContext<TState extends AnyState, TComputeds extends object> = Immutable<TState> & ComputedValues<TComputeds>;
|
|
65
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 };
|
|
@@ -77,6 +80,10 @@ type ActionContext<TState extends AnyState, TEvents extends AnyEvents, TComputed
|
|
|
77
80
|
/** Publishes the current action draft immediately so later boundaries use committed state. */commit(): void;
|
|
78
81
|
emit: Emit<TEvents>;
|
|
79
82
|
};
|
|
83
|
+
type DefinitionEvents<T extends SigmaDefinition> = T["events"] extends AnyEvents ? T["events"] : {};
|
|
84
|
+
type DefinitionComputeds<T extends SigmaDefinition> = T["computeds"] extends object ? T["computeds"] : {};
|
|
85
|
+
type DefinitionQueries<T extends SigmaDefinition> = T["queries"] extends object ? T["queries"] : {};
|
|
86
|
+
type DefinitionActions<T extends SigmaDefinition> = T["actions"] extends object ? T["actions"] : {};
|
|
80
87
|
/** The public shape shared by all sigma-state instances. */
|
|
81
88
|
interface AnySigmaState extends EventTarget {
|
|
82
89
|
readonly [sigmaStateBrand]: true;
|
|
@@ -114,6 +121,7 @@ type MapSigmaDefinition<T extends SigmaDefinition> = keyof T extends infer K ? K
|
|
|
114
121
|
/** The public instance shape produced by a configured sigma type. */
|
|
115
122
|
type SigmaState<T extends SigmaDefinition = SigmaDefinition> = AnySigmaState & Simplify<UnionToIntersection<MapSigmaDefinition<T>>>;
|
|
116
123
|
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;
|
|
117
125
|
emit: T["events"] extends object ? Emit<T["events"]> : never;
|
|
118
126
|
};
|
|
119
127
|
type MergeObjects<TLeft extends object, TRight> = [TRight] extends [object] ? Extract<Simplify<Omit<TLeft, keyof TRight> & TRight>, TLeft> : TLeft;
|
package/dist/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { freeze, immerable } from "immer";
|
|
|
4
4
|
import { useEffect, useRef, useState } from "preact/hooks";
|
|
5
5
|
//#region src/internal/context.ts
|
|
6
6
|
const disabledContextOptions = {
|
|
7
|
+
allowAct: false,
|
|
7
8
|
allowActions: false,
|
|
8
9
|
allowCommit: false,
|
|
9
10
|
allowEmit: false,
|
|
@@ -30,6 +31,7 @@ const publicContextOptions = {
|
|
|
30
31
|
},
|
|
31
32
|
setup: {
|
|
32
33
|
...disabledContextOptions,
|
|
34
|
+
allowAct: true,
|
|
33
35
|
allowActions: true,
|
|
34
36
|
allowEmit: true,
|
|
35
37
|
allowQueries: true
|
|
@@ -87,8 +89,12 @@ function registerContextOwner(context, owner) {
|
|
|
87
89
|
function createContext(instance, options, owner) {
|
|
88
90
|
const publicPrototype = Object.getPrototypeOf(instance.publicInstance);
|
|
89
91
|
return new Proxy(publicPrototype, {
|
|
90
|
-
get(_target, key) {
|
|
92
|
+
get(_target, key, receiver) {
|
|
91
93
|
if (typeof key !== "string") return Reflect.get(publicPrototype, key, owner?.actionContext ?? instance.publicInstance);
|
|
94
|
+
if (key === "act") return options.allowAct ? (actionFn) => {
|
|
95
|
+
if (typeof actionFn !== "function") throw new Error("[preact-sigma] act() requires a function");
|
|
96
|
+
return runAdHocAction(receiver, actionFn);
|
|
97
|
+
} : void 0;
|
|
92
98
|
if (key === "commit") return options.allowCommit && owner ? () => commitActionOwner(owner) : void 0;
|
|
93
99
|
if (key === "emit") return options.allowEmit && owner ? (name, detail) => {
|
|
94
100
|
handleActionBoundary(owner, "emit");
|
|
@@ -168,6 +174,7 @@ function getPublicContext(instance, kind) {
|
|
|
168
174
|
//#region src/internal/symbols.ts
|
|
169
175
|
const sigmaStateBrand = Symbol("sigma.v2.state");
|
|
170
176
|
const reservedKeys = new Set([
|
|
177
|
+
"act",
|
|
171
178
|
"get",
|
|
172
179
|
"emit",
|
|
173
180
|
"commit",
|
|
@@ -237,38 +244,13 @@ function buildQueryMethod(queryFunction) {
|
|
|
237
244
|
};
|
|
238
245
|
}
|
|
239
246
|
function buildActionMethod(actionName, actionFn) {
|
|
240
|
-
const actionIsAsync = isAsyncFunction(actionFn);
|
|
241
247
|
return function(...args) {
|
|
242
|
-
|
|
243
|
-
if (instance.disposed) throw new Error("[preact-sigma] Cannot run an action on a disposed sigma state");
|
|
244
|
-
return untracked$1(() => {
|
|
245
|
-
let owner;
|
|
246
|
-
const callerOwner = getContextOwner(this);
|
|
247
|
-
if (callerOwner && callerOwner.instance === instance && !actionIsAsync) owner = callerOwner;
|
|
248
|
-
else {
|
|
249
|
-
handleActionBoundary(callerOwner, "action", actionName);
|
|
250
|
-
owner = createActionOwner(instance, actionName, actionFn, args);
|
|
251
|
-
}
|
|
252
|
-
let result;
|
|
253
|
-
try {
|
|
254
|
-
result = actionFn.apply(owner.actionContext, [...args]);
|
|
255
|
-
} catch (error) {
|
|
256
|
-
clearCurrentDraft(owner);
|
|
257
|
-
throw error;
|
|
258
|
-
}
|
|
259
|
-
if (!actionIsAsync && isPromiseLike(result)) {
|
|
260
|
-
clearCurrentDraft(owner);
|
|
261
|
-
Promise.resolve(result).catch(() => {});
|
|
262
|
-
throw new Error(`[preact-sigma] Action "${actionName}" must use native async-await syntax to return a promise.`);
|
|
263
|
-
}
|
|
264
|
-
if (owner === callerOwner) return result;
|
|
265
|
-
const finalized = finalizeOwnerDraft(owner);
|
|
266
|
-
if (finalized?.changed) publishState(instance, finalized);
|
|
267
|
-
if (isPromiseLike(result)) return resolveAsyncActionResult(owner, result);
|
|
268
|
-
return result;
|
|
269
|
-
});
|
|
248
|
+
return runActionInvocation(this, actionName, actionFn, args);
|
|
270
249
|
};
|
|
271
250
|
}
|
|
251
|
+
function runAdHocAction(context, actionFn) {
|
|
252
|
+
return runActionInvocation(context, "act()", actionFn, []);
|
|
253
|
+
}
|
|
272
254
|
function readActionStateValue(owner, key, options) {
|
|
273
255
|
if (owner.currentDraft) return owner.currentDraft[key];
|
|
274
256
|
const signal = getSignal(owner.instance, key);
|
|
@@ -323,6 +305,44 @@ function createActionOwner(instance, actionName, actionFn, args) {
|
|
|
323
305
|
registerContextOwner(owner.actionContext, owner);
|
|
324
306
|
return owner;
|
|
325
307
|
}
|
|
308
|
+
function runActionInvocation(context, actionName, actionFn, args) {
|
|
309
|
+
const instance = getSigmaInternals(context);
|
|
310
|
+
if (instance.disposed) throw new Error("[preact-sigma] Cannot run an action on a disposed sigma state");
|
|
311
|
+
const isAdHocAction = actionName === "act()";
|
|
312
|
+
const actionIsAsync = isAsyncFunction(actionFn);
|
|
313
|
+
if (actionIsAsync && isAdHocAction) throw new Error("[preact-sigma] act() callbacks must stay synchronous");
|
|
314
|
+
return untracked$1(() => {
|
|
315
|
+
let owner;
|
|
316
|
+
const callerOwner = getContextOwner(context);
|
|
317
|
+
if (callerOwner && callerOwner.instance === instance && !actionIsAsync) owner = callerOwner;
|
|
318
|
+
else {
|
|
319
|
+
handleActionBoundary(callerOwner, "action", actionName);
|
|
320
|
+
owner = createActionOwner(instance, actionName, actionFn, args);
|
|
321
|
+
}
|
|
322
|
+
let result;
|
|
323
|
+
try {
|
|
324
|
+
result = actionFn.apply(owner.actionContext, args);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
clearCurrentDraft(owner);
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
if (isAdHocAction && isPromiseLike(result)) {
|
|
330
|
+
clearCurrentDraft(owner);
|
|
331
|
+
Promise.resolve(result).catch(() => {});
|
|
332
|
+
throw new Error("[preact-sigma] act() callbacks must stay synchronous");
|
|
333
|
+
}
|
|
334
|
+
if (!actionIsAsync && isPromiseLike(result)) {
|
|
335
|
+
clearCurrentDraft(owner);
|
|
336
|
+
Promise.resolve(result).catch(() => {});
|
|
337
|
+
throw new Error(`[preact-sigma] Action "${actionName}" must use native async-await syntax to return a promise.`);
|
|
338
|
+
}
|
|
339
|
+
if (owner === callerOwner) return result;
|
|
340
|
+
const finalized = finalizeOwnerDraft(owner);
|
|
341
|
+
if (finalized?.changed) publishState(instance, finalized);
|
|
342
|
+
if (isPromiseLike(result)) return resolveAsyncActionResult(owner, result);
|
|
343
|
+
return result;
|
|
344
|
+
});
|
|
345
|
+
}
|
|
326
346
|
function createCommitError(owner) {
|
|
327
347
|
return /* @__PURE__ */ new Error(`[preact-sigma] Async action "${owner.actionName}" finished with unpublished changes. Call this.commit() before await or return.`);
|
|
328
348
|
}
|
|
@@ -339,6 +359,7 @@ function createDraftMetadata(owner) {
|
|
|
339
359
|
function disposeCleanupResource(resource) {
|
|
340
360
|
if (typeof resource === "function") resource();
|
|
341
361
|
else if (resource instanceof AbortController) resource.abort();
|
|
362
|
+
else if ("dispose" in resource) resource.dispose();
|
|
342
363
|
else resource[Symbol.dispose]();
|
|
343
364
|
}
|
|
344
365
|
function assertExactStateKeys(instance, nextState) {
|
package/llms.txt
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
- `computed`: A tracked getter declared with `.computed({ ... })`.
|
|
9
9
|
- `query`: A tracked method declared with `.queries({ ... })` or created with `query(fn)`.
|
|
10
10
|
- `action`: A method declared with `.actions({ ... })` that reads and writes through one Immer draft for one synchronous call.
|
|
11
|
+
- `anonymous action`: A synchronous function passed to `this.act(...)` inside setup to run one anonymous action.
|
|
11
12
|
- `draft boundary`: Any point where sigma cannot keep reusing the current draft.
|
|
12
13
|
- `setup handler`: A function declared with `.setup(fn)` that returns an array of cleanup resources.
|
|
13
|
-
- `cleanup resource`: A cleanup function, an `AbortController`, or an object with `[Symbol.dispose]()`.
|
|
14
|
+
- `cleanup resource`: A cleanup function, an `AbortController`, an object with `dispose()`, or an object with `[Symbol.dispose]()`.
|
|
14
15
|
- `signal access`: Reading the underlying `ReadonlySignal` for a state property or computed through `instance.get(key)`.
|
|
15
16
|
|
|
16
17
|
## Navigation
|
|
@@ -75,7 +76,7 @@ import {
|
|
|
75
76
|
|
|
76
77
|
- `AnyDefaultState`: Describes the object accepted by `.defaultState(...)`.
|
|
77
78
|
- `AnyEvents`: Describes an event map from event names to payload objects or `void`.
|
|
78
|
-
- `AnyResource`: Describes a supported setup cleanup resource.
|
|
79
|
+
- `AnyResource`: Describes a supported setup cleanup resource, including cleanup functions, `AbortController`, objects with `dispose()`, and objects with `Symbol.dispose`.
|
|
79
80
|
- `AnySigmaState`: Describes the public shape shared by all sigma-state instances.
|
|
80
81
|
- `AnySigmaStateWithEvents`: Describes a sigma-state instance with a typed event map.
|
|
81
82
|
- `AnyState`: Describes the top-level state object for a sigma type.
|
|
@@ -169,7 +170,7 @@ Behavior:
|
|
|
169
170
|
- Constructor input shallowly overrides `defaultState`.
|
|
170
171
|
- If every required state property is covered by `defaultState`, constructor input is optional.
|
|
171
172
|
- Duplicate names across state properties, computeds, queries, and actions are rejected at runtime.
|
|
172
|
-
- Reserved public names are `get`, `setup`, `on`, and `emit`.
|
|
173
|
+
- Reserved public names are `act`, `get`, `setup`, `on`, and `emit`.
|
|
173
174
|
|
|
174
175
|
## Public Instance Shape
|
|
175
176
|
|
|
@@ -289,13 +290,18 @@ Behavior:
|
|
|
289
290
|
- calling `.setup(...)` again cleans up the previous setup first
|
|
290
291
|
- one `.setup(...)` call runs every registered setup handler in definition order
|
|
291
292
|
- the public `.setup(...)` method always returns one cleanup function
|
|
292
|
-
- `this` inside a setup handler exposes the public instance plus `emit(...)`
|
|
293
|
+
- `this` inside a setup handler exposes the public instance plus `emit(...)` and `act(fn)`
|
|
294
|
+
- `this.act(fn)` inside setup runs `fn` with normal action semantics without adding a public action method
|
|
295
|
+
- use `this.act(fn)` for setup-time initialization work or setup-owned callbacks that need action semantics
|
|
296
|
+
- pass a normal `function () {}` to `this.act(...)` so callback `this` receives the action context
|
|
297
|
+
- `this.act(fn)` functions must stay synchronous
|
|
293
298
|
- each setup handler returns an array of cleanup resources
|
|
294
299
|
- setup typing only exposes computeds, queries, and actions that were already present when that `.setup(...)` call happened
|
|
295
300
|
|
|
296
301
|
Supported cleanup resources:
|
|
297
302
|
|
|
298
303
|
- cleanup functions
|
|
304
|
+
- objects with `dispose()`
|
|
299
305
|
- objects with `[Symbol.dispose]()`
|
|
300
306
|
- `AbortController`
|
|
301
307
|
|