@rdlabo/workers-hono-kit 0.10.5 → 0.10.6

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.
@@ -121,14 +121,11 @@ export class KVCache {
121
121
  if (!key) {
122
122
  return undefined;
123
123
  }
124
- let data;
125
- try {
126
- data = await this.#kv.get(key);
127
- }
128
- catch (error) {
124
+ const read = async () => this.#kv.get(key);
125
+ const data = await read().catch((error) => {
129
126
  this.#reportError(error, { operation: 'read', table });
130
- return undefined;
131
- }
127
+ return null;
128
+ });
132
129
  if (!data) {
133
130
  return undefined;
134
131
  }
@@ -179,7 +176,8 @@ export class KVCache {
179
176
  return;
180
177
  }
181
178
  const ttl = Math.max(this.#minTtl, lifetime ?? this.#defaultLifetime);
182
- await this.#kv.put(key, payload, { expirationTtl: ttl }).catch((error) => {
179
+ const write = async () => this.#kv.put(key, payload, { expirationTtl: ttl });
180
+ await write().catch((error) => {
183
181
  this.#reportError(error, { operation: 'write', table });
184
182
  });
185
183
  }
@@ -243,7 +241,8 @@ export class KVCache {
243
241
  if (!key) {
244
242
  return;
245
243
  }
246
- await this.#kv.delete(key).catch((error) => {
244
+ const remove = async () => this.#kv.delete(key);
245
+ await remove().catch((error) => {
247
246
  this.#reportError(error, { operation: 'delete', table });
248
247
  });
249
248
  }
@@ -70,8 +70,8 @@ export function createHyperdriveDatabase(options) {
70
70
  return retryWhenDeadlock(() => dz.transaction(fn));
71
71
  },
72
72
  /** @deprecated Workers cleans up invocation-scoped connections automatically. */
73
- dispose() {
74
- return Promise.resolve();
73
+ async dispose() {
74
+ return;
75
75
  },
76
76
  };
77
77
  }
package/dist/db/retry.js CHANGED
@@ -27,17 +27,17 @@
27
27
  */
