preact-sigma 2.2.3 → 2.3.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/README.md CHANGED
@@ -43,5 +43,5 @@ console.log(counter.doubled); // 2
43
43
  - Concepts, lifecycle, invariants, and API selection live in [`docs/context.md`](./docs/context.md).
44
44
  - Quick-start usage lives in [`examples/basic-counter.ts`](./examples/basic-counter.ts).
45
45
  - An advanced end-to-end example lives in [`examples/command-palette.tsx`](./examples/command-palette.tsx).
46
- - Focused examples for non-obvious APIs live in [`examples/async-commit.ts`](./examples/async-commit.ts), [`examples/observe-and-restore.ts`](./examples/observe-and-restore.ts), [`examples/signal-access.ts`](./examples/signal-access.ts), and [`examples/setup-act.ts`](./examples/setup-act.ts).
46
+ - Focused examples for non-obvious APIs live in [`examples/async-commit.ts`](./examples/async-commit.ts), [`examples/observe-and-restore.ts`](./examples/observe-and-restore.ts), [`examples/setup-act.ts`](./examples/setup-act.ts), [`examples/sigma-target.ts`](./examples/sigma-target.ts), and [`examples/signal-access.ts`](./examples/signal-access.ts).
47
47
  - Exact exported signatures live in `dist/index.d.mts` after `pnpm build`.
