@fabricorg/experience-runtime 0.4.7 → 0.4.9
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
CHANGED
|
@@ -1,293 +1,330 @@
|
|
|
1
|
-
"
|
|
2
|
-
|
|
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);
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region offline.ts
|
|
27
3
|
function createOfflineCaptureQueue(options) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
4
|
+
if (options.store.protection !== "encrypted-at-rest" && !options.allowEphemeralStore) throw new Error("Production offline capture requires an encrypted-at-rest store.");
|
|
5
|
+
if (!options.gateway.replay) throw new Error("Offline capture requires a gateway replay adapter.");
|
|
6
|
+
const fragments = indexFragments(options.plan.root);
|
|
7
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
8
|
+
return {
|
|
9
|
+
async capture(fragmentId, eventName, parameters, idempotencyKey) {
|
|
10
|
+
if (!fragments.get(fragmentId)?.offlineEvents?.includes(eventName)) throw new Error(`Fragment event "${fragmentId}.${eventName}" is not offline-capturable.`);
|
|
11
|
+
if (!idempotencyKey || idempotencyKey.length > 256) throw new Error("Offline capture requires a bounded stable idempotency key.");
|
|
12
|
+
const capturedAt = now();
|
|
13
|
+
const captureExpiry = capturedAt.getTime() + (options.captureTtlMs ?? 864e5);
|
|
14
|
+
const planExpiry = Date.parse(options.plan.expiresAt);
|
|
15
|
+
if (!Number.isFinite(planExpiry) || planExpiry <= capturedAt.getTime()) throw new Error("Offline capture requires an unexpired render plan.");
|
|
16
|
+
const safeParameters = cloneOfflineParameters(parameters);
|
|
17
|
+
const captureId = options.nextCaptureId();
|
|
18
|
+
if (!captureId.trim()) throw new Error("Offline capture id is required.");
|
|
19
|
+
const record = {
|
|
20
|
+
captureId,
|
|
21
|
+
planId: options.plan.planId,
|
|
22
|
+
releaseDigest: options.plan.releaseDigest,
|
|
23
|
+
fragmentId,
|
|
24
|
+
eventName,
|
|
25
|
+
parameters: safeParameters,
|
|
26
|
+
idempotencyKey,
|
|
27
|
+
capturedAt: capturedAt.toISOString(),
|
|
28
|
+
expiresAt: new Date(Math.min(captureExpiry, planExpiry)).toISOString()
|
|
29
|
+
};
|
|
30
|
+
await options.store.put(record);
|
|
31
|
+
return record;
|
|
32
|
+
},
|
|
33
|
+
async replay() {
|
|
34
|
+
let completed = 0;
|
|
35
|
+
let failed = 0;
|
|
36
|
+
const parked = [];
|
|
37
|
+
const expired = [];
|
|
38
|
+
const reconciled = [];
|
|
39
|
+
const storeFailures = [];
|
|
40
|
+
const replayOne = async (record) => {
|
|
41
|
+
if (Date.parse(record.expiresAt) <= now().getTime()) {
|
|
42
|
+
await options.store.remove(record.captureId);
|
|
43
|
+
expired.push(record);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const request = {
|
|
47
|
+
planId: record.planId,
|
|
48
|
+
fragmentId: record.fragmentId,
|
|
49
|
+
eventName: record.eventName,
|
|
50
|
+
parameters: record.parameters,
|
|
51
|
+
idempotencyKey: record.idempotencyKey,
|
|
52
|
+
executionReason: "offline_replay",
|
|
53
|
+
capturedAt: record.capturedAt
|
|
54
|
+
};
|
|
55
|
+
let outcome;
|
|
56
|
+
try {
|
|
57
|
+
outcome = await options.gateway.replay(request);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const thrown = classifyThrownReplayFailure(error);
|
|
60
|
+
if (thrown.kind === "transient") {
|
|
61
|
+
failed += 1;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
await options.store.remove(record.captureId);
|
|
65
|
+
reconciled.push({
|
|
66
|
+
record,
|
|
67
|
+
status: thrown.status,
|
|
68
|
+
...thrown.actionInvocationId === void 0 ? {} : { actionInvocationId: thrown.actionInvocationId },
|
|
69
|
+
certainty: thrown.certainty,
|
|
70
|
+
reason: messageOf(error)
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const disposition = dispositionOf(outcome);
|
|
75
|
+
if (disposition.kind === "not-delivered") {
|
|
76
|
+
failed += 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
await options.store.remove(record.captureId);
|
|
80
|
+
const actionInvocationId = typeof outcome?.actionInvocationId === "string" ? outcome.actionInvocationId : void 0;
|
|
81
|
+
if (disposition.kind === "attention") reconciled.push({
|
|
82
|
+
record,
|
|
83
|
+
status: disposition.status,
|
|
84
|
+
...actionInvocationId === void 0 ? {} : { actionInvocationId },
|
|
85
|
+
certainty: disposition.certainty,
|
|
86
|
+
reason: disposition.reason
|
|
87
|
+
});
|
|
88
|
+
else if (disposition.kind === "parked") parked.push({
|
|
89
|
+
record,
|
|
90
|
+
...actionInvocationId === void 0 ? {} : { actionInvocationId }
|
|
91
|
+
});
|
|
92
|
+
else completed += 1;
|
|
93
|
+
};
|
|
94
|
+
for (const record of await options.store.list()) try {
|
|
95
|
+
await replayOne(record);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
storeFailures.push({
|
|
98
|
+
record,
|
|
99
|
+
reason: messageOf(error)
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
completed,
|
|
104
|
+
parked,
|
|
105
|
+
failed,
|
|
106
|
+
expired,
|
|
107
|
+
reconciled,
|
|
108
|
+
storeFailures
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
};
|
|
134
112
|
}
|
|
135
113
|
function cloneOfflineParameters(parameters) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (utf8ByteLength(serialized) > 65536) throw new Error("Offline capture parameters exceed the 65536-byte limit.");
|
|
158
|
-
return JSON.parse(serialized);
|
|
114
|
+
let nodes = 0;
|
|
115
|
+
const visit = (value, depth, key) => {
|
|
116
|
+
nodes += 1;
|
|
117
|
+
if (nodes > 1e4 || depth > 20) throw new Error("Offline capture parameters exceed structural limits.");
|
|
118
|
+
if (key !== void 0 && /(?:authorization|bearer|cookie|credential|password|secret|token|claims?)/i.test(key)) throw new Error(`Offline capture parameters may not contain credential or claim field "${key}".`);
|
|
119
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
120
|
+
if (typeof value === "number" && Number.isFinite(value)) return;
|
|
121
|
+
if (Array.isArray(value)) {
|
|
122
|
+
value.forEach((entry) => visit(entry, depth + 1));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
|
|
126
|
+
for (const [childKey, child] of Object.entries(value)) visit(child, depth + 1, childKey);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
throw new Error("Offline capture parameters must contain only portable JSON values.");
|
|
130
|
+
};
|
|
131
|
+
visit(parameters, 0);
|
|
132
|
+
const serialized = JSON.stringify(parameters);
|
|
133
|
+
if (utf8ByteLength(serialized) > 65536) throw new Error("Offline capture parameters exceed the 65536-byte limit.");
|
|
134
|
+
return JSON.parse(serialized);
|
|
159
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* UTF-8 length without an encoder global.
|
|
138
|
+
*
|
|
139
|
+
* This package is the interface a client uses to reach capabilities, so it
|
|
140
|
+
* assumes nothing about the host beyond JSON. A React Native runtime, a worker,
|
|
141
|
+
* or an older embedded engine may not supply the encoder or clone globals, and
|
|
142
|
+
* the parameters were validated as portable JSON just above, so a JSON round
|
|
143
|
+
* trip is the clone and a code-point walk is the byte count.
|
|
144
|
+
*/
|
|
160
145
|
function utf8ByteLength(text) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
146
|
+
let bytes = 0;
|
|
147
|
+
for (const character of text) {
|
|
148
|
+
const codePoint = character.codePointAt(0);
|
|
149
|
+
bytes += codePoint < 128 ? 1 : codePoint < 2048 ? 2 : codePoint < 65536 ? 3 : 4;
|
|
150
|
+
}
|
|
151
|
+
return bytes;
|
|
167
152
|
}
|
|
153
|
+
/** Records are validated JSON, so a round trip is a faithful clone on any host. */
|
|
168
154
|
function cloneRecord(value) {
|
|
169
|
-
|
|
155
|
+
return JSON.parse(JSON.stringify(value));
|
|
170
156
|
}
|
|
171
157
|
function indexFragments(root) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
158
|
+
const result = /* @__PURE__ */ new Map();
|
|
159
|
+
const visit = (fragment) => {
|
|
160
|
+
result.set(fragment.id, fragment);
|
|
161
|
+
for (const child of fragment.children ?? []) visit(child);
|
|
162
|
+
};
|
|
163
|
+
visit(root);
|
|
164
|
+
return result;
|
|
179
165
|
}
|
|
180
166
|
function createMemoryOfflineCaptureStore() {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
167
|
+
const records = /* @__PURE__ */ new Map();
|
|
168
|
+
return {
|
|
169
|
+
protection: "ephemeral",
|
|
170
|
+
async put(record) {
|
|
171
|
+
if (records.has(record.captureId)) throw new Error(`Offline capture "${record.captureId}" already exists.`);
|
|
172
|
+
records.set(record.captureId, cloneRecord(record));
|
|
173
|
+
},
|
|
174
|
+
async list() {
|
|
175
|
+
return [...records.values()].map((record) => cloneRecord(record));
|
|
176
|
+
},
|
|
177
|
+
async remove(captureId) {
|
|
178
|
+
records.delete(captureId);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
195
181
|
}
|
|
196
182
|
function messageOf(error) {
|
|
197
|
-
|
|
183
|
+
return error instanceof Error ? error.message : String(error);
|
|
198
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Read a returned outcome across the platform's whole status set.
|
|
187
|
+
*
|
|
188
|
+
* Enumerating the statuses that looked like refusals left the rest falling
|
|
189
|
+
* through to "applied", which is how a validation failure got reported to an
|
|
190
|
+
* operator as success. The switch covers every `ActionStatus`, and the default
|
|
191
|
+
* arm surfaces anything else rather than counting it a success — the status
|
|
192
|
+
* arrives as a string over a transport, so this is a runtime guarantee and not
|
|
193
|
+
* a compile-time one.
|
|
194
|
+
*
|
|
195
|
+
* There is no "already applied" branch. A key that legitimately repeats returns
|
|
196
|
+
* the original invocation without complaint, and a genuine idempotency conflict
|
|
197
|
+
* means the key was reused for a materially different command, so that capture
|
|
198
|
+
* was never applied and must not be discarded.
|
|
199
|
+
*/
|
|
199
200
|
function dispositionOf(outcome) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
201
|
+
if (!outcome || typeof outcome !== "object") return { kind: "not-delivered" };
|
|
202
|
+
const status = typeof outcome.status === "string" ? outcome.status : "";
|
|
203
|
+
const reason = outcome.reconciliation?.message ?? outcome.reconciliation?.kind ?? (typeof outcome.error === "string" ? outcome.error : status);
|
|
204
|
+
switch (status) {
|
|
205
|
+
case "pending":
|
|
206
|
+
case "running":
|
|
207
|
+
case "completed": return { kind: "delivered" };
|
|
208
|
+
case "waiting_for_approval": return { kind: "parked" };
|
|
209
|
+
case "reconciliation_required": {
|
|
210
|
+
const recovery = outcome.reconciliation?.executionReason === "recovery";
|
|
211
|
+
return {
|
|
212
|
+
kind: "attention",
|
|
213
|
+
status,
|
|
214
|
+
certainty: outcome.adapterReconciliation !== void 0 || recovery ? "unknown" : "refused",
|
|
215
|
+
reason
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
case "blocked_by_policy":
|
|
219
|
+
case "validation_failed": return {
|
|
220
|
+
kind: "attention",
|
|
221
|
+
status,
|
|
222
|
+
certainty: "unknown",
|
|
223
|
+
reason
|
|
224
|
+
};
|
|
225
|
+
case "failed": return {
|
|
226
|
+
kind: "attention",
|
|
227
|
+
status,
|
|
228
|
+
certainty: "unknown",
|
|
229
|
+
reason
|
|
230
|
+
};
|
|
231
|
+
default: return {
|
|
232
|
+
kind: "attention",
|
|
233
|
+
status: status || "unknown",
|
|
234
|
+
certainty: "unknown",
|
|
235
|
+
reason
|
|
236
|
+
};
|
|
237
|
+
}
|
|
225
238
|
}
|
|
226
|
-
|
|
239
|
+
const CONFLICT_REASONS = /* @__PURE__ */ new Set([
|
|
240
|
+
"actor",
|
|
241
|
+
"authority_binding",
|
|
242
|
+
"action_version",
|
|
243
|
+
"parameters"
|
|
244
|
+
]);
|
|
245
|
+
/**
|
|
246
|
+
* An idempotency conflict is settled — retrying yields the same conflict
|
|
247
|
+
* forever — but which way depends on *why* the key conflicted. `parameters`
|
|
248
|
+
* or `actor` means the key was reused for a materially different command, so
|
|
249
|
+
* this capture was never applied. `authority_binding` or `action_version`
|
|
250
|
+
* means the *original* invocation exists and may well have applied: the
|
|
251
|
+
* capture-authority adapter mints a fresh binding on every attempt, so a first
|
|
252
|
+
* attempt whose response was lost conflicts with itself on the second. Deleting
|
|
253
|
+
* that and calling it refused is exactly the wrong answer, so it is surfaced as
|
|
254
|
+
* unknown, with the invocation it conflicted with, since that is what
|
|
255
|
+
* `recordExternalReconciliation` needs.
|
|
256
|
+
*
|
|
257
|
+
* A conflict whose reasons cannot be read is still a conflict, so it is still
|
|
258
|
+
* settled; the previous version let it fall through to transient and retried
|
|
259
|
+
* it forever. Unreadable reasons resolve to `unknown`, never to `refused`.
|
|
260
|
+
*/
|
|
227
261
|
function classifyThrownReplayFailure(error) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
262
|
+
const conflict = conflictOf(error);
|
|
263
|
+
if (conflict) {
|
|
264
|
+
if (conflict.reasons.size > 0 && [...conflict.reasons].every((reason) => CONFLICT_REASONS.has(reason)) && (conflict.reasons.has("parameters") || conflict.reasons.has("actor"))) return {
|
|
265
|
+
kind: "settled",
|
|
266
|
+
status: "not_delivered",
|
|
267
|
+
certainty: "refused"
|
|
268
|
+
};
|
|
269
|
+
return {
|
|
270
|
+
kind: "settled",
|
|
271
|
+
status: "conflict_prior_invocation",
|
|
272
|
+
certainty: "unknown",
|
|
273
|
+
...conflict.existingInvocationId === void 0 ? {} : { actionInvocationId: conflict.existingInvocationId }
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return isSettledReplayFailure(error) ? {
|
|
277
|
+
kind: "settled",
|
|
278
|
+
status: "not_delivered",
|
|
279
|
+
certainty: "refused"
|
|
280
|
+
} : { kind: "transient" };
|
|
241
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* Read an idempotency conflict off a thrown error.
|
|
284
|
+
*
|
|
285
|
+
* The host's `IdempotencyConflictError` carries the conflict as data, and a
|
|
286
|
+
* gateway that rethrows it (or any error shaped like it) is read structurally.
|
|
287
|
+
* Only when that is absent is the message parsed, and then defensively: the
|
|
288
|
+
* phrase is located from the *end*, because the idempotency key is interpolated
|
|
289
|
+
* before it and a key is whatever the application chose; the reasons are read
|
|
290
|
+
* to the end of their own line, because a wrapped error may append a stack or
|
|
291
|
+
* a detail line below. A message that names the phrase but cannot be parsed
|
|
292
|
+
* further is still reported as a conflict, with no reasons.
|
|
293
|
+
*/
|
|
242
294
|
function conflictOf(error) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
existingInvocationId: tail[1],
|
|
261
|
-
reasons: new Set(tail[2].split(",").map((reason) => reason.trim()).filter(Boolean))
|
|
262
|
-
};
|
|
295
|
+
const structural = error !== null && typeof error === "object" ? error.conflict : void 0;
|
|
296
|
+
if (structural !== null && typeof structural === "object") {
|
|
297
|
+
const { existingInvocationId, reasons } = structural;
|
|
298
|
+
if (Array.isArray(reasons)) return {
|
|
299
|
+
...typeof existingInvocationId === "string" && existingInvocationId ? { existingInvocationId } : {},
|
|
300
|
+
reasons: new Set(reasons.filter((reason) => typeof reason === "string"))
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
const message = messageOf(error);
|
|
304
|
+
const at = message.toLowerCase().lastIndexOf("conflicts with invocation");
|
|
305
|
+
if (at < 0) return void 0;
|
|
306
|
+
const tail = /^\s+([^\s:]+):[ \t]*([^\n]*)/.exec(message.slice(at + 25));
|
|
307
|
+
if (!tail) return { reasons: /* @__PURE__ */ new Set() };
|
|
308
|
+
return {
|
|
309
|
+
existingInvocationId: tail[1],
|
|
310
|
+
reasons: new Set(tail[2].split(",").map((reason) => reason.trim()).filter(Boolean))
|
|
311
|
+
};
|
|
263
312
|
}
|
|
264
313
|
function isSettledReplayFailure(error) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
/capturedAt is older than/i,
|
|
277
|
-
// Malformed beyond repair: passes the grammar, names no real instant.
|
|
278
|
-
/capturedAt is not a parseable instant/i,
|
|
279
|
-
/capturedAt must be an RFC 3339/i
|
|
280
|
-
// Deliberately absent: a capture dated in the future comes good as real
|
|
281
|
-
// time advances; a gateway missing its replay window is a deployment
|
|
282
|
-
// mistake somebody will fix; and an action-version mismatch fires
|
|
283
|
-
// transiently mid-rollout when a client's plan is newer than the pod it
|
|
284
|
-
// reached. Discarding any of them would destroy work a clock correction,
|
|
285
|
-
// a config change, or the rollout finishing would have replayed.
|
|
286
|
-
].some((pattern) => pattern.test(messageOf(error)));
|
|
314
|
+
return [
|
|
315
|
+
/not eligible for offline replay/i,
|
|
316
|
+
/is not granted by the render plan/i,
|
|
317
|
+
/render plan was not found/i,
|
|
318
|
+
/render plan has expired/i,
|
|
319
|
+
/scope does not match/i,
|
|
320
|
+
/belongs to a different application assembly/i,
|
|
321
|
+
/capturedAt is older than/i,
|
|
322
|
+
/capturedAt is not a parseable instant/i,
|
|
323
|
+
/capturedAt must be an RFC 3339/i
|
|
324
|
+
].some((pattern) => pattern.test(messageOf(error)));
|
|
287
325
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
});
|
|
326
|
+
//#endregion
|
|
327
|
+
exports.createMemoryOfflineCaptureStore = createMemoryOfflineCaptureStore;
|
|
328
|
+
exports.createOfflineCaptureQueue = createOfflineCaptureQueue;
|
|
329
|
+
|
|
293
330
|
//# sourceMappingURL=offline.cjs.map
|