@hadoku/jobplatform-worker 2.32.22 → 2.32.24

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.
@@ -0,0 +1,14 @@
1
+ import type { AppEnv } from '../types.js';
2
+ export declare const TELEMETRY_SERVICE = "jobplatform";
3
+ export interface ServiceEvent {
4
+ level: 'info' | 'warn' | 'error';
5
+ /** Lands in the `event` column; keep it short and stable, it is what gets grouped by. */
6
+ event: string;
7
+ message: string;
8
+ context?: Record<string, unknown>;
9
+ }
10
+ /** True when this worker has what posting needs. Callers skip the post when it doesn't. */
11
+ export declare function canPostTelemetry(env: AppEnv | undefined): env is AppEnv;
12
+ /** Resolves when monitoring-api has accepted the row; rejects with the reason it didn't. */
13
+ export declare function postServiceEvent(env: AppEnv, ev: ServiceEvent): Promise<void>;
14
+ //# sourceMappingURL=telemetry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../../src/clients/telemetry.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,eAAO,MAAM,iBAAiB,gBAAgB,CAAC;AAE/C,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IACjC,yFAAyF;IACzF,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,2FAA2F;AAC3F,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,GAAG,IAAI,MAAM,CAEvE;AAED,4FAA4F;AAC5F,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBnF"}
@@ -16,6 +16,14 @@
16
16
  * failures that day were D1 refusing within 10-70ms while four other
17
17
  * batches held it for ~9s each. They were transient, and a transient D1
18
18
  * failure now answers 503 with Retry-After.
19
+ *
20
+ * Issue #26 added two more. The 2026-10-30 scrape lost 39 batches in a
21
+ * five-second window to `D1_ERROR: internal error; reference = ...`, with no
22
+ * long-held statement behind it and every batch around them succeeding. That
23
+ * message was not on the list, so each went out as a 500 the scraper drops
24
+ * rather than a 503 it retries. And every failure now also lands in
25
+ * monitoring-api's `service_events`, so a run of them is visible from this
26
+ * side, not only from the caller's.
19
27
  */
20
28
  import type { Context } from 'hono';
21
29
  export declare function isTransientD1Error(err: unknown): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"errorHandler.d.ts","sourceRoot":"","sources":["../src/errorHandler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AA2BpC,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAExD;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,GAAG,QAAQ,CA4B7D"}
