two-stroke 7.2.0 → 7.4.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/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
- cmd("wrangler types --check")
7
+ cmd("wrangler types --strict-vars false --check")
8
8
  }
9
9
  cmd("eslint --cache --max-warnings=0");
10
10
  cmd("prettier --cache --check .");
@@ -4,7 +4,22 @@ import fs from "fs";
4
4
  import { cmd } from "../src/cmd.mjs";
5
5
 
6
6
  if (fs.existsSync("wrangler.jsonc")) {
7
- cmd("wrangler types")
7
+ const devVarsFiles = fs.globSync("**/.dev.vars");
8
+ const backed = [];
9
+
10
+ for (const file of devVarsFiles) {
11
+ const tmp = file + ".bak.bak";
12
+ fs.renameSync(file, tmp);
13
+ backed.push({ original: file, tmp });
14
+ }
15
+
16
+ try {
17
+ cmd("wrangler types --strict-vars false");
18
+ } finally {
19
+ for (const { original, tmp } of backed) {
20
+ fs.renameSync(tmp, original);
21
+ }
22
+ }
8
23
  }
9
24
  cmd("wrangler deploy --env= --dry-run --outdir=dist");
10
25
  cmd("tsc --noEmit");
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "type-check": "./bin/type-check.mjs"
11
11
  },
12
12
  "dependencies": {
13
- "@cloudflare/vitest-pool-workers": "^0.13.5",
13
+ "@cloudflare/vitest-pool-workers": "^0.14.1",
14
14
  "@sentry/cli": "^3.3.4",
15
15
  "@types/node": "^25.5.0",
16
16
  "@typescript-eslint/eslint-plugin": "^8.57.2",
@@ -59,7 +59,7 @@
59
59
  "url": "https://github.com/change-engine/two-stroke"
60
60
  },
61
61
  "type": "module",
62
- "version": "7.2.0",
62
+ "version": "7.4.0",
63
63
  "devDependencies": {
64
64
  "@types/eslint": "^9.6.1",
65
65
  "eslint": "^10.1.0",
package/src/once.ts ADDED
@@ -0,0 +1,128 @@
1
+ import type { z, ZodSafeParseResult, ZodType } from "zod/v4";
2
+
3
+ export const once = async <T extends { retry_count: number }>(
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
+ ) => {
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
+ };
25
+
26
+
27
+ export const retryHandler =
28
+ <T, M extends ZodType>(
29
+ maxRetries: number,
30
+ handler: (
31
+ body: z.output<M>,
32
+ env: T,
33
+ isFinalAttempt: boolean,
34
+ ack: (cb: () => Promise<void>) => Promise<void>,
35
+ ) => Promise<void>,
36
+ ) =>
37
+ async ({
38
+ body,
39
+ waitUntil,
40
+ env,
41
+ }: {
42
+ body: z.infer<M> & { retry: number };
43
+ waitUntil: (p: Promise<void>) => void;
44
+ env: T;
45
+ }) => {
46
+ const isFinalAttempt = body.retry >= maxRetries;
47
+ await handler(
48
+ body,
49
+ env,
50
+ isFinalAttempt,
51
+ // eslint-disable-next-line @typescript-eslint/require-await
52
+ async (cb) => {
53
+ waitUntil(cb());
54
+ },
55
+ );
56
+
57
+ return { body: { ok: true } };
58
+ };
59
+
60
+ export const retryQueue =
61
+ <T, M extends ZodType>(
62
+ maxRetries: number,
63
+ handler: (
64
+ body: z.output<M>,
65
+ env: T,
66
+ isFinalAttempt: boolean,
67
+ ack: (cb: () => Promise<void>) => Promise<void>,
68
+ ) => Promise<void>,
69
+ ) =>
70
+ async ({
71
+ batch,
72
+ parsedBatch,
73
+ env,
74
+ }: {
75
+ batch: MessageBatch<z.input<M>>;
76
+ parsedBatch: ZodSafeParseResult<z.output<M>>[];
77
+ env: T;
78
+ }) => {
79
+ const message = batch.messages[0];
80
+ if (!message || batch.messages.length !== 1) {
81
+ console.warn({ batch });
82
+ throw new Error(
83
+ `Queue must only process one message at a time, got ${batch.messages.length}`,
84
+ );
85
+ }
86
+ if (!parsedBatch[0] || !parsedBatch[0].success) {
87
+ console.error({ batch });
88
+ throw new Error(`Queue message invalid`);
89
+ }
90
+
91
+ const isFinalAttempt = message.attempts >= maxRetries;
92
+
93
+ try {
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
+ };
103
+
104
+
105
+ const isDuplicateMessage = async (key: string, bucket: R2Bucket) => {
106
+ // Work around for CloudFlare R2 error:
107
+ // put: We encountered an internal error. Please try again. (10001)
108
+ for (let i = 0; i < 5; i++) {
109
+ try {
110
+ // Will throw if that error occurs
111
+ const putResult = await bucket.put(`lock-${key}`, "", {
112
+ onlyIf: new Headers({
113
+ "If-Unmodified-Since": "Wed, 21 Oct 2015 07:28:00 GMT",
114
+ }),
115
+ });
116
+ // if the returned result is null then the object already existed
117
+ const isDupe = putResult === null;
118
+ if (isDupe) console.log("Duplicate message", { key });
119
+ return isDupe;
120
+ } catch (err) {
121
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
122
+ console.warn(`Error putting ${key} to R2: ${err}`);
123
+ console.warn(`Retrying in ${i} seconds`);
124
+ await new Promise((resolve) => setTimeout(resolve, i * 1000));
125
+ }
126
+ }
127
+ throw new Error(`Failed to put ${key} to R2`);
128
+ };