two-stroke 7.4.0 → 7.6.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/.github/workflows/dependabot.yml +1 -1
- package/bin/api-types.mjs +3 -11
- package/bin/bulk.mjs +7 -10
- package/bin/lint.mjs +1 -1
- package/bin/test.mjs +2 -7
- package/package.json +4 -4
- package/src/fake.ts +1 -1
- package/src/index.ts +42 -77
- package/src/once.ts +111 -113
- package/src/open-api.ts +49 -59
- package/src/test.ts +18 -22
- package/src/types.ts +4 -18
- package/tsconfig.json +2 -6
- package/worker-configuration.d.ts +10953 -9819
- package/wrangler.jsonc +4 -7
package/bin/api-types.mjs
CHANGED
|
@@ -7,9 +7,7 @@ import ts from "typescript";
|
|
|
7
7
|
|
|
8
8
|
const services = process.argv[2].split(",");
|
|
9
9
|
|
|
10
|
-
const UUID = ts.factory.createTypeReferenceNode(
|
|
11
|
-
ts.factory.createIdentifier("UUID"),
|
|
12
|
-
);
|
|
10
|
+
const UUID = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("UUID"));
|
|
13
11
|
const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull());
|
|
14
12
|
|
|
15
13
|
await Promise.all(
|
|
@@ -17,9 +15,7 @@ await Promise.all(
|
|
|
17
15
|
const output = await openapiTS(`${service}/doc`, {
|
|
18
16
|
transform(schemaObject) {
|
|
19
17
|
if ("format" in schemaObject && schemaObject.format === "uuid") {
|
|
20
|
-
return schemaObject.nullable
|
|
21
|
-
? ts.factory.createUnionTypeNode([UUID, NULL])
|
|
22
|
-
: UUID;
|
|
18
|
+
return schemaObject.nullable ? ts.factory.createUnionTypeNode([UUID, NULL]) : UUID;
|
|
23
19
|
}
|
|
24
20
|
},
|
|
25
21
|
});
|
|
@@ -29,11 +25,7 @@ await Promise.all(
|
|
|
29
25
|
"",
|
|
30
26
|
ts.ScriptTarget.Latest,
|
|
31
27
|
);
|
|
32
|
-
const result = printer.printNode(
|
|
33
|
-
ts.EmitHint.Unspecified,
|
|
34
|
-
output[0],
|
|
35
|
-
resultFile,
|
|
36
|
-
);
|
|
28
|
+
const result = printer.printNode(ts.EmitHint.Unspecified, output[0], resultFile);
|
|
37
29
|
fs.writeFileSync(
|
|
38
30
|
`src/__definitions__/${service.replace("https://", "")}-definitions.ts`,
|
|
39
31
|
await prettier.format(
|
package/bin/bulk.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { cmd } from "../src/cmd.mjs";
|
|
|
6
6
|
import mime from "mime";
|
|
7
7
|
|
|
8
8
|
const entryOrRest = process.argv[2];
|
|
9
|
-
const entry = process.argv
|
|
9
|
+
const entry = process.argv.slice(3);
|
|
10
10
|
|
|
11
11
|
const ents = await fs.promises.readdir("dist", {
|
|
12
12
|
withFileTypes: true,
|
|
@@ -16,16 +16,14 @@ const files = await Promise.all(
|
|
|
16
16
|
ents
|
|
17
17
|
.filter((ent) => ent.isFile())
|
|
18
18
|
.filter((ent) =>
|
|
19
|
-
entryOrRest == "entry" ? ent.name
|
|
19
|
+
entryOrRest == "entry" ? entry.includes(ent.name) : !entry.includes(ent.name),
|
|
20
20
|
)
|
|
21
21
|
.map((ent) => {
|
|
22
22
|
const type = mime.getType(`${ent.parentPath}/${ent.name}`);
|
|
23
23
|
const key = `${ent.parentPath.substring(4)}/${ent.name}`;
|
|
24
24
|
return (async () => ({
|
|
25
|
-
key: ent.name
|
|
26
|
-
value: (
|
|
27
|
-
await fs.promises.readFile(`${ent.parentPath}/${ent.name}`)
|
|
28
|
-
).toString("base64"),
|
|
25
|
+
key: entry.includes(ent.name) ? `${process.env.DOMAIN}${key}` : key,
|
|
26
|
+
value: (await fs.promises.readFile(`${ent.parentPath}/${ent.name}`)).toString("base64"),
|
|
29
27
|
base64: true,
|
|
30
28
|
metadata: {
|
|
31
29
|
"Content-Type":
|
|
@@ -34,15 +32,14 @@ const files = await Promise.all(
|
|
|
34
32
|
: type === "text/javascript"
|
|
35
33
|
? "text/javascript;charset=utf-8"
|
|
36
34
|
: type,
|
|
37
|
-
"Cache-Control":
|
|
38
|
-
ent.name === entry ? "nocache" : "max-age=31536000, immutable",
|
|
35
|
+
"Cache-Control": entry.includes(ent.name) ? "nocache" : "max-age=31536000, immutable",
|
|
39
36
|
},
|
|
40
37
|
}))();
|
|
41
38
|
}),
|
|
42
39
|
);
|
|
43
40
|
|
|
44
|
-
fs.writeFileSync(`dist/bulk_${
|
|
41
|
+
fs.writeFileSync(`dist/bulk_${entryOrRest}.json`, JSON.stringify(files));
|
|
45
42
|
|
|
46
43
|
cmd(
|
|
47
|
-
`wrangler kv bulk put dist/bulk_${
|
|
44
|
+
`wrangler kv bulk put dist/bulk_${entryOrRest}.json --remote --namespace-id ${process.env.NAMESPACE}`,
|
|
48
45
|
);
|
package/bin/lint.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import fs from "fs";
|
|
|
4
4
|
import { cmd } from "../src/cmd.mjs";
|
|
5
5
|
|
|
6
6
|
if (fs.existsSync("wrangler.jsonc")) {
|
|
7
|
-
|
|
7
|
+
cmd("wrangler types --strict-vars false --check");
|
|
8
8
|
}
|
|
9
9
|
cmd("eslint --cache --max-warnings=0");
|
|
10
10
|
cmd("prettier --cache --check .");
|
package/bin/test.mjs
CHANGED
|
@@ -25,11 +25,7 @@ if (fs.existsSync("wrangler.jsonc")) {
|
|
|
25
25
|
if (request.status === 200) {
|
|
26
26
|
const types = await openapiTS(await consumers.json(request.body));
|
|
27
27
|
const printer = ts.createPrinter({});
|
|
28
|
-
const resultFile = ts.createSourceFile(
|
|
29
|
-
"test/api.d.ts",
|
|
30
|
-
"",
|
|
31
|
-
ts.ScriptTarget.Latest,
|
|
32
|
-
);
|
|
28
|
+
const resultFile = ts.createSourceFile("test/api.d.ts", "", ts.ScriptTarget.Latest);
|
|
33
29
|
const result = types
|
|
34
30
|
.map((t) => printer.printNode(ts.EmitHint.Unspecified, t, resultFile))
|
|
35
31
|
.join("\n\n");
|
|
@@ -40,8 +36,7 @@ if (fs.existsSync("wrangler.jsonc")) {
|
|
|
40
36
|
}
|
|
41
37
|
}
|
|
42
38
|
cmd("vitest", [
|
|
43
|
-
...(!process.argv.slice(2).includes("-w") &&
|
|
44
|
-
!process.argv.slice(2).includes("--watch")
|
|
39
|
+
...(!process.argv.slice(2).includes("-w") && !process.argv.slice(2).includes("--watch")
|
|
45
40
|
? ["--run"]
|
|
46
41
|
: []),
|
|
47
42
|
...process.argv.slice(2),
|
package/package.json
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
"type-check": "./bin/type-check.mjs"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@cloudflare/vitest-pool-workers": "^0.
|
|
13
|
+
"@cloudflare/vitest-pool-workers": "^0.15.2",
|
|
14
14
|
"@sentry/cli": "^3.3.4",
|
|
15
15
|
"@types/node": "^25.5.0",
|
|
16
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
16
|
+
"@typescript-eslint/eslint-plugin": "^8.59.1",
|
|
17
17
|
"@typescript-eslint/parser": "^8.57.2",
|
|
18
18
|
"@vitest/runner": "^4.1.2",
|
|
19
19
|
"@vitest/snapshot": "^4.1.2",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"openapi-typescript": "^7.13.0",
|
|
30
30
|
"pbkdf-subtle": "^1.1.5",
|
|
31
31
|
"toucan-js": "^4.1.1",
|
|
32
|
-
"zod": "^4.
|
|
32
|
+
"zod": "^4.4.2"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"@sentry/cli": ">=2",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"url": "https://github.com/change-engine/two-stroke"
|
|
60
60
|
},
|
|
61
61
|
"type": "module",
|
|
62
|
-
"version": "7.
|
|
62
|
+
"version": "7.6.0",
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@types/eslint": "^9.6.1",
|
|
65
65
|
"eslint": "^10.1.0",
|
package/src/fake.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -3,15 +3,14 @@ import { verify as pbkdfVerify } from "pbkdf-subtle";
|
|
|
3
3
|
import { Toucan } from "toucan-js";
|
|
4
4
|
import { type ZodSafeParseResult, z, ZodObject, ZodType } from "zod/v4";
|
|
5
5
|
import { openAPI } from "./open-api";
|
|
6
|
-
import { type
|
|
6
|
+
import { type Handler, type Route } from "./types";
|
|
7
7
|
|
|
8
8
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
9
9
|
const noAuth = async () => null;
|
|
10
10
|
|
|
11
|
-
const escapeRegex = (str: string) =>
|
|
12
|
-
str.replace(/([.*+?^=!:$()|[\]\\])/g, "\\$&");
|
|
11
|
+
const escapeRegex = (str: string) => str.replace(/([.*+?^=!:$()|[\]\\])/g, "\\$&");
|
|
13
12
|
|
|
14
|
-
export function twoStroke<T
|
|
13
|
+
export function twoStroke<T>(
|
|
15
14
|
title: string,
|
|
16
15
|
release: string,
|
|
17
16
|
origin?: (o: string | null) => string,
|
|
@@ -35,11 +34,7 @@ export function twoStroke<T extends Env>(
|
|
|
35
34
|
const crons: {
|
|
36
35
|
[cron: string]: (c: { env: T; sentry: Toucan }) => Promise<void>;
|
|
37
36
|
} = {};
|
|
38
|
-
let _email: (c: {
|
|
39
|
-
message: ForwardableEmailMessage;
|
|
40
|
-
env: T;
|
|
41
|
-
sentry: Toucan;
|
|
42
|
-
}) => Promise<void>;
|
|
37
|
+
let _email: (c: { message: ForwardableEmailMessage; env: T; sentry: Toucan }) => Promise<void>;
|
|
43
38
|
return {
|
|
44
39
|
async fetch(
|
|
45
40
|
req: Request,
|
|
@@ -85,9 +80,10 @@ export function twoStroke<T extends Env>(
|
|
|
85
80
|
for (const route of routes) {
|
|
86
81
|
if (req.method === route.method && route.matcher.test(pathname)) {
|
|
87
82
|
const params = Object.fromEntries(
|
|
88
|
-
Object.entries(pathname.match(route.matcher)?.groups ?? {}).map(
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
Object.entries(pathname.match(route.matcher)?.groups ?? {}).map(([k, v]) => [
|
|
84
|
+
k,
|
|
85
|
+
decodeURIComponent(v),
|
|
86
|
+
]),
|
|
91
87
|
);
|
|
92
88
|
let claims;
|
|
93
89
|
try {
|
|
@@ -108,8 +104,7 @@ export function twoStroke<T extends Env>(
|
|
|
108
104
|
let rawBody;
|
|
109
105
|
try {
|
|
110
106
|
rawBody = route.input
|
|
111
|
-
? req.headers.get("Content-Type") ===
|
|
112
|
-
"application/x-www-form-urlencoded"
|
|
107
|
+
? req.headers.get("Content-Type") === "application/x-www-form-urlencoded"
|
|
113
108
|
? Object.fromEntries(new URLSearchParams(await req.text()))
|
|
114
109
|
: await req.json()
|
|
115
110
|
: undefined;
|
|
@@ -132,10 +127,10 @@ export function twoStroke<T extends Env>(
|
|
|
132
127
|
const body = route.input
|
|
133
128
|
? route.input.safeParse(rawBody)
|
|
134
129
|
: {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
130
|
+
success: true,
|
|
131
|
+
data: undefined,
|
|
132
|
+
error: undefined,
|
|
133
|
+
};
|
|
139
134
|
if (body.success)
|
|
140
135
|
response = await route.handler({
|
|
141
136
|
req,
|
|
@@ -199,19 +194,16 @@ export function twoStroke<T extends Env>(
|
|
|
199
194
|
Object.entries({
|
|
200
195
|
...defaultHeaders,
|
|
201
196
|
"Content-Type": "application/json",
|
|
202
|
-
"Strict-Transport-Security":
|
|
203
|
-
"max-age=15552000; includeSubDomains",
|
|
197
|
+
"Strict-Transport-Security": "max-age=15552000; includeSubDomains",
|
|
204
198
|
"X-Content-Type-Options": "nosniff",
|
|
205
199
|
"Content-Security-Policy": "default-src 'self'",
|
|
206
200
|
}).forEach(([k, v]) => {
|
|
207
|
-
if (!responseWithHeaders.headers.has(k))
|
|
208
|
-
responseWithHeaders.headers.set(k, v);
|
|
201
|
+
if (!responseWithHeaders.headers.has(k)) responseWithHeaders.headers.set(k, v);
|
|
209
202
|
});
|
|
210
203
|
|
|
211
204
|
return new Response(
|
|
212
205
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
213
|
-
responseWithHeaders.headers.get("Content-Type") ===
|
|
214
|
-
"application/json"
|
|
206
|
+
responseWithHeaders.headers.get("Content-Type") === "application/json"
|
|
215
207
|
? JSON.stringify(response.body)
|
|
216
208
|
: response.body,
|
|
217
209
|
responseWithHeaders,
|
|
@@ -300,53 +292,38 @@ export function twoStroke<T extends Env>(
|
|
|
300
292
|
}
|
|
301
293
|
},
|
|
302
294
|
emailHandler(
|
|
303
|
-
handler: (c: {
|
|
304
|
-
env: T;
|
|
305
|
-
message: ForwardableEmailMessage;
|
|
306
|
-
sentry: Toucan;
|
|
307
|
-
}) => Promise<void>,
|
|
295
|
+
handler: (c: { env: T; message: ForwardableEmailMessage; sentry: Toucan }) => Promise<void>,
|
|
308
296
|
) {
|
|
309
297
|
_email = handler;
|
|
310
298
|
},
|
|
311
|
-
schedule(
|
|
312
|
-
cron: string,
|
|
313
|
-
handler: (c: { env: T; sentry: Toucan }) => Promise<void>,
|
|
314
|
-
) {
|
|
299
|
+
schedule(cron: string, handler: (c: { env: T; sentry: Toucan }) => Promise<void>) {
|
|
315
300
|
crons[cron] = handler;
|
|
316
301
|
},
|
|
317
302
|
noAuth,
|
|
318
303
|
pbkdf:
|
|
319
304
|
(k: keyof T, customHeaderName: string = "Authorization") =>
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
throw Error("Invalid");
|
|
330
|
-
},
|
|
305
|
+
async ({ req, env }: { req: Request; env: T }) => {
|
|
306
|
+
const [scheme, token] = (req.headers.get(customHeaderName) ?? " ").split(" ");
|
|
307
|
+
if (
|
|
308
|
+
(scheme === "token" || scheme === "Bearer") &&
|
|
309
|
+
(await pbkdfVerify(env[k] as string, token ?? ""))
|
|
310
|
+
)
|
|
311
|
+
return;
|
|
312
|
+
throw Error("Invalid");
|
|
313
|
+
},
|
|
331
314
|
jwt:
|
|
332
315
|
<J>(k: keyof T, ak: keyof T) =>
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
);
|
|
337
|
-
if (
|
|
338
|
-
|
|
339
|
-
token ?? "",
|
|
340
|
-
env[k] as string,
|
|
341
|
-
env[ak] as string,
|
|
342
|
-
);
|
|
343
|
-
if (!claims) {
|
|
344
|
-
throw Error("Invalid");
|
|
345
|
-
}
|
|
346
|
-
return claims;
|
|
316
|
+
async ({ req, env }: { req: Request; env: T }) => {
|
|
317
|
+
const [scheme, token] = (req.headers.get("Authorization") ?? " ").split(" ");
|
|
318
|
+
if (scheme === "Bearer") {
|
|
319
|
+
const claims = await jwkVerify<J>(token ?? "", env[k] as string, env[ak] as string);
|
|
320
|
+
if (!claims) {
|
|
321
|
+
throw Error("Invalid");
|
|
347
322
|
}
|
|
348
|
-
|
|
349
|
-
}
|
|
323
|
+
return claims;
|
|
324
|
+
}
|
|
325
|
+
throw Error("Invalid");
|
|
326
|
+
},
|
|
350
327
|
queueHandler<I extends ZodType>(
|
|
351
328
|
input: I,
|
|
352
329
|
handler: (c: {
|
|
@@ -387,9 +364,7 @@ export function twoStroke<T extends Env>(
|
|
|
387
364
|
auth,
|
|
388
365
|
method: "PUT",
|
|
389
366
|
path,
|
|
390
|
-
matcher: new RegExp(
|
|
391
|
-
`^${escapeRegex(path).replaceAll(/\/{([^}]*)}/g, "/(?<$1>[^\\/]*)")}$`,
|
|
392
|
-
),
|
|
367
|
+
matcher: new RegExp(`^${escapeRegex(path).replaceAll(/\/{([^}]*)}/g, "/(?<$1>[^\\/]*)")}$`),
|
|
393
368
|
input,
|
|
394
369
|
output,
|
|
395
370
|
handler,
|
|
@@ -415,9 +390,7 @@ export function twoStroke<T extends Env>(
|
|
|
415
390
|
auth,
|
|
416
391
|
method: "POST",
|
|
417
392
|
path,
|
|
418
|
-
matcher: new RegExp(
|
|
419
|
-
`^${escapeRegex(path).replaceAll(/\/{([^}]*)}/g, "/(?<$1>[^\\/]*)")}$`,
|
|
420
|
-
),
|
|
393
|
+
matcher: new RegExp(`^${escapeRegex(path).replaceAll(/\/{([^}]*)}/g, "/(?<$1>[^\\/]*)")}$`),
|
|
421
394
|
input,
|
|
422
395
|
output,
|
|
423
396
|
handler,
|
|
@@ -442,9 +415,7 @@ export function twoStroke<T extends Env>(
|
|
|
442
415
|
auth,
|
|
443
416
|
method: "GET",
|
|
444
417
|
path,
|
|
445
|
-
matcher: new RegExp(
|
|
446
|
-
`^${escapeRegex(path).replaceAll(/\/{([^}]*)}/g, "/(?<$1>[^\\/]*)")}$`,
|
|
447
|
-
),
|
|
418
|
+
matcher: new RegExp(`^${escapeRegex(path).replaceAll(/\/{([^}]*)}/g, "/(?<$1>[^\\/]*)")}$`),
|
|
448
419
|
output,
|
|
449
420
|
handler,
|
|
450
421
|
params,
|
|
@@ -467,9 +438,7 @@ export function twoStroke<T extends Env>(
|
|
|
467
438
|
auth,
|
|
468
439
|
method: "DELETE",
|
|
469
440
|
path,
|
|
470
|
-
matcher: new RegExp(
|
|
471
|
-
`^${escapeRegex(path).replaceAll(/\/{([^}]*)}/g, "/(?<$1>[^\\/]*)")}$`,
|
|
472
|
-
),
|
|
441
|
+
matcher: new RegExp(`^${escapeRegex(path).replaceAll(/\/{([^}]*)}/g, "/(?<$1>[^\\/]*)")}$`),
|
|
473
442
|
output,
|
|
474
443
|
handler,
|
|
475
444
|
params,
|
|
@@ -483,11 +452,7 @@ type AddToQueueConfig = QueueSendOptions & {
|
|
|
483
452
|
backoffFactor?: number;
|
|
484
453
|
};
|
|
485
454
|
|
|
486
|
-
export async function addToQueue<T>(
|
|
487
|
-
queue: Queue<T>,
|
|
488
|
-
message: T,
|
|
489
|
-
config: AddToQueueConfig = {},
|
|
490
|
-
) {
|
|
455
|
+
export async function addToQueue<T>(queue: Queue<T>, message: T, config: AddToQueueConfig = {}) {
|
|
491
456
|
const { retries, backoffFactor, ...options } = config;
|
|
492
457
|
|
|
493
458
|
for (let i = 0; i < (retries ?? 5); i++) {
|
package/src/once.ts
CHANGED
|
@@ -1,128 +1,126 @@
|
|
|
1
1
|
import type { z, ZodSafeParseResult, ZodType } from "zod/v4";
|
|
2
2
|
|
|
3
3
|
export const once = async <T extends { retry_count: number }>(
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
key: string,
|
|
5
|
+
bucket: R2Bucket,
|
|
6
|
+
queue: Queue<T>,
|
|
7
|
+
backoffExponent: number,
|
|
8
|
+
numRetries: number,
|
|
9
|
+
ack: (cb: () => Promise<void>) => Promise<void>,
|
|
10
|
+
cb: (retry: (m: T) => Promise<void>) => Promise<void>,
|
|
11
11
|
) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
12
|
+
const retry = async (m: T) => {
|
|
13
|
+
const retry_count = m.retry_count + 1;
|
|
14
|
+
if (retry_count > numRetries) throw new Error("Too many retries");
|
|
15
|
+
await queue.send(
|
|
16
|
+
{ ...m, retry_count },
|
|
17
|
+
{ delaySeconds: Math.min(Math.pow(retry_count, backoffExponent), 900) },
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
if (!(await isDuplicateMessage(key, bucket)))
|
|
21
|
+
await ack(async () => {
|
|
22
|
+
await cb(retry);
|
|
23
|
+
});
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
|
|
27
26
|
export const retryHandler =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
27
|
+
<T, M extends ZodType>(
|
|
28
|
+
maxRetries: number,
|
|
29
|
+
handler: (
|
|
30
|
+
body: z.output<M>,
|
|
31
|
+
env: T,
|
|
32
|
+
isFinalAttempt: boolean,
|
|
33
|
+
ack: (cb: () => Promise<void>) => Promise<void>,
|
|
34
|
+
) => Promise<void>,
|
|
35
|
+
) =>
|
|
36
|
+
async ({
|
|
37
|
+
body,
|
|
38
|
+
waitUntil,
|
|
39
|
+
env,
|
|
40
|
+
}: {
|
|
41
|
+
body: z.infer<M> & { retry: number };
|
|
42
|
+
waitUntil: (p: Promise<void>) => void;
|
|
43
|
+
env: T;
|
|
44
|
+
}) => {
|
|
45
|
+
const isFinalAttempt = body.retry >= maxRetries;
|
|
46
|
+
await handler(
|
|
47
|
+
body,
|
|
48
|
+
env,
|
|
49
|
+
isFinalAttempt,
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
51
|
+
async (cb) => {
|
|
52
|
+
waitUntil(cb());
|
|
53
|
+
},
|
|
54
|
+
);
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
return { body: { ok: true } };
|
|
57
|
+
};
|
|
59
58
|
|
|
60
59
|
export const retryQueue =
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const isFinalAttempt = message.attempts >= maxRetries;
|
|
60
|
+
<T, M extends ZodType>(
|
|
61
|
+
maxRetries: number,
|
|
62
|
+
handler: (
|
|
63
|
+
body: z.output<M>,
|
|
64
|
+
env: T,
|
|
65
|
+
isFinalAttempt: boolean,
|
|
66
|
+
ack: (cb: () => Promise<void>) => Promise<void>,
|
|
67
|
+
) => Promise<void>,
|
|
68
|
+
) =>
|
|
69
|
+
async ({
|
|
70
|
+
batch,
|
|
71
|
+
parsedBatch,
|
|
72
|
+
env,
|
|
73
|
+
}: {
|
|
74
|
+
batch: MessageBatch<z.input<M>>;
|
|
75
|
+
parsedBatch: ZodSafeParseResult<z.output<M>>[];
|
|
76
|
+
env: T;
|
|
77
|
+
}) => {
|
|
78
|
+
const message = batch.messages[0];
|
|
79
|
+
if (!message || batch.messages.length !== 1) {
|
|
80
|
+
console.warn({ batch });
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Queue must only process one message at a time, got ${batch.messages.length}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (!parsedBatch[0] || !parsedBatch[0].success) {
|
|
86
|
+
console.error({ batch });
|
|
87
|
+
throw new Error(`Queue message invalid`);
|
|
88
|
+
}
|
|
92
89
|
|
|
93
|
-
|
|
94
|
-
await handler(parsedBatch[0].data, env, isFinalAttempt, async (cb) => {
|
|
95
|
-
message.ack();
|
|
96
|
-
await cb();
|
|
97
|
-
});
|
|
98
|
-
} catch (err) {
|
|
99
|
-
if (!isFinalAttempt) message.retry();
|
|
100
|
-
throw err;
|
|
101
|
-
}
|
|
102
|
-
};
|
|
90
|
+
const isFinalAttempt = message.attempts >= maxRetries;
|
|
103
91
|
|
|
92
|
+
try {
|
|
93
|
+
await handler(parsedBatch[0].data, env, isFinalAttempt, async (cb) => {
|
|
94
|
+
message.ack();
|
|
95
|
+
await cb();
|
|
96
|
+
});
|
|
97
|
+
} catch (err) {
|
|
98
|
+
if (!isFinalAttempt) message.retry();
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
104
102
|
|
|
105
103
|
const isDuplicateMessage = async (key: string, bucket: R2Bucket) => {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
104
|
+
// Work around for CloudFlare R2 error:
|
|
105
|
+
// put: We encountered an internal error. Please try again. (10001)
|
|
106
|
+
for (let i = 0; i < 5; i++) {
|
|
107
|
+
try {
|
|
108
|
+
// Will throw if that error occurs
|
|
109
|
+
const putResult = await bucket.put(`lock-${key}`, "", {
|
|
110
|
+
onlyIf: new Headers({
|
|
111
|
+
"If-Unmodified-Since": "Wed, 21 Oct 2015 07:28:00 GMT",
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
// if the returned result is null then the object already existed
|
|
115
|
+
const isDupe = putResult === null;
|
|
116
|
+
if (isDupe) console.log("Duplicate message", { key });
|
|
117
|
+
return isDupe;
|
|
118
|
+
} catch (err) {
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
120
|
+
console.warn(`Error putting ${key} to R2: ${err}`);
|
|
121
|
+
console.warn(`Retrying in ${i} seconds`);
|
|
122
|
+
await new Promise((resolve) => setTimeout(resolve, i * 1000));
|
|
126
123
|
}
|
|
127
|
-
|
|
128
|
-
};
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`Failed to put ${key} to R2`);
|
|
126
|
+
};
|