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/dist/persist.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { c as sigma } from "./runtime-nX4Aygb8.mjs";
1
+ import { o as sigma, s as isPlainObject } from "./sigma-CJibGQ6g.mjs";
2
2
  //#region src/persist.ts
3
3
  function createIdentityCodec() {
4
4
  return {
@@ -11,12 +11,31 @@ function createIdentityCodec() {
11
11
  }
12
12
  };
13
13
  }
14
+ function createPickCodec(keys) {
15
+ return {
16
+ version: 1,
17
+ encode(state) {
18
+ const stored = {};
19
+ for (const key of keys) stored[key] = state[key];
20
+ return stored;
21
+ },
22
+ decode(stored, context) {
23
+ if (!isPlainObject(stored)) throw new Error("[preact-sigma/persist] pick requires a plain object payload");
24
+ const partialStored = stored;
25
+ const restored = { ...context.baseState };
26
+ for (const key of keys) if (key in partialStored) restored[key] = partialStored[key];
27
+ return restored;
28
+ }
29
+ };
30
+ }
14
31
  function getCodec(options) {
15
- return options.codec ?? createIdentityCodec();
32
+ if (options.codec) return options.codec;
33
+ if (options.pick) return createPickCodec(options.pick);
34
+ return createIdentityCodec();
16
35
  }
