@fabricorg/experience-runtime 0.2.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 +131 -0
- package/LICENSE +21 -0
- package/README.md +20 -0
- package/dist/client.cjs +146 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +23 -0
- package/dist/client.d.ts +23 -0
- package/dist/client.js +119 -0
- package/dist/client.js.map +1 -0
- package/dist/contracts.cjs +19 -0
- package/dist/contracts.cjs.map +1 -0
- package/dist/contracts.d.cts +95 -0
- package/dist/contracts.d.ts +95 -0
- package/dist/contracts.js +1 -0
- package/dist/contracts.js.map +1 -0
- package/dist/offline.cjs +231 -0
- package/dist/offline.cjs.map +1 -0
- package/dist/offline.d.cts +87 -0
- package/dist/offline.d.ts +87 -0
- package/dist/offline.js +205 -0
- package/dist/offline.js.map +1 -0
- package/dist/server.cjs +244 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +100 -0
- package/dist/server.d.ts +100 -0
- package/dist/server.js +219 -0
- package/dist/server.js.map +1 -0
- package/package.json +95 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
interface ExperienceScope {
|
|
2
|
+
tenantId: string;
|
|
3
|
+
spaceId: string;
|
|
4
|
+
actorId: string;
|
|
5
|
+
actorType: string;
|
|
6
|
+
}
|
|
7
|
+
interface RenderPlanFragment {
|
|
8
|
+
id: string;
|
|
9
|
+
component?: string;
|
|
10
|
+
props?: Record<string, unknown>;
|
|
11
|
+
slot?: string;
|
|
12
|
+
events?: readonly string[];
|
|
13
|
+
offlineEvents?: readonly string[];
|
|
14
|
+
bindings?: readonly string[];
|
|
15
|
+
children?: readonly RenderPlanFragment[];
|
|
16
|
+
}
|
|
17
|
+
interface ScopedRenderPlan {
|
|
18
|
+
formatVersion: 1;
|
|
19
|
+
planId: string;
|
|
20
|
+
application: string;
|
|
21
|
+
channel: string;
|
|
22
|
+
releaseDigest: string;
|
|
23
|
+
assemblyDigest: string;
|
|
24
|
+
treeId: string;
|
|
25
|
+
root: RenderPlanFragment;
|
|
26
|
+
tokenSet: {
|
|
27
|
+
name: string;
|
|
28
|
+
version: string;
|
|
29
|
+
};
|
|
30
|
+
expiresAt: string;
|
|
31
|
+
}
|
|
32
|
+
interface ExperienceIntentRequest {
|
|
33
|
+
planId: string;
|
|
34
|
+
fragmentId: string;
|
|
35
|
+
eventName: string;
|
|
36
|
+
parameters: Record<string, unknown>;
|
|
37
|
+
idempotencyKey?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A replay of an action captured while disconnected.
|
|
41
|
+
*
|
|
42
|
+
* The marker and the capture moment travel with the request rather than by
|
|
43
|
+
* adapter convention. An adapter that forwarded a replay as an ordinary intent
|
|
44
|
+
* would skip the Host's offline eligibility and reconciliation rules, and
|
|
45
|
+
* capture-authority evidence stamped at receipt cannot truthfully attest when
|
|
46
|
+
* the operator actually acted.
|
|
47
|
+
*/
|
|
48
|
+
interface ExperienceReplayRequest extends ExperienceIntentRequest {
|
|
49
|
+
executionReason: "offline_replay";
|
|
50
|
+
/** RFC 3339 instant the action was captured on the device. */
|
|
51
|
+
capturedAt: string;
|
|
52
|
+
idempotencyKey: string;
|
|
53
|
+
}
|
|
54
|
+
interface ExperienceReadRequest {
|
|
55
|
+
planId: string;
|
|
56
|
+
fragmentId: string;
|
|
57
|
+
bindingName: string;
|
|
58
|
+
parameters?: Record<string, unknown>;
|
|
59
|
+
}
|
|
60
|
+
interface ExperienceGatewayClient {
|
|
61
|
+
invoke(request: ExperienceIntentRequest): Promise<unknown>;
|
|
62
|
+
replay?(request: ExperienceReplayRequest): Promise<ExperienceInvocationOutcome>;
|
|
63
|
+
read(request: ExperienceReadRequest): Promise<{
|
|
64
|
+
data: unknown;
|
|
65
|
+
stale?: boolean;
|
|
66
|
+
}>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* What a governed submission comes back as.
|
|
70
|
+
*
|
|
71
|
+
* The Host reports a refusal by *returning* `reconciliation_required` with the
|
|
72
|
+
* reason, not by throwing: a lapsed authority is a business outcome rather than
|
|
73
|
+
* an error. Anything reading only the exception path will count a refusal as a
|
|
74
|
+
* success.
|
|
75
|
+
*/
|
|
76
|
+
interface ExperienceInvocationOutcome {
|
|
77
|
+
status?: string;
|
|
78
|
+
reconciliation?: {
|
|
79
|
+
kind?: string;
|
|
80
|
+
message?: string;
|
|
81
|
+
};
|
|
82
|
+
/** Present when an adapter's own outcome could not be determined. */
|
|
83
|
+
adapterReconciliation?: unknown;
|
|
84
|
+
[key: string]: unknown;
|
|
85
|
+
}
|
|
86
|
+
interface ExperienceSession {
|
|
87
|
+
readonly plan: ScopedRenderPlan;
|
|
88
|
+
invoke(fragmentId: string, eventName: string, parameters: Record<string, unknown>, idempotencyKey?: string): Promise<unknown>;
|
|
89
|
+
read(fragmentId: string, bindingName: string, parameters?: Record<string, unknown>): Promise<{
|
|
90
|
+
data: unknown;
|
|
91
|
+
stale?: boolean;
|
|
92
|
+
}>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type { ExperienceGatewayClient, ExperienceIntentRequest, ExperienceInvocationOutcome, ExperienceReadRequest, ExperienceReplayRequest, ExperienceScope, ExperienceSession, RenderPlanFragment, ScopedRenderPlan };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=contracts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/offline.cjs
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// offline.ts
|
|
21
|
+
var offline_exports = {};
|
|
22
|
+
__export(offline_exports, {
|
|
23
|
+
createMemoryOfflineCaptureStore: () => createMemoryOfflineCaptureStore,
|
|
24
|
+
createOfflineCaptureQueue: () => createOfflineCaptureQueue
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(offline_exports);
|
|
27
|
+
function createOfflineCaptureQueue(options) {
|
|
28
|
+
if (options.store.protection !== "encrypted-at-rest" && !options.allowEphemeralStore) {
|
|
29
|
+
throw new Error("Production offline capture requires an encrypted-at-rest store.");
|
|
30
|
+
}
|
|
31
|
+
if (!options.gateway.replay) throw new Error("Offline capture requires a gateway replay adapter.");
|
|
32
|
+
const fragments = indexFragments(options.plan.root);
|
|
33
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
34
|
+
return {
|
|
35
|
+
async capture(fragmentId, eventName, parameters, idempotencyKey) {
|
|
36
|
+
const fragment = fragments.get(fragmentId);
|
|
37
|
+
if (!fragment?.offlineEvents?.includes(eventName)) {
|
|
38
|
+
throw new Error(`Fragment event "${fragmentId}.${eventName}" is not offline-capturable.`);
|
|
39
|
+
}
|
|
40
|
+
if (!idempotencyKey || idempotencyKey.length > 256) throw new Error("Offline capture requires a bounded stable idempotency key.");
|
|
41
|
+
const capturedAt = now();
|
|
42
|
+
const captureExpiry = capturedAt.getTime() + (options.captureTtlMs ?? 864e5);
|
|
43
|
+
const planExpiry = Date.parse(options.plan.expiresAt);
|
|
44
|
+
if (!Number.isFinite(planExpiry) || planExpiry <= capturedAt.getTime()) throw new Error("Offline capture requires an unexpired render plan.");
|
|
45
|
+
const safeParameters = cloneOfflineParameters(parameters);
|
|
46
|
+
const captureId = options.nextCaptureId();
|
|
47
|
+
if (!captureId.trim()) throw new Error("Offline capture id is required.");
|
|
48
|
+
const record = {
|
|
49
|
+
captureId,
|
|
50
|
+
planId: options.plan.planId,
|
|
51
|
+
releaseDigest: options.plan.releaseDigest,
|
|
52
|
+
fragmentId,
|
|
53
|
+
eventName,
|
|
54
|
+
parameters: safeParameters,
|
|
55
|
+
idempotencyKey,
|
|
56
|
+
capturedAt: capturedAt.toISOString(),
|
|
57
|
+
expiresAt: new Date(Math.min(captureExpiry, planExpiry)).toISOString()
|
|
58
|
+
};
|
|
59
|
+
await options.store.put(record);
|
|
60
|
+
return record;
|
|
61
|
+
},
|
|
62
|
+
async replay() {
|
|
63
|
+
let completed = 0;
|
|
64
|
+
let failed = 0;
|
|
65
|
+
const parked = [];
|
|
66
|
+
const expired = [];
|
|
67
|
+
const reconciled = [];
|
|
68
|
+
for (const record of await options.store.list()) {
|
|
69
|
+
if (Date.parse(record.expiresAt) <= now().getTime()) {
|
|
70
|
+
await options.store.remove(record.captureId);
|
|
71
|
+
expired.push(record);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const request = {
|
|
75
|
+
planId: record.planId,
|
|
76
|
+
fragmentId: record.fragmentId,
|
|
77
|
+
eventName: record.eventName,
|
|
78
|
+
parameters: record.parameters,
|
|
79
|
+
idempotencyKey: record.idempotencyKey,
|
|
80
|
+
executionReason: "offline_replay",
|
|
81
|
+
capturedAt: record.capturedAt
|
|
82
|
+
};
|
|
83
|
+
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
|
+
}
|
|
101
|
+
} 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 {
|
|
106
|
+
failed += 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { completed, parked, failed, expired, reconciled };
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function cloneOfflineParameters(parameters) {
|
|
115
|
+
let nodes = 0;
|
|
116
|
+
const visit = (value, depth, key) => {
|
|
117
|
+
nodes += 1;
|
|
118
|
+
if (nodes > 1e4 || depth > 20) throw new Error("Offline capture parameters exceed structural limits.");
|
|
119
|
+
if (key !== void 0 && /(?:authorization|bearer|cookie|credential|password|secret|token|claims?)/i.test(key)) {
|
|
120
|
+
throw new Error(`Offline capture parameters may not contain credential or claim field "${key}".`);
|
|
121
|
+
}
|
|
122
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
123
|
+
if (typeof value === "number" && Number.isFinite(value)) return;
|
|
124
|
+
if (Array.isArray(value)) {
|
|
125
|
+
value.forEach((entry) => visit(entry, depth + 1));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
|
|
129
|
+
for (const [childKey, child] of Object.entries(value)) visit(child, depth + 1, childKey);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
throw new Error("Offline capture parameters must contain only portable JSON values.");
|
|
133
|
+
};
|
|
134
|
+
visit(parameters, 0);
|
|
135
|
+
const serialized = JSON.stringify(parameters);
|
|
136
|
+
if (new TextEncoder().encode(serialized).byteLength > 65536) throw new Error("Offline capture parameters exceed the 65536-byte limit.");
|
|
137
|
+
return structuredClone(parameters);
|
|
138
|
+
}
|
|
139
|
+
function indexFragments(root) {
|
|
140
|
+
const result = /* @__PURE__ */ new Map();
|
|
141
|
+
const visit = (fragment) => {
|
|
142
|
+
result.set(fragment.id, fragment);
|
|
143
|
+
for (const child of fragment.children ?? []) visit(child);
|
|
144
|
+
};
|
|
145
|
+
visit(root);
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
function createMemoryOfflineCaptureStore() {
|
|
149
|
+
const records = /* @__PURE__ */ new Map();
|
|
150
|
+
return {
|
|
151
|
+
protection: "ephemeral",
|
|
152
|
+
async put(record) {
|
|
153
|
+
if (records.has(record.captureId)) throw new Error(`Offline capture "${record.captureId}" already exists.`);
|
|
154
|
+
records.set(record.captureId, structuredClone(record));
|
|
155
|
+
},
|
|
156
|
+
async list() {
|
|
157
|
+
return [...records.values()].map((record) => structuredClone(record));
|
|
158
|
+
},
|
|
159
|
+
async remove(captureId) {
|
|
160
|
+
records.delete(captureId);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function messageOf(error) {
|
|
165
|
+
return error instanceof Error ? error.message : String(error);
|
|
166
|
+
}
|
|
167
|
+
function dispositionOf(outcome) {
|
|
168
|
+
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
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const status = typeof outcome.status === "string" ? outcome.status : "";
|
|
177
|
+
const reason = outcome.reconciliation?.message ?? outcome.reconciliation?.kind ?? (typeof outcome.error === "string" ? outcome.error : status);
|
|
178
|
+
switch (status) {
|
|
179
|
+
case "pending":
|
|
180
|
+
case "running":
|
|
181
|
+
case "completed":
|
|
182
|
+
return { kind: "delivered" };
|
|
183
|
+
case "waiting_for_approval":
|
|
184
|
+
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
|
+
};
|
|
192
|
+
case "blocked_by_policy":
|
|
193
|
+
case "validation_failed":
|
|
194
|
+
return { kind: "attention", status, certainty: "refused", reason };
|
|
195
|
+
case "failed":
|
|
196
|
+
return { kind: "attention", status, certainty: "unknown", reason };
|
|
197
|
+
default:
|
|
198
|
+
return { kind: "attention", status: status || "unknown", certainty: "unknown", reason };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function isSettledReplayFailure(error) {
|
|
202
|
+
return [
|
|
203
|
+
// The action was never offline-eligible.
|
|
204
|
+
/not eligible for offline replay/i,
|
|
205
|
+
// The plan no longer grants, no longer exists, or was never this actor's.
|
|
206
|
+
/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
|
+
/render plan was not found/i,
|
|
211
|
+
/render plan has expired/i,
|
|
212
|
+
/scope does not match/i,
|
|
213
|
+
/belongs to a different application assembly/i,
|
|
214
|
+
// A capture only gets older, so an age refusal can never come good. This
|
|
215
|
+
// one is independent of the capture TTL, so nothing else retires it.
|
|
216
|
+
/capturedAt is older than/i,
|
|
217
|
+
// Malformed beyond repair: passes the grammar, names no real instant.
|
|
218
|
+
/capturedAt is not a parseable instant/i,
|
|
219
|
+
/capturedAt must be an RFC 3339/i
|
|
220
|
+
// 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.
|
|
224
|
+
].some((pattern) => pattern.test(messageOf(error)));
|
|
225
|
+
}
|
|
226
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
227
|
+
0 && (module.exports = {
|
|
228
|
+
createMemoryOfflineCaptureStore,
|
|
229
|
+
createOfflineCaptureQueue
|
|
230
|
+
});
|
|
231
|
+
//# sourceMappingURL=offline.cjs.map
|
|
@@ -0,0 +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":[]}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { ScopedRenderPlan, ExperienceGatewayClient } from './contracts.cjs';
|
|
2
|
+
|
|
3
|
+
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
|
+
}
|
|
14
|
+
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
|
+
}
|
|
20
|
+
/** A delivered capture whose outcome a person needs to see. */
|
|
21
|
+
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
|
+
}
|
|
41
|
+
/** A delivered capture waiting on a human. */
|
|
42
|
+
interface OfflineParked {
|
|
43
|
+
record: OfflineCaptureRecord;
|
|
44
|
+
actionInvocationId?: string;
|
|
45
|
+
}
|
|
46
|
+
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
|
+
interface OfflineCaptureQueue {
|
|
73
|
+
capture(fragmentId: string, eventName: string, parameters: Record<string, unknown>, idempotencyKey: string): Promise<OfflineCaptureRecord>;
|
|
74
|
+
replay(): Promise<OfflineReplaySummary>;
|
|
75
|
+
}
|
|
76
|
+
declare function createOfflineCaptureQueue(options: {
|
|
77
|
+
plan: ScopedRenderPlan;
|
|
78
|
+
store: OfflineCaptureStore;
|
|
79
|
+
gateway: ExperienceGatewayClient;
|
|
80
|
+
nextCaptureId(): string;
|
|
81
|
+
now?: () => Date;
|
|
82
|
+
captureTtlMs?: number;
|
|
83
|
+
allowEphemeralStore?: boolean;
|
|
84
|
+
}): OfflineCaptureQueue;
|
|
85
|
+
declare function createMemoryOfflineCaptureStore(): OfflineCaptureStore;
|
|
86
|
+
|
|
87
|
+
export { type OfflineCaptureQueue, type OfflineCaptureRecord, type OfflineCaptureStore, type OfflineParked, type OfflineReconciliation, type OfflineReplaySummary, createMemoryOfflineCaptureStore, createOfflineCaptureQueue };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { ScopedRenderPlan, ExperienceGatewayClient } from './contracts.js';
|
|
2
|
+
|
|
3
|
+
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
|
+
}
|
|
14
|
+
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
|
+
}
|
|
20
|
+
/** A delivered capture whose outcome a person needs to see. */
|
|
21
|
+
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
|
+
}
|
|
41
|
+
/** A delivered capture waiting on a human. */
|
|
42
|
+
interface OfflineParked {
|
|
43
|
+
record: OfflineCaptureRecord;
|
|
44
|
+
actionInvocationId?: string;
|
|
45
|
+
}
|
|
46
|
+
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
|
+
interface OfflineCaptureQueue {
|
|
73
|
+
capture(fragmentId: string, eventName: string, parameters: Record<string, unknown>, idempotencyKey: string): Promise<OfflineCaptureRecord>;
|
|
74
|
+
replay(): Promise<OfflineReplaySummary>;
|
|
75
|
+
}
|
|
76
|
+
declare function createOfflineCaptureQueue(options: {
|
|
77
|
+
plan: ScopedRenderPlan;
|
|
78
|
+
store: OfflineCaptureStore;
|
|
79
|
+
gateway: ExperienceGatewayClient;
|
|
80
|
+
nextCaptureId(): string;
|
|
81
|
+
now?: () => Date;
|
|
82
|
+
captureTtlMs?: number;
|
|
83
|
+
allowEphemeralStore?: boolean;
|
|
84
|
+
}): OfflineCaptureQueue;
|
|
85
|
+
declare function createMemoryOfflineCaptureStore(): OfflineCaptureStore;
|
|
86
|
+
|
|
87
|
+
export { type OfflineCaptureQueue, type OfflineCaptureRecord, type OfflineCaptureStore, type OfflineParked, type OfflineReconciliation, type OfflineReplaySummary, createMemoryOfflineCaptureStore, createOfflineCaptureQueue };
|