@fabricorg/experience-runtime 0.4.8 → 0.4.10
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 +33 -0
- package/dist/client.cjs +120 -137
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +19 -20
- package/dist/client.d.ts +19 -20
- package/dist/client.js +117 -110
- package/dist/client.js.map +1 -1
- package/dist/contracts.cjs +0 -19
- package/dist/contracts.d.cts +71 -71
- package/dist/contracts.d.ts +71 -71
- package/dist/contracts.js +1 -1
- package/dist/offline.cjs +307 -270
- package/dist/offline.cjs.map +1 -1
- package/dist/offline.d.cts +87 -88
- package/dist/offline.d.ts +87 -88
- package/dist/offline.js +305 -244
- package/dist/offline.js.map +1 -1
- package/dist/server.cjs +230 -225
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +88 -89
- package/dist/server.d.ts +88 -89
- package/dist/server.js +226 -200
- package/dist/server.js.map +1 -1
- package/package.json +8 -7
- package/dist/contracts.cjs.map +0 -1
- package/dist/contracts.js.map +0 -1
package/dist/offline.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../offline.ts"],"sourcesContent":["import type {\n\tExperienceGatewayClient,\n\tExperienceInvocationOutcome,\n\tExperienceReplayRequest,\n\tRenderPlanFragment,\n\tScopedRenderPlan,\n} from \"./contracts\";\n\nexport interface OfflineCaptureRecord {\n\tcaptureId: string;\n\tplanId: string;\n\treleaseDigest: string;\n\tfragmentId: string;\n\teventName: string;\n\tparameters: Record<string, unknown>;\n\tidempotencyKey: string;\n\tcapturedAt: string;\n\texpiresAt: string;\n}\n\nexport interface OfflineCaptureStore {\n\tprotection: \"encrypted-at-rest\" | \"ephemeral\";\n\tput(record: OfflineCaptureRecord): Promise<void>;\n\tlist(): Promise<readonly OfflineCaptureRecord[]>;\n\tremove(captureId: string): Promise<void>;\n}\n\n/** A delivered capture whose outcome a person needs to see. */\nexport interface OfflineReconciliation {\n\trecord: OfflineCaptureRecord;\n\t/** The status the platform returned, so a caller can tell these apart. */\n\tstatus: string;\n\t/**\n\t * The invocation the platform recorded, when it got that far.\n\t *\n\t * `recordExternalReconciliation` needs it, so an outcome of `unknown` that\n\t * omits it cannot be reconciled from this summary — which is the whole\n\t * reason the entry exists.\n\t */\n\tactionInvocationId?: string;\n\t/**\n\t * Whether the effect is known not to have happened, or is simply unknown.\n\t * An adapter that timed out mid-charge is `unknown`: telling an operator it\n\t * was refused would be as wrong as telling them it succeeded.\n\t */\n\tcertainty: \"refused\" | \"unknown\";\n\treason: string;\n}\n\n/** A delivered capture waiting on a human. */\nexport interface OfflineParked {\n\trecord: OfflineCaptureRecord;\n\tactionInvocationId?: string;\n}\n\nexport interface OfflineReplaySummary {\n\t/** Delivered and progressing: the platform has the invocation. */\n\tcompleted: number;\n\t/**\n\t * Delivered and parked awaiting a human decision.\n\t *\n\t * Carries the records, because a bare count leaves nobody able to say which\n\t * action somebody now has to approve.\n\t */\n\tparked: OfflineParked[];\n\t/** Never delivered, and worth another attempt. Still queued. */\n\tfailed: number;\n\t/**\n\t * Outlived the capture TTL or the render plan before it could be sent.\n\t *\n\t * Also carries its records: a capture that expired never reached the\n\t * platform, so this is work the operator did that nobody has.\n\t */\n\texpired: OfflineCaptureRecord[];\n\t/**\n\t * Delivered, and something a person needs to see: refused, invalid, or an\n\t * outcome nobody can determine. Also covers a delivery that can never\n\t * succeed, since retrying one only delays the moment somebody is told.\n\t */\n\treconciled: OfflineReconciliation[];\n\t/**\n\t * Records the store itself failed on, mid-replay. 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.kind === \"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.status,\n\t\t\t\t\t\t...(thrown.actionInvocationId === undefined ? {} : { actionInvocationId: thrown.actionInvocationId }),\n\t\t\t\t\t\tcertainty: thrown.certainty,\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 (utf8ByteLength(serialized) > 65_536) throw new Error(\"Offline capture parameters exceed the 65536-byte limit.\");\n\treturn JSON.parse(serialized) as Record<string, unknown>;\n}\n\n/**\n * UTF-8 length without an encoder global.\n *\n * This package is the interface a client uses to reach capabilities, so it\n * assumes nothing about the host beyond JSON. A React Native runtime, a worker,\n * or an older embedded engine may not supply the encoder or clone globals, and\n * the parameters were validated as portable JSON just above, so a JSON round\n * trip is the clone and a code-point walk is the byte count.\n */\nfunction utf8ByteLength(text: string): number {\n\tlet bytes = 0;\n\tfor (const character of text) {\n\t\tconst codePoint = character.codePointAt(0)!;\n\t\tbytes += codePoint < 0x80 ? 1 : codePoint < 0x800 ? 2 : codePoint < 0x10000 ? 3 : 4;\n\t}\n\treturn bytes;\n}\n\n/** Records are validated JSON, so a round trip is a faithful clone on any host. */\nfunction cloneRecord<T>(value: T): T {\n\treturn JSON.parse(JSON.stringify(value)) as T;\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, cloneRecord(record));\n\t\t},\n\t\tasync list() {\n\t\t\treturn [...records.values()].map((record) => cloneRecord(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 =\n\t| { kind: \"transient\" }\n\t| { kind: \"settled\"; status: string; certainty: \"refused\" | \"unknown\"; actionInvocationId?: string };\n\nconst CONFLICT_REASONS: ReadonlySet<string> = new Set([\"actor\", \"authority_binding\", \"action_version\", \"parameters\"]);\n\n/**\n * An idempotency conflict is settled — retrying yields the same conflict\n * forever — but which way depends on *why* the key conflicted. `parameters`\n * or `actor` means the key was reused for a materially different command, so\n * this capture was never applied. `authority_binding` or `action_version`\n * means the *original* invocation exists and may well have applied: the\n * capture-authority adapter mints a fresh binding on every attempt, so a first\n * attempt whose response was lost conflicts with itself on the second. Deleting\n * that and calling it refused is exactly the wrong answer, so it is surfaced as\n * unknown, with the invocation it conflicted with, since that is what\n * `recordExternalReconciliation` needs.\n *\n * A conflict whose reasons cannot be read is still a conflict, so it is still\n * settled; the previous version let it fall through to transient and retried\n * it forever. Unreadable reasons resolve to `unknown`, never to `refused`.\n */\nfunction classifyThrownReplayFailure(error: unknown): ThrownReplayFailure {\n\tconst conflict = conflictOf(error);\n\tif (conflict) {\n\t\tconst readable = conflict.reasons.size > 0 && [...conflict.reasons].every((reason) => CONFLICT_REASONS.has(reason));\n\t\tconst neverApplied = readable && (conflict.reasons.has(\"parameters\") || conflict.reasons.has(\"actor\"));\n\t\tif (neverApplied) return { kind: \"settled\", status: \"not_delivered\", certainty: \"refused\" };\n\t\treturn {\n\t\t\tkind: \"settled\",\n\t\t\tstatus: \"conflict_prior_invocation\",\n\t\t\tcertainty: \"unknown\",\n\t\t\t...(conflict.existingInvocationId === undefined ? {} : { actionInvocationId: conflict.existingInvocationId }),\n\t\t};\n\t}\n\treturn isSettledReplayFailure(error)\n\t\t? { kind: \"settled\", status: \"not_delivered\", certainty: \"refused\" }\n\t\t: { kind: \"transient\" };\n}\n\n/**\n * Read an idempotency conflict off a thrown error.\n *\n * The host's `IdempotencyConflictError` carries the conflict as data, and a\n * gateway that rethrows it (or any error shaped like it) is read structurally.\n * Only when that is absent is the message parsed, and then defensively: the\n * phrase is located from the *end*, because the idempotency key is interpolated\n * before it and a key is whatever the application chose; the reasons are read\n * to the end of their own line, because a wrapped error may append a stack or\n * a detail line below. A message that names the phrase but cannot be parsed\n * further is still reported as a conflict, with no reasons.\n */\nfunction conflictOf(error: unknown): { existingInvocationId?: string; reasons: ReadonlySet<string> } | undefined {\n\tconst structural = error !== null && typeof error === \"object\"\n\t\t? (error as { conflict?: unknown }).conflict\n\t\t: undefined;\n\tif (structural !== null && typeof structural === \"object\") {\n\t\tconst { existingInvocationId, reasons } = structural as { existingInvocationId?: unknown; reasons?: unknown };\n\t\tif (Array.isArray(reasons)) {\n\t\t\treturn {\n\t\t\t\t...(typeof existingInvocationId === \"string\" && existingInvocationId ? { existingInvocationId } : {}),\n\t\t\t\treasons: new Set(reasons.filter((reason): reason is string => typeof reason === \"string\")),\n\t\t\t};\n\t\t}\n\t}\n\tconst message = messageOf(error);\n\tconst phrase = \"conflicts with invocation\";\n\tconst at = message.toLowerCase().lastIndexOf(phrase);\n\tif (at < 0) return undefined;\n\tconst tail = /^\\s+([^\\s:]+):[ \\t]*([^\\n]*)/.exec(message.slice(at + phrase.length));\n\tif (!tail) return { reasons: new Set() };\n\treturn {\n\t\texistingInvocationId: tail[1]!,\n\t\treasons: new Set(tail[2]!.split(\",\").map((reason) => reason.trim()).filter(Boolean)),\n\t};\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":[]}
|
|
1
|
+
{"version":3,"file":"offline.cjs","names":[],"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.kind === \"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.status,\n\t\t\t\t\t\t...(thrown.actionInvocationId === undefined ? {} : { actionInvocationId: thrown.actionInvocationId }),\n\t\t\t\t\t\tcertainty: thrown.certainty,\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 (utf8ByteLength(serialized) > 65_536) throw new Error(\"Offline capture parameters exceed the 65536-byte limit.\");\n\treturn JSON.parse(serialized) as Record<string, unknown>;\n}\n\n/**\n * UTF-8 length without an encoder global.\n *\n * This package is the interface a client uses to reach capabilities, so it\n * assumes nothing about the host beyond JSON. A React Native runtime, a worker,\n * or an older embedded engine may not supply the encoder or clone globals, and\n * the parameters were validated as portable JSON just above, so a JSON round\n * trip is the clone and a code-point walk is the byte count.\n */\nfunction utf8ByteLength(text: string): number {\n\tlet bytes = 0;\n\tfor (const character of text) {\n\t\tconst codePoint = character.codePointAt(0)!;\n\t\tbytes += codePoint < 0x80 ? 1 : codePoint < 0x800 ? 2 : codePoint < 0x10000 ? 3 : 4;\n\t}\n\treturn bytes;\n}\n\n/** Records are validated JSON, so a round trip is a faithful clone on any host. */\nfunction cloneRecord<T>(value: T): T {\n\treturn JSON.parse(JSON.stringify(value)) as T;\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, cloneRecord(record));\n\t\t},\n\t\tasync list() {\n\t\t\treturn [...records.values()].map((record) => cloneRecord(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 =\n\t| { kind: \"transient\" }\n\t| { kind: \"settled\"; status: string; certainty: \"refused\" | \"unknown\"; actionInvocationId?: string };\n\nconst CONFLICT_REASONS: ReadonlySet<string> = new Set([\"actor\", \"authority_binding\", \"action_version\", \"parameters\"]);\n\n/**\n * An idempotency conflict is settled — retrying yields the same conflict\n * forever — but which way depends on *why* the key conflicted. `parameters`\n * or `actor` means the key was reused for a materially different command, so\n * this capture was never applied. `authority_binding` or `action_version`\n * means the *original* invocation exists and may well have applied: the\n * capture-authority adapter mints a fresh binding on every attempt, so a first\n * attempt whose response was lost conflicts with itself on the second. Deleting\n * that and calling it refused is exactly the wrong answer, so it is surfaced as\n * unknown, with the invocation it conflicted with, since that is what\n * `recordExternalReconciliation` needs.\n *\n * A conflict whose reasons cannot be read is still a conflict, so it is still\n * settled; the previous version let it fall through to transient and retried\n * it forever. Unreadable reasons resolve to `unknown`, never to `refused`.\n */\nfunction classifyThrownReplayFailure(error: unknown): ThrownReplayFailure {\n\tconst conflict = conflictOf(error);\n\tif (conflict) {\n\t\tconst readable = conflict.reasons.size > 0 && [...conflict.reasons].every((reason) => CONFLICT_REASONS.has(reason));\n\t\tconst neverApplied = readable && (conflict.reasons.has(\"parameters\") || conflict.reasons.has(\"actor\"));\n\t\tif (neverApplied) return { kind: \"settled\", status: \"not_delivered\", certainty: \"refused\" };\n\t\treturn {\n\t\t\tkind: \"settled\",\n\t\t\tstatus: \"conflict_prior_invocation\",\n\t\t\tcertainty: \"unknown\",\n\t\t\t...(conflict.existingInvocationId === undefined ? {} : { actionInvocationId: conflict.existingInvocationId }),\n\t\t};\n\t}\n\treturn isSettledReplayFailure(error)\n\t\t? { kind: \"settled\", status: \"not_delivered\", certainty: \"refused\" }\n\t\t: { kind: \"transient\" };\n}\n\n/**\n * Read an idempotency conflict off a thrown error.\n *\n * The host's `IdempotencyConflictError` carries the conflict as data, and a\n * gateway that rethrows it (or any error shaped like it) is read structurally.\n * Only when that is absent is the message parsed, and then defensively: the\n * phrase is located from the *end*, because the idempotency key is interpolated\n * before it and a key is whatever the application chose; the reasons are read\n * to the end of their own line, because a wrapped error may append a stack or\n * a detail line below. A message that names the phrase but cannot be parsed\n * further is still reported as a conflict, with no reasons.\n */\nfunction conflictOf(error: unknown): { existingInvocationId?: string; reasons: ReadonlySet<string> } | undefined {\n\tconst structural = error !== null && typeof error === \"object\"\n\t\t? (error as { conflict?: unknown }).conflict\n\t\t: undefined;\n\tif (structural !== null && typeof structural === \"object\") {\n\t\tconst { existingInvocationId, reasons } = structural as { existingInvocationId?: unknown; reasons?: unknown };\n\t\tif (Array.isArray(reasons)) {\n\t\t\treturn {\n\t\t\t\t...(typeof existingInvocationId === \"string\" && existingInvocationId ? { existingInvocationId } : {}),\n\t\t\t\treasons: new Set(reasons.filter((reason): reason is string => typeof reason === \"string\")),\n\t\t\t};\n\t\t}\n\t}\n\tconst message = messageOf(error);\n\tconst phrase = \"conflicts with invocation\";\n\tconst at = message.toLowerCase().lastIndexOf(phrase);\n\tif (at < 0) return undefined;\n\tconst tail = /^\\s+([^\\s:]+):[ \\t]*([^\\n]*)/.exec(message.slice(at + phrase.length));\n\tif (!tail) return { reasons: new Set() };\n\treturn {\n\t\texistingInvocationId: tail[1]!,\n\t\treasons: new Set(tail[2]!.split(\",\").map((reason) => reason.trim()).filter(Boolean)),\n\t};\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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"}
|
package/dist/offline.d.cts
CHANGED
|
@@ -1,99 +1,98 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
interface OfflineCaptureRecord {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import { ExperienceGatewayClient, ScopedRenderPlan } from "./contracts.cjs";
|
|
2
|
+
//#region offline.d.ts
|
|
3
|
+
export interface OfflineCaptureRecord {
|
|
4
|
+
captureId: string;
|
|
5
|
+
planId: string;
|
|
6
|
+
releaseDigest: string;
|
|
7
|
+
fragmentId: string;
|
|
8
|
+
eventName: string;
|
|
9
|
+
parameters: Record<string, unknown>;
|
|
10
|
+
idempotencyKey: string;
|
|
11
|
+
capturedAt: string;
|
|
12
|
+
expiresAt: string;
|
|
13
13
|
}
|
|
14
|
-
interface OfflineCaptureStore {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
export interface OfflineCaptureStore {
|
|
15
|
+
protection: "encrypted-at-rest" | "ephemeral";
|
|
16
|
+
put(record: OfflineCaptureRecord): Promise<void>;
|
|
17
|
+
list(): Promise<readonly OfflineCaptureRecord[]>;
|
|
18
|
+
remove(captureId: string): Promise<void>;
|
|
19
19
|
}
|
|
20
20
|
/** A delivered capture whose outcome a person needs to see. */
|
|
21
|
-
interface OfflineReconciliation {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
21
|
+
export interface OfflineReconciliation {
|
|
22
|
+
record: OfflineCaptureRecord;
|
|
23
|
+
/** The status the platform returned, so a caller can tell these apart. */
|
|
24
|
+
status: string;
|
|
25
|
+
/**
|
|
26
|
+
* The invocation the platform recorded, when it got that far.
|
|
27
|
+
*
|
|
28
|
+
* `recordExternalReconciliation` needs it, so an outcome of `unknown` that
|
|
29
|
+
* omits it cannot be reconciled from this summary — which is the whole
|
|
30
|
+
* reason the entry exists.
|
|
31
|
+
*/
|
|
32
|
+
actionInvocationId?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Whether the effect is known not to have happened, or is simply unknown.
|
|
35
|
+
* An adapter that timed out mid-charge is `unknown`: telling an operator it
|
|
36
|
+
* was refused would be as wrong as telling them it succeeded.
|
|
37
|
+
*/
|
|
38
|
+
certainty: "refused" | "unknown";
|
|
39
|
+
reason: string;
|
|
40
40
|
}
|
|
41
41
|
/** A delivered capture waiting on a human. */
|
|
42
|
-
interface OfflineParked {
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
export interface OfflineParked {
|
|
43
|
+
record: OfflineCaptureRecord;
|
|
44
|
+
actionInvocationId?: string;
|
|
45
45
|
}
|
|
46
|
-
interface OfflineReplaySummary {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
46
|
+
export interface OfflineReplaySummary {
|
|
47
|
+
/** Delivered and progressing: the platform has the invocation. */
|
|
48
|
+
completed: number;
|
|
49
|
+
/**
|
|
50
|
+
* Delivered and parked awaiting a human decision.
|
|
51
|
+
*
|
|
52
|
+
* Carries the records, because a bare count leaves nobody able to say which
|
|
53
|
+
* action somebody now has to approve.
|
|
54
|
+
*/
|
|
55
|
+
parked: OfflineParked[];
|
|
56
|
+
/** Never delivered, and worth another attempt. Still queued. */
|
|
57
|
+
failed: number;
|
|
58
|
+
/**
|
|
59
|
+
* Outlived the capture TTL or the render plan before it could be sent.
|
|
60
|
+
*
|
|
61
|
+
* Also carries its records: a capture that expired never reached the
|
|
62
|
+
* platform, so this is work the operator did that nobody has.
|
|
63
|
+
*/
|
|
64
|
+
expired: OfflineCaptureRecord[];
|
|
65
|
+
/**
|
|
66
|
+
* Delivered, and something a person needs to see: refused, invalid, or an
|
|
67
|
+
* outcome nobody can determine. Also covers a delivery that can never
|
|
68
|
+
* succeed, since retrying one only delays the moment somebody is told.
|
|
69
|
+
*/
|
|
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
78
|
}
|
|
79
79
|
/** A record the capture store could not act on. */
|
|
80
|
-
interface OfflineStoreFailure {
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
export interface OfflineStoreFailure {
|
|
81
|
+
record: OfflineCaptureRecord;
|
|
82
|
+
reason: string;
|
|
83
83
|
}
|
|
84
|
-
interface OfflineCaptureQueue {
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
export interface OfflineCaptureQueue {
|
|
85
|
+
capture(fragmentId: string, eventName: string, parameters: Record<string, unknown>, idempotencyKey: string): Promise<OfflineCaptureRecord>;
|
|
86
|
+
replay(): Promise<OfflineReplaySummary>;
|
|
87
87
|
}
|
|
88
|
-
declare function createOfflineCaptureQueue(options: {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
88
|
+
export declare function createOfflineCaptureQueue(options: {
|
|
89
|
+
plan: ScopedRenderPlan;
|
|
90
|
+
store: OfflineCaptureStore;
|
|
91
|
+
gateway: ExperienceGatewayClient;
|
|
92
|
+
nextCaptureId(): string;
|
|
93
|
+
now?: () => Date;
|
|
94
|
+
captureTtlMs?: number;
|
|
95
|
+
allowEphemeralStore?: boolean;
|
|
96
96
|
}): OfflineCaptureQueue;
|
|
97
|
-
declare function createMemoryOfflineCaptureStore(): OfflineCaptureStore;
|
|
98
|
-
|
|
99
|
-
export { type OfflineCaptureQueue, type OfflineCaptureRecord, type OfflineCaptureStore, type OfflineParked, type OfflineReconciliation, type OfflineReplaySummary, type OfflineStoreFailure, createMemoryOfflineCaptureStore, createOfflineCaptureQueue };
|
|
97
|
+
export declare function createMemoryOfflineCaptureStore(): OfflineCaptureStore;
|
|
98
|
+
//#endregion
|
package/dist/offline.d.ts
CHANGED
|
@@ -1,99 +1,98 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
interface OfflineCaptureRecord {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import { ExperienceGatewayClient, ScopedRenderPlan } from "./contracts.js";
|
|
2
|
+
//#region offline.d.ts
|
|
3
|
+
export interface OfflineCaptureRecord {
|
|
4
|
+
captureId: string;
|
|
5
|
+
planId: string;
|
|
6
|
+
releaseDigest: string;
|
|
7
|
+
fragmentId: string;
|
|
8
|
+
eventName: string;
|
|
9
|
+
parameters: Record<string, unknown>;
|
|
10
|
+
idempotencyKey: string;
|
|
11
|
+
capturedAt: string;
|
|
12
|
+
expiresAt: string;
|
|
13
13
|
}
|
|
14
|
-
interface OfflineCaptureStore {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
export interface OfflineCaptureStore {
|
|
15
|
+
protection: "encrypted-at-rest" | "ephemeral";
|
|
16
|
+
put(record: OfflineCaptureRecord): Promise<void>;
|
|
17
|
+
list(): Promise<readonly OfflineCaptureRecord[]>;
|
|
18
|
+
remove(captureId: string): Promise<void>;
|
|
19
19
|
}
|
|
20
20
|
/** A delivered capture whose outcome a person needs to see. */
|
|
21
|
-
interface OfflineReconciliation {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
21
|
+
export interface OfflineReconciliation {
|
|
22
|
+
record: OfflineCaptureRecord;
|
|
23
|
+
/** The status the platform returned, so a caller can tell these apart. */
|
|
24
|
+
status: string;
|
|
25
|
+
/**
|
|
26
|
+
* The invocation the platform recorded, when it got that far.
|
|
27
|
+
*
|
|
28
|
+
* `recordExternalReconciliation` needs it, so an outcome of `unknown` that
|
|
29
|
+
* omits it cannot be reconciled from this summary — which is the whole
|
|
30
|
+
* reason the entry exists.
|
|
31
|
+
*/
|
|
32
|
+
actionInvocationId?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Whether the effect is known not to have happened, or is simply unknown.
|
|
35
|
+
* An adapter that timed out mid-charge is `unknown`: telling an operator it
|
|
36
|
+
* was refused would be as wrong as telling them it succeeded.
|
|
37
|
+
*/
|
|
38
|
+
certainty: "refused" | "unknown";
|
|
39
|
+
reason: string;
|
|
40
40
|
}
|
|
41
41
|
/** A delivered capture waiting on a human. */
|
|
42
|
-
interface OfflineParked {
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
export interface OfflineParked {
|
|
43
|
+
record: OfflineCaptureRecord;
|
|
44
|
+
actionInvocationId?: string;
|
|
45
45
|
}
|
|
46
|
-
interface OfflineReplaySummary {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
46
|
+
export interface OfflineReplaySummary {
|
|
47
|
+
/** Delivered and progressing: the platform has the invocation. */
|
|
48
|
+
completed: number;
|
|
49
|
+
/**
|
|
50
|
+
* Delivered and parked awaiting a human decision.
|
|
51
|
+
*
|
|
52
|
+
* Carries the records, because a bare count leaves nobody able to say which
|
|
53
|
+
* action somebody now has to approve.
|
|
54
|
+
*/
|
|
55
|
+
parked: OfflineParked[];
|
|
56
|
+
/** Never delivered, and worth another attempt. Still queued. */
|
|
57
|
+
failed: number;
|
|
58
|
+
/**
|
|
59
|
+
* Outlived the capture TTL or the render plan before it could be sent.
|
|
60
|
+
*
|
|
61
|
+
* Also carries its records: a capture that expired never reached the
|
|
62
|
+
* platform, so this is work the operator did that nobody has.
|
|
63
|
+
*/
|
|
64
|
+
expired: OfflineCaptureRecord[];
|
|
65
|
+
/**
|
|
66
|
+
* Delivered, and something a person needs to see: refused, invalid, or an
|
|
67
|
+
* outcome nobody can determine. Also covers a delivery that can never
|
|
68
|
+
* succeed, since retrying one only delays the moment somebody is told.
|
|
69
|
+
*/
|
|
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
78
|
}
|
|
79
79
|
/** A record the capture store could not act on. */
|
|
80
|
-
interface OfflineStoreFailure {
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
export interface OfflineStoreFailure {
|
|
81
|
+
record: OfflineCaptureRecord;
|
|
82
|
+
reason: string;
|
|
83
83
|
}
|
|
84
|
-
interface OfflineCaptureQueue {
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
export interface OfflineCaptureQueue {
|
|
85
|
+
capture(fragmentId: string, eventName: string, parameters: Record<string, unknown>, idempotencyKey: string): Promise<OfflineCaptureRecord>;
|
|
86
|
+
replay(): Promise<OfflineReplaySummary>;
|
|
87
87
|
}
|
|
88
|
-
declare function createOfflineCaptureQueue(options: {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
88
|
+
export declare function createOfflineCaptureQueue(options: {
|
|
89
|
+
plan: ScopedRenderPlan;
|
|
90
|
+
store: OfflineCaptureStore;
|
|
91
|
+
gateway: ExperienceGatewayClient;
|
|
92
|
+
nextCaptureId(): string;
|
|
93
|
+
now?: () => Date;
|
|
94
|
+
captureTtlMs?: number;
|
|
95
|
+
allowEphemeralStore?: boolean;
|
|
96
96
|
}): OfflineCaptureQueue;
|
|
97
|
-
declare function createMemoryOfflineCaptureStore(): OfflineCaptureStore;
|
|
98
|
-
|
|
99
|
-
export { type OfflineCaptureQueue, type OfflineCaptureRecord, type OfflineCaptureStore, type OfflineParked, type OfflineReconciliation, type OfflineReplaySummary, type OfflineStoreFailure, createMemoryOfflineCaptureStore, createOfflineCaptureQueue };
|
|
97
|
+
export declare function createMemoryOfflineCaptureStore(): OfflineCaptureStore;
|
|
98
|
+
//#endregion
|