preact-sigma 3.1.0 → 5.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.
@@ -0,0 +1,595 @@
1
+ import { batch, computed, signal, untracked } from "@preact/signals";
2
+ import * as immer from "immer";
3
+ //#region src/internal/symbols.ts
4
+ const sigmaStateBrand = Symbol("sigma.state");
5
+ const sigmaTargetBrand = Symbol("sigma.target");
6
+ const reservedKeys = new Set([
7
+ "act",
8
+ "emit",
9
+ "commit",
10
+ "setup"
11
+ ]);
12
+ //#endregion
13
+ //#region src/listener.ts
14
+ /** Listener registry used by sigma targets and sigma states for typed event delivery. */
15
+ var SigmaListenerMap = class extends Map {
16
+ /** Delivers one event payload to the current listeners for `name`. */
17
+ emit(name, detail) {
18
+ const listeners = this.get(name);
19
+ if (!listeners?.size) return;
20
+ for (const listener of [...listeners]) listener(detail);
21
+ }
22
+ /** Adds one listener for `name`, creating the listener set on first use. */
23
+ addListener(name, listener) {
24
+ let listeners = this.get(name);
25
+ if (!listeners) {
26
+ listeners = /* @__PURE__ */ new Set();
27
+ this.set(name, listeners);
28
+ }
29
+ listeners.add(listener);
30
+ }
31
+ /** Removes one listener for `name` and prunes the empty listener set. */
32
+ removeListener(name, listener) {
33
+ const listeners = this.get(name);
34
+ if (!listeners) return;
35
+ listeners.delete(listener);
36
+ if (!listeners.size) this.delete(name);
37
+ }
38
+ };
39
+ /**
40
+ * A standalone typed event hub with `emit(...)` and `on(...)` methods.
41
+ *
42
+ * `SigmaTarget` also works with `listen(...)` and `useListener(...)`.
43
+ */
44
+ var SigmaTarget = class {
45
+ [sigmaTargetBrand] = new SigmaListenerMap();
46
+ /**
47
+ * Emits a typed event from the hub.
48
+ *
49
+ * Void events notify listeners with `undefined`. Payload events pass their
50
+ * payload directly to listeners.
51
+ */
52
+ emit(name, ...[detail]) {
53
+ this[sigmaTargetBrand].emit(name, detail);
54
+ }
55
+ /**
56
+ * Registers a typed event listener and returns an unsubscribe function.
57
+ *
58
+ * Payload events pass their payload directly to the listener. Void events
59
+ * call the listener with no arguments.
60
+ */
61
+ on(name, listener) {
62
+ this[sigmaTargetBrand].addListener(name, listener);
63
+ return () => {
64
+ this[sigmaTargetBrand].removeListener(name, listener);
65
+ };
66
+ }
67
+ };
68
+ function listen(target, name, listener) {
69
+ if (Object.hasOwn(target, sigmaTargetBrand)) {
70
+ const sigmaTarget = target;
71
+ sigmaTarget[sigmaTargetBrand].addListener(name, listener);
72
+ return () => {
73
+ sigmaTarget[sigmaTargetBrand].removeListener(name, listener);
74
+ };
75
+ }
76
+ const eventTarget = target;
77
+ const eventListener = listener;
78
+ eventTarget.addEventListener(name, eventListener);
79
+ return () => {
80
+ eventTarget.removeEventListener(name, eventListener);
81
+ };
82
+ }
83
+ //#endregion
84
+ //#region src/internal/context.ts
85
+ const disabledContextOptions = {
86
+ allowAct: false,
87
+ allowActions: false,
88
+ allowCommit: false,
89
+ allowEmit: false,
90
+ allowQueries: false,
91
+ allowWrites: false,
92
+ draftAware: false,
93
+ draftOnRead: false,
94
+ liveComputeds: false,
95
+ reactiveReads: false
96
+ };
97
+ const publicContextOptions = {
98
+ computedReadonly: {
99
+ ...disabledContextOptions,
100
+ reactiveReads: true
101
+ },
102
+ observe: {
103
+ ...disabledContextOptions,
104
+ allowQueries: true
105
+ },
106
+ queryCommitted: {
107
+ ...disabledContextOptions,
108
+ allowQueries: true,
109
+ reactiveReads: true
110
+ },
111
+ setup: {
112
+ ...disabledContextOptions,
113
+ allowAct: true,
114
+ allowActions: true,
115
+ allowEmit: true,
116
+ allowQueries: true
117
+ }
118
+ };
119
+ const ownerContextOptions = {
120
+ action: {
121
+ ...disabledContextOptions,
122
+ allowActions: true,
123
+ allowCommit: true,
124
+ allowEmit: true,
125
+ allowQueries: true,
126
+ allowWrites: true,
127
+ draftAware: true,
128
+ draftOnRead: true,
129
+ liveComputeds: true
130
+ },
131
+ computedDraftAware: {
132
+ ...disabledContextOptions,
133
+ draftAware: true,
134
+ liveComputeds: true
135
+ },
136
+ queryDraftAware: {
137
+ ...disabledContextOptions,
138
+ allowQueries: true,
139
+ draftAware: true,
140
+ liveComputeds: true
141
+ }
142
+ };
143
+ const dirtyContexts = {
144
+ computedReadonly: /* @__PURE__ */ new Set(),
145
+ observe: /* @__PURE__ */ new Set(),
146
+ queryCommitted: /* @__PURE__ */ new Set(),
147
+ setup: /* @__PURE__ */ new Set()
148
+ };
149
+ const contextKinds = Object.keys(dirtyContexts);
150
+ const contextCache = {
151
+ computedReadonly: /* @__PURE__ */ new WeakMap(),
152
+ observe: /* @__PURE__ */ new WeakMap(),
153
+ queryCommitted: /* @__PURE__ */ new WeakMap(),
154
+ setup: /* @__PURE__ */ new WeakMap()
155
+ };
156
+ const contextOwnerMap = /* @__PURE__ */ new WeakMap();
157
+ let contextCacheFlushScheduled = false;
158
+ function getContext(target, kind) {
159
+ if (isOwnerContextKind(kind)) return getOwnerContext(target, kind);
160
+ return getPublicContext(target, kind);
161
+ }
162
+ function getContextOwner(context) {
163
+ return contextOwnerMap.get(context);
164
+ }
165
+ function registerContextOwner(context, owner) {
166
+ contextOwnerMap.set(context, owner);
167
+ }
168
+ function createContext(instance, options, owner) {
169
+ const publicPrototype = Object.getPrototypeOf(instance.publicInstance);
170
+ return new Proxy(publicPrototype, {
171
+ get(_target, key, receiver) {
172
+ if (typeof key !== "string") return Reflect.get(publicPrototype, key, owner?.actionContext ?? instance.publicInstance);
173
+ if (key === "act") return options.allowAct ? (actionFn) => {
174
+ if (typeof actionFn !== "function") throw new Error("[preact-sigma] act() requires a function");
175
+ return runAdHocAction(receiver, actionFn);
176
+ } : void 0;
177
+ if (key === "commit") return options.allowCommit && owner ? () => commitActionOwner(owner) : void 0;
178
+ if (key === "emit") return options.allowEmit && owner ? (name, detail) => {
179
+ handleActionBoundary(owner, "emit");
180
+ instance.publicInstance[sigmaTargetBrand].emit(name, detail);
181
+ } : void 0;
182
+ if (instance.stateKeys.has(key)) {
183
+ if (owner && options.draftAware) return readActionStateValue(owner, key, options);
184
+ const signal = getSignal(instance, key);
185
+ return options.reactiveReads ? signal.value : signal.peek();
186
+ }
187
+ if (key in instance.type._computeFunctions) {
188
+ if (owner && options.liveComputeds) return readActionComputedValue(owner, key);
189
+ const signal = getSignal(instance, key);
190
+ return options.reactiveReads ? signal.value : signal.peek();
191
+ }
192
+ if (options.allowQueries && key in instance.type._queryFunctions) return Reflect.get(instance.publicInstance, key);
193
+ if (options.allowActions && key in instance.type._actionFunctions) return Reflect.get(instance.publicInstance, key);
194
+ if (Reflect.has(publicPrototype, key)) return Reflect.get(publicPrototype, key, owner?.actionContext ?? instance.publicInstance);
195
+ },
196
+ set(_target, key, value) {
197
+ if (!owner || !options.allowWrites || typeof key !== "string" || !instance.stateKeys.has(key)) return false;
198
+ setActionStateValue(owner, key, value);
199
+ return true;
200
+ },
201
+ apply: unsupportedOperation,
202
+ construct: unsupportedOperation,
203
+ defineProperty: unsupportedOperation,
204
+ deleteProperty: unsupportedOperation,
205
+ getOwnPropertyDescriptor: unsupportedOperation,
206
+ has: unsupportedOperation,
207
+ isExtensible: unsupportedOperation,
208
+ ownKeys: unsupportedOperation,
209
+ preventExtensions: unsupportedOperation,
210
+ setPrototypeOf: unsupportedOperation
211
+ });
212
+ }
213
+ function unsupportedOperation() {
214
+ throw new Error("[preact-sigma] This operation is not supported by context proxies");
215
+ }
216
+ const kindToOwnerContextKey = {
217
+ action: "actionContext",
218
+ computedDraftAware: "computedContext",
219
+ queryDraftAware: "queryContext"
220
+ };
221
+ function isOwnerContextKind(kind) {
222
+ return kind in kindToOwnerContextKey;
223
+ }
224
+ function getOwnerContext(owner, kind) {
225
+ const contextKey = kindToOwnerContextKey[kind];
226
+ if (owner[contextKey]) return owner[contextKey];
227
+ const context = createContext(owner.instance, ownerContextOptions[kind], owner);
228
+ registerSigmaInternals(context, owner.instance);
229
+ registerContextOwner(context, owner);
230
+ owner[contextKey] = context;
231
+ return context;
232
+ }
233
+ function getPublicContext(instance, kind) {
234
+ const cachedContext = contextCache[kind].get(instance);
235
+ if (cachedContext) return cachedContext;
236
+ const context = createContext(instance, publicContextOptions[kind], void 0);
237
+ registerSigmaInternals(context, instance);
238
+ contextCache[kind].set(instance, context);
239
+ dirtyContexts[kind].add(instance);
240
+ if (!contextCacheFlushScheduled) {
241
+ contextCacheFlushScheduled = true;
242
+ setTimeout(() => {
243
+ for (const queuedKind of contextKinds) {
244
+ for (const queuedInstance of dirtyContexts[queuedKind]) contextCache[queuedKind].delete(queuedInstance);
245
+ dirtyContexts[queuedKind].clear();
246
+ }
247
+ contextCacheFlushScheduled = false;
248
+ }, 0);
249
+ }
250
+ return context;
251
+ }
252
+ //#endregion
253
+ //#region src/internal/runtime.ts
254
+ const sigmaInternalsMap = /* @__PURE__ */ new WeakMap();
255
+ let autoFreezeEnabled = true;
256
+ let nextActionOwnerId = 1;
257
+ let currentDraftOwner;
258
+ function registerSigmaInternals(context, instance) {
259
+ sigmaInternalsMap.set(context, instance);
260
+ }
261
+ function getSigmaInternals(context) {
262
+ const instance = sigmaInternalsMap.get(context);
263
+ if (!instance) throw new Error("[preact-sigma] Invalid sigma context");
264
+ return instance;
265
+ }
266
+ /** Controls whether sigma deep-freezes published public state. Auto-freezing starts enabled and the setting is shared across instances. */
267
+ function setAutoFreeze(autoFreeze) {
268
+ autoFreezeEnabled = autoFreeze;
269
+ immer.setAutoFreeze(autoFreeze);
270
+ }
271
+ function getSignal(instance, key) {
272
+ return instance.publicInstance["#" + key];
273
+ }
274
+ function initializeSigmaInstance(publicInstance, type, initialState) {
275
+ const stateKeys = new Set(type._defaultStateKeys);
276
+ if (initialState) for (const key in initialState) stateKeys.add(key);
277
+ const instance = {
278
+ changeSubscriptions: /* @__PURE__ */ new Set(),
279
+ currentSetupCleanup: void 0,
280
+ patchSubscriptions: 0,
281
+ publicInstance,
282
+ stateKeys,
283
+ type,
284
+ disposed: false
285
+ };
286
+ for (const key of stateKeys) {
287
+ if (reservedKeys.has(key)) throw new Error(`[preact-sigma] Reserved property name: ${key}`);
288
+ let value = initialState?.[key];
289
+ if (value === void 0) value = typeof type._defaultState[key] === "function" ? type._defaultState[key].call(void 0) : type._defaultState[key];
290
+ const container = signal(value);
291
+ Object.defineProperty(publicInstance, "#" + key, { value: container });
292
+ Object.defineProperty(publicInstance, key, {
293
+ get: () => container.value,
294
+ enumerable: true
295
+ });
296
+ }
297
+ for (const key in type._computeFunctions) Object.defineProperty(publicInstance, "#" + key, { value: computed(() => type._computeFunctions[key].call(getContext(instance, "computedReadonly"))) });
298
+ registerSigmaInternals(publicInstance, instance);
299
+ }
300
+ function buildQueryMethod(queryFunction) {
301
+ return function(...args) {
302
+ const instance = getSigmaInternals(this);
303
+ const owner = getContextOwner(this);
304
+ if (owner) return queryFunction.apply(getContext(owner, "queryDraftAware"), args);
305
+ return computed(() => queryFunction.apply(getContext(instance, "queryCommitted"), args)).value;
306
+ };
307
+ }
308
+ function buildActionMethod(actionName, actionFn) {
309
+ return function(...args) {
310
+ return runActionInvocation(this, actionName, actionFn, args);
311
+ };
312
+ }
313
+ function runAdHocAction(context, actionFn) {
314
+ return runActionInvocation(context, "act()", actionFn, []);
315
+ }
316
+ function readActionStateValue(owner, key, options) {
317
+ if (owner.currentDraft) return owner.currentDraft[key];
318
+ const signal = getSignal(owner.instance, key);
319
+ const committedValue = options.reactiveReads ? signal.value : signal.peek();
320
+ if (options.draftOnRead && immer.isDraftable(committedValue)) return ensureOwnerDraft(owner)[key];
321
+ return committedValue;
322
+ }
323
+ function readActionComputedValue(owner, key) {
324
+ return owner.instance.type._computeFunctions[key].call(getContext(owner, "computedDraftAware"));
325
+ }
326
+ function setActionStateValue(owner, key, value) {
327
+ ensureOwnerDraft(owner)[key] = value;
328
+ }
329
+ function commitActionOwner(owner) {
330
+ const finalized = finalizeOwnerDraft(owner);
331
+ if (finalized?.changed) publishState(owner.instance, finalized);
332
+ }
333
+ function handleActionBoundary(owner, boundary, actionName) {
334
+ const draftOwner = currentDraftOwner;
335
+ if (!draftOwner?.currentDraft) return;
336
+ if (!finalizeOwnerDraft(draftOwner)?.changed) return;
337
+ if (draftOwner === owner) {
338
+ const message = boundary === "emit" ? `[preact-sigma] Action "${draftOwner.actionName}" has unpublished changes. Call this.commit() before emit().` : `[preact-sigma] Action "${draftOwner.actionName}" has unpublished changes. Call this.commit() before calling another action.`;
339
+ throw new Error(message);
340
+ }
341
+ if (boundary === "emit") throw new Error("[preact-sigma] Unexpected emit boundary. This is a bug.");
342
+ console.warn(`[preact-sigma] Discarded unpublished action changes from "${draftOwner.actionName}" before running "${actionName ?? "another action"}".`, {
343
+ action: draftOwner.actionFn,
344
+ actionArgs: draftOwner.args,
345
+ actionId: draftOwner.id,
346
+ actionName: draftOwner.actionName,
347
+ draftedInstance: currentDraftOwner?.publicInstance ?? draftOwner.publicInstance,
348
+ instance: draftOwner.instance.publicInstance
349
+ });
350
+ }
351
+ function assertDefinitionKeyAvailable(builder, key, kind) {
352
+ if (reservedKeys.has(key)) throw new Error(`[preact-sigma] Reserved property name: ${key}`);
353
+ if (key in builder._computeFunctions || key in builder._queryFunctions || key in builder._actionFunctions) throw new Error(`[preact-sigma] Duplicate key for ${kind}: ${key}`);
354
+ }
355
+ function shouldSetup(publicInstance) {
356
+ return getSigmaInternals(publicInstance).type._setupFunction !== null;
357
+ }
358
+ function clearCurrentDraft(owner) {
359
+ owner.currentDraft = void 0;
360
+ owner.currentBase = void 0;
361
+ if (currentDraftOwner === owner) currentDraftOwner = void 0;
362
+ }
363
+ function createActionOwner(instance, actionName, actionFn, args) {
364
+ const owner = {
365
+ actionFn,
366
+ actionName,
367
+ args,
368
+ id: nextActionOwnerId++,
369
+ instance,
370
+ publicInstance: instance.publicInstance
371
+ };
372
+ owner.actionContext = getContext(owner, "action");
373
+ registerSigmaInternals(owner.actionContext, instance);
374
+ registerContextOwner(owner.actionContext, owner);
375
+ return owner;
376
+ }
377
+ function runActionInvocation(context, actionName, actionFn, args) {
378
+ const instance = getSigmaInternals(context);
379
+ if (instance.disposed) throw new Error("[preact-sigma] Cannot run an action on a disposed sigma state");
380
+ const isAdHocAction = actionName === "act()";
381
+ const actionIsAsync = actionFn.constructor.name === "AsyncFunction";
382
+ if (actionIsAsync && isAdHocAction) throw new Error("[preact-sigma] act() callbacks must stay synchronous");
383
+ return untracked(() => {
384
+ let owner;
385
+ const callerOwner = getContextOwner(context);
386
+ if (callerOwner && callerOwner.instance === instance && !actionIsAsync) owner = callerOwner;
387
+ else {
388
+ handleActionBoundary(callerOwner, "action", actionName);
389
+ owner = createActionOwner(instance, actionName, actionFn, args);
390
+ }
391
+ let result;
392
+ try {
393
+ result = actionFn.apply(owner.actionContext, args);
394
+ } catch (error) {
395
+ clearCurrentDraft(owner);
396
+ throw error;
397
+ }
398
+ if (isAdHocAction && isPromiseLike(result)) {
399
+ clearCurrentDraft(owner);
400
+ Promise.resolve(result).catch(() => {});
401
+ throw new Error("[preact-sigma] act() callbacks must stay synchronous");
402
+ }
403
+ if (!actionIsAsync && isPromiseLike(result)) {
404
+ clearCurrentDraft(owner);
405
+ Promise.resolve(result).catch(() => {});
406
+ throw new Error(`[preact-sigma] Action "${actionName}" must use native async-await syntax to return a promise.`);
407
+ }
408
+ if (owner === callerOwner) return result;
409
+ const finalized = finalizeOwnerDraft(owner);
410
+ if (finalized?.changed) publishState(instance, finalized);
411
+ if (isPromiseLike(result)) return resolveAsyncActionResult(owner, result);
412
+ return result;
413
+ });
414
+ }
415
+ function disposeCleanupResource(resource) {
416
+ if (typeof resource === "function") resource();
417
+ else if (resource instanceof AbortController) resource.abort();
418
+ else if ("dispose" in resource) resource.dispose();
419
+ else resource[Symbol.dispose]();
420
+ }
421
+ function assertExactStateKeys(stateKeys, nextState) {
422
+ const extraKeys = Object.keys(nextState).filter((key) => !stateKeys.has(key));
423
+ const missingKeys = [...stateKeys].filter((key) => !Object.prototype.hasOwnProperty.call(nextState, key));
424
+ if (!extraKeys.length && !missingKeys.length) return;
425
+ let message = "[preact-sigma] replaceState() requires exactly the instance's state keys";
426
+ if (missingKeys.length) message += `. Missing: ${missingKeys.join(", ")}`;
427
+ if (extraKeys.length) message += `. Extra: ${extraKeys.join(", ")}`;
428
+ throw new Error(message);
429
+ }
430
+ function assertNoPendingDraft(operationName) {
431
+ const owner = currentDraftOwner;
432
+ if (!owner?.currentDraft) return;
433
+ throw new Error(`[preact-sigma] ${operationName}() cannot run while action "${owner.actionName}" has unpublished changes. Call this.commit() before ${operationName}().`);
434
+ }
435
+ function snapshotState(instance) {
436
+ const snapshot = Object.create(null);
437
+ for (const key of instance.stateKeys) snapshot[key] = getSignal(instance, key).peek();
438
+ return snapshot;
439
+ }
440
+ function ensureOwnerDraft(owner) {
441
+ if (owner.currentDraft) return owner.currentDraft;
442
+ handleActionBoundary(owner, "action", owner.actionName);
443
+ owner.currentBase = snapshotState(owner.instance);
444
+ owner.currentDraft = immer.createDraft(owner.currentBase);
445
+ currentDraftOwner = owner;
446
+ return owner.currentDraft;
447
+ }
448
+ function finalizeOwnerDraft(owner) {
449
+ const currentDraft = owner.currentDraft;
450
+ const oldState = owner.currentBase;
451
+ if (!currentDraft || !oldState) return;
452
+ clearCurrentDraft(owner);
453
+ let patches;
454
+ let inversePatches;
455
+ const newState = owner.instance.patchSubscriptions > 0 ? immer.finishDraft(currentDraft, (nextPatches, nextInversePatches) => {
456
+ patches = nextPatches;
457
+ inversePatches = nextInversePatches;
458
+ }) : immer.finishDraft(currentDraft);
459
+ return {
460
+ changed: newState !== oldState,
461
+ inversePatches,
462
+ newState,
463
+ oldState,
464
+ patches
465
+ };
466
+ }
467
+ function finalizeReplacementState(instance, oldState, nextState) {
468
+ const draft = immer.createDraft(oldState);
469
+ for (const key of instance.stateKeys) draft[key] = nextState[key];
470
+ let patches;
471
+ let inversePatches;
472
+ const newState = instance.patchSubscriptions > 0 ? immer.finishDraft(draft, (nextPatches, nextInversePatches) => {
473
+ patches = nextPatches;
474
+ inversePatches = nextInversePatches;
475
+ }) : immer.finishDraft(draft);
476
+ return {
477
+ changed: newState !== oldState,
478
+ inversePatches,
479
+ newState,
480
+ oldState,
481
+ patches
482
+ };
483
+ }
484
+ function isPromiseLike(value) {
485
+ return value != null && typeof value.then === "function";
486
+ }
487
+ function publishState(instance, finalized) {
488
+ batch(() => {
489
+ for (const key of instance.stateKeys) {
490
+ const nextValue = finalized.newState[key];
491
+ if (autoFreezeEnabled) immer.freeze(nextValue, true);
492
+ const signal = getSignal(instance, key);
493
+ signal.value = nextValue;
494
+ }
495
+ });
496
+ if (instance.changeSubscriptions.size) {
497
+ const context = getContext(instance, "observe");
498
+ for (const subscription of instance.changeSubscriptions) subscription.listener.call(context, finalized);
499
+ }
500
+ }
501
+ function isPlainObject(value) {
502
+ if (typeof value !== "object" || value === null) return false;
503
+ const proto = Object.getPrototypeOf(value);
504
+ return proto === Object.prototype || proto === null;
505
+ }
506
+ /**
507
+ * Utility helpers for sigma-state instances.
508
+ *
509
+ * The helpers expose instance-specific built-ins without reserving names on the
510
+ * public instance object.
511
+ */
512
+ const sigma = Object.freeze({
513
+ getSignal: (publicInstance, key) => {
514
+ return publicInstance["#" + key];
515
+ },
516
+ getState: (publicInstance) => {
517
+ return snapshotState(getSigmaInternals(publicInstance));
518
+ },
519
+ replaceState: (publicInstance, nextState) => {
520
+ const instance = getSigmaInternals(publicInstance);
521
+ if (!isPlainObject(nextState)) throw new Error("[preact-sigma] replaceState() requires a plain object snapshot");
522
+ assertNoPendingDraft("replaceState");
523
+ assertExactStateKeys(instance.stateKeys, nextState);
524
+ const finalized = finalizeReplacementState(instance, snapshotState(instance), nextState);
525
+ if (finalized.changed) publishState(instance, finalized);
526
+ },
527
+ subscribe: ((publicInstance, keyOrListener, listenerOrOptions) => {
528
+ const instance = getSigmaInternals(publicInstance);
529
+ if (typeof keyOrListener === "string") return getSignal(instance, keyOrListener).subscribe(listenerOrOptions);
530
+ const subscription = {
531
+ listener: keyOrListener,
532
+ patches: listenerOrOptions?.patches ?? false
533
+ };
534
+ instance.changeSubscriptions.add(subscription);
535
+ if (subscription.patches) instance.patchSubscriptions += 1;
536
+ return () => {
537
+ if (!instance.changeSubscriptions.delete(subscription)) return;
538
+ if (subscription.patches) instance.patchSubscriptions -= 1;
539
+ };
540
+ })
541
+ });
542
+ async function resolveAsyncActionResult(owner, result) {
543
+ let settledValue;
544
+ let settledError;
545
+ let rejected = false;
546
+ try {
547
+ settledValue = await result;
548
+ } catch (error) {
549
+ rejected = true;
550
+ settledError = error;
551
+ }
552
+ if (currentDraftOwner === owner && owner.currentDraft) {
553
+ if (finalizeOwnerDraft(owner)?.changed) {
554
+ const commitError = /* @__PURE__ */ new Error(`[preact-sigma] Async action "${owner.actionName}" finished with unpublished changes. Call this.commit() before await or return.`);
555
+ if (rejected) throw new AggregateError([settledError, commitError], `[preact-sigma] Async action "${owner.actionName}" rejected and left unpublished changes`);
556
+ throw commitError;
557
+ }
558
+ }
559
+ if (rejected) throw settledError;
560
+ return settledValue;
561
+ }
562
+ var Sigma = class {
563
+ [sigmaTargetBrand] = new SigmaListenerMap();
564
+ setup(...args) {
565
+ const instance = getSigmaInternals(this);
566
+ if (!instance.type._setupFunction) throw new Error("[preact-sigma] Setup is undefined for this sigma state");
567
+ if (instance.disposed) throw new Error("[preact-sigma] Cannot set up a disposed sigma state");
568
+ instance.currentSetupCleanup?.();
569
+ instance.currentSetupCleanup = void 0;
570
+ const resources = instance.type._setupFunction.apply(getContext(instance, "setup"), args);
571
+ if (!Array.isArray(resources)) throw new Error("[preact-sigma] Sigma setup handlers must return an array");
572
+ let cleanup;
573
+ if (resources.length) {
574
+ let cleaned = false;
575
+ cleanup = () => {
576
+ if (instance.currentSetupCleanup === cleanup) instance.currentSetupCleanup = void 0;
577
+ if (cleaned) return;
578
+ cleaned = true;
579
+ let errors;
580
+ for (let index = resources.length - 1; index >= 0; index -= 1) try {
581
+ disposeCleanupResource(resources[index]);
582
+ } catch (error) {
583
+ errors ||= [];
584
+ errors.push(error);
585
+ }
586
+ if (errors) throw new AggregateError(errors, "Failed to dispose one or more sigma resources");
587
+ };
588
+ instance.currentSetupCleanup = cleanup;
589
+ } else cleanup = () => {};
590
+ return cleanup;
591
+ }
592
+ };
593
+ Object.defineProperty(Sigma.prototype, sigmaStateBrand, { value: true });
594
+ //#endregion
595
+ export { initializeSigmaInstance as a, sigma as c, listen as d, sigmaStateBrand as f, buildQueryMethod as i, SigmaListenerMap as l, assertDefinitionKeyAvailable as n, setAutoFreeze as o, buildActionMethod as r, shouldSetup as s, Sigma as t, SigmaTarget as u };
package/docs/context.md CHANGED
@@ -27,7 +27,7 @@
27
27
  - Query: a reactive read that accepts arguments, declared with `.queries(...)` or built locally with `query(fn)`.
