ihsm 0.0.20 → 0.0.22

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 (40) hide show
  1. package/README.md +161 -8
  2. package/lib/cjs/index.d.ts +517 -56
  3. package/lib/cjs/index.js +279 -5
  4. package/lib/cjs/index.js.map +1 -1
  5. package/lib/cjs/internal/defs.private.d.ts +10 -0
  6. package/lib/cjs/internal/dispatch.debug.js +2 -1
  7. package/lib/cjs/internal/dispatch.debug.js.map +1 -1
  8. package/lib/cjs/internal/dispatch.production.js +2 -1
  9. package/lib/cjs/internal/dispatch.production.js.map +1 -1
  10. package/lib/cjs/internal/dispatch.trace.js +8 -1
  11. package/lib/cjs/internal/dispatch.trace.js.map +1 -1
  12. package/lib/cjs/internal/hsm.d.ts +7 -1
  13. package/lib/cjs/internal/hsm.js +36 -3
  14. package/lib/cjs/internal/hsm.js.map +1 -1
  15. package/lib/cjs/internal/lookup.d.ts +15 -0
  16. package/lib/cjs/internal/lookup.js +32 -0
  17. package/lib/cjs/internal/lookup.js.map +1 -0
  18. package/lib/cjs/testing.d.ts +279 -0
  19. package/lib/cjs/testing.js +392 -0
  20. package/lib/cjs/testing.js.map +1 -0
  21. package/lib/esm/index.d.ts +517 -56
  22. package/lib/esm/index.js +275 -5
  23. package/lib/esm/index.js.map +1 -1
  24. package/lib/esm/internal/defs.private.d.ts +10 -0
  25. package/lib/esm/internal/dispatch.debug.js +2 -1
  26. package/lib/esm/internal/dispatch.debug.js.map +1 -1
  27. package/lib/esm/internal/dispatch.production.js +2 -1
  28. package/lib/esm/internal/dispatch.production.js.map +1 -1
  29. package/lib/esm/internal/dispatch.trace.js +8 -1
  30. package/lib/esm/internal/dispatch.trace.js.map +1 -1
  31. package/lib/esm/internal/hsm.d.ts +7 -1
  32. package/lib/esm/internal/hsm.js +36 -3
  33. package/lib/esm/internal/hsm.js.map +1 -1
  34. package/lib/esm/internal/lookup.d.ts +15 -0
  35. package/lib/esm/internal/lookup.js +29 -0
  36. package/lib/esm/internal/lookup.js.map +1 -0
  37. package/lib/esm/testing.d.ts +279 -0
  38. package/lib/esm/testing.js +370 -0
  39. package/lib/esm/testing.js.map +1 -0
  40. package/package.json +23 -5
@@ -199,7 +199,7 @@ export interface Properties<Context, Protocol extends {} | undefined> {
199
199
  /**
200
200
  * Fire-and-forget event posting API available on both {@link Hsm} (clients) and {@link State} (handlers).
201
201
  *
202
- * Events enqueue on the machine mailbox and run **one at a time** — no re-entrancy while a
202
+ * Events enqueue on the actor and run **one at a time, to completion** — no re-entrancy while a
203
203
  * handler is active.
204
204
  *
205
205
  * @category State machine
@@ -208,7 +208,7 @@ export interface Base<Context, Protocol extends {} | undefined> extends Properti
208
208
  /**
209
209
  * Enqueue a **normal-priority** event for later dispatch on the active state.
210
210
  *
211
- * Returns immediately; the handler runs asynchronously when the mailbox reaches this job.
211
+ * Returns immediately; the handler runs asynchronously when the actor reaches this job.
212
212
  * Dispatch walks the **prototype chain** from the current leaf upward until a method named
213
213
  * `eventName` is found.
214
214
  *
@@ -246,34 +246,6 @@ export interface Base<Context, Protocol extends {} | undefined> extends Properti
246
246
  * ```
247
247
  */
248
248
  post<EventName extends keyof Protocol>(eventName: PostedEvent<Protocol, EventName>, ...eventPayload: EventPayload<Protocol, EventName>): void;
249
- /**
250
- * Schedule a normal-priority {@link post} after a wall-clock delay.
251
- *
252
- * Uses `setTimeout` internally; when the timer fires, the event is enqueued like an ordinary
253
- * `post`. Timers are **not** cancelled if the machine transitions or the scheduling handler throws.
254
- *
255
- * @typeParam EventName - Literal key of `Protocol` being scheduled
256
- * @param millis - Delay in milliseconds before enqueueing (≥ 0). Subject to event-loop timer granularity
257
- * @param eventName - Event name (same constraints as {@link post})
258
- * @param eventPayload - Handler arguments tuple (same as {@link post})
259
- *
260
- * @remarks
261
- * Available on {@link State} and {@link Hsm}. Typical pattern: handler schedules reminder,
262
- * client waits with `await sleep(millis); await hsm.sync()`.
263
- *
264
- * Does **not** block the calling handler — returns as soon as the timer is registered.
265
- *
266
- * @example
267
- * ```ts
268
- * scheduleReminder(text: string): void {
269
- * this.deferredPost(50, 'deliver', text);
270
- * }
271
- * deliver(text: string): void {
272
- * this.ctx.message = text;
273
- * }
274
- * ```
275
- */
276
- deferredPost<EventName extends keyof Protocol>(millis: number, eventName: PostedEvent<Protocol, EventName>, ...eventPayload: EventPayload<Protocol, EventName>): void;
277
249
  }
