@happyvertical/smrt-jobs 0.37.2 → 0.37.3

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.
@@ -1,66 +1,58 @@
1
+ import { s as tuneSqliteForConcurrency } from "./chunks/worker-liveness-C1Gjnhax.js";
1
2
  import { parentPort, workerData } from "node:worker_threads";
2
3
  import { getDatabase } from "@happyvertical/sql";
3
- import { t as tuneSqliteForConcurrency } from "./chunks/worker-liveness-DOTjoIjr.js";
4
+ //#region src/worker-liveness-ticker.ts
4
5
  async function runLivenessTicker(options) {
5
- const db = await getDatabase({
6
- url: options.url,
7
- type: options.type
8
- });
9
- const looksSqlite = (options.type ?? "").toLowerCase() === "sqlite" || !options.type && /^file:|\.(db|sqlite)\b|:memory:/i.test(options.url);
10
- if (looksSqlite) await tuneSqliteForConcurrency(db);
11
- const renew = async () => {
12
- const now = /* @__PURE__ */ new Date();
13
- const expiresAt = new Date(
14
- now.getTime() + options.leaseTtlMs
15
- ).toISOString();
16
- await db.query(
17
- `UPDATE _smrt_workers
6
+ const db = await getDatabase({
7
+ url: options.url,
8
+ type: options.type
9
+ });
10
+ if ((options.type ?? "").toLowerCase() === "sqlite" || !options.type && /^file:|\.(db|sqlite)\b|:memory:/i.test(options.url)) await tuneSqliteForConcurrency(db);
11
+ const renew = async () => {
12
+ const now = /* @__PURE__ */ new Date();
13
+ const expiresAt = new Date(now.getTime() + options.leaseTtlMs).toISOString();
14
+ await db.query(`UPDATE _smrt_workers
18
15
  SET lease_expires_at = ?,
19
16
  heartbeat_at = ?
20
- WHERE worker_id = ?`,
21
- expiresAt,
22
- now.toISOString(),
23
- options.workerKey
24
- );
25
- };
26
- try {
27
- await renew();
28
- } catch (error) {
29
- await db.close?.().catch(() => {
30
- });
31
- throw error;
32
- }
33
- const timer = setInterval(() => {
34
- renew().catch(() => {
35
- });
36
- }, options.leaseTickMs);
37
- let stopped = false;
38
- return {
39
- stop: async () => {
40
- if (stopped) return;
41
- stopped = true;
42
- clearInterval(timer);
43
- try {
44
- await db.close?.();
45
- } catch {
46
- }
47
- }
48
- };
17
+ WHERE worker_id = ?`, expiresAt, now.toISOString(), options.workerKey);
18
+ };
19
+ try {
20
+ await renew();
21
+ } catch (error) {
22
+ await db.close?.().catch(() => {});
23
+ throw error;
24
+ }
25
+ const timer = setInterval(() => {
26
+ renew().catch(() => {});
27
+ }, options.leaseTickMs);
28
+ let stopped = false;
29
+ return { stop: async () => {
30
+ if (stopped) return;
31
+ stopped = true;
32
+ clearInterval(timer);
33
+ try {
34
+ await db.close?.();
35
+ } catch {}
36
+ } };
49
37
  }
38
+ //#endregion
39
+ //#region src/worker-liveness-thread.ts
50
40
  async function main() {
51
- const ticker = await runLivenessTicker(workerData);
52
- parentPort?.on("message", async (message) => {
53
- if (message === "stop") {
54
- await ticker.stop();
55
- parentPort?.postMessage("stopped");
56
- }
57
- });
58
- parentPort?.postMessage("ready");
41
+ const ticker = await runLivenessTicker(workerData);
42
+ parentPort?.on("message", async (message) => {
43
+ if (message === "stop") {
44
+ await ticker.stop();
45
+ parentPort?.postMessage("stopped");
46
+ }
47
+ });
48
+ parentPort?.postMessage("ready");
59
49
  }
60
50
  main().catch((error) => {
61
- parentPort?.postMessage({
62
- type: "error",
63
- message: error instanceof Error ? error.message : String(error)
64
- });
51
+ parentPort?.postMessage({
52
+ type: "error",
53
+ message: error instanceof Error ? error.message : String(error)
54
+ });
65
55
  });
