Releases: islumina/aifsmjs
Releases · islumina/aifsmjs
Release list
v0.6.0
Breaking
Runtime.send()/Runtime.reset(): a call made while the runtime is already processing an event (from middleware, an effect handler, asubscribeor'transition'listener, or a child runtime's listener) is now queued and processed after the current event's last notification (run-to-completion) instead of running inside it, because the README-recommended "send from an effect" pattern delivered notifications in reverse and left subscribers holding a stale snapshot; the nested call returns the snapshot committed at that moment, and an error from a queued event propagates from the outermost call. Migration: readgetSnapshot()after the outersend()/reset()returns (or subscribe) instead of using a nested call's return value or reading the snapshot right after it, and catch errors around the outermost call rather than around a nestedsend().Runtime.subscribe()/Runtime.on()/Runtime.onTransition(): a listener removed while a notification is running (by its unsubscribe function,once, itssignal, ordispose()) is now skipped for the rest of that round instead of still receiving the in-flight event, per the ai*js fan-out re-entrancy rule. Migration: if a removed listener must still see the current event, remove it after the call returns (for examplequeueMicrotask(off)), and do not rely on the remaining listeners running after a mid-notificationdispose().mergeContext()/ action results: for an object context (not an array or binary view) a plain-object patch is now merged into a copy that keeps the context's prototype, where a class-instance context used to be replaced by the bare patch and lose its other fields and methods, and a non-nullish primitive result such asfalseor0now throwsInvalidActionResultErrorfromstep()/send()instead of replacing the context. Migration: return a plain-object partial (orundefined) from actions on object contexts; to replace the context wholesale, return a new instance or array (non-plain objects still replace it).Runtime.send()/Runtime.reset()sub-machine lifecycle: when a transition replaces a child, the new child is now constructed before the old child is disposed, so an init failure leaves the old child live and still returned bysubRuntime()(it used to be disposed first, leavingundefined). Migration: code that runs while a new child is being created must not assume the previous sibling child is already disposed, and code that expectedsubRuntime()to beundefinedafter aSubMachineErrorwithphase: "init"should expect the previous child.after()/createScheduler().after(): anmsthat isNaN,±Infinity, negative or not a number now throwsRangeError, and a non-functionfnthrowsTypeError, synchronously and before any timer is set (they used to fire after about 1 ms, or throw later from inside the timer), and a finite delay above 2^31-1 ms is clamped to 2^31-1 instead of firing almost at once. Migration: pass a finitems >= 0and a function; callers usingInfinityto mean "never" should simply not schedule.defineMachine()/setup().defineMachine()/createMachine()/createRuntime()/Runtime.send()/Runtime.reset()/Runtime.subscribe()/Runtime.on()/Runtime.onTransition(): argument misuse now throwsInvalidDefinitionError(aifsmjs: <subject> must be <constraint>) at the call — a non-object definition,states, state or transition entry; a non-objectimplor options object, or amiddlewareoption that is not an array of functions; an event that is not an object with a stringtype; a non-function listener or an unknownon()event type — instead of a bareTypeError(at the call or at a latersend()), a listener that threw at every notification, or a silently accepted value. Migration: pass{}asimplwhen a machine uses no named implementations, pass object events with a stringtypeand function listeners, and catchInvalidDefinitionErrorwhere you caughtTypeError.
Changes
- Added:
InvalidActionResultError(root export,name === "InvalidActionResultError", with the offendingactionName) for an action that returns a non-nullish primitive for an object context. - Added:
mergeContext(current, patch, actionName?)takes an optional action name for that error (default"<inline>"). - Changed:
InvalidDefinitionErroris also the argument-validation error of the definition/runtime boundary; its non-objectstatesmessage now readsaifsmjs: definition states must be an object. - Changed: an async effect rejection with no
'error'listener (none registered, or cleared bydispose()) is still discarded, but is now reported viaconsole.warnwhenNODE_ENV !== "production"; production behaviour is unchanged and it never becomes an unhandled rejection. - Changed:
aifsmjs/pbt'spropertiesis a frozen object carrying the same eight functions instead of a module namespace object, which drops tsup's shared__exporthelper chunk (about 210 B gzip) from every subpath entry. - Changed:
step()returns a shared frozen emptyeffectsarray when nothing fires. - Changed: size budgets in
scripts/check-size.mjs(maintainer-approved for 0.6.0):dist/index.js6,500 -> 6,700 B anddist/pbt/index.js8,500 -> 8,800 B, other budgets unchanged; measured gzip closures 0.5.9 -> 0.6.0: index 6,359 -> 6,654, guards 1,375 -> 1,161, effects 1,574 -> 1,365, inspect 552 -> 329, replay 3,115 -> 3,139, pbt 8,471 -> 8,718, timer 1,071 -> 1,018 B. - Fixed: transition/implementation lookups (
state.on[event.type], guard/action/effect refs) now resolve by own key only, so an undeclared event type or ref named after anObject.prototypemember (toString,constructor,__proto__, ...) is no longer treated as a declared transition. - Fixed:
deepFreezeno longer throws on binary data (ArrayBufferviews, e.g.Uint8Array) reached through context or event payloads, in dev snapshots or via middleware in production. - Fixed:
deepFreezerecurses through an object that is already shallow-frozen (e.g. an effect descriptor), instead of stopping there — middleware can no longer mutate an effect payload before dispatch, and an already shallow-frozen dev context is still deep-frozen. - Fixed: runtime event listeners are isolated per-listener — a throwing
'dispose'or'error'listener no longer prevents later listeners for the same event from running. - Fixed:
assignDoesNotMutatedetects mutation by a structural fingerprint instead ofstructuredClone, so it no longer false-fails for a pure machine whose context holds a class instance or a callback. - Fixed:
setup().defineMachine()infersStatesfromkeyof statesonly, so a terminal state written as{}or{ final: true }no longer collapses the inferred state union. - Fixed: an explicit
context: undefinedpassed todefineMachine/setup().defineMachinenow defaults to{}, the same as an absentcontextkey. - Fixed: dev-mode detection reads
process.env.NODE_ENVdirectly, so Vite / webpack 5 define-replacement enables dev-only deep-freezing in browser builds that have noprocessglobal. - Fixed: the PBT
snapshotAlwaysFrozenandreachableStatesSubsetDeclaredproperties no longer dispatch real effects while driving generated commands through a runtime. - Fixed:
createScheduler().after()mergessignal/setTimeout/clearTimeoutfield-by-field with??instead of an object spread, so an explicitly-undefined per-call option no longer silently overrides the scheduler's default. - Fixed:
MachineConfig, the parameter type ofdefineMachine, is re-exported from the package root. - Fixed:
reset()now notifies subscribers, middleware (changed: true) and'transition'listeners when the context reference (or status) differs from the initial snapshot; it used to compare the state value alone and stay silent. - Fixed: middleware no longer deep-freezes the caller's event object (and its payload graph) in any
NODE_ENV;MiddlewareContext.eventis the caller's object, passed unfrozen. - Fixed: a parent
send()/reset()issued from a child's'dispose'listener during a transition is queued until the transition commits, so a live child can no longer be left in a state that has nosub. - Fixed: a parent disposed by a child's
'dispose'listener during a transition no longer adopts the replacement child; the replacement is disposed andsubRuntime()returnsundefined. - Fixed:
send()decides whether a same-value transition is external from the guard pass that produced the snapshot, so each guard runs once per event and a non-idempotent guard can no longer desync the sub-machine lifecycle from the committed state. - Fixed:
defineMachine()rejects a sub-machine cycle through initial states withInvalidDefinitionErrorinstead of lettingcreateRuntime()recurse until the stack overflows; self-references through non-initial states stay legal. - Fixed:
snapshotAlwaysFrozen,reachableStatesSubsetDeclaredandreplayEqualsFolddispose the runtime they create for each generated run, so itsAbortSignalfires and no run leaks a live runtime. - Fixed:
package.jsonexportsneststypesunderimportandrequire(require.typespoints at the.d.ctsfiles) for every subpath, sonode16/nodenextCommonJS consumers no longer hit TS1479 / TS1471;verify-exportswalks nested conditions. - Docs: corrected the
aifsmjs/effectsPublic Surface row (README/README_ZHTW) to name the real export,createEnqueuer(), instead ofenqueue.effect(). - Docs: STABILITY.md's Behavioral Contract states the run-to-completion and fan-out clauses, the reset, merge, argument-validation and timer rules, and the new sub-machine order; README and README_ZHTW Lifecycle Rules and Sharp Edges mirror them, and the
Runtime/MiddlewareContextJSDoc says the same.