@qitqode/qitqore 0.1.0 → 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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/dist/device-authorization.d.ts +67 -0
  4. package/dist/device-authorization.d.ts.map +1 -0
  5. package/dist/device-authorization.js +122 -0
  6. package/dist/device-authorization.js.map +1 -0
  7. package/dist/errors.d.ts +36 -0
  8. package/dist/errors.d.ts.map +1 -1
  9. package/dist/errors.js +108 -17
  10. package/dist/errors.js.map +1 -1
  11. package/dist/index.d.ts +10 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +9 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/recovery-capabilities.d.ts +26 -0
  16. package/dist/recovery-capabilities.d.ts.map +1 -0
  17. package/dist/recovery-capabilities.js +113 -0
  18. package/dist/recovery-capabilities.js.map +1 -0
  19. package/dist/recovery-directive.d.ts +20 -0
  20. package/dist/recovery-directive.d.ts.map +1 -0
  21. package/dist/recovery-directive.js +116 -0
  22. package/dist/recovery-directive.js.map +1 -0
  23. package/dist/recovery-lease.d.ts +80 -0
  24. package/dist/recovery-lease.d.ts.map +1 -0
  25. package/dist/recovery-lease.js +257 -0
  26. package/dist/recovery-lease.js.map +1 -0
  27. package/dist/recovery-status.d.ts +22 -0
  28. package/dist/recovery-status.d.ts.map +1 -0
  29. package/dist/recovery-status.js +113 -0
  30. package/dist/recovery-status.js.map +1 -0
  31. package/dist/recovery.d.ts +13 -0
  32. package/dist/recovery.d.ts.map +1 -1
  33. package/dist/recovery.js +94 -55
  34. package/dist/recovery.js.map +1 -1
  35. package/dist/redaction.d.ts +64 -0
  36. package/dist/redaction.d.ts.map +1 -0
  37. package/dist/redaction.js +154 -0
  38. package/dist/redaction.js.map +1 -0
  39. package/dist/route-contract.d.ts +36 -0
  40. package/dist/route-contract.d.ts.map +1 -0
  41. package/dist/route-contract.js +89 -0
  42. package/dist/route-contract.js.map +1 -0
  43. package/dist/stream-payload.d.ts +39 -0
  44. package/dist/stream-payload.d.ts.map +1 -0
  45. package/dist/stream-payload.js +86 -0
  46. package/dist/stream-payload.js.map +1 -0
  47. package/dist/transport.d.ts +10 -0
  48. package/dist/transport.d.ts.map +1 -1
  49. package/dist/transport.js +28 -0
  50. package/dist/transport.js.map +1 -1
  51. package/package.json +12 -2
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Qortex's recovery lease wire contract: the control envelope a host sends to
3
+ * bound additional work, and the usage projection Qortex returns for it.
4
+ * QitQore parses and encodes these values only. Hosts allocate token budgets,
5
+ * persist leases, authorize replay, and settle outcomes.
6
+ */
7
+ /** Request header that carries a base64url-encoded control envelope to Qortex. */
8
+ export const QORTEX_RECOVERY_CONTROL_HEADER = "x-qitqode-recovery-control";
9
+ /** Maximum additional-work tokens in one lease, matching Qortex's public limit. */
10
+ export const MAX_RECOVERY_ADDITIONAL_WORK = 12;
11
+ /** Longest root-operation window Qortex will honor for one dispatch. */
12
+ export const MAX_RECOVERY_ROOT_REMAINING_MS = 15 * 60 * 1_000;
13
+ const MAX_RECOVERY_CONTROL_HEADER_LENGTH = 16_384;
14
+ const MAX_IDENTIFIER_LENGTH = 256;
15
+ const MAX_STAGE_LENGTH = 128;
16
+ const HEADER_VALUE = /^[A-Za-z0-9_-]+$/;
17
+ const RECOVERY_ACTIONS = new Set([
18
+ "initial_dispatch",
19
+ "retry_same_work",
20
+ "changed_approach",
21
+ "continuation",
22
+ "compaction",
23
+ "worker",
24
+ "reconciliation",
25
+ ]);
26
+ const CONSUMED_WORK_ACTIONS = new Set([
27
+ "retry_same_work",
28
+ "changed_approach",
29
+ "continuation",
30
+ "compaction",
31
+ "worker",
32
+ "reconciliation",
33
+ ]);
34
+ const USAGE_FIELDS = new Set([
35
+ "contractVersion",
36
+ "leaseID",
37
+ "publicOperationID",
38
+ "operationGeneration",
39
+ "workID",
40
+ "attemptID",
41
+ "requestID",
42
+ "routeID",
43
+ "consumed",
44
+ "unusedTokenIDs",
45
+ ]);
46
+ const CONSUMED_FIELDS = new Set(["tokenID", "action", "stage"]);
47
+ const ENVELOPE_FIELDS = new Set(["contractVersion", "operation", "lease"]);
48
+ const OPERATION_FIELDS = new Set([
49
+ "contractVersion",
50
+ "publicOperationID",
51
+ "operationGeneration",
52
+ "workID",
53
+ "attemptID",
54
+ "parentWorkID",
55
+ "action",
56
+ "rootDeadlineAt",
57
+ "rootRemainingMsAtDispatch",
58
+ "claimID",
59
+ "leaseID",
60
+ "policyVersion",
61
+ ]);
62
+ const LEASE_FIELDS = new Set(["leaseID", "tokenIDs"]);
63
+ function isRecord(value) {
64
+ return value !== null && typeof value === "object" && !Array.isArray(value);
65
+ }
66
+ function assertKnownFields(value, allowed, label) {
67
+ for (const key of Object.keys(value)) {
68
+ if (!allowed.has(key))
69
+ throw new TypeError(`Qortex ${label} contains an unsupported field`);
70
+ }
71
+ }
72
+ function boundedString(value, label, maxLength) {
73
+ if (typeof value !== "string" || value.length === 0 || value.length > maxLength) {
74
+ throw new TypeError(`Qortex ${label} has an invalid identifier`);
75
+ }
76
+ return value;
77
+ }
78
+ function nonnegativeInteger(value, label) {
79
+ if (!Number.isSafeInteger(value) || value < 0) {
80
+ throw new TypeError(`Qortex ${label} must be a non-negative integer`);
81
+ }
82
+ return value;
83
+ }
84
+ function uniqueTokenIDs(tokenIDs, label) {
85
+ if (tokenIDs.length > MAX_RECOVERY_ADDITIONAL_WORK) {
86
+ throw new TypeError(`Qortex ${label} exceeds the additional-work limit`);
87
+ }
88
+ if (new Set(tokenIDs).size !== tokenIDs.length) {
89
+ throw new TypeError(`Qortex ${label} token IDs must be unique`);
90
+ }
91
+ return tokenIDs;
92
+ }
93
+ function tokenIDList(value, label) {
94
+ if (!Array.isArray(value) || value.length > MAX_RECOVERY_ADDITIONAL_WORK) {
95
+ throw new TypeError(`Qortex ${label} has an invalid token list`);
96
+ }
97
+ return value.map((item) => boundedString(item, label, MAX_IDENTIFIER_LENGTH));
98
+ }
99
+ /**
100
+ * Parse Qortex's lease usage projection. Every identity and token is bounded,
101
+ * token IDs must be unique across consumed and unused work, and unknown fields
102
+ * are rejected rather than forwarded.
103
+ */
104
+ export function parseRecoveryLeaseUsage(value) {
105
+ if (!isRecord(value))
106
+ throw new TypeError("Qortex recovery lease usage must be an object");
107
+ assertKnownFields(value, USAGE_FIELDS, "recovery lease usage");
108
+ if (value.contractVersion !== 1) {
109
+ throw new TypeError("Qortex recovery lease usage uses an unsupported contract version");
110
+ }
111
+ if (!Array.isArray(value.consumed) || value.consumed.length > MAX_RECOVERY_ADDITIONAL_WORK) {
112
+ throw new TypeError("Qortex recovery lease usage has an invalid consumed list");
113
+ }
114
+ const consumed = value.consumed.map((item) => {
115
+ if (!isRecord(item))
116
+ throw new TypeError("Qortex recovery lease usage has an invalid consumed entry");
117
+ assertKnownFields(item, CONSUMED_FIELDS, "recovery lease usage consumed entry");
118
+ if (typeof item.action !== "string" || !CONSUMED_WORK_ACTIONS.has(item.action)) {
119
+ throw new TypeError("Qortex recovery lease usage has an invalid consumed action");
120
+ }
121
+ return {
122
+ tokenID: boundedString(item.tokenID, "recovery lease usage tokenID", MAX_IDENTIFIER_LENGTH),
123
+ action: item.action,
124
+ stage: boundedString(item.stage, "recovery lease usage stage", MAX_STAGE_LENGTH),
125
+ };
126
+ });
127
+ const unusedTokenIDs = tokenIDList(value.unusedTokenIDs, "recovery lease usage unusedTokenIDs");
128
+ uniqueTokenIDs([...consumed.map((item) => item.tokenID), ...unusedTokenIDs], "recovery lease usage");
129
+ return {
130
+ contractVersion: 1,
131
+ leaseID: boundedString(value.leaseID, "recovery lease usage leaseID", MAX_IDENTIFIER_LENGTH),
132
+ publicOperationID: boundedString(value.publicOperationID, "recovery lease usage publicOperationID", MAX_IDENTIFIER_LENGTH),
133
+ operationGeneration: nonnegativeInteger(value.operationGeneration, "recovery lease usage operationGeneration"),
134
+ workID: boundedString(value.workID, "recovery lease usage workID", MAX_IDENTIFIER_LENGTH),
135
+ attemptID: boundedString(value.attemptID, "recovery lease usage attemptID", MAX_IDENTIFIER_LENGTH),
136
+ requestID: boundedString(value.requestID, "recovery lease usage requestID", MAX_IDENTIFIER_LENGTH),
137
+ routeID: boundedString(value.routeID, "recovery lease usage routeID", MAX_IDENTIFIER_LENGTH),
138
+ consumed,
139
+ unusedTokenIDs,
140
+ };
141
+ }
142
+ /**
143
+ * Validate a control envelope before it is sent. The operation's lease ID must
144
+ * name the same lease as the token list, and the list stays within Qortex's
145
+ * additional-work limit.
146
+ */
147
+ export function parseRecoveryControlEnvelope(value) {
148
+ if (!isRecord(value))
149
+ throw new TypeError("Qortex recovery control must be an object");
150
+ assertKnownFields(value, ENVELOPE_FIELDS, "recovery control");
151
+ if (value.contractVersion !== 1) {
152
+ throw new TypeError("Qortex recovery control uses an unsupported contract version");
153
+ }
154
+ if (!isRecord(value.operation))
155
+ throw new TypeError("Qortex recovery control operation must be an object");
156
+ const operation = value.operation;
157
+ assertKnownFields(operation, OPERATION_FIELDS, "recovery control operation");
158
+ if (operation.contractVersion !== 1) {
159
+ throw new TypeError("Qortex recovery control operation uses an unsupported contract version");
160
+ }
161
+ if (typeof operation.action !== "string" || !RECOVERY_ACTIONS.has(operation.action)) {
162
+ throw new TypeError("Qortex recovery control operation has an invalid action");
163
+ }
164
+ if (typeof operation.rootDeadlineAt !== "number" ||
165
+ !Number.isFinite(operation.rootDeadlineAt) ||
166
+ operation.rootDeadlineAt <= 0) {
167
+ throw new TypeError("Qortex recovery control operation has an invalid rootDeadlineAt");
168
+ }
169
+ if (typeof operation.rootRemainingMsAtDispatch !== "number" ||
170
+ !Number.isFinite(operation.rootRemainingMsAtDispatch) ||
171
+ operation.rootRemainingMsAtDispatch < 0 ||
172
+ operation.rootRemainingMsAtDispatch > MAX_RECOVERY_ROOT_REMAINING_MS) {
173
+ throw new TypeError("Qortex recovery control operation has an invalid rootRemainingMsAtDispatch");
174
+ }
175
+ if (!isRecord(value.lease))
176
+ throw new TypeError("Qortex recovery control lease must be an object");
177
+ assertKnownFields(value.lease, LEASE_FIELDS, "recovery control lease");
178
+ const leaseID = boundedString(value.lease.leaseID, "recovery control leaseID", MAX_IDENTIFIER_LENGTH);
179
+ const operationLeaseID = boundedString(operation.leaseID, "recovery control operation leaseID", MAX_IDENTIFIER_LENGTH);
180
+ if (operationLeaseID !== leaseID) {
181
+ throw new TypeError("Qortex recovery control lease ID must match the operation");
182
+ }
183
+ const tokenIDs = uniqueTokenIDs(tokenIDList(value.lease.tokenIDs, "recovery control tokenIDs"), "recovery control");
184
+ return {
185
+ contractVersion: 1,
186
+ operation: {
187
+ contractVersion: 1,
188
+ publicOperationID: boundedString(operation.publicOperationID, "recovery control publicOperationID", MAX_IDENTIFIER_LENGTH),
189
+ operationGeneration: nonnegativeInteger(operation.operationGeneration, "recovery control operationGeneration"),
190
+ workID: boundedString(operation.workID, "recovery control workID", MAX_IDENTIFIER_LENGTH),
191
+ attemptID: boundedString(operation.attemptID, "recovery control attemptID", MAX_IDENTIFIER_LENGTH),
192
+ ...(operation.parentWorkID === undefined
193
+ ? {}
194
+ : { parentWorkID: boundedString(operation.parentWorkID, "recovery control parentWorkID", MAX_IDENTIFIER_LENGTH) }),
195
+ action: operation.action,
196
+ rootDeadlineAt: operation.rootDeadlineAt,
197
+ rootRemainingMsAtDispatch: operation.rootRemainingMsAtDispatch,
198
+ ...(operation.claimID === undefined
199
+ ? {}
200
+ : { claimID: boundedString(operation.claimID, "recovery control claimID", MAX_IDENTIFIER_LENGTH) }),
201
+ leaseID,
202
+ ...(operation.policyVersion === undefined
203
+ ? {}
204
+ : { policyVersion: boundedString(operation.policyVersion, "recovery control policyVersion", MAX_IDENTIFIER_LENGTH) }),
205
+ },
206
+ lease: { leaseID, tokenIDs },
207
+ };
208
+ }
209
+ function base64UrlEncode(text) {
210
+ const bytes = new TextEncoder().encode(text);
211
+ let binary = "";
212
+ for (const byte of bytes)
213
+ binary += String.fromCharCode(byte);
214
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
215
+ }
216
+ function base64UrlDecode(value) {
217
+ const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
218
+ const binary = atob(padded);
219
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
220
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
221
+ }
222
+ /**
223
+ * Encode a validated control envelope as the base64url header value Qortex
224
+ * expects. Parser bounds keep every valid envelope under the header size limit.
225
+ */
226
+ export function encodeRecoveryControlEnvelope(value) {
227
+ return base64UrlEncode(JSON.stringify(parseRecoveryControlEnvelope(value)));
228
+ }
229
+ /**
230
+ * Decode a recovery-control header value. The encoding must be canonical base64url
231
+ * so that a value cannot be accepted by one parser and rewritten by another.
232
+ */
233
+ export function decodeRecoveryControlHeader(value) {
234
+ if (typeof value !== "string" ||
235
+ value.length === 0 ||
236
+ value.length > MAX_RECOVERY_CONTROL_HEADER_LENGTH ||
237
+ !HEADER_VALUE.test(value)) {
238
+ throw new TypeError("Qortex recovery control header is invalid");
239
+ }
240
+ let decoded;
241
+ try {
242
+ decoded = JSON.parse(base64UrlDecode(value));
243
+ }
244
+ catch {
245
+ throw new TypeError("Qortex recovery control header is invalid");
246
+ }
247
+ const envelope = parseRecoveryControlEnvelope(decoded);
248
+ if (base64UrlEncode(JSON.stringify(envelope)) !== value) {
249
+ throw new TypeError("Qortex recovery control header is not canonically encoded");
250
+ }
251
+ return envelope;
252
+ }
253
+ /** Return a copy of request headers that carries a validated recovery-control envelope. */
254
+ export function withRecoveryControlHeader(headers, control) {
255
+ return { ...headers, [QORTEX_RECOVERY_CONTROL_HEADER]: encodeRecoveryControlEnvelope(control) };
256
+ }
257
+ //# sourceMappingURL=recovery-lease.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recovery-lease.js","sourceRoot":"","sources":["../src/recovery-lease.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,kFAAkF;AAClF,MAAM,CAAC,MAAM,8BAA8B,GAAG,4BAA4B,CAAC;AAE3E,mFAAmF;AACnF,MAAM,CAAC,MAAM,4BAA4B,GAAG,EAAE,CAAC;AAE/C,wEAAwE;AACxE,MAAM,CAAC,MAAM,8BAA8B,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,CAAC;AAE9D,MAAM,kCAAkC,GAAG,MAAM,CAAC;AAClD,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAClC,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAC7B,MAAM,YAAY,GAAG,kBAAkB,CAAC;AAsDxC,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAuB;IACrD,kBAAkB;IAClB,iBAAiB;IACjB,kBAAkB;IAClB,cAAc;IACd,YAAY;IACZ,QAAQ;IACR,gBAAgB;CACjB,CAAC,CAAC;AACH,MAAM,qBAAqB,GAAG,IAAI,GAAG,CAAqC;IACxE,iBAAiB;IACjB,kBAAkB;IAClB,cAAc;IACd,YAAY;IACZ,QAAQ;IACR,gBAAgB;CACjB,CAAC,CAAC;AACH,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC;IAC3B,iBAAiB;IACjB,SAAS;IACT,mBAAmB;IACnB,qBAAqB;IACrB,QAAQ;IACR,WAAW;IACX,WAAW;IACX,SAAS;IACT,UAAU;IACV,gBAAgB;CACjB,CAAC,CAAC;AACH,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,SAAS,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;AAChE,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,iBAAiB,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;AAC3E,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC;IAC/B,iBAAiB;IACjB,mBAAmB;IACnB,qBAAqB;IACrB,QAAQ;IACR,WAAW;IACX,cAAc;IACd,QAAQ;IACR,gBAAgB;IAChB,2BAA2B;IAC3B,SAAS;IACT,SAAS;IACT,eAAe;CAChB,CAAC,CAAC;AACH,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC;AAEtD,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,iBAAiB,CAAC,KAA8B,EAAE,OAA4B,EAAE,KAAa;IACpG,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACrC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,MAAM,IAAI,SAAS,CAAC,UAAU,KAAK,gCAAgC,CAAC,CAAC;IAC9F,CAAC;AACH,CAAC;AAED,SAAS,aAAa,CAAC,KAAc,EAAE,KAAa,EAAE,SAAiB;IACrE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,SAAS,EAAE,CAAC;QAChF,MAAM,IAAI,SAAS,CAAC,UAAU,KAAK,4BAA4B,CAAC,CAAC;IACnE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,kBAAkB,CAAC,KAAc,EAAE,KAAa;IACvD,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,IAAK,KAAgB,GAAG,CAAC,EAAE,CAAC;QAC1D,MAAM,IAAI,SAAS,CAAC,UAAU,KAAK,iCAAiC,CAAC,CAAC;IACxE,CAAC;IACD,OAAO,KAAe,CAAC;AACzB,CAAC;AAED,SAAS,cAAc,CAAC,QAAkB,EAAE,KAAa;IACvD,IAAI,QAAQ,CAAC,MAAM,GAAG,4BAA4B,EAAE,CAAC;QACnD,MAAM,IAAI,SAAS,CAAC,UAAU,KAAK,oCAAoC,CAAC,CAAC;IAC3E,CAAC;IACD,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,MAAM,EAAE,CAAC;QAC/C,MAAM,IAAI,SAAS,CAAC,UAAU,KAAK,2BAA2B,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,WAAW,CAAC,KAAc,EAAE,KAAa;IAChD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,4BAA4B,EAAE,CAAC;QACzE,MAAM,IAAI,SAAS,CAAC,UAAU,KAAK,4BAA4B,CAAC,CAAC;IACnE,CAAC;IACD,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,KAAK,EAAE,qBAAqB,CAAC,CAAC,CAAC;AAChF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAAc;IACpD,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,SAAS,CAAC,+CAA+C,CAAC,CAAC;IAC3F,iBAAiB,CAAC,KAAK,EAAE,YAAY,EAAE,sBAAsB,CAAC,CAAC;IAC/D,IAAI,KAAK,CAAC,eAAe,KAAK,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,SAAS,CAAC,kEAAkE,CAAC,CAAC;IAC1F,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAG,4BAA4B,EAAE,CAAC;QAC3F,MAAM,IAAI,SAAS,CAAC,0DAA0D,CAAC,CAAC;IAClF,CAAC;IACD,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAA8B,EAAE;QACvE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,SAAS,CAAC,2DAA2D,CAAC,CAAC;QACtG,iBAAiB,CAAC,IAAI,EAAE,eAAe,EAAE,qCAAqC,CAAC,CAAC;QAChF,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,MAA4C,CAAC,EAAE,CAAC;YACrH,MAAM,IAAI,SAAS,CAAC,4DAA4D,CAAC,CAAC;QACpF,CAAC;QACD,OAAO;YACL,OAAO,EAAE,aAAa,CAAC,IAAI,CAAC,OAAO,EAAE,8BAA8B,EAAE,qBAAqB,CAAC;YAC3F,MAAM,EAAE,IAAI,CAAC,MAA4C;YACzD,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,KAAK,EAAE,4BAA4B,EAAE,gBAAgB,CAAC;SACjF,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,MAAM,cAAc,GAAG,WAAW,CAAC,KAAK,CAAC,cAAc,EAAE,qCAAqC,CAAC,CAAC;IAChG,cAAc,CACZ,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,GAAG,cAAc,CAAC,EAC5D,sBAAsB,CACvB,CAAC;IAEF,OAAO;QACL,eAAe,EAAE,CAAC;QAClB,OAAO,EAAE,aAAa,CAAC,KAAK,CAAC,OAAO,EAAE,8BAA8B,EAAE,qBAAqB,CAAC;QAC5F,iBAAiB,EAAE,aAAa,CAAC,KAAK,CAAC,iBAAiB,EAAE,wCAAwC,EAAE,qBAAqB,CAAC;QAC1H,mBAAmB,EAAE,kBAAkB,CAAC,KAAK,CAAC,mBAAmB,EAAE,0CAA0C,CAAC;QAC9G,MAAM,EAAE,aAAa,CAAC,KAAK,CAAC,MAAM,EAAE,6BAA6B,EAAE,qBAAqB,CAAC;QACzF,SAAS,EAAE,aAAa,CAAC,KAAK,CAAC,SAAS,EAAE,gCAAgC,EAAE,qBAAqB,CAAC;QAClG,SAAS,EAAE,aAAa,CAAC,KAAK,CAAC,SAAS,EAAE,gCAAgC,EAAE,qBAAqB,CAAC;QAClG,OAAO,EAAE,aAAa,CAAC,KAAK,CAAC,OAAO,EAAE,8BAA8B,EAAE,qBAAqB,CAAC;QAC5F,QAAQ;QACR,cAAc;KACf,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,4BAA4B,CAAC,KAAc;IACzD,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,SAAS,CAAC,2CAA2C,CAAC,CAAC;IACvF,iBAAiB,CAAC,KAAK,EAAE,eAAe,EAAE,kBAAkB,CAAC,CAAC;IAC9D,IAAI,KAAK,CAAC,eAAe,KAAK,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,SAAS,CAAC,8DAA8D,CAAC,CAAC;IACtF,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC;QAAE,MAAM,IAAI,SAAS,CAAC,qDAAqD,CAAC,CAAC;IAC3G,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;IAClC,iBAAiB,CAAC,SAAS,EAAE,gBAAgB,EAAE,4BAA4B,CAAC,CAAC;IAC7E,IAAI,SAAS,CAAC,eAAe,KAAK,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,SAAS,CAAC,wEAAwE,CAAC,CAAC;IAChG,CAAC;IACD,IAAI,OAAO,SAAS,CAAC,MAAM,KAAK,QAAQ,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,SAAS,CAAC,MAA8B,CAAC,EAAE,CAAC;QAC5G,MAAM,IAAI,SAAS,CAAC,yDAAyD,CAAC,CAAC;IACjF,CAAC;IACD,IACE,OAAO,SAAS,CAAC,cAAc,KAAK,QAAQ;QAC5C,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,cAAc,CAAC;QAC1C,SAAS,CAAC,cAAc,IAAI,CAAC,EAC7B,CAAC;QACD,MAAM,IAAI,SAAS,CAAC,iEAAiE,CAAC,CAAC;IACzF,CAAC;IACD,IACE,OAAO,SAAS,CAAC,yBAAyB,KAAK,QAAQ;QACvD,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,yBAAyB,CAAC;QACrD,SAAS,CAAC,yBAAyB,GAAG,CAAC;QACvC,SAAS,CAAC,yBAAyB,GAAG,8BAA8B,EACpE,CAAC;QACD,MAAM,IAAI,SAAS,CAAC,4EAA4E,CAAC,CAAC;IACpG,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,SAAS,CAAC,iDAAiD,CAAC,CAAC;IACnG,iBAAiB,CAAC,KAAK,CAAC,KAAK,EAAE,YAAY,EAAE,wBAAwB,CAAC,CAAC;IAEvE,MAAM,OAAO,GAAG,aAAa,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,0BAA0B,EAAE,qBAAqB,CAAC,CAAC;IACtG,MAAM,gBAAgB,GAAG,aAAa,CAAC,SAAS,CAAC,OAAO,EAAE,oCAAoC,EAAE,qBAAqB,CAAC,CAAC;IACvH,IAAI,gBAAgB,KAAK,OAAO,EAAE,CAAC;QACjC,MAAM,IAAI,SAAS,CAAC,2DAA2D,CAAC,CAAC;IACnF,CAAC;IACD,MAAM,QAAQ,GAAG,cAAc,CAAC,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,2BAA2B,CAAC,EAAE,kBAAkB,CAAC,CAAC;IAEpH,OAAO;QACL,eAAe,EAAE,CAAC;QAClB,SAAS,EAAE;YACT,eAAe,EAAE,CAAC;YAClB,iBAAiB,EAAE,aAAa,CAAC,SAAS,CAAC,iBAAiB,EAAE,oCAAoC,EAAE,qBAAqB,CAAC;YAC1H,mBAAmB,EAAE,kBAAkB,CAAC,SAAS,CAAC,mBAAmB,EAAE,sCAAsC,CAAC;YAC9G,MAAM,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,yBAAyB,EAAE,qBAAqB,CAAC;YACzF,SAAS,EAAE,aAAa,CAAC,SAAS,CAAC,SAAS,EAAE,4BAA4B,EAAE,qBAAqB,CAAC;YAClG,GAAG,CAAC,SAAS,CAAC,YAAY,KAAK,SAAS;gBACtC,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC,EAAE,YAAY,EAAE,aAAa,CAAC,SAAS,CAAC,YAAY,EAAE,+BAA+B,EAAE,qBAAqB,CAAC,EAAE,CAAC;YACpH,MAAM,EAAE,SAAS,CAAC,MAA8B;YAChD,cAAc,EAAE,SAAS,CAAC,cAAc;YACxC,yBAAyB,EAAE,SAAS,CAAC,yBAAyB;YAC9D,GAAG,CAAC,SAAS,CAAC,OAAO,KAAK,SAAS;gBACjC,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC,EAAE,OAAO,EAAE,aAAa,CAAC,SAAS,CAAC,OAAO,EAAE,0BAA0B,EAAE,qBAAqB,CAAC,EAAE,CAAC;YACrG,OAAO;YACP,GAAG,CAAC,SAAS,CAAC,aAAa,KAAK,SAAS;gBACvC,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC,EAAE,aAAa,EAAE,aAAa,CAAC,SAAS,CAAC,aAAa,EAAE,gCAAgC,EAAE,qBAAqB,CAAC,EAAE,CAAC;SACxH;QACD,KAAK,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE;KAC7B,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,IAAY;IACnC,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC7C,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IAC9D,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AACjF,CAAC;AAED,SAAS,eAAe,CAAC,KAAa;IACpC,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;IACxG,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5B,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IACpE,OAAO,IAAI,WAAW,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACjE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,6BAA6B,CAAC,KAAc;IAC1D,OAAO,eAAe,CAAC,IAAI,CAAC,SAAS,CAAC,4BAA4B,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAC9E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,2BAA2B,CAAC,KAAa;IACvD,IACE,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,CAAC,MAAM,KAAK,CAAC;QAClB,KAAK,CAAC,MAAM,GAAG,kCAAkC;QACjD,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,EACzB,CAAC;QACD,MAAM,IAAI,SAAS,CAAC,2CAA2C,CAAC,CAAC;IACnE,CAAC;IACD,IAAI,OAAgB,CAAC;IACrB,IAAI,CAAC;QACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,KAAK,CAAC,CAAY,CAAC;IAC1D,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,SAAS,CAAC,2CAA2C,CAAC,CAAC;IACnE,CAAC;IACD,MAAM,QAAQ,GAAG,4BAA4B,CAAC,OAAO,CAAC,CAAC;IACvD,IAAI,eAAe,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC;QACxD,MAAM,IAAI,SAAS,CAAC,2DAA2D,CAAC,CAAC;IACnF,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,yBAAyB,CACvC,OAA+B,EAC/B,OAAgB;IAEhB,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC,8BAA8B,CAAC,EAAE,6BAA6B,CAAC,OAAO,CAAC,EAAE,CAAC;AAClG,CAAC"}
@@ -0,0 +1,22 @@
1
+ import { type QortexRecoveryLeaseUsage } from "./recovery-lease.js";
2
+ export type QortexExecutionState = "not_started" | "running" | "succeeded" | "failed" | "unknown";
3
+ export type QortexResultRecovery = "replayable" | "pending" | "expired" | "unsupported";
4
+ export interface QortexRecoveryStatus {
5
+ contractVersion: 1;
6
+ requestID: string;
7
+ scope: "qortex_route";
8
+ executionState: QortexExecutionState;
9
+ resultRecovery: QortexResultRecovery;
10
+ replayUntil?: number;
11
+ routeID?: string;
12
+ /** Qortex's usage report for a lease it issued; it must name this request and route. */
13
+ recoveryLeaseUsage?: QortexRecoveryLeaseUsage;
14
+ lastUpdatedAt: number;
15
+ }
16
+ /**
17
+ * Parse Qortex's owner-scoped recovery projection. The parser accepts either
18
+ * the route's top-level success envelope or the standard `{ success, data }`
19
+ * wrapper, but it never forwards unrecognized fields to callers.
20
+ */
21
+ export declare function parseRecoveryStatus(value: unknown, expectedRequestID?: string): QortexRecoveryStatus;
22
+ //# sourceMappingURL=recovery-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recovery-status.d.ts","sourceRoot":"","sources":["../src/recovery-status.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAE7F,MAAM,MAAM,oBAAoB,GAC5B,aAAa,GACb,SAAS,GACT,WAAW,GACX,QAAQ,GACR,SAAS,CAAC;AAEd,MAAM,MAAM,oBAAoB,GAC5B,YAAY,GACZ,SAAS,GACT,SAAS,GACT,aAAa,CAAC;AAElB,MAAM,WAAW,oBAAoB;IACnC,eAAe,EAAE,CAAC,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,cAAc,CAAC;IACtB,cAAc,EAAE,oBAAoB,CAAC;IACrC,cAAc,EAAE,oBAAoB,CAAC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wFAAwF;IACxF,kBAAkB,CAAC,EAAE,wBAAwB,CAAC;IAC9C,aAAa,EAAE,MAAM,CAAC;CACvB;AA8CD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,OAAO,EACd,iBAAiB,CAAC,EAAE,MAAM,GACzB,oBAAoB,CAyEtB"}
@@ -0,0 +1,113 @@
1
+ import { parseRecoveryLeaseUsage } from "./recovery-lease.js";
2
+ const EXECUTION_STATES = new Set([
3
+ "not_started",
4
+ "running",
5
+ "succeeded",
6
+ "failed",
7
+ "unknown",
8
+ ]);
9
+ const RESULT_RECOVERY_STATES = new Set([
10
+ "replayable",
11
+ "pending",
12
+ "expired",
13
+ "unsupported",
14
+ ]);
15
+ const ALLOWED_FIELDS = new Set([
16
+ "contractVersion",
17
+ "requestID",
18
+ "scope",
19
+ "executionState",
20
+ "resultRecovery",
21
+ "replayUntil",
22
+ "routeID",
23
+ "recoveryLeaseUsage",
24
+ "lastUpdatedAt",
25
+ "success",
26
+ ]);
27
+ function isRecord(value) {
28
+ return value !== null && typeof value === "object" && !Array.isArray(value);
29
+ }
30
+ function boundedString(value, field, maxLength) {
31
+ if (typeof value !== "string" || value.length === 0 || value.length > maxLength) {
32
+ throw new TypeError(`Qortex recovery status has an invalid ${field}`);
33
+ }
34
+ return value;
35
+ }
36
+ function epoch(value, field) {
37
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
38
+ throw new TypeError(`Qortex recovery status has an invalid ${field}`);
39
+ }
40
+ return value;
41
+ }
42
+ /**
43
+ * Parse Qortex's owner-scoped recovery projection. The parser accepts either
44
+ * the route's top-level success envelope or the standard `{ success, data }`
45
+ * wrapper, but it never forwards unrecognized fields to callers.
46
+ */
47
+ export function parseRecoveryStatus(value, expectedRequestID) {
48
+ if (!isRecord(value)) {
49
+ throw new TypeError("Qortex recovery status must be an object");
50
+ }
51
+ let candidate = value;
52
+ if (value.success === true && Object.hasOwn(value, "data")) {
53
+ candidate = value.data;
54
+ }
55
+ if (!isRecord(candidate)) {
56
+ throw new TypeError("Qortex recovery status data must be an object");
57
+ }
58
+ for (const key of Object.keys(candidate)) {
59
+ if (!ALLOWED_FIELDS.has(key)) {
60
+ throw new TypeError("Qortex recovery status contains an unsupported field");
61
+ }
62
+ }
63
+ if (candidate.success !== undefined && candidate.success !== true) {
64
+ throw new TypeError("Qortex recovery status has an invalid success marker");
65
+ }
66
+ if (candidate.contractVersion !== 1) {
67
+ throw new TypeError("Qortex recovery status uses an unsupported contract version");
68
+ }
69
+ const requestID = boundedString(candidate.requestID, "requestID", 256);
70
+ if (expectedRequestID !== undefined && requestID !== expectedRequestID) {
71
+ throw new TypeError("Qortex recovery status requestID does not match the lookup");
72
+ }
73
+ if (candidate.scope !== "qortex_route") {
74
+ throw new TypeError("Qortex recovery status has an unsupported scope");
75
+ }
76
+ if (typeof candidate.executionState !== "string" || !EXECUTION_STATES.has(candidate.executionState)) {
77
+ throw new TypeError("Qortex recovery status has an invalid executionState");
78
+ }
79
+ if (typeof candidate.resultRecovery !== "string" || !RESULT_RECOVERY_STATES.has(candidate.resultRecovery)) {
80
+ throw new TypeError("Qortex recovery status has an invalid resultRecovery");
81
+ }
82
+ const replayUntil = candidate.replayUntil === undefined
83
+ ? undefined
84
+ : epoch(candidate.replayUntil, "replayUntil");
85
+ const routeID = candidate.routeID === undefined
86
+ ? undefined
87
+ : boundedString(candidate.routeID, "routeID", 256);
88
+ const recoveryLeaseUsage = candidate.recoveryLeaseUsage === undefined
89
+ ? undefined
90
+ : parseRecoveryLeaseUsage(candidate.recoveryLeaseUsage);
91
+ if (recoveryLeaseUsage !== undefined) {
92
+ // The request must always agree. Qortex's public schema lets the status omit its
93
+ // route, so the route is compared only when the status names one.
94
+ if (recoveryLeaseUsage.requestID !== requestID) {
95
+ throw new TypeError("Qortex recovery status lease usage does not match the request");
96
+ }
97
+ if (routeID !== undefined && recoveryLeaseUsage.routeID !== routeID) {
98
+ throw new TypeError("Qortex recovery status lease usage does not match the route");
99
+ }
100
+ }
101
+ return {
102
+ contractVersion: 1,
103
+ requestID,
104
+ scope: "qortex_route",
105
+ executionState: candidate.executionState,
106
+ resultRecovery: candidate.resultRecovery,
107
+ ...(replayUntil === undefined ? {} : { replayUntil }),
108
+ ...(routeID === undefined ? {} : { routeID }),
109
+ ...(recoveryLeaseUsage === undefined ? {} : { recoveryLeaseUsage }),
110
+ lastUpdatedAt: epoch(candidate.lastUpdatedAt, "lastUpdatedAt"),
111
+ };
112
+ }
113
+ //# sourceMappingURL=recovery-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recovery-status.js","sourceRoot":"","sources":["../src/recovery-status.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAiC,MAAM,qBAAqB,CAAC;AA4B7F,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAuB;IACrD,aAAa;IACb,SAAS;IACT,WAAW;IACX,QAAQ;IACR,SAAS;CACV,CAAC,CAAC;AACH,MAAM,sBAAsB,GAAG,IAAI,GAAG,CAAuB;IAC3D,YAAY;IACZ,SAAS;IACT,SAAS;IACT,aAAa;CACd,CAAC,CAAC;AACH,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC;IAC7B,iBAAiB;IACjB,WAAW;IACX,OAAO;IACP,gBAAgB;IAChB,gBAAgB;IAChB,aAAa;IACb,SAAS;IACT,oBAAoB;IACpB,eAAe;IACf,SAAS;CACV,CAAC,CAAC;AAEH,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,aAAa,CAAC,KAAc,EAAE,KAAa,EAAE,SAAiB;IACrE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,SAAS,EAAE,CAAC;QAChF,MAAM,IAAI,SAAS,CAAC,yCAAyC,KAAK,EAAE,CAAC,CAAC;IACxE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,KAAK,CAAC,KAAc,EAAE,KAAa;IAC1C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;QACvE,MAAM,IAAI,SAAS,CAAC,yCAAyC,KAAK,EAAE,CAAC,CAAC;IACxE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CACjC,KAAc,EACd,iBAA0B;IAE1B,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,SAAS,CAAC,0CAA0C,CAAC,CAAC;IAClE,CAAC;IAED,IAAI,SAAS,GAAY,KAAK,CAAC;IAC/B,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,CAAC;QAC3D,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC;IACzB,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,SAAS,CAAC,+CAA+C,CAAC,CAAC;IACvE,CAAC;IAED,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QACzC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC7B,MAAM,IAAI,SAAS,CAAC,sDAAsD,CAAC,CAAC;QAC9E,CAAC;IACH,CAAC;IACD,IAAI,SAAS,CAAC,OAAO,KAAK,SAAS,IAAI,SAAS,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;QAClE,MAAM,IAAI,SAAS,CAAC,sDAAsD,CAAC,CAAC;IAC9E,CAAC;IACD,IAAI,SAAS,CAAC,eAAe,KAAK,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,SAAS,CAAC,6DAA6D,CAAC,CAAC;IACrF,CAAC;IAED,MAAM,SAAS,GAAG,aAAa,CAAC,SAAS,CAAC,SAAS,EAAE,WAAW,EAAE,GAAG,CAAC,CAAC;IACvE,IAAI,iBAAiB,KAAK,SAAS,IAAI,SAAS,KAAK,iBAAiB,EAAE,CAAC;QACvE,MAAM,IAAI,SAAS,CAAC,4DAA4D,CAAC,CAAC;IACpF,CAAC;IACD,IAAI,SAAS,CAAC,KAAK,KAAK,cAAc,EAAE,CAAC;QACvC,MAAM,IAAI,SAAS,CAAC,iDAAiD,CAAC,CAAC;IACzE,CAAC;IACD,IAAI,OAAO,SAAS,CAAC,cAAc,KAAK,QAAQ,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,SAAS,CAAC,cAAsC,CAAC,EAAE,CAAC;QAC5H,MAAM,IAAI,SAAS,CAAC,sDAAsD,CAAC,CAAC;IAC9E,CAAC;IACD,IAAI,OAAO,SAAS,CAAC,cAAc,KAAK,QAAQ,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC,SAAS,CAAC,cAAsC,CAAC,EAAE,CAAC;QAClI,MAAM,IAAI,SAAS,CAAC,sDAAsD,CAAC,CAAC;IAC9E,CAAC;IAED,MAAM,WAAW,GACf,SAAS,CAAC,WAAW,KAAK,SAAS;QACjC,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;IAClD,MAAM,OAAO,GACX,SAAS,CAAC,OAAO,KAAK,SAAS;QAC7B,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,aAAa,CAAC,SAAS,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,CAAC,CAAC;IACvD,MAAM,kBAAkB,GACtB,SAAS,CAAC,kBAAkB,KAAK,SAAS;QACxC,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,uBAAuB,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC;IAC5D,IAAI,kBAAkB,KAAK,SAAS,EAAE,CAAC;QACrC,iFAAiF;QACjF,kEAAkE;QAClE,IAAI,kBAAkB,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;YAC/C,MAAM,IAAI,SAAS,CAAC,+DAA+D,CAAC,CAAC;QACvF,CAAC;QACD,IAAI,OAAO,KAAK,SAAS,IAAI,kBAAkB,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;YACpE,MAAM,IAAI,SAAS,CAAC,6DAA6D,CAAC,CAAC;QACrF,CAAC;IACH,CAAC;IAED,OAAO;QACL,eAAe,EAAE,CAAC;QAClB,SAAS;QACT,KAAK,EAAE,cAAc;QACrB,cAAc,EAAE,SAAS,CAAC,cAAsC;QAChE,cAAc,EAAE,SAAS,CAAC,cAAsC;QAChE,GAAG,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;QACrD,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC;QAC7C,GAAG,CAAC,kBAAkB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,kBAAkB,EAAE,CAAC;QACnE,aAAa,EAAE,KAAK,CAAC,SAAS,CAAC,aAAa,EAAE,eAAe,CAAC;KAC/D,CAAC;AACJ,CAAC"}
@@ -21,13 +21,26 @@ export interface ReplayAuthorizationContext {
21
21
  readonly outcome: Readonly<AttemptOutcome>;
22
22
  readonly signal?: AbortSignal;
23
23
  }