278
250
  /**
279
251
  * Handler-facing API available inside state class methods (`this` / `this.hsm`).
@@ -347,9 +319,9 @@ export interface State<Context, Protocol extends {} | undefined> extends Base<Co
347
319
  /**
348
320
  * Pause the **current handler** without blocking the JavaScript event loop.
349
321
  *
350
- * Returns a Promise resolved after `millis` milliseconds via `setTimeout`. The mailbox
351
- * remains **locked** to this handler until the Promise settles — other `post`/`call` jobs
352
- * queue but do not run.
322
+ * Returns a Promise resolved after `millis` milliseconds via `setTimeout`. The actor
323
+ * remains **locked** to this handler until the Promise settles (run-to-completion) — other
324
+ * `post`/`call` jobs queue but do not run.
353
325
  *
354
326
  * @param millis - Sleep duration in milliseconds (≥ 0)
355
327
  * @returns Promise that resolves (never rejects) when the delay elapses
@@ -367,6 +339,35 @@ export interface State<Context, Protocol extends {} | undefined> extends Base<Co
367
339
  * ```
368
340
  */
369
341
  sleep(millis: number): Promise<void>;
342
+ /**
343
+ * Schedule a normal-priority {@link post} after a wall-clock delay. **Handler-only.**
344
+ *
345
+ * Backed by the machine's {@link Port} timer service: when no custom port is supplied the
346
+ * runtime instantiates a {@link Port} whose `setTimeout`-based timer is used here. When
347
+ * the timer fires, the event is enqueued like an ordinary `post`. This is intentionally **not**
348
+ * on the external {@link Hsm} / {@link Actor} surface — clients schedule work via {@link post}.
349
+ *
350
+ * @typeParam EventName - Literal key of `Protocol` being scheduled
351
+ * @param millis - Delay in milliseconds before enqueueing (≥ 0). Subject to timer granularity
352
+ * @param eventName - Event name (same constraints as {@link post})
353
+ * @param eventPayload - Handler arguments tuple (same as {@link post})
354
+ *
355
+ * @remarks
356
+ * Does **not** block the calling handler — returns as soon as the timer is registered. Timers
357
+ * are **not** cancelled if the machine transitions or the scheduling handler throws. (Richer
358
+ * timer services — cancellation, virtual clocks — will arrive later through the port.)
359
+ *
360
+ * @example
361
+ * ```ts
362
+ * scheduleReminder(text: string): void {
363
+ * this.deferredPost(50, 'deliver', text);
364
+ * }
365
+ * deliver(text: string): void {
366
+ * this.ctx.message = text;
367
+ * }
368
+ * ```
369
+ */
370
+ deferredPost<EventName extends keyof Protocol>(millis: number, eventName: PostedEvent<Protocol, EventName>, ...eventPayload: EventPayload<Protocol, EventName>): void;
370
371
  /**
371
372
  * Enqueue a **hi-priority** event processed before normal {@link post} jobs from the same turn.
372
373
  *
@@ -409,7 +410,7 @@ export interface Hsm<Context = Any, Protocol extends {} | undefined = undefined>
409
410
  /** @inheritdoc State.ctx */
410
411
  readonly ctx: Context;
411
412
  /**
412
- * Wait until all previously enqueued mailbox work completes through a **sync marker**.
413
+ * Wait until all previously enqueued actor work completes through a **sync marker**.
413
414
  *
414
415
  * Returns a Promise resolved when the marker job reaches the front of the queue and runs —
415
416
  * meaning every job enqueued **before** this `sync()` call has finished (handlers, transitions,
@@ -437,7 +438,7 @@ export interface Hsm<Context = Any, Protocol extends {} | undefined = undefined>
437
438
  * Atomically replace the active leaf state and context **without** running `onExit` / `onEntry`.
438
439
  *
439
440
  * Used for persistence rehydration, snapshot restore, time-travel debugging, and test fixtures.
440
- * Does not enqueue mailbox jobs — the next `post`/`call` runs from the restored configuration.
441
+ * Does not enqueue dispatch jobs — the next `post`/`call` runs from the restored configuration.
441
442
  *
442
443
  * @param state - Leaf or composite state **class** to activate (prototype switched immediately)
443
444
  * @param ctx - New context object (replaces {@link ctx} reference entirely)
@@ -455,7 +456,7 @@ export interface Hsm<Context = Any, Protocol extends {} | undefined = undefined>
455
456
  */
456
457
  restore(state: StateClass<Context, Protocol>, ctx: Context): void;
457
458
  /**
458
- * Invoke a **service** handler and await its typed result over the mailbox.
459
+ * Invoke a **service** handler and await its typed result over the actor's run-to-completion dispatch.
459
460
  *
460
461
  * Enqueues a dispatch job like {@link post}, but the runtime prepends `resolve` and `reject`
461
462
  * callbacks to the handler invocation. The returned Promise settles when the handler calls
@@ -471,7 +472,7 @@ export interface Hsm<Context = Any, Protocol extends {} | undefined = undefined>
471
472
  * {@link UnhandledEventError} if dispatch fails before the service runs
472
473
  *
473
474
  * @remarks
474
- * - Same **serialized** mailbox as `post` — no concurrent handler re-entrancy
475
+ * - Same **serialized**, run-to-completion dispatch as `post` — no concurrent handler re-entrancy
475
476
  * - Return type {@link ServiceResponse} is inferred from `Protocol[eventName]`
476
477
  * - Use {@link ResolveCallback} / {@link RejectCallback} in handler signatures for clarity
477
478
  * - `async` handlers should `await` work then call `resolve(result)`
@@ -493,10 +494,12 @@ export interface Hsm<Context = Any, Protocol extends {} | undefined = undefined>
493
494
  *
494
495
  * @remarks
495
496
  * Collisions with `keyof State` become `never`, preventing `post('transition', …)` at compile time.
497
+ * Service-shaped keys (see {@link IsServiceMethod}) also become `never` — they must be invoked with
498
+ * {@link Hsm.call}, so `post('getBalance', …)` is a compile error (proposal T2).
496
499
  *
497
500
  * @category Event handler
498
501
  */
