libpetri 2.12.0 → 2.13.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.
@@ -1,6 +1,6 @@
1
1
  import { Writable } from 'node:stream';
2
- import { P as PetriNet, b as Transition, T as Token } from '../petri-net-B4wQwsUj.js';
3
- import { E as EventStore, N as NetEvent } from '../event-store-CAkN_ayv.js';
2
+ import { P as PetriNet, b as Transition, T as Token } from '../petri-net-C3LSY-vm.js';
3
+ import { E as EventStore, N as NetEvent } from '../event-store-DKTenPbC.js';
4
4
 
5
5
  /**
6
6
  * Commands sent from debug UI client to server via WebSocket.
@@ -1,5 +1,5 @@
1
1
  import { Application } from 'typedoc';
2
- import { P as PetriNet, S as SubnetDef, o as Instance } from '../petri-net-B4wQwsUj.js';
2
+ import { P as PetriNet, S as SubnetDef, o as Instance } from '../petri-net-C3LSY-vm.js';
3
3
 
4
4
  /**
5
5
  * TypeDoc plugin for `@petrinet` and `@subnet` tags — auto-generates
@@ -1,4 +1,4 @@
1
- import { T as Token } from './petri-net-B4wQwsUj.js';
1
+ import { T as Token } from './petri-net-C3LSY-vm.js';
2
2
 
3
3
  /**
4
4
  * Events emitted during Petri Net execution.
@@ -1,4 +1,4 @@
1
- import { P as PetriNet } from '../petri-net-B4wQwsUj.js';
1
+ import { P as PetriNet } from '../petri-net-C3LSY-vm.js';
2
2
 
3
3
  /**
4
4
  * Format-agnostic typed graph model.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { P as PetriNet, S as SubnetDef, a as Place, T as Token, b as Transition, E as EnvironmentPlace, c as TransitionContext, O as Out } from './petri-net-B4wQwsUj.js';
2
- export { A as Arc, d as ArcInhibitor, e as ArcInput, f as ArcOutput, g as ArcRead, h as ArcReset, C as Channel, i as ComposeBindings, F as FusionSet, j as FusionSetBuilder, I as In, k as InAll, l as InAtLeast, m as InExactly, n as InOne, o as Instance, p as Interface, q as InterfaceBuilder, K as KeyFn, L as LogFn, M as MAX_DURATION_MS, r as MatchKey, s as MatchSpec, N as NameId, t as OutAnd, u as OutForwardInput, v as OutPlace, w as OutTimeout, x as OutXor, y as OutputEntry, z as PetriNetBuilder, B as Port, D as PortDirection, G as SubnetDefBuilder, H as SubnetInstance, J as Timing, Q as TimingDeadline, R as TimingDelayed, U as TimingExact, V as TimingImmediate, W as TimingWindow, X as TokenInput, Y as TokenOutput, Z as TransitionAction, _ as TransitionBuilder, $ as VerificationHarness, a0 as VerificationResult, a1 as all, a2 as allPlaces, a3 as and, a4 as andPlaces, a5 as arcPlace, a6 as atLeast, a7 as consumptionCount, a8 as deadline, a9 as delayed, aa as earliest, ab as enumerateBranches, ac as environmentPlace, ad as exact, ae as exactly, af as fork, ag as forwardInput, ah as hasDeadline, ai as hasGuard, aj as immediate, ak as inhibitorArc, al as inputArc, am as isUnit, an as keyForPlace, ao as latest, ap as matchCorrelates, aq as matchKey, ar as matchSpec, as as matchesGuard, at as nameId, au as one, av as outPlace, aw as outputArc, ax as passthrough, ay as place, az as produce, aA as readArc, aB as requiredCount, aC as resetArc, aD as timeout, aE as timeoutPlace, aF as tokenAt, aG as tokenOf, aH as transform, aI as transformAsync, aJ as transformFrom, aK as unitToken, aL as window, aM as withTimeout, aN as xor, aO as xorPlaces } from './petri-net-B4wQwsUj.js';
3
- import { E as EventStore } from './event-store-CAkN_ayv.js';
4
- export { A as ActionTimedOut, a as ExecutionCompleted, b as ExecutionStarted, I as InMemoryEventStore, L as LogMessage, M as MarkingSnapshot, N as NetEvent, T as TokenAdded, c as TokenRemoved, d as TransitionClockRestarted, e as TransitionCompleted, f as TransitionEnabled, g as TransitionFailed, h as TransitionStarted, i as TransitionTimedOut, j as eventTransitionName, k as eventsOfType, l as failures, m as filterEvents, n as inMemoryEventStore, o as isFailureEvent, p as noopEventStore, t as transitionEvents } from './event-store-CAkN_ayv.js';
1
+ import { P as PetriNet, S as SubnetDef, a as Place, T as Token, b as Transition, E as EnvironmentPlace, c as TransitionContext, O as Out } from './petri-net-C3LSY-vm.js';
2
+ export { A as Arc, d as ArcInhibitor, e as ArcInput, f as ArcOutput, g as ArcRead, h as ArcReset, C as Channel, i as ComposeBindings, F as FusionSet, j as FusionSetBuilder, I as In, k as InAll, l as InAtLeast, m as InExactly, n as InOne, o as Instance, p as Interface, q as InterfaceBuilder, K as KeyFn, L as LogFn, M as MAX_DURATION_MS, r as MatchKey, s as MatchSpec, N as NameId, t as OutAnd, u as OutForwardInput, v as OutPlace, w as OutTimeout, x as OutXor, y as OutputEntry, z as PetriNetBuilder, B as Port, D as PortDirection, G as SubnetDefBuilder, H as SubnetInstance, J as Timing, Q as TimingDeadline, R as TimingDelayed, U as TimingExact, V as TimingImmediate, W as TimingWindow, X as TokenInput, Y as TokenOutput, Z as TransitionAction, _ as TransitionBuilder, $ as VerificationHarness, a0 as VerificationResult, a1 as all, a2 as allPlaces, a3 as and, a4 as andPlaces, a5 as arcPlace, a6 as atLeast, a7 as consumptionCount, a8 as deadline, a9 as delayed, aa as earliest, ab as enumerateBranches, ac as environmentPlace, ad as exact, ae as exactly, af as fork, ag as forwardInput, ah as hasDeadline, ai as hasGuard, aj as immediate, ak as inhibitorArc, al as inputArc, am as isPassthrough, an as isUnit, ao as keyForPlace, ap as latest, aq as matchCorrelates, ar as matchKey, as as matchSpec, at as matchesGuard, au as nameId, av as one, aw as outPlace, ax as outputArc, ay as passthrough, az as place, aA as produce, aB as readArc, aC as requiredCount, aD as resetArc, aE as timeout, aF as timeoutPlace, aG as tokenAt, aH as tokenOf, aI as transform, aJ as transformAsync, aK as transformFrom, aL as unitToken, aM as window, aN as withTimeout, aO as xor, aP as xorPlaces } from './petri-net-C3LSY-vm.js';
3
+ import { E as EventStore } from './event-store-DKTenPbC.js';
4
+ export { A as ActionTimedOut, a as ExecutionCompleted, b as ExecutionStarted, I as InMemoryEventStore, L as LogMessage, M as MarkingSnapshot, N as NetEvent, T as TokenAdded, c as TokenRemoved, d as TransitionClockRestarted, e as TransitionCompleted, f as TransitionEnabled, g as TransitionFailed, h as TransitionStarted, i as TransitionTimedOut, j as eventTransitionName, k as eventsOfType, l as failures, m as filterEvents, n as inMemoryEventStore, o as isFailureEvent, p as noopEventStore, t as transitionEvents } from './event-store-DKTenPbC.js';
5
5
 
6
6
  /**
7
7
  * Discriminated sum-type abstraction over Petri nets, distinguishing **closed**
@@ -344,6 +344,27 @@ declare class BitmapNetExecutor implements PetriNetExecutor {
344
344
  */
