@hatchet-dev/typescript-sdk 1.32.0 → 1.33.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hatchet-dev/typescript-sdk",
3
- "version": "1.32.0",
3
+ "version": "1.33.1",
4
4
  "engines": {
5
5
  "node": ">=20"
6
6
  },
@@ -23,6 +23,8 @@ export declare class HealthServer {
23
23
  constructor(port: number, getStatus: () => WorkerStatus, workerName: string, getSlots: () => number, getActions: () => string[], getLabels: () => Record<string, string | number>, logger: Logger);
24
24
  private handleRequest;
25
25
  private handleHealth;
26
+ private handleReadyz;
27
+ private handleLivez;
26
28
  private initializeMetrics;
27
29
  private handleMetrics;
28
30
  start(): Promise<void>;
@@ -40,6 +40,12 @@ class HealthServer {
40
40
  if (url === '/health' && req.method === 'GET') {
41
41
  yield this.handleHealth(res);
42
42
  }
43
+ else if (url === '/readyz' && req.method === 'GET') {
44
+ this.handleReadyz(res);
45
+ }
46
+ else if (url === '/livez' && req.method === 'GET') {
47
+ this.handleLivez(res);
48
+ }
43
49
  else if (url === '/metrics' && req.method === 'GET') {
44
50
  yield this.handleMetrics(res);
45
51
  }
@@ -59,10 +65,32 @@ class HealthServer {
59
65
  labels: this.getLabels(),
60
66
  nodeVersion: process.version,
61
67
  };
68
+ // Always return 200 for compatibility with consumers that inspect the JSON
69
+ // status. Use /readyz when the HTTP status must indicate readiness.
62
70
  res.writeHead(200, { 'Content-Type': 'application/json' });
63
71
  yield res.end(JSON.stringify(response));
64
72
  });
65
73
  }