499
- export type PostedEvent<Protocol extends {} | undefined, EventName extends keyof Protocol> = Protocol extends undefined ? string : EventName extends keyof State<any, any> ? never : EventName;
502
+ export type PostedEvent<Protocol extends {} | undefined, EventName extends keyof Protocol> = Protocol extends undefined ? string : EventName extends keyof State<any, any> ? never : IsServiceMethod<Protocol[EventName]> extends true ? never : EventName;
500
503
  /**
501
504
  * Tuple of arguments for {@link Base.post} after the event name, inferred from the handler signature.
502
505
  *
@@ -520,11 +523,378 @@ export type EventPayload<Protocol extends {} | undefined, EventName extends keyo
520
523
  * @typeParam Context - Domain context carried by the machine
521
524
  * @typeParam Protocol - Event/service vocabulary
522
525
  *
526
+ * @remarks
527
+ * The prototype constraint is the **port-less** handler contract ({@link State} &
528
+ * {@link StateEvents}) rather than {@link TopState} itself. This keeps the optional
529
+ * {@link TopState.port} member (which varies with the `Port` type parameter) out of
530
+ * transition / `restore` typing, so machines that declare a port remain assignable
531
+ * wherever a plain `StateClass` is expected.
532
+ *
523
533
  * @category State machine
524
534
  */
525
535
  export type StateClass<Context = Any, Protocol extends {} | undefined = undefined> = Function & {
526
- prototype: TopState<Context, Protocol>;
536
+ prototype: State<Context, Protocol> & StateEvents<Context, Protocol>;
527
537
  };