1
+ {"version":3,"file":"errorHandler.d.ts","sourceRoot":"","sources":["../src/errorHandler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAoCpC,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAExD;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,GAAG,QAAQ,CAuD7D"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAMN,KAAK,iBAAiB,EACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAUzC,UAAU,UAAU;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE;QAAE,WAAW,EAAE,iBAAiB,CAAA;KAAE,CAAC;CAC9C;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,SAAqB,uCAuDrE;AAYD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,IAErC,SAAS,OAAO,EAAE,MAAM,gBAAgB,kCAChD;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,IAC1C,MAAM,MAAM,UAGpB;AAED,YAAY,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACzC,cAAc,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhD,OAAO,EAMN,KAAK,iBAAiB,EACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAUzC,UAAU,UAAU;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE;QAAE,WAAW,EAAE,iBAAiB,CAAA;KAAE,CAAC;CAC9C;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,SAAqB,uCA+DrE;AAYD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,IAErC,SAAS,OAAO,EAAE,MAAM,gBAAgB,kCAChD;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,IAC1C,MAAM,MAAM,UAGpB;AAED,YAAY,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACzC,cAAc,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,91 @@
1
1
  import { z, OpenAPIHono, createRoute } from "@hono/zod-openapi";
2
2
  import { cors } from "hono/cors";
3
3
  import { DetailedErrorResponseSchema, createSuccessResponseSchema, tierAtLeast, requireMinTier, wrappedValidationHook, DEFAULT_HADOKU_ORIGINS, createEdgeAuth, createOpenAPIDocConfig, createErrorHandlers } from "@wolffm/worker-utils";
4
+ var getTime = () => {
5
+ try {
6
+ return performance.now();
7
+ } catch {
8
+ }
9
+ return Date.now();
10
+ };
11
+ var timing = (config) => {
12
+ const options = {
13
+ total: true,
14
+ enabled: true,
15
+ totalDescription: "Total Response Time",
16
+ autoEnd: true,
17
+ crossOrigin: false,
18
+ ...config
19
+ };
20
+ return async function timing2(c, next) {
21
+ const headers = [];
22
+ const timers = /* @__PURE__ */ new Map();
23
+ if (c.get("metric")) {
24
+ return await next();
25
+ }
26
+ c.set("metric", { headers, timers });
27
+ if (options.total) {
28
+ startTime(c, "total", options.totalDescription);
29
+ }
30
+ await next();
31
+ if (options.total) {
32
+ endTime(c, "total");
33
+ }
34
+ if (options.autoEnd) {
35
+ timers.forEach((_, key) => endTime(c, key));
36
+ }
37
+ const enabled = typeof options.enabled === "function" ? options.enabled(c) : options.enabled;
38
+ if (enabled) {
39
+ c.res.headers.append("Server-Timing", headers.join(","));
40
+ const crossOrigin = typeof options.crossOrigin === "function" ? options.crossOrigin(c) : options.crossOrigin;
41
+ if (crossOrigin) {
42
+ c.res.headers.append(
43
+ "Timing-Allow-Origin",
44
+ typeof crossOrigin === "string" ? crossOrigin : "*"
45
+ );
46
+ }
47
+ }
48
+ };
49
+ };
50
+ var setMetric = (c, name, valueDescription, description, precision) => {
51
+ const metrics = c.get("metric");
52
+ if (!metrics) {
53
+ console.warn("Metrics not initialized! Please add the `timing()` middleware to this route!");
54
+ return;
55
+ }
56
+ if (typeof valueDescription === "number") {
57
+ const dur = valueDescription.toFixed(1);
58
+ const metric = description ? `${name};dur=${dur};desc="${description}"` : `${name};dur=${dur}`;
59
+ metrics.headers.push(metric);
60
+ } else {
61
+ const metric = valueDescription ? `${name};desc="${valueDescription}"` : `${name}`;
62
+ metrics.headers.push(metric);
63
+ }
64
+ };
65
+ var startTime = (c, name, description) => {
66
+ const metrics = c.get("metric");
67
+ if (!metrics) {
68
+ console.warn("Metrics not initialized! Please add the `timing()` middleware to this route!");
69
+ return;
70
+ }
71
+ metrics.timers.set(name, { description, start: getTime() });
72
+ };
73
+ var endTime = (c, name, precision) => {
74
+ const metrics = c.get("metric");
75
+ if (!metrics) {
76
+ console.warn("Metrics not initialized! Please add the `timing()` middleware to this route!");
77
+ return;
78
+ }
79
+ const timer = metrics.timers.get(name);
80
+ if (!timer) {
81
+ console.warn(`Timer "${name}" does not exist!`);
82
+ return;
83
+ }
84
+ const { description, start } = timer;
85
+ const duration = getTime() - start;
86
+ setMetric(c, name, duration, description);
87
+ metrics.timers.delete(name);
88
+ };
4
89
  var KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
5
90
  function renderValue(value) {
6
91
  if (value === null) return "null";
@@ -106,12 +191,65 @@ function createWorkerLogger(options) {
106
191
  };
107
192
  }
108
193
  const logger = createWorkerLogger({ service: "jobplatform-api" });
194
+ var EDGE_BASE = "https://hadoku.me";
195
+ var MONITORING_BASE = `${EDGE_BASE}/health`;
196
+ const TELEMETRY_SERVICE = "jobplatform";
197
+ function canPostTelemetry(env) {
198
+ return Boolean(env?.EDGE && env.HADOKU_SERVICE_KEY?.trim());
199
+ }
200
+ async function postServiceEvent(env, ev) {
201
+ const key = env.HADOKU_SERVICE_KEY?.trim();
202
+ if (!env.EDGE || !key) throw new Error("EDGE binding or HADOKU_SERVICE_KEY not configured");
203
+ const res = await env.EDGE.fetch(`${MONITORING_BASE}/api/telemetry`, {
204
+ method: "POST",
205
+ headers: {
206
+ "Content-Type": "application/json",
207
+ "X-User-Key": key
208
+ },
209
+ body: JSON.stringify({
210
+ source: "worker",
211
+ level: ev.level,
212
+ type: "log",
213
+ message: `event=${ev.event} ${ev.message}`,
214
+ context: { ...ev.context, service: TELEMETRY_SERVICE }
215
+ })
216
+ });
217
+ if (!res.ok) throw new Error(`monitoring-api answered ${res.status}`);
218
+ }
219
+ function runAfterResponse(c, label, work) {
220
+ const guarded = work.catch((error) => {
221
+ logger.error("background work failed", {
222
+ task: label,
223
+ error: error instanceof Error ? error.message : String(error)
224
+ });
225
+ });
226
+ let ctx;
227
+ try {
228
+ ctx = c.executionCtx;
229
+ } catch {
230
+ ctx = void 0;
231
+ }
232
+ if (!ctx) {
233
+ logger.error("no ExecutionContext — background work will not survive the response", {
234
+ task: label,
235
+ hint: "the host worker must forward ctx: fetch(request, env, ctx) -> createFetchHandler(env)(request, ctx)"
236
+ });
237
+ return;
238
+ }
239
+ ctx.waitUntil(guarded);
240
+ }
109
241
  const TRANSIENT_D1 = [
110
242
  "D1 DB is overloaded",
111
243
  "Network connection lost",
112
244
  "storage caused object to be reset",
113
245
  "reset because its code was updated",
114
- "transient issue on remote node"
246
+ "transient issue on remote node",
247
+ // Not on Cloudflare's list, but it is how D1 answered the 2026-10-30 burst
248
+ // (issue #26): 39 refusals inside five seconds, nothing held the database,
249
+ // the same batches' neighbours went through. A retry costs the caller one
250
+ // round trip; a 500 costs it the batch. Prefixed so a non-D1 "internal
251
+ // error" from somewhere else is not swept in.
252
+ "D1_ERROR: internal error"
115
253
  ];
116
254
  const RETRY_AFTER_SECONDS = 5;
117
255
  function messagesOf(err) {
@@ -138,6 +276,26 @@ function errorHandler(err, c) {
138
276
  transient,
139
277
  stack: err.stack
140
278
  });
279
+ const env = c.env;
280
+ if (canPostTelemetry(env)) {
281
+ runAfterResponse(
282
+ c,
283
+ "telemetry: unhandled error",
284
+ postServiceEvent(env, {
285
+ level: transient ? "warn" : "error",
286
+ event: "unhandled_error",
287
+ message: `${c.req.method} ${c.req.path} ${message ?? err.name}`,
288
+ context: {
289
+ method: c.req.method,
290
+ path: c.req.path,
291
+ status: transient ? 503 : 500,
292
+ transient,
293
+ message,
294
+ causes
295
+ }
296
+ })
297
+ );
298
+ }
141
299
  if (transient) {
142
300
  c.header("Retry-After", String(RETRY_AFTER_SECONDS));
143
301
  return c.json(
@@ -1690,28 +1848,6 @@ function scoreJobLightAxes(job, profile) {
1690
1848
  bound: compose(breakdown, geo.penalty * comp.penalty)
1691
1849
  };
1692
1850
  }
1693
- function runAfterResponse(c, label, work) {
1694
- const guarded = work.catch((error) => {
1695
- logger.error("background work failed", {
1696
- task: label,
1697
- error: error instanceof Error ? error.message : String(error)
1698
- });
1699
- });
1700
- let ctx;
1701
- try {
1702
- ctx = c.executionCtx;
1703
- } catch {
1704
- ctx = void 0;
1705
- }
1706
- if (!ctx) {
1707
- logger.error("no ExecutionContext — background work will not survive the response", {
1708
- task: label,
1709
- hint: "the host worker must forward ctx: fetch(request, env, ctx) -> createFetchHandler(env)(request, ctx)"
1710
- });
1711
- return;
1712
- }
1713
- ctx.waitUntil(guarded);
1714
- }
1715
1851
  function criteriaHash(p) {
1716
1852
  const norm = (xs) => [...xs].map((s) => s.trim().toLowerCase()).sort();
1717
1853
  return JSON.stringify([
@@ -1957,10 +2093,12 @@ app$4.openapi(ingestRoute, async (c) => {
1957
2093
  );
1958
2094
  });
1959
2095
  const inserted = [];
2096
+ startTime(c, "insert");
1960
2097
  for (let i = 0; i < stmts.length; i += INGEST_BATCH_CHUNK) {
1961
2098
  const results = await db.batch(stmts.slice(i, i + INGEST_BATCH_CHUNK));
1962
2099
  for (const r of results) inserted.push((r.meta?.changes ?? 0) > 0);
1963
2100
  }
2101
+ endTime(c, "insert");
1964
2102
  const accepted = inserted.filter(Boolean).length;
1965
2103
  const skipped = jobs.length - accepted;
1966
2104
  const seen = jobs.filter((_, i) => !inserted[i]);
@@ -1986,7 +2124,10 @@ app$4.openapi(ingestRoute, async (c) => {
1986
2124
  );
1987
2125
  }
1988
2126
  }
2127
+ startTime(c, "seen");
1989
2128
  if (seenStmts.length) await db.batch(seenStmts);
2129
+ endTime(c, "seen");
2130
+ startTime(c, "restore");
1990
2131
  if (strippedStmts.length) {
1991
2132
  const found = await db.batch(strippedStmts);
1992
2133
  const stripped = new Set(found.flatMap((r) => r.results.map((row) => row.url)));
@@ -1997,8 +2138,11 @@ app$4.openapi(ingestRoute, async (c) => {
1997
2138
  await db.batch(restores.slice(i, i + INGEST_BATCH_CHUNK));
1998
2139
  }
1999
2140
  }
2141
+ endTime(c, "restore");
2000
2142
  if (accepted > 0) {
2143
+ startTime(c, "rank");
2001
2144
  await rankNewJobs(db);
2145
+ endTime(c, "rank");
2002
2146
  }
2003
2147
  return c.json(
2004
2148
  {
@@ -3884,7 +4028,6 @@ function registerFeedbackRoutes(app2) {
3884
4028
  }
3885
4029
  );
3886
4030
  }
3887
- var EDGE_BASE = "https://hadoku.me";
3888
4031
  async function loadTailoringFields(db, id) {
3889
4032
  return db.prepare("SELECT title, company, description, location, workplace_type FROM jobs WHERE id = ?").bind(id).first();
3890
4033
  }
@@ -5031,7 +5174,9 @@ function registerApplicationRoutes(app2) {
5031
5174
  }),
5032
5175
  async (c) => {
5033
5176
  const { status, ownerName } = c.req.valid("query");
5177
+ startTime(c, "who");
5034
5178
  const who = await effectiveUserId(c, ownerName);
5179
+ endTime(c, "who");
5035
5180
  if (isEffectiveUserError(who)) return c.json(who.error.body, who.error.status);
5036
5181
  const userId = who.userId;
5037
5182
  const filter = status ? " AND a.status = ?" : "";
@@ -5044,10 +5189,14 @@ function registerApplicationRoutes(app2) {
5044
5189
  WHERE a.user_id = ?${filter}
5045
5190
  ORDER BY a.updated_at DESC`
5046
5191
  );
5192
+ startTime(c, "apps");
5047
5193
  const rows = await (status ? stmt.bind(userId, status) : stmt.bind(userId)).all();
5194
+ endTime(c, "apps");
5195
+ startTime(c, "answers");
5048
5196
  const answerRows = await c.env.JOB_PLATFORM_DB.prepare(
5049
5197
  "SELECT question_key, question, answer FROM application_answers WHERE user_id = ?"
5050
5198
  ).bind(userId).all();
5199
+ endTime(c, "answers");
5051
5200
  const stored = new Map(
5052
5201
  answerRows.results.map((a) => [a.question_key, { question: a.question, answer: a.answer }])
5053
5202
  );
@@ -5587,6 +5736,7 @@ function createJobPlatformHandler(basePath = "/jobplatform/api") {
5587
5736
  maxAge: 86400
5588
5737
  })
5589
5738
  );
5739
+ app2.use("*", timing({ totalDescription: "worker" }));
5590
5740
  app2.use("*", createEdgeAuth());
5591
5741
  app2.route("/", healthRoutes);
5592
5742
  app2.route("/", ingestRoutes);
@@ -1 +1 @@
1
- {"version":3,"file":"ingest.d.ts","sourceRoot":"","sources":["../../src/routes/ingest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAkB,MAAM,mBAAmB,CAAC;AAChE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAkB,KAAK,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAuB9E,UAAU,YAAY;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE;QAAE,WAAW,EAAE,iBAAiB,CAAA;KAAE,CAAC;CAC9C;AAo7BD,eAAO,MAAM,YAAY,oCAAM,CAAC"}
1
+ {"version":3,"file":"ingest.d.ts","sourceRoot":"","sources":["../../src/routes/ingest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAkB,MAAM,mBAAmB,CAAC;AAEhE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAkB,KAAK,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAuB9E,UAAU,YAAY;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE;QAAE,WAAW,EAAE,iBAAiB,CAAA;KAAE,CAAC;CAC9C;AA47BD,eAAO,MAAM,YAAY,oCAAM,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"applications.d.ts","sourceRoot":"","sources":["../../../src/routes/jobs/applications.ts"],"names":[],"mappings":"AAYA,OAAO,EAKN,KAAK,OAAO,EAEZ,MAAM,aAAa,CAAC;AAwCrB,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAUhF;AAiJD,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAse5D"}
1
+ {"version":3,"file":"applications.d.ts","sourceRoot":"","sources":["../../../src/routes/jobs/applications.ts"],"names":[],"mappings":"AAaA,OAAO,EAKN,KAAK,OAAO,EAEZ,MAAM,aAAa,CAAC;AAwCrB,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAUhF;AAiJD,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CA4e5D"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hadoku/jobplatform-worker",
3
- "version": "2.32.22",
4
- "gitHead": "2dd1c7ef4e99a6642c8e203bf3b9f453e0b24dcb",
3
+ "version": "2.32.24",
4
+ "gitHead": "06ad8b82ef120d3f1e2858f5f29977752c08c3e8",
5
5
  "description": "Job listings aggregation and tracking dashboard - Worker package",
6
6
  "license": "MIT",
7
7
  "type": "module",