28
28
  - Action: a method declared with `.actions(...)` that reads and writes through sigma's draft and commit semantics.
29
29
  - Setup handler: a function declared with `.setup(...)` that owns side effects and cleanup resources explicitly.
30
- - Event: a typed notification emitted through `this.emit(...)` and observed through `.on(...)`, `listen(...)`, or `useListener(...)`.
30
+ - Event: a typed notification emitted through `this.emit(...)` and observed through `listen(...)` or `useListener(...)`.
31
31
 
32
32
  # Data Flow / Lifecycle
33
33
 
@@ -48,36 +48,55 @@
48
48
  - Keep a tracked helper local to one consumer module: `query(fn)`
49
49
  - Mutate state and emit typed notifications: `.actions(...)`
50
50
  - Publish before `await`, `emit(...)`, or another action boundary: `this.commit()`
51
- - React to committed state changes: `.observe(...)`
51
+ - React to committed state changes: `sigma.subscribe(instance, handler)` or `sigma.subscribe(instance, key, handler)`
52
+ - Read one top-level state property or computed as a `ReadonlySignal`: `sigma.getSignal(instance, key)`
52
53
  - Own timers, listeners, subscriptions, or nested setup: `.setup(...)`
53
54
  - Use a sigma state inside a component: `useSigma(...)`
