@fabricorg/experience-runtime 0.3.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8436c64: Close what a seventh review found in the previous follow-up, two of them regressions it introduced.
8
+
9
+ **An idempotency conflict is settled, but which way depends on why.** The previous release treated
10
+ every conflict as "never applied" and reported it refused. Only a `parameters` or `actor` conflict
11
+ means that. An `authority_binding` or `action_version` conflict means the _original_ invocation
12
+ exists and may well have applied — the capture-authority adapter mints a fresh binding on every
13
+ attempt, so a first attempt whose response was lost conflicts with itself on the second. Deleting that
14
+ capture and calling it refused was the worst available answer; it is now surfaced as `unknown` under
15
+ status `conflict_prior_invocation`.
16
+
17
+ **A policy block or validation failure is now `unknown`, not `refused`.** Within one attempt both are
18
+ decided before any effect. On a recovery re-execution the worker skips adapter steps that already
19
+ succeeded, then re-evaluates policy with a fresh clock and re-parses parameters against a possibly
20
+ newer action, and either can flip after effects committed. Unlike the authority path, the host
21
+ persists no execution reason for these, so the client cannot tell first delivery from recovery; it
22
+ errs toward a person checking. The precise fix is for the host to persist `executionReason` on failed
23
+ invocations, which is a store change and is recorded rather than attempted here.
24
+
25
+ **`failed` means "never delivered" again.** A store fault after a successful delivery was being counted
26
+ in it; it now lives only in `storeFailures`. The 0.3.0 changelog's disclosure misdescribed what 0.2.0
27
+ had changed — `parked` never shipped as a count; only `expired` did — and it is corrected in place.
28
+
29
+ Every validation pass now sees a duplicated contract once, so one root cause produces one finding
30
+ rather than one per copy. `deriveAuthorizationGrants` no longer emits a reference for an intent whose
31
+ action the contract does not declare, matching what promotion resolves. A superseded comment above
32
+ `PORT_ID` that asserted the claim the fix retracted is removed.
33
+
34
+ ### Patch Changes
35
+
36
+ - Updated dependencies [8436c64]
37
+ - @fabricorg/sdui-release@0.7.0
38
+
3
39
  ## 0.3.0
4
40
 
5
41
  ### Minor Changes
@@ -7,11 +43,14 @@
7
43
  - 4780974: Close what a sixth review found in the previously unreviewed fix batch, and disclose a breaking change
8
44
  that batch shipped under a minor.
9
45
 
10
- **Disclosure, first.** `experience-runtime@0.2.0` changed `OfflineReplaySummary.parked` and
11
- `expired` from counts to arrays of records. That is breaking for any consumer of 0.1.0 — `parked > 0`
12
- becomes permanently false and `if (expired)` permanently true — and its changelog described the
13
- counts as still present. This release adds `storeFailures` to the same summary; treat the shape as
14
- having changed at 0.2.0.
46
+ **Disclosure, first.** `experience-runtime@0.2.0` changed `OfflineReplaySummary.expired` from a
47
+ count to an array of records — breaking for a 0.1.0 consumer, for whom `if (summary.expired)` becomes
48
+ permanently true. (`parked` was introduced and reshaped inside the 0.2.0 window and never shipped as
49
+ a count, so no published consumer could have depended on it.) The 0.2.0 changelog did disclose the
50
+ change, but one of its four aggregated entries still described counts. This release adds
51
+ `storeFailures`, a **required** field on the returned summary: source-breaking for anything that
52
+ constructs an `OfflineReplaySummary` or implements the exported `OfflineCaptureQueue`, which a test
53
+ double may. Treat the shape as changed.
15
54
 
16
55
  **Offline replay.** An authority refusal on a _recovery_ re-execution lands after adapter steps
17
56
  already committed, since the lease worker re-runs authorization but skips succeeded steps rather
