preact-sigma 1.0.0 → 2.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.
Files changed (5) hide show
  1. package/README.md +113 -467
  2. package/dist/index.d.mts +207 -193
  3. package/dist/index.mjs +593 -265
  4. package/llms.txt +354 -238
  5. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -1,301 +1,629 @@
1
- import { Signal, action, batch, computed, computed as computed$1, signal, untracked } from "@preact/signals";
2
- import { castImmutable, freeze, produce } from "immer";
3
- import { useCallback, useEffect, useRef, useState } from "preact/hooks";
4
- //#region src/internal.ts
5
- const lensKeys = /* @__PURE__ */ new WeakMap();
6
- const queryMethods = /* @__PURE__ */ new WeakSet();
7
- var StateContainer = class extends EventTarget {
8
- _signals = /* @__PURE__ */ new Map();
9
- _view = computed$1(() => ({ ...this }));
10
- constructor(state, handle, props, dispose) {
11
- super();
12
- this.dispose = dispose;
13
- const propDescriptors = Object.getOwnPropertyDescriptors(props);
14
- for (const key in propDescriptors) {
15
- const propDescriptor = propDescriptors[key];
16
- if ("value" in propDescriptor) {
17
- let { value } = propDescriptor;
18
- if (typeof value === "function") {
19
- Object.defineProperty(this, key, { value: queryMethods.has(value) ? value : action(value) });
20
- continue;
21
- }
22
- const signal = getExposedSignal(value, state, handle);
23
- if (signal) {
24
- this._signals.set(key, signal);
25
- Object.defineProperty(this, key, {
26
- get: () => signal.value,
27
- enumerable: true
28
- });
29
- } else if (isManagedState(value)) Object.defineProperty(this, key, {
30
- value,
31
- enumerable: true
32
- });
33
- else throw new Error(`Invalid property: ${key}. Must be a function, a signal, a top-level lens, the state handle, or a managed state.`);
34
- } else throw new Error(`\`get ${key}() {}\` syntax is forbidden`);
35
- }
36
- }
37
- get(key) {
38
- if (!key) return this._view;
39
- return this._signals.get(key);
40
- }
41
- peek(key) {
42
- const signal = this.get(key);
43
- if (!signal) return;
44
- return signal.peek();
45
- }
46
- subscribe(...args) {
47
- if (args.length > 1) {
48
- const [key, listener] = args;
49
- const signal = this.get(key);
50
- if (!signal) throw new Error(`Property ${key} is not a signal`);
51
- return signal.subscribe(listener);
52
- }
53
- return this._view.subscribe(args[0]);
54
- }
55
- on(name, listener) {
56
- const adapter = (event) => {
57
- const detail = event.detail;
58
- if (detail === void 0) listener();
59
- else listener(detail);
60
- };
61
- this.addEventListener(name, adapter);
62
- return () => {
63
- this.removeEventListener(name, adapter);
64
- };
1
+ import { action, batch, batch as batch$1, computed, computed as computed$1, effect, signal, untracked, untracked as untracked$1 } from "@preact/signals";
2
+ import * as immer from "immer";
3
+ import { freeze, immerable } from "immer";
4
+ import { useEffect, useRef, useState } from "preact/hooks";
5
+ //#region src/internal/context.ts
6
+ const disabledContextOptions = {
7
+ allowActions: false,
8
+ allowCommit: false,
9
+ allowEmit: false,
10
+ allowQueries: false,
11
+ allowWrites: false,
12
+ draftAware: false,
13
+ draftOnRead: false,
14
+ liveComputeds: false,
15
+ reactiveReads: false
16
+ };
17
+ const publicContextOptions = {
18
+ computedReadonly: {
19
+ ...disabledContextOptions,
20
+ reactiveReads: true
21
+ },
22
+ observe: {
23
+ ...disabledContextOptions,
24
+ allowQueries: true
25
+ },
26
+ queryCommitted: {
27
+ ...disabledContextOptions,
28
+ allowQueries: true,
29
+ reactiveReads: true
30
+ },
31
+ setup: {
32
+ ...disabledContextOptions,
33
+ allowActions: true,
34
+ allowEmit: true,
35
+ allowQueries: true
65
36
  }
66
- [Symbol.dispose]() {
67
- this.dispose();
37
+ };
38
+ const ownerContextOptions = {
39
+ action: {
40
+ ...disabledContextOptions,
41
+ allowActions: true,
42
+ allowCommit: true,
43
+ allowEmit: true,
44
+ allowQueries: true,
45
+ allowWrites: true,
46
+ draftAware: true,
47
+ draftOnRead: true,
48
+ liveComputeds: true
49
+ },
50
+ computedDraftAware: {
51
+ ...disabledContextOptions,
52
+ draftAware: true,
53
+ liveComputeds: true
54
+ },
55
+ queryDraftAware: {
56
+ ...disabledContextOptions,
57
+ allowQueries: true,
58
+ draftAware: true,
59
+ liveComputeds: true
68
60
  }
69
61
  };
70
- function isProducer(value) {
71
- return typeof value === "function";
62
+ const dirtyContexts = {
63
+ computedReadonly: /* @__PURE__ */ new Set(),
64
+ observe: /* @__PURE__ */ new Set(),
65
+ queryCommitted: /* @__PURE__ */ new Set(),
66
+ setup: /* @__PURE__ */ new Set()
67
+ };
68
+ const contextKinds = Object.keys(dirtyContexts);
69
+ const contextCache = {
70
+ computedReadonly: /* @__PURE__ */ new WeakMap(),
71
+ observe: /* @__PURE__ */ new WeakMap(),
72
+ queryCommitted: /* @__PURE__ */ new WeakMap(),
73
+ setup: /* @__PURE__ */ new WeakMap()
74
+ };
75
+ const contextOwnerMap = /* @__PURE__ */ new WeakMap();
76
+ let contextCacheFlushScheduled = false;
77
+ function getContext(target, kind) {
78
+ if (isOwnerContextKind(kind)) return getOwnerContext(target, kind);
79
+ return getPublicContext(target, kind);
72
80
  }
73
- function makeNonEnumerable(object, keys) {
74
- for (const key of keys) Object.defineProperty(object, key, {
75
- ...Object.getOwnPropertyDescriptor(object, key),
76
- enumerable: false
77
- });
81
+ function getContextOwner(context) {
82
+ return contextOwnerMap.get(context);
78
83
  }
79
- var AnyStateHandle = class {
80
- constructor(state, emit, own) {
81
- this.state = state;
82
- this.emit = emit;
83
- this.own = own;
84
- makeNonEnumerable(this, ["emit", "own"]);
85
- }
86
- /** Read the current immutable base state. This read is tracked. */
87
- get() {
88
- return this.state.value;
89
- }
90
- /** Read the current immutable base state snapshot without tracking. */
91
- peek() {
92
- return this.state.peek();
93
- }
94
- /** Replace the base state, or update it with an Immer producer. */
95
- set(value) {
96
- this.state.value = isProducer(value) ? produce(this.state.value, value) : castImmutable(value);
97
- }
98
- };
99
- function createStateHandle(state, emit, own) {
100
- const handle = new AnyStateHandle(state, emit, own);
101
- let lenses;
102
- const getLensDescriptor = (key) => {
103
- const currentState = state.value;
104
- if (!isLensableState(currentState)) return;
105
- return Reflect.getOwnPropertyDescriptor(currentState, key);
106
- };
107
- const getLens = (key) => {
108
- let lens = (lenses ||= /* @__PURE__ */ new Map()).get(key);
109
- if (!lens) {
110
- lens = {
111
- get: () => handle.get()[key],
112
- set: (update) => {
113
- handle.set((draft) => {
114
- draft[key] = isProducer(update) ? produce(draft[key], update) : update;
115
- });
116
- }
117
- };
118
- lensKeys.set(lens, key);
119
- lenses.set(key, lens);
120
- }
121
- return lens;
122
- };
123
- return new Proxy(handle, {
124
- get(target, key, receiver) {
125
- if (Reflect.has(target, key)) return Reflect.get(target, key, receiver);
126
- if (!getLensDescriptor(key)) return;
127
- return getLens(key);
84
+ function registerContextOwner(context, owner) {
85
+ contextOwnerMap.set(context, owner);
86
+ }
87
+ function createContext(instance, options, owner) {
88
+ const publicPrototype = Object.getPrototypeOf(instance.publicInstance);
89
+ return new Proxy(publicPrototype, {
90
+ get(_target, key) {
91
+ if (typeof key !== "string") return Reflect.get(publicPrototype, key, owner?.actionContext ?? instance.publicInstance);
92
+ if (key === "commit") return options.allowCommit && owner ? () => commitActionOwner(owner) : void 0;
93
+ if (key === "emit") return options.allowEmit && owner ? (name, detail) => {
94
+ handleActionBoundary(owner, "emit");
95
+ instance.publicInstance.dispatchEvent(new CustomEvent(name, { detail }));
96
+ } : void 0;
97
+ if (instance.stateKeys.has(key)) {
98
+ if (owner && options.draftAware) return readActionStateValue(owner, key, options);
99
+ const signal = getSignal(instance, key);
100
+ return options.reactiveReads ? signal.value : signal.peek();
101
+ }
102
+ if (key in instance.type.computeFunctions) {
103
+ if (owner && options.liveComputeds) return readActionComputedValue(owner, key);
104
+ const signal = getSignal(instance, key);
105
+ return options.reactiveReads ? signal.value : signal.peek();
106
+ }
107
+ if (options.allowQueries && key in instance.type.queryFunctions) return Reflect.get(instance.publicInstance, key);
108
+ if (options.allowActions && key in instance.type.actionFunctions) return Reflect.get(instance.publicInstance, key);
109
+ if (Reflect.has(publicPrototype, key)) return Reflect.get(publicPrototype, key, owner?.actionContext ?? instance.publicInstance);
128
110
  },
129
- ownKeys(_target) {
130
- const currentState = state.value;
131
- if (!isLensableState(currentState)) return [];
132
- return Reflect.ownKeys(currentState);
111
+ set(_target, key, value) {
112
+ if (!owner || !options.allowWrites || typeof key !== "string" || !instance.stateKeys.has(key)) return false;
113
+ setActionStateValue(owner, key, value);
114
+ return true;
133
115
  },
134
- getOwnPropertyDescriptor(_target, key) {
135
- const lensDescriptor = getLensDescriptor(key);
136
- if (!lensDescriptor) return;
137
- return {
138
- configurable: true,
139
- enumerable: lensDescriptor.enumerable,
140
- value: getLens(key),
141
- writable: false
142
- };
143
- }
116
+ apply: unsupportedOperation,
117
+ construct: unsupportedOperation,
118
+ defineProperty: unsupportedOperation,
119
+ deleteProperty: unsupportedOperation,
120
+ getOwnPropertyDescriptor: unsupportedOperation,
121
+ has: unsupportedOperation,
122
+ isExtensible: unsupportedOperation,
123
+ ownKeys: unsupportedOperation,
124
+ preventExtensions: unsupportedOperation,
125
+ setPrototypeOf: unsupportedOperation
144
126
  });
145
127
  }
146
- function isLensableState(value) {
128
+ function unsupportedOperation() {
129
+ throw new Error("[preact-sigma] This operation is not supported by context proxies");
130
+ }
131
+ const kindToOwnerContextKey = {
132
+ action: "actionContext",
133
+ computedDraftAware: "computedContext",
134
+ queryDraftAware: "queryContext"
135
+ };
136
+ function isOwnerContextKind(kind) {
137
+ return kind in kindToOwnerContextKey;
138
+ }
139
+ function getOwnerContext(owner, kind) {
140
+ const contextKey = kindToOwnerContextKey[kind];
141
+ if (owner[contextKey]) return owner[contextKey];
142
+ const context = createContext(owner.instance, ownerContextOptions[kind], owner);
143
+ registerSigmaInternals(context, owner.instance);
144
+ registerContextOwner(context, owner);
145
+ owner[contextKey] = context;
146
+ return context;
147
+ }
148
+ function getPublicContext(instance, kind) {
149
+ const cachedContext = contextCache[kind].get(instance);
150
+ if (cachedContext) return cachedContext;
151
+ const context = createContext(instance, publicContextOptions[kind], void 0);
152
+ registerSigmaInternals(context, instance);
153
+ contextCache[kind].set(instance, context);
154
+ dirtyContexts[kind].add(instance);
155
+ if (!contextCacheFlushScheduled) {
156
+ contextCacheFlushScheduled = true;
157
+ setTimeout(() => {
158
+ for (const queuedKind of contextKinds) {
159
+ for (const queuedInstance of dirtyContexts[queuedKind]) contextCache[queuedKind].delete(queuedInstance);
160
+ dirtyContexts[queuedKind].clear();
161
+ }
162
+ contextCacheFlushScheduled = false;
163
+ }, 0);
164
+ }
165
+ return context;
166
+ }
167
+ //#endregion
168
+ //#region src/internal/symbols.ts
169
+ const sigmaStateBrand = Symbol("sigma.v2.state");
170
+ const reservedKeys = new Set([
171
+ "get",
172
+ "emit",
173
+ "commit",
174
+ "on",
175
+ "setup"
176
+ ]);
177
+ //#endregion
178
+ //#region src/internal/runtime.ts
179
+ const sigmaInternalsMap = /* @__PURE__ */ new WeakMap();
180
+ let autoFreezeEnabled = true;
181
+ let nextActionOwnerId = 1;
182
+ let currentDraftOwner;
183
+ function registerSigmaInternals(context, instance) {
184
+ sigmaInternalsMap.set(context, instance);
185
+ }
186
+ function getSigmaInternals(context) {
187
+ const instance = sigmaInternalsMap.get(context);
188
+ if (!instance) throw new Error("[preact-sigma] Invalid sigma context");
189
+ return instance;
190
+ }
191
+ function isAutoFreeze() {
192
+ return autoFreezeEnabled;
193
+ }
194
+ /** Controls whether sigma deep-freezes published public state. Auto-freezing starts enabled. */
195
+ function setAutoFreeze(autoFreeze) {
196
+ autoFreezeEnabled = autoFreeze;
197
+ immer.setAutoFreeze(autoFreeze);
198
+ }
199
+ function isPlainObject(value) {
147
200
  if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
148
201
  const prototype = Object.getPrototypeOf(value);
149
202
  return prototype === Object.prototype || prototype === null;
150
203
  }
151
- function getExposedSignal(value, state, handle) {
152
- if (value === handle) return state;
153
- if (value instanceof Signal) return value;
154
- const lensKey = getLensKey(value);
155
- if (lensKey !== void 0) return computed$1(() => state.value[lensKey]);
156
- }
157
- function getLensKey(value) {
158
- if (!value || typeof value !== "object") return;
159
- return lensKeys.get(value);
160
- }
161
- function disposeOwnedResources(resources) {
162
- let errors;
163
- for (let index = resources.length - 1; index >= 0; index -= 1) try {
164
- const resource = resources[index];
165
- if (typeof resource === "function") resource();
166
- else resource[Symbol.dispose]();
167
- } catch (error) {
168
- errors ||= [];
169
- errors.push(error);
204
+ function getSignal(instance, key) {
205
+ return instance.publicInstance["#" + key];
206
+ }
207
+ function initializeSigmaInstance(publicInstance, type, initialState) {
208
+ const stateKeys = new Set(type.defaultStateKeys);
209
+ if (initialState) for (const key in initialState) stateKeys.add(key);
210
+ const instance = {
211
+ currentSetupCleanup: void 0,
212
+ publicInstance,
213
+ stateKeys,
214
+ type,
215
+ disposed: false
216
+ };
217
+ for (const key of stateKeys) {
218
+ if (reservedKeys.has(key)) throw new Error(`[preact-sigma] Reserved property name: ${key}`);
219
+ let value = initialState?.[key];
220
+ if (value === void 0) value = typeof type.defaultState[key] === "function" ? type.defaultState[key].call(void 0) : type.defaultState[key];
221
+ const container = signal(value);
222
+ Object.defineProperty(publicInstance, "#" + key, { value: container });
223
+ Object.defineProperty(publicInstance, key, {
224
+ get: () => container.value,
225
+ enumerable: true
226
+ });
170
227
  }
171
- if (errors) throw new AggregateError(errors, "Failed to dispose one or more resources");
228
+ for (const key in type.computeFunctions) Object.defineProperty(publicInstance, "#" + key, { value: computed$1(() => type.computeFunctions[key].call(getContext(instance, "computedReadonly"))) });
229
+ registerSigmaInternals(publicInstance, instance);
172
230
  }
173
- /** Check whether a value is a managed-state instance. */
174
- function isManagedState(value) {
175
- return value instanceof StateContainer;
231
+ function buildQueryMethod(queryFunction) {
232
+ return function(...args) {
233
+ const instance = getSigmaInternals(this);
234
+ const owner = getContextOwner(this);
235
+ if (owner) return queryFunction.apply(getContext(owner, "queryDraftAware"), args);
236
+ return computed$1(() => queryFunction.apply(getContext(instance, "queryCommitted"), args)).value;
237
+ };
238
+ }
239
+ function buildActionMethod(actionName, actionFn) {
240
+ const actionIsAsync = isAsyncFunction(actionFn);
241
+ return function(...args) {
242
+ const instance = getSigmaInternals(this);
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
+ });
270
+ };
271
+ }
272
+ function readActionStateValue(owner, key, options) {
273
+ if (owner.currentDraft) return owner.currentDraft[key];
274
+ const signal = getSignal(owner.instance, key);
275
+ const committedValue = options.reactiveReads ? signal.value : signal.peek();
276
+ if (options.draftOnRead && immer.isDraftable(committedValue)) return ensureOwnerDraft(owner)[key];
277
+ return committedValue;
278
+ }
279
+ function readActionComputedValue(owner, key) {
280
+ return owner.instance.type.computeFunctions[key].call(getContext(owner, "computedDraftAware"));
281
+ }
282
+ function setActionStateValue(owner, key, value) {
283
+ ensureOwnerDraft(owner)[key] = value;
284
+ }
285
+ function commitActionOwner(owner) {
286
+ const finalized = finalizeOwnerDraft(owner);
287
+ if (finalized?.changed) publishState(owner.instance, finalized);
288
+ }
289
+ function handleActionBoundary(owner, boundary, actionName) {
290
+ const draftOwner = currentDraftOwner;
291
+ if (!draftOwner?.currentDraft) return;
292
+ if (!finalizeOwnerDraft(draftOwner)?.changed) return;
293
+ if (draftOwner === owner) {
294
+ 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.`;
295
+ throw new Error(message);
296
+ }
297
+ if (boundary === "emit") throw new Error("[preact-sigma] Unexpected emit boundary. This is a bug.");
298
+ console.warn(`[preact-sigma] Discarded unpublished action changes from "${draftOwner.actionName}" before running "${actionName ?? "another action"}".`, createDraftMetadata(draftOwner));
299
+ }
300
+ function assertDefinitionKeyAvailable(builder, key, kind) {
301
+ if (reservedKeys.has(key)) throw new Error(`[preact-sigma] Reserved property name: ${key}`);
302
+ if (key in builder.computeFunctions || key in builder.queryFunctions || key in builder.actionFunctions) throw new Error(`[preact-sigma] Duplicate key for ${kind}: ${key}`);
303
+ }
304
+ function shouldSetup(publicInstance) {
305
+ return getSigmaInternals(publicInstance).type.setupFunctions.length > 0;
306
+ }
307
+ function clearCurrentDraft(owner) {
308
+ owner.currentDraft = void 0;
309
+ owner.currentBase = void 0;
310
+ if (currentDraftOwner === owner) currentDraftOwner = void 0;
311
+ }
312
+ function createActionOwner(instance, actionName, actionFn, args) {
313
+ const owner = {
314
+ actionFn,
315
+ actionName,
316
+ args,
317
+ id: nextActionOwnerId++,
318
+ instance,
319
+ publicInstance: instance.publicInstance
320
+ };
321
+ owner.actionContext = getContext(owner, "action");
322
+ registerSigmaInternals(owner.actionContext, instance);
323
+ registerContextOwner(owner.actionContext, owner);
324
+ return owner;
325
+ }
326
+ function createCommitError(owner) {
327
+ return /* @__PURE__ */ new Error(`[preact-sigma] Async action "${owner.actionName}" finished with unpublished changes. Call this.commit() before await or return.`);
328
+ }
329
+ function createDraftMetadata(owner) {
330
+ return {
331
+ action: owner.actionFn,
332
+ actionArgs: owner.args,
333
+ actionId: owner.id,
334
+ actionName: owner.actionName,
335
+ draftedInstance: currentDraftOwner?.publicInstance ?? owner.publicInstance,
336
+ instance: owner.instance.publicInstance
337
+ };
338
+ }
339
+ function disposeCleanupResource(resource) {
340
+ if (typeof resource === "function") resource();
341
+ else if (resource instanceof AbortController) resource.abort();
342
+ else resource[Symbol.dispose]();
343
+ }
344
+ function assertExactStateKeys(instance, nextState) {
345
+ const extraKeys = Object.keys(nextState).filter((key) => !instance.stateKeys.has(key));
346
+ const missingKeys = [...instance.stateKeys].filter((key) => !Object.prototype.hasOwnProperty.call(nextState, key));
347
+ if (!extraKeys.length && !missingKeys.length) return;
348
+ let message = "[preact-sigma] replaceState() requires exactly the instance's state keys";
349
+ if (missingKeys.length) message += `. Missing: ${missingKeys.join(", ")}`;
350
+ if (extraKeys.length) message += `. Extra: ${extraKeys.join(", ")}`;
351
+ throw new Error(message);
352
+ }
353
+ function assertNoPendingDraft(operationName) {
354
+ const owner = currentDraftOwner;
355
+ if (!owner?.currentDraft) return;
356
+ throw new Error(`[preact-sigma] ${operationName}() cannot run while action "${owner.actionName}" has unpublished changes. Call this.commit() before ${operationName}().`);
357
+ }
358
+ function snapshotState(instance) {
359
+ const snapshot = Object.create(null);
360
+ for (const key of instance.stateKeys) snapshot[key] = getSignal(instance, key).peek();
361
+ return snapshot;
362
+ }
363
+ function ensureOwnerDraft(owner) {
364
+ if (owner.currentDraft) return owner.currentDraft;
365
+ handleActionBoundary(owner, "action", owner.actionName);
366
+ owner.currentBase = snapshotState(owner.instance);
367
+ owner.currentDraft = immer.createDraft(owner.currentBase);
368
+ currentDraftOwner = owner;
369
+ return owner.currentDraft;
370
+ }
371
+ function finalizeOwnerDraft(owner) {
372
+ const currentDraft = owner.currentDraft;
373
+ const oldState = owner.currentBase;
374
+ if (!currentDraft || !oldState) return;
375
+ clearCurrentDraft(owner);
376
+ let patches;
377
+ let inversePatches;
378
+ const newState = owner.instance.type.patchesEnabled ? immer.finishDraft(currentDraft, (nextPatches, nextInversePatches) => {
379
+ patches = nextPatches;
380
+ inversePatches = nextInversePatches;
381
+ }) : immer.finishDraft(currentDraft);
382
+ return {
383
+ changed: newState !== oldState,
384
+ inversePatches,
385
+ newState,
386
+ oldState,
387
+ patches
388
+ };
389
+ }
390
+ function finalizeReplacementState(instance, oldState, nextState) {
391
+ const draft = immer.createDraft(oldState);
392
+ for (const key of instance.stateKeys) draft[key] = nextState[key];
393
+ let patches;
394
+ let inversePatches;
395
+ const newState = instance.type.patchesEnabled ? immer.finishDraft(draft, (nextPatches, nextInversePatches) => {
396
+ patches = nextPatches;
397
+ inversePatches = nextInversePatches;
398
+ }) : immer.finishDraft(draft);
399
+ return {
400
+ changed: newState !== oldState,
401
+ inversePatches,
402
+ newState,
403
+ oldState,
404
+ patches
405
+ };
406
+ }
407
+ function isAsyncFunction(fn) {
408
+ return fn.constructor.name === "AsyncFunction";
409
+ }
410
+ function isPromiseLike(value) {
411
+ return value != null && typeof value.then === "function";
412
+ }
413
+ function publishState(instance, finalized) {
414
+ batch$1(() => {
415
+ for (const key of instance.stateKeys) {
416
+ const nextValue = finalized.newState[key];
417
+ if (isAutoFreeze()) immer.freeze(nextValue, true);
418
+ const signal = getSignal(instance, key);
419
+ signal.value = nextValue;
420
+ }
421
+ });
422
+ for (const observer of instance.type.observeFunctions) observer.call(getContext(instance, "observe"), finalized);
176
423
  }
177
- //#endregion
178
- //#region src/framework.ts
179
424
  /**
180
- * Mark a constructor-returned method as a tracked query.
425
+ * Returns a shallow snapshot of an instance's committed public state.
181
426
  *
182
- * Query methods wrap their body in `computed()`, so reads inside the method
183
- * participate in signal tracking even after the method is exposed publicly.
184
- * Query functions read from closed-over handles or signals and do not use an
185
- * instance receiver. Tagged query methods also skip the default `action()`
186
- * wrapping step.
427
+ * The snapshot includes one own property for each top-level state key and reads
428
+ * the current committed value for that key. Its type is inferred from the
429
+ * instance's sigma-state definition.
187
430
  */
188
- function query(fn) {
189
- const wrapped = ((...args) => computed$1(() => fn(...args)).value);
190
- queryMethods.add(wrapped);
191
- return wrapped;
431
+ function snapshot(publicInstance) {
432
+ return snapshotState(getSigmaInternals(publicInstance));
192
433
  }
193
434
  /**
194
- * Define a managed state class with a private mutable implementation and an
195
- * immutable public surface.
196
- *
197
- * `TState` may be any non-function value, including primitives.
198
- *
199
- * The constructor function's explicitly typed `StateHandle` parameter is what
200
- * the library uses to infer the internal state and event types.
201
- *
202
- * Methods are automatically wrapped with `action()` from `@preact/signals`, so
203
- * they are untracked and batched unless you opt into tracked reads with
204
- * `query()`.
435
+ * Replaces an instance's committed public state from a snapshot object.
205
436
  *
206
- * The state constructor must return an object with properties that are either:
207
- * - A function (to expose methods)
208
- * - A signal (to expose derived state)
209
- * - A top-level lens from the state handle (to expose one reactive property)
210
- * - The state handle (to expose the reactive immutable base state directly)
211
- * - A managed state instance (to compose another managed state as a property)
212
- *
213
- * Returned signals and top-level lenses are turned into getter properties, so
214
- * reads are tracked by the `@preact/signals` runtime. When the base state is
215
- * object-shaped, spreading the `StateHandle` into the returned object exposes
216
- * its current top-level lenses at once.
217
- *
218
- * Events can carry at most one argument.
219
- *
220
- * The state constructor should be side-effect free.
437
+ * The replacement snapshot must be a plain object with exactly the instance's
438
+ * top-level state keys. Its type is inferred from the instance's sigma-state
439
+ * definition.
221
440
  */
222
- function defineManagedState(constructor, initialState) {
223
- initialState = freeze(initialState);
224
- return class extends StateContainer {
225
- constructor(...params) {
226
- const state = signal(initialState);
227
- let owned;
228
- let disposed = false;
229
- const dispose = () => {
230
- if (disposed) return;
231
- disposed = true;
232
- const current = owned;
233
- owned = void 0;
234
- if (!current) return;
235
- disposeOwnedResources(current);
236
- };
237
- const handle = createStateHandle(state, (name, detail) => this.dispatchEvent(new CustomEvent(name, { detail })), (resources) => {
238
- const nextResources = Array.isArray(resources) ? resources : [resources];
239
- if (!nextResources.length) return;
240
- if (disposed) disposeOwnedResources(nextResources);
241
- else (owned ??= []).push(...nextResources);
242
- });
243
- const props = constructor(handle, ...params);
244
- super(state, handle, props, dispose);
441
+ function replaceState(publicInstance, nextState) {
442
+ const instance = getSigmaInternals(publicInstance);
443
+ if (!isPlainObject(nextState)) throw new Error("[preact-sigma] replaceState() requires a plain object snapshot");
444
+ assertNoPendingDraft("replaceState");
445
+ assertExactStateKeys(instance, nextState);
446
+ const finalized = finalizeReplacementState(instance, snapshot(publicInstance), nextState);
447
+ if (finalized.changed) publishState(instance, finalized);
448
+ }
449
+ async function resolveAsyncActionResult(owner, result) {
450
+ let settledValue;
451
+ let settledError;
452
+ let rejected = false;
453
+ try {
454
+ settledValue = await result;
455
+ } catch (error) {
456
+ rejected = true;
457
+ settledError = error;
458
+ }
459
+ if (currentDraftOwner === owner && owner.currentDraft) {
460
+ if (finalizeOwnerDraft(owner)?.changed) {
461
+ const commitError = createCommitError(owner);
462
+ if (rejected) throw new AggregateError([settledError, commitError], `[preact-sigma] Async action "${owner.actionName}" rejected and left unpublished changes`);
463
+ throw commitError;
245
464
  }
246
- };
465
+ }
466
+ if (rejected) throw settledError;
467
+ return settledValue;
247
468
  }
469
+ var Sigma = class extends EventTarget {
470
+ setup(...args) {
471
+ const instance = getSigmaInternals(this);
472
+ if (!instance.type.setupFunctions.length) throw new Error("[preact-sigma] Setup is undefined for this sigma state");
473
+ if (instance.disposed) throw new Error("[preact-sigma] Cannot set up a disposed sigma state");
474
+ instance.currentSetupCleanup?.();
475
+ instance.currentSetupCleanup = void 0;
476
+ const resources = instance.type.setupFunctions.flatMap((setup) => {
477
+ const result = setup.apply(getContext(instance, "setup"), args);
478
+ if (!Array.isArray(result)) throw new Error("[preact-sigma] Sigma setup handlers must return an array");
479
+ return result;
480
+ });
481
+ let cleanup;
482
+ if (resources.length) {
483
+ let cleaned = false;
484
+ cleanup = () => {
485
+ if (instance.currentSetupCleanup === cleanup) instance.currentSetupCleanup = void 0;
486
+ if (cleaned) return;
487
+ cleaned = true;
488
+ let errors;
489
+ for (let index = resources.length - 1; index >= 0; index -= 1) try {
490
+ disposeCleanupResource(resources[index]);
491
+ } catch (error) {
492
+ errors ||= [];
493
+ errors.push(error);
494
+ }
495
+ if (errors) throw new AggregateError(errors, "Failed to dispose one or more sigma resources");
496
+ };
497
+ instance.currentSetupCleanup = cleanup;
498
+ } else cleanup = () => {};
499
+ return cleanup;
500
+ }
501
+ get(key) {
502
+ return getSignal(getSigmaInternals(this), key);
503
+ }
504
+ on(name, listener) {
505
+ const adapter = (event) => {
506
+ const payload = event.detail;
507
+ if (payload === void 0) listener();
508
+ else listener(payload);
509
+ };
510
+ this.addEventListener(name, adapter);
511
+ return () => {
512
+ this.removeEventListener(name, adapter);
513
+ };
514
+ }
515
+ };
516
+ Object.defineProperty(Sigma.prototype, sigmaStateBrand, { value: true });
248
517
  //#endregion
249
- //#region src/hooks.ts
250
- function isFunction(value) {
251
- return typeof value === "function";
518
+ //#region src/framework.ts
519
+ /** Checks whether a value is a sigma-state instance. */
520
+ function isSigmaState(value) {
521
+ return Boolean(value && typeof value === "object" && value[sigmaStateBrand]);
252
522
  }
253
- /**
254
- * Clean encapsulation of complex UI state.
255
- *
256
- * Use this when a component needs the same managed-state API without defining a
257
- * separate class. The constructor follows the same rules as
258
- * `defineManagedState()`, including explicit typing of the `StateHandle`
259
- * parameter for state and event inference.
260
- */
261
- function useManagedState(constructor, initialState) {
262
- const managedState = useState(() => new (defineManagedState(constructor, isFunction(initialState) ? initialState() : initialState))())[0];
263
- useEffect(() => () => managedState.dispose(), [managedState]);
264
- return managedState;
523
+ /** Creates a standalone tracked query function with the same signature as `fn`. */
524
+ function query(fn) {
525
+ return ((...args) => computed$1(() => fn(...args)).value);
265
526
  }
266
527
  /**
267
- * Subscribe to future values from a subscribable source inside `useEffect`.
268
- *
269
- * The listener is kept fresh automatically, so a dependency array is not part
270
- * of this API. The listener receives the current value immediately and then
271
- * future updates. Pass `null` to disable the subscription temporarily.
528
+ * Builds sigma-state constructors by accumulating default state, computeds, queries,
529
+ * observers, actions, and setup handlers.
272
530
  */
273
- function useSubscribe(target, listener) {
274
- listener = useStableCallback(listener);
275
- useEffect(() => target?.subscribe(listener), [target]);
531
+ var SigmaType = class extends Function {
532
+ constructor(name = "Sigma") {
533
+ super();
534
+ const type = {
535
+ actionFunctions: Object.create(null),
536
+ computeFunctions: Object.create(null),
537
+ defaultState: Object.create(null),
538
+ defaultStateKeys: [],
539
+ observeFunctions: [],
540
+ patchesEnabled: false,
541
+ queryFunctions: Object.create(null),
542
+ setupFunctions: []
543
+ };
544
+ const { [name]: SigmaTypeBuilder } = { [name]: class extends Sigma {
545
+ constructor(initialState) {
546
+ super();
547
+ initializeSigmaInstance(this, type, initialState);
548
+ }
549
+ } };
550
+ SigmaTypeBuilder.defaultState = function(defaultState) {
551
+ for (const key in defaultState) {
552
+ if (defaultState[key] === void 0) continue;
553
+ type.defaultState[key] = defaultState[key];
554
+ type.defaultStateKeys.push(key);
555
+ }
556
+ return this;
557
+ };
558
+ SigmaTypeBuilder.computed = function(computeFunctions) {
559
+ for (const key in computeFunctions) {
560
+ assertDefinitionKeyAvailable(type, key, "computed");
561
+ type.computeFunctions[key] = computeFunctions[key];
562
+ Object.defineProperty(this.prototype, key, { get: function() {
563
+ return this["#" + key].value;
564
+ } });
565
+ }
566
+ return this;
567
+ };
568
+ SigmaTypeBuilder.queries = function(queryFunctions) {
569
+ for (const key in queryFunctions) {
570
+ assertDefinitionKeyAvailable(type, key, "query");
571
+ const queryFunction = queryFunctions[key];
572
+ type.queryFunctions[key] = queryFunction;
573
+ Object.defineProperty(this.prototype, key, { value: buildQueryMethod(queryFunction) });
574
+ }
575
+ return this;
576
+ };
577
+ SigmaTypeBuilder.observe = function(listener, options) {
578
+ type.observeFunctions.push(listener);
579
+ if (options?.patches) type.patchesEnabled = true;
580
+ return this;
581
+ };
582
+ SigmaTypeBuilder.actions = function(actionFunctions) {
583
+ for (const key in actionFunctions) {
584
+ assertDefinitionKeyAvailable(type, key, "action");
585
+ const actionFunction = actionFunctions[key];
586
+ type.actionFunctions[key] = actionFunction;
587
+ Object.defineProperty(this.prototype, key, { value: buildActionMethod(key, actionFunction) });
588
+ }
589
+ return this;
590
+ };
591
+ SigmaTypeBuilder.setup = function(setup) {
592
+ type.setupFunctions.push(setup);
593
+ return this;
594
+ };
595
+ return SigmaTypeBuilder;
596
+ }
597
+ };
598
+ //#endregion
599
+ //#region src/listener.ts
600
+ /** Adds an event listener and returns a cleanup function that removes it. */
601
+ function listen(target, name, fn) {
602
+ const listener = isSigmaState(target) ? (event) => fn(event.detail) : fn;
603
+ target.addEventListener(name, listener);
604
+ return () => {
605
+ target.removeEventListener(name, listener);
606
+ };
276
607
  }
277
- /**
278
- * Subscribe to events from an `EventTarget` or managed state inside `useEffect`.
279
- *
280
- * The listener is kept fresh automatically, so a dependency array is not part
281
- * of this API. Pass `null` to disable the subscription temporarily.
282
- *
283
- * For managed-state events, your listener receives the emitted argument
284
- * directly, or no argument at all.
285
- */
286
- function useEventTarget(target, name, listener) {
287
- listener = useStableCallback(listener);
608
+ //#endregion
609
+ //#region src/hooks.ts
610
+ /** Creates one sigma-state instance for a component and manages its setup cleanup. */
611
+ function useSigma(create, setupArgs) {
612
+ const sigmaState = useState(create)[0];
613
+ if (shouldSetup(sigmaState)) {
614
+ const args = setupArgs ?? [];
615
+ useEffect(() => sigmaState.setup(...args), [sigmaState, ...args]);
616
+ }
617
+ return sigmaState;
618
+ }
619
+ /** Attaches an event listener in a component and cleans it up when dependencies change. */
620
+ function useListener(target, name, listener) {
621
+ const listenerRef = useRef(listener);
622
+ listenerRef.current = listener;
288
623
  useEffect(() => {
289
624
  if (!target) return;
290
- if (isManagedState(target)) return target.on(name, listener);
291
- target.addEventListener(name, listener);
292
- return () => target.removeEventListener(name, listener);
625
+ return listen(target, name, ((event) => listenerRef.current(event)));
293
626
  }, [target, name]);
294
627
  }
295
- function useStableCallback(callback) {
296
- const ref = useRef(callback);
297
- ref.current = callback;
298
- return useCallback((...params) => (0, ref.current)(...params), []);
299
- }
300
628
  //#endregion
301
- export { batch, computed, defineManagedState, isManagedState, query, untracked, useEventTarget, useManagedState, useSubscribe };
629
+ export { SigmaType, action, batch, computed, effect, freeze, immerable, isSigmaState, listen, query, replaceState, setAutoFreeze, snapshot, untracked, useListener, useSigma };