54
55
  - Subscribe to sigma or DOM events in a component: `useListener(...)`
55
- - Create a standalone typed event hub with no managed state: `new SigmaTarget<TEvents>()`, `hub.emit(...)`, and `hub.on(...)`
56
- - Subscribe outside components: `.on(...)` or `listen(...)`
57
- - Read or restore committed top-level state: `snapshot(...)` and `replaceState(...)`
56
+ - Create a standalone typed event hub with no managed state: `new SigmaTarget<TEvents>()`, `hub.emit(...)`, and `listen(hub, ...)`
57
+ - Subscribe outside components: `listen(instance, ...)`
58
+ - Read or restore committed top-level state: `sigma.getState(...)` and `sigma.replaceState(...)`
58
59
 
59
- # Practical Guidelines
60
+ # Recommended Patterns
60
61
 
61
62
  - Put explicit type arguments on `new SigmaType<TState, TEvents>()` and let later builder methods infer from the objects you pass.
62
63
  - Keep frequently read values as separate top-level state properties. Each top-level key gets its own signal.
63
64
  - Use `.computed(...)` for argument-free derived reads.
64
65
  - Use `.queries(...)` for tracked reads with arguments.
65
66
  - Keep one-off calculations local until they become reusable model behavior.
66
- - Reach for `instance.get(key)` only when code specifically needs the underlying `ReadonlySignal`.
67
- - 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.
68
- - Use ordinary actions for routine writes. Reserve `snapshot(...)` and `replaceState(...)` for replay, reset, or undo-like flows on committed top-level state.
67
+ - Use ordinary actions for routine writes. Reserve `sigma.getState(...)` and `sigma.replaceState(...)` for replay, reset, or undo-like flows on committed top-level state.
68
+ - Prefer `listen(...)` for external event subscriptions. It works with sigma states, `SigmaTarget`, and DOM targets.
69
69
  - Put owned side effects in `.setup(...)`.