345
345
  private fireReadyImmediate;
346
346
  private fireReadyGeneral;
347
+ /**
348
+ * Fires a transition, containing any synchronous throw to that one firing so it fails
349
+ * only that transition, not the whole run.
350
+ *
351
+ * Without this boundary an unchecked throw raised while a transition fires — a hostile
352
+ * EventStore.append on a token-removed or transition-started emit, or an error thrown while
353
+ * consuming the matched tokens — unwinds out of the orchestrator loop and kills the executor.
354
+ * The transition is instead failed and marked dirty for re-evaluation, the same treatment an
355
+ * asynchronously-reported failure gets. Enablement-phase throws (a guard or key function that
356
+ * throws inside canEnable, before the firing) run outside this boundary and are not contained
357
+ * here.
358
+ *
359
+ * fireTransition removes tokens from the marking before it reconciles the presence
360
+ * bitmap, so a throw inside that window would leave bits asserting tokens that are gone;
361
+ * the recovery re-runs updateBitmapAfterConsumption against the real marking.
362
+ *
363
+ * Unlike the Java runtime there is no VirtualMachineError/LinkageError analogue on the JS
364
+ * side, so every throw is contained here — there is deliberately no fatal-rethrow escape
365
+ * hatch.
366
+ */
367
+ private fireTransitionContained;
347
368
  private fireTransition;
