@fabricorg/experience-runtime 0.2.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,82 @@
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
+
39
+ ## 0.3.0
40
+
41
+ ### Minor Changes
42
+
43
+ - 4780974: Close what a sixth review found in the previously unreviewed fix batch, and disclose a breaking change
44
+ that batch shipped under a minor.
45
+
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.
54
+
55
+ **Offline replay.** An authority refusal on a _recovery_ re-execution lands after adapter steps
56
+ already committed, since the lease worker re-runs authorization but skips succeeded steps rather
57
+ than undoing them; it is now reported as `unknown`, not `refused`, using the `executionReason` the
58
+ platform persists. A store that throws mid-replay now degrades one record instead of rejecting the
59
+ batch and losing every entry already removed. An idempotency conflict is settled — the key was reused
60
+ for a different command and retrying yields the same conflict forever — so it reaches a person rather
61
+ than looping. An action-version mismatch is transient, since it fires mid-rollout. An adapter
62
+ returning nothing keeps the capture queued rather than draining the queue into `unknown`.
63
+
64
+ **Release validation.** A grant that no supplied capability publishes now fails validation instead of
65
+ dying at promotion with "route missing"; ambiguity was scoped to granted references and had covered
66
+ only the two-or-more case. A contract supplied twice yields exactly one finding — the previous fix
67
+ still reported each of its granted views as declared twice, and the changelog claimed otherwise. An
68
+ intent whose action the contract does not declare no longer counts toward ambiguity, matching what
69
+ promotion resolves.
70
+
71
+ **Ports.** `PORT_ID` accepts any segment shape a manifest permits after the first dot, so every
72
+ namespaced name a capability can declare is a name a port can define. Dotless names remain
73
+ undefinable by design.
74
+
75
+ ### Patch Changes
76
+
77
+ - Updated dependencies [4780974]
78
+ - @fabricorg/sdui-release@0.6.0
79
+
3
80
  ## 0.2.0
4
81
 
5
82
  ### Minor Changes
@@ -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
@@ -65,11 +65,12 @@ function createOfflineCaptureQueue(options) {
65
65
  const parked = [];
66
66
  const expired = [];
67
67
  const reconciled = [];
68
- for (const record of await options.store.list()) {
68
+ const storeFailures = [];
69
+ const replayOne = async (record) => {
69
70
  if (Date.parse(record.expiresAt) <= now().getTime()) {
70
71
  await options.store.remove(record.captureId);
71
72
  expired.push(record);
72
- continue;
73
+ return;
73
74
  }
74
75
  const request = {
75
76
  planId: record.planId,
@@ -80,34 +81,53 @@ function createOfflineCaptureQueue(options) {
80
81
  executionReason: "offline_replay",
81
82
  capturedAt: record.capturedAt
82
83
  };
84
+ let outcome;
83
85
  try {
84
- const outcome = await options.gateway.replay(request);
85
- await options.store.remove(record.captureId);
86
- const disposition = dispositionOf(outcome);
87
- const actionInvocationId = typeof outcome?.actionInvocationId === "string" ? outcome.actionInvocationId : void 0;
88
- if (disposition.kind === "attention") {
89
- reconciled.push({
90
- record,
91
- status: disposition.status,
92
- ...actionInvocationId === void 0 ? {} : { actionInvocationId },
93
- certainty: disposition.certainty,
94
- reason: disposition.reason
95
- });
96
- } else if (disposition.kind === "parked") {
97
- parked.push({ record, ...actionInvocationId === void 0 ? {} : { actionInvocationId } });
98
- } else {
99
- completed += 1;
100
- }
86
+ outcome = await options.gateway.replay(request);
101
87
  } catch (error) {
102
- if (isSettledReplayFailure(error)) {
103
- await options.store.remove(record.captureId);
104
- reconciled.push({ record, status: "not_delivered", certainty: "refused", reason: messageOf(error) });
105
- } else {
88
+ const thrown = classifyThrownReplayFailure(error);
89
+ if (thrown === "transient") {
106
90
  failed += 1;
91
+ return;
107
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
+ });
100
+ return;
101
+ }
102
+ const disposition = dispositionOf(outcome);
103
+ if (disposition.kind === "not-delivered") {
104
+ failed += 1;
105
+ return;
106
+ }
107
+ await options.store.remove(record.captureId);
108
+ const actionInvocationId = typeof outcome?.actionInvocationId === "string" ? outcome.actionInvocationId : void 0;
109
+ if (disposition.kind === "attention") {
110
+ reconciled.push({
111
+ record,
112
+ status: disposition.status,
113
+ ...actionInvocationId === void 0 ? {} : { actionInvocationId },
114
+ certainty: disposition.certainty,
115
+ reason: disposition.reason
116
+ });
117
+ } else if (disposition.kind === "parked") {
118
+ parked.push({ record, ...actionInvocationId === void 0 ? {} : { actionInvocationId } });
119
+ } else {
120
+ completed += 1;
121
+ }
122
+ };
123
+ for (const record of await options.store.list()) {
124
+ try {
125
+ await replayOne(record);
126
+ } catch (error) {
127
+ storeFailures.push({ record, reason: messageOf(error) });
108
128
  }
109
129
  }
110
- return { completed, parked, failed, expired, reconciled };
130
+ return { completed, parked, failed, expired, reconciled, storeFailures };
111
131
  }
112
132
  };
113
133
  }