70
+ - Use `sigma.subscribe(this, ...)` inside `.setup(...)` when a setup-owned side effect should react to future committed publishes. Return that cleanup so the subscription stops with setup.
71
+ ```ts
72
+ .setup(function () {
73
+ return [
74
+ sigma.subscribe(this, (change) => {
75
+ console.log(change.newState);
76
+ }),
77
+ ];
78
+ })
79
+ ```
70
80
  - Use `this.act(function () { ... })` for setup-owned callbacks that need action semantics.
71
- - Call Immer's `enablePatches()` before relying on `.observe(..., { patches: true })`.
81
+
82
+ # Patterns to Avoid
83
+
84
+ - Reaching for `sigma.getSignal(instance, key)` when direct property reads already cover the use case.
85
+ - Crossing `emit(...)`, `await`, or another action boundary with unpublished changes when those changes need to stay visible afterward. Publish them first with `this.commit()`.
86
+ - Starting side effects during construction instead of through explicit `setup(...)`.
87
+ - Encoding storage, hydration, or migration policy directly into `SigmaType` definitions.
88
+ - Treating query calls as memoized across invocations.
89
+ - Relying on patch payloads without enabling Immer patches first.
72
90
 
73
91
  # Invariants and Constraints
74
92
 
75
93
  - Sigma only tracks top-level state properties. Each top-level key gets its own signal.