348
369
  private updateBitmapAfterConsumption;
349
370
  private processCompletedTransitions;
@@ -609,6 +630,28 @@ declare class PrecompiledNetExecutor implements PetriNetExecutor {
609
630
  */
610
631
  private fireReadyImmediate;
611
632
  private fireReadyGeneral;
633
+ /**
634
+ * Fires a transition, containing any synchronous throw to that one firing so it fails
635
+ * only that transition, not the whole run.
636
+ *
637
+ * Without this boundary an unchecked throw raised while a transition fires — a hostile
638
+ * EventStore.append on a token-removed or transition-started emit, or an error thrown while
639
+ * consuming the matched tokens — unwinds out of the orchestrator loop and kills the executor.
640
+ * The transition is instead failed and marked dirty for re-evaluation, the same treatment an
641
+ * asynchronously-reported failure gets. Enablement-phase throws (a guard or key function that
642
+ * throws inside canEnable, before the firing) run outside this boundary and are not contained
643
+ * here.
644
+ *
645
+ * fireTransition drains tokens from the queues before it reconciles the presence bitmap,
646
+ * so a throw inside that window would leave bits asserting tokens that are gone; the
647
+ * recovery re-runs updateBitmapAfterConsumption against the true queue lengths.
648
+ *
649
+ * Named to avoid colliding with the pre-existing {@link fireTransitionGuarded}
650
+ * guard-consume helper. Unlike the Java runtime there is no VirtualMachineError/
651
+ * LinkageError analogue on the JS side, so every throw is contained here — there is
652
+ * deliberately no fatal-rethrow escape hatch.
653
+ */
654
+ private fireTransitionContained;
612
655
  private fireTransition;
613
656
  /**
614
657
  * Consumes the name-matched tokens for a ν-net join (NU-020): correlated
@@ -677,6 +720,10 @@ declare function validateOutSpec(tName: string, spec: Out, producedPlaceNames: S
677
720
  * - **AND**: recurses into all children (all branches get tokens).
678
721
  * - **XOR**: disallowed — timeout cannot choose a branch non-deterministically.
679
722
  * - **Timeout**: disallowed — nested timeouts would create ambiguous recovery paths.
723
+ *
724
+ * Writes through {@link TransitionContext.outputToHarvest} rather than `output(...)`:
725
+ * by this point the context has been detached, so the ordinary write target is closed
726
+ * and only the harvest collector still reaches the marking.
680
727
  */
681
728
  declare function produceTimeoutOutput(context: TransitionContext, timeoutChild: Out): void;
682
729
 
package/dist/index.js CHANGED
@@ -4,15 +4,24 @@ import {
4
4
  and,
5
5
  andPlaces,
6
6
  enumerateBranches,
7
+ fork,
7
8
  forwardInput,
9
+ isPassthrough,
8
10
  isProven,
9
11
  isViolated,
10
12
  outPlace,
13
+ passthrough,
14
+ produce,
15
+ requireOutputProducingActions,
11
16
  timeout,
12
17
  timeoutPlace,
18
+ transform,
19
+ transformAsync,
20
+ transformFrom,
21
+ withTimeout,
13
22
  xor,
14
23
  xorPlaces
15
- } from "./chunk-V3WTQRHC.js";
24
+ } from "./chunk-7VJ5CYUU.js";
16
25
  import {
17
26
  eventTransitionName,
18
27
  isFailureEvent
@@ -142,83 +151,69 @@ function nameId(value) {
142
151
  return value;
143
152
  }
144
153
 
145
- // src/core/transition-action.ts
146
- function passthrough() {
147
- return PASSTHROUGH;
148
- }
149
- var PASSTHROUGH = async () => {
150
- };
151
- function transform(fn) {
152
- return async (ctx) => {
153
- const result = fn(ctx);
154
- for (const outputPlace of ctx.outputPlaces()) {
155
- ctx.output(outputPlace, result);
156
- }
157
- };
158
- }
159
- function fork() {
160
- return transform((ctx) => {
161
- const inputPlaces = ctx.inputPlaces();
162
- if (inputPlaces.size !== 1) {
163
- throw new Error(`Fork requires exactly 1 input place, found ${inputPlaces.size}`);
164
- }
165
- const inputPlace = inputPlaces.values().next().value;
166
- return ctx.input(inputPlace);
167
- });
168
- }
169
- function transformFrom(inputPlace, fn) {
170
- return transform((ctx) => fn(ctx.input(inputPlace)));
171
- }
172
- function transformAsync(fn) {
173
- return async (ctx) => {
174
- const result = await fn(ctx);
175
- for (const outputPlace of ctx.outputPlaces()) {
176
- ctx.output(outputPlace, result);
154
+ // src/core/token-output.ts
155
+ var TokenOutput = class {
156
+ _entries = [];
157
+ /**
158
+ * Once true, further writes are dropped instead of appended. Set by the executor
159
+ * (via {@link import('./transition-context.js').TransitionContext.detachForTimeout})
160
+ * when a firing times out, so the action it has stopped waiting for can no longer
161
+ * reach the marking. Irreversible.
162
+ */
163
+ detached = false;
164
+ /** Add a value to an output place (creates token with current timestamp). */
165
+ add(place2, value) {
166
+ if (this.detached) return this;
167
+ this._entries.push({ place: place2, token: tokenOf(value) });
168
+ return this;
169
+ }
170
+ /** Add a pre-existing token to an output place. */
171
+ addToken(place2, token) {
172
+ if (this.detached) return this;
173
+ this._entries.push({ place: place2, token });
174
+ return this;
175
+ }
176
+ /**
177
+ * @internal Severs this collector: subsequent {@link add}/{@link addToken} calls are
178
+ * dropped. Executor machinery invoked when a firing times out — never call from an action.
179
+ */
180
+ detach() {
181
+ this.detached = true;
182
+ }
183
+ /** Returns all collected outputs. */
184
+ entries() {
185
+ return this._entries;
186
+ }
187
+ /** Check if any outputs were produced. */
188
+ isEmpty() {
189
+ return this._entries.length === 0;
190
+ }
191
+ /** Returns the set of place names that received tokens. */
192
+ placesWithTokens() {
193
+ const result = /* @__PURE__ */ new Set();
194
+ for (const entry of this._entries) {
195
+ result.add(entry.place.name);
177
196
  }
178
- };
179
- }
180
- function produce(place2, value) {
181
- return async (ctx) => {
182
- ctx.output(place2, value);
183
- };
184
- }
185
- function withTimeout(action, timeoutMs, timeoutPlace2, timeoutValue) {
186
- return (ctx) => {
187
- return new Promise((resolve2, reject) => {
188
- let completed = false;
189
- const timer = setTimeout(() => {
190
- if (!completed) {
191
- completed = true;
192
- ctx.output(timeoutPlace2, timeoutValue);
193
- resolve2();
194
- }
195
- }, timeoutMs);
196
- action(ctx).then(
197
- () => {
198
- if (!completed) {
199
- completed = true;
200
- clearTimeout(timer);
201
- resolve2();
202
- }
203
- },
204
- (err) => {
205
- if (!completed) {
206
- completed = true;
207
- clearTimeout(timer);
208
- reject(err);
209
- }
210
- }
211
- );
212
- });
213
- };
214
- }
197
+ return result;
198
+ }
199
+ };
215
200
 
216
201
  // src/core/transition-context.ts
217
202
  var EMPTY_ALIAS = /* @__PURE__ */ new Map();
218
203
  var GLOBAL_FRESH_NAME_COUNTER = 0;
219
204
  var TransitionContext = class {
220
205
  rawInput;
206
+ /**
207
+ * What the executor harvests. Identical to {@link writeTarget} until
208
+ * {@link detachForTimeout} replaces it with a fresh collector the action cannot reach.
209
+ */
221
210
  _rawOutput;
211
+ /**
212
+ * What the action writes to. Never reassigned, so an action already inside
213
+ * `output(...)` cannot be redirected mid-write; severance happens by detaching this
214
+ * collector, not by swapping it.
215
+ */
216
+ writeTarget;
222
217
  allowedInputs;
223
218
  allowedReads;
224
219
  allowedOutputs;
@@ -234,6 +229,7 @@ var TransitionContext = class {
234
229
  this._transitionName = transitionName;
235
230
  this.rawInput = rawInput;
236
231
  this._rawOutput = rawOutput;
232
+ this.writeTarget = rawOutput;
237
233
  this._inputPlaces = inputPlaces;
238
234
  this._readPlaces = readPlaces;
239
235
  this._outputPlaces = outputPlaces;
@@ -340,7 +336,7 @@ var TransitionContext = class {
340
336
  const actual = this.resolve(place2);
341
337
  this.requireOutput(actual);
342
338
  for (const value of values) {
343
- this._rawOutput.add(actual, value);
339
+ this.writeTarget.add(actual, value);
344
340
  }
345
341
  return this;
346
342
  }
@@ -356,7 +352,7 @@ var TransitionContext = class {
356
352
  const actual = this.resolve(place2);
357
353
  this.requireOutput(actual);
358
354
  for (const token of tokens) {
359
- this._rawOutput.addToken(actual, token);
355
+ this.writeTarget.addToken(actual, token);
360
356
  }
361
357
  return this;
362
358
  }
@@ -371,6 +367,38 @@ var TransitionContext = class {
371
367
  );
372
368
  }
373
369
  }
370
+ /**
371
+ * Severs the action's output from the marking, for use when a firing times out.
372
+ *
373
+ * After this call the action's `output(...)` writes are dropped, and the executor
374
+ * harvests a fresh collector the action holds no reference to. Anything the action
375
+ * wrote *before* the timeout is discarded with it: a partial result merged with the
376
+ * timeout branch would violate the transition's own output spec.
377
+ *
378
+ * This is the only isolation available — libpetri does not own the promise the action
379
+ * runs on, so it cannot stop the work, only stop the result from landing.
380
+ *
381
+ * @internal Executor machinery — must be called by the executor, never by an action.
382
+ */
383
+ detachForTimeout() {
384
+ this.writeTarget.detach();
385
+ this._rawOutput = new TokenOutput();
386
+ }
387
+ /**
388
+ * Produces into the executor's harvest collector rather than the action's write target.
389
+ *
390
+ * Identical to {@link output} before {@link detachForTimeout}; after it, this is the
391
+ * only route that still reaches the marking. Used by the executor to deposit the
392
+ * timeout branch.
393
+ *
394
+ * @internal Executor machinery — must be called by the executor, never by an action.
395
+ */
396
+ outputToHarvest(place2, value) {
397
+ const actual = this.resolve(place2);
398
+ this.requireOutput(actual);
399
+ this._rawOutput.add(actual, value);
400
+ return this;
401
+ }
374
402
  // ==================== ν-name minting (NU-010) ====================
375
403
  /**
376
404
  * @internal Installs the ν-name minter. Wired by the executor at firing time
@@ -462,37 +490,6 @@ var TokenInput = class {
462
490
  }
463
491
  };
464
492
 
465
- // src/core/token-output.ts
466
- var TokenOutput = class {
467
- _entries = [];
468
- /** Add a value to an output place (creates token with current timestamp). */
469
- add(place2, value) {
470
- this._entries.push({ place: place2, token: tokenOf(value) });
471
- return this;
472
- }
473
- /** Add a pre-existing token to an output place. */
474
- addToken(place2, token) {
475
- this._entries.push({ place: place2, token });
476
- return this;
477
- }
478
- /** Returns all collected outputs. */
479
- entries() {
480
- return this._entries;
481
- }
482
- /** Check if any outputs were produced. */
483
- isEmpty() {
484
- return this._entries.length === 0;
485
- }
486
- /** Returns the set of place names that received tokens. */
487
- placesWithTokens() {
488
- const result = /* @__PURE__ */ new Set();
489
- for (const entry of this._entries) {
490
- result.add(entry.place.name);
491
- }
492
- return result;
493
- }
494
- };
495
-
496
493
  // src/core/transition.ts
497
494
  var TRANSITION_KEY = /* @__PURE__ */ Symbol("Transition.internal");
498
495
  var EMPTY_PLACE_ALIAS = /* @__PURE__ */ new Map();
@@ -1267,10 +1264,6 @@ function describeTiming(t) {
1267
1264
  return `Exact(atMs=${t.atMs})`;
1268
1265
  }
1269
1266
  }