17
36
  function applyRecord(instance, key, record, codec) {
18
37
  if (!record) return { status: "missing" };
19
- const baseState = sigma.getState(instance);
38
+ const baseState = sigma.captureState(instance);
20
39
  const nextState = codec.decode(record.value, {
21
40
  baseState,
22
41
  key,
@@ -29,46 +48,21 @@ function applyRecord(instance, key, record, codec) {
29
48
  storedVersion: record.version
30
49
  };
31
50
  }
32
- function isPlainObject(value) {
33
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
34
- const prototype = Object.getPrototypeOf(value);
35
- return prototype === Object.prototype || prototype === null;
36
- }
37
- /** Restores committed state from a persisted record through an async store. */
38
- async function restoreState(instance, options) {
51
+ async function restore(instance, options) {
39
52
  const codec = getCodec(options);
40
- const record = await options.store.read(options.key);
53
+ const record = await options.store.get(options.key);
41
54
  return applyRecord(instance, options.key, record, codec);
42
55
  }
43
- /** Restores committed state from a persisted record through a sync store. */
44
- function restoreStateSync(instance, options) {
56
+ function restoreSync(instance, options) {
45
57
  const codec = getCodec(options);
46
- const record = options.store.read(options.key);
58
+ const record = options.store.get(options.key);
47
59
  return applyRecord(instance, options.key, record, codec);
48
60
  }
49
- /** Creates a codec that persists selected top-level state keys and reconstructs a full snapshot on decode. */
50
- function pickStateCodec(keys) {
51
- return {
52
- version: 1,
53
- encode(state) {
54
- const stored = {};
55
- for (const key of keys) stored[key] = state[key];
56
- return stored;
57
- },
58
- decode(stored, context) {
59
- if (!isPlainObject(stored)) throw new Error("[preact-sigma/persist] pickStateCodec() requires a plain object payload");
60
- const partialStored = stored;
61
- const restored = { ...context.baseState };
62
- for (const key of keys) if (key in partialStored) restored[key] = partialStored[key];
63
- return restored;
64
- }
65
- };
66
- }
67
- /** Persists future committed state changes for one sigma-state instance. */
68
- function persistState(instance, options) {
61
+ function persist(instance, options) {
69
62
  const codec = getCodec(options);
70
63
  const schedule = options.schedule ?? "microtask";
71
64
  const key = options.key;
65
+ const store = options.store;
72
66
  let stopped = false;
73
67
  let suspended = false;
74
68
  let hasPendingState = false;
@@ -124,7 +118,7 @@ function persistState(instance, options) {
124
118
  }, schedule.debounceMs);
125
119
  };
126
120
  const queueStateWrite = () => {
127
- pendingState = sigma.getState(instance);
121
+ pendingState = sigma.captureState(instance);
128
122
  hasPendingState = true;
129
123
  scheduleWrite();
130
124
  };
@@ -135,7 +129,7 @@ function persistState(instance, options) {
135
129
  while (hasPendingState && !stopped && !suspended) {
136
130
  const state = pendingState;
137
131
  hasPendingState = false;
138
- await options.store.write(key, createRecord(state));
132
+ await store.set(key, createRecord(state));
139
133
  }
140
134
  })();
141
135
  try {
@@ -163,7 +157,7 @@ function persistState(instance, options) {
163
157
  pendingState = void 0;
164
158
  try {
165
159
  await runningWrite;
166
- await options.store.remove(key);
160
+ await store.delete(key);
167
161
  } finally {
168
162
  suspended = false;
169
163
  if (hasPendingState && !stopped) scheduleWrite();
@@ -184,13 +178,12 @@ function persistState(instance, options) {
184
178
  }
185
179
  };
186
180
  }
187
- /** Restores state, then begins persisting future committed changes. */
188
- function bindPersistence(instance, options) {
181
+ function hydrate(instance, options) {
189
182
  let stopped = false;
190
183
  let handle;
191
184
  const restored = (async () => {
192
- const result = await restoreState(instance, options);
193
- if (!stopped) handle = persistState(instance, options);
185
+ const result = await restore(instance, options);
186
+ if (!stopped) handle = persist(instance, options);
194
187
  return result;
195
188
  })();
196
189
  return {
@@ -203,14 +196,14 @@ function bindPersistence(instance, options) {
203
196
  try {
204
197
  await restored;
205
198
  } catch {
206
- await options.store.remove(options.key);
199
+ await options.store.delete(options.key);
207
200
  return;
208
201
  }
209
202
  if (handle) {
210
203
  await handle.clear();
211
204
  return;
212
205
  }
213
- await options.store.remove(options.key);
206
+ await options.store.delete(options.key);
214
207
  },
215
208
  async stop() {
216
209
  if (stopped) {
@@ -227,10 +220,9 @@ function bindPersistence(instance, options) {
227
220
  }
228
221
  };
229
222
  }
230
- /** Restores state synchronously, then begins persisting future committed changes. */
231
- function bindPersistenceSync(instance, options) {
232
- const restored = restoreStateSync(instance, options);
233
- const handle = persistState(instance, options);
223
+ function hydrateSync(instance, options) {
224
+ const restored = restoreSync(instance, options);
225
+ const handle = persist(instance, options);
234
226
  return {
235
227
  restored,
236
228
  clear() {
@@ -245,4 +237,4 @@ function bindPersistenceSync(instance, options) {
245
237
  };
246
238
  }
247
239
  //#endregion
248
- export { bindPersistence, bindPersistenceSync, persistState, pickStateCodec, restoreState, restoreStateSync };
240
+ export { hydrate, hydrateSync, persist, restore, restoreSync };
@@ -0,0 +1,383 @@
1
+ import { action, batch, computed, signal } from "@preact/signals";
2
+ import { createDraft as createDraft$1, finishDraft, freeze, isDraft, isDraftable, original, setAutoFreeze as setAutoFreeze$1 } from "immer";
3
+ //#region src/internal/symbols.ts
4
+ const instanceSymbol = Symbol.for("sigma:instance");
5
+ const listenersSymbol = Symbol.for("sigma:listeners");
6
+ const snapshotSymbol = Symbol.for("sigma:snapshot");
7
+ //#endregion
8
+ //#region src/internal/listener.ts
9
+ /** Listener registry used by sigma targets and sigma states for typed event delivery. */
10
+ var SigmaListenerMap = class extends Map {
11
+ /** Delivers one event payload to the current listeners for `name`. */
12
+ emit(name, detail) {
13
+ const listeners = this.get(name);
14
+ if (!listeners?.size) return;
15
+ for (const listener of [...listeners]) listener(detail);
16
+ }
17
+ /** Adds one listener for `name`, creating the listener set on first use. */
18
+ addListener(name, listener) {
19
+ let listeners = this.get(name);
20
+ if (!listeners) {
21
+ listeners = /* @__PURE__ */ new Set();
22
+ this.set(name, listeners);
23
+ }
24
+ listeners.add(listener);
25
+ }
26
+ /** Removes one listener for `name` and prunes the empty listener set. */
27
+ removeListener(name, listener) {
28
+ const listeners = this.get(name);
29
+ if (!listeners) return;
30
+ listeners.delete(listener);
31
+ if (!listeners.size) this.delete(name);
32
+ }
33
+ };
34
+ //#endregion
35
+ //#region src/internal/utils.ts
36
+ function isPlainObject(value) {
37
+ if (typeof value !== "object" || value === null) return false;
38
+ const prototype = Object.getPrototypeOf(value);
39
+ return prototype === Object.prototype || prototype === null;
40
+ }
41
+ function isPromiseLike(value) {
42
+ return value != null && typeof value.then === "function";
43
+ }
44
+ //#endregion
45
+ //#region src/sigma.ts
46
+ let autoFreezeEnabled = true;
47
+ /**
48
+ * Configures Immer auto-freezing for values published through sigma state.
49
+ *
50
+ * Auto-freezing is enabled by default, so draftable public values are deeply frozen after publish.
51
+ */
52
+ function setAutoFreeze(autoFreeze) {
53
+ setAutoFreeze$1(autoFreeze);
54
+ autoFreezeEnabled = autoFreeze;
55
+ }
56
+ const signalSuffix = "$";
57
+ const changeListenersMap = /* @__PURE__ */ new WeakMap();
58
+ const patchListeners = /* @__PURE__ */ new WeakSet();
59
+ const initializedPrototypes = /* @__PURE__ */ new WeakSet();
60
+ const queries = /* @__PURE__ */ new WeakSet();
61
+ const emptySentinel = {};
62
+ let activeInstance = null;
63
+ let activeDraft;
64
+ let activeDerivedReadDepth = 0;
65
+ function ensureActiveInstance(instance) {
66
+ if (!activeInstance) {
67
+ activeInstance = instance;
68
+ return false;
69
+ }
70
+ if (instance !== activeInstance) throw new Error("Draft was not committed before an external action was invoked.");
71
+ return true;
72
+ }
73
+ function clearActiveInstance() {
74
+ activeInstance = null;
75
+ activeDraft = null;
76
+ }
77
+ function isStateKey(instance, key) {
78
+ return Object.hasOwn(instance, key + signalSuffix);
79
+ }
80
+ function getStateSignal(instance, key) {
81
+ return instance[key + signalSuffix];
82
+ }
83
+ function createSnapshot(instance) {
84
+ if (instance[snapshotSymbol]) return instance[snapshotSymbol];
85
+ const state = {};
86
+ for (const key in instance) if (isStateKey(instance, key)) state[key] = instance[key];
87
+ return state;
88
+ }
89
+ function createDraft(instance) {
90
+ return createDraft$1(createSnapshot(instance));
91
+ }
92
+ function runDerivedRead(callback) {
93
+ activeDerivedReadDepth += 1;
94
+ try {
95
+ return callback();
96
+ } finally {
97
+ activeDerivedReadDepth -= 1;
98
+ }
99
+ }
100
+ function assertActionResult(key, result) {
101
+ if (isDraft(result)) throw new Error(`[preact-sigma] Action named "${key}" returned an active draft.`);
102
+ }
103
+ function hasPatchListeners(instance) {
104
+ const listeners = changeListenersMap.get(instance);
105
+ if (!listeners) return false;
106
+ for (const sub of listeners) if (patchListeners.has(sub)) return true;
107
+ return false;
108
+ }
109
+ function publishState(instance, nextState, baseState, patches, inversePatches) {
110
+ instance[snapshotSymbol] = nextState;
111
+ batch(() => {
112
+ const missingKeys = new Set(Object.keys(baseState));
113
+ for (const key in nextState) {
114
+ const nextValue = nextState[key];
115
+ if (autoFreezeEnabled) freeze(nextValue, true);
116
+ const signal = getStateSignal(instance, key);
117
+ if (signal) signal.value = nextValue;
118
+ else defineSignalProperty(instance, key, nextValue);
119
+ missingKeys.delete(key);
120
+ }
121
+ for (const key of missingKeys) {
122
+ const signal = getStateSignal(instance, key);
123
+ if (signal) signal.value = void 0;
124
+ }
125
+ });
126
+ changeListenersMap.get(instance)?.forEach((listener) => {
127
+ listener(nextState, baseState, patches, inversePatches);
128
+ });
129
+ }
130
+ function createActionContext(instance) {
131
+ return new Proxy(instance, {
132
+ get(target, key, receiver) {
133
+ if (typeof key === "string" && isStateKey(target, key)) {
134
+ if (activeDraft) {
135
+ ensureActiveInstance(target);
136
+ return activeDraft[key];
137
+ }
138
+ const { value } = getStateSignal(target, key);
139
+ if (isDraftable(value)) {
140
+ activeDraft = createDraft(instance);
141
+ return activeDraft[key];
142
+ }
143
+ return value;
144
+ }
145
+ if (key === instanceSymbol) return instance;
146
+ return Reflect.get(target, key, receiver);
147
+ },
148
+ set(target, key, value) {
149
+ if (typeof key === "string" && isStateKey(target, key)) {
150
+ ensureActiveInstance(instance);
151
+ activeDraft ??= createDraft(instance);
152
+ activeDraft[key] = value;
153
+ return true;
154
+ }
155
+ return Reflect.set(target, key, value);
156
+ }
157
+ });
158
+ }
159
+ function getActionInstance(context) {
160
+ return context[instanceSymbol];
161
+ }
162
+ function ensureDraftCommitted(instance) {
163
+ if (activeInstance && instance !== activeInstance) throw new Error("Draft was not committed before an external action was invoked.");
164
+ activeInstance = null;
165
+ if (!activeDraft) return false;
166
+ const draft = activeDraft;
167
+ activeDraft = null;
168
+ let patches;
169
+ let inversePatches;
170
+ let patchListener;
171
+ if (hasPatchListeners(instance)) patchListener = (nextPatches, nextInversePatches) => {
172
+ patches = nextPatches;
173
+ inversePatches = nextInversePatches;
174
+ };
175
+ const baseState = original(draft);
176
+ const nextState = finishDraft(draft, patchListener);
177
+ const changed = baseState !== nextState;
178
+ if (changed) publishState(instance, nextState, baseState, patches, inversePatches);
179
+ return changed;
180
+ }
181
+ function initializePrototype(prototype) {
182
+ const descriptors = Object.getOwnPropertyDescriptors(prototype);
183
+ for (const key in descriptors) {
184
+ if (key === "constructor" || key === "onSetup") continue;
185
+ const { get, value } = descriptors[key];
186
+ if (get) descriptors[key].get = function() {
187
+ const instance = getActionInstance(this);
188
+ return (instance[key + signalSuffix] ??= computed(() => runDerivedRead(() => get.call(instance)))).value;
189
+ };
190
+ else if (typeof value === "function" && !queries.has(value)) {
191
+ const actionFn = action(value);
192
+ descriptors[key].value = function(...args) {
193
+ if (activeDerivedReadDepth) throw new Error("[preact-sigma] Computeds and queries cannot call actions.");
194
+ const instance = getActionInstance(this);
195
+ if (ensureActiveInstance(instance)) {
196
+ const result = value.apply(this, args);
197
+ assertActionResult(key, result);
198
+ return result;
199
+ }
200
+ let result;
201
+ try {
202
+ const actionContext = createActionContext(instance);
203
+ result = actionFn.apply(actionContext, args);
204
+ assertActionResult(key, result);
205
+ } catch (error) {
206
+ clearActiveInstance();
207
+ throw error;
208
+ }
209
+ const changed = ensureDraftCommitted(instance);
210
+ if (isPromiseLike(result)) {
211
+ if (changed) throw new Error(`[preact-sigma] Action named "${key}" forgot to commit() its draft before returning a promise.`);
212
+ const onResolveAsyncAction = (promiseResult) => {
213
+ if (activeDraft && instance === activeInstance) {
214
+ if (ensureDraftCommitted(instance)) throw new Error(`[preact-sigma] Action named "${key}" forgot to commit() its draft before its promise resolved.`);
215
+ }
216
+ return promiseResult;
217
+ };
218
+ return result.then(onResolveAsyncAction);
219
+ }
220
+ return result;
221
+ };
222
+ }
223
+ }
224
+ Object.defineProperties(prototype, descriptors);
225
+ }
226
+ function initializeType(type) {
227
+ for (let prototype = type.prototype; prototype && prototype !== Sigma.prototype && prototype !== SigmaTarget.prototype; prototype = Object.getPrototypeOf(prototype)) {
228
+ if (initializedPrototypes.has(prototype)) break;
229
+ initializePrototype(prototype);
230
+ initializedPrototypes.add(prototype);
231
+ }
232
+ }
233
+ function disposeCleanupResource(resource) {
234
+ if (typeof resource === "function") resource();
235
+ else if ("dispose" in resource) resource.dispose();
236
+ else resource[Symbol.dispose]();
237
+ }
238
+ function act(fn) {
239
+ const instance = getActionInstance(this);
240
+ if (instance !== this) throw new Error("Cannot act() from inside an action.");
241
+ ensureActiveInstance(instance);
242
+ try {
243
+ const context = createActionContext(instance);
244
+ if (isPromiseLike(action(fn).call(context))) throw new Error("[preact-sigma] act() callbacks must be synchronous");
245
+ } catch (error) {
246
+ clearActiveInstance();
247
+ throw error;
248
+ }
249
+ ensureDraftCommitted(instance);
250
+ }
251
+ function defineSignalProperty(instance, key, value) {
252
+ Object.defineProperty(instance, key + signalSuffix, { value: signal(value) });
253
+ if (!Object.hasOwn(instance.constructor.prototype, key)) Object.defineProperty(instance.constructor.prototype, key, {
254
+ get() {
255
+ return this[key + signalSuffix].value;
256
+ },
257
+ enumerable: true
258
+ });
259
+ }
260
+ /**
261
+ * Base class for signal-backed state models.
262
+ *
263
+ * `TState` is the source of typing for top-level state keys, subscriptions, signals, and replacement snapshots.
264
+ * Merge a same-named interface with the class when direct property reads should be typed on the instance.
265
+ */
266
+ var Sigma = class {
267
+ get [instanceSymbol]() {
268
+ return this;
269
+ }
270
+ constructor(initialState) {
271
+ initializeType(this.constructor);
272
+ if (initialState === emptySentinel) return;
273
+ for (const key in initialState) {
274
+ const initialValue = initialState[key];
275
+ if (autoFreezeEnabled) freeze(initialValue, true);
276
+ defineSignalProperty(this, key, initialValue);
277
+ }
278
+ }
279
+ /** Runs `onSetup(...)` and returns a cleanup that disposes returned resources in reverse order. */
280
+ setup(...args) {
281
+ const setupContext = new Proxy(this, { get(target, key, receiver) {
282
+ if (key === instanceSymbol) return target;
283
+ if (key === "act") return act.bind(target);
284
+ return Reflect.get(target, key, receiver);
285
+ } });
286
+ const resources = this.onSetup.apply(setupContext, args);
287
+ return () => {
288
+ for (let i = resources.length - 1; i >= 0; i--) disposeCleanupResource(resources[i]);
289
+ };
290
+ }
291
+ /**
292
+ * Publishes the current action draft.
293
+ *
294
+ * Use this before unpublished changes cross an async, event, or external-action boundary.
295
+ * A callback runs after publish in an action context.
296
+ */
297
+ commit(callback) {
298
+ const instance = getActionInstance(this);
299
+ if (instance === this) throw new Error("Cannot commit() from outside an action.");
300
+ ensureDraftCommitted(instance);
301
+ if (callback) {
302
+ const context = new Proxy(instance, { get(target, key, receiver) {
303
+ if (key === instanceSymbol) return instance;
304
+ return Reflect.get(target, key, receiver);
305
+ } });
306
+ return callback.call(context);
307
+ }
308
+ }
309
+ /** Runs a synchronous setup-owned callback with action semantics from an `onSetup(...)` context. */
310
+ act(fn) {
311
+ throw new Error("Cannot act() from outside an onSetup() context.");
312
+ }
313
+ };
314
+ /** Casts a sigma instance to its readonly public consumer view. */
315
+ function castProtected(instance) {
316
+ return instance;
317
+ }
318
+ /**
319
+ * Sigma state model that can emit typed events.
320
+ *
321
+ * `TEvents` maps event names to payload types, and `TState` types reactive state.
322
+ */
323
+ var SigmaTarget = class extends Sigma {
324
+ [listenersSymbol] = new SigmaListenerMap();
325
+ constructor(state) {
326
+ super(state ?? emptySentinel);
327
+ }
328
+ /** Emits a typed event from an action after unpublished draft changes are committed. */
329
+ emit(name, ...[detail]) {
330
+ const instance = getActionInstance(this);
331
+ if (instance === this) throw new Error("Cannot emit() from outside an action.");
332
+ if (instance === activeInstance && activeDraft) throw new Error("Cannot emit() until you commit() your draft.");
333
+ this[listenersSymbol].emit(name, detail);
334
+ }
335
+ };
336
+ /** Helpers for observing, accessing, capturing, and replacing committed sigma state. */
337
+ const sigma = /* @__PURE__ */ Object.freeze({
338
+ subscribe: ((instance, keyOrListener, listenerOrOptions) => {
339
+ if (typeof keyOrListener === "string") {
340
+ const signal = getStateSignal(instance, keyOrListener);
341
+ if (!signal) throw new Error(`[preact-sigma] Property named "${keyOrListener}" is not signal-backed.`);
342
+ return signal.subscribe(listenerOrOptions);
343
+ }
344
+ const listener = keyOrListener;
345
+ if (listenerOrOptions?.patches) patchListeners.add(listener);
346
+ let subscriptions = changeListenersMap.get(instance);
347
+ if (!subscriptions) {
348
+ subscriptions = /* @__PURE__ */ new Set();
349
+ changeListenersMap.set(instance, subscriptions);
350
+ }
351
+ subscriptions.add(listener);
352
+ return () => {
353
+ subscriptions.delete(listener);
354
+ if (!subscriptions.size) changeListenersMap.delete(instance);
355
+ };
356
+ }),
357
+ getSignal(instance, key) {
358
+ return getStateSignal(instance, key);
359
+ },
360
+ captureState(instance) {
361
+ return Object.freeze(createSnapshot(instance));
362
+ },
363
+ replaceState(target, nextState) {
364
+ if (!isPlainObject(nextState)) throw new Error("[preact-sigma] replaceState() requires a plain object snapshot");
365
+ if (activeDraft) throw new Error(`[preact-sigma] replaceState() cannot run while an action has unpublished changes.`);
366
+ const instance = getActionInstance(target);
367
+ const baseState = createSnapshot(instance);
368
+ const patches = hasPatchListeners(instance) ? [] : void 0;
369
+ publishState(instance, nextState, baseState, patches, patches ? [] : void 0);
370
+ }
371
+ });
372
+ /** Marks a class method as a committed-state reactive read with arguments instead of an action. */
373
+ function query(method) {
374
+ queries.add(method);
375
+ function queryMethod(...args) {
376
+ const instance = getActionInstance(this);
377
+ return computed(() => runDerivedRead(() => method.apply(instance, args))).value;
378
+ }
379
+ queries.add(queryMethod);
380
+ return queryMethod;
381
+ }
382
+ //#endregion
383
+ export { setAutoFreeze as a, listenersSymbol as c, query as i, SigmaTarget as n, sigma as o, castProtected as r, isPlainObject as s, Sigma as t };