@@ -166,12 +186,7 @@ function messageOf(error) {
166
186
  }
167
187
  function dispositionOf(outcome) {
168
188
  if (!outcome || typeof outcome !== "object") {
169
- return {
170
- kind: "attention",
171
- status: "no_outcome",
172
- certainty: "unknown",
173
- reason: "the replay adapter returned no outcome, so whether the action was applied is unknown"
174
- };
189
+ return { kind: "not-delivered" };
175
190
  }
176
191
  const status = typeof outcome.status === "string" ? outcome.status : "";
177
192
  const reason = outcome.reconciliation?.message ?? outcome.reconciliation?.kind ?? (typeof outcome.error === "string" ? outcome.error : status);
@@ -182,31 +197,36 @@ function dispositionOf(outcome) {
182
197
  return { kind: "delivered" };
183
198
  case "waiting_for_approval":
184
199
  return { kind: "parked" };
185
- case "reconciliation_required":
186
- return {
187
- kind: "attention",
188
- status,
189
- certainty: outcome.adapterReconciliation === void 0 ? "refused" : "unknown",
190
- reason
191
- };
200
+ case "reconciliation_required": {
201
+ const recovery = outcome.reconciliation?.executionReason === "recovery";
202
+ const unknown = outcome.adapterReconciliation !== void 0 || recovery;
203
+ return { kind: "attention", status, certainty: unknown ? "unknown" : "refused", reason };
204
+ }
192
205
  case "blocked_by_policy":
193
206
  case "validation_failed":
194
- return { kind: "attention", status, certainty: "refused", reason };
207
+ return { kind: "attention", status, certainty: "unknown", reason };
195
208
  case "failed":
196
209
  return { kind: "attention", status, certainty: "unknown", reason };
197
210
  default:
198
211
  return { kind: "attention", status: status || "unknown", certainty: "unknown", reason };
199
212
  }
200
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
+ }
201
224
  function isSettledReplayFailure(error) {
202
225
  return [
203
226
  // The action was never offline-eligible.
204
227
  /not eligible for offline replay/i,
205
228
  // The plan no longer grants, no longer exists, or was never this actor's.
206
229
  /is not granted by the render plan/i,
207
- // A plan minted against a superseded deployment. Same class as the
208
- // assembly mismatch on the line below it, and equally permanent.
209
- /does not match the registered action version/i,
210
230
  /render plan was not found/i,
211
231
  /render plan has expired/i,
212
232
  /scope does not match/i,
@@ -218,9 +238,11 @@ function isSettledReplayFailure(error) {
218
238
  /capturedAt is not a parseable instant/i,
219
239
  /capturedAt must be an RFC 3339/i
220
240
  // Deliberately absent: a capture dated in the future comes good as real
221
- // time advances, and a gateway missing its replay window is a
222
- // deployment mistake somebody will fix. Discarding either would destroy
223
- // work that a clock correction or a config change would have replayed.
241
+ // time advances; a gateway missing its replay window is a deployment
242
+ // mistake somebody will fix; and an action-version mismatch fires
243
+ // transiently mid-rollout when a client's plan is newer than the pod it
244
+ // reached. Discarding any of them would destroy work a clock correction,
245
+ // a config change, or the rollout finishing would have replayed.
224
246
  ].some((pattern) => pattern.test(messageOf(error)));
225
247
  }
226
248
  // Annotate the CommonJS export names for ESM import in node:
@@ -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}\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\tfor (const record of await options.store.list()) {\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\tcontinue;\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\t\t\t\ttry {\n\t\t\t\t\tconst outcome = await options.gateway.replay!(request);\n\n\t\t\t\t\t// Anything returned means the platform durably holds the\n\t\t\t\t\t// invocation, so the capture has done its job either way and\n\t\t\t\t\t// leaves the queue. What differs is whether a person needs to\n\t\t\t\t\t// hear about it, which is decided over the whole status set\n\t\t\t\t\t// rather than the two that were obviously refusals.\n\t\t\t\t\tawait options.store.remove(record.captureId);\n\t\t\t\t\tconst disposition = dispositionOf(outcome);\n\t\t\t\t\tconst actionInvocationId = typeof outcome?.actionInvocationId === \"string\"\n\t\t\t\t\t\t? outcome.actionInvocationId\n\t\t\t\t\t\t: undefined;\n\t\t\t\t\tif (disposition.kind === \"attention\") {\n\t\t\t\t\t\treconciled.push({\n\t\t\t\t\t\t\trecord,\n\t\t\t\t\t\t\tstatus: disposition.status,\n\t\t\t\t\t\t\t...(actionInvocationId === undefined ? {} : { actionInvocationId }),\n\t\t\t\t\t\t\tcertainty: disposition.certainty,\n\t\t\t\t\t\t\treason: disposition.reason,\n\t\t\t\t\t\t});\n\t\t\t\t\t} else if (disposition.kind === \"parked\") {\n\t\t\t\t\t\tparked.push({ record, ...(actionInvocationId === undefined ? {} : { actionInvocationId }) });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcompleted += 1;\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Only genuine throws reach here: the plan expired, the\n\t\t\t\t\t// scope no longer matches, the event is not offline-eligible.\n\t\t\t\t\t// Those are settled, and retrying one only delays the moment\n\t\t\t\t\t// somebody is told. Anything unrecognised stays transient, so\n\t\t\t\t\t// an 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\t// Never delivered, and never will be. The effect certainly\n\t\t\t\t\t\t// did not happen, which is more than can be said for a\n\t\t\t\t\t\t// delivery whose outcome came back unknown.\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}\n\t\t\t}\n\t\t\treturn { completed, parked, failed, expired, reconciled };\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: \"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, and an absent\n\t// outcome deserves at least the suspicion an empty one gets.\n\tif (!outcome || typeof outcome !== \"object\") {\n\t\treturn {\n\t\t\tkind: \"attention\",\n\t\t\tstatus: \"no_outcome\",\n\t\t\tcertainty: \"unknown\",\n\t\t\treason: \"the replay adapter returned no outcome, so whether the action was applied is unknown\",\n\t\t};\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// An adapter whose outcome could not be determined also arrives here.\n\t\t\t// The external effect may well have happened, so calling it refused\n\t\t\t// would be as wrong as calling it applied.\n\t\t\treturn {\n\t\t\t\tkind: \"attention\",\n\t\t\t\tstatus,\n\t\t\t\tcertainty: outcome.adapterReconciliation === undefined ? \"refused\" : \"unknown\",\n\t\t\t\treason,\n\t\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// A plan minted against a superseded deployment. Same class as the\n\t\t// assembly mismatch on the line below it, and equally permanent.\n\t\t/does not match the registered action version/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, and a gateway missing its replay window is a\n\t\t// deployment mistake somebody will fix. Discarding either would destroy\n\t\t// work that a clock correction or a config change 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":[]}
@@ -68,6 +68,18 @@ interface OfflineReplaySummary {
68
68
  * succeed, since retrying one only delays the moment somebody is told.
69
69
  */
70
70
  reconciled: OfflineReconciliation[];
71
+ /**
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.
76
+ */
77
+ storeFailures: OfflineStoreFailure[];
78
+ }
79
+ /** A record the capture store could not act on. */
80
+ interface OfflineStoreFailure {
81
+ record: OfflineCaptureRecord;
82
+ reason: string;
71
83
  }
72
84
  interface OfflineCaptureQueue {
73
85
  capture(fragmentId: string, eventName: string, parameters: Record<string, unknown>, idempotencyKey: string): Promise<OfflineCaptureRecord>;
@@ -84,4 +96,4 @@ declare function createOfflineCaptureQueue(options: {
84
96
  }): OfflineCaptureQueue;
85
97
  declare function createMemoryOfflineCaptureStore(): OfflineCaptureStore;
86
98
 
87
- export { type OfflineCaptureQueue, type OfflineCaptureRecord, type OfflineCaptureStore, type OfflineParked, type OfflineReconciliation, type OfflineReplaySummary, createMemoryOfflineCaptureStore, createOfflineCaptureQueue };
99
+ export { type OfflineCaptureQueue, type OfflineCaptureRecord, type OfflineCaptureStore, type OfflineParked, type OfflineReconciliation, type OfflineReplaySummary, type OfflineStoreFailure, createMemoryOfflineCaptureStore, createOfflineCaptureQueue };
package/dist/offline.d.ts CHANGED
@@ -68,6 +68,18 @@ interface OfflineReplaySummary {
68
68
  * succeed, since retrying one only delays the moment somebody is told.
69
69
  */
70
70
  reconciled: OfflineReconciliation[];
71
+ /**
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.
76
+ */
77
+ storeFailures: OfflineStoreFailure[];
78
+ }
79
+ /** A record the capture store could not act on. */
80
+ interface OfflineStoreFailure {
81
+ record: OfflineCaptureRecord;
82
+ reason: string;
71
83
  }
72
84
  interface OfflineCaptureQueue {
73
85
  capture(fragmentId: string, eventName: string, parameters: Record<string, unknown>, idempotencyKey: string): Promise<OfflineCaptureRecord>;
@@ -84,4 +96,4 @@ declare function createOfflineCaptureQueue(options: {
84
96
  }): OfflineCaptureQueue;
85
97
  declare function createMemoryOfflineCaptureStore(): OfflineCaptureStore;
86
98
 
87
- export { type OfflineCaptureQueue, type OfflineCaptureRecord, type OfflineCaptureStore, type OfflineParked, type OfflineReconciliation, type OfflineReplaySummary, createMemoryOfflineCaptureStore, createOfflineCaptureQueue };
99
+ export { type OfflineCaptureQueue, type OfflineCaptureRecord, type OfflineCaptureStore, type OfflineParked, type OfflineReconciliation, type OfflineReplaySummary, type OfflineStoreFailure, createMemoryOfflineCaptureStore, createOfflineCaptureQueue };
package/dist/offline.js CHANGED
@@ -40,11 +40,12 @@ function createOfflineCaptureQueue(options) {
40
40
  const parked = [];
41
41
  const expired = [];
42
42
  const reconciled = [];
43
- for (const record of await options.store.list()) {
43
+ const storeFailures = [];
44
+ const replayOne = async (record) => {
44
45
  if (Date.parse(record.expiresAt) <= now().getTime()) {
45
46
  await options.store.remove(record.captureId);
46
47
  expired.push(record);
47
- continue;
48
+ return;
48
49
  }
49
50
  const request = {
50
51
  planId: record.planId,
@@ -55,34 +56,53 @@ function createOfflineCaptureQueue(options) {
55
56
  executionReason: "offline_replay",
56
57
  capturedAt: record.capturedAt
57
58
  };
59
+ let outcome;
58
60
  try {
59
- const outcome = await options.gateway.replay(request);
60
- await options.store.remove(record.captureId);
61
- const disposition = dispositionOf(outcome);
62
- const actionInvocationId = typeof outcome?.actionInvocationId === "string" ? outcome.actionInvocationId : void 0;
63
- if (disposition.kind === "attention") {
64
- reconciled.push({
65
- record,
66
- status: disposition.status,
67
- ...actionInvocationId === void 0 ? {} : { actionInvocationId },
68
- certainty: disposition.certainty,
69
- reason: disposition.reason
70
- });
71
- } else if (disposition.kind === "parked") {
72
- parked.push({ record, ...actionInvocationId === void 0 ? {} : { actionInvocationId } });
73
- } else {
74
- completed += 1;
75
- }
61
+ outcome = await options.gateway.replay(request);
76
62
  } catch (error) {
77
- if (isSettledReplayFailure(error)) {
78
- await options.store.remove(record.captureId);
79
- reconciled.push({ record, status: "not_delivered", certainty: "refused", reason: messageOf(error) });
80
- } else {
63
+ const thrown = classifyThrownReplayFailure(error);
64
+ if (thrown === "transient") {
81
65
  failed += 1;
66
+ return;
82
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
+ });
75
+ return;
76
+ }
77
+ const disposition = dispositionOf(outcome);
78
+ if (disposition.kind === "not-delivered") {
79
+ failed += 1;
80
+ return;
81
+ }
82
+ await options.store.remove(record.captureId);
83
+ const actionInvocationId = typeof outcome?.actionInvocationId === "string" ? outcome.actionInvocationId : void 0;
84
+ if (disposition.kind === "attention") {
85
+ reconciled.push({
86
+ record,
87
+ status: disposition.status,
88
+ ...actionInvocationId === void 0 ? {} : { actionInvocationId },
89
+ certainty: disposition.certainty,
90
+ reason: disposition.reason
91
+ });
92
+ } else if (disposition.kind === "parked") {
93
+ parked.push({ record, ...actionInvocationId === void 0 ? {} : { actionInvocationId } });
94
+ } else {
95
+ completed += 1;
96
+ }
97
+ };
98
+ for (const record of await options.store.list()) {
99
+ try {
100
+ await replayOne(record);
101
+ } catch (error) {
102
+ storeFailures.push({ record, reason: messageOf(error) });
83
103
  }
84
104
  }
85
- return { completed, parked, failed, expired, reconciled };
105
+ return { completed, parked, failed, expired, reconciled, storeFailures };
86
106
  }
87
107
  };
88
108
  }
@@ -141,12 +161,7 @@ function messageOf(error) {
141
161
  }
142
162
  function dispositionOf(outcome) {
143
163
  if (!outcome || typeof outcome !== "object") {
144
- return {
145
- kind: "attention",
146
- status: "no_outcome",
147
- certainty: "unknown",
148
- reason: "the replay adapter returned no outcome, so whether the action was applied is unknown"
149
- };
164
+ return { kind: "not-delivered" };
150
165
  }
151
166
  const status = typeof outcome.status === "string" ? outcome.status : "";
152
167
  const reason = outcome.reconciliation?.message ?? outcome.reconciliation?.kind ?? (typeof outcome.error === "string" ? outcome.error : status);
@@ -157,31 +172,36 @@ function dispositionOf(outcome) {
157
172
  return { kind: "delivered" };
158
173
  case "waiting_for_approval":
159
174
  return { kind: "parked" };
160
- case "reconciliation_required":
161
- return {
162
- kind: "attention",
163
- status,
164
- certainty: outcome.adapterReconciliation === void 0 ? "refused" : "unknown",
165
- reason
166
- };
175
+ case "reconciliation_required": {
176
+ const recovery = outcome.reconciliation?.executionReason === "recovery";
177
+ const unknown = outcome.adapterReconciliation !== void 0 || recovery;
178
+ return { kind: "attention", status, certainty: unknown ? "unknown" : "refused", reason };
179
+ }
167
180
  case "blocked_by_policy":
168
181
  case "validation_failed":
169
- return { kind: "attention", status, certainty: "refused", reason };
182
+ return { kind: "attention", status, certainty: "unknown", reason };
170
183
  case "failed":
171
184
  return { kind: "attention", status, certainty: "unknown", reason };
172
185
  default:
173
186
  return { kind: "attention", status: status || "unknown", certainty: "unknown", reason };
174
187
  }
175
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
+ }
176
199
  function isSettledReplayFailure(error) {
177
200
  return [
178
201
  // The action was never offline-eligible.
179
202
  /not eligible for offline replay/i,
180
203
  // The plan no longer grants, no longer exists, or was never this actor's.
181
204
  /is not granted by the render plan/i,
182
- // A plan minted against a superseded deployment. Same class as the
183
- // assembly mismatch on the line below it, and equally permanent.
184
- /does not match the registered action version/i,
185
205
  /render plan was not found/i,
186
206
  /render plan has expired/i,
187
207
  /scope does not match/i,
@@ -193,9 +213,11 @@ function isSettledReplayFailure(error) {
193
213
  /capturedAt is not a parseable instant/i,
194
214
  /capturedAt must be an RFC 3339/i
195
215
  // Deliberately absent: a capture dated in the future comes good as real
196
- // time advances, and a gateway missing its replay window is a
197
- // deployment mistake somebody will fix. Discarding either would destroy
198
- // work that a clock correction or a config change would have replayed.
216
+ // time advances; a gateway missing its replay window is a deployment
217
+ // mistake somebody will fix; and an action-version mismatch fires
218
+ // transiently mid-rollout when a client's plan is newer than the pod it
219
+ // reached. Discarding any of them would destroy work a clock correction,
220
+ // a config change, or the rollout finishing would have replayed.
199
221
  ].some((pattern) => pattern.test(messageOf(error)));
200
222
  }
201
223
  export {
@@ -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}\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\tfor (const record of await options.store.list()) {\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\tcontinue;\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\t\t\t\ttry {\n\t\t\t\t\tconst outcome = await options.gateway.replay!(request);\n\n\t\t\t\t\t// Anything returned means the platform durably holds the\n\t\t\t\t\t// invocation, so the capture has done its job either way and\n\t\t\t\t\t// leaves the queue. What differs is whether a person needs to\n\t\t\t\t\t// hear about it, which is decided over the whole status set\n\t\t\t\t\t// rather than the two that were obviously refusals.\n\t\t\t\t\tawait options.store.remove(record.captureId);\n\t\t\t\t\tconst disposition = dispositionOf(outcome);\n\t\t\t\t\tconst actionInvocationId = typeof outcome?.actionInvocationId === \"string\"\n\t\t\t\t\t\t? outcome.actionInvocationId\n\t\t\t\t\t\t: undefined;\n\t\t\t\t\tif (disposition.kind === \"attention\") {\n\t\t\t\t\t\treconciled.push({\n\t\t\t\t\t\t\trecord,\n\t\t\t\t\t\t\tstatus: disposition.status,\n\t\t\t\t\t\t\t...(actionInvocationId === undefined ? {} : { actionInvocationId }),\n\t\t\t\t\t\t\tcertainty: disposition.certainty,\n\t\t\t\t\t\t\treason: disposition.reason,\n\t\t\t\t\t\t});\n\t\t\t\t\t} else if (disposition.kind === \"parked\") {\n\t\t\t\t\t\tparked.push({ record, ...(actionInvocationId === undefined ? {} : { actionInvocationId }) });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcompleted += 1;\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Only genuine throws reach here: the plan expired, the\n\t\t\t\t\t// scope no longer matches, the event is not offline-eligible.\n\t\t\t\t\t// Those are settled, and retrying one only delays the moment\n\t\t\t\t\t// somebody is told. Anything unrecognised stays transient, so\n\t\t\t\t\t// an 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\t// Never delivered, and never will be. The effect certainly\n\t\t\t\t\t\t// did not happen, which is more than can be said for a\n\t\t\t\t\t\t// delivery whose outcome came back unknown.\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}\n\t\t\t}\n\t\t\treturn { completed, parked, failed, expired, reconciled };\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: \"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, and an absent\n\t// outcome deserves at least the suspicion an empty one gets.\n\tif (!outcome || typeof outcome !== \"object\") {\n\t\treturn {\n\t\t\tkind: \"attention\",\n\t\t\tstatus: \"no_outcome\",\n\t\t\tcertainty: \"unknown\",\n\t\t\treason: \"the replay adapter returned no outcome, so whether the action was applied is unknown\",\n\t\t};\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// An adapter whose outcome could not be determined also arrives here.\n\t\t\t// The external effect may well have happened, so calling it refused\n\t\t\t// would be as wrong as calling it applied.\n\t\t\treturn {\n\t\t\t\tkind: \"attention\",\n\t\t\t\tstatus,\n\t\t\t\tcertainty: outcome.adapterReconciliation === undefined ? \"refused\" : \"unknown\",\n\t\t\t\treason,\n\t\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// A plan minted against a superseded deployment. Same class as the\n\t\t// assembly mismatch on the line below it, and equally permanent.\n\t\t/does not match the registered action version/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, and a gateway missing its replay window is a\n\t\t// deployment mistake somebody will fix. Discarding either would destroy\n\t\t// work that a clock correction or a config change 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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabricorg/experience-runtime",
3
- "version": "0.2.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.5.0"
78
+ "@fabricorg/sdui-release": "^0.7.0"
79
79
  },
80
80
  "peerDependencies": {
81
81
  "@fabricorg/platform": "^1.3.0"