538
+ /**
539
+ * Resource teardown handle returned alongside subscription-style port results.
540
+ *
541
+ * `dispose()` must be **idempotent** — calling it more than once is a no-op. Ports hand
542
+ * one back via {@link ResultWithSubscription}; the state machine owns it and disposes it
543
+ * when the corresponding observation is no longer wanted.
544
+ *
545
+ * @category Port
546
+ */
547
+ export interface Disposable {
548
+ /** Release the resource / detach listeners. Safe to call repeatedly. */
549
+ dispose(): void;
550
+ }
551
+ /**
552
+ * A port result paired with the {@link Disposable} that tears down its subscription.
553
+ *
554
+ * Returned by port methods that both produce a value (e.g. a process id) **and** wire
555
+ * ongoing observations. The machine stores `value` and is responsible for calling
556
+ * `subscription.dispose()` during teardown.
557
+ *
558
+ * @typeParam Result - The immediate value produced by the port call
559
+ *
560
+ * @example
561
+ * ```ts
562
+ * spawn(spec: SpawnSpec): ResultWithSubscription<number> {
563
+ * const child = spawnProcess(spec);
564
+ * const bag = wireListeners(child, this.hsm());
565
+ * return { value: child.pid, subscription: bag };
566
+ * }
567
+ * ```
568
+ *
569
+ * @category Port
570
+ */
571
+ export interface ResultWithSubscription<Result> {
572
+ /** Immediate result of the port call. */
573
+ readonly value: Result;
574
+ /** Teardown handle for the observations the call established. */
575
+ readonly subscription: Disposable;
576
+ }
577
+ /**
578
+ * A single recorded interaction: an event name plus its payload.
579
+ *
580
+ * Produced both by {@link testing!TestPort} (the messages a test double records) and by the
581
+ * {@link testing!TestActor} `subscribe` observer stream (every event posted through the machine).
582
+ *
583
+ * @category Testing
584
+ */
585
+ export interface TracedMessage {
586
+ /** Event/service name, or a free-form label recorded by a {@link testing!TestPort}. */
587
+ readonly event: string;
588
+ /** Arguments captured with the event (a defensive copy). */
589
+ readonly payload: readonly unknown[];
590
+ }
591
+ /**
592
+ * Observer invoked for **every** event posted through a {@link testing!TestActor}.
593
+ *
594
+ * Registered via {@link testing!TestActor} `subscribe` — a capability unique to the test surface.
595
+ * Wire it to {@link testing!TestPort.record} to build a golden trace on the port under test.
596
+ *
597
+ * @category Testing
598
+ */
599
+ export type EventObserver = (message: TracedMessage) => void;
600
+ /**
601
+ * The effective protocol a machine **dispatches** over: the public {@link Protocol} merged
602
+ * with its `InternalProtocol`.
603
+ *
604
+ * Handlers (and the `Port` back-channel) see this union; external clients see only the
605
+ * public half. Legacy untyped machines (`Protocol extends undefined`) stay untyped.
606
+ *
607
+ * @typeParam Protocol - Public event/service vocabulary (client-callable)
608
+ * @typeParam InternalProtocol - Events only a port may post (e.g. `onSpawn`, `onExit`)
609
+ *
610
+ * @category State machine
611
+ */
612
+ export type Dispatch<Protocol extends {} | undefined, InternalProtocol extends {}> = {} extends InternalProtocol ? Protocol : Protocol extends undefined ? undefined : Protocol & InternalProtocol;
613
+ /**
614
+ * Compile-time guard asserting the public and internal protocols share **no** event names.
615
+ *
616
+ * Resolves to `true` when `keyof Public` and `keyof Internal` are disjoint; otherwise to a
617
+ * descriptive tuple that fails the `extends true` constraint on {@link makeActor} /
618
+ * {@link testing!makeTestActor}, surfacing the overlapping keys at the call site.
619
+ *
620
+ * @typeParam Public - Public protocol
621
+ * @typeParam Internal - Internal protocol
622
+ *
623
+ * @category State machine
624
+ */
625
+ export type Disjoint<Public, Internal> = Extract<keyof Public, keyof Internal> extends never ? true : ['ihsm: public and internal protocols must not share keys', Extract<keyof Public, keyof Internal>];
626
+ /**
627
+ * Phantom type carrier that lets a {@link TopState} subclass expose its four type parameters
628
+ * for extraction. It never exists at runtime — it is a `declare`d marker (see {@link TopState}).
629
+ *
630
+ * @typeParam Context - Domain context
631
+ * @typeParam Public - Public protocol
632
+ * @typeParam Internal - Internal (port-driven) protocol
633
+ * @typeParam Port - Port type
634
+ *
635
+ * @category State machine
636
+ */
637
+ export interface MachineTypes<Context, Public, Internal, Port> {
638
+ readonly context: Context;
639
+ readonly public: Public;
640
+ readonly internal: Internal;
641
+ readonly port: Port;
642
+ }
643
+ /**
644
+ * Extracts the **context** type from a {@link TopState} subclass — making the `TopState` the
645
+ * single point where the four machine types are declared.
646
+ *
647
+ * @typeParam T - A {@link TopState} subclass (instance type, e.g. `ConnTop`)
648
+ *
649
+ * @category State machine
650
+ */
651
+ export type MachineContext<T> = T extends {
652
+ readonly __ihsm: MachineTypes<infer Context, any, any, any>;
653
+ } ? Context : never;
654
+ /**
655
+ * Extracts the **public** protocol from a {@link TopState} subclass.
656
+ *
657
+ * @typeParam T - A {@link TopState} subclass (instance type)
658
+ *
659
+ * @category State machine
660
+ */
661
+ export type MachinePublic<T> = T extends {
662
+ readonly __ihsm: MachineTypes<any, infer Public, any, any>;
663
+ } ? Public : never;
664
+ /**
665
+ * Extracts the **internal** protocol from a {@link TopState} subclass. The result is always
666
+ * within `{} | undefined`, so it can drive {@link PostedEvent} / {@link EventPayload} directly.
667
+ *
668
+ * @typeParam T - A {@link TopState} subclass (instance type)
669
+ *
670
+ * @category State machine
671
+ */
672
+ export type MachineInternal<T> = T extends {
673
+ readonly __ihsm: MachineTypes<any, any, infer Internal, any>;
674
+ } ? (Internal extends {} | undefined ? Internal : never) : never;
675
+ /**
676
+ * Extracts the **port** type from a {@link TopState} subclass.
677
+ *
678
+ * @typeParam T - A {@link TopState} subclass (instance type)
679
+ *
680
+ * @category State machine
681
+ */
682
+ export type MachinePort<T> = T extends {
683
+ readonly __ihsm: MachineTypes<any, any, any, infer Port>;
684
+ } ? Port : never;
685
+ /**
686
+ * Restricted handle a {@link Port} uses to post **internal** events back into its machine.
687
+ *
688
+ * It is the {@link Base} surface narrowed to the `InternalProtocol`, so a port can only
689
+ * `post` the events it is allowed to raise — never public commands.
690
+ *
691
+ * @typeParam Context - Domain context
692
+ * @typeParam InternalProtocol - Events the port may post
693
+ *
694
+ * @category Port
695
+ */
696
+ export type InboundPoster<Context, InternalProtocol extends {} | undefined> = Base<Context, InternalProtocol>;
697
+ /**
698
+ * Outbound boundary between a machine and the impure world (processes, sockets, timers).
699
+ *
700
+ * Passed as the `port` instance to {@link makeActor} / {@link testing!makeTestActor} (or defaulted to a
701
+ * {@link Port}), and surfaced to handlers as {@link TopState.port}. The factory binds the
702
+ * port's {@link PortHandle.actor | actor} lazily, so the port can post internal events back via
703
+ * {@link PortHandle.hsm}.
704
+ *
705
+ * @typeParam Context - Domain context
706
+ * @typeParam InternalProtocol - Events this port may post inward
707
+ *
708
+ * @category Port
709
+ */
710
+ export interface PortHandle<Context = Any, InternalProtocol extends {} | undefined = undefined> {
711
+ /**
712
+ * The machine handle this port posts internal events through. **Bound lazily** by the runtime
713
+ * ({@link makeHsm} / {@link makeActor} / {@link testing!makeTestActor}) right after the actor is
714
+ * constructed — so a port is created with no constructor arguments and wired up afterwards.
715
+ * `undefined` before binding / after teardown.
716
+ */
717
+ actor: InboundPoster<Context, InternalProtocol> | undefined;
718
+ /** The bound machine handle (same as {@link PortHandle.actor}); `undefined` before binding. */
719
+ hsm(): InboundPoster<Context, InternalProtocol> | undefined;
720
+ }
721
+ /** Opaque timer handle returned by {@link Port.setTimeout} / {@link Port.setInterval}. */
722
+ export type TimerHandle = number;
723
+ /**
724
+ * Standard JavaScript random-generation surface exposed by {@link Port} and mocked by
725
+ * {@link testing!TestPort}.
726
+ *
727
+ * Route every nondeterministic draw through the machine's port — never `Math.random()` or
728
+ * `crypto.*` directly in handlers — so tests can script values with
729
+ * {@link testing!TestPort.feedRandom | feedRandom} / {@link testing!TestPort.feedCryptoRandom | feedCryptoRandom} /
730
+ * {@link testing!TestPort.feedUUID | feedUUID} / {@link testing!TestPort.feedRandomBytes | feedRandomBytes}.
731
+ *
732
+ * @category Port
733
+ */
734
+ export interface RandomService {
735
+ /** Uniform in `[0, 1)` — `Math.random()`. */
736
+ random(): number;
737
+ /** Uniform in `[0, 1)` — `crypto.random()` when available, otherwise `Math.random()`. */
738
+ cryptoRandom(): number;
739
+ /** RFC 4122 UUID — `crypto.randomUUID()`. */
740
+ randomUUID(): string;
741
+ /** In-place fill — `crypto.getRandomValues()`. */
742
+ getRandomValues<T extends ArrayBufferView>(array: T): T;
743
+ }
744
+ /**
745
+ * External, public-only view of a machine returned by {@link makeActor}.
746
+ *
747
+ * Identical to {@link Hsm} over the **public** protocol: clients can `post` / `call` only
748
+ * public events — internal (port-driven) events are not in the callable surface.
749
+ *
750
+ * @typeParam Context - Domain context
751
+ * @typeParam Protocol - Public protocol
752
+ *
753
+ * @category State machine
754
+ */
755
+ export type Actor<Context = Any, Protocol extends {} | undefined = undefined> = Hsm<Context, Protocol>;
756
+ /**
757
+ * Abstract base class for **any** port — production or test.
758
+ *
759
+ * It takes the machine's root {@link TopState} as its single type argument and derives the
760
+ * context and internal protocol from it (via {@link MachineContext} / {@link MachineInternal}),
761
+ * so the `TopState` is the one place those types are declared. Extend {@link Port} (not `BasePort`
762
+ * directly) for production ports so timer and random services are available.
763
+ *
764
+ * The {@link BasePort.actor | actor} link is **bound lazily** by the runtime: construct the port
765
+ * with no arguments and pass the instance to a factory, which wires the actor in afterwards.
766
+ *
767
+ * @typeParam T - The machine's root {@link TopState} subclass (e.g. `ConnTop`)
768
+ *
769
+ * @category Port
770
+ */
771
+ export declare abstract class BasePort<T> implements PortHandle<MachineContext<T>, MachineInternal<T>> {
772
+ /**
773
+ * Phantom carrier of the root {@link TopState} type, so {@link testing!makeTestPort} can recover `T`
774
+ * (and therefore the port surface, via {@link MachinePort}) from a mock class. Type-only.
775
+ */
776
+ readonly __topState: T;
777
+ /**
778
+ * @inheritdoc PortHandle.actor
779
+ *
780
+ * Set once by the runtime right after the machine is built; `undefined` before binding.
781
+ */
782
+ actor: InboundPoster<MachineContext<T>, MachineInternal<T>> | undefined;
783
+ /** @inheritdoc PortHandle.hsm */
784
+ hsm(): InboundPoster<MachineContext<T>, MachineInternal<T>> | undefined;
785
+ /**
786
+ * Post an internal event inward through the bound {@link BasePort.actor | actor}.
787
+ *
788
+ * This is the one channel a port (or a test driving the port) uses to feed the machine its
789
+ * internal protocol. Because emission is explicit — never a side effect of the outbound call
790
+ * the machine made — a single mock works across many tests: the test decides *when* (and
791
+ * whether) to push each internal event.
792
+ *
793
+ * @param eventName - Internal event to post
794
+ * @param payload - Arguments for the event
795
+ * @throws If called before the actor has been bound by a factory
796
+ */
797
+ send<EventName extends keyof MachineInternal<T>>(eventName: PostedEvent<MachineInternal<T>, EventName>, ...payload: EventPayload<MachineInternal<T>, EventName>): void;
798
+ }
799
+ /**
800
+ * Production port base with standard JavaScript timer and random services.
801
+ *
802
+ * Extend this class for domain ports in production code. It inherits the lazily-bound
803
+ * {@link BasePort.actor | actor}, {@link BasePort.hsm | hsm}, and {@link BasePort.send | send}
804
+ * from {@link BasePort}, and adds {@link Port.setTimeout | setTimeout} /
805
+ * {@link Port.setInterval | setInterval} / {@link Port.clearTimeout | clearTimeout} /
806
+ * {@link Port.clearInterval | clearInterval} plus the {@link RandomService} methods
807
+ * ({@link Port.random | random}, {@link Port.cryptoRandom | cryptoRandom},
808
+ * {@link Port.randomUUID | randomUUID}, {@link Port.getRandomValues | getRandomValues}).
809
+ *
810
+ * {@link State.deferredPost} delegates to {@link Port.setTimeout}. When no custom port is
811
+ * supplied the runtime instantiates a plain `Port` for that purpose.
812
+ *
813
+ * @typeParam T - The machine's root {@link TopState} subclass (e.g. `ConnTop`)
814
+ *
815
+ * @example A minimal domain port
816
+ * ```ts
817
+ * class ConnPortImpl extends ihsm.Port<ConnTop> implements ConnPort {
818
+ * private nextId = 1;
819
+ * connect(host: string): ihsm.ResultWithSubscription<number> {
820
+ * const id = this.nextId++;
821
+ * return { value: id, subscription: { dispose: () => {} } };
822
+ * }
823
+ * disconnect(id: number): void {}
824
+ * }
825
+ * const port = new ConnPortImpl();
826
+ * const conn = ihsm.makeActor(ConnTop, ctx, port); // binds port.actor
827
+ * ```
828
+ *
829
+ * @category Port
830
+ */
831
+ export declare class Port<T = Any> extends BasePort<T> implements RandomService {
832
+ private _timerSeq;
833
+ private readonly _timeoutHandles;
834
+ private readonly _intervalHandles;
835
+ /**
836
+ * Schedule `callback` after `millis` milliseconds — same argument order as `globalThis.setTimeout`.
837
+ *
838
+ * @returns An opaque handle for {@link Port.clearTimeout}
839
+ */
840
+ setTimeout(callback: () => void, millis?: number): TimerHandle;
841
+ /** Cancel a pending {@link Port.setTimeout} handle. No-op when `id` is `undefined` or unknown. */
842
+ clearTimeout(id: TimerHandle | undefined): void;
843
+ /**
844
+ * Schedule `callback` every `millis` milliseconds — same argument order as `globalThis.setInterval`.
845
+ *
846
+ * @returns An opaque handle for {@link Port.clearInterval}
847
+ */
848
+ setInterval(callback: () => void, millis?: number): TimerHandle;
849
+ /** Cancel a pending {@link Port.setInterval} handle. No-op when `id` is `undefined` or unknown. */
850
+ clearInterval(id: TimerHandle | undefined): void;
851
+ /** @inheritdoc RandomService.random */
852
+ random(): number;
853
+ /** @inheritdoc RandomService.cryptoRandom */
854
+ cryptoRandom(): number;
855
+ /** @inheritdoc RandomService.randomUUID */
856
+ randomUUID(): string;
857
+ /** @inheritdoc RandomService.getRandomValues */
858
+ getRandomValues<T extends ArrayBufferView>(array: T): T;
859
+ }
860
+ /**
861
+ * `true` when handler `M` is **service-shaped** — its parameter list begins with a resolve callback
862
+ * and a reject callback (`(resolve, reject, ...payload) => void | Promise<void>`), matching
863
+ * {@link ResolveCallback} / {@link RejectCallback}; `false` for plain **event** handlers whose
864
+ * parameters are data.
865
+ *
866
+ * This is the single discriminator that routes a protocol key to {@link Hsm.call} (services) versus
867
+ * {@link Base.post} (events): {@link ServiceName} / {@link ServiceKeys} accept only keys where this
868
+ * is `true`, while {@link PostedEvent} / {@link EventKeys} reject them (proposal T2).
869
+ *
870
+ * @remarks
871
+ * The check is **structural and heuristic**: a handler counts as a service iff its first two
872
+ * parameters are callables. A plain event that genuinely takes two leading callbacks (rare) would
873
+ * be classified as a service — give such a handler `(): void` / data parameters, and reserve the
874
+ * leading `(resolve, reject)` shape for real services.
875
+ *
876
+ * @category Event handler
877
+ */
878
+ export type IsServiceMethod<M> = M extends (...args: infer Args) => Promise<void> | void ? (Args extends [resolve: (result: any) => void, reject: (error: any) => void, ...payload: any[]] ? true : false) : false;
879
+ /**
880
+ * Union of protocol keys whose handlers are **services** — invocable with {@link Hsm.call}. Reserved
881
+ * {@link State} method names are excluded; resolves to `string` for the untyped (`undefined`) protocol.
882
+ *
883
+ * @category Event handler
884
+ */
885
+ export type ServiceKeys<Protocol extends {} | undefined> = Protocol extends undefined ? string : Exclude<{
886
+ [K in keyof Protocol]-?: IsServiceMethod<Protocol[K]> extends true ? K : never;
887
+ }[keyof Protocol], keyof State<any, any>>;
888
+ /**
889
+ * Union of protocol keys whose handlers are **events** — postable with {@link Base.post}. Reserved
890
+ * {@link State} method names and service-shaped keys are excluded; resolves to `string` for the
891
+ * untyped (`undefined`) protocol.
892
+ *
893
+ * @category Event handler
894
+ */
895
+ export type EventKeys<Protocol extends {} | undefined> = Protocol extends undefined ? string : Exclude<{
896
+ [K in keyof Protocol]-?: IsServiceMethod<Protocol[K]> extends true ? never : K;
897
+ }[keyof Protocol], keyof State<any, any>>;
528
898
  /**
529
899
  * Tuple of client-supplied arguments to {@link Hsm.call}, excluding injected resolve/reject.
530
900
  *
@@ -551,14 +921,15 @@ export type ServiceRequest<Protocol, EventName extends keyof Protocol> = Protoco
551
921
  export type ServiceResponse<Protocol, EventName extends keyof Protocol> = Protocol extends undefined ? any : Protocol[EventName] extends (resolve: infer Reply, reject: infer Error, ...payload: infer Payload) => Promise<void> | void ? Reply : never;
552
922
  /**
553
923
  * Valid first argument to {@link Hsm.call} — protocol keys whose handlers use the service signature
554
- * `(resolve, reject, ...payload)`.
924
+ * `(resolve, reject, ...payload)`. Reserved {@link State} names and **event-shaped** keys become
925
+ * `never`, so `call('open')` on a void event is a compile error (proposal T2).
555
926
  *
556
927
  * @typeParam Protocol - Machine vocabulary interface
557
- * @typeParam EventName - Candidate key (unused generic for symmetry with other aliases)
928
+ * @typeParam EventName - Candidate key being constrained
558
929
  *
559
930
  * @category Event handler
560
931
  */