1270
- var PASSTHROUGH_REF = passthrough();
1271
- function isPassthrough(action) {
1272
- return action === PASSTHROUGH_REF;
1273
- }
1274
1267
  function keyOfIn(arc) {
1275
1268
  switch (arc.type) {
1276
1269
  case "one":
@@ -1513,6 +1506,7 @@ var SubnetDef = class _SubnetDef {
1513
1506
  }
1514
1507
  }
1515
1508
  const syntheticNet = PetriNet.builder("verify_" + this.name).compose(sut, portMappings).build();
1509
+ requireOutputProducingActions(syntheticNet);
1516
1510
  const perProperty = /* @__PURE__ */ new Map();
1517
1511
  for (const property of properties) {
1518
1512
  const verifier = SmtVerifier.forNet(syntheticNet).property(property);
@@ -1864,6 +1858,10 @@ var PetriNet = class _PetriNet {
1864
1858
  }
1865
1859
  /**
1866
1860
  * Creates a new PetriNet with actions bound via a resolver function.
1861
+ *
1862
+ * The resolver is called once per transition with its name. Returning `null`
1863
+ * defers that transition — it keeps whatever action it already carries — which
1864
+ * is what makes staged binding (**MOD-024** AC7) work.
1867
1865
  */
1868
1866
  bindActionsWithResolver(actionResolver) {
1869
1867
  const boundTransitions = /* @__PURE__ */ new Set();
@@ -2497,6 +2495,7 @@ var CompiledNet = class _CompiledNet {
2497
2495
  _hasMatch;
2498
2496
  constructor(net) {
2499
2497
  this.net = net;
2498
+ requireOutputProducingActions(net);
2500
2499
  const allPlacesSet = /* @__PURE__ */ new Map();
2501
2500
  for (const t of net.transitions) {
2502
2501
  for (const spec of t.inputSpecs) allPlacesSet.set(spec.place.name, spec.place);
@@ -3006,6 +3005,25 @@ var OutViolationError = class extends Error {
3006
3005
  };
3007
3006
 
3008
3007
  // src/runtime/executor-support.ts
3008
+ function executeAction(t, context) {
3009
+ try {
3010
+ const stage = t.action(context);
3011
+ if (stage == null || typeof stage.then !== "function") {
3012
+ return Promise.reject(new Error(
3013
+ `'${t.name}': action returned null/non-thenable instead of a Promise`
3014
+ ));
3015
+ }
3016
+ return Promise.resolve(stage);
3017
+ } catch (err) {
3018
+ return Promise.reject(err);
3019
+ }
3020
+ }
3021
+ function swallowEventStoreFailure(when, err) {
3022
+ console.warn(
3023
+ `libpetri: EventStore.append threw while emitting ${when}; the event is dropped`,
3024
+ err
3025
+ );
3026
+ }
3009
3027
  var DEADLINE_TOLERANCE_MS = 5;
3010
3028
  function validateOutSpec(tName, spec, producedPlaceNames) {
3011
3029
  switch (spec.type) {
@@ -3047,11 +3065,11 @@ function validateOutSpec(tName, spec, producedPlaceNames) {
3047
3065
  function produceTimeoutOutput(context, timeoutChild) {
3048
3066
  switch (timeoutChild.type) {
3049
3067
  case "place":
3050
- context.output(timeoutChild.place, null);
3068
+ context.outputToHarvest(timeoutChild.place, null);
3051
3069
  break;
3052
3070
  case "forward-input": {
3053
3071
  const value = context.input(timeoutChild.from);
3054
- context.output(timeoutChild.to, value);
3072
+ context.outputToHarvest(timeoutChild.to, value);
3055
3073
  break;
3056
3074
  }
3057
3075
  case "and":
@@ -3506,7 +3524,7 @@ var BitmapNetExecutor = class {
3506
3524
  for (let tid = 0; tid < this.compiled.transitionCount; tid++) {
3507
3525
  if (!this.enabledFlags[tid] || this.inFlightFlags[tid]) continue;
3508
3526
  if (this.canEnable(tid, this.markedPlaces)) {
3509
- this.fireTransition(tid);
3527
+ this.fireTransitionContained(tid);
3510
3528
  } else {
3511
3529
  this.enabledFlags[tid] = 0;
3512
3530
  this.enabledTransitionCount--;
@@ -3538,7 +3556,7 @@ var BitmapNetExecutor = class {
3538
3556
  for (const entry of ready) {
3539
3557
  const { tid } = entry;
3540
3558
  if (this.enabledFlags[tid] && this.canEnable(tid, freshSnap)) {
3541
- this.fireTransition(tid);
3559
+ this.fireTransitionContained(tid);
3542
3560
  freshSnap.set(this.markedPlaces);
3543
3561
  } else {
3544
3562
  this.enabledFlags[tid] = 0;
@@ -3547,6 +3565,53 @@ var BitmapNetExecutor = class {
3547
3565
  }
3548
3566
  }
3549
3567
  }
3568
+ /**
3569
+ * Fires a transition, containing any synchronous throw to that one firing so it fails
3570
+ * only that transition, not the whole run.
3571
+ *
3572
+ * Without this boundary an unchecked throw raised while a transition fires — a hostile
3573
+ * EventStore.append on a token-removed or transition-started emit, or an error thrown while
3574
+ * consuming the matched tokens — unwinds out of the orchestrator loop and kills the executor.
3575
+ * The transition is instead failed and marked dirty for re-evaluation, the same treatment an
3576
+ * asynchronously-reported failure gets. Enablement-phase throws (a guard or key function that
3577
+ * throws inside canEnable, before the firing) run outside this boundary and are not contained
3578
+ * here.
3579
+ *
3580
+ * fireTransition removes tokens from the marking before it reconciles the presence
3581
+ * bitmap, so a throw inside that window would leave bits asserting tokens that are gone;
3582
+ * the recovery re-runs updateBitmapAfterConsumption against the real marking.
3583
+ *
3584
+ * Unlike the Java runtime there is no VirtualMachineError/LinkageError analogue on the JS
3585
+ * side, so every throw is contained here — there is deliberately no fatal-rethrow escape
3586
+ * hatch.
3587
+ */
3588
+ fireTransitionContained(tid) {
3589
+ try {
3590
+ this.fireTransition(tid);
3591
+ } catch (e) {
3592
+ const t = this.compiled.transition(tid);
3593
+ if (this.enabledFlags[tid]) {
3594
+ this.enabledFlags[tid] = 0;
3595
+ this.enabledTransitionCount--;
3596
+ this.enabledAtMs[tid] = -Infinity;
3597
+ }
3598
+ if (this.inFlight.delete(t)) {
3599
+ this.inFlightFlags[tid] = 0;
3600
+ }
3601
+ this.updateBitmapAfterConsumption(tid);
3602
+ this.matchCaches[tid] = null;
3603
+ const err = e instanceof Error ? e : new Error(String(e));
3604
+ this.emitEvent({
3605
+ type: "transition-failed",
3606
+ timestamp: Date.now(),
3607
+ transitionName: t.name,
3608
+ errorMessage: err.message,
3609
+ exceptionType: err.name,
3610
+ stack: err.stack
3611
+ });
3612
+ this.markTransitionDirty(tid);
3613
+ }
3614
+ }
3550
3615
  fireTransition(tid) {
3551
3616
  const t = this.compiled.transition(tid);
3552
3617
  const inputs = new TokenInput();
@@ -3645,7 +3710,7 @@ var BitmapNetExecutor = class {
3645
3710
  );
3646
3711
  const freshNameBase = t.name;
3647
3712
  context.setFreshNameSupplier(() => nameId(`${freshNameBase}#${this.freshNameCounter++}`));
3648
- let actionPromise = t.action(context);
3713
+ let actionPromise = executeAction(t, context);
3649
3714
  if (t.hasActionTimeout()) {
3650
3715
  const timeoutSpec = t.actionTimeout;
3651
3716
  if (timeoutSpec === null) throw new Error(`Expected actionTimeout on ${t.name}`);
@@ -3657,6 +3722,7 @@ var BitmapNetExecutor = class {
3657
3722
  )
3658
3723
  ]).catch((err) => {
3659
3724
  if (err instanceof TimeoutSentinel) {
3725
+ context.detachForTimeout();
3660
3726
  produceTimeoutOutput(context, timeoutSpec.child);
3661
3727
  this.emitEvent({
3662
3728
  type: "action-timed-out",
@@ -3927,7 +3993,11 @@ var BitmapNetExecutor = class {
3927
3993
  // ======================== Event Emission ========================
3928
3994
  emitEvent(event) {
3929
3995
  if (this.eventStoreEnabled) {
3930
- this.eventStore.append(event);
3996
+ try {
3997
+ this.eventStore.append(event);
3998
+ } catch (err) {
3999
+ swallowEventStoreFailure(event.type, err);
4000
+ }
3931
4001
  }
3932
4002
  }
3933
4003
  };
@@ -4764,7 +4834,7 @@ var PrecompiledNetExecutor = class {
4764
4834
  for (let tid = 0; tid < tc; tid++) {
4765
4835
  if (!this.enabledFlags[tid] || this.inFlightFlags[tid]) continue;
4766
4836
  if (this.canEnable(tid, this.markingBitmap)) {
4767
- this.fireTransition(tid);
4837
+ this.fireTransitionContained(tid);
4768
4838
  } else {
4769
4839
  this.enabledFlags[tid] = 0;
4770
4840
  this.enabledTransitionCount--;
@@ -4795,7 +4865,7 @@ var PrecompiledNetExecutor = class {
4795
4865
  for (const entry of ready) {
4796
4866
  const { tid } = entry;
4797
4867
  if (this.enabledFlags[tid] && this.canEnable(tid, freshSnap)) {
4798
- this.fireTransition(tid);
4868
+ this.fireTransitionContained(tid);
4799
4869
  freshSnap.set(this.markingBitmap);
4800
4870
  } else {
4801
4871
  this.enabledFlags[tid] = 0;
@@ -4804,6 +4874,60 @@ var PrecompiledNetExecutor = class {
4804
4874
  }
4805
4875
  }
4806
4876
  }
4877
+ /**
4878
+ * Fires a transition, containing any synchronous throw to that one firing so it fails
4879
+ * only that transition, not the whole run.
4880
+ *
4881
+ * Without this boundary an unchecked throw raised while a transition fires — a hostile
4882
+ * EventStore.append on a token-removed or transition-started emit, or an error thrown while
4883
+ * consuming the matched tokens — unwinds out of the orchestrator loop and kills the executor.
4884
+ * The transition is instead failed and marked dirty for re-evaluation, the same treatment an
4885
+ * asynchronously-reported failure gets. Enablement-phase throws (a guard or key function that
4886
+ * throws inside canEnable, before the firing) run outside this boundary and are not contained
4887
+ * here.
4888
+ *
4889
+ * fireTransition drains tokens from the queues before it reconciles the presence bitmap,
4890
+ * so a throw inside that window would leave bits asserting tokens that are gone; the
4891
+ * recovery re-runs updateBitmapAfterConsumption against the true queue lengths.
4892
+ *
4893
+ * Named to avoid colliding with the pre-existing {@link fireTransitionGuarded}
4894
+ * guard-consume helper. Unlike the Java runtime there is no VirtualMachineError/
4895
+ * LinkageError analogue on the JS side, so every throw is contained here — there is
4896
+ * deliberately no fatal-rethrow escape hatch.
4897
+ */
4898
+ fireTransitionContained(tid) {
4899
+ try {
4900
+ this.fireTransition(tid);
4901
+ } catch (e) {
4902
+ const t = this.program.compiled.transition(tid);
4903
+ if (this.enabledFlags[tid]) {
4904
+ this.enabledFlags[tid] = 0;
4905
+ this.enabledTransitionCount--;
4906
+ this.enabledAtMs[tid] = -Infinity;
4907
+ }
4908
+ if (this.inFlightFlags[tid]) {
4909
+ this.inFlightPromises[tid] = null;
4910
+ this.inFlightContexts[tid] = null;
4911
+ this.inFlightConsumed[tid] = null;
4912
+ this.inFlightResolves[tid] = null;
4913
+ this.inFlightErrors[tid] = null;
4914
+ this.inFlightFlags[tid] = 0;
4915
+ this.inFlightCount--;
4916
+ }
4917
+ this.updateBitmapAfterConsumption(tid);
4918
+ this.matchCaches[tid] = null;
4919
+ const err = e instanceof Error ? e : new Error(String(e));
4920
+ this.emitEvent({
4921
+ type: "transition-failed",
4922
+ timestamp: Date.now(),
4923
+ transitionName: t.name,
4924
+ errorMessage: err.message,
4925
+ exceptionType: err.name,
4926
+ stack: err.stack
4927
+ });
4928
+ this.markTransitionDirty(tid);
4929
+ }
4930
+ }
4807
4931
  fireTransition(tid) {
4808
4932
  const prog = this.program;
4809
4933
  const t = prog.compiled.transition(tid);
@@ -4952,7 +5076,7 @@ var PrecompiledNetExecutor = class {
4952
5076
  this.freshNameSuppliers[tid] = freshNameSupplier;
4953
5077
  }
4954
5078
  context.setFreshNameSupplier(freshNameSupplier);
4955
- let actionPromise = t.action(context);
5079
+ let actionPromise = executeAction(t, context);
4956
5080
  if (t.hasActionTimeout()) {
4957
5081
  const timeoutSpec = t.actionTimeout;
4958
5082
  if (timeoutSpec === null) throw new Error(`Expected actionTimeout on ${t.name}`);
@@ -4964,6 +5088,7 @@ var PrecompiledNetExecutor = class {
4964
5088
  )
4965
5089
  ]).catch((err) => {
4966
5090
  if (err instanceof TimeoutSentinel2) {
5091
+ context.detachForTimeout();
4967
5092
  produceTimeoutOutput(context, timeoutSpec.child);
4968
5093
  this.emitEvent({
4969
5094
  type: "action-timed-out",
@@ -5358,7 +5483,11 @@ var PrecompiledNetExecutor = class {
5358
5483
  // ======================== Event Emission ========================
5359
5484
  emitEvent(event) {
5360
5485
  if (this.eventStoreEnabled) {
5361
- this.eventStore.append(event);
5486
+ try {
5487
+ this.eventStore.append(event);
5488
+ } catch (err) {
5489
+ swallowEventStoreFailure(event.type, err);
5490
+ }
5362
5491
  }
5363
5492
  }
5364
5493
  };
@@ -5423,6 +5552,7 @@ export {
5423
5552
  inputArc,
5424
5553
  intersects,
5425
5554
  isFailureEvent,
5555
+ isPassthrough,
5426
5556
  isUnit,
5427
5557
  keyForPlace,
5428
5558
  latest,