24
+ export interface RecoveryAttemptOutcomeContext {
25
+ readonly operationId: string;
26
+ readonly attempt: number;
27
+ readonly outcome: Readonly<AttemptOutcome>;
28
+ readonly signal?: AbortSignal;
29
+ readonly error?: unknown;
30
+ }
24
31
  export interface RecoveryOptions<T> {
25
32
  operationId: string;
26
33
  maxAttempts: number;
27
34
  signal?: AbortSignal;
35
+ /** Absolute epoch deadline for the whole logical operation. */
36
+ deadlineAt?: number;
28
37
  execute(context: QortexAttemptContext): Promise<T>;
29
38
  shouldRetry(error: unknown, context: ReplayAuthorizationContext): boolean;
30
39
  authorizeReplay(context: ReplayAuthorizationContext): boolean | Promise<boolean>;
40
+ /** Called immediately before an attempt is dispatched. */
41
+ onAttemptStart?: (context: QortexAttemptContext) => void | Promise<void>;
42
+ /** Called after an attempt settles, including failed and partially observed attempts. */
43
+ onAttemptOutcome?: (context: RecoveryAttemptOutcomeContext) => void | Promise<void>;
31
44
  backoffMs?: number | ((nextAttempt: number, error: unknown) => number);
32
45
  }