74
+ // Kubernetes-style readiness probe: 200 only when the worker is HEALTHY
75
+ // (registered and holding an action listener), 503 otherwise. Unlike
76
+ // /health, this is a plain status-code contract a vanilla `httpGet` probe
77
+ // can consume directly, with no body to parse.
78
+ handleReadyz(res) {
79
+ const ready = this.getStatus() === exports.workerStatus.HEALTHY;
80
+ res.writeHead(ready ? 200 : 503, { 'Content-Type': 'text/plain' });
81
+ res.end(ready ? 'ok' : 'not ready');
82
+ }
83
+ // Kubernetes-style liveness probe: 200 whenever this handler can run at
84
+ // all, deliberately independent of Hatchet connectivity. A worker that has
85
+ // temporarily lost its action listener (UNHEALTHY) should be pulled from
86
+ // rotation via /readyz, not killed and restarted via /livez — restarting
87
+ // does nothing to fix an upstream Hatchet outage and drops in-flight work.
88
+ // /livez only needs to fail when the process itself is deadlocked/wedged,
89
+ // which this HTTP response reaching the caller already disproves.
90
+ handleLivez(res) {
91
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
92
+ res.end('ok');
93
+ }
66
94
  initializeMetrics() {
67
95
  try {
68
96
  // THIS IS AN OPTIONAL DEPENDENCY
package/v1/embedded.js CHANGED
@@ -83,6 +83,46 @@ const net_1 = require("net");
83
83
  const promises_1 = require("stream/promises");
84
84
  const REPO_URL = 'https://github.com/hatchet-dev/hatchet-embedded';
85
85
  const DEFAULT_READY_TIMEOUT_MS = 300000;
86
+ const SLOW_NOTICE_DELAY_MS = 2000;
87
+ const HEARTBEAT_INTERVAL_MS = 30000;
88
+ // first-contact progress goes to stderr (like the engine's own output) so it
89
+ // never corrupts program output; warm starts print at most one line
90
+ function logProgress(message) {
91
+ process.stderr.write(`hatchet embedded: ${message}\n`);
92
+ }
93
+ // prints startMsg only if fn is still running after SLOW_NOTICE_DELAY_MS (and
94
+ // doneMsg once it finishes), so fast warm-start network calls stay quiet while
95
+ // a blocked one explains what the process is waiting on
96
+ function withSlowNotice(startMsg, doneMsg, fn) {
97
+ return __awaiter(this, void 0, void 0, function* () {
98
+ var _a;
99
+ let noticed = false;
100
+ let finished = false;
101
+ const timer = setTimeout(() => {
102
+ // guards a callback already queued when fn settles, so a stale start
103
+ // message can never print after completion
104
+ if (finished) {
105
+ return;
106
+ }
107
+ noticed = true;
108
+ logProgress(startMsg);
109
+ }, SLOW_NOTICE_DELAY_MS);
110
+ (_a = timer.unref) === null || _a === void 0 ? void 0 : _a.call(timer);
111
+ let result;
112
+ try {
113
+ result = yield fn();
114
+ }
115
+ finally {
116
+ finished = true;
117
+ clearTimeout(timer);
118
+ }
119
+ // only reached when fn succeeded
120
+ if (noticed) {
121
+ logProgress(doneMsg);
122
+ }
123
+ return result;
124
+ });
125
+ }
86
126
  function sidecarAssetName() {
87
127
  const platform = { darwin: 'darwin', linux: 'linux' }[process.platform];
88
128
  const arch = { x64: 'amd64', arm64: 'arm64' }[process.arch];
@@ -168,17 +208,18 @@ function resolveExpectedChecksum(tag, asset, binPath) {
168
208
  }
169
209
  function ensureSidecarBinary(version, checksum) {
170
210
  return __awaiter(this, void 0, void 0, function* () {
171
- const tag = yield resolveVersion(version);
211
+ const tag = yield withSlowNotice(`resolving the latest hatchet-embedded release from ${REPO_URL}`, 'resolved the latest hatchet-embedded release', () => resolveVersion(version));
172
212
  const asset = sidecarAssetName();
173
213
  const binPath = path.join(os.homedir(), '.hatchet', 'embedded', tag, asset);
174
214
  yield fs.mkdir(path.dirname(binPath), { recursive: true });
175
215
  // verified on every start, not just at download; a cached binary that no
176
216
  // longer matches the expected checksum is re-downloaded
177
- const expected = checksum !== null && checksum !== void 0 ? checksum : (yield resolveExpectedChecksum(tag, asset, binPath));
217
+ const expected = checksum !== null && checksum !== void 0 ? checksum : (yield withSlowNotice(`resolving the expected checksum for ${tag} (the cached checksum is used if the release cannot be reached)`, 'expected checksum resolved', () => resolveExpectedChecksum(tag, asset, binPath)));
178
218
  const cached = yield fs.access(binPath).then(() => true, () => false);
179
219
  if (cached && (yield sha256File(binPath)) === expected) {
180
220
  return binPath;
181
221
  }
222
+ logProgress(`downloading the embedded engine sidecar ${tag} to ${binPath} (tens of MB, cached for later runs)`);
182
223
  const url = `${REPO_URL}/releases/download/${tag}/${asset}`;
183
224
  const res = yield fetch(url);
184
225
  if (!res.ok || !res.body) {
@@ -199,12 +240,15 @@ function ensureSidecarBinary(version, checksum) {
199
240
  finally {
200
241
  yield fs.rm(tmpPath, { force: true });
201
242
  }
243
+ logProgress(`sidecar ${tag} downloaded`);
202
244
  return binPath;
203
245
  });
204
246
  }
205
247
  function waitForHandshake(child, handshakePath, timeoutMs) {
206
248
  return __awaiter(this, void 0, void 0, function* () {
207
- const deadline = Date.now() + timeoutMs;
249
+ const start = Date.now();
250
+ const deadline = start + timeoutMs;
251
+ let nextHeartbeat = start + HEARTBEAT_INTERVAL_MS;
208
252
  let exited;
209
253
  child.once('exit', (code) => {
210
254
  exited = new Error(`hatchet embedded sidecar exited with code ${code} before becoming ready`);
@@ -213,6 +257,11 @@ function waitForHandshake(child, handshakePath, timeoutMs) {
213
257
  if (exited) {
214
258
  throw exited;
215
259
  }
260
+ if (Date.now() >= nextHeartbeat) {
261
+ const elapsed = Math.round((Date.now() - start) / 1000);
262
+ logProgress(`still waiting for the embedded engine (${elapsed}s elapsed)`);
263
+ nextHeartbeat += HEARTBEAT_INTERVAL_MS;
264
+ }
216
265
  try {
217
266
  const handshake = JSON.parse(yield fs.readFile(handshakePath, 'utf8'));
218
267
  if (handshake.token) {
@@ -232,6 +281,8 @@ function waitForHandshake(child, handshakePath, timeoutMs) {
232
281
  }
233
282
  // sidecars started in this process that have not been stopped yet
234
283
  const activeSidecars = new Set();
284
+ // the ambient-token warning is printed at most once per process
285
+ let warnedAmbientToken = false;
235
286
  /**
236
287
  * Gracefully stops every sidecar started in this process by
237
288
  * `HatchetEmbeddedClient.init()` (or `startEmbeddedSidecar`) and resolves once
@@ -254,6 +305,10 @@ function stopEmbeddedSidecar() {
254
305
  function startEmbeddedSidecar() {
255
306
  return __awaiter(this, arguments, void 0, function* (opts = {}) {
256
307
  var _a, _b;
308
+ if (process.env.HATCHET_CLIENT_TOKEN && !warnedAmbientToken) {
309
+ warnedAmbientToken = true;
310
+ logProgress('warning: HATCHET_CLIENT_TOKEN is set in the environment. Hatchet clients created with the standard constructor in this process will NOT use the embedded engine; unset HATCHET_CLIENT_TOKEN for embedded runs.');
311
+ }
257
312
  const suppliedPath = (_a = opts.binaryPath) !== null && _a !== void 0 ? _a : process.env.HATCHET_CLIENT_EMBEDDED_BINARY_PATH;
258
313
  let binPath;
259
314
  if (suppliedPath) {
@@ -299,6 +354,9 @@ function startEmbeddedSidecar() {
299
354
  const child = (0, child_process_1.spawn)(binPath, args, { stdio: ['pipe', 'ignore', 'inherit'] });
300
355
  const killChild = () => child.kill();
301
356
  process.once('exit', killChild);
357
+ logProgress(opts.databaseUrl
358
+ ? 'starting the embedded engine'
359
+ : 'starting the embedded engine (first run initializes a bundled Postgres and can take a minute)');
302
360
  let handshake;
303
361
  try {
304
362
  handshake = yield waitForHandshake(child, handshakePath, (_b = opts.readyTimeoutMs) !== null && _b !== void 0 ? _b : DEFAULT_READY_TIMEOUT_MS);
package/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const HATCHET_VERSION = "1.32.0";
1
+ export declare const HATCHET_VERSION = "1.33.1";
package/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HATCHET_VERSION = void 0;
4
- exports.HATCHET_VERSION = '1.32.0';
4
+ exports.HATCHET_VERSION = '1.33.1';