561
- export type ServiceName<Protocol, EventName> = Protocol extends undefined ? string : EventName extends keyof State<any, any> ? never : EventName;
932
+ export type ServiceName<Protocol, EventName> = Protocol extends undefined ? string : EventName extends keyof State<any, any> ? never : EventName extends keyof Protocol ? (IsServiceMethod<Protocol[EventName]> extends true ? EventName : never) : never;
562
933
  /**
563
934
  * Lifecycle hooks optionally overridden on state classes.
564
935
  *
@@ -634,12 +1005,30 @@ export interface StateEvents<Context, Protocol extends {} | undefined> {
634
1005
  *
635
1006
  * @category State machine
636
1007
  */
637
- export declare abstract class TopState<Context = Any, Protocol extends {} | undefined = undefined> implements State<Context, Protocol>, StateEvents<Context, Protocol> {
1008
+ export declare abstract class TopState<Context = Any, Protocol extends {} | undefined = undefined, InternalProtocol extends {} = {}, Port = undefined> implements State<Context, Dispatch<Protocol, InternalProtocol>>, StateEvents<Context, Dispatch<Protocol, InternalProtocol>> {
638
1009
  /** Domain context (injected by runtime — do not assign in constructors). */
639
1010
  readonly ctx: Context;
640
1011
  /** Handler view of the machine (`this` inside methods delegates here for core operations). */
641
- readonly hsm: State<Context, Protocol>;
1012
+ readonly hsm: State<Context, Dispatch<Protocol, InternalProtocol>>;
1013
+ /**
1014
+ * Phantom type carrier — **never exists at runtime** (`declare`d, never assigned). It makes a
1015
+ * `TopState` subclass the single configuration point for the four machine types, so helpers
1016
+ * like {@link MachineContext} / {@link MachineInternal} / {@link MachinePort} (and
1017
+ * {@link BasePort} / {@link testing!TestPort}) can derive everything from the root state alone.
1018
+ *
1019
+ * @internal
1020
+ */
1021
+ readonly __ihsm: MachineTypes<Context, Protocol, InternalProtocol, Port>;
642
1022
  constructor();
1023
+ /**
1024
+ * Outbound boundary — the `port` instance passed to {@link makeActor} / {@link testing!makeTestActor};
1025
+ * all impure I/O flows through here.
1026
+ *
1027
+ * Typed `undefined` for machines created without a port (the default), so existing
1028
+ * port-less machines are unaffected. At runtime a {@link Port} always backs such
1029
+ * machines — it is what {@link State.deferredPost} uses for its timer service.
1030
+ */
1031
+ get port(): Port;
643
1032
  /** @inheritdoc Properties.eventName */
644
1033
  get eventName(): string;
645
1034
  /** @inheritdoc Properties.eventPayload */
@@ -647,11 +1036,11 @@ export declare abstract class TopState<Context = Any, Protocol extends {} | unde
647
1036
  /** @inheritdoc Properties.traceHeader */
648
1037
  get traceHeader(): string;
649
1038
  /** @inheritdoc Properties.topState */
650
- get topState(): StateClass<Context, Protocol>;
1039
+ get topState(): StateClass<Context, Dispatch<Protocol, InternalProtocol>>;
651
1040
  /** @inheritdoc Properties.currentStateName */
652
1041
  get currentStateName(): string;
653
1042
  /** @inheritdoc Properties.currentState */
654
- get currentState(): StateClass<Context, Protocol>;
1043
+ get currentState(): StateClass<Context, Dispatch<Protocol, InternalProtocol>>;
655
1044
  /** @inheritdoc Properties.ctxTypeName */
656
1045
  get ctxTypeName(): string;
657
1046
  /** @inheritdoc Properties.traceLevel */
@@ -665,29 +1054,29 @@ export declare abstract class TopState<Context = Any, Protocol extends {} | unde
665
1054
  /** @inheritdoc Properties.traceWriter */
666
1055
  set traceWriter(value: TraceWriter);
667
1056
  /** @inheritdoc Properties.dispatchErrorCallback */
668
- get dispatchErrorCallback(): DispatchErrorCallback<Context, Protocol>;
1057
+ get dispatchErrorCallback(): DispatchErrorCallback<Context, Dispatch<Protocol, InternalProtocol>>;
669
1058
  /** @inheritdoc Properties.dispatchErrorCallback */
670
- set dispatchErrorCallback(value: DispatchErrorCallback<Context, Protocol>);
1059
+ set dispatchErrorCallback(value: DispatchErrorCallback<Context, Dispatch<Protocol, InternalProtocol>>);
671
1060
  /** @inheritdoc State.transition */
672
- transition(nextState: StateClass<Context, Protocol>): void;
1061
+ transition(nextState: StateClass<Context, Dispatch<Protocol, InternalProtocol>>): void;
673
1062
  /** @inheritdoc State.unhandled */
674
1063
  unhandled(): never;
675
1064
  /** @inheritdoc State.sleep */
676
1065
  sleep(millis: number): Promise<void>;
677
1066
  /** @inheritdoc Base.post */
678
- post<EventName extends keyof Protocol>(eventName: PostedEvent<Protocol, EventName>, ...eventPayload: EventPayload<Protocol, EventName>): void;
679
- /** @inheritdoc Base.deferredPost */
680
- deferredPost<EventName extends keyof Protocol>(millis: number, eventName: PostedEvent<Protocol, EventName>, ...eventPayload: EventPayload<Protocol, EventName>): void;
1067
+ post<EventName extends keyof Dispatch<Protocol, InternalProtocol>>(eventName: PostedEvent<Dispatch<Protocol, InternalProtocol>, EventName>, ...eventPayload: EventPayload<Dispatch<Protocol, InternalProtocol>, EventName>): void;
1068
+ /** @inheritdoc State.deferredPost */
1069
+ deferredPost<EventName extends keyof Dispatch<Protocol, InternalProtocol>>(millis: number, eventName: PostedEvent<Dispatch<Protocol, InternalProtocol>, EventName>, ...eventPayload: EventPayload<Dispatch<Protocol, InternalProtocol>, EventName>): void;
681
1070
  /** @inheritdoc State.postNow */
682
- postNow<EventName extends keyof Protocol>(eventName: PostedEvent<Protocol, EventName>, ...eventPayload: EventPayload<Protocol, EventName>): void;
1071
+ postNow<EventName extends keyof Dispatch<Protocol, InternalProtocol>>(eventName: PostedEvent<Dispatch<Protocol, InternalProtocol>, EventName>, ...eventPayload: EventPayload<Dispatch<Protocol, InternalProtocol>, EventName>): void;
683
1072
  /** @inheritdoc StateEvents.onExit */
684
1073
  onExit(): Promise<void> | void;
685
1074
  /** @inheritdoc StateEvents.onEntry */
686
1075
  onEntry(): Promise<void> | void;
687
1076
  /** @inheritdoc StateEvents.onError */
688
- onError<EventName extends keyof Protocol>(error: RuntimeError<Context, Protocol, EventName>): Promise<void> | void;
1077
+ onError<EventName extends keyof Dispatch<Protocol, InternalProtocol>>(error: RuntimeError<Context, Dispatch<Protocol, InternalProtocol>, EventName>): Promise<void> | void;
689
1078
  /** @inheritdoc StateEvents.onUnhandled */
690
- onUnhandled<EventName extends keyof Protocol>(error: UnhandledEventError<Context, Protocol, EventName>): Promise<void> | void;
1079
+ onUnhandled<EventName extends keyof Dispatch<Protocol, InternalProtocol>>(error: UnhandledEventError<Context, Dispatch<Protocol, InternalProtocol>, EventName>): Promise<void> | void;
691
1080
  }
692
1081
  /**
693
1082
  * Base class for all ihsm runtime errors carrying machine context.
@@ -891,6 +1280,12 @@ export declare function defineStateName<Context, Protocol extends {} | undefined
891
1280
  * @category State machine
892
1281
  */
893
1282
  export declare function registerStateNames(exports: Record<string, unknown>): void;
1283
+ /** @internal — shared by the core factories and (via re-export) the `ihsm/testing` factories. */
1284
+ export declare function defaultDispatchErrorCallback<Context, Protocol extends {} | undefined>(hsm: Base<Context, Protocol>, err: Error): void;
1285
+ /** @internal */
1286
+ export declare const defaultTraceWriter: TraceWriter;
1287
+ /** @internal */
1288
+ export declare const defaultInitialize = true;
894
1289
  /**
895
1290
  * Creates and optionally initializes a hierarchical state machine **actor** bound to `ctx`.
896
1291
  *
@@ -931,4 +1326,70 @@ export declare function registerStateNames(exports: Record<string, unknown>): vo
931
1326
  *
932
1327
  * @category Factory
933
1328
  */
934
- export declare function makeHsm<Context, Protocol extends undefined | {}>(topState: StateClass<Context, Protocol>, ctx: Context, initialize?: boolean, traceLevel?: TraceLevel, traceWriter?: TraceWriter, dispatchErrorCallback?: DispatchErrorCallback<Context, Protocol>): Hsm<Context, Protocol>;
1329
+ export declare function makeHsm<Context, Protocol extends undefined | {}>(topState: StateClass<Context, Protocol>, ctx: Context, initialize?: boolean, traceLevel?: TraceLevel, traceWriter?: TraceWriter, dispatchErrorCallback?: DispatchErrorCallback<Context, Protocol>, port?: PortHandle<Context, Protocol>): Hsm<Context, Protocol>;
1330
+ /**
1331
+ * The constrained root-state argument shared by {@link makeActor} / {@link testing!makeTestActor}.
1332
+ *
1333
+ * Its prototype carries the {@link MachineTypes} marker, so `Context`, `Public`, and `Internal` are
1334
+ * **inferred from the `topState`** at the call site — you never pass those generics explicitly.
1335
+ *
1336
+ * @typeParam Context - Domain context type
1337
+ * @typeParam Public - Public, client-callable protocol
1338
+ * @typeParam Internal - Internal protocol — only postable by the port / handlers
1339
+ *
1340
+ * @category Factory
1341
+ */
1342
+ export type TopStateArg<Context, Public extends undefined | {}, Internal extends {}> = StateClass<Context, Dispatch<Public, Internal>> & {
1343
+ readonly prototype: {
1344
+ readonly __ihsm: MachineTypes<Context, Public, Internal, unknown>;
1345
+ };
1346
+ };
1347
+ /**
1348
+ * Optional tuning passed as the **last** argument to {@link makeActor} / {@link testing!makeTestActor},
1349
+ * after the three mandatory positional arguments (`topState`, `ctx`, `port`). Every field has a
1350
+ * sensible default; omit the whole object to take them all.
1351
+ *
1352
+ * @typeParam Context - Domain context type
1353
+ * @typeParam Public - Public protocol
1354
+ * @typeParam Internal - Internal protocol
1355
+ *
1356
+ * @category Factory
1357
+ */
1358
+ export interface ActorOptions<Context, Public extends undefined | {}, Internal extends {} = {}> {
1359
+ /** Run the initial `@InitialState` walk (default `true`). */
1360
+ initialize?: boolean;
1361
+ /** Initial {@link TraceLevel} (default {@link TraceLevel.DEBUG}). */
1362
+ traceLevel?: TraceLevel;
1363
+ /** {@link TraceWriter} implementation (default: prefixes with state name, logs to `console`). */
1364
+ traceWriter?: TraceWriter;
1365
+ /** Last-resort error hook (default: trace + rethrow). */
1366
+ dispatchErrorCallback?: DispatchErrorCallback<Context, Dispatch<Public, Internal>>;
1367
+ }
1368
+ /**
1369
+ * Creates an actor exposing only its **public** protocol, with an optional outbound {@link Port}.
1370
+ *
1371
+ * Like {@link makeHsm} but separates the public, client-callable protocol from an
1372
+ * `InternalProtocol` that only the port may post inward. The returned {@link Actor} surfaces
1373
+ * just the public events; handlers (and the port) may post the merged {@link Dispatch} protocol.
1374
+ *
1375
+ * The trailing `Disjoint` guard is a compile-time gate: if `Public` and `Internal` share an event
1376
+ * name, the call fails to type-check, pointing at the overlapping keys.
1377
+ *
1378
+ * @typeParam Context - Domain context type
1379
+ * @typeParam Public - Public, client-callable protocol
1380
+ * @typeParam Internal - Internal protocol — only postable by the port / handlers
1381
+ * @typeParam P - Concrete {@link Port} type assigned to `this.port`
1382
+ * @param topState - Root state class; `Context` / `Public` / `Internal` are inferred from it (see {@link TopStateArg})
1383
+ * @param ctx - Mutable domain object shared by all states
1384
+ * @param port - Outbound port instance (its `actor` is bound by the factory; use {@link Port} if none)
1385
+ * @param options - Optional tuning: `initialize` / `traceLevel` / `traceWriter` / … (see {@link ActorOptions})
1386
+ * @returns A public-only {@link Actor} handle
1387
+ *
1388
+ * @example
1389
+ * ```ts
1390
+ * const conn = makeActor(ConnTop, new ConnCtx(), port, { traceLevel: TraceLevel.PRODUCTION });
1391
+ * ```
1392
+ *
1393
+ * @category Factory
1394
+ */
1395
+ export declare function makeActor<Context, Public extends undefined | {}, Internal extends {} = {}, P extends PortHandle<Context, Internal> = Port>(topState: TopStateArg<Context, Public, Internal>, ctx: Context, port: P, options?: ActorOptions<Context, Public, Internal>, ..._disjointGuard: Disjoint<Public, Internal> extends true ? [] : [error: Disjoint<Public, Internal>]): Actor<Context, Public>;