33
46
  export declare function abortableDelay(ms: number, signal?: AbortSignal): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../src/recovery.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,cAAc,GAAG,UAAU,CAAC;AAEvE,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,gBAAgB,CAAC;IAC7B,aAAa,EAAE,OAAO,CAAC;IACvB,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,eAAe,IAAI,IAAI,CAAC;IACxB,YAAY,IAAI,IAAI,CAAC;IACrB,iBAAiB,IAAI,IAAI,CAAC;IAC1B,qBAAqB,IAAI,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC;IAC3C,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,OAAO,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACnD,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC;IAC1E,eAAe,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjF,SAAS,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC,CAAC;CACxE;AAMD,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAwB9E;AAMD,wBAAsB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAyEhF"}
1
+ {"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../src/recovery.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,cAAc,GAAG,UAAU,CAAC;AAEvE,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,gBAAgB,CAAC;IAC7B,aAAa,EAAE,OAAO,CAAC;IACvB,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,eAAe,IAAI,IAAI,CAAC;IACxB,YAAY,IAAI,IAAI,CAAC;IACrB,iBAAiB,IAAI,IAAI,CAAC;IAC1B,qBAAqB,IAAI,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC;IAC3C,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC;IAC3C,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACnD,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC;IAC1E,eAAe,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjF,0DAA0D;IAC1D,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE,yFAAyF;IACzF,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,6BAA6B,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpF,SAAS,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC,CAAC;CACxE;AAMD,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAwB9E;AAMD,wBAAsB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAiHhF"}
package/dist/recovery.js CHANGED
@@ -41,66 +41,105 @@ export async function runWithRecovery(options) {
41
41
  if (!Number.isInteger(options.maxAttempts) || options.maxAttempts < 1 || options.maxAttempts > 5) {
42
42
  throw new RangeError("maxAttempts must be an integer between 1 and 5");
43
43
  }
44
- for (let attempt = 1; attempt <= options.maxAttempts; attempt += 1) {
45
- if (options.signal?.aborted)
46
- throw abortReason(options.signal);
47
- const outcome = {
48
- acceptance: "unknown",
49
- partialOutput: false,
50
- sideEffectStarted: false,
51
- };
52
- const context = {
53
- operationId: options.operationId,
54
- attempt,
55
- ...(options.signal ? { signal: options.signal } : {}),
56
- markNotAccepted() {
57
- if (outcome.acceptance !== "accepted")
58
- outcome.acceptance = "not-accepted";
59
- },
60
- markAccepted() {
61
- outcome.acceptance = "accepted";
62
- },
63
- markPartialOutput() {
64
- outcome.partialOutput = true;
65
- },
66
- markSideEffectStarted() {
67
- outcome.sideEffectStarted = true;
68
- },
69
- };
70
- try {
71
- return await options.execute(context);
72
- }
73
- catch (error) {
74
- if (options.signal?.aborted)
75
- throw abortReason(options.signal);
76
- if (attempt >= options.maxAttempts)
77
- throw error;
78
- const replayContext = Object.freeze({
44
+ if (options.deadlineAt !== undefined && (!Number.isFinite(options.deadlineAt) || options.deadlineAt <= 0)) {
45
+ throw new RangeError("deadlineAt must be a positive finite epoch timestamp");
46
+ }
47
+ if (options.deadlineAt !== undefined && options.deadlineAt <= Date.now()) {
48
+ throw new Error("The recovery operation deadline expired");
49
+ }
50
+ const deadlineController = options.deadlineAt === undefined ? undefined : new AbortController();
51
+ const deadlineTimer = deadlineController
52
+ ? setTimeout(() => deadlineController.abort(new Error("The recovery operation deadline expired")), Math.max(0, options.deadlineAt - Date.now()))
53
+ : undefined;
54
+ const signal = deadlineController
55
+ ? AbortSignal.any([...(options.signal ? [options.signal] : []), deadlineController.signal])
56
+ : options.signal;
57
+ try {
58
+ for (let attempt = 1; attempt <= options.maxAttempts; attempt += 1) {
59
+ if (signal?.aborted)
60
+ throw abortReason(signal);
61
+ const outcome = {
62
+ acceptance: "unknown",
63
+ partialOutput: false,
64
+ sideEffectStarted: false,
65
+ };
66
+ const context = {
79
67
  operationId: options.operationId,
80
68
  attempt,
81
- nextAttempt: attempt + 1,
82
- error,
83
- outcome: snapshot(outcome),
84
- ...(options.signal ? { signal: options.signal } : {}),
85
- });
86
- if (options.shouldRetry(error, replayContext) !== true)
87
- throw error;
88
- if (outcome.acceptance === "accepted" ||
89
- outcome.partialOutput ||
90
- outcome.sideEffectStarted) {
91
- throw error;
69
+ ...(signal ? { signal } : {}),
70
+ markNotAccepted() {
71
+ if (outcome.acceptance !== "accepted")
72
+ outcome.acceptance = "not-accepted";
73
+ },
74
+ markAccepted() {
75
+ outcome.acceptance = "accepted";
76
+ },
77
+ markPartialOutput() {
78
+ outcome.partialOutput = true;
79
+ },
80
+ markSideEffectStarted() {
81
+ outcome.sideEffectStarted = true;
82
+ },
83
+ };
84
+ try {
85
+ if (options.onAttemptStart)
86
+ await options.onAttemptStart(context);
87
+ const result = await options.execute(context);
88
+ if (options.onAttemptOutcome) {
89
+ await options.onAttemptOutcome(Object.freeze({
90
+ operationId: options.operationId,
91
+ attempt,
92
+ outcome: snapshot(outcome),
93
+ ...(signal ? { signal } : {}),
94
+ }));
95
+ }
96
+ return result;
97
+ }
98
+ catch (error) {
99
+ if (options.onAttemptOutcome) {
100
+ await options.onAttemptOutcome(Object.freeze({
101
+ operationId: options.operationId,
102
+ attempt,
103
+ outcome: snapshot(outcome),
104
+ ...(signal ? { signal } : {}),
105
+ error,
106
+ }));
107
+ }
108
+ if (signal?.aborted)
109
+ throw abortReason(signal);
110
+ if (attempt >= options.maxAttempts)
111
+ throw error;
112
+ const replayContext = Object.freeze({
113
+ operationId: options.operationId,
114
+ attempt,
115
+ nextAttempt: attempt + 1,
116
+ error,
117
+ outcome: snapshot(outcome),
118
+ ...(signal ? { signal } : {}),
119
+ });
120
+ if (options.shouldRetry(error, replayContext) !== true)
121
+ throw error;
122
+ if (outcome.acceptance === "accepted" ||
123
+ outcome.partialOutput ||
124
+ outcome.sideEffectStarted) {
125
+ throw error;
126
+ }
127
+ if ((await options.authorizeReplay(replayContext)) !== true)
128
+ throw error;
129
+ if (signal?.aborted)
130
+ throw abortReason(signal);
131
+ const configuredDelay = typeof options.backoffMs === "function"
132
+ ? options.backoffMs(attempt + 1, error)
133
+ : options.backoffMs ?? 0;
134
+ const delay = Math.min(30_000, Math.max(0, configuredDelay));
135
+ await abortableDelay(delay, signal);
92
136
  }
93
- if ((await options.authorizeReplay(replayContext)) !== true)
94
- throw error;
95
- if (options.signal?.aborted)
96
- throw abortReason(options.signal);
97
- const configuredDelay = typeof options.backoffMs === "function"
98
- ? options.backoffMs(attempt + 1, error)
99
- : options.backoffMs ?? 0;
100
- const delay = Math.min(30_000, Math.max(0, configuredDelay));
101
- await abortableDelay(delay, options.signal);
102
137
  }
103
138
  }
139
+ finally {
140
+ if (deadlineTimer !== undefined)
141
+ clearTimeout(deadlineTimer);
142
+ }
104
143
  throw new Error("Unreachable recovery state");
105
144
  }
106
145
  //# sourceMappingURL=recovery.js.map