aifsmjs 0.5.9 → 0.6.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 +14 -7
- package/README_ZHTW.md +14 -7
- package/dist/{chunk-ZLQ7HZCE.js → chunk-D6H64FSI.js} +66 -46
- package/dist/chunk-D6H64FSI.js.map +1 -0
- package/dist/{chunk-NEJYZAKR.js → chunk-Q45LGXHO.js} +3 -3
- package/dist/{chunk-NEJYZAKR.js.map → chunk-Q45LGXHO.js.map} +1 -1
- package/dist/{chunk-CDK25FTD.cjs → chunk-QCTA2X4J.cjs} +7 -3
- package/dist/chunk-QCTA2X4J.cjs.map +1 -0
- package/dist/{chunk-A7U7QQL5.js → chunk-SSNKGEVB.js} +7 -4
- package/dist/chunk-SSNKGEVB.js.map +1 -0
- package/dist/{chunk-I354FONA.cjs → chunk-VGLF5NQH.cjs} +69 -46
- package/dist/chunk-VGLF5NQH.cjs.map +1 -0
- package/dist/{chunk-FHTQ7LSQ.cjs → chunk-VV5TKFQO.cjs} +4 -4
- package/dist/{chunk-FHTQ7LSQ.cjs.map → chunk-VV5TKFQO.cjs.map} +1 -1
- package/dist/{chunk-LG2AH5X6.js → chunk-XA24A7VP.js} +143 -152
- package/dist/chunk-XA24A7VP.js.map +1 -0
- package/dist/{chunk-TPCDOVU4.cjs → chunk-YK25NVFC.cjs} +147 -156
- package/dist/chunk-YK25NVFC.cjs.map +1 -0
- package/dist/effects/index.cjs +3 -4
- package/dist/effects/index.cjs.map +1 -1
- package/dist/effects/index.d.cts +1 -1
- package/dist/effects/index.d.ts +1 -1
- package/dist/effects/index.js +2 -3
- package/dist/effects/index.js.map +1 -1
- package/dist/guards/index.cjs +5 -6
- package/dist/guards/index.cjs.map +1 -1
- package/dist/guards/index.d.cts +1 -1
- package/dist/guards/index.d.ts +1 -1
- package/dist/guards/index.js +2 -3
- package/dist/guards/index.js.map +1 -1
- package/dist/index.cjs +31 -28
- package/dist/index.d.cts +66 -18
- package/dist/index.d.ts +66 -18
- package/dist/index.js +3 -4
- package/dist/inspect/index.cjs +0 -2
- package/dist/inspect/index.cjs.map +1 -1
- package/dist/inspect/index.d.cts +1 -1
- package/dist/inspect/index.d.ts +1 -1
- package/dist/inspect/index.js +0 -2
- package/dist/inspect/index.js.map +1 -1
- package/dist/pbt/index.cjs +65 -49
- package/dist/pbt/index.cjs.map +1 -1
- package/dist/pbt/index.d.cts +13 -17
- package/dist/pbt/index.d.ts +13 -17
- package/dist/pbt/index.js +57 -41
- package/dist/pbt/index.js.map +1 -1
- package/dist/replay/index.cjs +4 -5
- package/dist/replay/index.d.cts +1 -1
- package/dist/replay/index.d.ts +1 -1
- package/dist/replay/index.js +3 -4
- package/dist/timer/index.cjs +18 -5
- package/dist/timer/index.cjs.map +1 -1
- package/dist/timer/index.d.cts +6 -0
- package/dist/timer/index.d.ts +6 -0
- package/dist/timer/index.js +18 -5
- package/dist/timer/index.js.map +1 -1
- package/dist/{types-DIM7QTtf.d.ts → types-CrDxFfBx.d.cts} +64 -16
- package/dist/{types-DIM7QTtf.d.cts → types-CrDxFfBx.d.ts} +64 -16
- package/llms-full.txt +70 -16
- package/package.json +57 -22
- package/dist/chunk-A7U7QQL5.js.map +0 -1
- package/dist/chunk-CDK25FTD.cjs.map +0 -1
- package/dist/chunk-I354FONA.cjs.map +0 -1
- package/dist/chunk-LG2AH5X6.js.map +0 -1
- package/dist/chunk-PZ5AY32C.js +0 -9
- package/dist/chunk-PZ5AY32C.js.map +0 -1
- package/dist/chunk-Q7SFCCGT.cjs +0 -11
- package/dist/chunk-Q7SFCCGT.cjs.map +0 -1
- package/dist/chunk-TPCDOVU4.cjs.map +0 -1
- package/dist/chunk-ZLQ7HZCE.js.map +0 -1
package/dist/pbt/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/pbt/commands.ts","../../src/pbt/properties.ts","../../src/pbt/index.ts"],"names":["fc"],"mappings":";;;;;;;;;AAyBO,SAAS,aACd,GAAA,EACuB;AACvB,EAAA,OAAO;AAAA,IACL,OAAO,GAAA,CAAI,OAAA;AAAA,IACX,SAAS,GAAA,CAAI,OAAA;AAAA,IACb,QAAQ,GAAA,CAAI,MAAA,CAAO,IAAI,OAAO,CAAA,EAAG,QAAQ,OAAA,GAAU,QAAA;AAAA,IACnD,yBAAS,IAAI,GAAA,CAAI,CAAC,GAAA,CAAI,OAAO,CAAC;AAAA,GAChC;AACF;AAEA,IAAM,cAAN,MAEA;AAAA,EACE,WAAA,CACmB,KAAA,EACA,GAAA,EACA,IAAA,EACjB;AAHiB,IAAA,IAAA,CAAA,KAAA,GAAA,KAAA;AACA,IAAA,IAAA,CAAA,GAAA,GAAA,GAAA;AACA,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAAA,EAChB;AAAA,EAHgB,KAAA;AAAA,EACA,GAAA;AAAA,EACA,IAAA;AAAA,EAGnB,MAAM,EAAA,EAA8C;AAElD,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,GAAA,CAAI,GAA0B,CAAA,EAAoC;AAChE,IAAA,MAAM,MAAA,GAAgC,EAAE,WAAA,EAAY;AACpD,IAAA,CAAA,CAAE,IAAA,CAAK,KAAK,KAAK,CAAA;AACjB,IAAA,MAAM,KAAA,GAAQ,EAAE,WAAA,EAAY;AAE5B,IAAA,MAAM,SAAA,GAAY,KAAK,IAAA,CAAK,GAAA,EAAK,QAAQ,IAAA,CAAK,KAAA,EAAO,KAAK,IAAI,CAAA;AAI9D,IAAA,IAAI,SAAA,CAAU,QAAA,CAAS,KAAA,KAAU,KAAA,CAAM,KAAA,EAAO;AAC5C,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,qDAAA,EAAmD,MAAA,CAAO,SAAA,CAAU,QAAA,CAAS,KAAK,CAAC,CAAA,wBAAA,EAA2B,MAAA,CAAO,KAAA,CAAM,KAAK,CAAC,CAAA,QAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AAAA,OAC7J;AAAA,IACF;AAGA,IAAA,CAAA,CAAE,QAAQ,KAAA,CAAM,KAAA;AAChB,IAAA,CAAA,CAAE,UAAU,KAAA,CAAM,OAAA;AAClB,IAAA,CAAA,CAAE,SAAS,KAAA,CAAM,MAAA;AACjB,IAAA,CAAA,CAAE,OAAA,CAAQ,GAAA,CAAI,KAAA,CAAM,KAAK,CAAA;AAAA,EAC3B;AAAA,EAEA,QAAA,GAAmB;AACjB,IAAA,OAAO,CAAA,KAAA,EAAQ,IAAA,CAAK,SAAA,CAAU,IAAA,CAAK,KAAK,CAAC,CAAA,CAAA,CAAA;AAAA,EAC3C;AACF,CAAA;AAWO,SAAS,mBAAA,CACd,GAAA,EACA,IAAA,EACA,gBAAA,EACsD;AACtD,EAAA,MAAM,OAAqD,EAAC;AAC5D,EAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,MAAA,CAAO,gBAAgB,CAAA,EAAG;AACjD,IAAA,IAAA,CAAK,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,CAAC,KAAA,KAAU,IAAI,WAAA,CAAY,KAAA,EAAO,GAAA,EAAK,IAAI,CAAC,CAAC,CAAA;AAAA,EACjE;AACA,EAAA,OAAUA,GAAA,CAAA,QAAA,CAAS,IAAA,EAAM,EAAE,IAAA,EAAM,MAAM,CAAA;AACzC;;;AChGA,IAAA,kBAAA,GAAA,EAAA;AAAA,QAAA,CAAA,kBAAA,EAAA;AAAA,EAAA,SAAA,EAAA,MAAA,SAAA;AAAA,EAAA,mBAAA,EAAA,MAAA,mBAAA;AAAA,EAAA,aAAA,EAAA,MAAA,aAAA;AAAA,EAAA,uBAAA,EAAA,MAAA,uBAAA;AAAA,EAAA,6BAAA,EAAA,MAAA,6BAAA;AAAA,EAAA,gBAAA,EAAA,MAAA,gBAAA;AAAA,EAAA,oBAAA,EAAA,MAAA,oBAAA;AAAA,EAAA,gBAAA,EAAA,MAAA;AAAA,CAAA,CAAA;AAsBA,SAAS,gBAAgB,IAAA,EAAsD;AAC7E,EAAA,MAAM,MAA8B,EAAC;AACrC,EAAA,IAAI,IAAA,EAAM,OAAA,KAAY,MAAA,EAAW,GAAA,CAAI,UAAU,IAAA,CAAK,OAAA;AACpD,EAAA,IAAI,IAAA,EAAM,IAAA,KAAS,MAAA,EAAW,GAAA,CAAI,OAAO,IAAA,CAAK,IAAA;AAC9C,EAAA,IAAI,IAAA,EAAM,OAAA,EAAS,GAAA,CAAI,OAAA,GAAU,IAAA;AACjC,EAAA,OAAO,GAAA;AACT;AAiBO,SAAS,aAAA,CAAc,GAAY,CAAA,EAAqB;AAC7D,EAAA,OAAO,iBAAA,CAAkB,GAAG,CAAC,CAAA;AAC/B;AAMO,SAAS,oBAAA,CACd,GAAA,EACA,IAAA,EACA,gBAAA,EACA,IAAA,EACM;AACN,EAAG,GAAA,CAAA,MAAA;AAAA,IACE,aAAS,mBAAA,CAAoB,GAAA,EAAK,MAAM,gBAAgB,CAAA,EAAG,CAAC,IAAA,KAAS;AACtE,MAAA,MAAM,IAAA,GAAO,aAAA,CAAc,GAAA,EAAK,IAAI,CAAA;AACpC,MAAA,MAAM,KAAA,GAA+B,aAAa,GAAG,CAAA;AACrD,MAAG,aAAS,OAAO,EAAE,KAAA,EAAO,IAAA,KAAS,IAAI,CAAA;AACzC,MAAA,OAAO,MAAA,CAAO,QAAA,CAAS,IAAA,CAAK,WAAA,EAAa,CAAA;AAAA,IAC3C,CAAC,CAAA;AAAA,IACD,gBAAgB,IAAI;AAAA,GACtB;AACF;AAMO,SAAS,gBAAA,CACd,GAAA,EACA,IAAA,EACA,WAAA,EACA,IAAA,EACM;AACN,EAAG,GAAA,CAAA,MAAA;AAAA,IACE,GAAA,CAAA,QAAA,CAAY,GAAA,CAAA,QAAA,CAAS,WAAW,CAAA,EAAG,CAAC,CAAA,KAAM;AAC3C,MAAA,MAAM,OAAA,GAAU,gBAAgB,GAAG,CAAA;AACnC,MAAA,MAAM,MAAA,GAAS,KAAK,GAAA,EAAK,OAAA,EAAS,EAAE,IAAA,EAAM,CAAA,IAAuB,IAAI,CAAA;AACrE,MAAA,OAAO,MAAA,CAAO,YAAY,KAAA,IAAS,MAAA,CAAO,aAAa,OAAA,IAAW,MAAA,CAAO,QAAQ,MAAA,KAAW,CAAA;AAAA,IAC9F,CAAC,CAAA;AAAA,IACD,gBAAgB,IAAI;AAAA,GACtB;AACF;AAMO,SAAS,6BAAA,CAKd,GAAA,EACA,IAAA,EACA,gBAAA,EACA,IAAA,EACM;AACN,EAAA,MAAM,WAAW,IAAI,GAAA,CAAY,OAAO,IAAA,CAAK,GAAA,CAAI,MAAM,CAAC,CAAA;AACxD,EAAG,GAAA,CAAA,MAAA;AAAA,IACE,aAAS,mBAAA,CAAoB,GAAA,EAAK,MAAM,gBAAgB,CAAA,EAAG,CAAC,IAAA,KAAS;AACtE,MAAA,MAAM,IAAA,GAAO,aAAA,CAAc,GAAA,EAAK,IAAI,CAAA;AACpC,MAAA,MAAM,KAAA,GAA+B,aAAa,GAAG,CAAA;AACrD,MAAG,aAAS,OAAO,EAAE,KAAA,EAAO,IAAA,KAAS,IAAI,CAAA;AACzC,MAAA,KAAA,MAAW,CAAA,IAAK,MAAM,OAAA,EAAwB;AAE5C,QAAA,IAAI,CAAC,QAAA,CAAS,GAAA,CAAI,CAAC,GAAG,OAAO,KAAA;AAAA,MAC/B;AACA,MAAA,OAAO,QAAA,CAAS,GAAA,CAAI,IAAA,CAAK,WAAA,GAAc,KAAK,CAAA;AAAA,IAC9C,CAAC,CAAA;AAAA,IACD,gBAAgB,IAAI;AAAA,GACtB;AACF;AAOO,SAAS,gBAAA,CACd,GAAA,EACA,IAAA,EACA,gBAAA,EACA,IAAA,EACM;AACN,EAAA,MAAM,WAAc,GAAA,CAAA,KAAA,CAAM,GAAG,MAAA,CAAO,MAAA,CAAO,gBAAgB,CAAC,CAAA;AAC5D,EAAG,GAAA,CAAA,MAAA;AAAA,IACE,GAAA,CAAA,QAAA,CAAY,UAAM,QAAA,EAAU,EAAE,WAAW,EAAA,EAAI,CAAA,EAAG,CAAC,MAAA,KAAW;AAC7D,MAAA,MAAM,OAAO,aAAA,CAAc,GAAA,EAAK,MAAM,EAAE,eAAA,EAAiB,OAAO,CAAA;AAChE,MAAA,KAAA,MAAW,CAAA,IAAK,MAAA,EAAQ,IAAA,CAAK,IAAA,CAAK,CAAC,CAAA;AACnC,MAAA,MAAM,IAAA,GAAO,KAAK,WAAA,EAAY;AAC9B,MAAA,MAAM,QAAA,GAAW,OAAO,eAAA,CAAgB,GAAG,GAAG,MAAA,EAAQ,GAAA,EAAK,IAAI,CAAA,CAAE,QAAA;AACjE,MAAA,OAAO,IAAA,CAAK,UAAU,QAAA,CAAS,KAAA,IAAS,cAAc,IAAA,CAAK,OAAA,EAAS,SAAS,OAAO,CAAA;AAAA,IACtF,CAAC,CAAA;AAAA,IACD,gBAAgB,IAAI;AAAA,GACtB;AACF;AAaO,SAAS,uBAAA,CACd,GAAA,EACA,IAAA,EACA,gBAAA,EACA,IAAA,EACM;AACN,EAAA,MAAM,gBAAgB,IAAI,KAAA;AAAA,IACxB,EAAC;AAAA,IACD;AAAA,MACE,GAAA,EAAK,MAAM,MAAM;AAAA;AACnB,GACF;AACA,EAAA,MAAM,WAAA,GAAyC;AAAA,IAC7C,GAAG,IAAA;AAAA,IACH,MAAA,EAAQ;AAAA,GACV;AAIA,EAAA,MAAM,cAAA,GAAiB,CAAC,KAAA,EAAe,SAAA,KAA+B;AACpE,IAAA,MAAM,UAAA,GAAa,qBAAqB,GAAA,CAAI,MAAA,CAAO,KAAK,CAAA,EAAG,EAAA,GAAK,SAAS,CAAC,CAAA;AAC1E,IAAA,OAAO,UAAA,CAAW,SAAS,CAAA,IAAK,UAAA,CAAW,MAAM,CAAC,CAAA,KAAM,CAAA,CAAE,KAAA,KAAU,MAAS,CAAA;AAAA,EAC/E,CAAA;AACA,EAAG,GAAA,CAAA,MAAA;AAAA,IACE,GAAA,CAAA,QAAA;AAAA,MACE,GAAA,CAAA,KAAA,CAAS,GAAA,CAAA,KAAA,CAAM,GAAG,MAAA,CAAO,MAAA,CAAO,gBAAgB,CAAC,CAAA,EAAG,EAAE,SAAA,EAAW,EAAA,EAAI,CAAA;AAAA,MACxE,CAAC,MAAA,KAAW;AACV,QAAA,IAAI,IAAA,GAAO,gBAAgB,GAAG,CAAA;AAC9B,QAAA,KAAA,MAAW,KAAK,MAAA,EAAQ;AACtB,UAAA,MAAM,YAAA,GAAe,cAAA,CAAe,IAAA,CAAK,KAAA,EAAO,EAAE,IAAI,CAAA;AACtD,UAAA,MAAM,CAAA,GAAI,IAAA,CAAK,GAAA,EAAK,IAAA,EAAM,GAAG,WAAW,CAAA;AAIxC,UAAA,IAAI,YAAA,IAAgB,CAAA,CAAE,OAAA,KAAY,KAAA,EAAO,OAAO,KAAA;AAChD,UAAA,IAAA,GAAO,CAAA,CAAE,QAAA;AAAA,QACX;AACA,QAAA,OAAO,IAAA;AAAA,MACT;AAAA,KACF;AAAA,IACA,gBAAgB,IAAI;AAAA,GACtB;AACF;AAOO,SAAS,mBAAA,CACd,GAAA,EACA,IAAA,EACA,gBAAA,EACA,IAAA,EACM;AAEN,EAAA,MAAM,KAAA,GAAQ,EAAE,CAAA,EAAG,CAAA,EAAG,GAAG,CAAA,EAAE;AAC3B,EAAA,MAAM,SAAS,YAAA,CAAa,KAAA,EAAO,EAAE,CAAA,EAAG,GAAG,CAAA;AAE3C,EAAA,IAAI,MAAA,KAAW,KAAA,EAAO,MAAM,IAAI,MAAM,uDAAuD,CAAA;AAE7F,EAAG,GAAA,CAAA,MAAA;AAAA,IACE,GAAA,CAAA,QAAA;AAAA,MACE,GAAA,CAAA,KAAA,CAAS,GAAA,CAAA,KAAA,CAAM,GAAG,MAAA,CAAO,MAAA,CAAO,gBAAgB,CAAC,CAAA,EAAG,EAAE,SAAA,EAAW,EAAA,EAAI,CAAA;AAAA,MACxE,CAAC,MAAA,KAAW;AACV,QAAA,IAAI,IAAA,GAAO,gBAAgB,GAAG,CAAA;AAC9B,QAAA,KAAA,MAAW,KAAK,MAAA,EAAQ;AAMtB,UAAA,MAAM,SAAA,GAAY,eAAA,CAAgB,IAAA,CAAK,OAAO,CAAA;AAC9C,UAAA,IAAA,CAAK,GAAA,EAAK,IAAA,EAAM,CAAA,EAAG,IAAI,CAAA;AAEvB,UAAA,IAAI,CAAC,aAAA,CAAc,IAAA,CAAK,OAAA,EAAS,SAAS,GAAG,OAAO,KAAA;AAEpD,UAAA,IAAA,GAAO,IAAA,CAAK,GAAA,EAAK,IAAA,EAAM,CAAA,EAAG,IAAI,CAAA,CAAE,QAAA;AAAA,QAClC;AACA,QAAA,OAAO,IAAA;AAAA,MACT;AAAA,KACF;AAAA,IACA,gBAAgB,IAAI;AAAA,GACtB;AACF;AAMO,SAAS,SAAA,CACd,GAAA,EACA,IAAA,EACA,gBAAA,EACA,IAAA,EACM;AACN,EAAA,oBAAA,CAAqB,GAAA,EAAK,IAAA,EAAM,gBAAA,EAAkB,IAAI,CAAA;AACtD,EAAA,gBAAA,CAAiB,GAAA,EAAK,IAAA,EAAM,IAAA,EAAM,gBAAA,IAAoB,uBAAuB,IAAI,CAAA;AACjF,EAAA,6BAAA,CAA8B,GAAA,EAAK,IAAA,EAAM,gBAAA,EAAkB,IAAI,CAAA;AAC/D,EAAA,gBAAA,CAAiB,GAAA,EAAK,IAAA,EAAM,gBAAA,EAAkB,IAAI,CAAA;AAClD,EAAA,uBAAA,CAAwB,GAAA,EAAK,IAAA,EAAM,gBAAA,EAAkB,IAAI,CAAA;AACzD,EAAA,mBAAA,CAAoB,GAAA,EAAK,IAAA,EAAM,gBAAA,EAAkB,IAAI,CAAA;AACvD;;;AC1OO,IAAM,UAAA,GAAa","file":"index.js","sourcesContent":["import * as fc from \"fast-check\";\nimport { step } from \"../fsm/lifecycle.js\";\nimport type { Implementations, MachineDef, Runtime, Snapshot } from \"../fsm/types.js\";\n\n/**\n * Pure-model representation of an FSM run, used by `fc.commands`.\n * `reached` tracks every state visited so generic properties can check\n * containment without re-running.\n */\nexport type FsmModel<Ctx, States extends string> = {\n value: States;\n context: Ctx;\n status: \"active\" | \"final\";\n reached: Set<States>;\n};\n\nexport type EventArbitraries<Evt extends { type: string }> = Readonly<\n Record<string, fc.Arbitrary<Evt>>\n>;\n\nexport type FsmCommand<Ctx, Evt extends { type: string }, States extends string> = fc.Command<\n FsmModel<Ctx, States>,\n Runtime<Ctx, Evt, States>\n>;\n\nexport function initialModel<Ctx, Evt extends { type: string }, States extends string>(\n def: MachineDef<Ctx, Evt, States>,\n): FsmModel<Ctx, States> {\n return {\n value: def.initial,\n context: def.context,\n status: def.states[def.initial]?.final ? \"final\" : \"active\",\n reached: new Set([def.initial]),\n };\n}\n\nclass SendCommand<Ctx, Evt extends { type: string }, States extends string>\n implements FsmCommand<Ctx, Evt, States>\n{\n constructor(\n private readonly event: Evt,\n private readonly def: MachineDef<Ctx, Evt, States>,\n private readonly impl: Implementations<Ctx, Evt>,\n ) {}\n\n check(_m: Readonly<FsmModel<Ctx, States>>): boolean {\n // Every event is always applicable; invariants are asserted in run().\n return true;\n }\n\n run(m: FsmModel<Ctx, States>, r: Runtime<Ctx, Evt, States>): void {\n const before: Snapshot<Ctx, States> = r.getSnapshot();\n r.send(this.event);\n const after = r.getSnapshot();\n\n const predicted = step(this.def, before, this.event, this.impl);\n\n /* v8 ignore start — invariant guard: fires only when the live runtime\n diverges from the pure step() prediction, i.e. an internal bug. */\n if (predicted.snapshot.value !== after.value) {\n throw new Error(\n `aifsmjs/pbt: determinism violation — predicted \"${String(predicted.snapshot.value)}\" but runtime returned \"${String(after.value)}\" after ${this.toString()}`,\n );\n }\n /* v8 ignore stop */\n\n m.value = after.value;\n m.context = after.context as Ctx;\n m.status = after.status;\n m.reached.add(after.value);\n }\n\n toString(): string {\n return `send(${JSON.stringify(this.event)})`;\n }\n}\n\n/**\n * Build an `fc.Arbitrary` of FSM command sequences. Each command pulls one\n * event from the user-supplied arbitrary map and, when run, asserts that the\n * pure `step()` prediction matches the runtime's observable outcome.\n *\n * Pair this arbitrary with `fc.property(...)` inside a `fc.assert(...)` call,\n * or use the helpers in `aifsmjs/pbt` properties to get the six generic\n * invariants for free.\n */\nexport function commandsFromMachine<Ctx, Evt extends { type: string }, States extends string>(\n def: MachineDef<Ctx, Evt, States>,\n impl: Implementations<Ctx, Evt>,\n eventArbitraries: EventArbitraries<Evt>,\n): fc.Arbitrary<Iterable<FsmCommand<Ctx, Evt, States>>> {\n const arbs: fc.Arbitrary<FsmCommand<Ctx, Evt, States>>[] = [];\n for (const arb of Object.values(eventArbitraries)) {\n arbs.push(arb.map((event) => new SendCommand(event, def, impl)));\n }\n return fc.commands(arbs, { size: \"+1\" });\n}\n","import { isDeepStrictEqual } from \"node:util\";\nimport * as fc from \"fast-check\";\nimport { initialSnapshot } from \"../fsm/definition.js\";\nimport { step } from \"../fsm/lifecycle.js\";\nimport { normalizeTransitions } from \"../fsm/resolver.js\";\nimport { createRuntime } from \"../fsm/runtime.js\";\nimport type { Guard, Implementations, MachineDef } from \"../fsm/types.js\";\nimport { mergeContext } from \"../fsm/updater.js\";\nimport { replay } from \"../replay/index.js\";\nimport {\n type EventArbitraries,\n type FsmModel,\n commandsFromMachine,\n initialModel,\n} from \"./commands.js\";\n\nexport type AssertOpts = Readonly<{\n numRuns?: number;\n seed?: number;\n verbose?: boolean;\n}>;\n\nfunction buildAssertOpts(opts: AssertOpts | undefined): fc.Parameters<unknown> {\n const out: fc.Parameters<unknown> = {};\n if (opts?.numRuns !== undefined) out.numRuns = opts.numRuns;\n if (opts?.seed !== undefined) out.seed = opts.seed;\n if (opts?.verbose) out.verbose = true;\n return out;\n}\n\n/**\n * Structural deep-equality for two context values (C3). Backed by `node:util`\n * `isDeepStrictEqual`, replacing the previous `JSON.stringify(a) === JSON.stringify(b)`\n * oracle which was unsound:\n *\n * - key-order-sensitive → false-FAIL on `{a:1,b:2}` vs `{b:2,a:1}`;\n * - drops undefined keys → false-PASS on `{v:undefined,w:1}` vs `{w:1}`;\n * - lossy for `Map`/`Set`/`Date` (all serialise to `{}` or an ISO string);\n * - throws on `BigInt`.\n *\n * `isDeepStrictEqual` distinguishes present-but-undefined from absent keys,\n * compares `Map`/`Set`/`Date` by contents, and tolerates `BigInt` — exactly\n * the verdicts a context-equality oracle for PBT requires. No new dependency\n * (Node built-in; the package already targets Node >=18).\n */\nexport function contextEquals(a: unknown, b: unknown): boolean {\n return isDeepStrictEqual(a, b);\n}\n\n/**\n * #1 snapshotAlwaysFrozen — after any event sequence the live snapshot remains\n * frozen at the top level.\n */\nexport function snapshotAlwaysFrozen<Ctx, Evt extends { type: string }, States extends string>(\n def: MachineDef<Ctx, Evt, States>,\n impl: Implementations<Ctx, Evt>,\n eventArbitraries: EventArbitraries<Evt>,\n opts?: AssertOpts,\n): void {\n fc.assert(\n fc.property(commandsFromMachine(def, impl, eventArbitraries), (cmds) => {\n const real = createRuntime(def, impl);\n const model: FsmModel<Ctx, States> = initialModel(def);\n fc.modelRun(() => ({ model, real }), cmds);\n return Object.isFrozen(real.getSnapshot());\n }),\n buildAssertOpts(opts),\n );\n}\n\n/**\n * #2 unknownEventNoOp — sending an event whose `type` is not declared in any\n * state's `on` map never changes the snapshot.\n */\nexport function unknownEventNoOp<Ctx, Evt extends { type: string }, States extends string>(\n def: MachineDef<Ctx, Evt, States>,\n impl: Implementations<Ctx, Evt>,\n unknownType: string,\n opts?: AssertOpts,\n): void {\n fc.assert(\n fc.property(fc.constant(unknownType), (t) => {\n const initial = initialSnapshot(def);\n const result = step(def, initial, { type: t } as unknown as Evt, impl);\n return result.changed === false && result.snapshot === initial && result.effects.length === 0;\n }),\n buildAssertOpts(opts),\n );\n}\n\n/**\n * #3 reachableStatesSubsetDeclared — every state visited during a run belongs\n * to `def.states`.\n */\nexport function reachableStatesSubsetDeclared<\n Ctx,\n Evt extends { type: string },\n States extends string,\n>(\n def: MachineDef<Ctx, Evt, States>,\n impl: Implementations<Ctx, Evt>,\n eventArbitraries: EventArbitraries<Evt>,\n opts?: AssertOpts,\n): void {\n const declared = new Set<string>(Object.keys(def.states));\n fc.assert(\n fc.property(commandsFromMachine(def, impl, eventArbitraries), (cmds) => {\n const real = createRuntime(def, impl);\n const model: FsmModel<Ctx, States> = initialModel(def);\n fc.modelRun(() => ({ model, real }), cmds);\n for (const s of model.reached as Set<string>) {\n /* v8 ignore next — property failure branch; an unreachable state would indicate a bug. */\n if (!declared.has(s)) return false;\n }\n return declared.has(real.getSnapshot().value);\n }),\n buildAssertOpts(opts),\n );\n}\n\n/**\n * #4 replayEqualsFold — `replay(initial, log)` produces the same final state\n * as a live runtime fed the same events. Effects dispatched by the runtime are\n * ignored; the comparison is on `{ value, context }`.\n */\nexport function replayEqualsFold<Ctx, Evt extends { type: string }, States extends string>(\n def: MachineDef<Ctx, Evt, States>,\n impl: Implementations<Ctx, Evt>,\n eventArbitraries: EventArbitraries<Evt>,\n opts?: AssertOpts,\n): void {\n const eventArb = fc.oneof(...Object.values(eventArbitraries));\n fc.assert(\n fc.property(fc.array(eventArb, { maxLength: 32 }), (events) => {\n const real = createRuntime(def, impl, { dispatchEffects: false });\n for (const e of events) real.send(e);\n const live = real.getSnapshot();\n const replayed = replay(initialSnapshot(def), events, def, impl).snapshot;\n return live.value === replayed.value && contextEquals(live.context, replayed.context);\n }),\n buildAssertOpts(opts),\n );\n}\n\n/**\n * #5 guardsFalseNoTransition — when every candidate transition for the current\n * (state, event) pair carries a guard and every guard returns `false`, the\n * snapshot is unchanged (`changed === false`).\n *\n * Implementation: synthesise an impl that forces every guard to `false`, then\n * for each step whose candidate list is fully guarded, assert the step did not\n * change state. Steps with an unguarded fallback candidate (which fires even\n * when all guards are false) are skipped — the README claim is specifically\n * about the all-guards-false case.\n */\nexport function guardsFalseNoTransition<Ctx, Evt extends { type: string }, States extends string>(\n def: MachineDef<Ctx, Evt, States>,\n impl: Implementations<Ctx, Evt>,\n eventArbitraries: EventArbitraries<Evt>,\n opts?: AssertOpts,\n): void {\n const blockedGuards = new Proxy(\n {},\n {\n get: () => () => false,\n },\n ) as Readonly<Record<string, Guard<Ctx, Evt>>>;\n const blockedImpl: Implementations<Ctx, Evt> = {\n ...impl,\n guards: blockedGuards,\n };\n // True when every candidate transition for (value, eventType) carries a\n // guard — i.e. blocking all guards leaves no unconditional fallback, so a\n // correct step() must report changed === false.\n const isFullyGuarded = (value: States, eventType: string): boolean => {\n const candidates = normalizeTransitions(def.states[value]?.on?.[eventType]);\n return candidates.length > 0 && candidates.every((t) => t.guard !== undefined);\n };\n fc.assert(\n fc.property(\n fc.array(fc.oneof(...Object.values(eventArbitraries)), { maxLength: 16 }),\n (events) => {\n let snap = initialSnapshot(def);\n for (const e of events) {\n const fullyGuarded = isFullyGuarded(snap.value, e.type);\n const r = step(def, snap, e, blockedImpl);\n // The named invariant: all guards false + no unconditional fallback\n // ⇒ no transition. Without this assertion the property was vacuous\n // (it only failed if step() threw).\n if (fullyGuarded && r.changed !== false) return false;\n snap = r.snapshot;\n }\n return true;\n },\n ),\n buildAssertOpts(opts),\n );\n}\n\n/**\n * #6 assignDoesNotMutate — running an `assign`-style action never mutates the\n * previous context object. Verified by deep-equality check on a snapshot taken\n * before each event.\n */\nexport function assignDoesNotMutate<Ctx, Evt extends { type: string }, States extends string>(\n def: MachineDef<Ctx, Evt, States>,\n impl: Implementations<Ctx, Evt>,\n eventArbitraries: EventArbitraries<Evt>,\n opts?: AssertOpts,\n): void {\n // Quick sanity guard: mergeContext is the only context mutator used by step.\n const dummy = { a: 1, b: 2 };\n const merged = mergeContext(dummy, { b: 3 });\n /* v8 ignore next — invariant guard; mergeContext returning the same ref would mean unit tests have already broken. */\n if (merged === dummy) throw new Error(\"aifsmjs/pbt: mergeContext returned the same reference\");\n\n fc.assert(\n fc.property(\n fc.array(fc.oneof(...Object.values(eventArbitraries)), { maxLength: 16 }),\n (events) => {\n let snap = initialSnapshot(def);\n for (const e of events) {\n // Structural snapshot of the pre-step context (C3). structuredClone +\n // contextEquals replaces the old JSON.stringify round-trip, which was\n // lossy for Map/Set/Date and threw on BigInt. structuredClone produces\n // an independent copy so a subsequent in-place mutation by step() is\n // detectable by deep comparison.\n const beforeCtx = structuredClone(snap.context);\n step(def, snap, e, impl);\n /* v8 ignore next — property failure branch; step() mutating snap.context would indicate a bug. */\n if (!contextEquals(snap.context, beforeCtx)) return false;\n // Continue with the actual result for subsequent events\n snap = step(def, snap, e, impl).snapshot;\n }\n return true;\n },\n ),\n buildAssertOpts(opts),\n );\n}\n\n/**\n * Assert every generic property in one call. Use this when you don't need\n * fine-grained control over per-property options.\n */\nexport function assertAll<Ctx, Evt extends { type: string }, States extends string>(\n def: MachineDef<Ctx, Evt, States>,\n impl: Implementations<Ctx, Evt>,\n eventArbitraries: EventArbitraries<Evt>,\n opts?: AssertOpts & { unknownEventType?: string },\n): void {\n snapshotAlwaysFrozen(def, impl, eventArbitraries, opts);\n unknownEventNoOp(def, impl, opts?.unknownEventType ?? \"__AIFSMJS_UNKNOWN__\", opts);\n reachableStatesSubsetDeclared(def, impl, eventArbitraries, opts);\n replayEqualsFold(def, impl, eventArbitraries, opts);\n guardsFalseNoTransition(def, impl, eventArbitraries, opts);\n assignDoesNotMutate(def, impl, eventArbitraries, opts);\n}\n","export {\n commandsFromMachine,\n initialModel,\n type EventArbitraries,\n type FsmCommand,\n type FsmModel,\n} from \"./commands.js\";\n\nexport {\n assertAll,\n assignDoesNotMutate,\n guardsFalseNoTransition,\n reachableStatesSubsetDeclared,\n replayEqualsFold,\n snapshotAlwaysFrozen,\n unknownEventNoOp,\n type AssertOpts,\n} from \"./properties.js\";\n\n// Convenience namespace mirroring the README:\n// import { properties } from \"aifsmjs/pbt\"\n// properties.snapshotAlwaysFrozen(...)\nimport * as propertiesModule from \"./properties.js\";\nexport const properties = propertiesModule;\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/pbt/commands.ts","../../src/pbt/properties.ts","../../src/pbt/index.ts"],"names":["fc"],"mappings":";;;;;;;;AAyBO,SAAS,aACd,GAAA,EACuB;AACvB,EAAA,OAAO;AAAA,IACL,OAAO,GAAA,CAAI,OAAA;AAAA,IACX,SAAS,GAAA,CAAI,OAAA;AAAA,IACb,QAAQ,GAAA,CAAI,MAAA,CAAO,IAAI,OAAO,CAAA,EAAG,QAAQ,OAAA,GAAU,QAAA;AAAA,IACnD,yBAAS,IAAI,GAAA,CAAI,CAAC,GAAA,CAAI,OAAO,CAAC;AAAA,GAChC;AACF;AAEA,IAAM,cAAN,MAEA;AAAA,EACE,WAAA,CACmB,KAAA,EACA,GAAA,EACA,IAAA,EACjB;AAHiB,IAAA,IAAA,CAAA,KAAA,GAAA,KAAA;AACA,IAAA,IAAA,CAAA,GAAA,GAAA,GAAA;AACA,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAAA,EAChB;AAAA,EAHgB,KAAA;AAAA,EACA,GAAA;AAAA,EACA,IAAA;AAAA,EAGnB,MAAM,EAAA,EAA8C;AAElD,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,GAAA,CAAI,GAA0B,CAAA,EAAoC;AAChE,IAAA,MAAM,MAAA,GAAgC,EAAE,WAAA,EAAY;AACpD,IAAA,CAAA,CAAE,IAAA,CAAK,KAAK,KAAK,CAAA;AACjB,IAAA,MAAM,KAAA,GAAQ,EAAE,WAAA,EAAY;AAE5B,IAAA,MAAM,SAAA,GAAY,KAAK,IAAA,CAAK,GAAA,EAAK,QAAQ,IAAA,CAAK,KAAA,EAAO,KAAK,IAAI,CAAA;AAI9D,IAAA,IAAI,SAAA,CAAU,QAAA,CAAS,KAAA,KAAU,KAAA,CAAM,KAAA,EAAO;AAC5C,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,qDAAA,EAAmD,MAAA,CAAO,SAAA,CAAU,QAAA,CAAS,KAAK,CAAC,CAAA,wBAAA,EAA2B,MAAA,CAAO,KAAA,CAAM,KAAK,CAAC,CAAA,QAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AAAA,OAC7J;AAAA,IACF;AAGA,IAAA,CAAA,CAAE,QAAQ,KAAA,CAAM,KAAA;AAChB,IAAA,CAAA,CAAE,UAAU,KAAA,CAAM,OAAA;AAClB,IAAA,CAAA,CAAE,SAAS,KAAA,CAAM,MAAA;AACjB,IAAA,CAAA,CAAE,OAAA,CAAQ,GAAA,CAAI,KAAA,CAAM,KAAK,CAAA;AAAA,EAC3B;AAAA,EAEA,QAAA,GAAmB;AACjB,IAAA,OAAO,CAAA,KAAA,EAAQ,IAAA,CAAK,SAAA,CAAU,IAAA,CAAK,KAAK,CAAC,CAAA,CAAA,CAAA;AAAA,EAC3C;AACF,CAAA;AAWO,SAAS,mBAAA,CACd,GAAA,EACA,IAAA,EACA,gBAAA,EACsD;AACtD,EAAA,MAAM,OAAqD,EAAC;AAC5D,EAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,MAAA,CAAO,gBAAgB,CAAA,EAAG;AACjD,IAAA,IAAA,CAAK,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,CAAC,KAAA,KAAU,IAAI,WAAA,CAAY,KAAA,EAAO,GAAA,EAAK,IAAI,CAAC,CAAC,CAAA;AAAA,EACjE;AACA,EAAA,OAAUA,GAAA,CAAA,QAAA,CAAS,IAAA,EAAM,EAAE,IAAA,EAAM,MAAM,CAAA;AACzC;AC1EA,SAAS,gBAAgB,IAAA,EAAsD;AAC7E,EAAA,MAAM,MAA8B,EAAC;AACrC,EAAA,IAAI,IAAA,EAAM,OAAA,KAAY,MAAA,EAAW,GAAA,CAAI,UAAU,IAAA,CAAK,OAAA;AACpD,EAAA,IAAI,IAAA,EAAM,IAAA,KAAS,MAAA,EAAW,GAAA,CAAI,OAAO,IAAA,CAAK,IAAA;AAC9C,EAAA,IAAI,IAAA,EAAM,OAAA,EAAS,GAAA,CAAI,OAAA,GAAU,IAAA;AACjC,EAAA,OAAO,GAAA;AACT;AAiBO,SAAS,aAAA,CAAc,GAAY,CAAA,EAAqB;AAC7D,EAAA,OAAO,iBAAA,CAAkB,GAAG,CAAC,CAAA;AAC/B;AAMO,SAAS,oBAAA,CACd,GAAA,EACA,IAAA,EACA,gBAAA,EACA,IAAA,EACM;AACN,EAAG,GAAA,CAAA,MAAA;AAAA,IACE,aAAS,mBAAA,CAAoB,GAAA,EAAK,MAAM,gBAAgB,CAAA,EAAG,CAAC,IAAA,KAAS;AACtE,MAAA,MAAM,OAAO,aAAA,CAAc,GAAA,EAAK,MAAM,EAAE,eAAA,EAAiB,OAAO,CAAA;AAGhE,MAAA,IAAI;AACF,QAAA,MAAM,KAAA,GAA+B,aAAa,GAAG,CAAA;AACrD,QAAG,aAAS,OAAO,EAAE,KAAA,EAAO,IAAA,KAAS,IAAI,CAAA;AACzC,QAAA,OAAO,MAAA,CAAO,QAAA,CAAS,IAAA,CAAK,WAAA,EAAa,CAAA;AAAA,MAC3C,CAAA,SAAE;AACA,QAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,MACf;AAAA,IACF,CAAC,CAAA;AAAA,IACD,gBAAgB,IAAI;AAAA,GACtB;AACF;AAMO,SAAS,gBAAA,CACd,GAAA,EACA,IAAA,EACA,WAAA,EACA,IAAA,EACM;AACN,EAAG,GAAA,CAAA,MAAA;AAAA,IACE,GAAA,CAAA,QAAA,CAAY,GAAA,CAAA,QAAA,CAAS,WAAW,CAAA,EAAG,CAAC,CAAA,KAAM;AAC3C,MAAA,MAAM,OAAA,GAAU,gBAAgB,GAAG,CAAA;AACnC,MAAA,MAAM,MAAA,GAAS,KAAK,GAAA,EAAK,OAAA,EAAS,EAAE,IAAA,EAAM,CAAA,IAAuB,IAAI,CAAA;AACrE,MAAA,OAAO,MAAA,CAAO,YAAY,KAAA,IAAS,MAAA,CAAO,aAAa,OAAA,IAAW,MAAA,CAAO,QAAQ,MAAA,KAAW,CAAA;AAAA,IAC9F,CAAC,CAAA;AAAA,IACD,gBAAgB,IAAI;AAAA,GACtB;AACF;AAMO,SAAS,6BAAA,CAKd,GAAA,EACA,IAAA,EACA,gBAAA,EACA,IAAA,EACM;AACN,EAAA,MAAM,WAAW,IAAI,GAAA,CAAY,OAAO,IAAA,CAAK,GAAA,CAAI,MAAM,CAAC,CAAA;AACxD,EAAG,GAAA,CAAA,MAAA;AAAA,IACE,aAAS,mBAAA,CAAoB,GAAA,EAAK,MAAM,gBAAgB,CAAA,EAAG,CAAC,IAAA,KAAS;AACtE,MAAA,MAAM,OAAO,aAAA,CAAc,GAAA,EAAK,MAAM,EAAE,eAAA,EAAiB,OAAO,CAAA;AAChE,MAAA,IAAI;AACF,QAAA,MAAM,KAAA,GAA+B,aAAa,GAAG,CAAA;AACrD,QAAG,aAAS,OAAO,EAAE,KAAA,EAAO,IAAA,KAAS,IAAI,CAAA;AACzC,QAAA,KAAA,MAAW,CAAA,IAAK,MAAM,OAAA,EAAwB;AAE5C,UAAA,IAAI,CAAC,QAAA,CAAS,GAAA,CAAI,CAAC,GAAG,OAAO,KAAA;AAAA,QAC/B;AACA,QAAA,OAAO,QAAA,CAAS,GAAA,CAAI,IAAA,CAAK,WAAA,GAAc,KAAK,CAAA;AAAA,MAC9C,CAAA,SAAE;AACA,QAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,MACf;AAAA,IACF,CAAC,CAAA;AAAA,IACD,gBAAgB,IAAI;AAAA,GACtB;AACF;AAOO,SAAS,gBAAA,CACd,GAAA,EACA,IAAA,EACA,gBAAA,EACA,IAAA,EACM;AACN,EAAA,MAAM,WAAc,GAAA,CAAA,KAAA,CAAM,GAAG,MAAA,CAAO,MAAA,CAAO,gBAAgB,CAAC,CAAA;AAC5D,EAAG,GAAA,CAAA,MAAA;AAAA,IACE,GAAA,CAAA,QAAA,CAAY,UAAM,QAAA,EAAU,EAAE,WAAW,EAAA,EAAI,CAAA,EAAG,CAAC,MAAA,KAAW;AAC7D,MAAA,MAAM,OAAO,aAAA,CAAc,GAAA,EAAK,MAAM,EAAE,eAAA,EAAiB,OAAO,CAAA;AAChE,MAAA,IAAI;AACF,QAAA,KAAA,MAAW,CAAA,IAAK,MAAA,EAAQ,IAAA,CAAK,IAAA,CAAK,CAAC,CAAA;AACnC,QAAA,MAAM,IAAA,GAAO,KAAK,WAAA,EAAY;AAC9B,QAAA,MAAM,QAAA,GAAW,OAAO,eAAA,CAAgB,GAAG,GAAG,MAAA,EAAQ,GAAA,EAAK,IAAI,CAAA,CAAE,QAAA;AACjE,QAAA,OAAO,IAAA,CAAK,UAAU,QAAA,CAAS,KAAA,IAAS,cAAc,IAAA,CAAK,OAAA,EAAS,SAAS,OAAO,CAAA;AAAA,MACtF,CAAA,SAAE;AACA,QAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,MACf;AAAA,IACF,CAAC,CAAA;AAAA,IACD,gBAAgB,IAAI;AAAA,GACtB;AACF;AAaO,SAAS,uBAAA,CACd,GAAA,EACA,IAAA,EACA,gBAAA,EACA,IAAA,EACM;AAIN,EAAA,MAAM,gBAAgB,IAAI,KAAA;AAAA,IACxB,EAAC;AAAA,IACD;AAAA,MACE,GAAA,EAAK,MAAM,MAAM,KAAA;AAAA,MACjB,wBAAA,EAA0B,OAAO,EAAE,YAAA,EAAc,IAAA,EAAK;AAAA;AACxD,GACF;AACA,EAAA,MAAM,WAAA,GAAyC;AAAA,IAC7C,GAAG,IAAA;AAAA,IACH,MAAA,EAAQ;AAAA,GACV;AAIA,EAAA,MAAM,cAAA,GAAiB,CAAC,KAAA,EAAe,SAAA,KAA+B;AACpE,IAAA,MAAM,UAAA,GAAa,qBAAqB,QAAA,CAAS,GAAA,CAAI,OAAO,KAAK,CAAA,EAAG,EAAA,EAAI,SAAS,CAAC,CAAA;AAClF,IAAA,OAAO,UAAA,CAAW,SAAS,CAAA,IAAK,UAAA,CAAW,MAAM,CAAC,CAAA,KAAM,CAAA,CAAE,KAAA,KAAU,MAAS,CAAA;AAAA,EAC/E,CAAA;AACA,EAAG,GAAA,CAAA,MAAA;AAAA,IACE,GAAA,CAAA,QAAA;AAAA,MACE,GAAA,CAAA,KAAA,CAAS,GAAA,CAAA,KAAA,CAAM,GAAG,MAAA,CAAO,MAAA,CAAO,gBAAgB,CAAC,CAAA,EAAG,EAAE,SAAA,EAAW,EAAA,EAAI,CAAA;AAAA,MACxE,CAAC,MAAA,KAAW;AACV,QAAA,IAAI,IAAA,GAAO,gBAAgB,GAAG,CAAA;AAC9B,QAAA,KAAA,MAAW,KAAK,MAAA,EAAQ;AACtB,UAAA,MAAM,YAAA,GAAe,cAAA,CAAe,IAAA,CAAK,KAAA,EAAO,EAAE,IAAI,CAAA;AACtD,UAAA,MAAM,CAAA,GAAI,IAAA,CAAK,GAAA,EAAK,IAAA,EAAM,GAAG,WAAW,CAAA;AAIxC,UAAA,IAAI,YAAA,IAAgB,CAAA,CAAE,OAAA,KAAY,KAAA,EAAO,OAAO,KAAA;AAChD,UAAA,IAAA,GAAO,CAAA,CAAE,QAAA;AAAA,QACX;AACA,QAAA,OAAO,IAAA;AAAA,MACT;AAAA,KACF;AAAA,IACA,gBAAgB,IAAI;AAAA,GACtB;AACF;AAGA,IAAM,QAAA,GAAW,CAAC,CAAA,KAAyB;AAAA,EACzC,GAAG,OAAA,CAAQ,OAAA,CAAQ,CAAC,CAAA,CAAE,OAAA,CAAQ,CAAC,CAAA,KAAM,CAAC,CAAA,EAAI,CAAA,CAAmC,CAAC,CAAC,CAAC,CAAA;AAAA,EAChF,GAAI,CAAA,YAAa,GAAA,IAAO,CAAA,YAAa,GAAA,GAAM,CAAC,GAAG,CAAA,CAAE,OAAA,EAAS,CAAA,CAAE,IAAA,KAAS,EAAC;AAAA,EACtE,CAAA,YAAa,IAAA,IAAQ,CAAA,CAAE,OAAA;AACzB,CAAA;AAOO,SAAS,mBAAA,CACd,GAAA,EACA,IAAA,EACA,gBAAA,EACA,IAAA,EACM;AACN,EAAG,GAAA,CAAA,MAAA;AAAA,IACE,GAAA,CAAA,QAAA;AAAA,MACE,GAAA,CAAA,KAAA,CAAS,GAAA,CAAA,KAAA,CAAM,GAAG,MAAA,CAAO,MAAA,CAAO,gBAAgB,CAAC,CAAA,EAAG,EAAE,SAAA,EAAW,EAAA,EAAI,CAAA;AAAA,MACxE,CAAC,MAAA,KAAW;AACV,QAAA,IAAI,IAAA,GAAO,gBAAgB,GAAG,CAAA;AAC9B,QAAA,KAAA,MAAW,KAAK,MAAA,EAAQ;AAEtB,UAAA,MAAM,MAAA,uBAAa,GAAA,EAAuB;AAC1C,UAAA,MAAM,IAAA,GAAO,CAAC,CAAA,KAAqB;AACjC,YAAA,IAAI,CAAC,KAAK,OAAO,CAAA,KAAM,YAAY,MAAA,CAAO,GAAA,CAAI,CAAC,CAAA,EAAG;AAClD,YAAA,MAAM,GAAA,GAAM,SAAS,CAAC,CAAA;AACtB,YAAA,MAAA,CAAO,GAAA,CAAI,GAAG,GAAG,CAAA;AACjB,YAAA,GAAA,CAAI,QAAQ,IAAI,CAAA;AAAA,UAClB,CAAA;AACA,UAAA,IAAA,CAAK,KAAK,OAAO,CAAA;AAEjB,UAAA,IAAA,GAAO,IAAA,CAAK,GAAA,EAAK,IAAA,EAAM,CAAA,EAAG,IAAI,CAAA,CAAE,QAAA;AAChC,UAAA,KAAA,MAAW,CAAC,GAAA,EAAK,GAAG,CAAA,IAAK,MAAA,EAAQ,IAAI,CAAC,aAAA,CAAc,QAAA,CAAS,GAAG,CAAA,EAAG,GAAG,GAAG,OAAO,KAAA;AAAA,QAClF;AACA,QAAA,OAAO,IAAA;AAAA,MACT;AAAA,KACF;AAAA,IACA,gBAAgB,IAAI;AAAA,GACtB;AACF;AAMO,SAAS,SAAA,CACd,GAAA,EACA,IAAA,EACA,gBAAA,EACA,IAAA,EACM;AACN,EAAA,oBAAA,CAAqB,GAAA,EAAK,IAAA,EAAM,gBAAA,EAAkB,IAAI,CAAA;AACtD,EAAA,gBAAA,CAAiB,GAAA,EAAK,IAAA,EAAM,IAAA,EAAM,gBAAA,IAAoB,uBAAuB,IAAI,CAAA;AACjF,EAAA,6BAAA,CAA8B,GAAA,EAAK,IAAA,EAAM,gBAAA,EAAkB,IAAI,CAAA;AAC/D,EAAA,gBAAA,CAAiB,GAAA,EAAK,IAAA,EAAM,gBAAA,EAAkB,IAAI,CAAA;AAClD,EAAA,uBAAA,CAAwB,GAAA,EAAK,IAAA,EAAM,gBAAA,EAAkB,IAAI,CAAA;AACzD,EAAA,mBAAA,CAAoB,GAAA,EAAK,IAAA,EAAM,gBAAA,EAAkB,IAAI,CAAA;AACvD;;;AClPO,IAAM,UAAA,GAAa,OAAO,MAAA,CAAO;AAAA,EACtC,SAAA;AAAA,EACA,mBAAA;AAAA,EACA,aAAA;AAAA,EACA,uBAAA;AAAA,EACA,6BAAA;AAAA,EACA,gBAAA;AAAA,EACA,oBAAA;AAAA,EACA;AACF,CAAC","file":"index.js","sourcesContent":["import * as fc from \"fast-check\";\nimport { step } from \"../fsm/lifecycle.js\";\nimport type { Implementations, MachineDef, Runtime, Snapshot } from \"../fsm/types.js\";\n\n/**\n * Pure-model representation of an FSM run, used by `fc.commands`.\n * `reached` tracks every state visited so generic properties can check\n * containment without re-running.\n */\nexport type FsmModel<Ctx, States extends string> = {\n value: States;\n context: Ctx;\n status: \"active\" | \"final\";\n reached: Set<States>;\n};\n\nexport type EventArbitraries<Evt extends { type: string }> = Readonly<\n Record<string, fc.Arbitrary<Evt>>\n>;\n\nexport type FsmCommand<Ctx, Evt extends { type: string }, States extends string> = fc.Command<\n FsmModel<Ctx, States>,\n Runtime<Ctx, Evt, States>\n>;\n\nexport function initialModel<Ctx, Evt extends { type: string }, States extends string>(\n def: MachineDef<Ctx, Evt, States>,\n): FsmModel<Ctx, States> {\n return {\n value: def.initial,\n context: def.context,\n status: def.states[def.initial]?.final ? \"final\" : \"active\",\n reached: new Set([def.initial]),\n };\n}\n\nclass SendCommand<Ctx, Evt extends { type: string }, States extends string>\n implements FsmCommand<Ctx, Evt, States>\n{\n constructor(\n private readonly event: Evt,\n private readonly def: MachineDef<Ctx, Evt, States>,\n private readonly impl: Implementations<Ctx, Evt>,\n ) {}\n\n check(_m: Readonly<FsmModel<Ctx, States>>): boolean {\n // Every event is always applicable; invariants are asserted in run().\n return true;\n }\n\n run(m: FsmModel<Ctx, States>, r: Runtime<Ctx, Evt, States>): void {\n const before: Snapshot<Ctx, States> = r.getSnapshot();\n r.send(this.event);\n const after = r.getSnapshot();\n\n const predicted = step(this.def, before, this.event, this.impl);\n\n /* v8 ignore start — invariant guard: fires only when the live runtime\n diverges from the pure step() prediction, i.e. an internal bug. */\n if (predicted.snapshot.value !== after.value) {\n throw new Error(\n `aifsmjs/pbt: determinism violation — predicted \"${String(predicted.snapshot.value)}\" but runtime returned \"${String(after.value)}\" after ${this.toString()}`,\n );\n }\n /* v8 ignore stop */\n\n m.value = after.value;\n m.context = after.context as Ctx;\n m.status = after.status;\n m.reached.add(after.value);\n }\n\n toString(): string {\n return `send(${JSON.stringify(this.event)})`;\n }\n}\n\n/**\n * Build an `fc.Arbitrary` of FSM command sequences. Each command pulls one\n * event from the user-supplied arbitrary map and, when run, asserts that the\n * pure `step()` prediction matches the runtime's observable outcome.\n *\n * Pair this arbitrary with `fc.property(...)` inside a `fc.assert(...)` call,\n * or use the helpers in `aifsmjs/pbt` properties to get the six generic\n * invariants for free.\n */\nexport function commandsFromMachine<Ctx, Evt extends { type: string }, States extends string>(\n def: MachineDef<Ctx, Evt, States>,\n impl: Implementations<Ctx, Evt>,\n eventArbitraries: EventArbitraries<Evt>,\n): fc.Arbitrary<Iterable<FsmCommand<Ctx, Evt, States>>> {\n const arbs: fc.Arbitrary<FsmCommand<Ctx, Evt, States>>[] = [];\n for (const arb of Object.values(eventArbitraries)) {\n arbs.push(arb.map((event) => new SendCommand(event, def, impl)));\n }\n return fc.commands(arbs, { size: \"+1\" });\n}\n","import { isDeepStrictEqual } from \"node:util\";\nimport * as fc from \"fast-check\";\nimport { initialSnapshot } from \"../fsm/definition.js\";\nimport { ownValue } from \"../fsm/evaluator.js\";\nimport { step } from \"../fsm/lifecycle.js\";\nimport { normalizeTransitions } from \"../fsm/resolver.js\";\nimport { createRuntime } from \"../fsm/runtime.js\";\nimport type { Guard, Implementations, MachineDef } from \"../fsm/types.js\";\nimport { replay } from \"../replay/index.js\";\nimport {\n type EventArbitraries,\n type FsmModel,\n commandsFromMachine,\n initialModel,\n} from \"./commands.js\";\n\nexport type AssertOpts = Readonly<{\n numRuns?: number;\n seed?: number;\n verbose?: boolean;\n}>;\n\nfunction buildAssertOpts(opts: AssertOpts | undefined): fc.Parameters<unknown> {\n const out: fc.Parameters<unknown> = {};\n if (opts?.numRuns !== undefined) out.numRuns = opts.numRuns;\n if (opts?.seed !== undefined) out.seed = opts.seed;\n if (opts?.verbose) out.verbose = true;\n return out;\n}\n\n/**\n * Structural deep-equality for two context values (C3). Backed by `node:util`\n * `isDeepStrictEqual`, replacing the previous `JSON.stringify(a) === JSON.stringify(b)`\n * oracle which was unsound:\n *\n * - key-order-sensitive → false-FAIL on `{a:1,b:2}` vs `{b:2,a:1}`;\n * - drops undefined keys → false-PASS on `{v:undefined,w:1}` vs `{w:1}`;\n * - lossy for `Map`/`Set`/`Date` (all serialise to `{}` or an ISO string);\n * - throws on `BigInt`.\n *\n * `isDeepStrictEqual` distinguishes present-but-undefined from absent keys,\n * compares `Map`/`Set`/`Date` by contents, and tolerates `BigInt` — exactly\n * the verdicts a context-equality oracle for PBT requires. No new dependency\n * (Node built-in; the package already targets Node >=18).\n */\nexport function contextEquals(a: unknown, b: unknown): boolean {\n return isDeepStrictEqual(a, b);\n}\n\n/**\n * #1 snapshotAlwaysFrozen — after any event sequence the live snapshot remains\n * frozen at the top level.\n */\nexport function snapshotAlwaysFrozen<Ctx, Evt extends { type: string }, States extends string>(\n def: MachineDef<Ctx, Evt, States>,\n impl: Implementations<Ctx, Evt>,\n eventArbitraries: EventArbitraries<Evt>,\n opts?: AssertOpts,\n): void {\n fc.assert(\n fc.property(commandsFromMachine(def, impl, eventArbitraries), (cmds) => {\n const real = createRuntime(def, impl, { dispatchEffects: false });\n // Dispose every generated run's runtime (its signal aborts, its child\n // runtimes are torn down); the asserted value is computed first.\n try {\n const model: FsmModel<Ctx, States> = initialModel(def);\n fc.modelRun(() => ({ model, real }), cmds);\n return Object.isFrozen(real.getSnapshot());\n } finally {\n real.dispose();\n }\n }),\n buildAssertOpts(opts),\n );\n}\n\n/**\n * #2 unknownEventNoOp — sending an event whose `type` is not declared in any\n * state's `on` map never changes the snapshot.\n */\nexport function unknownEventNoOp<Ctx, Evt extends { type: string }, States extends string>(\n def: MachineDef<Ctx, Evt, States>,\n impl: Implementations<Ctx, Evt>,\n unknownType: string,\n opts?: AssertOpts,\n): void {\n fc.assert(\n fc.property(fc.constant(unknownType), (t) => {\n const initial = initialSnapshot(def);\n const result = step(def, initial, { type: t } as unknown as Evt, impl);\n return result.changed === false && result.snapshot === initial && result.effects.length === 0;\n }),\n buildAssertOpts(opts),\n );\n}\n\n/**\n * #3 reachableStatesSubsetDeclared — every state visited during a run belongs\n * to `def.states`.\n */\nexport function reachableStatesSubsetDeclared<\n Ctx,\n Evt extends { type: string },\n States extends string,\n>(\n def: MachineDef<Ctx, Evt, States>,\n impl: Implementations<Ctx, Evt>,\n eventArbitraries: EventArbitraries<Evt>,\n opts?: AssertOpts,\n): void {\n const declared = new Set<string>(Object.keys(def.states));\n fc.assert(\n fc.property(commandsFromMachine(def, impl, eventArbitraries), (cmds) => {\n const real = createRuntime(def, impl, { dispatchEffects: false });\n try {\n const model: FsmModel<Ctx, States> = initialModel(def);\n fc.modelRun(() => ({ model, real }), cmds);\n for (const s of model.reached as Set<string>) {\n /* v8 ignore next — property failure branch; an unreachable state would indicate a bug. */\n if (!declared.has(s)) return false;\n }\n return declared.has(real.getSnapshot().value);\n } finally {\n real.dispose();\n }\n }),\n buildAssertOpts(opts),\n );\n}\n\n/**\n * #4 replayEqualsFold — `replay(initial, log)` produces the same final state\n * as a live runtime fed the same events. Effects dispatched by the runtime are\n * ignored; the comparison is on `{ value, context }`.\n */\nexport function replayEqualsFold<Ctx, Evt extends { type: string }, States extends string>(\n def: MachineDef<Ctx, Evt, States>,\n impl: Implementations<Ctx, Evt>,\n eventArbitraries: EventArbitraries<Evt>,\n opts?: AssertOpts,\n): void {\n const eventArb = fc.oneof(...Object.values(eventArbitraries));\n fc.assert(\n fc.property(fc.array(eventArb, { maxLength: 32 }), (events) => {\n const real = createRuntime(def, impl, { dispatchEffects: false });\n try {\n for (const e of events) real.send(e);\n const live = real.getSnapshot();\n const replayed = replay(initialSnapshot(def), events, def, impl).snapshot;\n return live.value === replayed.value && contextEquals(live.context, replayed.context);\n } finally {\n real.dispose();\n }\n }),\n buildAssertOpts(opts),\n );\n}\n\n/**\n * #5 guardsFalseNoTransition — when every candidate transition for the current\n * (state, event) pair carries a guard and every guard returns `false`, the\n * snapshot is unchanged (`changed === false`).\n *\n * Implementation: synthesise an impl that forces every guard to `false`, then\n * for each step whose candidate list is fully guarded, assert the step did not\n * change state. Steps with an unguarded fallback candidate (which fires even\n * when all guards are false) are skipped — the README claim is specifically\n * about the all-guards-false case.\n */\nexport function guardsFalseNoTransition<Ctx, Evt extends { type: string }, States extends string>(\n def: MachineDef<Ctx, Evt, States>,\n impl: Implementations<Ctx, Evt>,\n eventArbitraries: EventArbitraries<Evt>,\n opts?: AssertOpts,\n): void {\n // Every key reads as an own property: step() resolves guard refs with an\n // own-key lookup (the value itself still comes from `get`), so a `get` trap\n // alone would surface as UnknownGuardError.\n const blockedGuards = new Proxy(\n {},\n {\n get: () => () => false,\n getOwnPropertyDescriptor: () => ({ configurable: true }),\n },\n ) as Readonly<Record<string, Guard<Ctx, Evt>>>;\n const blockedImpl: Implementations<Ctx, Evt> = {\n ...impl,\n guards: blockedGuards,\n };\n // True when every candidate transition for (value, eventType) carries a\n // guard — i.e. blocking all guards leaves no unconditional fallback, so a\n // correct step() must report changed === false.\n const isFullyGuarded = (value: States, eventType: string): boolean => {\n const candidates = normalizeTransitions(ownValue(def.states[value]?.on, eventType));\n return candidates.length > 0 && candidates.every((t) => t.guard !== undefined);\n };\n fc.assert(\n fc.property(\n fc.array(fc.oneof(...Object.values(eventArbitraries)), { maxLength: 16 }),\n (events) => {\n let snap = initialSnapshot(def);\n for (const e of events) {\n const fullyGuarded = isFullyGuarded(snap.value, e.type);\n const r = step(def, snap, e, blockedImpl);\n // The named invariant: all guards false + no unconditional fallback\n // ⇒ no transition. Without this assertion the property was vacuous\n // (it only failed if step() threw).\n if (fullyGuarded && r.changed !== false) return false;\n snap = r.snapshot;\n }\n return true;\n },\n ),\n buildAssertOpts(opts),\n );\n}\n\n// Own keys/values of one object, plus Map/Set entries and Date time.\nconst ownState = (v: object): unknown[] => [\n ...Reflect.ownKeys(v).flatMap((k) => [k, (v as Record<PropertyKey, unknown>)[k]]),\n ...(v instanceof Map || v instanceof Set ? [...v.entries()].flat() : []),\n v instanceof Date && v.getTime(),\n];\n\n/**\n * #6 assignDoesNotMutate — running an `assign`-style action never mutates the\n * previous context object. Verified by re-checking the own state of every\n * object reachable from the pre-step context.\n */\nexport function assignDoesNotMutate<Ctx, Evt extends { type: string }, States extends string>(\n def: MachineDef<Ctx, Evt, States>,\n impl: Implementations<Ctx, Evt>,\n eventArbitraries: EventArbitraries<Evt>,\n opts?: AssertOpts,\n): void {\n fc.assert(\n fc.property(\n fc.array(fc.oneof(...Object.values(eventArbitraries)), { maxLength: 16 }),\n (events) => {\n let snap = initialSnapshot(def);\n for (const e of events) {\n // Record, not clone (clones drop prototypes, throw on functions).\n const before = new Map<object, unknown[]>();\n const walk = (v: unknown): void => {\n if (!v || typeof v !== \"object\" || before.has(v)) return;\n const own = ownState(v);\n before.set(v, own);\n own.forEach(walk);\n };\n walk(snap.context);\n // `before` keeps the pre-step objects, so advancing `snap` first is safe.\n snap = step(def, snap, e, impl).snapshot;\n for (const [obj, own] of before) if (!contextEquals(ownState(obj), own)) return false;\n }\n return true;\n },\n ),\n buildAssertOpts(opts),\n );\n}\n\n/**\n * Assert every generic property in one call. Use this when you don't need\n * fine-grained control over per-property options.\n */\nexport function assertAll<Ctx, Evt extends { type: string }, States extends string>(\n def: MachineDef<Ctx, Evt, States>,\n impl: Implementations<Ctx, Evt>,\n eventArbitraries: EventArbitraries<Evt>,\n opts?: AssertOpts & { unknownEventType?: string },\n): void {\n snapshotAlwaysFrozen(def, impl, eventArbitraries, opts);\n unknownEventNoOp(def, impl, opts?.unknownEventType ?? \"__AIFSMJS_UNKNOWN__\", opts);\n reachableStatesSubsetDeclared(def, impl, eventArbitraries, opts);\n replayEqualsFold(def, impl, eventArbitraries, opts);\n guardsFalseNoTransition(def, impl, eventArbitraries, opts);\n assignDoesNotMutate(def, impl, eventArbitraries, opts);\n}\n","export {\n commandsFromMachine,\n initialModel,\n type EventArbitraries,\n type FsmCommand,\n type FsmModel,\n} from \"./commands.js\";\n\nexport {\n assertAll,\n assignDoesNotMutate,\n guardsFalseNoTransition,\n reachableStatesSubsetDeclared,\n replayEqualsFold,\n snapshotAlwaysFrozen,\n unknownEventNoOp,\n type AssertOpts,\n} from \"./properties.js\";\n\n// Convenience namespace mirroring the README:\n// import { properties } from \"aifsmjs/pbt\"\n// properties.snapshotAlwaysFrozen(...)\n// A frozen object literal rather than `import * as`: a namespace re-export\n// makes tsup emit a shared `__export` helper chunk that every subpath entry\n// (root, guards, timer, ...) then imports and pays for.\nimport {\n assertAll,\n assignDoesNotMutate,\n contextEquals,\n guardsFalseNoTransition,\n reachableStatesSubsetDeclared,\n replayEqualsFold,\n snapshotAlwaysFrozen,\n unknownEventNoOp,\n} from \"./properties.js\";\nexport const properties = Object.freeze({\n assertAll,\n assignDoesNotMutate,\n contextEquals,\n guardsFalseNoTransition,\n reachableStatesSubsetDeclared,\n replayEqualsFold,\n snapshotAlwaysFrozen,\n unknownEventNoOp,\n});\n"]}
|
package/dist/replay/index.cjs
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
require('../chunk-
|
|
3
|
+
var chunkVV5TKFQO_cjs = require('../chunk-VV5TKFQO.cjs');
|
|
4
|
+
require('../chunk-VGLF5NQH.cjs');
|
|
5
5
|
require('../chunk-3B2USJ3H.cjs');
|
|
6
|
-
require('../chunk-
|
|
7
|
-
require('../chunk-Q7SFCCGT.cjs');
|
|
6
|
+
require('../chunk-QCTA2X4J.cjs');
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
Object.defineProperty(exports, "replay", {
|
|
12
11
|
enumerable: true,
|
|
13
|
-
get: function () { return
|
|
12
|
+
get: function () { return chunkVV5TKFQO_cjs.replay; }
|
|
14
13
|
});
|
|
15
14
|
//# sourceMappingURL=index.cjs.map
|
|
16
15
|
//# sourceMappingURL=index.cjs.map
|
package/dist/replay/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as Snapshot, E as Effect, f as MachineDef, I as Implementations } from '../types-
|
|
1
|
+
import { S as Snapshot, E as Effect, f as MachineDef, I as Implementations } from '../types-CrDxFfBx.cjs';
|
|
2
2
|
|
|
3
3
|
type ReplayResult<Ctx, States extends string> = Readonly<{
|
|
4
4
|
snapshot: Snapshot<Ctx, States>;
|
package/dist/replay/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as Snapshot, E as Effect, f as MachineDef, I as Implementations } from '../types-
|
|
1
|
+
import { S as Snapshot, E as Effect, f as MachineDef, I as Implementations } from '../types-CrDxFfBx.js';
|
|
2
2
|
|
|
3
3
|
type ReplayResult<Ctx, States extends string> = Readonly<{
|
|
4
4
|
snapshot: Snapshot<Ctx, States>;
|
package/dist/replay/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
export { replay } from '../chunk-
|
|
2
|
-
import '../chunk-
|
|
1
|
+
export { replay } from '../chunk-Q45LGXHO.js';
|
|
2
|
+
import '../chunk-D6H64FSI.js';
|
|
3
3
|
import '../chunk-JKZAOPQC.js';
|
|
4
|
-
import '../chunk-
|
|
5
|
-
import '../chunk-PZ5AY32C.js';
|
|
4
|
+
import '../chunk-SSNKGEVB.js';
|
|
6
5
|
//# sourceMappingURL=index.js.map
|
|
7
6
|
//# sourceMappingURL=index.js.map
|
package/dist/timer/index.cjs
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
require('../chunk-Q7SFCCGT.cjs');
|
|
4
|
-
|
|
5
3
|
// src/timer/scheduler.ts
|
|
6
4
|
var NOOP = Object.freeze({ cancel: () => {
|
|
7
5
|
} });
|
|
6
|
+
var MAX_DELAY = 2147483647;
|
|
7
|
+
var clampDelay = (ms) => Math.min(ms, MAX_DELAY);
|
|
8
|
+
function checkArgs(ms, fn) {
|
|
9
|
+
if (!Number.isFinite(ms) || ms < 0) {
|
|
10
|
+
throw new RangeError("aifsmjs: after() ms must be a finite number >= 0");
|
|
11
|
+
}
|
|
12
|
+
if (typeof fn !== "function") throw new TypeError("aifsmjs: after() fn must be a function");
|
|
13
|
+
}
|
|
8
14
|
function resolveTimers(opts) {
|
|
9
15
|
return {
|
|
10
16
|
st: opts?.setTimeout ?? ((fn, ms) => globalThis.setTimeout(fn, ms)),
|
|
@@ -12,6 +18,7 @@ function resolveTimers(opts) {
|
|
|
12
18
|
};
|
|
13
19
|
}
|
|
14
20
|
function after(ms, fn, opts) {
|
|
21
|
+
checkArgs(ms, fn);
|
|
15
22
|
if (opts?.signal?.aborted) return NOOP;
|
|
16
23
|
const { st, ct } = resolveTimers(opts);
|
|
17
24
|
let fired = false;
|
|
@@ -28,7 +35,7 @@ function after(ms, fn, opts) {
|
|
|
28
35
|
if (cancelled) return;
|
|
29
36
|
if (opts?.signal) opts.signal.removeEventListener("abort", cancel);
|
|
30
37
|
fn();
|
|
31
|
-
}, ms);
|
|
38
|
+
}, clampDelay(ms));
|
|
32
39
|
if (opts?.signal && !fired) {
|
|
33
40
|
opts.signal.addEventListener("abort", cancel, { once: true });
|
|
34
41
|
}
|
|
@@ -38,8 +45,14 @@ function createScheduler(defaults) {
|
|
|
38
45
|
const pending = /* @__PURE__ */ new Set();
|
|
39
46
|
const sched = {
|
|
40
47
|
after(ms, fn, opts) {
|
|
41
|
-
|
|
42
|
-
const
|
|
48
|
+
checkArgs(ms, fn);
|
|
49
|
+
const signal = opts?.signal ?? defaults?.signal;
|
|
50
|
+
const setTimeoutFn = opts?.setTimeout ?? defaults?.setTimeout;
|
|
51
|
+
const clearTimeoutFn = opts?.clearTimeout ?? defaults?.clearTimeout;
|
|
52
|
+
const innerOpts = {
|
|
53
|
+
...setTimeoutFn !== void 0 && { setTimeout: setTimeoutFn },
|
|
54
|
+
...clearTimeoutFn !== void 0 && { clearTimeout: clearTimeoutFn }
|
|
55
|
+
};
|
|
43
56
|
if (signal?.aborted) return NOOP;
|
|
44
57
|
const slot = {};
|
|
45
58
|
let fired = false;
|
package/dist/timer/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/timer/scheduler.ts"],"names":[],"mappings":";;;;;AAuBA,IAAM,IAAA,GAAoB,MAAA,CAAO,MAAA,CAAO,EAAE,QAAQ,MAAM;AAAC,CAAA,EAAG,CAAA;AAE5D,SAAS,cAAc,IAAA,EAGrB;AACA,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,MAAM,UAAA,KAAe,CAAC,IAAI,EAAA,KAAO,UAAA,CAAW,UAAA,CAAW,EAAA,EAAI,EAAE,CAAA,CAAA;AAAA,IACjE,IAAI,IAAA,EAAM,YAAA,KAAiB,CAAC,CAAA,KAAM,UAAA,CAAW,aAAa,CAAW,CAAA;AAAA,GACvE;AACF;AAeO,SAAS,KAAA,CAAM,EAAA,EAAY,EAAA,EAAgB,IAAA,EAAkC;AAClF,EAAA,IAAI,IAAA,EAAM,MAAA,EAAQ,OAAA,EAAS,OAAO,IAAA;AAElC,EAAA,MAAM,EAAE,EAAA,EAAI,EAAA,EAAG,GAAI,cAAc,IAAI,CAAA;AACrC,EAAA,IAAI,KAAA,GAAQ,KAAA;AACZ,EAAA,IAAI,SAAA,GAAY,KAAA;AAOhB,EAAA,MAAM,QAA4C,EAAC;AAEnD,EAAA,MAAM,SAAS,MAAM;AACnB,IAAA,IAAI,SAAS,SAAA,EAAW;AACxB,IAAA,SAAA,GAAY,IAAA;AACZ,IAAA,IAAI,KAAA,CAAM,MAAA,KAAW,MAAA,EAAW,EAAA,CAAG,MAAM,MAAM,CAAA;AAG/C,IAAA,IAAI,MAAM,MAAA,EAAQ,IAAA,CAAK,MAAA,CAAO,mBAAA,CAAoB,SAAS,MAAM,CAAA;AAAA,EACnE,CAAA;AAEA,EAAA,KAAA,CAAM,MAAA,GAAS,GAAG,MAAM;AACtB,IAAA,KAAA,GAAQ,IAAA;AAER,IAAA,IAAI,SAAA,EAAW;AAGf,IAAA,IAAI,MAAM,MAAA,EAAQ,IAAA,CAAK,MAAA,CAAO,mBAAA,CAAoB,SAAS,MAAM,CAAA;AACjE,IAAA,EAAA,EAAG;AAAA,EACL,GAAG,EAAE,CAAA;AAKL,EAAA,IAAI,IAAA,EAAM,MAAA,IAAU,CAAC,KAAA,EAAO;AAC1B,IAAA,IAAA,CAAK,OAAO,gBAAA,CAAiB,OAAA,EAAS,QAAQ,EAAE,IAAA,EAAM,MAAM,CAAA;AAAA,EAC9D;AAEA,EAAA,OAAO,MAAA,CAAO,MAAA,CAAO,EAAE,MAAA,EAAQ,CAAA;AACjC;AAgBO,SAAS,gBAAgB,QAAA,EAAoC;AAClE,EAAA,MAAM,OAAA,uBAAc,GAAA,EAAiB;AAErC,EAAA,MAAM,KAAA,GAAmB;AAAA,IACvB,KAAA,CAAM,EAAA,EAAI,EAAA,EAAI,IAAA,EAAM;AAClB,MAAA,MAAM,MAAA,GAAuB,EAAE,GAAG,QAAA,EAAU,GAAG,IAAA,EAAK;AAOpD,MAAA,MAAM,EAAE,MAAA,EAAQ,GAAG,SAAA,EAAU,GAAI,MAAA;AAKjC,MAAA,IAAI,MAAA,EAAQ,SAAS,OAAO,IAAA;AAI5B,MAAA,MAAM,OAA8B,EAAC;AACrC,MAAA,IAAI,KAAA,GAAQ,KAAA;AACZ,MAAA,IAAI,OAAA,GAAU,KAAA;AAEd,MAAA,MAAM,cAAc,MAAM;AACxB,QAAA,IAAI,MAAA,EAAQ,MAAA,CAAO,mBAAA,CAAoB,OAAA,EAAS,OAAO,CAAA;AAAA,MACzD,CAAA;AAGA,MAAA,MAAM,SAAS,MAAM;AACnB,QAAA,IAAI,OAAA,EAAS;AACb,QAAA,OAAA,GAAU,IAAA;AACV,QAAA,IAAI,IAAA,CAAK,GAAA,EAAK,OAAA,CAAQ,MAAA,CAAO,KAAK,GAAG,CAAA;AACrC,QAAA,WAAA,EAAY;AAAA,MACd,CAAA;AAEA,MAAA,MAAM,UAAU,MAAM;AACpB,QAAA,KAAA,GAAQ,IAAA;AACR,QAAA,MAAA,EAAO;AACP,QAAA,EAAA,EAAG;AAAA,MACL,CAAA;AAIA,MAAA,MAAM,SAAS,MAAM;AACnB,QAAA,KAAA,CAAM,MAAA,EAAO;AACb,QAAA,MAAA,EAAO;AAAA,MACT,CAAA;AACA,MAAA,SAAS,OAAA,GAAU;AACjB,QAAA,MAAA,EAAO;AAAA,MACT;AAEA,MAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,EAAA,EAAI,OAAA,EAAS,SAAS,CAAA;AAE1C,MAAA,MAAM,MAAA,GAAsB,MAAA,CAAO,MAAA,CAAO,EAAE,QAAQ,CAAA;AACpD,MAAA,IAAA,CAAK,GAAA,GAAM,MAAA;AAMX,MAAA,IAAI,CAAC,KAAA,EAAO;AACV,QAAA,OAAA,CAAQ,IAAI,MAAM,CAAA;AAKlB,QAAA,IAAI,MAAA,SAAe,gBAAA,CAAiB,OAAA,EAAS,SAAS,EAAE,IAAA,EAAM,MAAM,CAAA;AAAA,MACtE;AAEA,MAAA,OAAO,MAAA;AAAA,IACT,CAAA;AAAA,IACA,SAAA,GAAY;AACV,MAAA,KAAA,MAAW,CAAA,IAAK,OAAA,EAAS,CAAA,CAAE,MAAA,EAAO;AAClC,MAAA,OAAA,CAAQ,KAAA,EAAM;AAAA,IAChB,CAAA;AAAA,IACA,IAAI,IAAA,GAAO;AACT,MAAA,OAAO,OAAA,CAAQ,IAAA;AAAA,IACjB;AAAA,GACF;AACA,EAAA,OAAO,KAAA;AACT","file":"index.cjs","sourcesContent":["export type AfterHandle = Readonly<{\n cancel(): void;\n}>;\n\nexport type SetTimeoutFn = (fn: () => void, ms: number) => unknown;\nexport type ClearTimeoutFn = (handle: unknown) => void;\n\nexport type AfterOptions = Readonly<{\n /**\n * If supplied and aborted, the callback never runs and any pending timer is\n * cleared. Aborting after fire is a no-op.\n */\n signal?: AbortSignal;\n /**\n * Override `setTimeout` (testing, SSR, custom loops). Defaults to globalThis.\n */\n setTimeout?: SetTimeoutFn;\n /**\n * Override `clearTimeout`. Must match the `setTimeout` you injected.\n */\n clearTimeout?: ClearTimeoutFn;\n}>;\n\nconst NOOP: AfterHandle = Object.freeze({ cancel: () => {} });\n\nfunction resolveTimers(opts: AfterOptions | undefined): {\n st: SetTimeoutFn;\n ct: ClearTimeoutFn;\n} {\n return {\n st: opts?.setTimeout ?? ((fn, ms) => globalThis.setTimeout(fn, ms)),\n ct: opts?.clearTimeout ?? ((h) => globalThis.clearTimeout(h as number)),\n };\n}\n\n/**\n * Schedule `fn` to run after `ms` milliseconds. Returns a handle whose\n * `cancel()` clears the pending timer. Optional `signal` aborts the timer when\n * triggered. Aborting after the callback fires is a no-op.\n *\n * The abort listener is registered with `{ once: true }` as a baseline, but\n * `{ once: true }` alone does NOT prevent listener accumulation when the same\n * signal is reused across many timers: it only removes the listener when the\n * signal aborts, not when the timer fires normally or `cancel()` is called.\n * We therefore explicitly call `signal.removeEventListener(\"abort\", cancel)`\n * inside the fire callback and at the end of `cancel()` so that a shared,\n * long-lived signal never accumulates dead listeners across timer reuse.\n */\nexport function after(ms: number, fn: () => void, opts?: AfterOptions): AfterHandle {\n if (opts?.signal?.aborted) return NOOP;\n\n const { st, ct } = resolveTimers(opts);\n let fired = false;\n let cancelled = false;\n // `cancel` and the timer handle reference each other. A const cell holds the\n // handle so `cancel` can be defined BEFORE `st(...)` runs (letting a custom\n // `st` that fires its callback synchronously reference `cancel` without\n // hitting the temporal-dead-zone) while still being able to clear the handle\n // assigned afterwards. A synchronous fire sets `fired=true`, so cancel() never\n // reads the still-unset handle in that path.\n const timer: { handle?: ReturnType<typeof st> } = {};\n\n const cancel = () => {\n if (fired || cancelled) return;\n cancelled = true;\n if (timer.handle !== undefined) ct(timer.handle);\n // Detach the abort listener so a reused signal does not accumulate dead\n // closures after this timer is cancelled.\n if (opts?.signal) opts.signal.removeEventListener(\"abort\", cancel);\n };\n\n timer.handle = st(() => {\n fired = true;\n /* v8 ignore next — defensive race guard: cancel() sets cancelled=true and clears the timer, but if a custom setTimeout fires after clear, this short-circuits fn(). */\n if (cancelled) return;\n // Detach the abort listener now that the timer has fired — the listener\n // will never be invoked and must not accumulate on a reused signal.\n if (opts?.signal) opts.signal.removeEventListener(\"abort\", cancel);\n fn();\n }, ms);\n\n // Attach only if the timer has not already fired synchronously (a custom `st`\n // may fire inline); otherwise the listener would be registered AFTER the fire\n // path's removal ran and would then leak until the signal aborts.\n if (opts?.signal && !fired) {\n opts.signal.addEventListener(\"abort\", cancel, { once: true });\n }\n\n return Object.freeze({ cancel });\n}\n\nexport type Scheduler = Readonly<{\n after(ms: number, fn: () => void, opts?: AfterOptions): AfterHandle;\n cancelAll(): void;\n readonly size: number;\n}>;\n\n/**\n * Build a scheduler that tracks every pending `after()` so they can be\n * cancelled together (e.g. on machine destroy). Each `after` returns a handle\n * whose `cancel()` also removes it from the tracking set.\n *\n * `defaults` are merged into every call — typically you inject `setTimeout` /\n * `clearTimeout` once at construction.\n */\nexport function createScheduler(defaults?: AfterOptions): Scheduler {\n const pending = new Set<AfterHandle>();\n\n const sched: Scheduler = {\n after(ms, fn, opts) {\n const merged: AfterOptions = { ...defaults, ...opts };\n // Signal handling is lifted to the scheduler layer: we own one abort\n // listener per timer and route it through the scheduler-level cancel so\n // the abort path also removes the handle from `pending`. The inner\n // after() therefore must NOT see the signal — otherwise it would clear\n // its timer on abort without ever touching `pending`, leaking the entry\n // (FSM-R-01, path a).\n const { signal, ...innerOpts } = merged;\n\n // Path b: scheduling on an already-aborted signal must not grow the Set.\n // after() returns NOOP in that case; tracking it would be a permanent\n // dead entry. Return the same NOOP without adding.\n if (signal?.aborted) return NOOP;\n\n // Forward-reference slot so `wrapped`/`cancel` can find the tracked\n // handle before it is constructed below.\n const slot: { ref?: AfterHandle } = {};\n let fired = false;\n let settled = false; // true once removed from pending (fire or cancel)\n\n const detachAbort = () => {\n if (signal) signal.removeEventListener(\"abort\", onAbort);\n };\n\n // Single removal path shared by fire, explicit cancel, and abort.\n const settle = () => {\n if (settled) return;\n settled = true;\n if (slot.ref) pending.delete(slot.ref);\n detachAbort();\n };\n\n const wrapped = () => {\n fired = true;\n settle();\n fn();\n };\n\n // Scheduler-level cancel: cancel the inner timer AND drop from pending\n // AND detach the abort listener. Registered on the abort path too.\n const cancel = () => {\n inner.cancel();\n settle();\n };\n function onAbort() {\n cancel();\n }\n\n const inner = after(ms, wrapped, innerOpts);\n\n const handle: AfterHandle = Object.freeze({ cancel });\n slot.ref = handle;\n\n // Path c (sync-fire): a custom setTimeout may fire `wrapped` inline,\n // during the after() call above — before we reach here. In that case the\n // timer is already done; adding it now would leave a permanent dead\n // entry. Only track timers that are still live.\n if (!fired) {\n pending.add(handle);\n // Attach the abort listener only for a live timer on a real signal.\n // { once: true } removes it on abort; settle()/detachAbort() remove it\n // on fire/cancel so a long-lived shared signal never accumulates dead\n // listeners.\n if (signal) signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n\n return handle;\n },\n cancelAll() {\n for (const h of pending) h.cancel();\n pending.clear();\n },\n get size() {\n return pending.size;\n },\n };\n return sched;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/timer/scheduler.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["export type AfterHandle = Readonly<{\n cancel(): void;\n}>;\n\nexport type SetTimeoutFn = (fn: () => void, ms: number) => unknown;\nexport type ClearTimeoutFn = (handle: unknown) => void;\n\nexport type AfterOptions = Readonly<{\n /**\n * If supplied and aborted, the callback never runs and any pending timer is\n * cleared. Aborting after fire is a no-op.\n */\n signal?: AbortSignal;\n /**\n * Override `setTimeout` (testing, SSR, custom loops). Defaults to globalThis.\n */\n setTimeout?: SetTimeoutFn;\n /**\n * Override `clearTimeout`. Must match the `setTimeout` you injected.\n */\n clearTimeout?: ClearTimeoutFn;\n}>;\n\nconst NOOP: AfterHandle = Object.freeze({ cancel: () => {} });\n\n// Largest delay setTimeout honours (2^31-1 ms, about 24.8 days); hosts treat\n// anything larger as ~1 ms. Every delay handed to setTimeout goes through\n// clampDelay (ai*js timer rule; no timer chaining).\nconst MAX_DELAY = 2_147_483_647;\nconst clampDelay = (ms: number): number => Math.min(ms, MAX_DELAY);\n\n// Argument validation shared by after() and createScheduler().after(), run\n// before any side effect. aifsmjs/timer exports no error class, so misuse is\n// a prefixed built-in RangeError / TypeError.\nfunction checkArgs(ms: unknown, fn: unknown): void {\n if (!Number.isFinite(ms) || (ms as number) < 0) {\n throw new RangeError(\"aifsmjs: after() ms must be a finite number >= 0\");\n }\n if (typeof fn !== \"function\") throw new TypeError(\"aifsmjs: after() fn must be a function\");\n}\n\nfunction resolveTimers(opts: AfterOptions | undefined): {\n st: SetTimeoutFn;\n ct: ClearTimeoutFn;\n} {\n return {\n st: opts?.setTimeout ?? ((fn, ms) => globalThis.setTimeout(fn, ms)),\n ct: opts?.clearTimeout ?? ((h) => globalThis.clearTimeout(h as number)),\n };\n}\n\n/**\n * Schedule `fn` to run after `ms` milliseconds. Returns a handle whose\n * `cancel()` clears the pending timer. Optional `signal` aborts the timer when\n * triggered. Aborting after the callback fires is a no-op.\n *\n * `ms` must be a finite number >= 0 (`NaN`, `Infinity`, negatives and\n * non-numbers throw `RangeError`) and `fn` a function (`TypeError`); both are\n * checked before anything else, including an already-aborted `signal`. A\n * finite `ms` above 2^31-1 (about 24.8 days) is clamped to 2^31-1 when handed\n * to `setTimeout`. To mean \"never\", do not schedule.\n *\n * The abort listener is registered with `{ once: true }` as a baseline, but\n * `{ once: true }` alone does NOT prevent listener accumulation when the same\n * signal is reused across many timers: it only removes the listener when the\n * signal aborts, not when the timer fires normally or `cancel()` is called.\n * We therefore explicitly call `signal.removeEventListener(\"abort\", cancel)`\n * inside the fire callback and at the end of `cancel()` so that a shared,\n * long-lived signal never accumulates dead listeners across timer reuse.\n */\nexport function after(ms: number, fn: () => void, opts?: AfterOptions): AfterHandle {\n checkArgs(ms, fn);\n if (opts?.signal?.aborted) return NOOP;\n\n const { st, ct } = resolveTimers(opts);\n let fired = false;\n let cancelled = false;\n // `cancel` and the timer handle reference each other. A const cell holds the\n // handle so `cancel` can be defined BEFORE `st(...)` runs (letting a custom\n // `st` that fires its callback synchronously reference `cancel` without\n // hitting the temporal-dead-zone) while still being able to clear the handle\n // assigned afterwards. A synchronous fire sets `fired=true`, so cancel() never\n // reads the still-unset handle in that path.\n const timer: { handle?: ReturnType<typeof st> } = {};\n\n const cancel = () => {\n if (fired || cancelled) return;\n cancelled = true;\n if (timer.handle !== undefined) ct(timer.handle);\n // Detach the abort listener so a reused signal does not accumulate dead\n // closures after this timer is cancelled.\n if (opts?.signal) opts.signal.removeEventListener(\"abort\", cancel);\n };\n\n timer.handle = st(() => {\n fired = true;\n /* v8 ignore next — defensive race guard: cancel() sets cancelled=true and clears the timer, but if a custom setTimeout fires after clear, this short-circuits fn(). */\n if (cancelled) return;\n // Detach the abort listener now that the timer has fired — the listener\n // will never be invoked and must not accumulate on a reused signal.\n if (opts?.signal) opts.signal.removeEventListener(\"abort\", cancel);\n fn();\n }, clampDelay(ms));\n\n // Attach only if the timer has not already fired synchronously (a custom `st`\n // may fire inline); otherwise the listener would be registered AFTER the fire\n // path's removal ran and would then leak until the signal aborts.\n if (opts?.signal && !fired) {\n opts.signal.addEventListener(\"abort\", cancel, { once: true });\n }\n\n return Object.freeze({ cancel });\n}\n\nexport type Scheduler = Readonly<{\n after(ms: number, fn: () => void, opts?: AfterOptions): AfterHandle;\n cancelAll(): void;\n readonly size: number;\n}>;\n\n/**\n * Build a scheduler that tracks every pending `after()` so they can be\n * cancelled together (e.g. on machine destroy). Each `after` returns a handle\n * whose `cancel()` also removes it from the tracking set.\n *\n * `defaults` are merged into every call — typically you inject `setTimeout` /\n * `clearTimeout` once at construction.\n */\nexport function createScheduler(defaults?: AfterOptions): Scheduler {\n const pending = new Set<AfterHandle>();\n\n const sched: Scheduler = {\n after(ms, fn, opts) {\n // Same validation as after(), before the aborted-signal shortcut and\n // before `pending` is touched.\n checkArgs(ms, fn);\n // Field-by-field merge with `??`: an explicitly-undefined per-call field\n // (common when forwarding optional options in JS, or in TS without\n // exactOptionalPropertyTypes) must fall back to the scheduler's default,\n // not silently win over it the way `{ ...defaults, ...opts }` would.\n // Built with `exactOptionalPropertyTypes` in mind: an option that ends\n // up undefined after the merge is left OUT of the object rather than\n // set to `undefined`, so the AfterOptions type is honoured exactly.\n const signal = opts?.signal ?? defaults?.signal;\n const setTimeoutFn = opts?.setTimeout ?? defaults?.setTimeout;\n const clearTimeoutFn = opts?.clearTimeout ?? defaults?.clearTimeout;\n // Signal handling is lifted to the scheduler layer: we own one abort\n // listener per timer and route it through the scheduler-level cancel so\n // the abort path also removes the handle from `pending`. The inner\n // after() therefore must NOT see the signal — otherwise it would clear\n // its timer on abort without ever touching `pending`, leaking the entry\n // (FSM-R-01, path a).\n const innerOpts: AfterOptions = {\n ...(setTimeoutFn !== undefined && { setTimeout: setTimeoutFn }),\n ...(clearTimeoutFn !== undefined && { clearTimeout: clearTimeoutFn }),\n };\n\n // Path b: scheduling on an already-aborted signal must not grow the Set.\n // after() returns NOOP in that case; tracking it would be a permanent\n // dead entry. Return the same NOOP without adding.\n if (signal?.aborted) return NOOP;\n\n // Forward-reference slot so `wrapped`/`cancel` can find the tracked\n // handle before it is constructed below.\n const slot: { ref?: AfterHandle } = {};\n let fired = false;\n let settled = false; // true once removed from pending (fire or cancel)\n\n const detachAbort = () => {\n if (signal) signal.removeEventListener(\"abort\", onAbort);\n };\n\n // Single removal path shared by fire, explicit cancel, and abort.\n const settle = () => {\n if (settled) return;\n settled = true;\n if (slot.ref) pending.delete(slot.ref);\n detachAbort();\n };\n\n const wrapped = () => {\n fired = true;\n settle();\n fn();\n };\n\n // Scheduler-level cancel: cancel the inner timer AND drop from pending\n // AND detach the abort listener. Registered on the abort path too.\n const cancel = () => {\n inner.cancel();\n settle();\n };\n function onAbort() {\n cancel();\n }\n\n const inner = after(ms, wrapped, innerOpts);\n\n const handle: AfterHandle = Object.freeze({ cancel });\n slot.ref = handle;\n\n // Path c (sync-fire): a custom setTimeout may fire `wrapped` inline,\n // during the after() call above — before we reach here. In that case the\n // timer is already done; adding it now would leave a permanent dead\n // entry. Only track timers that are still live.\n if (!fired) {\n pending.add(handle);\n // Attach the abort listener only for a live timer on a real signal.\n // { once: true } removes it on abort; settle()/detachAbort() remove it\n // on fire/cancel so a long-lived shared signal never accumulates dead\n // listeners.\n if (signal) signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n\n return handle;\n },\n cancelAll() {\n for (const h of pending) h.cancel();\n pending.clear();\n },\n get size() {\n return pending.size;\n },\n };\n return sched;\n}\n"]}
|
package/dist/timer/index.d.cts
CHANGED
|
@@ -23,6 +23,12 @@ type AfterOptions = Readonly<{
|
|
|
23
23
|
* `cancel()` clears the pending timer. Optional `signal` aborts the timer when
|
|
24
24
|
* triggered. Aborting after the callback fires is a no-op.
|
|
25
25
|
*
|
|
26
|
+
* `ms` must be a finite number >= 0 (`NaN`, `Infinity`, negatives and
|
|
27
|
+
* non-numbers throw `RangeError`) and `fn` a function (`TypeError`); both are
|
|
28
|
+
* checked before anything else, including an already-aborted `signal`. A
|
|
29
|
+
* finite `ms` above 2^31-1 (about 24.8 days) is clamped to 2^31-1 when handed
|
|
30
|
+
* to `setTimeout`. To mean "never", do not schedule.
|
|
31
|
+
*
|
|
26
32
|
* The abort listener is registered with `{ once: true }` as a baseline, but
|
|
27
33
|
* `{ once: true }` alone does NOT prevent listener accumulation when the same
|
|
28
34
|
* signal is reused across many timers: it only removes the listener when the
|
package/dist/timer/index.d.ts
CHANGED
|
@@ -23,6 +23,12 @@ type AfterOptions = Readonly<{
|
|
|
23
23
|
* `cancel()` clears the pending timer. Optional `signal` aborts the timer when
|
|
24
24
|
* triggered. Aborting after the callback fires is a no-op.
|
|
25
25
|
*
|
|
26
|
+
* `ms` must be a finite number >= 0 (`NaN`, `Infinity`, negatives and
|
|
27
|
+
* non-numbers throw `RangeError`) and `fn` a function (`TypeError`); both are
|
|
28
|
+
* checked before anything else, including an already-aborted `signal`. A
|
|
29
|
+
* finite `ms` above 2^31-1 (about 24.8 days) is clamped to 2^31-1 when handed
|
|
30
|
+
* to `setTimeout`. To mean "never", do not schedule.
|
|
31
|
+
*
|
|
26
32
|
* The abort listener is registered with `{ once: true }` as a baseline, but
|
|
27
33
|
* `{ once: true }` alone does NOT prevent listener accumulation when the same
|
|
28
34
|
* signal is reused across many timers: it only removes the listener when the
|
package/dist/timer/index.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import '../chunk-PZ5AY32C.js';
|
|
2
|
-
|
|
3
1
|
// src/timer/scheduler.ts
|
|
4
2
|
var NOOP = Object.freeze({ cancel: () => {
|
|
5
3
|
} });
|
|
4
|
+
var MAX_DELAY = 2147483647;
|
|
5
|
+
var clampDelay = (ms) => Math.min(ms, MAX_DELAY);
|
|
6
|
+
function checkArgs(ms, fn) {
|
|
7
|
+
if (!Number.isFinite(ms) || ms < 0) {
|
|
8
|
+
throw new RangeError("aifsmjs: after() ms must be a finite number >= 0");
|
|
9
|
+
}
|
|
10
|
+
if (typeof fn !== "function") throw new TypeError("aifsmjs: after() fn must be a function");
|
|
11
|
+
}
|
|
6
12
|
function resolveTimers(opts) {
|
|
7
13
|
return {
|
|
8
14
|
st: opts?.setTimeout ?? ((fn, ms) => globalThis.setTimeout(fn, ms)),
|
|
@@ -10,6 +16,7 @@ function resolveTimers(opts) {
|
|
|
10
16
|
};
|
|
11
17
|
}
|
|
12
18
|
function after(ms, fn, opts) {
|
|
19
|
+
checkArgs(ms, fn);
|
|
13
20
|
if (opts?.signal?.aborted) return NOOP;
|
|
14
21
|
const { st, ct } = resolveTimers(opts);
|
|
15
22
|
let fired = false;
|
|
@@ -26,7 +33,7 @@ function after(ms, fn, opts) {
|
|
|
26
33
|
if (cancelled) return;
|
|
27
34
|
if (opts?.signal) opts.signal.removeEventListener("abort", cancel);
|
|
28
35
|
fn();
|
|
29
|
-
}, ms);
|
|
36
|
+
}, clampDelay(ms));
|
|
30
37
|
if (opts?.signal && !fired) {
|
|
31
38
|
opts.signal.addEventListener("abort", cancel, { once: true });
|
|
32
39
|
}
|
|
@@ -36,8 +43,14 @@ function createScheduler(defaults) {
|
|
|
36
43
|
const pending = /* @__PURE__ */ new Set();
|
|
37
44
|
const sched = {
|
|
38
45
|
after(ms, fn, opts) {
|
|
39
|
-
|
|
40
|
-
const
|
|
46
|
+
checkArgs(ms, fn);
|
|
47
|
+
const signal = opts?.signal ?? defaults?.signal;
|
|
48
|
+
const setTimeoutFn = opts?.setTimeout ?? defaults?.setTimeout;
|
|
49
|
+
const clearTimeoutFn = opts?.clearTimeout ?? defaults?.clearTimeout;
|
|
50
|
+
const innerOpts = {
|
|
51
|
+
...setTimeoutFn !== void 0 && { setTimeout: setTimeoutFn },
|
|
52
|
+
...clearTimeoutFn !== void 0 && { clearTimeout: clearTimeoutFn }
|
|
53
|
+
};
|
|
41
54
|
if (signal?.aborted) return NOOP;
|
|
42
55
|
const slot = {};
|
|
43
56
|
let fired = false;
|
package/dist/timer/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/timer/scheduler.ts"],"names":[],"mappings":";;;AAuBA,IAAM,IAAA,GAAoB,MAAA,CAAO,MAAA,CAAO,EAAE,QAAQ,MAAM;AAAC,CAAA,EAAG,CAAA;AAE5D,SAAS,cAAc,IAAA,EAGrB;AACA,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,MAAM,UAAA,KAAe,CAAC,IAAI,EAAA,KAAO,UAAA,CAAW,UAAA,CAAW,EAAA,EAAI,EAAE,CAAA,CAAA;AAAA,IACjE,IAAI,IAAA,EAAM,YAAA,KAAiB,CAAC,CAAA,KAAM,UAAA,CAAW,aAAa,CAAW,CAAA;AAAA,GACvE;AACF;AAeO,SAAS,KAAA,CAAM,EAAA,EAAY,EAAA,EAAgB,IAAA,EAAkC;AAClF,EAAA,IAAI,IAAA,EAAM,MAAA,EAAQ,OAAA,EAAS,OAAO,IAAA;AAElC,EAAA,MAAM,EAAE,EAAA,EAAI,EAAA,EAAG,GAAI,cAAc,IAAI,CAAA;AACrC,EAAA,IAAI,KAAA,GAAQ,KAAA;AACZ,EAAA,IAAI,SAAA,GAAY,KAAA;AAOhB,EAAA,MAAM,QAA4C,EAAC;AAEnD,EAAA,MAAM,SAAS,MAAM;AACnB,IAAA,IAAI,SAAS,SAAA,EAAW;AACxB,IAAA,SAAA,GAAY,IAAA;AACZ,IAAA,IAAI,KAAA,CAAM,MAAA,KAAW,MAAA,EAAW,EAAA,CAAG,MAAM,MAAM,CAAA;AAG/C,IAAA,IAAI,MAAM,MAAA,EAAQ,IAAA,CAAK,MAAA,CAAO,mBAAA,CAAoB,SAAS,MAAM,CAAA;AAAA,EACnE,CAAA;AAEA,EAAA,KAAA,CAAM,MAAA,GAAS,GAAG,MAAM;AACtB,IAAA,KAAA,GAAQ,IAAA;AAER,IAAA,IAAI,SAAA,EAAW;AAGf,IAAA,IAAI,MAAM,MAAA,EAAQ,IAAA,CAAK,MAAA,CAAO,mBAAA,CAAoB,SAAS,MAAM,CAAA;AACjE,IAAA,EAAA,EAAG;AAAA,EACL,GAAG,EAAE,CAAA;AAKL,EAAA,IAAI,IAAA,EAAM,MAAA,IAAU,CAAC,KAAA,EAAO;AAC1B,IAAA,IAAA,CAAK,OAAO,gBAAA,CAAiB,OAAA,EAAS,QAAQ,EAAE,IAAA,EAAM,MAAM,CAAA;AAAA,EAC9D;AAEA,EAAA,OAAO,MAAA,CAAO,MAAA,CAAO,EAAE,MAAA,EAAQ,CAAA;AACjC;AAgBO,SAAS,gBAAgB,QAAA,EAAoC;AAClE,EAAA,MAAM,OAAA,uBAAc,GAAA,EAAiB;AAErC,EAAA,MAAM,KAAA,GAAmB;AAAA,IACvB,KAAA,CAAM,EAAA,EAAI,EAAA,EAAI,IAAA,EAAM;AAClB,MAAA,MAAM,MAAA,GAAuB,EAAE,GAAG,QAAA,EAAU,GAAG,IAAA,EAAK;AAOpD,MAAA,MAAM,EAAE,MAAA,EAAQ,GAAG,SAAA,EAAU,GAAI,MAAA;AAKjC,MAAA,IAAI,MAAA,EAAQ,SAAS,OAAO,IAAA;AAI5B,MAAA,MAAM,OAA8B,EAAC;AACrC,MAAA,IAAI,KAAA,GAAQ,KAAA;AACZ,MAAA,IAAI,OAAA,GAAU,KAAA;AAEd,MAAA,MAAM,cAAc,MAAM;AACxB,QAAA,IAAI,MAAA,EAAQ,MAAA,CAAO,mBAAA,CAAoB,OAAA,EAAS,OAAO,CAAA;AAAA,MACzD,CAAA;AAGA,MAAA,MAAM,SAAS,MAAM;AACnB,QAAA,IAAI,OAAA,EAAS;AACb,QAAA,OAAA,GAAU,IAAA;AACV,QAAA,IAAI,IAAA,CAAK,GAAA,EAAK,OAAA,CAAQ,MAAA,CAAO,KAAK,GAAG,CAAA;AACrC,QAAA,WAAA,EAAY;AAAA,MACd,CAAA;AAEA,MAAA,MAAM,UAAU,MAAM;AACpB,QAAA,KAAA,GAAQ,IAAA;AACR,QAAA,MAAA,EAAO;AACP,QAAA,EAAA,EAAG;AAAA,MACL,CAAA;AAIA,MAAA,MAAM,SAAS,MAAM;AACnB,QAAA,KAAA,CAAM,MAAA,EAAO;AACb,QAAA,MAAA,EAAO;AAAA,MACT,CAAA;AACA,MAAA,SAAS,OAAA,GAAU;AACjB,QAAA,MAAA,EAAO;AAAA,MACT;AAEA,MAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,EAAA,EAAI,OAAA,EAAS,SAAS,CAAA;AAE1C,MAAA,MAAM,MAAA,GAAsB,MAAA,CAAO,MAAA,CAAO,EAAE,QAAQ,CAAA;AACpD,MAAA,IAAA,CAAK,GAAA,GAAM,MAAA;AAMX,MAAA,IAAI,CAAC,KAAA,EAAO;AACV,QAAA,OAAA,CAAQ,IAAI,MAAM,CAAA;AAKlB,QAAA,IAAI,MAAA,SAAe,gBAAA,CAAiB,OAAA,EAAS,SAAS,EAAE,IAAA,EAAM,MAAM,CAAA;AAAA,MACtE;AAEA,MAAA,OAAO,MAAA;AAAA,IACT,CAAA;AAAA,IACA,SAAA,GAAY;AACV,MAAA,KAAA,MAAW,CAAA,IAAK,OAAA,EAAS,CAAA,CAAE,MAAA,EAAO;AAClC,MAAA,OAAA,CAAQ,KAAA,EAAM;AAAA,IAChB,CAAA;AAAA,IACA,IAAI,IAAA,GAAO;AACT,MAAA,OAAO,OAAA,CAAQ,IAAA;AAAA,IACjB;AAAA,GACF;AACA,EAAA,OAAO,KAAA;AACT","file":"index.js","sourcesContent":["export type AfterHandle = Readonly<{\n cancel(): void;\n}>;\n\nexport type SetTimeoutFn = (fn: () => void, ms: number) => unknown;\nexport type ClearTimeoutFn = (handle: unknown) => void;\n\nexport type AfterOptions = Readonly<{\n /**\n * If supplied and aborted, the callback never runs and any pending timer is\n * cleared. Aborting after fire is a no-op.\n */\n signal?: AbortSignal;\n /**\n * Override `setTimeout` (testing, SSR, custom loops). Defaults to globalThis.\n */\n setTimeout?: SetTimeoutFn;\n /**\n * Override `clearTimeout`. Must match the `setTimeout` you injected.\n */\n clearTimeout?: ClearTimeoutFn;\n}>;\n\nconst NOOP: AfterHandle = Object.freeze({ cancel: () => {} });\n\nfunction resolveTimers(opts: AfterOptions | undefined): {\n st: SetTimeoutFn;\n ct: ClearTimeoutFn;\n} {\n return {\n st: opts?.setTimeout ?? ((fn, ms) => globalThis.setTimeout(fn, ms)),\n ct: opts?.clearTimeout ?? ((h) => globalThis.clearTimeout(h as number)),\n };\n}\n\n/**\n * Schedule `fn` to run after `ms` milliseconds. Returns a handle whose\n * `cancel()` clears the pending timer. Optional `signal` aborts the timer when\n * triggered. Aborting after the callback fires is a no-op.\n *\n * The abort listener is registered with `{ once: true }` as a baseline, but\n * `{ once: true }` alone does NOT prevent listener accumulation when the same\n * signal is reused across many timers: it only removes the listener when the\n * signal aborts, not when the timer fires normally or `cancel()` is called.\n * We therefore explicitly call `signal.removeEventListener(\"abort\", cancel)`\n * inside the fire callback and at the end of `cancel()` so that a shared,\n * long-lived signal never accumulates dead listeners across timer reuse.\n */\nexport function after(ms: number, fn: () => void, opts?: AfterOptions): AfterHandle {\n if (opts?.signal?.aborted) return NOOP;\n\n const { st, ct } = resolveTimers(opts);\n let fired = false;\n let cancelled = false;\n // `cancel` and the timer handle reference each other. A const cell holds the\n // handle so `cancel` can be defined BEFORE `st(...)` runs (letting a custom\n // `st` that fires its callback synchronously reference `cancel` without\n // hitting the temporal-dead-zone) while still being able to clear the handle\n // assigned afterwards. A synchronous fire sets `fired=true`, so cancel() never\n // reads the still-unset handle in that path.\n const timer: { handle?: ReturnType<typeof st> } = {};\n\n const cancel = () => {\n if (fired || cancelled) return;\n cancelled = true;\n if (timer.handle !== undefined) ct(timer.handle);\n // Detach the abort listener so a reused signal does not accumulate dead\n // closures after this timer is cancelled.\n if (opts?.signal) opts.signal.removeEventListener(\"abort\", cancel);\n };\n\n timer.handle = st(() => {\n fired = true;\n /* v8 ignore next — defensive race guard: cancel() sets cancelled=true and clears the timer, but if a custom setTimeout fires after clear, this short-circuits fn(). */\n if (cancelled) return;\n // Detach the abort listener now that the timer has fired — the listener\n // will never be invoked and must not accumulate on a reused signal.\n if (opts?.signal) opts.signal.removeEventListener(\"abort\", cancel);\n fn();\n }, ms);\n\n // Attach only if the timer has not already fired synchronously (a custom `st`\n // may fire inline); otherwise the listener would be registered AFTER the fire\n // path's removal ran and would then leak until the signal aborts.\n if (opts?.signal && !fired) {\n opts.signal.addEventListener(\"abort\", cancel, { once: true });\n }\n\n return Object.freeze({ cancel });\n}\n\nexport type Scheduler = Readonly<{\n after(ms: number, fn: () => void, opts?: AfterOptions): AfterHandle;\n cancelAll(): void;\n readonly size: number;\n}>;\n\n/**\n * Build a scheduler that tracks every pending `after()` so they can be\n * cancelled together (e.g. on machine destroy). Each `after` returns a handle\n * whose `cancel()` also removes it from the tracking set.\n *\n * `defaults` are merged into every call — typically you inject `setTimeout` /\n * `clearTimeout` once at construction.\n */\nexport function createScheduler(defaults?: AfterOptions): Scheduler {\n const pending = new Set<AfterHandle>();\n\n const sched: Scheduler = {\n after(ms, fn, opts) {\n const merged: AfterOptions = { ...defaults, ...opts };\n // Signal handling is lifted to the scheduler layer: we own one abort\n // listener per timer and route it through the scheduler-level cancel so\n // the abort path also removes the handle from `pending`. The inner\n // after() therefore must NOT see the signal — otherwise it would clear\n // its timer on abort without ever touching `pending`, leaking the entry\n // (FSM-R-01, path a).\n const { signal, ...innerOpts } = merged;\n\n // Path b: scheduling on an already-aborted signal must not grow the Set.\n // after() returns NOOP in that case; tracking it would be a permanent\n // dead entry. Return the same NOOP without adding.\n if (signal?.aborted) return NOOP;\n\n // Forward-reference slot so `wrapped`/`cancel` can find the tracked\n // handle before it is constructed below.\n const slot: { ref?: AfterHandle } = {};\n let fired = false;\n let settled = false; // true once removed from pending (fire or cancel)\n\n const detachAbort = () => {\n if (signal) signal.removeEventListener(\"abort\", onAbort);\n };\n\n // Single removal path shared by fire, explicit cancel, and abort.\n const settle = () => {\n if (settled) return;\n settled = true;\n if (slot.ref) pending.delete(slot.ref);\n detachAbort();\n };\n\n const wrapped = () => {\n fired = true;\n settle();\n fn();\n };\n\n // Scheduler-level cancel: cancel the inner timer AND drop from pending\n // AND detach the abort listener. Registered on the abort path too.\n const cancel = () => {\n inner.cancel();\n settle();\n };\n function onAbort() {\n cancel();\n }\n\n const inner = after(ms, wrapped, innerOpts);\n\n const handle: AfterHandle = Object.freeze({ cancel });\n slot.ref = handle;\n\n // Path c (sync-fire): a custom setTimeout may fire `wrapped` inline,\n // during the after() call above — before we reach here. In that case the\n // timer is already done; adding it now would leave a permanent dead\n // entry. Only track timers that are still live.\n if (!fired) {\n pending.add(handle);\n // Attach the abort listener only for a live timer on a real signal.\n // { once: true } removes it on abort; settle()/detachAbort() remove it\n // on fire/cancel so a long-lived shared signal never accumulates dead\n // listeners.\n if (signal) signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n\n return handle;\n },\n cancelAll() {\n for (const h of pending) h.cancel();\n pending.clear();\n },\n get size() {\n return pending.size;\n },\n };\n return sched;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/timer/scheduler.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["export type AfterHandle = Readonly<{\n cancel(): void;\n}>;\n\nexport type SetTimeoutFn = (fn: () => void, ms: number) => unknown;\nexport type ClearTimeoutFn = (handle: unknown) => void;\n\nexport type AfterOptions = Readonly<{\n /**\n * If supplied and aborted, the callback never runs and any pending timer is\n * cleared. Aborting after fire is a no-op.\n */\n signal?: AbortSignal;\n /**\n * Override `setTimeout` (testing, SSR, custom loops). Defaults to globalThis.\n */\n setTimeout?: SetTimeoutFn;\n /**\n * Override `clearTimeout`. Must match the `setTimeout` you injected.\n */\n clearTimeout?: ClearTimeoutFn;\n}>;\n\nconst NOOP: AfterHandle = Object.freeze({ cancel: () => {} });\n\n// Largest delay setTimeout honours (2^31-1 ms, about 24.8 days); hosts treat\n// anything larger as ~1 ms. Every delay handed to setTimeout goes through\n// clampDelay (ai*js timer rule; no timer chaining).\nconst MAX_DELAY = 2_147_483_647;\nconst clampDelay = (ms: number): number => Math.min(ms, MAX_DELAY);\n\n// Argument validation shared by after() and createScheduler().after(), run\n// before any side effect. aifsmjs/timer exports no error class, so misuse is\n// a prefixed built-in RangeError / TypeError.\nfunction checkArgs(ms: unknown, fn: unknown): void {\n if (!Number.isFinite(ms) || (ms as number) < 0) {\n throw new RangeError(\"aifsmjs: after() ms must be a finite number >= 0\");\n }\n if (typeof fn !== \"function\") throw new TypeError(\"aifsmjs: after() fn must be a function\");\n}\n\nfunction resolveTimers(opts: AfterOptions | undefined): {\n st: SetTimeoutFn;\n ct: ClearTimeoutFn;\n} {\n return {\n st: opts?.setTimeout ?? ((fn, ms) => globalThis.setTimeout(fn, ms)),\n ct: opts?.clearTimeout ?? ((h) => globalThis.clearTimeout(h as number)),\n };\n}\n\n/**\n * Schedule `fn` to run after `ms` milliseconds. Returns a handle whose\n * `cancel()` clears the pending timer. Optional `signal` aborts the timer when\n * triggered. Aborting after the callback fires is a no-op.\n *\n * `ms` must be a finite number >= 0 (`NaN`, `Infinity`, negatives and\n * non-numbers throw `RangeError`) and `fn` a function (`TypeError`); both are\n * checked before anything else, including an already-aborted `signal`. A\n * finite `ms` above 2^31-1 (about 24.8 days) is clamped to 2^31-1 when handed\n * to `setTimeout`. To mean \"never\", do not schedule.\n *\n * The abort listener is registered with `{ once: true }` as a baseline, but\n * `{ once: true }` alone does NOT prevent listener accumulation when the same\n * signal is reused across many timers: it only removes the listener when the\n * signal aborts, not when the timer fires normally or `cancel()` is called.\n * We therefore explicitly call `signal.removeEventListener(\"abort\", cancel)`\n * inside the fire callback and at the end of `cancel()` so that a shared,\n * long-lived signal never accumulates dead listeners across timer reuse.\n */\nexport function after(ms: number, fn: () => void, opts?: AfterOptions): AfterHandle {\n checkArgs(ms, fn);\n if (opts?.signal?.aborted) return NOOP;\n\n const { st, ct } = resolveTimers(opts);\n let fired = false;\n let cancelled = false;\n // `cancel` and the timer handle reference each other. A const cell holds the\n // handle so `cancel` can be defined BEFORE `st(...)` runs (letting a custom\n // `st` that fires its callback synchronously reference `cancel` without\n // hitting the temporal-dead-zone) while still being able to clear the handle\n // assigned afterwards. A synchronous fire sets `fired=true`, so cancel() never\n // reads the still-unset handle in that path.\n const timer: { handle?: ReturnType<typeof st> } = {};\n\n const cancel = () => {\n if (fired || cancelled) return;\n cancelled = true;\n if (timer.handle !== undefined) ct(timer.handle);\n // Detach the abort listener so a reused signal does not accumulate dead\n // closures after this timer is cancelled.\n if (opts?.signal) opts.signal.removeEventListener(\"abort\", cancel);\n };\n\n timer.handle = st(() => {\n fired = true;\n /* v8 ignore next — defensive race guard: cancel() sets cancelled=true and clears the timer, but if a custom setTimeout fires after clear, this short-circuits fn(). */\n if (cancelled) return;\n // Detach the abort listener now that the timer has fired — the listener\n // will never be invoked and must not accumulate on a reused signal.\n if (opts?.signal) opts.signal.removeEventListener(\"abort\", cancel);\n fn();\n }, clampDelay(ms));\n\n // Attach only if the timer has not already fired synchronously (a custom `st`\n // may fire inline); otherwise the listener would be registered AFTER the fire\n // path's removal ran and would then leak until the signal aborts.\n if (opts?.signal && !fired) {\n opts.signal.addEventListener(\"abort\", cancel, { once: true });\n }\n\n return Object.freeze({ cancel });\n}\n\nexport type Scheduler = Readonly<{\n after(ms: number, fn: () => void, opts?: AfterOptions): AfterHandle;\n cancelAll(): void;\n readonly size: number;\n}>;\n\n/**\n * Build a scheduler that tracks every pending `after()` so they can be\n * cancelled together (e.g. on machine destroy). Each `after` returns a handle\n * whose `cancel()` also removes it from the tracking set.\n *\n * `defaults` are merged into every call — typically you inject `setTimeout` /\n * `clearTimeout` once at construction.\n */\nexport function createScheduler(defaults?: AfterOptions): Scheduler {\n const pending = new Set<AfterHandle>();\n\n const sched: Scheduler = {\n after(ms, fn, opts) {\n // Same validation as after(), before the aborted-signal shortcut and\n // before `pending` is touched.\n checkArgs(ms, fn);\n // Field-by-field merge with `??`: an explicitly-undefined per-call field\n // (common when forwarding optional options in JS, or in TS without\n // exactOptionalPropertyTypes) must fall back to the scheduler's default,\n // not silently win over it the way `{ ...defaults, ...opts }` would.\n // Built with `exactOptionalPropertyTypes` in mind: an option that ends\n // up undefined after the merge is left OUT of the object rather than\n // set to `undefined`, so the AfterOptions type is honoured exactly.\n const signal = opts?.signal ?? defaults?.signal;\n const setTimeoutFn = opts?.setTimeout ?? defaults?.setTimeout;\n const clearTimeoutFn = opts?.clearTimeout ?? defaults?.clearTimeout;\n // Signal handling is lifted to the scheduler layer: we own one abort\n // listener per timer and route it through the scheduler-level cancel so\n // the abort path also removes the handle from `pending`. The inner\n // after() therefore must NOT see the signal — otherwise it would clear\n // its timer on abort without ever touching `pending`, leaking the entry\n // (FSM-R-01, path a).\n const innerOpts: AfterOptions = {\n ...(setTimeoutFn !== undefined && { setTimeout: setTimeoutFn }),\n ...(clearTimeoutFn !== undefined && { clearTimeout: clearTimeoutFn }),\n };\n\n // Path b: scheduling on an already-aborted signal must not grow the Set.\n // after() returns NOOP in that case; tracking it would be a permanent\n // dead entry. Return the same NOOP without adding.\n if (signal?.aborted) return NOOP;\n\n // Forward-reference slot so `wrapped`/`cancel` can find the tracked\n // handle before it is constructed below.\n const slot: { ref?: AfterHandle } = {};\n let fired = false;\n let settled = false; // true once removed from pending (fire or cancel)\n\n const detachAbort = () => {\n if (signal) signal.removeEventListener(\"abort\", onAbort);\n };\n\n // Single removal path shared by fire, explicit cancel, and abort.\n const settle = () => {\n if (settled) return;\n settled = true;\n if (slot.ref) pending.delete(slot.ref);\n detachAbort();\n };\n\n const wrapped = () => {\n fired = true;\n settle();\n fn();\n };\n\n // Scheduler-level cancel: cancel the inner timer AND drop from pending\n // AND detach the abort listener. Registered on the abort path too.\n const cancel = () => {\n inner.cancel();\n settle();\n };\n function onAbort() {\n cancel();\n }\n\n const inner = after(ms, wrapped, innerOpts);\n\n const handle: AfterHandle = Object.freeze({ cancel });\n slot.ref = handle;\n\n // Path c (sync-fire): a custom setTimeout may fire `wrapped` inline,\n // during the after() call above — before we reach here. In that case the\n // timer is already done; adding it now would leave a permanent dead\n // entry. Only track timers that are still live.\n if (!fired) {\n pending.add(handle);\n // Attach the abort listener only for a live timer on a real signal.\n // { once: true } removes it on abort; settle()/detachAbort() remove it\n // on fire/cancel so a long-lived shared signal never accumulates dead\n // listeners.\n if (signal) signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n\n return handle;\n },\n cancelAll() {\n for (const h of pending) h.cancel();\n pending.clear();\n },\n get size() {\n return pending.size;\n },\n };\n return sched;\n}\n"]}
|
|
@@ -150,16 +150,19 @@ type MiddlewareContext<Ctx, Evt, States extends string> = Readonly<{
|
|
|
150
150
|
/**
|
|
151
151
|
* The triggering event. May be the user's `Evt` (from `send()` or an
|
|
152
152
|
* explicit `reset(event)`) or the `ResetEvent` sentinel emitted by a
|
|
153
|
-
* `reset()` with no event argument.
|
|
153
|
+
* `reset()` with no event argument. This is the caller's event object,
|
|
154
|
+
* passed unfrozen; treat it as read-only.
|
|
154
155
|
*/
|
|
155
156
|
event: Evt | ResetEvent;
|
|
157
|
+
/** Deep-frozen effect descriptors (payloads included) about to be dispatched. */
|
|
156
158
|
effects: readonly Effect[];
|
|
157
159
|
changed: boolean;
|
|
158
160
|
}>;
|
|
159
161
|
type Middleware<Ctx, Evt, States extends string> = (ctx: MiddlewareContext<Ctx, Evt, States>, next: () => void) => void;
|
|
160
162
|
/**
|
|
161
|
-
* Payload of the `'transition'` runtime event — emitted
|
|
162
|
-
*
|
|
163
|
+
* Payload of the `'transition'` runtime event — emitted whenever a transition
|
|
164
|
+
* fired (`changed === true`), including an internal transition whose state
|
|
165
|
+
* `value` did not change (only its `context` did).
|
|
163
166
|
*/
|
|
164
167
|
type RuntimeTransitionEvent<Ctx, Evt, States extends string> = Readonly<{
|
|
165
168
|
prev: Snapshot<Ctx, States>;
|
|
@@ -170,9 +173,11 @@ type RuntimeTransitionEvent<Ctx, Evt, States extends string> = Readonly<{
|
|
|
170
173
|
}>;
|
|
171
174
|
/**
|
|
172
175
|
* Payload of the `'error'` runtime event — currently emitted for async effect
|
|
173
|
-
* handler rejections (which would otherwise become unhandled).
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
+
* handler rejections (which would otherwise become unhandled). With no
|
|
177
|
+
* `'error'` listener (none registered, or cleared by `dispose()`) a rejection
|
|
178
|
+
* is discarded; outside production (`NODE_ENV !== "production"`) it is also
|
|
179
|
+
* reported via `console.warn`. Synchronous throws from effect handlers and
|
|
180
|
+
* middleware still propagate to the caller of `send()` / `reset()`.
|
|
176
181
|
*/
|
|
177
182
|
type RuntimeErrorEvent<Evt> = Readonly<{
|
|
178
183
|
error: unknown;
|
|
@@ -189,6 +194,24 @@ interface Runtime<Ctx, Evt extends {
|
|
|
189
194
|
getSnapshot(): Snapshot<Ctx, States>;
|
|
190
195
|
/** Alias for `getSnapshot()`. */
|
|
191
196
|
snapshot(): Snapshot<Ctx, States>;
|
|
197
|
+
/**
|
|
198
|
+
* Process `event`: `step()` -> sub-machine lifecycle -> commit ->
|
|
199
|
+
* middleware -> effects -> `subscribe` listeners -> `'transition'`
|
|
200
|
+
* listeners, then return the committed snapshot.
|
|
201
|
+
*
|
|
202
|
+
* Run-to-completion: a `send()`/`reset()` made while this runtime is already
|
|
203
|
+
* processing an event (from middleware, an effect handler, a listener, or a
|
|
204
|
+
* child runtime's listener) is queued FIFO and processed after the current
|
|
205
|
+
* event's last notification, with the same full sequence. Such a nested
|
|
206
|
+
* call returns the snapshot committed at the time of the call, not the
|
|
207
|
+
* outcome of its own event — read `getSnapshot()` after the outermost call
|
|
208
|
+
* returns (or subscribe). A throw from any queued event discards the rest
|
|
209
|
+
* of the queue and propagates from the outermost call.
|
|
210
|
+
*
|
|
211
|
+
* Throws `RuntimeDisposedError` after `dispose()`, and
|
|
212
|
+
* `InvalidDefinitionError` when `event` is not an object with a string
|
|
213
|
+
* `type`.
|
|
214
|
+
*/
|
|
192
215
|
send(event: Evt): Snapshot<Ctx, States>;
|
|
193
216
|
/**
|
|
194
217
|
* Predict whether sending `event` would fire a transition. Reuses
|
|
@@ -196,11 +219,23 @@ interface Runtime<Ctx, Evt extends {
|
|
|
196
219
|
* are expected to be pure; `can` then matches `send` for the same input.
|
|
197
220
|
*/
|
|
198
221
|
can(event: Evt): boolean;
|
|
222
|
+
/**
|
|
223
|
+
* Call `listener` with the committed snapshot after every event that fired
|
|
224
|
+
* a transition (`changed === true`), after middleware and effects and before
|
|
225
|
+
* `'transition'` listeners. A listener removed during a notification round
|
|
226
|
+
* is skipped for the rest of it; one added waits for the next event.
|
|
227
|
+
* Throws `InvalidDefinitionError` if `listener` is not a function. Returns
|
|
228
|
+
* an unsubscribe function (a no-op after `dispose()`).
|
|
229
|
+
*/
|
|
199
230
|
subscribe(listener: (snap: Snapshot<Ctx, States>) => void): () => void;
|
|
200
231
|
/**
|
|
201
232
|
* EventTarget-like typed listener API. Returns an unsubscribe function.
|
|
202
233
|
* `options.signal` removes the listener when aborted; `options.once`
|
|
203
|
-
* removes the listener
|
|
234
|
+
* removes the listener before its first invocation. A listener removed
|
|
235
|
+
* while an event is being dispatched (by its unsubscribe, `once`, its
|
|
236
|
+
* signal, or `dispose()`) is skipped for the rest of that dispatch; one
|
|
237
|
+
* added waits for the next event. Throws `InvalidDefinitionError` for an
|
|
238
|
+
* unknown event `type` or a non-function `listener`. After `dispose()`,
|
|
204
239
|
* `on()` is a no-op and returns a no-op unsubscribe.
|
|
205
240
|
*/
|
|
206
241
|
on<K extends keyof RuntimeEventMap<Ctx, Evt, States>>(type: K, listener: (payload: RuntimeEventMap<Ctx, Evt, States>[K]) => void, options?: {
|
|
@@ -208,17 +243,25 @@ interface Runtime<Ctx, Evt extends {
|
|
|
208
243
|
once?: boolean;
|
|
209
244
|
}): () => void;
|
|
210
245
|
/**
|
|
211
|
-
* Re-initialise the runtime to the definition's initial snapshot.
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
246
|
+
* Re-initialise the runtime to the definition's initial snapshot. Does NOT
|
|
247
|
+
* run entry actions (reset = re-birth, not "transition into initial"); the
|
|
248
|
+
* current sub-machine child is always replaced. Notifies subscribers,
|
|
249
|
+
* middleware (`changed: true`) and `'transition'` listeners whenever the
|
|
250
|
+
* value, status, or context reference differs from the initial snapshot.
|
|
251
|
+
* Throws RuntimeDisposedError if disposed. If an `event` is supplied (an
|
|
252
|
+
* object with a string `type`, else `InvalidDefinitionError`), middleware
|
|
253
|
+
* sees it as the trigger; otherwise a sentinel
|
|
254
|
+
* `{ type: "@@aifsmjs/RESET" }` is synthesised. Run-to-completion like
|
|
255
|
+
* `send()`: a nested call is queued.
|
|
216
256
|
*/
|
|
217
257
|
reset(event?: Evt): Snapshot<Ctx, States>;
|
|
218
258
|
/**
|
|
219
259
|
* Tear down: abort the internal AbortController (effect handlers see signal
|
|
220
260
|
* fire), clear listeners, and mark this runtime as disposed. Subsequent
|
|
221
|
-
* send()/reset() calls throw RuntimeDisposedError. Idempotent
|
|
261
|
+
* send()/reset() calls throw RuntimeDisposedError. Idempotent and never
|
|
262
|
+
* throws. Never queued: called during a dispatch it runs at once, drops any
|
|
263
|
+
* queued send()/reset() calls, and the outer call returns the last
|
|
264
|
+
* committed snapshot.
|
|
222
265
|
*/
|
|
223
266
|
dispose(): void;
|
|
224
267
|
/**
|
|
@@ -237,11 +280,16 @@ interface Runtime<Ctx, Evt extends {
|
|
|
237
280
|
* Returns the currently active sub-Runtime for the current parent state,
|
|
238
281
|
* or undefined if:
|
|
239
282
|
* - the current state has no `sub` definition, OR
|
|
240
|
-
* - the
|
|
241
|
-
*
|
|
242
|
-
* OR
|
|
283
|
+
* - the previous child's `dispose()` threw during a transition
|
|
284
|
+
* (SubMachineError phase "dispose"; the parent stays in its state and
|
|
285
|
+
* the child is recreated when the state is re-entered), OR
|
|
243
286
|
* - the parent runtime has been disposed.
|
|
244
287
|
*
|
|
288
|
+
* When a transition's new child fails to initialise (SubMachineError phase
|
|
289
|
+
* "init"), the previous child is left untouched and is still returned. A
|
|
290
|
+
* child that fails to initialise at `createRuntime` bootstrap makes
|
|
291
|
+
* `createRuntime` itself throw, so there is no runtime to ask.
|
|
292
|
+
*
|
|
245
293
|
* The returned Runtime is typed at the loosest sub-machine signature.
|
|
246
294
|
* Caller casts to the concrete sub type.
|
|
247
295
|
*
|