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