package/dist/index.d.mts CHANGED
@@ -160,7 +160,7 @@ declare function replaceState<T extends AnySigmaState>(publicInstance: T, nextSt
160
160
  //#endregion
161
161
  //#region src/framework.d.ts
162
162
  /** Checks whether a value is an instance created by a configured sigma type. */
163
- declare function isSigmaState(value: unknown): value is AnySigmaState;
163
+ declare function isSigmaState(value: object): value is AnySigmaState;
164
164
  /**
165
165
  * Creates a standalone tracked query helper with the same signature as `fn`.
166
166
  *
@@ -272,22 +272,45 @@ interface SigmaType<TState extends AnyState, TEvents extends AnyEvents, TDefault
272
272
  }
273
273
  //#endregion
274
274
  //#region src/listener.d.ts
275
- type TryGet<T, K extends PropertyKey, TCatch = never> = K extends keyof T ? T[K] : TCatch;
276
- type TypedEventListener<TEventMap, TEvent extends string, TCurrentTarget extends EventTarget> = ((event: TryGet<TEventMap, string extends TEvent ? keyof TEventMap : TEvent, CustomEvent> & {
277
- readonly currentTarget: TCurrentTarget;
278
- }) => void) & {
279
- __eventType?: string extends TEvent ? keyof TEventMap : TEvent;
280
- };
275
+ type InferEventMap<TTarget extends EventTarget> = TTarget extends {
276
+ [sigmaEventsBrand]: infer TEvents extends AnyEvents;
277
+ } ? TEvents : TTarget extends Window ? WindowEventMap : TTarget extends Document ? DocumentEventMap : TTarget extends HTMLBodyElement ? HTMLBodyElementEventMap : TTarget extends HTMLMediaElement ? HTMLMediaElementEventMap : TTarget extends HTMLElement ? HTMLElementEventMap : TTarget extends SVGSVGElement ? SVGSVGElementEventMap : TTarget extends SVGElement ? SVGElementEventMap : never;
278
+ type InferListenerArgs<TEvents extends object, TTarget extends EventTarget, TEvent extends string> = [(TEvent extends keyof TEvents ? TEvents[TEvent] : never) extends infer TPayload ? TTarget extends {
279
+ [sigmaEventsBrand]: TEvents;
280
+ } ? [TPayload] extends [never] ? never : [TPayload] extends [void] ? undefined : TPayload : ([TPayload] extends [never] ? CustomEvent : Extract<TPayload, Event>) & {
281
+ readonly currentTarget: TTarget;
282
+ } : never];
283
+ /** Infers the listener callback shape for a target and event name. Sigma states receive payloads directly, while DOM targets receive typed events. */
284
+ type InferListener<TTarget extends EventTarget, TEvent extends string = string> = InferEventMap<TTarget> extends infer TEvents extends object ? ((...args: InferListenerArgs<TEvents, TTarget, TEvent>) => void) & {
285
+ __eventType?: TEvent;
286
+ } : never;
281
287
  /** Infers the event names accepted by `listen(...)` or `useListener(...)` for a target. */
282
288
  type InferEventType<TTarget extends EventTarget> = (InferListener<TTarget> extends {
283
289
  __eventType?: infer TEvent;
284
290
  } ? string & TEvent : never) | (string & {});
285
- /** Infers the listener callback shape for a target and event name. Sigma states receive payloads directly, while DOM targets receive typed events. */
286
- type InferListener<TTarget extends EventTarget, TEvent extends string = string> = TTarget extends AnySigmaStateWithEvents<infer TEvents> ? TEvent extends keyof TEvents ? (event: TEvents[TEvent]) => void : never : TTarget extends Window ? TypedEventListener<WindowEventMap, TEvent, TTarget> : TTarget extends Document ? TypedEventListener<DocumentEventMap, TEvent, TTarget> : TTarget extends HTMLBodyElement ? TypedEventListener<HTMLBodyElementEventMap, TEvent, TTarget> : TTarget extends HTMLMediaElement ? TypedEventListener<HTMLMediaElementEventMap, TEvent, TTarget> : TTarget extends HTMLElement ? TypedEventListener<HTMLElementEventMap, TEvent, TTarget> : TTarget extends SVGSVGElement ? TypedEventListener<SVGSVGElementEventMap, TEvent, TTarget> : TTarget extends SVGElement ? TypedEventListener<SVGElementEventMap, TEvent, TTarget> : (event: Event & {
287
- readonly currentTarget: TTarget;
288
- }) => void;
291
+ /**
292
+ * A standalone typed event hub with `emit(...)` and `on(...)` methods and full
293
+ * `EventTarget`, `listen(...)`, and `useListener(...)` compatibility.
294
+ */
295
+ declare class SigmaTarget<TEvents extends AnyEvents = {}> extends EventTarget {
296
+ readonly [sigmaEventsBrand]: TEvents;
297
+ /**
298
+ * Emits a typed event from the hub.
299
+ *
300
+ * Void events dispatch a plain `Event`. Payload events dispatch a
301
+ * `CustomEvent` whose `detail` holds the payload.
302
+ */
303
+ emit<TEvent extends string & keyof TEvents>(name: TEvent, ...args: [TEvents[TEvent]] extends [void] ? [] : [payload: TEvents[TEvent]]): void;
304
+ /**
305
+ * Registers a typed event listener and returns an unsubscribe function.
306
+ *
307
+ * Payload events pass their payload directly to the listener. Void events
308
+ * call the listener with no arguments.
309
+ */
310
+ on<TEvent extends string & keyof TEvents>(name: TEvent, listener: (...args: InferListenerArgs<TEvents, this, TEvent>) => void): () => void;
311
+ }
289
312
  /** Adds a listener to a sigma state or DOM target and returns a cleanup function that removes it. */
290
- declare function listen<TTarget extends EventTarget, TEvent extends InferEventType<TTarget>>(target: TTarget, name: TEvent, fn: InferListener<TTarget, TEvent>): () => void;
313
+ declare function listen<TTarget extends EventTarget, TEvent extends InferEventType<TTarget>>(target: TTarget, name: TEvent, listener: InferListener<TTarget, TEvent>): () => void;
291
314
  //#endregion
292
315
  //#region src/hooks.d.ts
293
316
  /**
@@ -306,4 +329,4 @@ declare function useSigma<T extends AnySigmaState>(create: () => T, setupArgs?:
306
329
  */
307
330
  declare function useListener<TTarget extends EventTarget | AnySigmaState, TEvent extends InferEventType<TTarget>>(target: TTarget | null, name: TEvent, listener: InferListener<TTarget, TEvent>): void;
308
331
  //#endregion
309
- export { type AnyDefaultState, type AnyEvents, type AnyResource, type AnySigmaState, type AnySigmaStateWithEvents, type AnyState, InferEventType, InferListener, type InferSetupArgs, type SigmaObserveChange, type SigmaObserveOptions, type SigmaRef, type SigmaState, SigmaType, action, batch, computed, effect, freeze, immerable, isSigmaState, listen, query, replaceState, setAutoFreeze, snapshot, untracked, useListener, useSigma };
332
+ export { type AnyDefaultState, type AnyEvents, type AnyResource, type AnySigmaState, type AnySigmaStateWithEvents, type AnyState, InferEventType, InferListener, type InferSetupArgs, type SigmaObserveChange, type SigmaObserveOptions, type SigmaRef, type SigmaState, SigmaTarget, SigmaType, action, batch, computed, effect, freeze, immerable, isSigmaState, listen, query, replaceState, setAutoFreeze, snapshot, untracked, useListener, useSigma };
package/dist/index.mjs CHANGED
@@ -541,7 +541,7 @@ Object.defineProperty(Sigma.prototype, sigmaStateBrand, { value: true });
541
541
  //#region src/framework.ts
542
542
  /** Checks whether a value is an instance created by a configured sigma type. */
543
543
  function isSigmaState(value) {
544
- return Boolean(value && typeof value === "object" && value[sigmaStateBrand]);
544
+ return Boolean(value[sigmaStateBrand]);
545
545
  }
546
546
  /**
547
547
  * Creates a standalone tracked query helper with the same signature as `fn`.
@@ -629,12 +629,40 @@ var SigmaType = class extends Function {
629
629
  };
630
630
  //#endregion
631
631
  //#region src/listener.ts
632
+ /**
633
+ * A standalone typed event hub with `emit(...)` and `on(...)` methods and full
634
+ * `EventTarget`, `listen(...)`, and `useListener(...)` compatibility.
635
+ */
636
+ var SigmaTarget = class extends EventTarget {
637
+ /**
638
+ * Emits a typed event from the hub.
639
+ *
640
+ * Void events dispatch a plain `Event`. Payload events dispatch a
641
+ * `CustomEvent` whose `detail` holds the payload.
642
+ */
643
+ emit(name, ...args) {
644
+ this.dispatchEvent(args.length === 0 ? new Event(name) : new CustomEvent(name, { detail: args[0] }));
645
+ }
646
+ /**
647
+ * Registers a typed event listener and returns an unsubscribe function.
648
+ *
649
+ * Payload events pass their payload directly to the listener. Void events
650
+ * call the listener with no arguments.
651
+ */
652
+ on(name, listener) {
653
+ const adapter = (event) => listener(event.detail);
654
+ this.addEventListener(name, adapter);
655
+ return () => {
656
+ this.removeEventListener(name, adapter);
657
+ };
658
+ }
659
+ };
632
660
  /** Adds a listener to a sigma state or DOM target and returns a cleanup function that removes it. */
633
- function listen(target, name, fn) {
634
- const listener = isSigmaState(target) ? (event) => fn(event.detail) : fn;
635
- target.addEventListener(name, listener);
661
+ function listen(target, name, listener) {
662
+ const adapter = isSigmaState(target) || target instanceof SigmaTarget ? (event) => listener(event.detail) : listener;
663
+ target.addEventListener(name, adapter);
636
664
  return () => {
637
- target.removeEventListener(name, listener);
665
+ target.removeEventListener(name, adapter);
638
666
  };
639
667
  }
640
668
  //#endregion
@@ -669,4 +697,4 @@ function useListener(target, name, listener) {
669
697
  }, [target, name]);
670
698
  }
