@context-action/core 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1414,7 +1414,7 @@ var ActionRegister = class {
1414
1414
  return newUnregister;
1415
1415
  } else {
1416
1416
  if (!existing) throw new Error("Internal error: existing handler should be defined in duplicate handler block");
1417
- this.log(`Handler duplicate ignored, returning existing unregister: ${String(action)}`, {
1417
+ this.log(`Handler duplicate ignored, returning no-op unregister: ${String(action)}`, {
1418
1418
  handlerId,
1419
1419
  existingPriority: existing.config.priority,
1420
1420
  newPriority: config.priority,
@@ -1422,12 +1422,7 @@ var ActionRegister = class {
1422
1422
  newBlocking: config.blocking,
1423
1423
  note: "Use replaceExisting:true to replace"
1424
1424
  }, "warn");
1425
- if (existingUnregister) return existingUnregister;
1426
- else {
1427
- const newUnregister = this.createUnregisterFunction(action, handlerId, existing);
1428
- actionUnregisterFunctions.set(handlerId, newUnregister);
1429
- return newUnregister;
1430
- }
1425
+ return () => {};
1431
1426
  }
1432
1427
  }
1433
1428
  pipeline.push(registration);
@@ -1451,7 +1446,8 @@ var ActionRegister = class {
1451
1446
  const attemptState = { count: 0 };
1452
1447
  const plan = this.resolveDispatchPlan(action, options);
1453
1448
  const hasTimingGuard = plan.debounceMs !== void 0 || plan.throttleMs !== void 0;
1454
- let observerNotified = false;
1449
+ const notifiedObservers = /* @__PURE__ */ new Set();
1450
+ const notifiedObserverOutcomes = /* @__PURE__ */ new Set();
1455
1451
  let terminalErrorReported = false;
1456
1452
  const reportTerminalError = (error) => {
1457
1453
  if (terminalErrorReported) return;
@@ -1459,9 +1455,9 @@ var ActionRegister = class {
1459
1455
  this.invokeErrorHandler(error, action, payload, options, attemptState.count);
1460
1456
  };
1461
1457
  const notifyObservers = async (event) => {
1462
- if (observerNotified) return;
1463
- observerNotified = true;
1464
- await this.executeObservers(action, plan, event);
1458
+ if (notifiedObserverOutcomes.has(event.outcome)) return;
1459
+ notifiedObserverOutcomes.add(event.outcome);
1460
+ await this.executeObservers(action, plan, event, notifiedObservers);
1465
1461
  };
1466
1462
  const pipelineOperation = async () => {
1467
1463
  const guard = plan.guards.length > 0 ? await this.executeGuardPhase(action, payload, timeoutScope.options, plan, dispatchHandlerPromises) : {
@@ -1496,6 +1492,18 @@ var ActionRegister = class {
1496
1492
  this.cleanupOneTimeHandlers(action, executedHandlers, dispatchHandlerPromises);
1497
1493
  }
1498
1494
  }, timeoutScope.options, attemptState, (result) => result.outcome === "failed", () => this.getAttemptHandlers(action, plan).length > 0, void 0, this.shouldDrainBeforeRetry(plan, timeoutScope.options) ? () => this.drainAttemptHandlers(dispatchHandlerPromises) : void 0);
1495
+ if (timeoutScope.options?.signal?.aborted) {
1496
+ if (timeoutScope.options.signal.reason instanceof ActionTimeoutError) throw timeoutScope.options.signal.reason;
1497
+ await notifyObservers({
1498
+ action: String(action),
1499
+ payload: guard.payload,
1500
+ outcome: "cancelled",
1501
+ result: void 0,
1502
+ errors: execution.errors,
1503
+ signal: timeoutScope.options.signal
1504
+ });
1505
+ return;
1506
+ }
1499
1507
  if (execution.outcome === "failed") throw execution.errors[execution.errors.length - 1]?.error ?? /* @__PURE__ */ new Error(`Action "${String(action)}" failed`);
1500
1508
  const result = this.processResults(execution.results, execution.terminated, execution.terminated ? execution.result : void 0, options?.result);
1501
1509
  await notifyObservers({
@@ -1595,7 +1603,7 @@ var ActionRegister = class {
1595
1603
  }
1596
1604
  const observedPromise = this.raceWithTimeout(observedDispatchPromise, timeoutScope, dispatchHandlerPromises).catch(async (error) => {
1597
1605
  reportTerminalError(error);
1598
- if (!observerNotified) await this.trackGlobalHandlerPromise(notifyObservers({
1606
+ this.trackGlobalHandlerPromise(notifyObservers({
1599
1607
  action: String(action),
1600
1608
  payload,
1601
1609
  outcome: "failed",
@@ -1607,7 +1615,9 @@ var ActionRegister = class {
1607
1615
  severity: "blocking"
1608
1616
  }],
1609
1617
  signal: timeoutScope.options?.signal
1610
- }));
1618
+ })).catch((observerError) => {
1619
+ this.log(`Failure observer delivery failed for ${String(action)}`, observerError, "warn");
1620
+ });
1611
1621
  throw error;
1612
1622
  });
1613
1623
  observedPromise.catch(() => {});
@@ -1955,12 +1965,18 @@ var ActionRegister = class {
1955
1965
  /** Observers run after the canonical result has been constructed. Their
1956
1966
  * failures are isolated from that immutable result; detached observers are
1957
1967
  * still tracked for registry shutdown. */
1958
- async executeObservers(action, plan, event) {
1968
+ async executeObservers(action, plan, event, notifiedObservers = /* @__PURE__ */ new Set()) {
1959
1969
  const observerEvent = this.safeSnapshotObserverEvent(event);
1970
+ const selectedObservers = [];
1960
1971
  for (const [registration, observerEntry] of this.getObservers(action, plan)) {
1972
+ if (notifiedObservers.has(registration)) continue;
1961
1973
  const successful = observerEvent.outcome === "completed" || observerEvent.outcome === "completed_with_errors";
1962
1974
  if (observerEntry.when === "success" && !successful) continue;
1963
1975
  if (observerEntry.when === "failure" && successful) continue;
1976
+ notifiedObservers.add(registration);
1977
+ selectedObservers.push([registration, observerEntry]);
1978
+ }
1979
+ for (const [registration, observerEntry] of selectedObservers) {
1964
1980
  let shouldRun = true;
1965
1981
  try {
1966
1982
  shouldRun = registration.config.condition?.(observerEvent.payload) ?? true;
@@ -2227,13 +2243,16 @@ var ActionRegister = class {
2227
2243
  this.cleanupOneTimeHandlers(action, executedHandlers, dispatchHandlerPromises);
2228
2244
  }
2229
2245
  }, timeoutScope.options, attemptState, (result) => result.outcome === "failed", () => this.getAttemptHandlers(action, plan).length > 0, retryTelemetry, this.shouldDrainBeforeRetry(plan, timeoutScope.options) ? () => this.drainAttemptHandlers(dispatchHandlerPromises) : void 0);
2230
- if (timeoutScope.options?.signal?.aborted) return {
2231
- ...rawExecution,
2232
- success: false,
2233
- aborted: true,
2234
- abortReason: typeof timeoutScope.options.signal.reason === "string" ? timeoutScope.options.signal.reason : "Action dispatch aborted by signal",
2235
- outcome: "cancelled"
2236
- };
2246
+ if (timeoutScope.options?.signal?.aborted) {
2247
+ if (timeoutScope.options.signal.reason instanceof ActionTimeoutError) throw timeoutScope.options.signal.reason;
2248
+ return {
2249
+ ...rawExecution,
2250
+ success: false,
2251
+ aborted: true,
2252
+ abortReason: typeof timeoutScope.options.signal.reason === "string" ? timeoutScope.options.signal.reason : "Action dispatch aborted by signal",
2253
+ outcome: "cancelled"
2254
+ };
2255
+ }
2237
2256
  const executionWithGuards = {
2238
2257
  ...rawExecution,
2239
2258
  handlers: [...guard.outcomes.map((outcome) => ({
@@ -2294,7 +2313,8 @@ var ActionRegister = class {
2294
2313
  timeoutScope.onTimeout((error) => queued.cancel(error));
2295
2314
  return queued.promise;
2296
2315
  };
2297
- let observerNotified = false;
2316
+ const notifiedObservers = /* @__PURE__ */ new Set();
2317
+ const notifiedObserverOutcomes = /* @__PURE__ */ new Set();
2298
2318
  let terminalErrorReported = false;
2299
2319
  const reportTerminalError = (error) => {
2300
2320
  if (terminalErrorReported) return;
@@ -2302,9 +2322,9 @@ var ActionRegister = class {
2302
2322
  this.invokeErrorHandler(error, action, payload, options, attemptState.count);
2303
2323
  };
2304
2324
  const notifyObservers = async (event) => {
2305
- if (observerNotified) return;
2306
- observerNotified = true;
2307
- await this.executeObservers(action, plan, event);
2325
+ if (notifiedObserverOutcomes.has(event.outcome)) return;
2326
+ notifiedObserverOutcomes.add(event.outcome);
2327
+ await this.executeObservers(action, plan, event, notifiedObservers);
2308
2328
  };
2309
2329
  let dispatchPromise;
2310
2330
  let observedDispatchPromise;
@@ -2362,7 +2382,7 @@ var ActionRegister = class {
2362
2382
  }
2363
2383
  const observedPromise = this.raceWithTimeout(observedDispatchPromise, timeoutScope, dispatchHandlerPromises).catch(async (error) => {
2364
2384
  reportTerminalError(error);
2365
- if (!observerNotified) await this.trackGlobalHandlerPromise(notifyObservers({
2385
+ this.trackGlobalHandlerPromise(notifyObservers({
2366
2386
  action: String(action),
2367
2387
  payload: observerPayload,
2368
2388
  outcome: "failed",
@@ -2374,7 +2394,9 @@ var ActionRegister = class {
2374
2394
  severity: "blocking"
2375
2395
  }],
2376
2396
  signal: timeoutScope.options?.signal
2377
- }));
2397
+ })).catch((observerError) => {
2398
+ this.log(`Failure observer delivery failed for ${String(action)}`, observerError, "warn");
2399
+ });
2378
2400
  throw error;
2379
2401
  });
2380
2402
  observedPromise.catch(() => {});
@@ -2614,7 +2636,7 @@ var ActionRegister = class {
2614
2636
  if (resultOptions.maxResults !== void 0 && (!Number.isSafeInteger(resultOptions.maxResults) || resultOptions.maxResults < 0)) throw new RangeError("maxResults must be a non-negative safe integer.");
2615
2637
  }
2616
2638
  processResults(results, terminated, terminationResult, resultOptions) {
2617
- if (terminated && terminationResult !== void 0) return terminationResult;
2639
+ if (terminated) return terminationResult;
2618
2640
  if (!resultOptions) return results.length > 0 ? results[results.length - 1] : void 0;
2619
2641
  if (!resultOptions.collect && !resultOptions.strategy) return;
2620
2642
  const collectedResults = results.filter((result) => result !== void 0);
@@ -2988,7 +3010,7 @@ var ActionRegister = class {
2988
3010
  cancelPendingDispatches() {
2989
3011
  this.dispatchQueue?.clear({ rejectPending: true });
2990
3012
  }
2991
- beginShutdown() {
3013
+ beginShutdown(deferCleanup = false) {
2992
3014
  if (this.destroyAsyncPromise) return this.destroyAsyncPromise;
2993
3015
  if (this.lifecycleState === "destroyed") {
2994
3016
  this.destroyAsyncPromise = Promise.resolve();
@@ -3008,7 +3030,7 @@ var ActionRegister = class {
3008
3030
  rejectPending: true,
3009
3031
  reason: shutdownError
3010
3032
  });
3011
- if (this.dispatchConstructionDepth === 0 && this.activeDispatches.size === 0 && this.activeHandlerPromises.size === 0) {
3033
+ if (!deferCleanup && this.dispatchConstructionDepth === 0 && this.activeDispatches.size === 0 && this.activeHandlerPromises.size === 0) {
3012
3034
  this.finalizeDestroy();
3013
3035
  resolveShutdown();
3014
3036
  return this.destroyAsyncPromise;
@@ -3046,13 +3068,18 @@ var ActionRegister = class {
3046
3068
  * Begin terminal shutdown and resolve after all started handlers have settled
3047
3069
  * and their registered cleanup functions have run.
3048
3070
  *
3071
+ * `deferCleanup` closes the register synchronously while deferring final
3072
+ * registered cleanup until the next microtask. This is useful for React
3073
+ * commit phases that must invalidate stale dispatchers immediately without
3074
+ * invoking user cleanup code inside the commit hook itself.
3075
+ *
3049
3076
  * Repeated calls return the same promise. New registrations and dispatches are
3050
3077
  * rejected as soon as shutdown begins.
3051
3078
  *
3052
3079
  * @public
3053
3080
  */
3054
- destroyAsync() {
3055
- return this.beginShutdown();
3081
+ destroyAsync(options = {}) {
3082
+ return this.beginShutdown(options.deferCleanup ?? false);
3056
3083
  }
3057
3084
  };
3058
3085