@@ -1 +1 @@
1
- {"version":3,"sources":["../contracts.ts"],"sourcesContent":["export interface ExperienceScope {\n\ttenantId: string;\n\tspaceId: string;\n\tactorId: string;\n\tactorType: string;\n}\n\nexport interface RenderPlanFragment {\n\tid: string;\n\tcomponent?: string;\n\tprops?: Record<string, unknown>;\n\tslot?: string;\n\tevents?: readonly string[];\n\tofflineEvents?: readonly string[];\n\tbindings?: readonly string[];\n\tchildren?: readonly RenderPlanFragment[];\n}\n\nexport interface ScopedRenderPlan {\n\tformatVersion: 1;\n\tplanId: string;\n\tapplication: string;\n\tchannel: string;\n\treleaseDigest: string;\n\tassemblyDigest: string;\n\ttreeId: string;\n\troot: RenderPlanFragment;\n\ttokenSet: { name: string; version: string };\n\texpiresAt: string;\n}\n\nexport interface ExperienceIntentRequest {\n\tplanId: string;\n\tfragmentId: string;\n\teventName: string;\n\tparameters: Record<string, unknown>;\n\tidempotencyKey?: string;\n}\n\n/**\n * A replay of an action captured while disconnected.\n *\n * The marker and the capture moment travel with the request rather than by\n * adapter convention. An adapter that forwarded a replay as an ordinary intent\n * would skip the Host's offline eligibility and reconciliation rules, and\n * capture-authority evidence stamped at receipt cannot truthfully attest when\n * the operator actually acted.\n */\nexport interface ExperienceReplayRequest extends ExperienceIntentRequest {\n\texecutionReason: \"offline_replay\";\n\t/** RFC 3339 instant the action was captured on the device. */\n\tcapturedAt: string;\n\tidempotencyKey: string;\n}\n\nexport interface ExperienceReadRequest {\n\tplanId: string;\n\tfragmentId: string;\n\tbindingName: string;\n\tparameters?: Record<string, unknown>;\n}\n\nexport interface ExperienceGatewayClient {\n\tinvoke(request: ExperienceIntentRequest): Promise<unknown>;\n\treplay?(request: ExperienceReplayRequest): Promise<ExperienceInvocationOutcome>;\n\tread(request: ExperienceReadRequest): Promise<{ data: unknown; stale?: boolean }>;\n}\n\n/**\n * What a governed submission comes back as.\n *\n * The Host reports a refusal by *returning* `reconciliation_required` with the\n * reason, not by throwing: a lapsed authority is a business outcome rather than\n * an error. Anything reading only the exception path will count a refusal as a\n * success.\n */\nexport interface ExperienceInvocationOutcome {\n\tstatus?: string;\n\treconciliation?: { kind?: string; message?: string };\n\t/** Present when an adapter's own outcome could not be determined. */\n\tadapterReconciliation?: unknown;\n\t[key: string]: unknown;\n}\n\nexport interface ExperienceSession {\n\treadonly plan: ScopedRenderPlan;\n\tinvoke(fragmentId: string, eventName: string, parameters: Record<string, unknown>, idempotencyKey?: string): Promise<unknown>;\n\tread(fragmentId: string, bindingName: string, parameters?: Record<string, unknown>): Promise<{ data: unknown; stale?: boolean }>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
1
+ {"version":3,"sources":["../contracts.ts"],"sourcesContent":["export interface ExperienceScope {\n\ttenantId: string;\n\tspaceId: string;\n\tactorId: string;\n\tactorType: string;\n}\n\nexport interface RenderPlanFragment {\n\tid: string;\n\tcomponent?: string;\n\tprops?: Record<string, unknown>;\n\tslot?: string;\n\tevents?: readonly string[];\n\tofflineEvents?: readonly string[];\n\tbindings?: readonly string[];\n\tchildren?: readonly RenderPlanFragment[];\n}\n\nexport interface ScopedRenderPlan {\n\tformatVersion: 1;\n\tplanId: string;\n\tapplication: string;\n\tchannel: string;\n\treleaseDigest: string;\n\tassemblyDigest: string;\n\ttreeId: string;\n\troot: RenderPlanFragment;\n\ttokenSet: { name: string; version: string };\n\texpiresAt: string;\n}\n\nexport interface ExperienceIntentRequest {\n\tplanId: string;\n\tfragmentId: string;\n\teventName: string;\n\tparameters: Record<string, unknown>;\n\tidempotencyKey?: string;\n}\n\n/**\n * A replay of an action captured while disconnected.\n *\n * The marker and the capture moment travel with the request rather than by\n * adapter convention. An adapter that forwarded a replay as an ordinary intent\n * would skip the Host's offline eligibility and reconciliation rules, and\n * capture-authority evidence stamped at receipt cannot truthfully attest when\n * the operator actually acted.\n */\nexport interface ExperienceReplayRequest extends ExperienceIntentRequest {\n\texecutionReason: \"offline_replay\";\n\t/** RFC 3339 instant the action was captured on the device. */\n\tcapturedAt: string;\n\tidempotencyKey: string;\n}\n\nexport interface ExperienceReadRequest {\n\tplanId: string;\n\tfragmentId: string;\n\tbindingName: string;\n\tparameters?: Record<string, unknown>;\n}\n\nexport interface ExperienceGatewayClient {\n\tinvoke(request: ExperienceIntentRequest): Promise<unknown>;\n\treplay?(request: ExperienceReplayRequest): Promise<ExperienceInvocationOutcome>;\n\tread(request: ExperienceReadRequest): Promise<{ data: unknown; stale?: boolean }>;\n}\n\n/**\n * What a governed submission comes back as.\n *\n * The Host reports a refusal by *returning* `reconciliation_required` with the\n * reason, not by throwing: a lapsed authority is a business outcome rather than\n * an error. Anything reading only the exception path will count a refusal as a\n * success.\n */\nexport interface ExperienceInvocationOutcome {\n\tstatus?: string;\n\t/** `executionReason` names the attempt that produced this; `\"recovery\"` means effects may already have landed. */\n\treconciliation?: { kind?: string; message?: string; executionReason?: string };\n\t/** Present when an adapter's own outcome could not be determined. */\n\tadapterReconciliation?: unknown;\n\t[key: string]: unknown;\n}\n\nexport interface ExperienceSession {\n\treadonly plan: ScopedRenderPlan;\n\tinvoke(fragmentId: string, eventName: string, parameters: Record<string, unknown>, idempotencyKey?: string): Promise<unknown>;\n\tread(fragmentId: string, bindingName: string, parameters?: Record<string, unknown>): Promise<{ data: unknown; stale?: boolean }>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
@@ -75,9 +75,11 @@ interface ExperienceGatewayClient {
75
75
  */
76
76
  interface ExperienceInvocationOutcome {
77
77
  status?: string;
78
+ /** `executionReason` names the attempt that produced this; `"recovery"` means effects may already have landed. */
78
79
  reconciliation?: {
79
80
  kind?: string;
80
81
  message?: string;
82
+ executionReason?: string;
81
83
  };
82
84
  /** Present when an adapter's own outcome could not be determined. */
83
85
  adapterReconciliation?: unknown;
@@ -75,9 +75,11 @@ interface ExperienceGatewayClient {
75
75
  */
76
76
  interface ExperienceInvocationOutcome {
77
77
  status?: string;
78
+ /** `executionReason` names the attempt that produced this; `"recovery"` means effects may already have landed. */
78
79
  reconciliation?: {
79
80
  kind?: string;
80
81
  message?: string;
82
+ executionReason?: string;
81
83
  };
82
84
  /** Present when an adapter's own outcome could not be determined. */
83
85
  adapterReconciliation?: unknown;
package/dist/offline.cjs CHANGED
@@ -85,12 +85,18 @@ function createOfflineCaptureQueue(options) {
85
85
  try {
86
86
  outcome = await options.gateway.replay(request);
87
87
  } catch (error) {
88
- if (isSettledReplayFailure(error)) {
89
- await options.store.remove(record.captureId);
90
- reconciled.push({ record, status: "not_delivered", certainty: "refused", reason: messageOf(error) });
91
- } else {
88
+ const thrown = classifyThrownReplayFailure(error);
89
+ if (thrown === "transient") {
92
90
  failed += 1;
91
+ return;
93
92
  }
93
+ await options.store.remove(record.captureId);
94
+ reconciled.push({
95
+ record,
96
+ status: thrown === "settled-unknown" ? "conflict_prior_invocation" : "not_delivered",
97
+ certainty: thrown === "settled-unknown" ? "unknown" : "refused",
98
+ reason: messageOf(error)
99
+ });
94
100
  return;
95
101
  }
96
102
  const disposition = dispositionOf(outcome);
@@ -118,7 +124,6 @@ function createOfflineCaptureQueue(options) {
118
124
  try {
119
125
  await replayOne(record);
120
126
  } catch (error) {
121
- failed += 1;
122
127
  storeFailures.push({ record, reason: messageOf(error) });
123
128
  }
124
129
  }
@@ -199,24 +204,29 @@ function dispositionOf(outcome) {
199
204
  }
200
205
  case "blocked_by_policy":
201
206
  case "validation_failed":
202
- return { kind: "attention", status, certainty: "refused", reason };
207
+ return { kind: "attention", status, certainty: "unknown", reason };
203
208
  case "failed":
204
209
  return { kind: "attention", status, certainty: "unknown", reason };
205
210
  default:
206
211
  return { kind: "attention", status: status || "unknown", certainty: "unknown", reason };
207
212
  }
208
213
  }
214
+ function classifyThrownReplayFailure(error) {
215
+ const message = messageOf(error);
216
+ const conflict = /conflicts with invocation [^:]+:\s*(.+)$/i.exec(message);
217
+ if (conflict) {
218
+ const reasons = new Set(conflict[1].split(",").map((reason) => reason.trim()));
219
+ const neverApplied = reasons.has("parameters") || reasons.has("actor");
220
+ return neverApplied ? "settled-refused" : "settled-unknown";
221
+ }
222
+ return isSettledReplayFailure(error) ? "settled-refused" : "transient";
223
+ }
209
224
  function isSettledReplayFailure(error) {
210
225
  return [
211
226
  // The action was never offline-eligible.
212
227
  /not eligible for offline replay/i,
213
228
  // The plan no longer grants, no longer exists, or was never this actor's.
214
229
  /is not granted by the render plan/i,
215
- // An idempotency conflict is permanent: the key was reused for a
216
- // materially different command, and replaying it again yields the same
217
- // conflict forever. It must reach a person, who has the operator's work
218
- // in the record and can resubmit it under a fresh key.
219
- /conflicts with invocation/i,
220
230
  /render plan was not found/i,
221
231
  /render plan has expired/i,
222
232
  /scope does not match/i,
@@ -1 +1 @@
1
- {"version":3,"sources":["../offline.ts"],"sourcesContent":["import type {\n\tExperienceGatewayClient,\n\tExperienceInvocationOutcome,\n\tExperienceReplayRequest,\n\tRenderPlanFragment,\n\tScopedRenderPlan,\n} from \"./contracts\";\n\nexport interface OfflineCaptureRecord {\n\tcaptureId: string;\n\tplanId: string;\n\treleaseDigest: string;\n\tfragmentId: string;\n\teventName: string;\n\tparameters: Record<string, unknown>;\n\tidempotencyKey: string;\n\tcapturedAt: string;\n\texpiresAt: string;\n}\n\nexport interface OfflineCaptureStore {\n\tprotection: \"encrypted-at-rest\" | \"ephemeral\";\n\tput(record: OfflineCaptureRecord): Promise<void>;\n\tlist(): Promise<readonly OfflineCaptureRecord[]>;\n\tremove(captureId: string): Promise<void>;\n}\n\n/** A delivered capture whose outcome a person needs to see. */\nexport interface OfflineReconciliation {\n\trecord: OfflineCaptureRecord;\n\t/** The status the platform returned, so a caller can tell these apart. */\n\tstatus: string;\n\t/**\n\t * The invocation the platform recorded, when it got that far.\n\t *\n\t * `recordExternalReconciliation` needs it, so an outcome of `unknown` that\n\t * omits it cannot be reconciled from this summary — which is the whole\n\t * reason the entry exists.\n\t */\n\tactionInvocationId?: string;\n\t/**\n\t * Whether the effect is known not to have happened, or is simply unknown.\n\t * An adapter that timed out mid-charge is `unknown`: telling an operator it\n\t * was refused would be as wrong as telling them it succeeded.\n\t */\n\tcertainty: \"refused\" | \"unknown\";\n\treason: string;\n}\n\n/** A delivered capture waiting on a human. */\nexport interface OfflineParked {\n\trecord: OfflineCaptureRecord;\n\tactionInvocationId?: string;\n}\n\nexport interface OfflineReplaySummary {\n\t/** Delivered and progressing: the platform has the invocation. */\n\tcompleted: number;\n\t/**\n\t * Delivered and parked awaiting a human decision.\n\t *\n\t * Carries the records, because a bare count leaves nobody able to say which\n\t * action somebody now has to approve.\n\t */\n\tparked: OfflineParked[];\n\t/** Never delivered, and worth another attempt. Still queued. */\n\tfailed: number;\n\t/**\n\t * Outlived the capture TTL or the render plan before it could be sent.\n\t *\n\t * Also carries its records: a capture that expired never reached the\n\t * platform, so this is work the operator did that nobody has.\n\t */\n\texpired: OfflineCaptureRecord[];\n\t/**\n\t * Delivered, and something a person needs to see: refused, invalid, or an\n\t * outcome nobody can determine. Also covers a delivery that can never\n\t * succeed, since retrying one only delays the moment somebody is told.\n\t */\n\treconciled: OfflineReconciliation[];\n\t/**\n\t * Records the store itself failed on, mid-replay. They are still in the\n\t * store — nothing was removed on this path — and counted in `failed`; this\n\t * names them so a persistent store fault is diagnosable from the summary.\n\t */\n\tstoreFailures: OfflineStoreFailure[];\n}\n\n/** A record the capture store could not act on. */\nexport interface OfflineStoreFailure {\n\trecord: OfflineCaptureRecord;\n\treason: string;\n}\n\nexport interface OfflineCaptureQueue {\n\tcapture(fragmentId: string, eventName: string, parameters: Record<string, unknown>, idempotencyKey: string): Promise<OfflineCaptureRecord>;\n\treplay(): Promise<OfflineReplaySummary>;\n}\n\nexport function createOfflineCaptureQueue(options: {\n\tplan: ScopedRenderPlan;\n\tstore: OfflineCaptureStore;\n\tgateway: ExperienceGatewayClient;\n\tnextCaptureId(): string;\n\tnow?: () => Date;\n\tcaptureTtlMs?: number;\n\tallowEphemeralStore?: boolean;\n}): OfflineCaptureQueue {\n\tif (options.store.protection !== \"encrypted-at-rest\" && !options.allowEphemeralStore) {\n\t\tthrow new Error(\"Production offline capture requires an encrypted-at-rest store.\");\n\t}\n\tif (!options.gateway.replay) throw new Error(\"Offline capture requires a gateway replay adapter.\");\n\tconst fragments = indexFragments(options.plan.root);\n\tconst now = options.now ?? (() => new Date());\n\treturn {\n\t\tasync capture(fragmentId, eventName, parameters, idempotencyKey) {\n\t\t\tconst fragment = fragments.get(fragmentId);\n\t\t\tif (!fragment?.offlineEvents?.includes(eventName)) {\n\t\t\t\tthrow new Error(`Fragment event \"${fragmentId}.${eventName}\" is not offline-capturable.`);\n\t\t\t}\n\t\t\tif (!idempotencyKey || idempotencyKey.length > 256) throw new Error(\"Offline capture requires a bounded stable idempotency key.\");\n\t\t\tconst capturedAt = now();\n\t\t\tconst captureExpiry = capturedAt.getTime() + (options.captureTtlMs ?? 86_400_000);\n\t\t\tconst planExpiry = Date.parse(options.plan.expiresAt);\n\t\t\tif (!Number.isFinite(planExpiry) || planExpiry <= capturedAt.getTime()) throw new Error(\"Offline capture requires an unexpired render plan.\");\n\t\t\tconst safeParameters = cloneOfflineParameters(parameters);\n\t\t\tconst captureId = options.nextCaptureId();\n\t\t\tif (!captureId.trim()) throw new Error(\"Offline capture id is required.\");\n\t\t\tconst record: OfflineCaptureRecord = {\n\t\t\t\tcaptureId,\n\t\t\t\tplanId: options.plan.planId,\n\t\t\t\treleaseDigest: options.plan.releaseDigest,\n\t\t\t\tfragmentId,\n\t\t\t\teventName,\n\t\t\t\tparameters: safeParameters,\n\t\t\t\tidempotencyKey,\n\t\t\t\tcapturedAt: capturedAt.toISOString(),\n\t\t\t\texpiresAt: new Date(Math.min(captureExpiry, planExpiry)).toISOString(),\n\t\t\t};\n\t\t\tawait options.store.put(record);\n\t\t\treturn record;\n\t\t},\n\t\tasync replay() {\n\t\t\tlet completed = 0;\n\t\t\tlet failed = 0;\n\t\t\tconst parked: OfflineParked[] = [];\n\t\t\tconst expired: OfflineCaptureRecord[] = [];\n\t\t\tconst reconciled: OfflineReconciliation[] = [];\n\t\t\tconst storeFailures: OfflineStoreFailure[] = [];\n\n\t\t\tconst replayOne = async (record: OfflineCaptureRecord): Promise<void> => {\n\t\t\t\tif (Date.parse(record.expiresAt) <= now().getTime()) {\n\t\t\t\t\tawait options.store.remove(record.captureId);\n\t\t\t\t\texpired.push(record);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst request: ExperienceReplayRequest = {\n\t\t\t\t\tplanId: record.planId,\n\t\t\t\t\tfragmentId: record.fragmentId,\n\t\t\t\t\teventName: record.eventName,\n\t\t\t\t\tparameters: record.parameters,\n\t\t\t\t\tidempotencyKey: record.idempotencyKey,\n\t\t\t\t\texecutionReason: \"offline_replay\",\n\t\t\t\t\tcapturedAt: record.capturedAt,\n\t\t\t\t};\n\n\t\t\t\tlet outcome: ExperienceInvocationOutcome | undefined;\n\t\t\t\ttry {\n\t\t\t\t\toutcome = await options.gateway.replay!(request);\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Only genuine throws reach here. A settled one is never\n\t\t\t\t\t// deliverable and retrying it only delays the moment somebody\n\t\t\t\t\t// is told; anything unrecognised stays transient, so an\n\t\t\t\t\t// unfamiliar failure is retried rather than discarded.\n\t\t\t\t\tif (isSettledReplayFailure(error)) {\n\t\t\t\t\t\tawait options.store.remove(record.captureId);\n\t\t\t\t\t\treconciled.push({ record, status: \"not_delivered\", certainty: \"refused\", reason: messageOf(error) });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfailed += 1;\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// The disposition is decided before the record is removed, because\n\t\t\t\t// an adapter that said nothing has not delivered and the capture\n\t\t\t\t// must stay queued. Anything else returned means the platform\n\t\t\t\t// durably holds the invocation, so the capture has done its job\n\t\t\t\t// and leaves the queue; what differs is whether a person needs to\n\t\t\t\t// hear about it.\n\t\t\t\tconst disposition = dispositionOf(outcome);\n\t\t\t\tif (disposition.kind === \"not-delivered\") {\n\t\t\t\t\tfailed += 1;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tawait options.store.remove(record.captureId);\n\t\t\t\tconst actionInvocationId = typeof outcome?.actionInvocationId === \"string\"\n\t\t\t\t\t? outcome.actionInvocationId\n\t\t\t\t\t: undefined;\n\t\t\t\tif (disposition.kind === \"attention\") {\n\t\t\t\t\treconciled.push({\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tstatus: disposition.status,\n\t\t\t\t\t\t...(actionInvocationId === undefined ? {} : { actionInvocationId }),\n\t\t\t\t\t\tcertainty: disposition.certainty,\n\t\t\t\t\t\treason: disposition.reason,\n\t\t\t\t\t});\n\t\t\t\t} else if (disposition.kind === \"parked\") {\n\t\t\t\t\tparked.push({ record, ...(actionInvocationId === undefined ? {} : { actionInvocationId }) });\n\t\t\t\t} else {\n\t\t\t\t\tcompleted += 1;\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tfor (const record of await options.store.list()) {\n\t\t\t\t// Every store call sits inside this guard. A store that throws on\n\t\t\t\t// remove — an IndexedDB abort is not exotic — would otherwise\n\t\t\t\t// reject replay() after earlier records were already deleted, and\n\t\t\t\t// their summary entries, now the only place those records live,\n\t\t\t\t// would go with it. One failing record degrades one record.\n\t\t\t\ttry {\n\t\t\t\t\tawait replayOne(record);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tfailed += 1;\n\t\t\t\t\tstoreFailures.push({ record, reason: messageOf(error) });\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { completed, parked, failed, expired, reconciled, storeFailures };\n\t\t},\n\t};\n}\n\nfunction cloneOfflineParameters(parameters: Record<string, unknown>): Record<string, unknown> {\n\tlet nodes = 0;\n\tconst visit = (value: unknown, depth: number, key?: string): void => {\n\t\tnodes += 1;\n\t\tif (nodes > 10_000 || depth > 20) throw new Error(\"Offline capture parameters exceed structural limits.\");\n\t\tif (key !== undefined && /(?:authorization|bearer|cookie|credential|password|secret|token|claims?)/i.test(key)) {\n\t\t\tthrow new Error(`Offline capture parameters may not contain credential or claim field \"${key}\".`);\n\t\t}\n\t\tif (value === null || typeof value === \"string\" || typeof value === \"boolean\") return;\n\t\tif (typeof value === \"number\" && Number.isFinite(value)) return;\n\t\tif (Array.isArray(value)) {\n\t\t\tvalue.forEach((entry) => visit(entry, depth + 1));\n\t\t\treturn;\n\t\t}\n\t\tif (value && typeof value === \"object\" && Object.getPrototypeOf(value) === Object.prototype) {\n\t\t\tfor (const [childKey, child] of Object.entries(value)) visit(child, depth + 1, childKey);\n\t\t\treturn;\n\t\t}\n\t\tthrow new Error(\"Offline capture parameters must contain only portable JSON values.\");\n\t};\n\tvisit(parameters, 0);\n\tconst serialized = JSON.stringify(parameters);\n\tif (new TextEncoder().encode(serialized).byteLength > 65_536) throw new Error(\"Offline capture parameters exceed the 65536-byte limit.\");\n\treturn structuredClone(parameters);\n}\n\nfunction indexFragments(root: RenderPlanFragment): Map<string, RenderPlanFragment> {\n\tconst result = new Map<string, RenderPlanFragment>();\n\tconst visit = (fragment: RenderPlanFragment): void => {\n\t\tresult.set(fragment.id, fragment);\n\t\tfor (const child of fragment.children ?? []) visit(child);\n\t};\n\tvisit(root);\n\treturn result;\n}\n\nexport function createMemoryOfflineCaptureStore(): OfflineCaptureStore {\n\tconst records = new Map<string, OfflineCaptureRecord>();\n\treturn {\n\t\tprotection: \"ephemeral\",\n\t\tasync put(record) {\n\t\t\tif (records.has(record.captureId)) throw new Error(`Offline capture \"${record.captureId}\" already exists.`);\n\t\t\trecords.set(record.captureId, structuredClone(record));\n\t\t},\n\t\tasync list() {\n\t\t\treturn [...records.values()].map((record) => structuredClone(record));\n\t\t},\n\t\tasync remove(captureId) {\n\t\t\trecords.delete(captureId);\n\t\t},\n\t};\n}\n\nfunction messageOf(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\ntype ReplayDisposition =\n\t| { kind: \"delivered\" }\n\t| { kind: \"not-delivered\" }\n\t| { kind: \"parked\" }\n\t| { kind: \"attention\"; status: string; certainty: \"refused\" | \"unknown\"; reason: string };\n\n/**\n * Read a returned outcome across the platform's whole status set.\n *\n * Enumerating the statuses that looked like refusals left the rest falling\n * through to \"applied\", which is how a validation failure got reported to an\n * operator as success. The switch covers every `ActionStatus`, and the default\n * arm surfaces anything else rather than counting it a success — the status\n * arrives as a string over a transport, so this is a runtime guarantee and not\n * a compile-time one.\n *\n * There is no \"already applied\" branch. A key that legitimately repeats returns\n * the original invocation without complaint, and a genuine idempotency conflict\n * means the key was reused for a materially different command, so that capture\n * was never applied and must not be discarded.\n */\nfunction dispositionOf(outcome: ExperienceInvocationOutcome | undefined): ReplayDisposition {\n\t// An adapter that returns nothing has told us nothing. Reading that as\n\t// success is the failure this whole switch exists to prevent — but so is\n\t// draining a whole queue into \"unknown\" because one adapter forgot its\n\t// return statement. Nothing was delivered as far as we can tell, so the\n\t// capture stays queued and the adapter fault surfaces as a transient.\n\tif (!outcome || typeof outcome !== \"object\") {\n\t\treturn { kind: \"not-delivered\" };\n\t}\n\tconst status = typeof outcome.status === \"string\" ? outcome.status : \"\";\n\tconst reason = outcome.reconciliation?.message\n\t\t?? outcome.reconciliation?.kind\n\t\t?? (typeof outcome.error === \"string\" ? outcome.error : status);\n\n\tswitch (status) {\n\t\tcase \"pending\":\n\t\tcase \"running\":\n\t\tcase \"completed\":\n\t\t\treturn { kind: \"delivered\" };\n\t\tcase \"waiting_for_approval\":\n\t\t\treturn { kind: \"parked\" };\n\t\tcase \"reconciliation_required\": {\n\t\t\t// Two things arrive here that are not refusals. An adapter whose\n\t\t\t// outcome could not be determined, and an authority check that\n\t\t\t// failed on a *recovery* re-execution: the lease worker re-runs the\n\t\t\t// whole authorization block, but already-succeeded adapter steps are\n\t\t\t// skipped rather than undone, so the external effect may have landed\n\t\t\t// before the binding expired. The persisted reconciliation says\n\t\t\t// which execution it was.\n\t\t\tconst recovery = (outcome.reconciliation as { executionReason?: string } | undefined)?.executionReason === \"recovery\";\n\t\t\tconst unknown = outcome.adapterReconciliation !== undefined || recovery;\n\t\t\treturn { kind: \"attention\", status, certainty: unknown ? \"unknown\" : \"refused\", reason };\n\t\t}\n\t\tcase \"blocked_by_policy\":\n\t\tcase \"validation_failed\":\n\t\t\t// Both are decided before any effect: policy refuses at the gate and\n\t\t\t// validation refuses the parameters.\n\t\t\treturn { kind: \"attention\", status, certainty: \"refused\", reason };\n\t\tcase \"failed\":\n\t\t\t// Not the same. A handler or adapter step can fail after earlier\n\t\t\t// steps succeeded and the domain transaction committed, so telling an\n\t\t\t// operator this was refused could be as wrong as calling it applied.\n\t\t\treturn { kind: \"attention\", status, certainty: \"unknown\", reason };\n\t\tdefault:\n\t\t\t// An unfamiliar status is surfaced rather than counted as success.\n\t\t\treturn { kind: \"attention\", status: status || \"unknown\", certainty: \"unknown\", reason };\n\t}\n}\n\n/**\n * A thrown failure the platform will never accept on a later attempt.\n *\n * Deliberately narrow, and matched on what the gateway and runtime actually\n * throw. Anything else is transient, because keeping a capture costs a retry\n * and discarding one costs the operator their work.\n */\nfunction isSettledReplayFailure(error: unknown): boolean {\n\treturn [\n\t\t// The action was never offline-eligible.\n\t\t/not eligible for offline replay/i,\n\t\t// The plan no longer grants, no longer exists, or was never this actor's.\n\t\t/is not granted by the render plan/i,\n\t\t// An idempotency conflict is permanent: the key was reused for a\n\t\t// materially different command, and replaying it again yields the same\n\t\t// conflict forever. It must reach a person, who has the operator's work\n\t\t// in the record and can resubmit it under a fresh key.\n\t\t/conflicts with invocation/i,\n\t\t/render plan was not found/i,\n\t\t/render plan has expired/i,\n\t\t/scope does not match/i,\n\t\t/belongs to a different application assembly/i,\n\t\t// A capture only gets older, so an age refusal can never come good. This\n\t\t// one is independent of the capture TTL, so nothing else retires it.\n\t\t/capturedAt is older than/i,\n\t\t// Malformed beyond repair: passes the grammar, names no real instant.\n\t\t/capturedAt is not a parseable instant/i,\n\t\t/capturedAt must be an RFC 3339/i,\n\t\t// Deliberately absent: a capture dated in the future comes good as real\n\t\t// time advances; a gateway missing its replay window is a deployment\n\t\t// mistake somebody will fix; and an action-version mismatch fires\n\t\t// transiently mid-rollout when a client's plan is newer than the pod it\n\t\t// reached. Discarding any of them would destroy work a clock correction,\n\t\t// a config change, or the rollout finishing would have replayed.\n\t].some((pattern) => pattern.test(messageOf(error)));\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../offline.ts"],"sourcesContent":["import type {\n\tExperienceGatewayClient,\n\tExperienceInvocationOutcome,\n\tExperienceReplayRequest,\n\tRenderPlanFragment,\n\tScopedRenderPlan,\n} from \"./contracts\";\n\nexport interface OfflineCaptureRecord {\n\tcaptureId: string;\n\tplanId: string;\n\treleaseDigest: string;\n\tfragmentId: string;\n\teventName: string;\n\tparameters: Record<string, unknown>;\n\tidempotencyKey: string;\n\tcapturedAt: string;\n\texpiresAt: string;\n}\n\nexport interface OfflineCaptureStore {\n\tprotection: \"encrypted-at-rest\" | \"ephemeral\";\n\tput(record: OfflineCaptureRecord): Promise<void>;\n\tlist(): Promise<readonly OfflineCaptureRecord[]>;\n\tremove(captureId: string): Promise<void>;\n}\n\n/** A delivered capture whose outcome a person needs to see. */\nexport interface OfflineReconciliation {\n\trecord: OfflineCaptureRecord;\n\t/** The status the platform returned, so a caller can tell these apart. */\n\tstatus: string;\n\t/**\n\t * The invocation the platform recorded, when it got that far.\n\t *\n\t * `recordExternalReconciliation` needs it, so an outcome of `unknown` that\n\t * omits it cannot be reconciled from this summary — which is the whole\n\t * reason the entry exists.\n\t */\n\tactionInvocationId?: string;\n\t/**\n\t * Whether the effect is known not to have happened, or is simply unknown.\n\t * An adapter that timed out mid-charge is `unknown`: telling an operator it\n\t * was refused would be as wrong as telling them it succeeded.\n\t */\n\tcertainty: \"refused\" | \"unknown\";\n\treason: string;\n}\n\n/** A delivered capture waiting on a human. */\nexport interface OfflineParked {\n\trecord: OfflineCaptureRecord;\n\tactionInvocationId?: string;\n}\n\nexport interface OfflineReplaySummary {\n\t/** Delivered and progressing: the platform has the invocation. */\n\tcompleted: number;\n\t/**\n\t * Delivered and parked awaiting a human decision.\n\t *\n\t * Carries the records, because a bare count leaves nobody able to say which\n\t * action somebody now has to approve.\n\t */\n\tparked: OfflineParked[];\n\t/** Never delivered, and worth another attempt. Still queued. */\n\tfailed: number;\n\t/**\n\t * Outlived the capture TTL or the render plan before it could be sent.\n\t *\n\t * Also carries its records: a capture that expired never reached the\n\t * platform, so this is work the operator did that nobody has.\n\t */\n\texpired: OfflineCaptureRecord[];\n\t/**\n\t * Delivered, and something a person needs to see: refused, invalid, or an\n\t * outcome nobody can determine. Also covers a delivery that can never\n\t * succeed, since retrying one only delays the moment somebody is told.\n\t */\n\treconciled: OfflineReconciliation[];\n\t/**\n\t * Records the store itself failed on, mid-replay. Not counted in `failed`,\n\t * because the delivery may have succeeded before the store threw — the\n\t * record is still in the store either way, so the next replay resubmits\n\t * under the same idempotency key and the platform returns the original.\n\t */\n\tstoreFailures: OfflineStoreFailure[];\n}\n\n/** A record the capture store could not act on. */\nexport interface OfflineStoreFailure {\n\trecord: OfflineCaptureRecord;\n\treason: string;\n}\n\nexport interface OfflineCaptureQueue {\n\tcapture(fragmentId: string, eventName: string, parameters: Record<string, unknown>, idempotencyKey: string): Promise<OfflineCaptureRecord>;\n\treplay(): Promise<OfflineReplaySummary>;\n}\n\nexport function createOfflineCaptureQueue(options: {\n\tplan: ScopedRenderPlan;\n\tstore: OfflineCaptureStore;\n\tgateway: ExperienceGatewayClient;\n\tnextCaptureId(): string;\n\tnow?: () => Date;\n\tcaptureTtlMs?: number;\n\tallowEphemeralStore?: boolean;\n}): OfflineCaptureQueue {\n\tif (options.store.protection !== \"encrypted-at-rest\" && !options.allowEphemeralStore) {\n\t\tthrow new Error(\"Production offline capture requires an encrypted-at-rest store.\");\n\t}\n\tif (!options.gateway.replay) throw new Error(\"Offline capture requires a gateway replay adapter.\");\n\tconst fragments = indexFragments(options.plan.root);\n\tconst now = options.now ?? (() => new Date());\n\treturn {\n\t\tasync capture(fragmentId, eventName, parameters, idempotencyKey) {\n\t\t\tconst fragment = fragments.get(fragmentId);\n\t\t\tif (!fragment?.offlineEvents?.includes(eventName)) {\n\t\t\t\tthrow new Error(`Fragment event \"${fragmentId}.${eventName}\" is not offline-capturable.`);\n\t\t\t}\n\t\t\tif (!idempotencyKey || idempotencyKey.length > 256) throw new Error(\"Offline capture requires a bounded stable idempotency key.\");\n\t\t\tconst capturedAt = now();\n\t\t\tconst captureExpiry = capturedAt.getTime() + (options.captureTtlMs ?? 86_400_000);\n\t\t\tconst planExpiry = Date.parse(options.plan.expiresAt);\n\t\t\tif (!Number.isFinite(planExpiry) || planExpiry <= capturedAt.getTime()) throw new Error(\"Offline capture requires an unexpired render plan.\");\n\t\t\tconst safeParameters = cloneOfflineParameters(parameters);\n\t\t\tconst captureId = options.nextCaptureId();\n\t\t\tif (!captureId.trim()) throw new Error(\"Offline capture id is required.\");\n\t\t\tconst record: OfflineCaptureRecord = {\n\t\t\t\tcaptureId,\n\t\t\t\tplanId: options.plan.planId,\n\t\t\t\treleaseDigest: options.plan.releaseDigest,\n\t\t\t\tfragmentId,\n\t\t\t\teventName,\n\t\t\t\tparameters: safeParameters,\n\t\t\t\tidempotencyKey,\n\t\t\t\tcapturedAt: capturedAt.toISOString(),\n\t\t\t\texpiresAt: new Date(Math.min(captureExpiry, planExpiry)).toISOString(),\n\t\t\t};\n\t\t\tawait options.store.put(record);\n\t\t\treturn record;\n\t\t},\n\t\tasync replay() {\n\t\t\tlet completed = 0;\n\t\t\tlet failed = 0;\n\t\t\tconst parked: OfflineParked[] = [];\n\t\t\tconst expired: OfflineCaptureRecord[] = [];\n\t\t\tconst reconciled: OfflineReconciliation[] = [];\n\t\t\tconst storeFailures: OfflineStoreFailure[] = [];\n\n\t\t\tconst replayOne = async (record: OfflineCaptureRecord): Promise<void> => {\n\t\t\t\tif (Date.parse(record.expiresAt) <= now().getTime()) {\n\t\t\t\t\tawait options.store.remove(record.captureId);\n\t\t\t\t\texpired.push(record);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst request: ExperienceReplayRequest = {\n\t\t\t\t\tplanId: record.planId,\n\t\t\t\t\tfragmentId: record.fragmentId,\n\t\t\t\t\teventName: record.eventName,\n\t\t\t\t\tparameters: record.parameters,\n\t\t\t\t\tidempotencyKey: record.idempotencyKey,\n\t\t\t\t\texecutionReason: \"offline_replay\",\n\t\t\t\t\tcapturedAt: record.capturedAt,\n\t\t\t\t};\n\n\t\t\t\tlet outcome: ExperienceInvocationOutcome | undefined;\n\t\t\t\ttry {\n\t\t\t\t\toutcome = await options.gateway.replay!(request);\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Only genuine throws reach here. A settled one is never\n\t\t\t\t\t// deliverable on a later attempt and retrying only delays the\n\t\t\t\t\t// moment somebody is told; anything unrecognised stays\n\t\t\t\t\t// transient, so an unfamiliar failure is retried, not discarded.\n\t\t\t\t\tconst thrown = classifyThrownReplayFailure(error);\n\t\t\t\t\tif (thrown === \"transient\") {\n\t\t\t\t\t\tfailed += 1;\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tawait options.store.remove(record.captureId);\n\t\t\t\t\treconciled.push({\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tstatus: thrown === \"settled-unknown\" ? \"conflict_prior_invocation\" : \"not_delivered\",\n\t\t\t\t\t\tcertainty: thrown === \"settled-unknown\" ? \"unknown\" : \"refused\",\n\t\t\t\t\t\treason: messageOf(error),\n\t\t\t\t\t});\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// The disposition is decided before the record is removed, because\n\t\t\t\t// an adapter that said nothing has not delivered and the capture\n\t\t\t\t// must stay queued. Anything else returned means the platform\n\t\t\t\t// durably holds the invocation, so the capture has done its job\n\t\t\t\t// and leaves the queue; what differs is whether a person needs to\n\t\t\t\t// hear about it.\n\t\t\t\tconst disposition = dispositionOf(outcome);\n\t\t\t\tif (disposition.kind === \"not-delivered\") {\n\t\t\t\t\tfailed += 1;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tawait options.store.remove(record.captureId);\n\t\t\t\tconst actionInvocationId = typeof outcome?.actionInvocationId === \"string\"\n\t\t\t\t\t? outcome.actionInvocationId\n\t\t\t\t\t: undefined;\n\t\t\t\tif (disposition.kind === \"attention\") {\n\t\t\t\t\treconciled.push({\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tstatus: disposition.status,\n\t\t\t\t\t\t...(actionInvocationId === undefined ? {} : { actionInvocationId }),\n\t\t\t\t\t\tcertainty: disposition.certainty,\n\t\t\t\t\t\treason: disposition.reason,\n\t\t\t\t\t});\n\t\t\t\t} else if (disposition.kind === \"parked\") {\n\t\t\t\t\tparked.push({ record, ...(actionInvocationId === undefined ? {} : { actionInvocationId }) });\n\t\t\t\t} else {\n\t\t\t\t\tcompleted += 1;\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tfor (const record of await options.store.list()) {\n\t\t\t\t// Every store call sits inside this guard. A store that throws on\n\t\t\t\t// remove — an IndexedDB abort is not exotic — would otherwise\n\t\t\t\t// reject replay() after earlier records were already deleted, and\n\t\t\t\t// their summary entries, now the only place those records live,\n\t\t\t\t// would go with it. One failing record degrades one record.\n\t\t\t\ttry {\n\t\t\t\t\tawait replayOne(record);\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Not counted in `failed`: that bucket means \"never delivered\",\n\t\t\t\t\t// and a store fault after a successful delivery is neither\n\t\t\t\t\t// that nor a success. It has its own bucket so it is neither\n\t\t\t\t\t// hidden nor misdescribed.\n\t\t\t\t\tstoreFailures.push({ record, reason: messageOf(error) });\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { completed, parked, failed, expired, reconciled, storeFailures };\n\t\t},\n\t};\n}\n\nfunction cloneOfflineParameters(parameters: Record<string, unknown>): Record<string, unknown> {\n\tlet nodes = 0;\n\tconst visit = (value: unknown, depth: number, key?: string): void => {\n\t\tnodes += 1;\n\t\tif (nodes > 10_000 || depth > 20) throw new Error(\"Offline capture parameters exceed structural limits.\");\n\t\tif (key !== undefined && /(?:authorization|bearer|cookie|credential|password|secret|token|claims?)/i.test(key)) {\n\t\t\tthrow new Error(`Offline capture parameters may not contain credential or claim field \"${key}\".`);\n\t\t}\n\t\tif (value === null || typeof value === \"string\" || typeof value === \"boolean\") return;\n\t\tif (typeof value === \"number\" && Number.isFinite(value)) return;\n\t\tif (Array.isArray(value)) {\n\t\t\tvalue.forEach((entry) => visit(entry, depth + 1));\n\t\t\treturn;\n\t\t}\n\t\tif (value && typeof value === \"object\" && Object.getPrototypeOf(value) === Object.prototype) {\n\t\t\tfor (const [childKey, child] of Object.entries(value)) visit(child, depth + 1, childKey);\n\t\t\treturn;\n\t\t}\n\t\tthrow new Error(\"Offline capture parameters must contain only portable JSON values.\");\n\t};\n\tvisit(parameters, 0);\n\tconst serialized = JSON.stringify(parameters);\n\tif (new TextEncoder().encode(serialized).byteLength > 65_536) throw new Error(\"Offline capture parameters exceed the 65536-byte limit.\");\n\treturn structuredClone(parameters);\n}\n\nfunction indexFragments(root: RenderPlanFragment): Map<string, RenderPlanFragment> {\n\tconst result = new Map<string, RenderPlanFragment>();\n\tconst visit = (fragment: RenderPlanFragment): void => {\n\t\tresult.set(fragment.id, fragment);\n\t\tfor (const child of fragment.children ?? []) visit(child);\n\t};\n\tvisit(root);\n\treturn result;\n}\n\nexport function createMemoryOfflineCaptureStore(): OfflineCaptureStore {\n\tconst records = new Map<string, OfflineCaptureRecord>();\n\treturn {\n\t\tprotection: \"ephemeral\",\n\t\tasync put(record) {\n\t\t\tif (records.has(record.captureId)) throw new Error(`Offline capture \"${record.captureId}\" already exists.`);\n\t\t\trecords.set(record.captureId, structuredClone(record));\n\t\t},\n\t\tasync list() {\n\t\t\treturn [...records.values()].map((record) => structuredClone(record));\n\t\t},\n\t\tasync remove(captureId) {\n\t\t\trecords.delete(captureId);\n\t\t},\n\t};\n}\n\nfunction messageOf(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\ntype ReplayDisposition =\n\t| { kind: \"delivered\" }\n\t| { kind: \"not-delivered\" }\n\t| { kind: \"parked\" }\n\t| { kind: \"attention\"; status: string; certainty: \"refused\" | \"unknown\"; reason: string };\n\n/**\n * Read a returned outcome across the platform's whole status set.\n *\n * Enumerating the statuses that looked like refusals left the rest falling\n * through to \"applied\", which is how a validation failure got reported to an\n * operator as success. The switch covers every `ActionStatus`, and the default\n * arm surfaces anything else rather than counting it a success — the status\n * arrives as a string over a transport, so this is a runtime guarantee and not\n * a compile-time one.\n *\n * There is no \"already applied\" branch. A key that legitimately repeats returns\n * the original invocation without complaint, and a genuine idempotency conflict\n * means the key was reused for a materially different command, so that capture\n * was never applied and must not be discarded.\n */\nfunction dispositionOf(outcome: ExperienceInvocationOutcome | undefined): ReplayDisposition {\n\t// An adapter that returns nothing has told us nothing. Reading that as\n\t// success is the failure this whole switch exists to prevent — but so is\n\t// draining a whole queue into \"unknown\" because one adapter forgot its\n\t// return statement. Nothing was delivered as far as we can tell, so the\n\t// capture stays queued and the adapter fault surfaces as a transient.\n\tif (!outcome || typeof outcome !== \"object\") {\n\t\treturn { kind: \"not-delivered\" };\n\t}\n\tconst status = typeof outcome.status === \"string\" ? outcome.status : \"\";\n\tconst reason = outcome.reconciliation?.message\n\t\t?? outcome.reconciliation?.kind\n\t\t?? (typeof outcome.error === \"string\" ? outcome.error : status);\n\n\tswitch (status) {\n\t\tcase \"pending\":\n\t\tcase \"running\":\n\t\tcase \"completed\":\n\t\t\treturn { kind: \"delivered\" };\n\t\tcase \"waiting_for_approval\":\n\t\t\treturn { kind: \"parked\" };\n\t\tcase \"reconciliation_required\": {\n\t\t\t// Two things arrive here that are not refusals. An adapter whose\n\t\t\t// outcome could not be determined, and an authority check that\n\t\t\t// failed on a *recovery* re-execution: the lease worker re-runs the\n\t\t\t// whole authorization block, but already-succeeded adapter steps are\n\t\t\t// skipped rather than undone, so the external effect may have landed\n\t\t\t// before the binding expired. The persisted reconciliation says\n\t\t\t// which execution it was.\n\t\t\tconst recovery = outcome.reconciliation?.executionReason === \"recovery\";\n\t\t\tconst unknown = outcome.adapterReconciliation !== undefined || recovery;\n\t\t\treturn { kind: \"attention\", status, certainty: unknown ? \"unknown\" : \"refused\", reason };\n\t\t}\n\t\tcase \"blocked_by_policy\":\n\t\tcase \"validation_failed\":\n\t\t\t// Within one attempt these are decided before any effect. Across\n\t\t\t// attempts they are not: a recovery re-execution skips adapter steps\n\t\t\t// that already succeeded, then re-evaluates policy with a fresh clock\n\t\t\t// and re-parses parameters against a possibly newer action, and\n\t\t\t// either can flip after the external effect committed. Unlike the\n\t\t\t// authority path, fail() persists no execution reason, so the client\n\t\t\t// cannot tell first delivery from recovery. Erring toward \"a person\n\t\t\t// checks\" is the cheaper mistake; the precise fix is for the host to\n\t\t\t// persist executionReason on failed invocations.\n\t\t\treturn { kind: \"attention\", status, certainty: \"unknown\", reason };\n\t\tcase \"failed\":\n\t\t\t// Not the same. A handler or adapter step can fail after earlier\n\t\t\t// steps succeeded and the domain transaction committed, so telling an\n\t\t\t// operator this was refused could be as wrong as calling it applied.\n\t\t\treturn { kind: \"attention\", status, certainty: \"unknown\", reason };\n\t\tdefault:\n\t\t\t// An unfamiliar status is surfaced rather than counted as success.\n\t\t\treturn { kind: \"attention\", status: status || \"unknown\", certainty: \"unknown\", reason };\n\t}\n}\n\n/**\n * A thrown failure the platform will never accept on a later attempt.\n *\n * Deliberately narrow, and matched on what the gateway and runtime actually\n * throw. Anything else is transient, because keeping a capture costs a retry\n * and discarding one costs the operator their work.\n */\ntype ThrownReplayFailure = \"transient\" | \"settled-refused\" | \"settled-unknown\";\n\n/**\n * An idempotency conflict is settled — retrying yields the same conflict\n * forever — but which way depends on *why* the key conflicted. The host lists\n * the reasons after the colon. `parameters` or `actor` means the key was reused\n * for a materially different command, so this capture was never applied.\n * `authority_binding` or `action_version` means the *original* invocation\n * exists and may well have applied: the capture-authority adapter mints a fresh\n * binding on every attempt, so a first attempt whose response was lost\n * conflicts with itself on the second. Deleting that and calling it refused is\n * exactly the wrong answer, so it is surfaced as unknown instead.\n */\nfunction classifyThrownReplayFailure(error: unknown): ThrownReplayFailure {\n\tconst message = messageOf(error);\n\tconst conflict = /conflicts with invocation [^:]+:\\s*(.+)$/i.exec(message);\n\tif (conflict) {\n\t\tconst reasons = new Set(conflict[1]!.split(\",\").map((reason) => reason.trim()));\n\t\tconst neverApplied = reasons.has(\"parameters\") || reasons.has(\"actor\");\n\t\treturn neverApplied ? \"settled-refused\" : \"settled-unknown\";\n\t}\n\treturn isSettledReplayFailure(error) ? \"settled-refused\" : \"transient\";\n}\n\nfunction isSettledReplayFailure(error: unknown): boolean {\n\treturn [\n\t\t// The action was never offline-eligible.\n\t\t/not eligible for offline replay/i,\n\t\t// The plan no longer grants, no longer exists, or was never this actor's.\n\t\t/is not granted by the render plan/i,\n\t\t/render plan was not found/i,\n\t\t/render plan has expired/i,\n\t\t/scope does not match/i,\n\t\t/belongs to a different application assembly/i,\n\t\t// A capture only gets older, so an age refusal can never come good. This\n\t\t// one is independent of the capture TTL, so nothing else retires it.\n\t\t/capturedAt is older than/i,\n\t\t// Malformed beyond repair: passes the grammar, names no real instant.\n\t\t/capturedAt is not a parseable instant/i,\n\t\t/capturedAt must be an RFC 3339/i,\n\t\t// Deliberately absent: a capture dated in the future comes good as real\n\t\t// time advances; a gateway missing its replay window is a deployment\n\t\t// mistake somebody will fix; and an action-version mismatch fires\n\t\t// transiently mid-rollout when a client's plan is newer than the pod it\n\t\t// reached. Discarding any of them would destroy work a clock correction,\n\t\t// a config change, or the rollout finishing would have replayed.\n\t].some((pattern) => pattern.test(messageOf(error)));\n}\n"],"mappings":"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","names":[]}
@@ -69,9 +69,10 @@ interface OfflineReplaySummary {
69
69
  */
70
70
  reconciled: OfflineReconciliation[];
71
71
  /**
72
- * Records the store itself failed on, mid-replay. They are still in the
73
- * store — nothing was removed on this path — and counted in `failed`; this
74
- * names them so a persistent store fault is diagnosable from the summary.
72
+ * Records the store itself failed on, mid-replay. Not counted in `failed`,
73
+ * because the delivery may have succeeded before the store threw — the
74
+ * record is still in the store either way, so the next replay resubmits
75
+ * under the same idempotency key and the platform returns the original.
75
76
  */
76
77
  storeFailures: OfflineStoreFailure[];
77
78
  }
package/dist/offline.d.ts CHANGED
@@ -69,9 +69,10 @@ interface OfflineReplaySummary {
69
69
  */
70
70
  reconciled: OfflineReconciliation[];
71
71
  /**
72
- * Records the store itself failed on, mid-replay. They are still in the
73
- * store — nothing was removed on this path — and counted in `failed`; this
74
- * names them so a persistent store fault is diagnosable from the summary.
72
+ * Records the store itself failed on, mid-replay. Not counted in `failed`,
73
+ * because the delivery may have succeeded before the store threw — the
74
+ * record is still in the store either way, so the next replay resubmits
75
+ * under the same idempotency key and the platform returns the original.
75
76
  */
76
77
  storeFailures: OfflineStoreFailure[];
77
78
  }
package/dist/offline.js CHANGED
@@ -60,12 +60,18 @@ function createOfflineCaptureQueue(options) {
60
60
  try {
61
61
  outcome = await options.gateway.replay(request);
62
62
  } catch (error) {
63
- if (isSettledReplayFailure(error)) {
64
- await options.store.remove(record.captureId);
65
- reconciled.push({ record, status: "not_delivered", certainty: "refused", reason: messageOf(error) });
66
- } else {
63
+ const thrown = classifyThrownReplayFailure(error);
64
+ if (thrown === "transient") {
67
65
  failed += 1;
66
+ return;
68
67
  }
68
+ await options.store.remove(record.captureId);
69
+ reconciled.push({
70
+ record,
71
+ status: thrown === "settled-unknown" ? "conflict_prior_invocation" : "not_delivered",
72
+ certainty: thrown === "settled-unknown" ? "unknown" : "refused",
73
+ reason: messageOf(error)
74
+ });
69
75
  return;
70
76
  }
71
77
  const disposition = dispositionOf(outcome);
@@ -93,7 +99,6 @@ function createOfflineCaptureQueue(options) {
93
99
  try {
94
100
  await replayOne(record);
95
101
  } catch (error) {
96
- failed += 1;
97
102
  storeFailures.push({ record, reason: messageOf(error) });
98
103
  }
99
104
  }
@@ -174,24 +179,29 @@ function dispositionOf(outcome) {
174
179
  }
175
180
  case "blocked_by_policy":
176
181
  case "validation_failed":
177
- return { kind: "attention", status, certainty: "refused", reason };
182
+ return { kind: "attention", status, certainty: "unknown", reason };
178
183
  case "failed":
179
184
  return { kind: "attention", status, certainty: "unknown", reason };
180
185
  default:
181
186
  return { kind: "attention", status: status || "unknown", certainty: "unknown", reason };
182
187
  }
183
188
  }
189
+ function classifyThrownReplayFailure(error) {
190
+ const message = messageOf(error);
191
+ const conflict = /conflicts with invocation [^:]+:\s*(.+)$/i.exec(message);
192
+ if (conflict) {
193
+ const reasons = new Set(conflict[1].split(",").map((reason) => reason.trim()));
194
+ const neverApplied = reasons.has("parameters") || reasons.has("actor");
195
+ return neverApplied ? "settled-refused" : "settled-unknown";
196
+ }
197
+ return isSettledReplayFailure(error) ? "settled-refused" : "transient";
198
+ }
184
199
  function isSettledReplayFailure(error) {
185
200
  return [
186
201
  // The action was never offline-eligible.
187
202
  /not eligible for offline replay/i,
188
203
  // The plan no longer grants, no longer exists, or was never this actor's.
189
204
  /is not granted by the render plan/i,
190
- // An idempotency conflict is permanent: the key was reused for a
191
- // materially different command, and replaying it again yields the same
192
- // conflict forever. It must reach a person, who has the operator's work
193
- // in the record and can resubmit it under a fresh key.
194
- /conflicts with invocation/i,
195
205
  /render plan was not found/i,
196
206
  /render plan has expired/i,
197
207
  /scope does not match/i,
@@ -1 +1 @@
1
- {"version":3,"sources":["../offline.ts"],"sourcesContent":["import type {\n\tExperienceGatewayClient,\n\tExperienceInvocationOutcome,\n\tExperienceReplayRequest,\n\tRenderPlanFragment,\n\tScopedRenderPlan,\n} from \"./contracts\";\n\nexport interface OfflineCaptureRecord {\n\tcaptureId: string;\n\tplanId: string;\n\treleaseDigest: string;\n\tfragmentId: string;\n\teventName: string;\n\tparameters: Record<string, unknown>;\n\tidempotencyKey: string;\n\tcapturedAt: string;\n\texpiresAt: string;\n}\n\nexport interface OfflineCaptureStore {\n\tprotection: \"encrypted-at-rest\" | \"ephemeral\";\n\tput(record: OfflineCaptureRecord): Promise<void>;\n\tlist(): Promise<readonly OfflineCaptureRecord[]>;\n\tremove(captureId: string): Promise<void>;\n}\n\n/** A delivered capture whose outcome a person needs to see. */\nexport interface OfflineReconciliation {\n\trecord: OfflineCaptureRecord;\n\t/** The status the platform returned, so a caller can tell these apart. */\n\tstatus: string;\n\t/**\n\t * The invocation the platform recorded, when it got that far.\n\t *\n\t * `recordExternalReconciliation` needs it, so an outcome of `unknown` that\n\t * omits it cannot be reconciled from this summary — which is the whole\n\t * reason the entry exists.\n\t */\n\tactionInvocationId?: string;\n\t/**\n\t * Whether the effect is known not to have happened, or is simply unknown.\n\t * An adapter that timed out mid-charge is `unknown`: telling an operator it\n\t * was refused would be as wrong as telling them it succeeded.\n\t */\n\tcertainty: \"refused\" | \"unknown\";\n\treason: string;\n}\n\n/** A delivered capture waiting on a human. */\nexport interface OfflineParked {\n\trecord: OfflineCaptureRecord;\n\tactionInvocationId?: string;\n}\n\nexport interface OfflineReplaySummary {\n\t/** Delivered and progressing: the platform has the invocation. */\n\tcompleted: number;\n\t/**\n\t * Delivered and parked awaiting a human decision.\n\t *\n\t * Carries the records, because a bare count leaves nobody able to say which\n\t * action somebody now has to approve.\n\t */\n\tparked: OfflineParked[];\n\t/** Never delivered, and worth another attempt. Still queued. */\n\tfailed: number;\n\t/**\n\t * Outlived the capture TTL or the render plan before it could be sent.\n\t *\n\t * Also carries its records: a capture that expired never reached the\n\t * platform, so this is work the operator did that nobody has.\n\t */\n\texpired: OfflineCaptureRecord[];\n\t/**\n\t * Delivered, and something a person needs to see: refused, invalid, or an\n\t * outcome nobody can determine. Also covers a delivery that can never\n\t * succeed, since retrying one only delays the moment somebody is told.\n\t */\n\treconciled: OfflineReconciliation[];\n\t/**\n\t * Records the store itself failed on, mid-replay. They are still in the\n\t * store — nothing was removed on this path — and counted in `failed`; this\n\t * names them so a persistent store fault is diagnosable from the summary.\n\t */\n\tstoreFailures: OfflineStoreFailure[];\n}\n\n/** A record the capture store could not act on. */\nexport interface OfflineStoreFailure {\n\trecord: OfflineCaptureRecord;\n\treason: string;\n}\n\nexport interface OfflineCaptureQueue {\n\tcapture(fragmentId: string, eventName: string, parameters: Record<string, unknown>, idempotencyKey: string): Promise<OfflineCaptureRecord>;\n\treplay(): Promise<OfflineReplaySummary>;\n}\n\nexport function createOfflineCaptureQueue(options: {\n\tplan: ScopedRenderPlan;\n\tstore: OfflineCaptureStore;\n\tgateway: ExperienceGatewayClient;\n\tnextCaptureId(): string;\n\tnow?: () => Date;\n\tcaptureTtlMs?: number;\n\tallowEphemeralStore?: boolean;\n}): OfflineCaptureQueue {\n\tif (options.store.protection !== \"encrypted-at-rest\" && !options.allowEphemeralStore) {\n\t\tthrow new Error(\"Production offline capture requires an encrypted-at-rest store.\");\n\t}\n\tif (!options.gateway.replay) throw new Error(\"Offline capture requires a gateway replay adapter.\");\n\tconst fragments = indexFragments(options.plan.root);\n\tconst now = options.now ?? (() => new Date());\n\treturn {\n\t\tasync capture(fragmentId, eventName, parameters, idempotencyKey) {\n\t\t\tconst fragment = fragments.get(fragmentId);\n\t\t\tif (!fragment?.offlineEvents?.includes(eventName)) {\n\t\t\t\tthrow new Error(`Fragment event \"${fragmentId}.${eventName}\" is not offline-capturable.`);\n\t\t\t}\n\t\t\tif (!idempotencyKey || idempotencyKey.length > 256) throw new Error(\"Offline capture requires a bounded stable idempotency key.\");\n\t\t\tconst capturedAt = now();\n\t\t\tconst captureExpiry = capturedAt.getTime() + (options.captureTtlMs ?? 86_400_000);\n\t\t\tconst planExpiry = Date.parse(options.plan.expiresAt);\n\t\t\tif (!Number.isFinite(planExpiry) || planExpiry <= capturedAt.getTime()) throw new Error(\"Offline capture requires an unexpired render plan.\");\n\t\t\tconst safeParameters = cloneOfflineParameters(parameters);\n\t\t\tconst captureId = options.nextCaptureId();\n\t\t\tif (!captureId.trim()) throw new Error(\"Offline capture id is required.\");\n\t\t\tconst record: OfflineCaptureRecord = {\n\t\t\t\tcaptureId,\n\t\t\t\tplanId: options.plan.planId,\n\t\t\t\treleaseDigest: options.plan.releaseDigest,\n\t\t\t\tfragmentId,\n\t\t\t\teventName,\n\t\t\t\tparameters: safeParameters,\n\t\t\t\tidempotencyKey,\n\t\t\t\tcapturedAt: capturedAt.toISOString(),\n\t\t\t\texpiresAt: new Date(Math.min(captureExpiry, planExpiry)).toISOString(),\n\t\t\t};\n\t\t\tawait options.store.put(record);\n\t\t\treturn record;\n\t\t},\n\t\tasync replay() {\n\t\t\tlet completed = 0;\n\t\t\tlet failed = 0;\n\t\t\tconst parked: OfflineParked[] = [];\n\t\t\tconst expired: OfflineCaptureRecord[] = [];\n\t\t\tconst reconciled: OfflineReconciliation[] = [];\n\t\t\tconst storeFailures: OfflineStoreFailure[] = [];\n\n\t\t\tconst replayOne = async (record: OfflineCaptureRecord): Promise<void> => {\n\t\t\t\tif (Date.parse(record.expiresAt) <= now().getTime()) {\n\t\t\t\t\tawait options.store.remove(record.captureId);\n\t\t\t\t\texpired.push(record);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst request: ExperienceReplayRequest = {\n\t\t\t\t\tplanId: record.planId,\n\t\t\t\t\tfragmentId: record.fragmentId,\n\t\t\t\t\teventName: record.eventName,\n\t\t\t\t\tparameters: record.parameters,\n\t\t\t\t\tidempotencyKey: record.idempotencyKey,\n\t\t\t\t\texecutionReason: \"offline_replay\",\n\t\t\t\t\tcapturedAt: record.capturedAt,\n\t\t\t\t};\n\n\t\t\t\tlet outcome: ExperienceInvocationOutcome | undefined;\n\t\t\t\ttry {\n\t\t\t\t\toutcome = await options.gateway.replay!(request);\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Only genuine throws reach here. A settled one is never\n\t\t\t\t\t// deliverable and retrying it only delays the moment somebody\n\t\t\t\t\t// is told; anything unrecognised stays transient, so an\n\t\t\t\t\t// unfamiliar failure is retried rather than discarded.\n\t\t\t\t\tif (isSettledReplayFailure(error)) {\n\t\t\t\t\t\tawait options.store.remove(record.captureId);\n\t\t\t\t\t\treconciled.push({ record, status: \"not_delivered\", certainty: \"refused\", reason: messageOf(error) });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfailed += 1;\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// The disposition is decided before the record is removed, because\n\t\t\t\t// an adapter that said nothing has not delivered and the capture\n\t\t\t\t// must stay queued. Anything else returned means the platform\n\t\t\t\t// durably holds the invocation, so the capture has done its job\n\t\t\t\t// and leaves the queue; what differs is whether a person needs to\n\t\t\t\t// hear about it.\n\t\t\t\tconst disposition = dispositionOf(outcome);\n\t\t\t\tif (disposition.kind === \"not-delivered\") {\n\t\t\t\t\tfailed += 1;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tawait options.store.remove(record.captureId);\n\t\t\t\tconst actionInvocationId = typeof outcome?.actionInvocationId === \"string\"\n\t\t\t\t\t? outcome.actionInvocationId\n\t\t\t\t\t: undefined;\n\t\t\t\tif (disposition.kind === \"attention\") {\n\t\t\t\t\treconciled.push({\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tstatus: disposition.status,\n\t\t\t\t\t\t...(actionInvocationId === undefined ? {} : { actionInvocationId }),\n\t\t\t\t\t\tcertainty: disposition.certainty,\n\t\t\t\t\t\treason: disposition.reason,\n\t\t\t\t\t});\n\t\t\t\t} else if (disposition.kind === \"parked\") {\n\t\t\t\t\tparked.push({ record, ...(actionInvocationId === undefined ? {} : { actionInvocationId }) });\n\t\t\t\t} else {\n\t\t\t\t\tcompleted += 1;\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tfor (const record of await options.store.list()) {\n\t\t\t\t// Every store call sits inside this guard. A store that throws on\n\t\t\t\t// remove — an IndexedDB abort is not exotic — would otherwise\n\t\t\t\t// reject replay() after earlier records were already deleted, and\n\t\t\t\t// their summary entries, now the only place those records live,\n\t\t\t\t// would go with it. One failing record degrades one record.\n\t\t\t\ttry {\n\t\t\t\t\tawait replayOne(record);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tfailed += 1;\n\t\t\t\t\tstoreFailures.push({ record, reason: messageOf(error) });\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { completed, parked, failed, expired, reconciled, storeFailures };\n\t\t},\n\t};\n}\n\nfunction cloneOfflineParameters(parameters: Record<string, unknown>): Record<string, unknown> {\n\tlet nodes = 0;\n\tconst visit = (value: unknown, depth: number, key?: string): void => {\n\t\tnodes += 1;\n\t\tif (nodes > 10_000 || depth > 20) throw new Error(\"Offline capture parameters exceed structural limits.\");\n\t\tif (key !== undefined && /(?:authorization|bearer|cookie|credential|password|secret|token|claims?)/i.test(key)) {\n\t\t\tthrow new Error(`Offline capture parameters may not contain credential or claim field \"${key}\".`);\n\t\t}\n\t\tif (value === null || typeof value === \"string\" || typeof value === \"boolean\") return;\n\t\tif (typeof value === \"number\" && Number.isFinite(value)) return;\n\t\tif (Array.isArray(value)) {\n\t\t\tvalue.forEach((entry) => visit(entry, depth + 1));\n\t\t\treturn;\n\t\t}\n\t\tif (value && typeof value === \"object\" && Object.getPrototypeOf(value) === Object.prototype) {\n\t\t\tfor (const [childKey, child] of Object.entries(value)) visit(child, depth + 1, childKey);\n\t\t\treturn;\n\t\t}\n\t\tthrow new Error(\"Offline capture parameters must contain only portable JSON values.\");\n\t};\n\tvisit(parameters, 0);\n\tconst serialized = JSON.stringify(parameters);\n\tif (new TextEncoder().encode(serialized).byteLength > 65_536) throw new Error(\"Offline capture parameters exceed the 65536-byte limit.\");\n\treturn structuredClone(parameters);\n}\n\nfunction indexFragments(root: RenderPlanFragment): Map<string, RenderPlanFragment> {\n\tconst result = new Map<string, RenderPlanFragment>();\n\tconst visit = (fragment: RenderPlanFragment): void => {\n\t\tresult.set(fragment.id, fragment);\n\t\tfor (const child of fragment.children ?? []) visit(child);\n\t};\n\tvisit(root);\n\treturn result;\n}\n\nexport function createMemoryOfflineCaptureStore(): OfflineCaptureStore {\n\tconst records = new Map<string, OfflineCaptureRecord>();\n\treturn {\n\t\tprotection: \"ephemeral\",\n\t\tasync put(record) {\n\t\t\tif (records.has(record.captureId)) throw new Error(`Offline capture \"${record.captureId}\" already exists.`);\n\t\t\trecords.set(record.captureId, structuredClone(record));\n\t\t},\n\t\tasync list() {\n\t\t\treturn [...records.values()].map((record) => structuredClone(record));\n\t\t},\n\t\tasync remove(captureId) {\n\t\t\trecords.delete(captureId);\n\t\t},\n\t};\n}\n\nfunction messageOf(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\ntype ReplayDisposition =\n\t| { kind: \"delivered\" }\n\t| { kind: \"not-delivered\" }\n\t| { kind: \"parked\" }\n\t| { kind: \"attention\"; status: string; certainty: \"refused\" | \"unknown\"; reason: string };\n\n/**\n * Read a returned outcome across the platform's whole status set.\n *\n * Enumerating the statuses that looked like refusals left the rest falling\n * through to \"applied\", which is how a validation failure got reported to an\n * operator as success. The switch covers every `ActionStatus`, and the default\n * arm surfaces anything else rather than counting it a success — the status\n * arrives as a string over a transport, so this is a runtime guarantee and not\n * a compile-time one.\n *\n * There is no \"already applied\" branch. A key that legitimately repeats returns\n * the original invocation without complaint, and a genuine idempotency conflict\n * means the key was reused for a materially different command, so that capture\n * was never applied and must not be discarded.\n */\nfunction dispositionOf(outcome: ExperienceInvocationOutcome | undefined): ReplayDisposition {\n\t// An adapter that returns nothing has told us nothing. Reading that as\n\t// success is the failure this whole switch exists to prevent — but so is\n\t// draining a whole queue into \"unknown\" because one adapter forgot its\n\t// return statement. Nothing was delivered as far as we can tell, so the\n\t// capture stays queued and the adapter fault surfaces as a transient.\n\tif (!outcome || typeof outcome !== \"object\") {\n\t\treturn { kind: \"not-delivered\" };\n\t}\n\tconst status = typeof outcome.status === \"string\" ? outcome.status : \"\";\n\tconst reason = outcome.reconciliation?.message\n\t\t?? outcome.reconciliation?.kind\n\t\t?? (typeof outcome.error === \"string\" ? outcome.error : status);\n\n\tswitch (status) {\n\t\tcase \"pending\":\n\t\tcase \"running\":\n\t\tcase \"completed\":\n\t\t\treturn { kind: \"delivered\" };\n\t\tcase \"waiting_for_approval\":\n\t\t\treturn { kind: \"parked\" };\n\t\tcase \"reconciliation_required\": {\n\t\t\t// Two things arrive here that are not refusals. An adapter whose\n\t\t\t// outcome could not be determined, and an authority check that\n\t\t\t// failed on a *recovery* re-execution: the lease worker re-runs the\n\t\t\t// whole authorization block, but already-succeeded adapter steps are\n\t\t\t// skipped rather than undone, so the external effect may have landed\n\t\t\t// before the binding expired. The persisted reconciliation says\n\t\t\t// which execution it was.\n\t\t\tconst recovery = (outcome.reconciliation as { executionReason?: string } | undefined)?.executionReason === \"recovery\";\n\t\t\tconst unknown = outcome.adapterReconciliation !== undefined || recovery;\n\t\t\treturn { kind: \"attention\", status, certainty: unknown ? \"unknown\" : \"refused\", reason };\n\t\t}\n\t\tcase \"blocked_by_policy\":\n\t\tcase \"validation_failed\":\n\t\t\t// Both are decided before any effect: policy refuses at the gate and\n\t\t\t// validation refuses the parameters.\n\t\t\treturn { kind: \"attention\", status, certainty: \"refused\", reason };\n\t\tcase \"failed\":\n\t\t\t// Not the same. A handler or adapter step can fail after earlier\n\t\t\t// steps succeeded and the domain transaction committed, so telling an\n\t\t\t// operator this was refused could be as wrong as calling it applied.\n\t\t\treturn { kind: \"attention\", status, certainty: \"unknown\", reason };\n\t\tdefault:\n\t\t\t// An unfamiliar status is surfaced rather than counted as success.\n\t\t\treturn { kind: \"attention\", status: status || \"unknown\", certainty: \"unknown\", reason };\n\t}\n}\n\n/**\n * A thrown failure the platform will never accept on a later attempt.\n *\n * Deliberately narrow, and matched on what the gateway and runtime actually\n * throw. Anything else is transient, because keeping a capture costs a retry\n * and discarding one costs the operator their work.\n */\nfunction isSettledReplayFailure(error: unknown): boolean {\n\treturn [\n\t\t// The action was never offline-eligible.\n\t\t/not eligible for offline replay/i,\n\t\t// The plan no longer grants, no longer exists, or was never this actor's.\n\t\t/is not granted by the render plan/i,\n\t\t// An idempotency conflict is permanent: the key was reused for a\n\t\t// materially different command, and replaying it again yields the same\n\t\t// conflict forever. It must reach a person, who has the operator's work\n\t\t// in the record and can resubmit it under a fresh key.\n\t\t/conflicts with invocation/i,\n\t\t/render plan was not found/i,\n\t\t/render plan has expired/i,\n\t\t/scope does not match/i,\n\t\t/belongs to a different application assembly/i,\n\t\t// A capture only gets older, so an age refusal can never come good. This\n\t\t// one is independent of the capture TTL, so nothing else retires it.\n\t\t/capturedAt is older than/i,\n\t\t// Malformed beyond repair: passes the grammar, names no real instant.\n\t\t/capturedAt is not a parseable instant/i,\n\t\t/capturedAt must be an RFC 3339/i,\n\t\t// Deliberately absent: a capture dated in the future comes good as real\n\t\t// time advances; a gateway missing its replay window is a deployment\n\t\t// mistake somebody will fix; and an action-version mismatch fires\n\t\t// transiently mid-rollout when a client's plan is newer than the pod it\n\t\t// reached. Discarding any of them would destroy work a clock correction,\n\t\t// a config change, or the rollout finishing would have replayed.\n\t].some((pattern) => pattern.test(messageOf(error)));\n}\n"],"mappings":";AAmGO,SAAS,0BAA0B,SAQlB;AACvB,MAAI,QAAQ,MAAM,eAAe,uBAAuB,CAAC,QAAQ,qBAAqB;AACrF,UAAM,IAAI,MAAM,iEAAiE;AAAA,EAClF;AACA,MAAI,CAAC,QAAQ,QAAQ,OAAQ,OAAM,IAAI,MAAM,oDAAoD;AACjG,QAAM,YAAY,eAAe,QAAQ,KAAK,IAAI;AAClD,QAAM,MAAM,QAAQ,QAAQ,MAAM,oBAAI,KAAK;AAC3C,SAAO;AAAA,IACN,MAAM,QAAQ,YAAY,WAAW,YAAY,gBAAgB;AAChE,YAAM,WAAW,UAAU,IAAI,UAAU;AACzC,UAAI,CAAC,UAAU,eAAe,SAAS,SAAS,GAAG;AAClD,cAAM,IAAI,MAAM,mBAAmB,UAAU,IAAI,SAAS,8BAA8B;AAAA,MACzF;AACA,UAAI,CAAC,kBAAkB,eAAe,SAAS,IAAK,OAAM,IAAI,MAAM,4DAA4D;AAChI,YAAM,aAAa,IAAI;AACvB,YAAM,gBAAgB,WAAW,QAAQ,KAAK,QAAQ,gBAAgB;AACtE,YAAM,aAAa,KAAK,MAAM,QAAQ,KAAK,SAAS;AACpD,UAAI,CAAC,OAAO,SAAS,UAAU,KAAK,cAAc,WAAW,QAAQ,EAAG,OAAM,IAAI,MAAM,oDAAoD;AAC5I,YAAM,iBAAiB,uBAAuB,UAAU;AACxD,YAAM,YAAY,QAAQ,cAAc;AACxC,UAAI,CAAC,UAAU,KAAK,EAAG,OAAM,IAAI,MAAM,iCAAiC;AACxE,YAAM,SAA+B;AAAA,QACpC;AAAA,QACA,QAAQ,QAAQ,KAAK;AAAA,QACrB,eAAe,QAAQ,KAAK;AAAA,QAC5B;AAAA,QACA;AAAA,QACA,YAAY;AAAA,QACZ;AAAA,QACA,YAAY,WAAW,YAAY;AAAA,QACnC,WAAW,IAAI,KAAK,KAAK,IAAI,eAAe,UAAU,CAAC,EAAE,YAAY;AAAA,MACtE;AACA,YAAM,QAAQ,MAAM,IAAI,MAAM;AAC9B,aAAO;AAAA,IACR;AAAA,IACA,MAAM,SAAS;AACd,UAAI,YAAY;AAChB,UAAI,SAAS;AACb,YAAM,SAA0B,CAAC;AACjC,YAAM,UAAkC,CAAC;AACzC,YAAM,aAAsC,CAAC;AAC7C,YAAM,gBAAuC,CAAC;AAE9C,YAAM,YAAY,OAAO,WAAgD;AACxE,YAAI,KAAK,MAAM,OAAO,SAAS,KAAK,IAAI,EAAE,QAAQ,GAAG;AACpD,gBAAM,QAAQ,MAAM,OAAO,OAAO,SAAS;AAC3C,kBAAQ,KAAK,MAAM;AACnB;AAAA,QACD;AACA,cAAM,UAAmC;AAAA,UACxC,QAAQ,OAAO;AAAA,UACf,YAAY,OAAO;AAAA,UACnB,WAAW,OAAO;AAAA,UAClB,YAAY,OAAO;AAAA,UACnB,gBAAgB,OAAO;AAAA,UACvB,iBAAiB;AAAA,UACjB,YAAY,OAAO;AAAA,QACpB;AAEA,YAAI;AACJ,YAAI;AACH,oBAAU,MAAM,QAAQ,QAAQ,OAAQ,OAAO;AAAA,QAChD,SAAS,OAAO;AAKf,cAAI,uBAAuB,KAAK,GAAG;AAClC,kBAAM,QAAQ,MAAM,OAAO,OAAO,SAAS;AAC3C,uBAAW,KAAK,EAAE,QAAQ,QAAQ,iBAAiB,WAAW,WAAW,QAAQ,UAAU,KAAK,EAAE,CAAC;AAAA,UACpG,OAAO;AACN,sBAAU;AAAA,UACX;AACA;AAAA,QACD;AAQA,cAAM,cAAc,cAAc,OAAO;AACzC,YAAI,YAAY,SAAS,iBAAiB;AACzC,oBAAU;AACV;AAAA,QACD;AACA,cAAM,QAAQ,MAAM,OAAO,OAAO,SAAS;AAC3C,cAAM,qBAAqB,OAAO,SAAS,uBAAuB,WAC/D,QAAQ,qBACR;AACH,YAAI,YAAY,SAAS,aAAa;AACrC,qBAAW,KAAK;AAAA,YACf;AAAA,YACA,QAAQ,YAAY;AAAA,YACpB,GAAI,uBAAuB,SAAY,CAAC,IAAI,EAAE,mBAAmB;AAAA,YACjE,WAAW,YAAY;AAAA,YACvB,QAAQ,YAAY;AAAA,UACrB,CAAC;AAAA,QACF,WAAW,YAAY,SAAS,UAAU;AACzC,iBAAO,KAAK,EAAE,QAAQ,GAAI,uBAAuB,SAAY,CAAC,IAAI,EAAE,mBAAmB,EAAG,CAAC;AAAA,QAC5F,OAAO;AACN,uBAAa;AAAA,QACd;AAAA,MACD;AAEA,iBAAW,UAAU,MAAM,QAAQ,MAAM,KAAK,GAAG;AAMhD,YAAI;AACH,gBAAM,UAAU,MAAM;AAAA,QACvB,SAAS,OAAO;AACf,oBAAU;AACV,wBAAc,KAAK,EAAE,QAAQ,QAAQ,UAAU,KAAK,EAAE,CAAC;AAAA,QACxD;AAAA,MACD;AACA,aAAO,EAAE,WAAW,QAAQ,QAAQ,SAAS,YAAY,cAAc;AAAA,IACxE;AAAA,EACD;AACD;AAEA,SAAS,uBAAuB,YAA8D;AAC7F,MAAI,QAAQ;AACZ,QAAM,QAAQ,CAAC,OAAgB,OAAe,QAAuB;AACpE,aAAS;AACT,QAAI,QAAQ,OAAU,QAAQ,GAAI,OAAM,IAAI,MAAM,sDAAsD;AACxG,QAAI,QAAQ,UAAa,4EAA4E,KAAK,GAAG,GAAG;AAC/G,YAAM,IAAI,MAAM,yEAAyE,GAAG,IAAI;AAAA,IACjG;AACA,QAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,OAAO,UAAU,UAAW;AAC/E,QAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,EAAG;AACzD,QAAI,MAAM,QAAQ,KAAK,GAAG;AACzB,YAAM,QAAQ,CAAC,UAAU,MAAM,OAAO,QAAQ,CAAC,CAAC;AAChD;AAAA,IACD;AACA,QAAI,SAAS,OAAO,UAAU,YAAY,OAAO,eAAe,KAAK,MAAM,OAAO,WAAW;AAC5F,iBAAW,CAAC,UAAU,KAAK,KAAK,OAAO,QAAQ,KAAK,EAAG,OAAM,OAAO,QAAQ,GAAG,QAAQ;AACvF;AAAA,IACD;AACA,UAAM,IAAI,MAAM,oEAAoE;AAAA,EACrF;AACA,QAAM,YAAY,CAAC;AACnB,QAAM,aAAa,KAAK,UAAU,UAAU;AAC5C,MAAI,IAAI,YAAY,EAAE,OAAO,UAAU,EAAE,aAAa,MAAQ,OAAM,IAAI,MAAM,yDAAyD;AACvI,SAAO,gBAAgB,UAAU;AAClC;AAEA,SAAS,eAAe,MAA2D;AAClF,QAAM,SAAS,oBAAI,IAAgC;AACnD,QAAM,QAAQ,CAAC,aAAuC;AACrD,WAAO,IAAI,SAAS,IAAI,QAAQ;AAChC,eAAW,SAAS,SAAS,YAAY,CAAC,EAAG,OAAM,KAAK;AAAA,EACzD;AACA,QAAM,IAAI;AACV,SAAO;AACR;AAEO,SAAS,kCAAuD;AACtE,QAAM,UAAU,oBAAI,IAAkC;AACtD,SAAO;AAAA,IACN,YAAY;AAAA,IACZ,MAAM,IAAI,QAAQ;AACjB,UAAI,QAAQ,IAAI,OAAO,SAAS,EAAG,OAAM,IAAI,MAAM,oBAAoB,OAAO,SAAS,mBAAmB;AAC1G,cAAQ,IAAI,OAAO,WAAW,gBAAgB,MAAM,CAAC;AAAA,IACtD;AAAA,IACA,MAAM,OAAO;AACZ,aAAO,CAAC,GAAG,QAAQ,OAAO,CAAC,EAAE,IAAI,CAAC,WAAW,gBAAgB,MAAM,CAAC;AAAA,IACrE;AAAA,IACA,MAAM,OAAO,WAAW;AACvB,cAAQ,OAAO,SAAS;AAAA,IACzB;AAAA,EACD;AACD;AAEA,SAAS,UAAU,OAAwB;AAC1C,SAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC7D;AAuBA,SAAS,cAAc,SAAqE;AAM3F,MAAI,CAAC,WAAW,OAAO,YAAY,UAAU;AAC5C,WAAO,EAAE,MAAM,gBAAgB;AAAA,EAChC;AACA,QAAM,SAAS,OAAO,QAAQ,WAAW,WAAW,QAAQ,SAAS;AACrE,QAAM,SAAS,QAAQ,gBAAgB,WACnC,QAAQ,gBAAgB,SACvB,OAAO,QAAQ,UAAU,WAAW,QAAQ,QAAQ;AAEzD,UAAQ,QAAQ;AAAA,IACf,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACJ,aAAO,EAAE,MAAM,YAAY;AAAA,IAC5B,KAAK;AACJ,aAAO,EAAE,MAAM,SAAS;AAAA,IACzB,KAAK,2BAA2B;AAQ/B,YAAM,WAAY,QAAQ,gBAA6D,oBAAoB;AAC3G,YAAM,UAAU,QAAQ,0BAA0B,UAAa;AAC/D,aAAO,EAAE,MAAM,aAAa,QAAQ,WAAW,UAAU,YAAY,WAAW,OAAO;AAAA,IACxF;AAAA,IACA,KAAK;AAAA,IACL,KAAK;AAGJ,aAAO,EAAE,MAAM,aAAa,QAAQ,WAAW,WAAW,OAAO;AAAA,IAClE,KAAK;AAIJ,aAAO,EAAE,MAAM,aAAa,QAAQ,WAAW,WAAW,OAAO;AAAA,IAClE;AAEC,aAAO,EAAE,MAAM,aAAa,QAAQ,UAAU,WAAW,WAAW,WAAW,OAAO;AAAA,EACxF;AACD;AASA,SAAS,uBAAuB,OAAyB;AACxD,SAAO;AAAA;AAAA,IAEN;AAAA;AAAA,IAEA;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA;AAAA,IAGA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOD,EAAE,KAAK,CAAC,YAAY,QAAQ,KAAK,UAAU,KAAK,CAAC,CAAC;AACnD;","names":[]}
1
+ {"version":3,"sources":["../offline.ts"],"sourcesContent":["import type {\n\tExperienceGatewayClient,\n\tExperienceInvocationOutcome,\n\tExperienceReplayRequest,\n\tRenderPlanFragment,\n\tScopedRenderPlan,\n} from \"./contracts\";\n\nexport interface OfflineCaptureRecord {\n\tcaptureId: string;\n\tplanId: string;\n\treleaseDigest: string;\n\tfragmentId: string;\n\teventName: string;\n\tparameters: Record<string, unknown>;\n\tidempotencyKey: string;\n\tcapturedAt: string;\n\texpiresAt: string;\n}\n\nexport interface OfflineCaptureStore {\n\tprotection: \"encrypted-at-rest\" | \"ephemeral\";\n\tput(record: OfflineCaptureRecord): Promise<void>;\n\tlist(): Promise<readonly OfflineCaptureRecord[]>;\n\tremove(captureId: string): Promise<void>;\n}\n\n/** A delivered capture whose outcome a person needs to see. */\nexport interface OfflineReconciliation {\n\trecord: OfflineCaptureRecord;\n\t/** The status the platform returned, so a caller can tell these apart. */\n\tstatus: string;\n\t/**\n\t * The invocation the platform recorded, when it got that far.\n\t *\n\t * `recordExternalReconciliation` needs it, so an outcome of `unknown` that\n\t * omits it cannot be reconciled from this summary — which is the whole\n\t * reason the entry exists.\n\t */\n\tactionInvocationId?: string;\n\t/**\n\t * Whether the effect is known not to have happened, or is simply unknown.\n\t * An adapter that timed out mid-charge is `unknown`: telling an operator it\n\t * was refused would be as wrong as telling them it succeeded.\n\t */\n\tcertainty: \"refused\" | \"unknown\";\n\treason: string;\n}\n\n/** A delivered capture waiting on a human. */\nexport interface OfflineParked {\n\trecord: OfflineCaptureRecord;\n\tactionInvocationId?: string;\n}\n\nexport interface OfflineReplaySummary {\n\t/** Delivered and progressing: the platform has the invocation. */\n\tcompleted: number;\n\t/**\n\t * Delivered and parked awaiting a human decision.\n\t *\n\t * Carries the records, because a bare count leaves nobody able to say which\n\t * action somebody now has to approve.\n\t */\n\tparked: OfflineParked[];\n\t/** Never delivered, and worth another attempt. Still queued. */\n\tfailed: number;\n\t/**\n\t * Outlived the capture TTL or the render plan before it could be sent.\n\t *\n\t * Also carries its records: a capture that expired never reached the\n\t * platform, so this is work the operator did that nobody has.\n\t */\n\texpired: OfflineCaptureRecord[];\n\t/**\n\t * Delivered, and something a person needs to see: refused, invalid, or an\n\t * outcome nobody can determine. Also covers a delivery that can never\n\t * succeed, since retrying one only delays the moment somebody is told.\n\t */\n\treconciled: OfflineReconciliation[];\n\t/**\n\t * Records the store itself failed on, mid-replay. Not counted in `failed`,\n\t * because the delivery may have succeeded before the store threw — the\n\t * record is still in the store either way, so the next replay resubmits\n\t * under the same idempotency key and the platform returns the original.\n\t */\n\tstoreFailures: OfflineStoreFailure[];\n}\n\n/** A record the capture store could not act on. */\nexport interface OfflineStoreFailure {\n\trecord: OfflineCaptureRecord;\n\treason: string;\n}\n\nexport interface OfflineCaptureQueue {\n\tcapture(fragmentId: string, eventName: string, parameters: Record<string, unknown>, idempotencyKey: string): Promise<OfflineCaptureRecord>;\n\treplay(): Promise<OfflineReplaySummary>;\n}\n\nexport function createOfflineCaptureQueue(options: {\n\tplan: ScopedRenderPlan;\n\tstore: OfflineCaptureStore;\n\tgateway: ExperienceGatewayClient;\n\tnextCaptureId(): string;\n\tnow?: () => Date;\n\tcaptureTtlMs?: number;\n\tallowEphemeralStore?: boolean;\n}): OfflineCaptureQueue {\n\tif (options.store.protection !== \"encrypted-at-rest\" && !options.allowEphemeralStore) {\n\t\tthrow new Error(\"Production offline capture requires an encrypted-at-rest store.\");\n\t}\n\tif (!options.gateway.replay) throw new Error(\"Offline capture requires a gateway replay adapter.\");\n\tconst fragments = indexFragments(options.plan.root);\n\tconst now = options.now ?? (() => new Date());\n\treturn {\n\t\tasync capture(fragmentId, eventName, parameters, idempotencyKey) {\n\t\t\tconst fragment = fragments.get(fragmentId);\n\t\t\tif (!fragment?.offlineEvents?.includes(eventName)) {\n\t\t\t\tthrow new Error(`Fragment event \"${fragmentId}.${eventName}\" is not offline-capturable.`);\n\t\t\t}\n\t\t\tif (!idempotencyKey || idempotencyKey.length > 256) throw new Error(\"Offline capture requires a bounded stable idempotency key.\");\n\t\t\tconst capturedAt = now();\n\t\t\tconst captureExpiry = capturedAt.getTime() + (options.captureTtlMs ?? 86_400_000);\n\t\t\tconst planExpiry = Date.parse(options.plan.expiresAt);\n\t\t\tif (!Number.isFinite(planExpiry) || planExpiry <= capturedAt.getTime()) throw new Error(\"Offline capture requires an unexpired render plan.\");\n\t\t\tconst safeParameters = cloneOfflineParameters(parameters);\n\t\t\tconst captureId = options.nextCaptureId();\n\t\t\tif (!captureId.trim()) throw new Error(\"Offline capture id is required.\");\n\t\t\tconst record: OfflineCaptureRecord = {\n\t\t\t\tcaptureId,\n\t\t\t\tplanId: options.plan.planId,\n\t\t\t\treleaseDigest: options.plan.releaseDigest,\n\t\t\t\tfragmentId,\n\t\t\t\teventName,\n\t\t\t\tparameters: safeParameters,\n\t\t\t\tidempotencyKey,\n\t\t\t\tcapturedAt: capturedAt.toISOString(),\n\t\t\t\texpiresAt: new Date(Math.min(captureExpiry, planExpiry)).toISOString(),\n\t\t\t};\n\t\t\tawait options.store.put(record);\n\t\t\treturn record;\n\t\t},\n\t\tasync replay() {\n\t\t\tlet completed = 0;\n\t\t\tlet failed = 0;\n\t\t\tconst parked: OfflineParked[] = [];\n\t\t\tconst expired: OfflineCaptureRecord[] = [];\n\t\t\tconst reconciled: OfflineReconciliation[] = [];\n\t\t\tconst storeFailures: OfflineStoreFailure[] = [];\n\n\t\t\tconst replayOne = async (record: OfflineCaptureRecord): Promise<void> => {\n\t\t\t\tif (Date.parse(record.expiresAt) <= now().getTime()) {\n\t\t\t\t\tawait options.store.remove(record.captureId);\n\t\t\t\t\texpired.push(record);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst request: ExperienceReplayRequest = {\n\t\t\t\t\tplanId: record.planId,\n\t\t\t\t\tfragmentId: record.fragmentId,\n\t\t\t\t\teventName: record.eventName,\n\t\t\t\t\tparameters: record.parameters,\n\t\t\t\t\tidempotencyKey: record.idempotencyKey,\n\t\t\t\t\texecutionReason: \"offline_replay\",\n\t\t\t\t\tcapturedAt: record.capturedAt,\n\t\t\t\t};\n\n\t\t\t\tlet outcome: ExperienceInvocationOutcome | undefined;\n\t\t\t\ttry {\n\t\t\t\t\toutcome = await options.gateway.replay!(request);\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Only genuine throws reach here. A settled one is never\n\t\t\t\t\t// deliverable on a later attempt and retrying only delays the\n\t\t\t\t\t// moment somebody is told; anything unrecognised stays\n\t\t\t\t\t// transient, so an unfamiliar failure is retried, not discarded.\n\t\t\t\t\tconst thrown = classifyThrownReplayFailure(error);\n\t\t\t\t\tif (thrown === \"transient\") {\n\t\t\t\t\t\tfailed += 1;\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tawait options.store.remove(record.captureId);\n\t\t\t\t\treconciled.push({\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tstatus: thrown === \"settled-unknown\" ? \"conflict_prior_invocation\" : \"not_delivered\",\n\t\t\t\t\t\tcertainty: thrown === \"settled-unknown\" ? \"unknown\" : \"refused\",\n\t\t\t\t\t\treason: messageOf(error),\n\t\t\t\t\t});\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// The disposition is decided before the record is removed, because\n\t\t\t\t// an adapter that said nothing has not delivered and the capture\n\t\t\t\t// must stay queued. Anything else returned means the platform\n\t\t\t\t// durably holds the invocation, so the capture has done its job\n\t\t\t\t// and leaves the queue; what differs is whether a person needs to\n\t\t\t\t// hear about it.\n\t\t\t\tconst disposition = dispositionOf(outcome);\n\t\t\t\tif (disposition.kind === \"not-delivered\") {\n\t\t\t\t\tfailed += 1;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tawait options.store.remove(record.captureId);\n\t\t\t\tconst actionInvocationId = typeof outcome?.actionInvocationId === \"string\"\n\t\t\t\t\t? outcome.actionInvocationId\n\t\t\t\t\t: undefined;\n\t\t\t\tif (disposition.kind === \"attention\") {\n\t\t\t\t\treconciled.push({\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tstatus: disposition.status,\n\t\t\t\t\t\t...(actionInvocationId === undefined ? {} : { actionInvocationId }),\n\t\t\t\t\t\tcertainty: disposition.certainty,\n\t\t\t\t\t\treason: disposition.reason,\n\t\t\t\t\t});\n\t\t\t\t} else if (disposition.kind === \"parked\") {\n\t\t\t\t\tparked.push({ record, ...(actionInvocationId === undefined ? {} : { actionInvocationId }) });\n\t\t\t\t} else {\n\t\t\t\t\tcompleted += 1;\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tfor (const record of await options.store.list()) {\n\t\t\t\t// Every store call sits inside this guard. A store that throws on\n\t\t\t\t// remove — an IndexedDB abort is not exotic — would otherwise\n\t\t\t\t// reject replay() after earlier records were already deleted, and\n\t\t\t\t// their summary entries, now the only place those records live,\n\t\t\t\t// would go with it. One failing record degrades one record.\n\t\t\t\ttry {\n\t\t\t\t\tawait replayOne(record);\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Not counted in `failed`: that bucket means \"never delivered\",\n\t\t\t\t\t// and a store fault after a successful delivery is neither\n\t\t\t\t\t// that nor a success. It has its own bucket so it is neither\n\t\t\t\t\t// hidden nor misdescribed.\n\t\t\t\t\tstoreFailures.push({ record, reason: messageOf(error) });\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { completed, parked, failed, expired, reconciled, storeFailures };\n\t\t},\n\t};\n}\n\nfunction cloneOfflineParameters(parameters: Record<string, unknown>): Record<string, unknown> {\n\tlet nodes = 0;\n\tconst visit = (value: unknown, depth: number, key?: string): void => {\n\t\tnodes += 1;\n\t\tif (nodes > 10_000 || depth > 20) throw new Error(\"Offline capture parameters exceed structural limits.\");\n\t\tif (key !== undefined && /(?:authorization|bearer|cookie|credential|password|secret|token|claims?)/i.test(key)) {\n\t\t\tthrow new Error(`Offline capture parameters may not contain credential or claim field \"${key}\".`);\n\t\t}\n\t\tif (value === null || typeof value === \"string\" || typeof value === \"boolean\") return;\n\t\tif (typeof value === \"number\" && Number.isFinite(value)) return;\n\t\tif (Array.isArray(value)) {\n\t\t\tvalue.forEach((entry) => visit(entry, depth + 1));\n\t\t\treturn;\n\t\t}\n\t\tif (value && typeof value === \"object\" && Object.getPrototypeOf(value) === Object.prototype) {\n\t\t\tfor (const [childKey, child] of Object.entries(value)) visit(child, depth + 1, childKey);\n\t\t\treturn;\n\t\t}\n\t\tthrow new Error(\"Offline capture parameters must contain only portable JSON values.\");\n\t};\n\tvisit(parameters, 0);\n\tconst serialized = JSON.stringify(parameters);\n\tif (new TextEncoder().encode(serialized).byteLength > 65_536) throw new Error(\"Offline capture parameters exceed the 65536-byte limit.\");\n\treturn structuredClone(parameters);\n}\n\nfunction indexFragments(root: RenderPlanFragment): Map<string, RenderPlanFragment> {\n\tconst result = new Map<string, RenderPlanFragment>();\n\tconst visit = (fragment: RenderPlanFragment): void => {\n\t\tresult.set(fragment.id, fragment);\n\t\tfor (const child of fragment.children ?? []) visit(child);\n\t};\n\tvisit(root);\n\treturn result;\n}\n\nexport function createMemoryOfflineCaptureStore(): OfflineCaptureStore {\n\tconst records = new Map<string, OfflineCaptureRecord>();\n\treturn {\n\t\tprotection: \"ephemeral\",\n\t\tasync put(record) {\n\t\t\tif (records.has(record.captureId)) throw new Error(`Offline capture \"${record.captureId}\" already exists.`);\n\t\t\trecords.set(record.captureId, structuredClone(record));\n\t\t},\n\t\tasync list() {\n\t\t\treturn [...records.values()].map((record) => structuredClone(record));\n\t\t},\n\t\tasync remove(captureId) {\n\t\t\trecords.delete(captureId);\n\t\t},\n\t};\n}\n\nfunction messageOf(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\ntype ReplayDisposition =\n\t| { kind: \"delivered\" }\n\t| { kind: \"not-delivered\" }\n\t| { kind: \"parked\" }\n\t| { kind: \"attention\"; status: string; certainty: \"refused\" | \"unknown\"; reason: string };\n\n/**\n * Read a returned outcome across the platform's whole status set.\n *\n * Enumerating the statuses that looked like refusals left the rest falling\n * through to \"applied\", which is how a validation failure got reported to an\n * operator as success. The switch covers every `ActionStatus`, and the default\n * arm surfaces anything else rather than counting it a success — the status\n * arrives as a string over a transport, so this is a runtime guarantee and not\n * a compile-time one.\n *\n * There is no \"already applied\" branch. A key that legitimately repeats returns\n * the original invocation without complaint, and a genuine idempotency conflict\n * means the key was reused for a materially different command, so that capture\n * was never applied and must not be discarded.\n */\nfunction dispositionOf(outcome: ExperienceInvocationOutcome | undefined): ReplayDisposition {\n\t// An adapter that returns nothing has told us nothing. Reading that as\n\t// success is the failure this whole switch exists to prevent — but so is\n\t// draining a whole queue into \"unknown\" because one adapter forgot its\n\t// return statement. Nothing was delivered as far as we can tell, so the\n\t// capture stays queued and the adapter fault surfaces as a transient.\n\tif (!outcome || typeof outcome !== \"object\") {\n\t\treturn { kind: \"not-delivered\" };\n\t}\n\tconst status = typeof outcome.status === \"string\" ? outcome.status : \"\";\n\tconst reason = outcome.reconciliation?.message\n\t\t?? outcome.reconciliation?.kind\n\t\t?? (typeof outcome.error === \"string\" ? outcome.error : status);\n\n\tswitch (status) {\n\t\tcase \"pending\":\n\t\tcase \"running\":\n\t\tcase \"completed\":\n\t\t\treturn { kind: \"delivered\" };\n\t\tcase \"waiting_for_approval\":\n\t\t\treturn { kind: \"parked\" };\n\t\tcase \"reconciliation_required\": {\n\t\t\t// Two things arrive here that are not refusals. An adapter whose\n\t\t\t// outcome could not be determined, and an authority check that\n\t\t\t// failed on a *recovery* re-execution: the lease worker re-runs the\n\t\t\t// whole authorization block, but already-succeeded adapter steps are\n\t\t\t// skipped rather than undone, so the external effect may have landed\n\t\t\t// before the binding expired. The persisted reconciliation says\n\t\t\t// which execution it was.\n\t\t\tconst recovery = outcome.reconciliation?.executionReason === \"recovery\";\n\t\t\tconst unknown = outcome.adapterReconciliation !== undefined || recovery;\n\t\t\treturn { kind: \"attention\", status, certainty: unknown ? \"unknown\" : \"refused\", reason };\n\t\t}\n\t\tcase \"blocked_by_policy\":\n\t\tcase \"validation_failed\":\n\t\t\t// Within one attempt these are decided before any effect. Across\n\t\t\t// attempts they are not: a recovery re-execution skips adapter steps\n\t\t\t// that already succeeded, then re-evaluates policy with a fresh clock\n\t\t\t// and re-parses parameters against a possibly newer action, and\n\t\t\t// either can flip after the external effect committed. Unlike the\n\t\t\t// authority path, fail() persists no execution reason, so the client\n\t\t\t// cannot tell first delivery from recovery. Erring toward \"a person\n\t\t\t// checks\" is the cheaper mistake; the precise fix is for the host to\n\t\t\t// persist executionReason on failed invocations.\n\t\t\treturn { kind: \"attention\", status, certainty: \"unknown\", reason };\n\t\tcase \"failed\":\n\t\t\t// Not the same. A handler or adapter step can fail after earlier\n\t\t\t// steps succeeded and the domain transaction committed, so telling an\n\t\t\t// operator this was refused could be as wrong as calling it applied.\n\t\t\treturn { kind: \"attention\", status, certainty: \"unknown\", reason };\n\t\tdefault:\n\t\t\t// An unfamiliar status is surfaced rather than counted as success.\n\t\t\treturn { kind: \"attention\", status: status || \"unknown\", certainty: \"unknown\", reason };\n\t}\n}\n\n/**\n * A thrown failure the platform will never accept on a later attempt.\n *\n * Deliberately narrow, and matched on what the gateway and runtime actually\n * throw. Anything else is transient, because keeping a capture costs a retry\n * and discarding one costs the operator their work.\n */\ntype ThrownReplayFailure = \"transient\" | \"settled-refused\" | \"settled-unknown\";\n\n/**\n * An idempotency conflict is settled — retrying yields the same conflict\n * forever — but which way depends on *why* the key conflicted. The host lists\n * the reasons after the colon. `parameters` or `actor` means the key was reused\n * for a materially different command, so this capture was never applied.\n * `authority_binding` or `action_version` means the *original* invocation\n * exists and may well have applied: the capture-authority adapter mints a fresh\n * binding on every attempt, so a first attempt whose response was lost\n * conflicts with itself on the second. Deleting that and calling it refused is\n * exactly the wrong answer, so it is surfaced as unknown instead.\n */\nfunction classifyThrownReplayFailure(error: unknown): ThrownReplayFailure {\n\tconst message = messageOf(error);\n\tconst conflict = /conflicts with invocation [^:]+:\\s*(.+)$/i.exec(message);\n\tif (conflict) {\n\t\tconst reasons = new Set(conflict[1]!.split(\",\").map((reason) => reason.trim()));\n\t\tconst neverApplied = reasons.has(\"parameters\") || reasons.has(\"actor\");\n\t\treturn neverApplied ? \"settled-refused\" : \"settled-unknown\";\n\t}\n\treturn isSettledReplayFailure(error) ? \"settled-refused\" : \"transient\";\n}\n\nfunction isSettledReplayFailure(error: unknown): boolean {\n\treturn [\n\t\t// The action was never offline-eligible.\n\t\t/not eligible for offline replay/i,\n\t\t// The plan no longer grants, no longer exists, or was never this actor's.\n\t\t/is not granted by the render plan/i,\n\t\t/render plan was not found/i,\n\t\t/render plan has expired/i,\n\t\t/scope does not match/i,\n\t\t/belongs to a different application assembly/i,\n\t\t// A capture only gets older, so an age refusal can never come good. This\n\t\t// one is independent of the capture TTL, so nothing else retires it.\n\t\t/capturedAt is older than/i,\n\t\t// Malformed beyond repair: passes the grammar, names no real instant.\n\t\t/capturedAt is not a parseable instant/i,\n\t\t/capturedAt must be an RFC 3339/i,\n\t\t// Deliberately absent: a capture dated in the future comes good as real\n\t\t// time advances; a gateway missing its replay window is a deployment\n\t\t// mistake somebody will fix; and an action-version mismatch fires\n\t\t// transiently mid-rollout when a client's plan is newer than the pod it\n\t\t// reached. Discarding any of them would destroy work a clock correction,\n\t\t// a config change, or the rollout finishing would have replayed.\n\t].some((pattern) => pattern.test(messageOf(error)));\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabricorg/experience-runtime",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Server-issued scoped render plans and browser-safe experience sessions.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -75,7 +75,7 @@
75
75
  "@fabricorg/platform": "^1.3.0"
76
76
  },
77
77
  "dependencies": {
78
- "@fabricorg/sdui-release": "^0.6.0"
78
+ "@fabricorg/sdui-release": "^0.7.0"
79
79
  },
80
80
  "peerDependencies": {
81
81
  "@fabricorg/platform": "^1.3.0"