671
699
  //#endregion
672
- export { SigmaType, action, batch, computed, effect, freeze, immerable, isSigmaState, listen, query, replaceState, setAutoFreeze, snapshot, untracked, useListener, useSigma };
700
+ export { SigmaTarget, SigmaType, action, batch, computed, effect, freeze, immerable, isSigmaState, listen, query, replaceState, setAutoFreeze, snapshot, untracked, useListener, useSigma };
package/docs/context.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Overview
2
2
 
3
- `preact-sigma` builds reusable state models from one definition. A configured `SigmaType` owns top-level state, derived reads, writes, setup handlers, and typed events. Each top-level state property is exposed as a reactive public property backed by its own Preact signal, while actions use Immer-style mutation semantics to publish committed state.
3
+ `preact-sigma` builds reusable state models from one definition. A configured `SigmaType` owns top-level state, derived reads, writes, setup handlers, and typed events. Each top-level state property is exposed as a reactive public property backed by its own Preact signal, while actions use Immer-style mutation semantics to publish committed state. For event-only flows, `SigmaTarget` provides a standalone typed event hub with no managed state.
4
4
 
5
5
  # When to Use
6
6
 
@@ -21,6 +21,7 @@
21
21
 
22
22
  - Sigma type: the builder returned by `new SigmaType<TState, TEvents>()`. After configuration, it is also the constructor for instances.
23
23
  - Sigma state: an instance created from a configured sigma type.
24
+ - Sigma target: a standalone typed event hub created with `new SigmaTarget<TEvents>()` when you need typed events without managed state.
24
25
  - State property: a top-level key from `TState`. Each one becomes a readonly reactive public property and gets its own signal.
25
26
  - Computed: an argument-free derived getter declared with `.computed(...)`.
26
27
  - Query: a reactive read that accepts arguments, declared with `.queries(...)` or built locally with `query(fn)`.
@@ -51,6 +52,7 @@
51
52
  - Own timers, listeners, subscriptions, or nested setup: `.setup(...)`
52
53
  - Use a sigma state inside a component: `useSigma(...)`
53
54
  - 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(...)`
54
56
  - Subscribe outside components: `.on(...)` or `listen(...)`
55
57
  - Read or restore committed top-level state: `snapshot(...)` and `replaceState(...)`
56
58
 
@@ -0,0 +1,26 @@
1
+ import { listen, SigmaTarget } from "preact-sigma";
2
+
3
+ const notifications = new SigmaTarget<{
4
+ saved: {
5
+ id: string;
6
+ title: string;
7
+ };
8
+ reset: void;
9
+ }>();
10
+
11
+ const stopSaved = notifications.on("saved", ({ id, title }) => {
12
+ console.log(`Saved ${id}: ${title}`);
13
+ });
14
+
15
+ const stopReset = listen(notifications, "reset", () => {
16
+ console.log("Reset");
17
+ });
18
+
19
+ notifications.emit("saved", {
20
+ id: "note-1",
21
+ title: "Draft post",
22
+ });
23
+ notifications.emit("reset");
24
+
25
+ stopSaved();
26
+ stopReset();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "preact-sigma",
3
- "version": "2.2.3",
3
+ "version": "2.3.0",
4
4
  "keywords": [],
5
5
  "license": "MIT",
6
6
  "author": "Alec Larson",