66
- //# sourceMappingURL=worker-liveness-thread.js.map
56
+ //#endregion
57
+
58
+ //# sourceMappingURL=worker-liveness-thread.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"worker-liveness-thread.js","sources":["../src/worker-liveness-ticker.ts","../src/worker-liveness-thread.ts"],"sourcesContent":["import { getDatabase } from '@happyvertical/sql';\nimport { tuneSqliteForConcurrency } from './worker-liveness.js';\n\n/**\n * Configuration for an off-loop liveness ticker (passed via `workerData`).\n */\nexport interface LivenessTickerOptions {\n /** Database URL the owning runner is using. */\n url: string;\n /** Optional explicit engine/type hint. */\n type?: string;\n /** This runner incarnation's worker key (the `_smrt_workers.worker_id`). */\n workerKey: string;\n /** Lease time-to-live in milliseconds. */\n leaseTtlMs: number;\n /** Renewal cadence in milliseconds. */\n leaseTickMs: number;\n}\n\nexport interface LivenessTicker {\n /** Stop renewing and close the connection. */\n stop: () => Promise<void>;\n}\n\n/**\n * Renew a worker's liveness lease from *outside* the handler event loop.\n *\n * Runs on a `node:worker_threads` thread (or, in tests, directly) with its own\n * database connection, so a CPU-bound synchronous handler on the main thread\n * cannot starve the renewal — the failure mode that made the old heartbeat\n * false-recover running jobs (#1474). A dead process stops renewing and the\n * lease expires within its TTL, which is how recovery detects death.\n */\nexport async function runLivenessTicker(\n options: LivenessTickerOptions,\n): Promise<LivenessTicker> {\n const db = await getDatabase({\n url: options.url,\n type: options.type as 'sqlite' | 'postgres' | 'duckdb' | undefined,\n });\n\n // The ticker's connection shares the SQLite file with the runner's main\n // connection; enable WAL + a busy timeout so renewals don't lose a lock race.\n const looksSqlite =\n (options.type ?? '').toLowerCase() === 'sqlite' ||\n (!options.type && /^file:|\\.(db|sqlite)\\b|:memory:/i.test(options.url));\n if (looksSqlite) await tuneSqliteForConcurrency(db);\n\n const renew = async (): Promise<void> => {\n const now = new Date();\n const expiresAt = new Date(\n now.getTime() + options.leaseTtlMs,\n ).toISOString();\n await db.query(\n `UPDATE _smrt_workers\n SET lease_expires_at = ?,\n heartbeat_at = ?\n WHERE worker_id = ?`,\n expiresAt,\n now.toISOString(),\n options.workerKey,\n );\n };\n\n try {\n // Renew immediately so the lease is fresh the moment the ticker starts.\n await renew();\n } catch (error) {\n // Don't leak the connection if the very first renewal fails.\n await db.close?.().catch(() => {});\n throw error;\n }\n\n const timer = setInterval(() => {\n renew().catch(() => {\n // Transient renewal failures are tolerated; the next tick retries and\n // the lease TTL absorbs a missed beat (TTL >= 3x tick).\n });\n }, options.leaseTickMs);\n\n let stopped = false;\n return {\n stop: async () => {\n if (stopped) return;\n stopped = true;\n clearInterval(timer);\n try {\n await db.close?.();\n } catch {\n // Best-effort connection teardown.\n }\n },\n };\n}\n","/**\n * Worker-thread entry for the off-loop liveness ticker.\n *\n * Thin shell: read {@link LivenessTickerOptions} from `workerData`, start the\n * ticker, and stop it on a `'stop'` message from the parent. All real logic\n * lives in {@link runLivenessTicker} so it can be unit-tested without spawning\n * a thread. Keep this file decorator-free so Node type-stripping can run the\n * `.ts` directly under vitest.\n */\nimport { parentPort, workerData } from 'node:worker_threads';\nimport {\n type LivenessTickerOptions,\n runLivenessTicker,\n} from './worker-liveness-ticker.js';\n\nasync function main(): Promise<void> {\n const ticker = await runLivenessTicker(workerData as LivenessTickerOptions);\n\n parentPort?.on('message', async (message) => {\n if (message === 'stop') {\n await ticker.stop();\n parentPort?.postMessage('stopped');\n }\n });\n\n // Signal the parent that the lease is seeded and renewal is running.\n parentPort?.postMessage('ready');\n}\n\nmain().catch((error) => {\n // Surface startup failure to the parent so the runner can fall back to\n // main-loop renewal rather than silently losing its lease.\n parentPort?.postMessage({\n type: 'error',\n message: error instanceof Error ? error.message : String(error),\n });\n});\n"],"names":[],"mappings":";;;AAiCA,eAAsB,kBACpB,SACyB;AACzB,QAAM,KAAK,MAAM,YAAY;AAAA,IAC3B,KAAK,QAAQ;AAAA,IACb,MAAM,QAAQ;AAAA,EAAA,CACf;AAID,QAAM,eACH,QAAQ,QAAQ,IAAI,YAAA,MAAkB,YACtC,CAAC,QAAQ,QAAQ,mCAAmC,KAAK,QAAQ,GAAG;AACvE,MAAI,YAAa,OAAM,yBAAyB,EAAE;AAElD,QAAM,QAAQ,YAA2B;AACvC,UAAM,0BAAU,KAAA;AAChB,UAAM,YAAY,IAAI;AAAA,MACpB,IAAI,QAAA,IAAY,QAAQ;AAAA,IAAA,EACxB,YAAA;AACF,UAAM,GAAG;AAAA,MACP;AAAA;AAAA;AAAA;AAAA,MAIA;AAAA,MACA,IAAI,YAAA;AAAA,MACJ,QAAQ;AAAA,IAAA;AAAA,EAEZ;AAEA,MAAI;AAEF,UAAM,MAAA;AAAA,EACR,SAAS,OAAO;AAEd,UAAM,GAAG,UAAU,MAAM,MAAM;AAAA,IAAC,CAAC;AACjC,UAAM;AAAA,EACR;AAEA,QAAM,QAAQ,YAAY,MAAM;AAC9B,UAAA,EAAQ,MAAM,MAAM;AAAA,IAGpB,CAAC;AAAA,EACH,GAAG,QAAQ,WAAW;AAEtB,MAAI,UAAU;AACd,SAAO;AAAA,IACL,MAAM,YAAY;AAChB,UAAI,QAAS;AACb,gBAAU;AACV,oBAAc,KAAK;AACnB,UAAI;AACF,cAAM,GAAG,QAAA;AAAA,MACX,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EAAA;AAEJ;AC9EA,eAAe,OAAsB;AACnC,QAAM,SAAS,MAAM,kBAAkB,UAAmC;AAE1E,cAAY,GAAG,WAAW,OAAO,YAAY;AAC3C,QAAI,YAAY,QAAQ;AACtB,YAAM,OAAO,KAAA;AACb,kBAAY,YAAY,SAAS;AAAA,IACnC;AAAA,EACF,CAAC;AAGD,cAAY,YAAY,OAAO;AACjC;AAEA,OAAO,MAAM,CAAC,UAAU;AAGtB,cAAY,YAAY;AAAA,IACtB,MAAM;AAAA,IACN,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,EAAA,CAC/D;AACH,CAAC;"}
1
+ {"version":3,"file":"worker-liveness-thread.js","names":[],"sources":["../src/worker-liveness-ticker.ts","../src/worker-liveness-thread.ts"],"sourcesContent":["import { getDatabase } from '@happyvertical/sql';\nimport { tuneSqliteForConcurrency } from './worker-liveness.js';\n\n/**\n * Configuration for an off-loop liveness ticker (passed via `workerData`).\n */\nexport interface LivenessTickerOptions {\n /** Database URL the owning runner is using. */\n url: string;\n /** Optional explicit engine/type hint. */\n type?: string;\n /** This runner incarnation's worker key (the `_smrt_workers.worker_id`). */\n workerKey: string;\n /** Lease time-to-live in milliseconds. */\n leaseTtlMs: number;\n /** Renewal cadence in milliseconds. */\n leaseTickMs: number;\n}\n\nexport interface LivenessTicker {\n /** Stop renewing and close the connection. */\n stop: () => Promise<void>;\n}\n\n/**\n * Renew a worker's liveness lease from *outside* the handler event loop.\n *\n * Runs on a `node:worker_threads` thread (or, in tests, directly) with its own\n * database connection, so a CPU-bound synchronous handler on the main thread\n * cannot starve the renewal — the failure mode that made the old heartbeat\n * false-recover running jobs (#1474). A dead process stops renewing and the\n * lease expires within its TTL, which is how recovery detects death.\n */\nexport async function runLivenessTicker(\n options: LivenessTickerOptions,\n): Promise<LivenessTicker> {\n const db = await getDatabase({\n url: options.url,\n type: options.type as 'sqlite' | 'postgres' | 'duckdb' | undefined,\n });\n\n // The ticker's connection shares the SQLite file with the runner's main\n // connection; enable WAL + a busy timeout so renewals don't lose a lock race.\n const looksSqlite =\n (options.type ?? '').toLowerCase() === 'sqlite' ||\n (!options.type && /^file:|\\.(db|sqlite)\\b|:memory:/i.test(options.url));\n if (looksSqlite) await tuneSqliteForConcurrency(db);\n\n const renew = async (): Promise<void> => {\n const now = new Date();\n const expiresAt = new Date(\n now.getTime() + options.leaseTtlMs,\n ).toISOString();\n await db.query(\n `UPDATE _smrt_workers\n SET lease_expires_at = ?,\n heartbeat_at = ?\n WHERE worker_id = ?`,\n expiresAt,\n now.toISOString(),\n options.workerKey,\n );\n };\n\n try {\n // Renew immediately so the lease is fresh the moment the ticker starts.\n await renew();\n } catch (error) {\n // Don't leak the connection if the very first renewal fails.\n await db.close?.().catch(() => {});\n throw error;\n }\n\n const timer = setInterval(() => {\n renew().catch(() => {\n // Transient renewal failures are tolerated; the next tick retries and\n // the lease TTL absorbs a missed beat (TTL >= 3x tick).\n });\n }, options.leaseTickMs);\n\n let stopped = false;\n return {\n stop: async () => {\n if (stopped) return;\n stopped = true;\n clearInterval(timer);\n try {\n await db.close?.();\n } catch {\n // Best-effort connection teardown.\n }\n },\n };\n}\n","/**\n * Worker-thread entry for the off-loop liveness ticker.\n *\n * Thin shell: read {@link LivenessTickerOptions} from `workerData`, start the\n * ticker, and stop it on a `'stop'` message from the parent. All real logic\n * lives in {@link runLivenessTicker} so it can be unit-tested without spawning\n * a thread. Keep this file decorator-free so Node type-stripping can run the\n * `.ts` directly under vitest.\n */\nimport { parentPort, workerData } from 'node:worker_threads';\nimport {\n type LivenessTickerOptions,\n runLivenessTicker,\n} from './worker-liveness-ticker.js';\n\nasync function main(): Promise<void> {\n const ticker = await runLivenessTicker(workerData as LivenessTickerOptions);\n\n parentPort?.on('message', async (message) => {\n if (message === 'stop') {\n await ticker.stop();\n parentPort?.postMessage('stopped');\n }\n });\n\n // Signal the parent that the lease is seeded and renewal is running.\n parentPort?.postMessage('ready');\n}\n\nmain().catch((error) => {\n // Surface startup failure to the parent so the runner can fall back to\n // main-loop renewal rather than silently losing its lease.\n parentPort?.postMessage({\n type: 'error',\n message: error instanceof Error ? error.message : String(error),\n });\n});\n"],"mappings":";;;;AAiCA,eAAsB,kBACpB,SACyB;CACzB,MAAM,KAAK,MAAM,YAAY;EAC3B,KAAK,QAAQ;EACb,MAAM,QAAQ;CAChB,CAAC;CAOD,KAFG,QAAQ,QAAQ,GAAA,CAAI,YAAY,MAAM,YACtC,CAAC,QAAQ,QAAQ,mCAAmC,KAAK,QAAQ,GAAG,GACtD,MAAM,yBAAyB,EAAE;CAElD,MAAM,QAAQ,YAA2B;EACvC,MAAM,sBAAM,IAAI,KAAK;EACrB,MAAM,YAAY,IAAI,KACpB,IAAI,QAAQ,IAAI,QAAQ,UAC1B,CAAA,CAAE,YAAY;EACd,MAAM,GAAG,MACP;;;8BAIA,WACA,IAAI,YAAY,GAChB,QAAQ,SACV;CACF;CAEA,IAAI;EAEF,MAAM,MAAM;CACd,SAAS,OAAO;EAEd,MAAM,GAAG,QAAQ,CAAA,CAAE,YAAY,CAAC,CAAC;EACjC,MAAM;CACR;CAEA,MAAM,QAAQ,kBAAkB;EAC9B,MAAM,CAAA,CAAE,YAAY,CAGpB,CAAC;CACH,GAAG,QAAQ,WAAW;CAEtB,IAAI,UAAU;CACd,OAAO,EACL,MAAM,YAAY;EAChB,IAAI,SAAS;EACb,UAAU;EACV,cAAc,KAAK;EACnB,IAAI;GACF,MAAM,GAAG,QAAQ;EACnB,QAAQ,CAER;CACF,EACF;AACF;;;AC9EA,eAAe,OAAsB;CACnC,MAAM,SAAS,MAAM,kBAAkB,UAAmC;CAE1E,YAAY,GAAG,WAAW,OAAO,YAAY;EAC3C,IAAI,YAAY,QAAQ;GACtB,MAAM,OAAO,KAAK;GAClB,YAAY,YAAY,SAAS;EACnC;CACF,CAAC;CAGD,YAAY,YAAY,OAAO;AACjC;AAEA,KAAK,CAAA,CAAE,OAAO,UAAU;CAGtB,YAAY,YAAY;EACtB,MAAM;EACN,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;CAChE,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-jobs",
3
- "version": "0.37.2",
3
+ "version": "0.37.3",
4
4
  "description": "Background job processing for SMRT objects with persistence and scheduling",
5
5
  "author": "HappyVertical",
6
6
  "type": "module",
@@ -42,7 +42,7 @@
42
42
  }
