@ezghcloud/workflows 0.0.0-stage → 1.0.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/LICENSE +202 -0
- package/README.md +417 -2
- package/dist/client.d.ts +209 -0
- package/dist/client.js +259 -0
- package/dist/env.d.ts +2 -0
- package/dist/env.js +32 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +7 -0
- package/dist/protocol/bytes.d.ts +8 -0
- package/dist/protocol/bytes.js +49 -0
- package/dist/protocol/callback.d.ts +6 -0
- package/dist/protocol/callback.js +31 -0
- package/dist/protocol/codec.d.ts +6 -0
- package/dist/protocol/codec.js +38 -0
- package/dist/protocol/derived.d.ts +5 -0
- package/dist/protocol/derived.js +41 -0
- package/dist/protocol/index.d.ts +14 -0
- package/dist/protocol/index.js +21 -0
- package/dist/protocol/public.d.ts +2 -0
- package/dist/protocol/public.js +1 -0
- package/dist/protocol/signature.d.ts +17 -0
- package/dist/protocol/signature.js +101 -0
- package/dist/protocol/stepid.d.ts +4 -0
- package/dist/protocol/stepid.js +9 -0
- package/dist/protocol/tokens.d.ts +2 -0
- package/dist/protocol/tokens.js +38 -0
- package/dist/protocol/types.d.ts +185 -0
- package/dist/protocol/types.js +1 -0
- package/dist/protocol/validate.d.ts +15 -0
- package/dist/protocol/validate.js +279 -0
- package/dist/queues.d.ts +47 -0
- package/dist/queues.js +126 -0
- package/dist/runtime/context.d.ts +82 -0
- package/dist/runtime/context.js +1 -0
- package/dist/runtime/errors.d.ts +49 -0
- package/dist/runtime/errors.js +160 -0
- package/dist/runtime/execution.d.ts +17 -0
- package/dist/runtime/execution.js +608 -0
- package/dist/runtime/invocation.d.ts +5 -0
- package/dist/runtime/invocation.js +47 -0
- package/dist/runtime/live.d.ts +23 -0
- package/dist/runtime/live.js +103 -0
- package/dist/runtime/workflow.d.ts +26 -0
- package/dist/runtime/workflow.js +49 -0
- package/dist/serve.d.ts +12 -0
- package/dist/serve.js +149 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/dist/worker.d.ts +43 -0
- package/dist/worker.js +364 -0
- package/package.json +49 -4
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// What the engine accepts from an SDK (workflows-stack internal/protocol validate.go and
|
|
2
|
+
// protocol/schemas), checked on this side: the SDK's tests run every message it sends through
|
|
3
|
+
// these, against workflows-stack's vectors (protocol/vectors/messages.json). Internal.
|
|
4
|
+
import { utf8Length } from "./bytes.js";
|
|
5
|
+
import { parseCallbackToken } from "./callback.js";
|
|
6
|
+
const MAX_RETRY_ATTEMPTS = 100;
|
|
7
|
+
const MAX_RETRY_MS = 86_400_000;
|
|
8
|
+
const MAX_MULTIPLIER = 10;
|
|
9
|
+
const MAX_STEP_NAME_BYTES = 256;
|
|
10
|
+
const MAX_STACK_BYTES = 8192;
|
|
11
|
+
const MAX_ERROR_TEXT_BYTES = 8192;
|
|
12
|
+
const MAX_ERROR_DATA_BYTES = 64 << 10;
|
|
13
|
+
export const MAX_ERROR_DEPTH = 8;
|
|
14
|
+
const MAX_VALUE_BYTES = 64 << 10;
|
|
15
|
+
const MAX_DEPLOYMENT_BYTES = 256;
|
|
16
|
+
export const MAX_PLANNED_STEPS = 1000;
|
|
17
|
+
const MAX_POLL_TASKS = 10;
|
|
18
|
+
const MAX_WORKFLOWS = 1000;
|
|
19
|
+
const MAX_SDK_NAME_BYTES = 200;
|
|
20
|
+
const MAX_SDK_VERSION_BYTES = 100;
|
|
21
|
+
const ERROR_CODE = /^(user|workflows\.[a-z_]+)$/;
|
|
22
|
+
const STEP_ID = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
23
|
+
const PAYLOAD_ID = /^pay_[a-z2-7]{26}$/;
|
|
24
|
+
const INVOCATION_ID = /^inv_[a-z2-7]{26}$/;
|
|
25
|
+
const WORKFLOW = /^[A-Za-z0-9_-]{1,64}$/;
|
|
26
|
+
export const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
27
|
+
const QUEUE = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
28
|
+
const WORKER_ID = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
29
|
+
const RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
|
|
30
|
+
const COMMAND_TYPES = new Set([
|
|
31
|
+
"stepStarted", "stepCompleted", "stepFailed", "valueRecorded", "sleep", "runCompleted", "runFailed",
|
|
32
|
+
"workflowNotFound", "callbackCreated", "waitForCallback", "invoke", "planParallel", "messageWaitCreated", "waitForMessage",
|
|
33
|
+
]);
|
|
34
|
+
// Message waits (protocol 1.2): the queue's ID, the key, and the timeout (1 s to 30 days).
|
|
35
|
+
export const MESSAGE_KEY = /^[A-Za-z0-9_.:-]{1,256}$/;
|
|
36
|
+
export const MIN_MESSAGE_TIMEOUT_MS = 1000;
|
|
37
|
+
export const MAX_MESSAGE_TIMEOUT_MS = 2_592_000_000;
|
|
38
|
+
export class InvalidMessage extends Error {
|
|
39
|
+
name = "InvalidMessage";
|
|
40
|
+
}
|
|
41
|
+
const fail = (msg) => { throw new InvalidMessage(msg); };
|
|
42
|
+
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
43
|
+
const isInt = (v) => typeof v === "number" && Number.isInteger(v);
|
|
44
|
+
const str = (v) => (typeof v === "string" ? v : "");
|
|
45
|
+
const jsonBytes = (v) => utf8Length(JSON.stringify(v));
|
|
46
|
+
export function validateRetry(p) {
|
|
47
|
+
if (!isObj(p))
|
|
48
|
+
fail("retry isn't an object");
|
|
49
|
+
const r = p;
|
|
50
|
+
if (!isInt(r.maxAttempts) || r.maxAttempts < 1 || r.maxAttempts > MAX_RETRY_ATTEMPTS)
|
|
51
|
+
fail(`retry.maxAttempts ${String(r.maxAttempts)}`);
|
|
52
|
+
if (!isInt(r.initialMs) || r.initialMs < 0)
|
|
53
|
+
fail(`retry.initialMs ${String(r.initialMs)}`);
|
|
54
|
+
if (!isInt(r.maxMs) || r.maxMs < 0 || r.maxMs > MAX_RETRY_MS)
|
|
55
|
+
fail(`retry.maxMs ${String(r.maxMs)}`);
|
|
56
|
+
if (typeof r.multiplier !== "number" || Number.isNaN(r.multiplier) || r.multiplier < 1 || r.multiplier > MAX_MULTIPLIER)
|
|
57
|
+
fail(`retry.multiplier ${String(r.multiplier)}`);
|
|
58
|
+
}
|
|
59
|
+
export function validateError(e) {
|
|
60
|
+
let cur = e;
|
|
61
|
+
for (let depth = 0; cur !== undefined; depth++) {
|
|
62
|
+
if (depth >= MAX_ERROR_DEPTH)
|
|
63
|
+
fail(`error causes nest deeper than ${MAX_ERROR_DEPTH}`);
|
|
64
|
+
if (!isObj(cur))
|
|
65
|
+
fail("error isn't an object");
|
|
66
|
+
const w = cur;
|
|
67
|
+
if (!ERROR_CODE.test(str(w.code)))
|
|
68
|
+
fail(`error code ${JSON.stringify(w.code)} isn't user or workflows.*`);
|
|
69
|
+
if (typeof w.name !== "string" || typeof w.message !== "string")
|
|
70
|
+
fail("error needs name and message");
|
|
71
|
+
if (utf8Length(str(w.name)) > MAX_ERROR_TEXT_BYTES || utf8Length(str(w.message)) > MAX_ERROR_TEXT_BYTES)
|
|
72
|
+
fail("error name or message too long");
|
|
73
|
+
if (w.stack !== undefined && (typeof w.stack !== "string" || utf8Length(w.stack) > MAX_STACK_BYTES))
|
|
74
|
+
fail("error stack too long");
|
|
75
|
+
if (w.data !== undefined && jsonBytes(w.data) > MAX_ERROR_DATA_BYTES)
|
|
76
|
+
fail("error data too long");
|
|
77
|
+
cur = w.cause;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const validName = (n) => typeof n === "string" && n !== "" && utf8Length(n) <= MAX_STEP_NAME_BYTES && !n.includes("\u0000");
|
|
81
|
+
export function validateCommand(cmd) {
|
|
82
|
+
if (!isObj(cmd))
|
|
83
|
+
fail("command isn't an object");
|
|
84
|
+
const c = cmd;
|
|
85
|
+
const type = str(c.type);
|
|
86
|
+
if (!COMMAND_TYPES.has(type))
|
|
87
|
+
fail(`unknown command type ${JSON.stringify(c.type)}`);
|
|
88
|
+
if (c.stepId !== undefined && !STEP_ID.test(str(c.stepId)))
|
|
89
|
+
fail(`stepId ${JSON.stringify(c.stepId)}`);
|
|
90
|
+
if (c.name !== undefined && (typeof c.name !== "string" || utf8Length(c.name) > MAX_STEP_NAME_BYTES || c.name.includes("\u0000")))
|
|
91
|
+
fail("name");
|
|
92
|
+
if (c.causeStepId !== undefined && !STEP_ID.test(str(c.causeStepId)))
|
|
93
|
+
fail("causeStepId");
|
|
94
|
+
if (c.value !== undefined && jsonBytes(c.value) > MAX_VALUE_BYTES)
|
|
95
|
+
fail("value too long");
|
|
96
|
+
if (c.attempt !== undefined && (!isInt(c.attempt) || c.attempt < 0))
|
|
97
|
+
fail("attempt");
|
|
98
|
+
if (c.retry !== undefined)
|
|
99
|
+
validateRetry(c.retry);
|
|
100
|
+
if (c.timeoutMs !== undefined && (!isInt(c.timeoutMs) || c.timeoutMs < 0))
|
|
101
|
+
fail("timeoutMs");
|
|
102
|
+
if (c.retryAfterMs !== undefined && (!isInt(c.retryAfterMs) || c.retryAfterMs < 0))
|
|
103
|
+
fail("retryAfterMs");
|
|
104
|
+
if (c.outputRef !== undefined && !PAYLOAD_ID.test(str(c.outputRef)))
|
|
105
|
+
fail("outputRef");
|
|
106
|
+
if (c.error !== undefined)
|
|
107
|
+
validateError(c.error);
|
|
108
|
+
if (c.workflow !== undefined && !WORKFLOW.test(str(c.workflow)))
|
|
109
|
+
fail(`workflow ${JSON.stringify(c.workflow)}`);
|
|
110
|
+
if (c.token !== undefined) {
|
|
111
|
+
if (type !== "callbackCreated")
|
|
112
|
+
fail(`token on ${type}`);
|
|
113
|
+
try {
|
|
114
|
+
parseCallbackToken(str(c.token));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
fail("token isn't a callback token");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (c.inputRef !== undefined && !PAYLOAD_ID.test(str(c.inputRef)))
|
|
121
|
+
fail("inputRef");
|
|
122
|
+
if (c.input !== undefined && c.inputRef !== undefined)
|
|
123
|
+
fail("input and inputRef are exclusive");
|
|
124
|
+
if ((c.input !== undefined || c.inputRef !== undefined) && type !== "invoke")
|
|
125
|
+
fail(`input on ${type}`);
|
|
126
|
+
if (c.steps !== undefined && type !== "planParallel")
|
|
127
|
+
fail(`steps on ${type}`);
|
|
128
|
+
if ((c.queueId !== undefined || c.key !== undefined) && type !== "messageWaitCreated")
|
|
129
|
+
fail(`queueId or key on ${type}`);
|
|
130
|
+
const step = () => { if (!STEP_ID.test(str(c.stepId)) || !validName(c.name))
|
|
131
|
+
fail(`${type} needs stepId and name`); };
|
|
132
|
+
switch (type) {
|
|
133
|
+
case "stepStarted":
|
|
134
|
+
case "stepFailed":
|
|
135
|
+
step();
|
|
136
|
+
if (!isInt(c.attempt) || c.attempt < 1)
|
|
137
|
+
fail(`${type} needs attempt ≥ 1`);
|
|
138
|
+
if (type === "stepStarted" && c.retry === undefined)
|
|
139
|
+
fail("stepStarted needs retry");
|
|
140
|
+
if (type === "stepFailed" && (c.error === undefined || typeof c.retryable !== "boolean"))
|
|
141
|
+
fail("stepFailed needs error and retryable");
|
|
142
|
+
break;
|
|
143
|
+
case "stepCompleted":
|
|
144
|
+
step();
|
|
145
|
+
if (!("output" in c) && c.outputRef === undefined)
|
|
146
|
+
fail("stepCompleted needs output");
|
|
147
|
+
break;
|
|
148
|
+
case "valueRecorded":
|
|
149
|
+
step();
|
|
150
|
+
if (!("value" in c))
|
|
151
|
+
fail("valueRecorded needs value");
|
|
152
|
+
break;
|
|
153
|
+
case "sleep":
|
|
154
|
+
step();
|
|
155
|
+
if (!RFC3339.test(str(c.until)) || Number.isNaN(Date.parse(str(c.until))))
|
|
156
|
+
fail("sleep needs until (RFC 3339)");
|
|
157
|
+
break;
|
|
158
|
+
case "runCompleted":
|
|
159
|
+
if (!("output" in c) && c.outputRef === undefined)
|
|
160
|
+
fail("runCompleted needs output");
|
|
161
|
+
break;
|
|
162
|
+
case "runFailed":
|
|
163
|
+
if (c.error === undefined)
|
|
164
|
+
fail("runFailed needs error");
|
|
165
|
+
break;
|
|
166
|
+
case "workflowNotFound":
|
|
167
|
+
if (c.workflow === undefined)
|
|
168
|
+
fail("workflowNotFound needs workflow");
|
|
169
|
+
break;
|
|
170
|
+
case "callbackCreated":
|
|
171
|
+
step();
|
|
172
|
+
if (c.token === undefined || !isInt(c.timeoutMs) || c.timeoutMs < 1)
|
|
173
|
+
fail("callbackCreated needs token and timeoutMs ≥ 1");
|
|
174
|
+
break;
|
|
175
|
+
case "waitForCallback":
|
|
176
|
+
case "waitForMessage":
|
|
177
|
+
if (!STEP_ID.test(str(c.stepId)))
|
|
178
|
+
fail(`${type} needs stepId`);
|
|
179
|
+
break;
|
|
180
|
+
case "messageWaitCreated":
|
|
181
|
+
step();
|
|
182
|
+
if (!UUID.test(str(c.queueId)))
|
|
183
|
+
fail("messageWaitCreated needs queueId (a UUID)");
|
|
184
|
+
if (!MESSAGE_KEY.test(str(c.key)))
|
|
185
|
+
fail("messageWaitCreated needs key (^[A-Za-z0-9_.:-]{1,256}$)");
|
|
186
|
+
if (!isInt(c.timeoutMs) || c.timeoutMs < MIN_MESSAGE_TIMEOUT_MS || c.timeoutMs > MAX_MESSAGE_TIMEOUT_MS)
|
|
187
|
+
fail("messageWaitCreated needs timeoutMs from 1,000 to 2,592,000,000");
|
|
188
|
+
break;
|
|
189
|
+
case "invoke":
|
|
190
|
+
step();
|
|
191
|
+
if (c.workflow === undefined)
|
|
192
|
+
fail("invoke needs workflow");
|
|
193
|
+
break;
|
|
194
|
+
case "planParallel": {
|
|
195
|
+
const steps = c.steps;
|
|
196
|
+
if (!Array.isArray(steps) || steps.length === 0 || steps.length > MAX_PLANNED_STEPS)
|
|
197
|
+
fail(`planParallel needs 1-${MAX_PLANNED_STEPS} steps`);
|
|
198
|
+
const seen = new Set();
|
|
199
|
+
for (const [i, s] of steps.entries()) {
|
|
200
|
+
if (!isObj(s) || !STEP_ID.test(str(s.stepId)) || !validName(s.name))
|
|
201
|
+
fail(`steps[${i}]: stepId and name`);
|
|
202
|
+
const p = s;
|
|
203
|
+
if (seen.has(str(p.stepId)))
|
|
204
|
+
fail(`steps[${i}]: stepId twice`);
|
|
205
|
+
seen.add(str(p.stepId));
|
|
206
|
+
if (p.retry !== undefined)
|
|
207
|
+
validateRetry(p.retry);
|
|
208
|
+
if (p.timeoutMs !== undefined && (!isInt(p.timeoutMs) || p.timeoutMs < 0))
|
|
209
|
+
fail(`steps[${i}]: timeoutMs`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function validateSDK(s) {
|
|
215
|
+
if (!isObj(s))
|
|
216
|
+
fail("sdk is required");
|
|
217
|
+
const sdk = s;
|
|
218
|
+
if (str(sdk.name).trim() === "" || str(sdk.version).trim() === "")
|
|
219
|
+
fail("sdk name and version are required");
|
|
220
|
+
if (utf8Length(str(sdk.name)) > MAX_SDK_NAME_BYTES || utf8Length(str(sdk.version)) > MAX_SDK_VERSION_BYTES)
|
|
221
|
+
fail("sdk name or version too long");
|
|
222
|
+
}
|
|
223
|
+
function validateWorkflows(ws) {
|
|
224
|
+
if (ws === undefined)
|
|
225
|
+
return;
|
|
226
|
+
if (!Array.isArray(ws) || ws.length > MAX_WORKFLOWS)
|
|
227
|
+
fail(`at most ${MAX_WORKFLOWS} workflows`);
|
|
228
|
+
const seen = new Set();
|
|
229
|
+
for (const [i, w] of ws.entries()) {
|
|
230
|
+
if (!isObj(w) || !WORKFLOW.test(str(w.name)) || seen.has(str(w.name)))
|
|
231
|
+
fail(`workflows[${i}].name is invalid or repeated`);
|
|
232
|
+
const wf = w;
|
|
233
|
+
seen.add(str(wf.name));
|
|
234
|
+
if (wf.timeoutMs !== undefined && (!isInt(wf.timeoutMs) || wf.timeoutMs < 0))
|
|
235
|
+
fail(`workflow ${str(wf.name)}: timeoutMs`);
|
|
236
|
+
if (wf.retry !== undefined)
|
|
237
|
+
validateRetry(wf.retry);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
export function validateCheckpoint(cp) {
|
|
241
|
+
if (!isObj(cp))
|
|
242
|
+
fail("checkpoint isn't an object");
|
|
243
|
+
const c = cp;
|
|
244
|
+
if (!INVOCATION_ID.test(str(c.invocationId)))
|
|
245
|
+
fail("invocationId");
|
|
246
|
+
if (!isInt(c.seq) || c.seq < 1)
|
|
247
|
+
fail("seq");
|
|
248
|
+
if (!Array.isArray(c.commands))
|
|
249
|
+
fail("commands");
|
|
250
|
+
c.commands.forEach(validateCommand);
|
|
251
|
+
}
|
|
252
|
+
export function validateInvocationResult(r) {
|
|
253
|
+
if (!isObj(r))
|
|
254
|
+
fail("result isn't an object");
|
|
255
|
+
const res = r;
|
|
256
|
+
if (!Array.isArray(res.commands))
|
|
257
|
+
fail("commands");
|
|
258
|
+
validateSDK(res.sdk);
|
|
259
|
+
if (res.deployment !== undefined && (typeof res.deployment !== "string" || utf8Length(res.deployment) > MAX_DEPLOYMENT_BYTES || res.deployment.includes("\u0000")))
|
|
260
|
+
fail("deployment");
|
|
261
|
+
res.commands.forEach(validateCommand);
|
|
262
|
+
}
|
|
263
|
+
export function validatePollRequest(r) {
|
|
264
|
+
if (!isObj(r))
|
|
265
|
+
fail("poll isn't an object");
|
|
266
|
+
const p = r;
|
|
267
|
+
if (!UUID.test(str(p.app)))
|
|
268
|
+
fail(`app ${JSON.stringify(p.app)}`);
|
|
269
|
+
if (!QUEUE.test(str(p.queue)))
|
|
270
|
+
fail(`queue ${JSON.stringify(p.queue)}`);
|
|
271
|
+
if (!isInt(p.maxTasks) || p.maxTasks < 1 || p.maxTasks > MAX_POLL_TASKS)
|
|
272
|
+
fail(`maxTasks ${String(p.maxTasks)}`);
|
|
273
|
+
if (!WORKER_ID.test(str(p.workerId)))
|
|
274
|
+
fail("workerId");
|
|
275
|
+
if (!Array.isArray(p.protocolVersions) || p.protocolVersions.length === 0 || !p.protocolVersions.every((v) => /^\d+\.\d+$/.test(str(v))))
|
|
276
|
+
fail("protocolVersions");
|
|
277
|
+
validateSDK(p.sdk);
|
|
278
|
+
validateWorkflows(p.workflows);
|
|
279
|
+
}
|
package/dist/queues.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { FetchLike } from "./runtime/live.ts";
|
|
2
|
+
export interface QueueMessageInput {
|
|
3
|
+
body: string;
|
|
4
|
+
attributes?: Record<string, string>;
|
|
5
|
+
delaySeconds?: number;
|
|
6
|
+
groupId?: string;
|
|
7
|
+
dedupId?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface SentMessage {
|
|
10
|
+
messageId: string;
|
|
11
|
+
seq?: string;
|
|
12
|
+
duplicate?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface SentMessages {
|
|
15
|
+
messages: SentMessage[];
|
|
16
|
+
}
|
|
17
|
+
export declare class QueuesApiError extends Error {
|
|
18
|
+
readonly status: number;
|
|
19
|
+
readonly code: string;
|
|
20
|
+
readonly retryAfterSeconds?: number | undefined;
|
|
21
|
+
name: string;
|
|
22
|
+
readonly data: {
|
|
23
|
+
status: number;
|
|
24
|
+
code: string;
|
|
25
|
+
};
|
|
26
|
+
constructor(status: number, code: string, message: string, retryAfterSeconds?: number | undefined);
|
|
27
|
+
}
|
|
28
|
+
export interface QueuesClientOptions {
|
|
29
|
+
apiKey: string;
|
|
30
|
+
organizationId: string;
|
|
31
|
+
projectId: string;
|
|
32
|
+
baseUrl?: string;
|
|
33
|
+
fetch?: FetchLike;
|
|
34
|
+
retries?: number;
|
|
35
|
+
maxRetryDelayMs?: number;
|
|
36
|
+
timeoutMs?: number;
|
|
37
|
+
}
|
|
38
|
+
export declare const DEFAULT_QUEUES_BASE = "https://queues-api.ezghcloud.com";
|
|
39
|
+
export declare class QueuesClient {
|
|
40
|
+
#private;
|
|
41
|
+
constructor(opts: QueuesClientOptions);
|
|
42
|
+
send(queueId: string, messages: QueueMessageInput[], opts?: {
|
|
43
|
+
idempotencyKey?: string;
|
|
44
|
+
}): Promise<SentMessages>;
|
|
45
|
+
}
|
|
46
|
+
export declare function sendToQueueDigest(runId: string, stepId: string): Promise<string>;
|
|
47
|
+
export declare function withDedupIds(messages: QueueMessageInput[], digest: string): QueueMessageInput[];
|
package/dist/queues.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { toHex, utf8 } from "./protocol/bytes.js";
|
|
2
|
+
// An error answer from the Queues API: the HTTP status and the platform error code
|
|
3
|
+
// (status 0, code "network_error" when every attempt failed in transit).
|
|
4
|
+
// data ({ status, code }) is what a step failure records, so a replay's WorkflowError keeps them.
|
|
5
|
+
export class QueuesApiError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
code;
|
|
8
|
+
retryAfterSeconds;
|
|
9
|
+
name = "QueuesApiError";
|
|
10
|
+
data;
|
|
11
|
+
constructor(status, code, message, retryAfterSeconds) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
16
|
+
this.data = { status, code };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export const DEFAULT_QUEUES_BASE = "https://queues-api.ezghcloud.com";
|
|
20
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
21
|
+
const IDEMPOTENCY_KEY = /^[\x21-\x7e]{1,200}$/;
|
|
22
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
23
|
+
const id = encodeURIComponent;
|
|
24
|
+
function apiError(res, data) {
|
|
25
|
+
const e = data?.error;
|
|
26
|
+
if (res.status >= 300 && res.status < 400)
|
|
27
|
+
return new QueuesApiError(res.status, "redirect", `The Queues API answered a redirect (${res.status}); requests are never redirected, so check baseUrl`);
|
|
28
|
+
const raw = res.headers.get("Retry-After");
|
|
29
|
+
const ra = raw === null ? NaN : Number(raw);
|
|
30
|
+
return new QueuesApiError(res.status, typeof e?.code === "string" ? e.code : "unknown", typeof e?.message === "string" ? e.message : `HTTP ${res.status}`, Number.isFinite(ra) && ra >= 0 ? ra : undefined);
|
|
31
|
+
}
|
|
32
|
+
// resendable: answers a resend with the same Idempotency-Key may fix.
|
|
33
|
+
function resendable(err) {
|
|
34
|
+
if (err.status === 429)
|
|
35
|
+
return err.code !== "quota_exceeded";
|
|
36
|
+
if (err.status === 409)
|
|
37
|
+
return err.code === "idempotency_in_progress";
|
|
38
|
+
return err.status >= 500;
|
|
39
|
+
}
|
|
40
|
+
// QueuesClient sends messages to EZGH Queues for one project. Pass it to step.sendToQueue, or call
|
|
41
|
+
// send directly outside a workflow.
|
|
42
|
+
export class QueuesClient {
|
|
43
|
+
// ES private fields: the API key never shows in console.log, util.inspect or JSON.stringify.
|
|
44
|
+
#apiKey;
|
|
45
|
+
#base;
|
|
46
|
+
#fetch;
|
|
47
|
+
#retries;
|
|
48
|
+
#maxDelay;
|
|
49
|
+
#timeout;
|
|
50
|
+
constructor(opts) {
|
|
51
|
+
if (typeof opts?.apiKey !== "string" || opts.apiKey === "")
|
|
52
|
+
throw new TypeError("QueuesClient needs an apiKey");
|
|
53
|
+
if (typeof opts.organizationId !== "string" || opts.organizationId === "")
|
|
54
|
+
throw new TypeError("QueuesClient needs an organizationId");
|
|
55
|
+
if (typeof opts.projectId !== "string" || opts.projectId === "")
|
|
56
|
+
throw new TypeError("QueuesClient needs a projectId");
|
|
57
|
+
this.#apiKey = opts.apiKey;
|
|
58
|
+
this.#base = `${(opts.baseUrl ?? DEFAULT_QUEUES_BASE).replace(/\/+$/, "")}/v1/organizations/${id(opts.organizationId)}/projects/${id(opts.projectId)}`;
|
|
59
|
+
this.#fetch = opts.fetch ?? ((u, i) => fetch(u, i));
|
|
60
|
+
this.#retries = opts.retries ?? 3;
|
|
61
|
+
this.#maxDelay = opts.maxRetryDelayMs ?? 10_000;
|
|
62
|
+
this.#timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
63
|
+
if (!Number.isInteger(this.#retries) || this.#retries < 0)
|
|
64
|
+
throw new TypeError("retries is a whole number ≥ 0");
|
|
65
|
+
if (!Number.isFinite(this.#maxDelay) || this.#maxDelay < 0)
|
|
66
|
+
throw new TypeError("maxRetryDelayMs is a number ≥ 0");
|
|
67
|
+
if (!Number.isFinite(this.#timeout) || this.#timeout <= 0)
|
|
68
|
+
throw new TypeError("timeoutMs is a number > 0");
|
|
69
|
+
}
|
|
70
|
+
// send sends 1–10 messages to a queue (queues.messages.send) in one request: all are stored, or
|
|
71
|
+
// none. Every attempt carries the same Idempotency-Key (yours, or a random one), which the API
|
|
72
|
+
// honors for 48 hours: a resend answers the first response instead of sending again.
|
|
73
|
+
async send(queueId, messages, opts = {}) {
|
|
74
|
+
if (typeof queueId !== "string" || queueId === "")
|
|
75
|
+
throw new TypeError("send needs a queueId");
|
|
76
|
+
if (!Array.isArray(messages) || messages.length < 1 || messages.length > 10)
|
|
77
|
+
throw new TypeError("send takes 1 to 10 messages");
|
|
78
|
+
const key = opts.idempotencyKey ?? crypto.randomUUID();
|
|
79
|
+
if (typeof key !== "string" || !IDEMPOTENCY_KEY.test(key))
|
|
80
|
+
throw new TypeError("An idempotencyKey is 1–200 printable ASCII characters");
|
|
81
|
+
const headers = { Authorization: `Bearer ${this.#apiKey}`, Accept: "application/json", "Content-Type": "application/json", "Idempotency-Key": key };
|
|
82
|
+
const body = JSON.stringify({ messages });
|
|
83
|
+
const url = `${this.#base}/queues/${id(queueId)}/messages`;
|
|
84
|
+
for (let attempt = 0;; attempt++) {
|
|
85
|
+
let res;
|
|
86
|
+
try {
|
|
87
|
+
res = await this.#fetch(url, { method: "POST", headers, body, redirect: "manual", signal: AbortSignal.timeout(this.#timeout) });
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
if (attempt >= this.#retries)
|
|
91
|
+
throw new QueuesApiError(0, "network_error", `Sending to the queue failed in transit: ${e instanceof Error ? e.message : String(e)}`);
|
|
92
|
+
await sleep(Math.min(this.#maxDelay, 500 * 2 ** attempt));
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const text = await res.text();
|
|
96
|
+
let data;
|
|
97
|
+
try {
|
|
98
|
+
data = text ? JSON.parse(text) : undefined;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
data = undefined;
|
|
102
|
+
}
|
|
103
|
+
if (res.ok) {
|
|
104
|
+
if (!Array.isArray(data?.messages))
|
|
105
|
+
throw new QueuesApiError(res.status, "invalid_response", "The Queues API answered without a messages list");
|
|
106
|
+
return data;
|
|
107
|
+
}
|
|
108
|
+
const err = apiError(res, data);
|
|
109
|
+
if (!resendable(err) || attempt >= this.#retries)
|
|
110
|
+
throw err;
|
|
111
|
+
await sleep(Math.min(this.#maxDelay, err.retryAfterSeconds !== undefined ? err.retryAfterSeconds * 1000 : 500 * 2 ** attempt));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// sendToQueueDigest is step.sendToQueue's key material: lowercase hex of
|
|
116
|
+
// SHA-256(runId + ":" + stepId). The step sends Idempotency-Key "wfq_" + digest, and gives each
|
|
117
|
+
// FIFO message (one with a groupId) without a dedupId the dedupId digest + ":" + its index.
|
|
118
|
+
export async function sendToQueueDigest(runId, stepId) {
|
|
119
|
+
return toHex(new Uint8Array(await crypto.subtle.digest("SHA-256", utf8(`${runId}:${stepId}`))));
|
|
120
|
+
}
|
|
121
|
+
// withDedupIds: the messages step.sendToQueue sends, a FIFO message (one with a groupId) given the
|
|
122
|
+
// dedupId digest:index unless it has its own. Standard queues refuse dedupId, so messages without
|
|
123
|
+
// a groupId are sent as they are.
|
|
124
|
+
export function withDedupIds(messages, digest) {
|
|
125
|
+
return messages.map((m, i) => (m && typeof m === "object" && m.groupId !== undefined && m.dedupId === undefined ? { ...m, dedupId: `${digest}:${i}` } : m));
|
|
126
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { RetryPolicy } from "../protocol/index.ts";
|
|
2
|
+
import type { QueueMessageInput, QueuesClient, SentMessages } from "../queues.ts";
|
|
3
|
+
import type { Workflow } from "./workflow.ts";
|
|
4
|
+
export type Jsonify<T> = T extends Date ? string : T extends undefined ? null : T extends (infer U)[] ? Jsonify<U>[] : T extends object ? JsonifyObject<T> : T;
|
|
5
|
+
type JsonifyObject<T> = Flatten<{
|
|
6
|
+
[K in keyof T as undefined extends T[K] ? K : never]?: Jsonify<Exclude<T[K], undefined>>;
|
|
7
|
+
} & {
|
|
8
|
+
[K in keyof T as undefined extends T[K] ? never : K]: Jsonify<T[K]>;
|
|
9
|
+
}>;
|
|
10
|
+
type Flatten<T> = {
|
|
11
|
+
[K in keyof T]: T[K];
|
|
12
|
+
};
|
|
13
|
+
export interface StepRunOptions {
|
|
14
|
+
retry?: Partial<RetryPolicy>;
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
id?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface Callback<T = unknown> {
|
|
19
|
+
readonly url: string;
|
|
20
|
+
readonly token: string;
|
|
21
|
+
wait(): Promise<Jsonify<T>>;
|
|
22
|
+
}
|
|
23
|
+
export interface CreateCallbackOptions {
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
id?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface InvokeOptions<I = unknown, O = unknown> {
|
|
28
|
+
workflow: string | Workflow<I, O>;
|
|
29
|
+
input?: I;
|
|
30
|
+
id?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface ReceivedMessage {
|
|
33
|
+
messageId: string;
|
|
34
|
+
body: string;
|
|
35
|
+
attributes: Record<string, string>;
|
|
36
|
+
sentAt: string;
|
|
37
|
+
queueId: string;
|
|
38
|
+
groupId?: string;
|
|
39
|
+
seq?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface WaitForMessageOptions {
|
|
42
|
+
queueId: string;
|
|
43
|
+
key: string;
|
|
44
|
+
timeoutSeconds: number;
|
|
45
|
+
id?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface StepRunContext {
|
|
48
|
+
signal: AbortSignal;
|
|
49
|
+
attempt: number;
|
|
50
|
+
idempotencyKey: string;
|
|
51
|
+
}
|
|
52
|
+
export interface Step {
|
|
53
|
+
run<T>(name: string, fn: (ctx: StepRunContext) => Promise<T> | T, opts?: StepRunOptions): Promise<Jsonify<T>>;
|
|
54
|
+
sleep(name: string, duration: string | number, opts?: {
|
|
55
|
+
id?: string;
|
|
56
|
+
}): Promise<void>;
|
|
57
|
+
sleepUntil(name: string, at: Date | string, opts?: {
|
|
58
|
+
id?: string;
|
|
59
|
+
}): Promise<void>;
|
|
60
|
+
now(name: string): Promise<Date>;
|
|
61
|
+
uuid(name: string): Promise<string>;
|
|
62
|
+
createCallback<T = unknown>(name: string, opts?: CreateCallbackOptions): Promise<Callback<T>>;
|
|
63
|
+
invoke<I = unknown, O = unknown>(name: string, opts: InvokeOptions<I, O>): Promise<Jsonify<O>>;
|
|
64
|
+
sendToQueue(name: string, client: QueuesClient, queueId: string, messages: QueueMessageInput[]): Promise<SentMessages>;
|
|
65
|
+
waitForMessage(name: string, opts: WaitForMessageOptions): Promise<ReceivedMessage | null>;
|
|
66
|
+
idempotencyKey(): string;
|
|
67
|
+
}
|
|
68
|
+
export interface WorkflowContext<I> {
|
|
69
|
+
input: I;
|
|
70
|
+
run: {
|
|
71
|
+
id: string;
|
|
72
|
+
runKey?: string;
|
|
73
|
+
attempt: number;
|
|
74
|
+
schedule?: {
|
|
75
|
+
id: string;
|
|
76
|
+
scheduledTime: string;
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
step: Step;
|
|
80
|
+
traceparent: string;
|
|
81
|
+
}
|
|
82
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { WireError } from "../protocol/index.ts";
|
|
2
|
+
export declare class NonRetriableError extends Error {
|
|
3
|
+
name: string;
|
|
4
|
+
readonly code = "workflows.non_retriable";
|
|
5
|
+
}
|
|
6
|
+
export declare class RetryAfterError extends Error {
|
|
7
|
+
readonly delayMs: number;
|
|
8
|
+
name: string;
|
|
9
|
+
constructor(message: string, delayMs: number);
|
|
10
|
+
}
|
|
11
|
+
export declare class CancelledError extends Error {
|
|
12
|
+
name: string;
|
|
13
|
+
readonly code = "workflows.cancelled";
|
|
14
|
+
}
|
|
15
|
+
export declare class WorkflowError extends Error {
|
|
16
|
+
code: string;
|
|
17
|
+
data?: unknown;
|
|
18
|
+
constructor(message: string, opts?: {
|
|
19
|
+
code?: string;
|
|
20
|
+
name?: string;
|
|
21
|
+
data?: unknown;
|
|
22
|
+
cause?: unknown;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export declare class CallbackTimeout extends WorkflowError {
|
|
26
|
+
constructor(message: string, opts?: {
|
|
27
|
+
name?: string;
|
|
28
|
+
data?: unknown;
|
|
29
|
+
cause?: unknown;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export declare class ChildFailed extends WorkflowError {
|
|
33
|
+
constructor(message: string, opts?: {
|
|
34
|
+
name?: string;
|
|
35
|
+
data?: unknown;
|
|
36
|
+
cause?: unknown;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export declare class StepTimeout extends Error {
|
|
40
|
+
name: string;
|
|
41
|
+
}
|
|
42
|
+
export declare class DuplicateStepName extends Error {
|
|
43
|
+
name: string;
|
|
44
|
+
}
|
|
45
|
+
export declare class OutputTooLarge extends Error {
|
|
46
|
+
name: string;
|
|
47
|
+
}
|
|
48
|
+
export declare function encodeError(e: unknown, depth?: number): WireError;
|
|
49
|
+
export declare function decodeError(w: WireError): Error;
|