76
94
  - Public state is readonly outside actions and `this.act(...)` inside setup.
77
- - Duplicate names across state properties, computeds, queries, and actions are rejected at runtime. Reserved public names include `act`, `emit`, `get`, `on`, and `setup`.
95
+ - Duplicate names across state properties, computeds, queries, and actions are rejected at runtime. Reserved public names include `act`, `commit`, `emit`, and `setup`.
78
96
  - Query calls are reactive at the call site but do not memoize across invocations.
79
97
  - Setup handlers return arrays of cleanup resources, and cleanup runs in reverse order.
80
- - `replaceState(...)` works on committed top-level state and requires the exact state-key shape.
98
+ - Call Immer's `enablePatches()` before relying on `sigma.subscribe(instance, handler, { patches: true })`.
99
+ - `sigma.replaceState(...)` works on committed top-level state and requires the exact state-key shape.
81
100
  - Published draftable public state is deep-frozen by default. `setAutoFreeze(false)` disables that behavior globally.
82
101
 
83
102
  # Error Model
@@ -86,13 +105,13 @@
86
105
  - If another invocation crosses a boundary while unpublished changes still exist, sigma warns and discards those changes before continuing.
87
106
  - Calling `setup(...)` on a sigma state without registered setup handlers throws.
88
107
  - Cleanup rethrows an `AggregateError` when more than one cleanup resource fails.
89
- - `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.
108
+ - `sigma.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.
90
109
 
91
110
  # Terminology
92
111
 
93
112
  - Draft boundary: a point where sigma cannot keep reusing the current unpublished draft.
94
113
  - Committed state: the published top-level public state visible outside the current action draft.
95
- - Signal access: reading the underlying `ReadonlySignal` for a top-level state key or computed through `instance.get(key)`.
114
+ - Signal access: reading the underlying `ReadonlySignal` for a top-level state key or computed through `sigma.getSignal(instance, key)`.
96
115
  - Cleanup resource: a cleanup function, `AbortController`, object with `dispose()`, or object with `[Symbol.dispose]()`.
97
116
  - 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.
98
117