aifsmjs 0.2.1 → 0.3.1

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/llms-full.txt CHANGED
@@ -119,15 +119,16 @@ The three layers are fully decoupled: take `step()` alone for replay, take `Mach
119
119
 
120
120
  | Will do (v1) | Won't do |
121
121
  | --------------------------------------------------- | ------------------------------------------------- |
122
- | Flat states + transitions | Hierarchical / compound states |
122
+ | Flat states + transitions | Parallel state regions |
123
+ | Hierarchical sugar via `state.sub` (experimental, since 0.3.0) | Closures embedded in definition (breaks serialize) |
123
124
  | Guards (sync only; inline async throws `InvalidDefinitionError` at `defineMachine`; runtime throws `AsyncGuardError` on thenable return) | Async guards |
124
125
  | Actions (assign + enqueue effects) | Async API inside an action (use an effect) |
125
126
  | Fire-and-forget effects | Actor invocation / spawn |
126
127
  | Read-only inspect middleware | Cancellable transition middleware |
127
128
  | `replay(initial, log, def, impl)` pure function | Time-travel debugger (v2 candidate) |
128
129
  | `fast-check` `fc.commands` adapter | Custom PBT framework |
129
- | String ref + runtime injection | Closures embedded in definition (breaks serialize) |
130
- | Tree-shake friendly subpath exports | Single root import for everything |
130
+ | String ref + runtime injection | Single root import for everything |
131
+ | Tree-shake friendly subpath exports | ECS / Pixi bridges (opt-in subpath, not core) |
131
132
 
132
133
  ---
133
134
 
@@ -369,6 +370,27 @@ Non-goals:
369
370
  - No async lifecycle hook
370
371
  - Inspect middleware cannot alter the transition outcome
371
372
 
373
+ ### Sub-machine lifecycle (since 0.3.0, experimental)
374
+
375
+ When a state declares `sub`, the per-transition ordering is:
376
+
377
+ 1. Parent `step()` runs: `exit actions → transition.actions → entry actions`.
378
+ 2. Old child (if any) `dispose()` — synchronous; exceptions become
379
+ `SubMachineError(phase: "dispose")`.
380
+ 3. New child (if next state has `sub`) instantiation — exceptions become
381
+ `SubMachineError(phase: "init")`.
382
+ 4. Parent snapshot commits.
383
+ 5. Middleware pipeline runs.
384
+ 6. Effects dispatch.
385
+ 7. `'transition'` event emits to `on()` / `onTransition()` subscribers.
386
+
387
+ If step 2 or 3 throws, the parent snapshot is **not** committed (rollback
388
+ to `prev`); no middleware / effects / `'transition'` runs.
389
+
390
+ `runtime.dispose()` cascades to the child via `controller.signal`'s abort
391
+ listener and an explicit `child.dispose()` call. Cascade swallows child
392
+ exceptions to honour the never-throws dispose contract.
393
+
372
394
  ---
373
395
 
374
396
  ## Lifecycle Protocol
@@ -451,7 +473,7 @@ Example-first, PBT-augmented. Lesson from jssm: "3000+ tests / 100% coverage" tu
451
473
  - **Example tests** (vitest): for every src module, write happy path + edge + error-message triplets.
452
474
  - **PBT smoke**: each generic property runs 50 iterations as an invariant guard, not as a coverage source.
453
475
  - **CI-enforced thresholds**: `@vitest/coverage-v8` is wired to **100% statements / 100% lines / 100% functions / ≥90% branches**. The few defensive invariant-guard branches (e.g. runtime determinism mismatch) carry `/* v8 ignore */` annotations with rationale.
454
- - **Size budget**: `scripts/check-size.mjs` enforces per-subpath gzip caps in CI — core ≤3 KB, replay ≤1.6 KB, pbt ≤4.5 KB, others ≤1 KB. Exceeding any cap fails the build.
476
+ - **Size budget**: `scripts/check-size.mjs` enforces per-subpath gzip caps in CI — core ≤4.7 KB (raised in 0.3.0 for sub-machine sugar), replay ≤1.8 KB, pbt ≤5.5 KB (raised in 0.3.0 because `pbt` transitively imports `createRuntime`), others ≤1 KB. Exceeding any cap fails the build.
455
477
 
456
478
  ### The 6 built-in generic properties
457
479
 
@@ -471,7 +493,7 @@ Example-first, PBT-augmented. Lesson from jssm: "3000+ tests / 100% coverage" tu
471
493
  | | aifsmjs | XState v5 | Robot3 | @xstate/store | Zag.js |
472
494
  | -------------------------- | -------------- | ----------------- | ----------------- | ----------------- | ----------------- |
473
495
  | Core size (gzip) | ~2.8KB | ~15KB | ~1KB | < 1KB | per-component |
474
- | Hierarchical states | No (v1) | Yes | No | N/A | Yes |
496
+ | Hierarchical states | Sugar (0.3.0) | Yes | No | N/A | Yes |
475
497
  | Async invoke / actor | No | Yes | No | N/A | No |
476
498
  | Guard combinators | and/or/not | and/or/not | No | N/A | No |
477
499
  | Effects dual-track | enqueue | enqueueActions | reduce/action | enq.effect() | array of names |
@@ -487,12 +509,23 @@ Example-first, PBT-augmented. Lesson from jssm: "3000+ tests / 100% coverage" tu
487
509
  | Version | Scope |
488
510
  | ------- | ------------------------------------------------------------------ |
489
511
  | v0.1 | core + guards + effects + inspect + replay + pbt (this release) |
490
- | v0.2 | Hierarchical / compound states, including entry/exit ordering |
491
- | v0.3 | Parallel state regions |
492
- | v0.4 | Actor invocation (async) and spawn |
512
+ | v0.2 | Async-guard detection, coverage tuning, llms-full.txt verify gate |
513
+ | v0.3 | Hierarchical sugar via `state.sub` (experimental, this release) |
514
+ | v0.4 | `historyState` — remember last active sub-state on re-entry |
493
515
  | v0.5 | `aifsmjs-bridge-bitecs` / `aifsmjs-bridge-pixi` (separate sub-packages) |
494
516
  | v1.0 | API freeze and stability guarantee |
495
517
 
518
+ **Out of scope (v1)**:
519
+
520
+ - **Parallel state regions** (out of scope for v1)
521
+ - **Actor invocation / spawn** (out of scope for v1)
522
+ - **Tick / game-loop hook** (out of scope for v1)
523
+ - **ECS / Pixi bridges** (out of scope for v1)
524
+
525
+ **v0.4 candidate**: `historyState` — remember last active sub-state on
526
+ re-entry. Workaround today: snapshot via `onTransition` and restore
527
+ manually.
528
+
496
529
  ---
497
530
 
498
531
  ## License
@@ -510,6 +543,77 @@ All notable changes to this project will be documented in this file.
510
543
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
511
544
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
512
545
 
546
+ ## [0.3.1] — 2026-05-29
547
+
548
+ ### Fixed
549
+
550
+ - **Memory: `on(type, fn, { once: true, signal })` left the abort listener
551
+ attached after the once-handler fired.** The `once` wrapper removed itself
552
+ from the listener set but did not detach the `AbortSignal` listener or drop
553
+ its entry from the internal cleanup set, so the closure lingered on the
554
+ external signal until the signal aborted or `dispose()` ran. With a
555
+ long-lived signal and repeated once+signal registration, dead listeners
556
+ accumulated. `on()` now routes the once-wrapper, the abort handler, and the
557
+ returned unsubscribe through a single `cleanup()` that always detaches the
558
+ abort listener. `runtime.onTransition(fn, { once, signal })` inherits the
559
+ fix (it delegates to `on`). Same class of leak as the [0.1.2] abort-listener
560
+ fix; `once` + `signal` together was the remaining gap. Present since 0.1.2.
561
+
562
+ This release is **non-breaking**. No API surface change; `once`-only,
563
+ `signal`-only, and no-option callers are byte-for-byte unaffected at runtime.
564
+ Core gzip 4,393 B → 4,387 B (the shared `cleanup` closure deduplicates).
565
+
566
+ ## [0.3.0] — 2026-05-29
567
+
568
+ ### Added
569
+
570
+ - **Hierarchical / sub-machine sugar** (experimental). `StateDef` accepts
571
+ two new optional fields: `sub` (a child `SubMachineDef`) and `subImpl`
572
+ (the child's `Implementations`). When the runtime enters a state with
573
+ `sub`, a child `Runtime` is lazily instantiated; when it exits, the child
574
+ is disposed. Access via `runtime.subRuntime()`. See `STABILITY.md` for
575
+ the experimental contract.
576
+ - New error: `SubMachineError` (`{ parentState, phase, cause }`) thrown
577
+ by `send()` / `reset()` on child init / dispose failure. `dispose()`
578
+ cascade swallows child dispose exceptions (idempotent + never-throws
579
+ contract).
580
+ - New type alias: `SubMachineDef<SubCtx, SubEvt, SubStates>`.
581
+ - **`runtime.onTransition(handler, opts?)`** — semantic sugar over
582
+ `runtime.on('transition', handler, opts)`. One-line delegation; shares
583
+ the same listener Set, so registration order across both APIs determines
584
+ invocation order. Returned unsubscribe identical to `on('transition', ...)`.
585
+
586
+ ### Stability
587
+
588
+ - New file: `STABILITY.md`. Documents the three tiers: **stable**
589
+ (everything shipped 0.1.0–0.2.1), **experimental** (`sub`, `subImpl`,
590
+ `subRuntime`, `SubMachineError`, `SubMachineDef`), **draft**
591
+ (`historyState`, v0.4 candidate).
592
+
593
+ ### Changed (positioning)
594
+
595
+ - **README "Capabilities / Limitations" table**: "Hierarchical / compound
596
+ states" moved out of the "Won't do" column. Added on the "Will do" side
597
+ as "Hierarchical sugar via `state.sub` (experimental since 0.3.0)".
598
+ - **README Lifecycle Invariants**: documented the sub-machine ordering —
599
+ parent `step()` lifecycle (exit / actions / entry) runs first, then
600
+ child dispose → child init → snapshot commit → middleware → effects →
601
+ `transition` emit.
602
+
603
+ ### Build & tooling
604
+
605
+ - **`scripts/check-size.mjs`**: core gzip budget raised 3,700 → 4,700 B
606
+ to absorb sub-machine lifecycle, `SubMachineError`, and `onTransition`
607
+ sugar (measured at 4,465 B). `pbt` budget raised 4,600 → 5,500 B because
608
+ `pbt/properties.ts` imports `createRuntime` from `runtime.ts`; the new
609
+ sub-machine code is pulled in transitively (measured at 5,228 B).
610
+
611
+ ### Compatibility
612
+
613
+ This release is **non-breaking** for v0.2.1 callers who do not opt into
614
+ the new sub-machine fields. All existing API signatures, error types, and
615
+ runtime behaviour are byte-identical.
616
+
513
617
  ## [0.2.1] — 2026-05-28
514
618
 
515
619
  ### Security
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aifsmjs",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Small, strict FSM library for deterministic, replayable state machines in any TypeScript/JS app — multi-step forms, checkout funnels, auth flows, tutorials, scene flow. Pure step() lifecycle, opt-in effects, inspect, replay, and a fast-check property-based testing adapter. Browser / Node / Bun / Deno / WebView / Worker friendly.",
5
5
  "keywords": [
6
6
  "fsm",
@@ -74,6 +74,7 @@
74
74
  "dist",
75
75
  "README.md",
76
76
  "README_ZHTW.md",
77
+ "STABILITY.md",
77
78
  "LICENSE",
78
79
  "llms.txt",
79
80
  "llms-full.txt"