28
28
  export async function retryWhenDeadlock(fn, retries = 3, delay = 100) {
29
29
  for (let attempt = 0; attempt < retries; attempt++) {
30
- try {
31
- return await fn();
30
+ const invoke = async () => fn();
31
+ const outcome = await invoke().then((value) => ({ ok: true, value }), (error) => ({ ok: false, error }));
32
+ if (outcome.ok) {
33
+ return outcome.value;
32
34
  }
33
- catch (error) {
34
- const code = error.code;
35
- if (code === 'ER_LOCK_DEADLOCK' && attempt < retries - 1) {
36
- await new Promise((resolve) => setTimeout(resolve, delay * (attempt + 1)));
37
- continue;
38
- }
39
- throw error;
35
+ const code = outcome.error.code;
36
+ if (code === 'ER_LOCK_DEADLOCK' && attempt < retries - 1) {
37
+ await new Promise((resolve) => setTimeout(resolve, delay * (attempt + 1)));
38
+ continue;
40
39
  }
40
+ throw outcome.error;
41
41
  }
42
42
  // Unreachable: the loop returns on success and throws on the final failed attempt.
43
43
  throw new Error('retryWhenDeadlock: exhausted retries');
@@ -43,7 +43,7 @@ export function createAuthMiddleware(options) {
43
43
  return async (c, next) => {
44
44
  let stage = 'token';
45
45
  let tokenPresent = false;
46
- try {
46
+ const authenticate = async () => {
47
47
  const token = c.req.header(tokenHeader) ?? '';
48
48
  tokenPresent = token.trim().length > 0;
49
49
  if (!tokenPresent && rejectMissingToken) {
@@ -57,24 +57,23 @@ export function createAuthMiddleware(options) {
57
57
  const userId = resolveUserId ? await resolveUserId(verified, c, appInfo) : undefined;
58
58
  stage = 'setContext';
59
59
  setContext(c, { verified, appInfo, userId });
60
- }
61
- catch (e) {
60
+ };
61
+ const outcome = await authenticate().then(() => ({ ok: true }), (error) => ({ ok: false, error }));
62
+ if (!outcome.ok) {
62
63
  const details = { stage, tokenPresent };
63
64
  if (reportFailure) {
64
- try {
65
- await reportFailure(e, c, details);
66
- }
67
- catch (reportingError) {
65
+ const report = async () => reportFailure(outcome.error, c, details);
66
+ await report().catch((reportingError) => {
68
67
  // Observability must never alter the authentication response.
69
68
  console.error(reportingError);
70
- }
69
+ });
71
70
  }
72
71
  else {
73
72
  // Preserve the historical default for consumers that have not adopted classified reporting.
74
- console.error(e);
73
+ console.error(outcome.error);
75
74
  }
76
75
  if (onFailure) {
77
- return onFailure(e, c, details);
76
+ return onFailure(outcome.error, c, details);
78
77
  }
79
78
  throw new HTTPException(failureStatus, { message: failureMessage });
80
79
  }
@@ -98,12 +98,13 @@ export function perfLog(options = {}) {
98
98
  // In-code sampling thins Analytics Engine writes only; Workers Logs volume is controlled separately
99
99
  // by the observability `head_sampling_rate`. Low-traffic Workers should leave `sampleRate` at 1.
100
100
  if (sink && (rate >= 1 || Math.random() < rate)) {
101
+ const point = {
102
+ doubles: [tApp, cold ? 1 : 0, status],
103
+ blobs: [path, colo, method],
104
+ indexes: [analyticsIndex(path)],
105
+ };
101
106
  try {
102
- sink.writeDataPoint({
103
- doubles: [tApp, cold ? 1 : 0, status],
104
- blobs: [path, colo, method],
105
- indexes: [analyticsIndex(path)],
106
- });
107
+ sink.writeDataPoint(point);
107
108
  }
108
109
  catch (error) {
109
110
  // Telemetry must never replace an otherwise successful application response with a 500.
@@ -69,28 +69,30 @@ export async function processBatch(batch, handler, options) {
69
69
  let discarded = 0;
70
70
  let failed = 0;
71
71
  for (const message of batch.messages) {
72
- try {
72
+ const processMessage = async () => {
73
73
  await handler(message.body, message);
74
74
  message.ack();
75
+ };
76
+ const outcome = await processMessage().then(() => ({ ok: true }), (error) => ({ ok: false, error }));
77
+ if (outcome.ok) {
75
78
  processed++;
79
+ continue;
80
+ }
81
+ try {
82
+ onError(outcome.error, message);
76
83
  }
77
- catch (error) {
78
- try {
79
- onError(error, message);
80
- }
81
- catch (reportingError) {
82
- // Reporting is best-effort. Preserve the domain error's disposition, but never let a broken
83
- // custom reporter make a permanent failure disappear without any local trace.
84
- console.error(`[queue:${batch.queue}] onError failed for message ${message.id}`, reportingError, 'original error:', error);
85
- }
86
- if (isNonRetryableQueueError(error)) {
87
- message.ack();
88
- discarded++;
89
- continue;
90
- }
91
- message.retry(retryOptions);
92
- failed++;
84
+ catch (reportingError) {
85
+ // Reporting is best-effort. Preserve the domain error's disposition, but never let a broken
86
+ // custom reporter make a permanent failure disappear without any local trace.
87
+ console.error(`[queue:${batch.queue}] onError failed for message ${message.id}`, reportingError, 'original error:', outcome.error);
88
+ }
89
+ if (isNonRetryableQueueError(outcome.error)) {
90
+ message.ack();
91
+ discarded++;
92
+ continue;
93
93
  }
94
+ message.retry(retryOptions);
95
+ failed++;
94
96
  }
95
97
  return { processed, discarded, failed };
96
98
  }
@@ -27,16 +27,16 @@ export async function retryDurableObjectOperation(operation, options = {}) {
27
27
  const random = options.random ?? Math.random;
28
28
  const wait = options.wait ?? defaultWait;
29
29
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
30
- try {
31
- return await operation(attempt);
30
+ const invoke = async () => operation(attempt);
31
+ const outcome = await invoke().then((value) => ({ ok: true, value }), (error) => ({ ok: false, error }));
32
+ if (outcome.ok) {
33
+ return outcome.value;
32
34
  }
33
- catch (error) {
34
- if (!isRetryableDurableObjectError(error) || attempt + 1 >= maxAttempts) {
35
- throw error;
36
- }
37
- const delayMs = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt * random());
38
- await wait(delayMs);
35
+ if (!isRetryableDurableObjectError(outcome.error) || attempt + 1 >= maxAttempts) {
36
+ throw outcome.error;
39
37
  }
38
+ const delayMs = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt * random());
39
+ await wait(delayMs);
40
40
  }
41
41
  throw new Error('Durable Object retry exhausted');
42
42
  }
@@ -162,28 +162,26 @@ export function parsePaymentFailure(receipt) {
162
162
  if (!receipt) {
163
163
  return null;
164
164
  }
165
+ let parsed;
165
166
  try {
166
- const parsed = JSON.parse(receipt);
167
- const r = asRecord(parsed);
168
- if (!r) {
169
- return null;
170
- }
171
- if (asRecord(r.reason)) {
172
- if ((r.source !== undefined && typeof r.source !== 'string') ||
173
- (r.occurredAt !== undefined && typeof r.occurredAt !== 'string')) {
174
- return null;
175
- }
176
- return parsed;
177
- }
178
- // IAP rows store the reason itself. `code` is required so arbitrary JSON is not accepted as a reason.
179
- if (typeof r.code === 'string') {
180
- return { reason: parsed };
181
- }
182
- return null;
167
+ parsed = JSON.parse(receipt);
183
168
  }
184
169
  catch {
185
170
  return null;
186
171
  }
172
+ const r = asRecord(parsed);
173
+ if (!r) {
174
+ return null;
175
+ }
176
+ if (asRecord(r.reason)) {
177
+ if ((r.source !== undefined && typeof r.source !== 'string') ||
178
+ (r.occurredAt !== undefined && typeof r.occurredAt !== 'string')) {
179
+ return null;
180
+ }
181
+ return parsed;
182
+ }
183
+ // IAP rows store the reason itself. `code` is required so arbitrary JSON is not accepted as a reason.
184
+ return typeof r.code === 'string' ? { reason: parsed } : null;
187
185
  }
188
186
  /**
189
187
  * HTTP error for a synchronous card decline, carrying a user-facing Japanese message.
@@ -83,14 +83,13 @@ export function createTestDb(options) {
83
83
  await pool.query(`INSERT INTO \`${table}\` (${columnList}) VALUES (${placeholders})`, Object.values(row));
84
84
  },
85
85
  async mysqlReachable() {
86
- try {
87
- const c = await createConnection({ ...connection });
88
- await c.end();
89
- return true;
90
- }
91
- catch {
86
+ const connect = async () => createConnection({ ...connection });
87
+ const c = await connect().catch(() => undefined);
88
+ if (!c) {
92
89
  return false;
93
90
  }
91
+ const close = async () => c.end();
92
+ return close().then(() => true, () => false);
94
93
  },
95
94
  };
96
95
  }
@@ -21,16 +21,14 @@ export function fakeQueue() {
21
21
  get batchCount() {
22
22
  return batchCount;
23
23
  },
24
- send(body) {
24
+ async send(body) {
25
25
  sent.push(body);
26
- return Promise.resolve();
27
26
  },
28
- sendBatch(messages) {
27
+ async sendBatch(messages) {
29
28
  batchCount++;
30
29
  for (const m of messages) {
31
30
  sent.push(m.body);
32
31
  }
33
- return Promise.resolve();
34
32
  },
35
33
  };
36
34
  }
@@ -47,16 +45,14 @@ export function fakeQueue() {
47
45
  export function fakeKv() {
48
46
  const store = new Map();
49
47
  return {
50
- get: (key) => Promise.resolve(store.get(key) ?? null),
51
- put: (key, value) => {
48
+ get: async (key) => store.get(key) ?? null,
49
+ put: async (key, value) => {
52
50
  store.set(key, value);
53
- return Promise.resolve();
54
51
  },
55
- delete: (key) => {
52
+ delete: async (key) => {
56
53
  store.delete(key);
57
- return Promise.resolve();
58
54
  },
59
- list: () => Promise.resolve({ keys: [], list_complete: true, cacheStatus: null }),
60
- getWithMetadata: () => Promise.resolve({ value: null, metadata: null, cacheStatus: null }),
55
+ list: async () => ({ keys: [], list_complete: true, cacheStatus: null }),
56
+ getWithMetadata: async () => ({ value: null, metadata: null, cacheStatus: null }),
61
57
  };
62
58
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rdlabo/workers-hono-kit",
3
- "version": "0.10.5",
3
+ "version": "0.10.6",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -33,6 +33,13 @@
33
33
  "engines": {
34
34
  "node": ">=20.0.0"
35
35
  },
36
+ "devEngines": {
37
+ "runtime": {
38
+ "name": "node",
39
+ "version": "^20.19.0 || ^22.13.0 || >=24.0.0",
40
+ "onFail": "error"
41
+ }
42
+ },
36
43
  "files": [
37
44
  "dist",
38
45
  "scripts",
@@ -120,8 +127,13 @@
120
127
  "devDependencies": {
121
128
  "@ai-sdk/anthropic": "^3.0.84",
122
129
  "@ai-sdk/openai": "^3.0.71",
130
+ "@angular-eslint/template-parser": "^21.4.0",
131
+ "@angular/core": "^21.2.20",
132
+ "@angular/forms": "^21.2.20",
133
+ "@angular/router": "^21.2.20",
123
134
  "@hono/eslint-config": "^2.1.0",
124
135
  "@hono/zod-validator": "^0.8.0",
136
+ "@rdlabo/eslint-plugin-rules": "^21.2.6",
125
137
  "@types/node": "^22.19.21",
126
138
  "ai": "^6.0.204",
127
139
  "ai-gateway-provider": "^3.1.3",