43
43
  },
44
44
  "peerDependencies": {
45
- "svelte": "^5.18.0"
45
+ "svelte": "^5.56.4"
46
46
  },
47
47
  "peerDependenciesMeta": {
48
48
  "svelte": {
@@ -50,26 +50,26 @@
50
50
  }
51
51
  },
52
52
  "dependencies": {
53
- "@happyvertical/jobs": "^0.74.10",
54
- "@happyvertical/logger": "^0.74.10",
55
- "@happyvertical/sql": "^0.74.10",
56
- "@happyvertical/utils": "^0.74.10",
57
- "@happyvertical/smrt-config": "0.37.2",
58
- "@happyvertical/smrt-core": "0.37.2",
59
- "@happyvertical/smrt-tenancy": "0.37.2",
60
- "@happyvertical/smrt-types": "0.37.2",
61
- "@happyvertical/smrt-ui": "0.37.2"
53
+ "@happyvertical/jobs": "^0.74.11",
54
+ "@happyvertical/logger": "^0.74.11",
55
+ "@happyvertical/sql": "^0.74.11",
56
+ "@happyvertical/utils": "^0.74.11",
57
+ "@happyvertical/smrt-core": "0.37.3",
58
+ "@happyvertical/smrt-config": "0.37.3",
59
+ "@happyvertical/smrt-tenancy": "0.37.3",
60
+ "@happyvertical/smrt-types": "0.37.3",
61
+ "@happyvertical/smrt-ui": "0.37.3"
62
62
  },
63
63
  "devDependencies": {
64
- "@sveltejs/package": "^2.5.7",
65
- "@sveltejs/vite-plugin-svelte": "^6.2.4",
66
- "@types/node": "25.0.9",
67
- "svelte": "^5.46.4",
68
- "svelte-check": "^4.3.5",
64
+ "@sveltejs/package": "^2.5.8",
65
+ "@sveltejs/vite-plugin-svelte": "^7.1.2",
66
+ "@types/node": "24.13.2",
67
+ "svelte": "^5.56.4",
68
+ "svelte-check": "^4.7.1",
69
69
  "typescript": "^5.9.3",
70
- "vite": "^7.3.6",
71
- "vitest": "^4.0.17",
72
- "@happyvertical/smrt-vitest": "0.37.2"
70
+ "vite": "^8.1.2",
71
+ "vitest": "^4.1.9",
72
+ "@happyvertical/smrt-vitest": "0.37.3"
73
73
  },
74
74
  "publishConfig": {
75
75
  "registry": "https://registry.npmjs.org",