@chronos.sh/sdk 0.0.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/LICENSE +21 -0
- package/README.md +66 -0
- package/dist/index.cjs +552 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +323 -0
- package/dist/index.d.ts +323 -0
- package/dist/index.js +544 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* Base class for all errors thrown by the Chronos SDK. Catch this in a
|
|
4
|
+
* single `catch` to handle any SDK failure generically; use the subclasses
|
|
5
|
+
* to branch on cause.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { Chronos, ChronosError } from '@chronos.sh/sdk';
|
|
10
|
+
*
|
|
11
|
+
* const chronos = new Chronos({ apiKey: 'chrns_...' });
|
|
12
|
+
* try {
|
|
13
|
+
* await chronos.worker.start();
|
|
14
|
+
* } catch (err) {
|
|
15
|
+
* if (err instanceof ChronosError) {
|
|
16
|
+
* console.error('Chronos failed:', err.message);
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
var ChronosError = class extends Error {
|
|
22
|
+
constructor(message, options) {
|
|
23
|
+
super(message, options);
|
|
24
|
+
this.name = "ChronosError";
|
|
25
|
+
if (options && !("cause" in this)) Object.defineProperty(this, "cause", {
|
|
26
|
+
value: options.cause,
|
|
27
|
+
configurable: true,
|
|
28
|
+
writable: true
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Thrown when SDK options fail validation at `new Chronos({ ... })`. Covers
|
|
34
|
+
* `apiKey`, `baseUrl`, `pollWaitTimeSeconds`, and `retryDelayMs`.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* import { Chronos, ChronosConfigError } from '@chronos.sh/sdk';
|
|
39
|
+
*
|
|
40
|
+
* try {
|
|
41
|
+
* const chronos = new Chronos({ apiKey: '' });
|
|
42
|
+
* } catch (err) {
|
|
43
|
+
* if (err instanceof ChronosConfigError) {
|
|
44
|
+
* console.error('Invalid Chronos config:', err.message);
|
|
45
|
+
* }
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
var ChronosConfigError = class extends ChronosError {
|
|
50
|
+
constructor(message) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = "ChronosConfigError";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Thrown when the Chronos API responds with a non-2xx status or a
|
|
57
|
+
* `success: false` envelope. Carries HTTP `status`, the application
|
|
58
|
+
* `code`, parsed `body`, and the API's `X-Request-Id`.
|
|
59
|
+
*
|
|
60
|
+
* `instanceof ChronosApiError` means the server replied;
|
|
61
|
+
* network/transport failures throw {@link ChronosNetworkError} instead.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* import { ChronosApiError } from '@chronos.sh/sdk';
|
|
66
|
+
*
|
|
67
|
+
* function handleSdkError(err: unknown) {
|
|
68
|
+
* if (err instanceof ChronosApiError) {
|
|
69
|
+
* if (err.status === 401) return refreshAuth();
|
|
70
|
+
* console.error('API error', { status: err.status, requestId: err.requestId });
|
|
71
|
+
* }
|
|
72
|
+
* }
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
var ChronosApiError = class extends ChronosError {
|
|
76
|
+
/** HTTP status code returned by the Chronos API. */
|
|
77
|
+
status;
|
|
78
|
+
/** Application-level error code from the response envelope, when present. */
|
|
79
|
+
code;
|
|
80
|
+
/** Full parsed response payload (envelope + data, or whatever the server returned). */
|
|
81
|
+
body;
|
|
82
|
+
/** Value of the `X-Request-Id` response header, when present. */
|
|
83
|
+
requestId;
|
|
84
|
+
constructor(message, options) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.name = "ChronosApiError";
|
|
87
|
+
this.status = options.status;
|
|
88
|
+
this.code = options.code;
|
|
89
|
+
this.body = options.body;
|
|
90
|
+
this.requestId = options.requestId;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Thrown when the underlying `fetch` rejects before the server replies —
|
|
95
|
+
* DNS failure, TCP reset, connection refused, etc. The original error is
|
|
96
|
+
* available on `.cause`.
|
|
97
|
+
*
|
|
98
|
+
* Abort signals propagate unwrapped — `instanceof ChronosNetworkError`
|
|
99
|
+
* always means a real transport failure, not a graceful shutdown.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* import { ChronosNetworkError } from '@chronos.sh/sdk';
|
|
104
|
+
*
|
|
105
|
+
* function handleSdkError(err: unknown) {
|
|
106
|
+
* if (err instanceof ChronosNetworkError) {
|
|
107
|
+
* console.warn('Transport blip', { cause: err.cause });
|
|
108
|
+
* }
|
|
109
|
+
* }
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
var ChronosNetworkError = class extends ChronosError {
|
|
113
|
+
constructor(message, options) {
|
|
114
|
+
super(message, options);
|
|
115
|
+
this.name = "ChronosNetworkError";
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Wraps an exception thrown by a user-supplied {@link ChronosHandler}. The
|
|
120
|
+
* original error is on `.cause`; `.message` is copied from the original so
|
|
121
|
+
* the SDK reports it to the API as the failure reason.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* import { ChronosHandlerError } from '@chronos.sh/sdk';
|
|
126
|
+
*
|
|
127
|
+
* if (err instanceof ChronosHandlerError) {
|
|
128
|
+
* console.error('Handler threw', err.cause);
|
|
129
|
+
* }
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
var ChronosHandlerError = class extends ChronosError {
|
|
133
|
+
constructor(message, options) {
|
|
134
|
+
super(message, options);
|
|
135
|
+
this.name = "ChronosHandlerError";
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/internal/logger.ts
|
|
140
|
+
const defaultLogger = {
|
|
141
|
+
debug: (message, meta) => logToConsole(console.debug, message, meta),
|
|
142
|
+
info: (message, meta) => logToConsole(console.info, message, meta),
|
|
143
|
+
warn: (message, meta) => logToConsole(console.warn, message, meta),
|
|
144
|
+
error: (message, meta) => logToConsole(console.error, message, meta)
|
|
145
|
+
};
|
|
146
|
+
function logToConsole(method, message, meta) {
|
|
147
|
+
if (meta) method(message, meta);
|
|
148
|
+
else method(message);
|
|
149
|
+
}
|
|
150
|
+
function validateApiKey(apiKey) {
|
|
151
|
+
const trimmed = apiKey?.trim();
|
|
152
|
+
if (!trimmed) throw new ChronosConfigError("Chronos apiKey is required");
|
|
153
|
+
return trimmed;
|
|
154
|
+
}
|
|
155
|
+
function validatePollWaitTime(seconds) {
|
|
156
|
+
if (!Number.isInteger(seconds) || seconds < 0 || seconds > 20) throw new ChronosConfigError(`pollWaitTimeSeconds must be an integer between 0 and 20`);
|
|
157
|
+
}
|
|
158
|
+
function validateRetryDelayMs(ms) {
|
|
159
|
+
if (!Number.isFinite(ms) || ms < 0) throw new ChronosConfigError("retryDelayMs must be a non-negative number");
|
|
160
|
+
}
|
|
161
|
+
function normalizeHandlerName(name) {
|
|
162
|
+
const normalized = name.trim();
|
|
163
|
+
if (!normalized) throw new ChronosError("Handler name is required");
|
|
164
|
+
if (normalized.length > 255) throw new ChronosError(`Handler name must be 255 characters or fewer`);
|
|
165
|
+
return normalized;
|
|
166
|
+
}
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/client.ts
|
|
169
|
+
/** Default Chronos API base URL. */
|
|
170
|
+
const DEFAULT_BASE_URL = "https://api.chronos.sh";
|
|
171
|
+
var BaseClient = class {
|
|
172
|
+
apiKey;
|
|
173
|
+
baseUrl;
|
|
174
|
+
fetch;
|
|
175
|
+
logger;
|
|
176
|
+
headers;
|
|
177
|
+
constructor(options) {
|
|
178
|
+
this.apiKey = validateApiKey(options.apiKey);
|
|
179
|
+
this.baseUrl = normalizeBaseUrl(options.baseUrl ?? "https://api.chronos.sh");
|
|
180
|
+
this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
181
|
+
this.logger = options.logger ?? defaultLogger;
|
|
182
|
+
this.headers = {
|
|
183
|
+
"content-type": "application/json",
|
|
184
|
+
authorization: `Bearer ${this.apiKey}`
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
async request(path, body, signal) {
|
|
188
|
+
let response;
|
|
189
|
+
try {
|
|
190
|
+
response = await this.fetch(`${this.baseUrl}${path}`, {
|
|
191
|
+
method: "POST",
|
|
192
|
+
headers: this.headers,
|
|
193
|
+
body: JSON.stringify(body),
|
|
194
|
+
signal
|
|
195
|
+
});
|
|
196
|
+
} catch (err) {
|
|
197
|
+
if (signal?.aborted) throw err;
|
|
198
|
+
throw new ChronosNetworkError(`Chronos API request failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
199
|
+
}
|
|
200
|
+
const payload = await parseJsonResponse(response);
|
|
201
|
+
const envelope = isEnvelope(payload) ? payload : void 0;
|
|
202
|
+
if (!response.ok || envelope && !envelope.success) throw new ChronosApiError(apiErrorMessage(response, envelope), {
|
|
203
|
+
status: response.status,
|
|
204
|
+
code: typeof envelope?.code === "string" ? envelope.code : void 0,
|
|
205
|
+
body: payload,
|
|
206
|
+
requestId: response.headers.get("x-request-id") ?? void 0
|
|
207
|
+
});
|
|
208
|
+
if (!envelope) throw new ChronosError("Chronos API returned an invalid response");
|
|
209
|
+
return envelope.data;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
function normalizeBaseUrl(baseUrl) {
|
|
213
|
+
const normalized = baseUrl.trim().replace(/\/+$/, "");
|
|
214
|
+
if (!normalized) throw new ChronosConfigError("Chronos baseUrl is required");
|
|
215
|
+
return normalized;
|
|
216
|
+
}
|
|
217
|
+
async function parseJsonResponse(response) {
|
|
218
|
+
try {
|
|
219
|
+
return await response.json();
|
|
220
|
+
} catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function apiErrorMessage(response, envelope) {
|
|
225
|
+
if (typeof envelope?.message === "string" && envelope.message.trim()) return envelope.message;
|
|
226
|
+
return response.ok ? "Chronos API returned an invalid response" : `Chronos API request failed with status ${response.status}`;
|
|
227
|
+
}
|
|
228
|
+
function isEnvelope(value) {
|
|
229
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
230
|
+
return typeof value.success === "boolean";
|
|
231
|
+
}
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/worker.ts
|
|
234
|
+
/** Default worker long-poll wait time in seconds. Equal to the API maximum. */
|
|
235
|
+
const DEFAULT_POLL_WAIT_TIME_SECONDS = 20;
|
|
236
|
+
const DEFAULT_RETRY_DELAY_MS = 1e3;
|
|
237
|
+
const RESULT_REPORT_MAX_ATTEMPTS = 3;
|
|
238
|
+
const MAX_REPORTED_ERROR_LENGTH = 4096;
|
|
239
|
+
const DEFAULT_HANDLER_ERROR = "Chronos handler failed";
|
|
240
|
+
/**
|
|
241
|
+
* Long-poll worker. Claims jobs from the Chronos API, dispatches them to
|
|
242
|
+
* registered handlers, and reports results.
|
|
243
|
+
*
|
|
244
|
+
* Construct via `new Chronos({ apiKey }).worker` rather than directly.
|
|
245
|
+
*/
|
|
246
|
+
var Worker = class {
|
|
247
|
+
client;
|
|
248
|
+
pollWaitTimeSeconds;
|
|
249
|
+
retryDelayMs;
|
|
250
|
+
handlers = /* @__PURE__ */ new Map();
|
|
251
|
+
handlerNames = [];
|
|
252
|
+
startPromise;
|
|
253
|
+
pollController;
|
|
254
|
+
constructor(client, options) {
|
|
255
|
+
this.client = client;
|
|
256
|
+
this.pollWaitTimeSeconds = options.pollWaitTimeSeconds ?? 20;
|
|
257
|
+
this.retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
258
|
+
validatePollWaitTime(this.pollWaitTimeSeconds);
|
|
259
|
+
validateRetryDelayMs(this.retryDelayMs);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Register a handler for a named job type. Invoked when the Chronos API
|
|
263
|
+
* claims a job whose `handler` field matches `name`. Names are trimmed and
|
|
264
|
+
* must be 1–255 characters.
|
|
265
|
+
*
|
|
266
|
+
* @param name - Handler name. Must match the schedule's `handler` on the API side.
|
|
267
|
+
* @param handler - Async function invoked with the job context. Return a
|
|
268
|
+
* plain object to record a result, or `undefined` for none.
|
|
269
|
+
* @returns The Worker, for chaining.
|
|
270
|
+
* @throws {ChronosError} If the name is invalid, already registered, or `handler` is not a function.
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* ```ts
|
|
274
|
+
* chronos.worker
|
|
275
|
+
* .handle('send-email', async (ctx) => ({ sent: true }))
|
|
276
|
+
* .handle('cleanup', async () => undefined);
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
handle(name, handler) {
|
|
280
|
+
const normalizedName = normalizeHandlerName(name);
|
|
281
|
+
if (this.handlers.has(normalizedName)) throw new ChronosError(`Handler "${normalizedName}" is already registered`);
|
|
282
|
+
if (typeof handler !== "function") throw new ChronosError(`Handler "${normalizedName}" must be a function`);
|
|
283
|
+
this.handlers.set(normalizedName, handler);
|
|
284
|
+
this.handlerNames.push(normalizedName);
|
|
285
|
+
return this;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Begin long-polling for jobs. The returned promise resolves when
|
|
289
|
+
* {@link Worker.stop} is called and any in-flight job completes.
|
|
290
|
+
*
|
|
291
|
+
* @throws {ChronosError} Synchronously, if no handlers are registered or the worker is already running.
|
|
292
|
+
*/
|
|
293
|
+
start() {
|
|
294
|
+
if (this.startPromise) throw new ChronosError("Chronos worker is already started");
|
|
295
|
+
if (this.handlers.size === 0) throw new ChronosError("Register at least one handler before starting Chronos");
|
|
296
|
+
this.pollController = new AbortController();
|
|
297
|
+
this.startPromise = this.runLoop().finally(() => {
|
|
298
|
+
this.startPromise = void 0;
|
|
299
|
+
this.pollController = void 0;
|
|
300
|
+
});
|
|
301
|
+
return this.startPromise;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Request graceful shutdown. The poll loop is aborted immediately; any
|
|
305
|
+
* in-flight handler and result-report are allowed to complete to preserve
|
|
306
|
+
* at-least-once delivery. Returns the same promise as the active
|
|
307
|
+
* {@link Worker.start}, or a resolved promise if the worker isn't running.
|
|
308
|
+
*/
|
|
309
|
+
stop() {
|
|
310
|
+
this.pollController?.abort();
|
|
311
|
+
return this.startPromise ?? Promise.resolve();
|
|
312
|
+
}
|
|
313
|
+
get isStopped() {
|
|
314
|
+
return this.pollController?.signal.aborted ?? true;
|
|
315
|
+
}
|
|
316
|
+
async runLoop() {
|
|
317
|
+
while (!this.isStopped) try {
|
|
318
|
+
const job = await this.claimJob();
|
|
319
|
+
if (job) await this.processJob(job);
|
|
320
|
+
} catch (err) {
|
|
321
|
+
if (this.isStopped) break;
|
|
322
|
+
this.client.logger.error("Chronos poll loop error", { err: errorToLogValue(err) });
|
|
323
|
+
await sleep(this.retryDelayMs, this.pollController?.signal);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async claimJob() {
|
|
327
|
+
if (this.handlerNames.length === 0) throw new ChronosError("Cannot claim jobs without registered handlers");
|
|
328
|
+
return this.client.request("/v1/worker/jobs/claim", {
|
|
329
|
+
wait_time_seconds: this.pollWaitTimeSeconds,
|
|
330
|
+
handlers: this.handlerNames
|
|
331
|
+
}, this.pollController?.signal);
|
|
332
|
+
}
|
|
333
|
+
async processJob(job) {
|
|
334
|
+
const handler = this.handlers.get(job.handler);
|
|
335
|
+
if (!handler) return this.handleUnregisteredJob(job);
|
|
336
|
+
let handlerResult;
|
|
337
|
+
try {
|
|
338
|
+
handlerResult = await handler(createContext(job));
|
|
339
|
+
} catch (err) {
|
|
340
|
+
const message = errorMessage(err);
|
|
341
|
+
const handlerErr = new ChronosHandlerError(message, { cause: err });
|
|
342
|
+
this.client.logger.error("Chronos handler failed", {
|
|
343
|
+
...jobLogMeta(job),
|
|
344
|
+
err: errorToLogValue(handlerErr)
|
|
345
|
+
});
|
|
346
|
+
await this.safeReportFailed(job, message);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
let result;
|
|
350
|
+
try {
|
|
351
|
+
result = normalizeHandlerResult(handlerResult);
|
|
352
|
+
} catch (err) {
|
|
353
|
+
this.client.logger.error("Chronos handler returned invalid result", {
|
|
354
|
+
...jobLogMeta(job),
|
|
355
|
+
err: errorToLogValue(err)
|
|
356
|
+
});
|
|
357
|
+
await this.safeReportFailed(job, errorMessage(err));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
await this.reportCompleted(job.execution_id, result);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
this.logResultReportFailure(err, job, "completed");
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
async handleUnregisteredJob(job) {
|
|
367
|
+
const message = `Chronos SDK received job for unregistered handler "${job.handler}"`;
|
|
368
|
+
this.client.logger.error(message, jobLogMeta(job));
|
|
369
|
+
await this.safeReportFailed(job, message);
|
|
370
|
+
}
|
|
371
|
+
async safeReportFailed(job, message) {
|
|
372
|
+
try {
|
|
373
|
+
await this.reportFailed(job.execution_id, message);
|
|
374
|
+
} catch (err) {
|
|
375
|
+
this.logResultReportFailure(err, job, "failed");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async reportCompleted(executionId, result) {
|
|
379
|
+
await this.reportResultWithRetry(executionId, {
|
|
380
|
+
status: "completed",
|
|
381
|
+
...result === void 0 ? {} : { result }
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
async reportFailed(executionId, error) {
|
|
385
|
+
await this.reportResultWithRetry(executionId, {
|
|
386
|
+
status: "failed",
|
|
387
|
+
error: truncate(error, MAX_REPORTED_ERROR_LENGTH)
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
async reportResultWithRetry(executionId, body) {
|
|
391
|
+
let lastErr;
|
|
392
|
+
for (let attempt = 1; attempt <= RESULT_REPORT_MAX_ATTEMPTS; attempt++) try {
|
|
393
|
+
await this.client.request(`/v1/worker/executions/${encodeURIComponent(executionId)}/result`, body);
|
|
394
|
+
return;
|
|
395
|
+
} catch (err) {
|
|
396
|
+
const apiErr = err instanceof ChronosApiError ? err : void 0;
|
|
397
|
+
if (apiErr?.status === 409) {
|
|
398
|
+
this.client.logger.warn("Chronos result discarded; execution already terminal", {
|
|
399
|
+
executionId,
|
|
400
|
+
status: body.status,
|
|
401
|
+
code: apiErr.code
|
|
402
|
+
});
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (apiErr && isTerminalReportError(apiErr.status)) throw apiErr;
|
|
406
|
+
lastErr = err;
|
|
407
|
+
if (attempt === RESULT_REPORT_MAX_ATTEMPTS) break;
|
|
408
|
+
this.client.logger.warn("Chronos result report failed; retrying", {
|
|
409
|
+
err: errorToLogValue(err),
|
|
410
|
+
executionId,
|
|
411
|
+
status: body.status,
|
|
412
|
+
attempt,
|
|
413
|
+
maxAttempts: RESULT_REPORT_MAX_ATTEMPTS
|
|
414
|
+
});
|
|
415
|
+
await sleep(this.retryDelayMs);
|
|
416
|
+
}
|
|
417
|
+
throw lastErr;
|
|
418
|
+
}
|
|
419
|
+
logResultReportFailure(err, job, status) {
|
|
420
|
+
const isTerminal = err instanceof ChronosApiError && isTerminalReportError(err.status);
|
|
421
|
+
this.client.logger.error(isTerminal ? "Chronos result report rejected by API" : "Chronos result report failed after retries", {
|
|
422
|
+
...jobLogMeta(job),
|
|
423
|
+
err: errorToLogValue(err),
|
|
424
|
+
status
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
function createContext(job) {
|
|
429
|
+
return {
|
|
430
|
+
jobId: job.job_id,
|
|
431
|
+
executionId: job.execution_id,
|
|
432
|
+
handler: job.handler,
|
|
433
|
+
payload: job.payload,
|
|
434
|
+
scheduledFor: new Date(job.scheduled_for),
|
|
435
|
+
attempt: job.attempt,
|
|
436
|
+
timeout: job.timeout,
|
|
437
|
+
schedule: job.schedule
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function jobLogMeta(job) {
|
|
441
|
+
return {
|
|
442
|
+
jobId: job.job_id,
|
|
443
|
+
executionId: job.execution_id,
|
|
444
|
+
handler: job.handler
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
function normalizeHandlerResult(result) {
|
|
448
|
+
if (result === void 0) return;
|
|
449
|
+
if (!isPlainObject(result)) throw new ChronosError("Chronos handler result must be a plain object or undefined");
|
|
450
|
+
try {
|
|
451
|
+
JSON.stringify(result);
|
|
452
|
+
} catch (err) {
|
|
453
|
+
throw new ChronosError(`Chronos handler result is not JSON-encodable: ${err instanceof Error ? err.message : "value is not JSON-encodable"}`);
|
|
454
|
+
}
|
|
455
|
+
return result;
|
|
456
|
+
}
|
|
457
|
+
function isPlainObject(value) {
|
|
458
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
459
|
+
const proto = Object.getPrototypeOf(value);
|
|
460
|
+
return proto === Object.prototype || proto === null;
|
|
461
|
+
}
|
|
462
|
+
function errorMessage(err) {
|
|
463
|
+
return (err instanceof Error ? err.message : String(err)).trim() || DEFAULT_HANDLER_ERROR;
|
|
464
|
+
}
|
|
465
|
+
function errorToLogValue(err) {
|
|
466
|
+
if (!(err instanceof Error)) return err;
|
|
467
|
+
const base = {
|
|
468
|
+
name: err.name,
|
|
469
|
+
message: err.message,
|
|
470
|
+
stack: err.stack
|
|
471
|
+
};
|
|
472
|
+
if (err instanceof ChronosApiError) return {
|
|
473
|
+
...base,
|
|
474
|
+
status: err.status,
|
|
475
|
+
code: err.code,
|
|
476
|
+
requestId: err.requestId
|
|
477
|
+
};
|
|
478
|
+
if (err instanceof ChronosNetworkError || err instanceof ChronosHandlerError) return {
|
|
479
|
+
...base,
|
|
480
|
+
cause: errorToLogValue(err.cause)
|
|
481
|
+
};
|
|
482
|
+
return base;
|
|
483
|
+
}
|
|
484
|
+
function truncate(value, maxLength) {
|
|
485
|
+
return value.length <= maxLength ? value : value.slice(0, maxLength);
|
|
486
|
+
}
|
|
487
|
+
const NON_TERMINAL_4XX = new Set([
|
|
488
|
+
408,
|
|
489
|
+
409,
|
|
490
|
+
429
|
|
491
|
+
]);
|
|
492
|
+
function isTerminalReportError(status) {
|
|
493
|
+
if (status === 200) return true;
|
|
494
|
+
if (NON_TERMINAL_4XX.has(status)) return false;
|
|
495
|
+
return status >= 400 && status < 500;
|
|
496
|
+
}
|
|
497
|
+
function sleep(ms, signal) {
|
|
498
|
+
if (signal?.aborted) return Promise.resolve();
|
|
499
|
+
return new Promise((resolve) => {
|
|
500
|
+
const timeout = setTimeout(done, ms);
|
|
501
|
+
function done() {
|
|
502
|
+
clearTimeout(timeout);
|
|
503
|
+
signal?.removeEventListener("abort", done);
|
|
504
|
+
resolve();
|
|
505
|
+
}
|
|
506
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
//#endregion
|
|
510
|
+
//#region src/index.ts
|
|
511
|
+
/**
|
|
512
|
+
* The Chronos SDK client. Composes worker and (future) REST resource
|
|
513
|
+
* subclients from a single instance.
|
|
514
|
+
*
|
|
515
|
+
* @example
|
|
516
|
+
* ```ts
|
|
517
|
+
* import { Chronos } from '@chronos.sh/sdk';
|
|
518
|
+
*
|
|
519
|
+
* const chronos = new Chronos({ apiKey: process.env.CHRONOS_API_KEY! });
|
|
520
|
+
*
|
|
521
|
+
* chronos.worker.handle<{ to: string }>('send-email', async (ctx) => {
|
|
522
|
+
* await sendEmail(ctx.payload.to);
|
|
523
|
+
* return { sent: true };
|
|
524
|
+
* });
|
|
525
|
+
*
|
|
526
|
+
* await chronos.worker.start();
|
|
527
|
+
* ```
|
|
528
|
+
*/
|
|
529
|
+
var Chronos = class {
|
|
530
|
+
/** Long-poll worker for executing pull-mode jobs. */
|
|
531
|
+
worker;
|
|
532
|
+
/**
|
|
533
|
+
* @param options - Client configuration. Only `apiKey` is required.
|
|
534
|
+
* @throws {ChronosConfigError} If `apiKey` is missing or any option fails validation.
|
|
535
|
+
*/
|
|
536
|
+
constructor(options) {
|
|
537
|
+
const client = new BaseClient(options);
|
|
538
|
+
this.worker = new Worker(client, options);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
//#endregion
|
|
542
|
+
export { Chronos, ChronosApiError, ChronosConfigError, ChronosError, ChronosHandlerError, ChronosNetworkError, DEFAULT_BASE_URL, DEFAULT_POLL_WAIT_TIME_SECONDS };
|
|
543
|
+
|
|
544
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/errors.ts","../src/internal/logger.ts","../src/internal/validate.ts","../src/client.ts","../src/worker.ts","../src/index.ts"],"sourcesContent":["/**\n * Base class for all errors thrown by the Chronos SDK. Catch this in a\n * single `catch` to handle any SDK failure generically; use the subclasses\n * to branch on cause.\n *\n * @example\n * ```ts\n * import { Chronos, ChronosError } from '@chronos.sh/sdk';\n *\n * const chronos = new Chronos({ apiKey: 'chrns_...' });\n * try {\n * await chronos.worker.start();\n * } catch (err) {\n * if (err instanceof ChronosError) {\n * console.error('Chronos failed:', err.message);\n * }\n * }\n * ```\n */\nexport class ChronosError extends Error {\n /** Original error/value that caused this SDK error, when one is available. */\n declare readonly cause?: unknown;\n\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = 'ChronosError';\n\n if (options && !('cause' in this)) {\n Object.defineProperty(this, 'cause', {\n value: options.cause,\n configurable: true,\n writable: true,\n });\n }\n }\n}\n\n/**\n * Thrown when SDK options fail validation at `new Chronos({ ... })`. Covers\n * `apiKey`, `baseUrl`, `pollWaitTimeSeconds`, and `retryDelayMs`.\n *\n * @example\n * ```ts\n * import { Chronos, ChronosConfigError } from '@chronos.sh/sdk';\n *\n * try {\n * const chronos = new Chronos({ apiKey: '' });\n * } catch (err) {\n * if (err instanceof ChronosConfigError) {\n * console.error('Invalid Chronos config:', err.message);\n * }\n * }\n * ```\n */\nexport class ChronosConfigError extends ChronosError {\n constructor(message: string) {\n super(message);\n this.name = 'ChronosConfigError';\n }\n}\n\n/** Options for constructing a {@link ChronosApiError}. */\nexport type ChronosApiErrorOptions = {\n /** HTTP status code returned by the Chronos API. */\n status: number;\n /** Application-level error code from the response envelope, when present. */\n code?: string;\n /** Full parsed response payload (envelope + data, or whatever the server returned). */\n body?: unknown;\n /** Value of the `X-Request-Id` response header, when present. Pair with server logs. */\n requestId?: string;\n};\n\n/**\n * Thrown when the Chronos API responds with a non-2xx status or a\n * `success: false` envelope. Carries HTTP `status`, the application\n * `code`, parsed `body`, and the API's `X-Request-Id`.\n *\n * `instanceof ChronosApiError` means the server replied;\n * network/transport failures throw {@link ChronosNetworkError} instead.\n *\n * @example\n * ```ts\n * import { ChronosApiError } from '@chronos.sh/sdk';\n *\n * function handleSdkError(err: unknown) {\n * if (err instanceof ChronosApiError) {\n * if (err.status === 401) return refreshAuth();\n * console.error('API error', { status: err.status, requestId: err.requestId });\n * }\n * }\n * ```\n */\nexport class ChronosApiError extends ChronosError {\n /** HTTP status code returned by the Chronos API. */\n readonly status: number;\n /** Application-level error code from the response envelope, when present. */\n readonly code?: string;\n /** Full parsed response payload (envelope + data, or whatever the server returned). */\n readonly body?: unknown;\n /** Value of the `X-Request-Id` response header, when present. */\n readonly requestId?: string;\n\n constructor(message: string, options: ChronosApiErrorOptions) {\n super(message);\n this.name = 'ChronosApiError';\n this.status = options.status;\n this.code = options.code;\n this.body = options.body;\n this.requestId = options.requestId;\n }\n}\n\n/**\n * Thrown when the underlying `fetch` rejects before the server replies —\n * DNS failure, TCP reset, connection refused, etc. The original error is\n * available on `.cause`.\n *\n * Abort signals propagate unwrapped — `instanceof ChronosNetworkError`\n * always means a real transport failure, not a graceful shutdown.\n *\n * @example\n * ```ts\n * import { ChronosNetworkError } from '@chronos.sh/sdk';\n *\n * function handleSdkError(err: unknown) {\n * if (err instanceof ChronosNetworkError) {\n * console.warn('Transport blip', { cause: err.cause });\n * }\n * }\n * ```\n */\nexport class ChronosNetworkError extends ChronosError {\n /** Original error/value rejected by the underlying `fetch`. */\n declare readonly cause: unknown;\n\n constructor(message: string, options: { cause: unknown }) {\n super(message, options);\n this.name = 'ChronosNetworkError';\n }\n}\n\n/**\n * Wraps an exception thrown by a user-supplied {@link ChronosHandler}. The\n * original error is on `.cause`; `.message` is copied from the original so\n * the SDK reports it to the API as the failure reason.\n *\n * @example\n * ```ts\n * import { ChronosHandlerError } from '@chronos.sh/sdk';\n *\n * if (err instanceof ChronosHandlerError) {\n * console.error('Handler threw', err.cause);\n * }\n * ```\n */\nexport class ChronosHandlerError extends ChronosError {\n /** Original error/value thrown by the user-supplied handler. */\n declare readonly cause: unknown;\n\n constructor(message: string, options: { cause: unknown }) {\n super(message, options);\n this.name = 'ChronosHandlerError';\n }\n}\n","import type { ChronosLogger } from '../types';\n\nexport const defaultLogger: ChronosLogger = {\n debug: (message, meta) => logToConsole(console.debug, message, meta),\n info: (message, meta) => logToConsole(console.info, message, meta),\n warn: (message, meta) => logToConsole(console.warn, message, meta),\n error: (message, meta) => logToConsole(console.error, message, meta),\n};\n\nfunction logToConsole(\n method: (message?: unknown, ...optionalParams: unknown[]) => void,\n message: string,\n meta?: Record<string, unknown>,\n): void {\n if (meta) {\n method(message, meta);\n } else {\n method(message);\n }\n}\n","import { ChronosConfigError, ChronosError } from '../errors';\n\nexport const MAX_HANDLER_NAME_LENGTH = 255;\nexport const MAX_POLL_WAIT_TIME_SECONDS = 20;\n\nexport function validateApiKey(apiKey: string | undefined): string {\n const trimmed = apiKey?.trim();\n if (!trimmed) {\n throw new ChronosConfigError('Chronos apiKey is required');\n }\n return trimmed;\n}\n\nexport function validatePollWaitTime(seconds: number): void {\n if (!Number.isInteger(seconds) || seconds < 0 || seconds > MAX_POLL_WAIT_TIME_SECONDS) {\n throw new ChronosConfigError(\n `pollWaitTimeSeconds must be an integer between 0 and ${MAX_POLL_WAIT_TIME_SECONDS}`,\n );\n }\n}\n\nexport function validateRetryDelayMs(ms: number): void {\n if (!Number.isFinite(ms) || ms < 0) {\n throw new ChronosConfigError('retryDelayMs must be a non-negative number');\n }\n}\n\nexport function normalizeHandlerName(name: string): string {\n const normalized = name.trim();\n\n if (!normalized) {\n throw new ChronosError('Handler name is required');\n }\n\n if (normalized.length > MAX_HANDLER_NAME_LENGTH) {\n throw new ChronosError(`Handler name must be ${MAX_HANDLER_NAME_LENGTH} characters or fewer`);\n }\n\n return normalized;\n}\n","import { ChronosApiError, ChronosConfigError, ChronosError, ChronosNetworkError } from './errors';\nimport { defaultLogger } from './internal/logger';\nimport { validateApiKey } from './internal/validate';\nimport type { ChronosLogger, ChronosOptions, FetchLike } from './types';\n\n/** Default Chronos API base URL. */\nexport const DEFAULT_BASE_URL = 'https://api.chronos.sh';\n\ntype ApiResponse<T> = {\n success: boolean;\n message?: string;\n code?: string;\n data: T;\n};\n\nexport class BaseClient {\n readonly apiKey: string;\n readonly baseUrl: string;\n readonly fetch: FetchLike;\n readonly logger: ChronosLogger;\n private readonly headers: Record<string, string>;\n\n constructor(options: ChronosOptions) {\n this.apiKey = validateApiKey(options.apiKey);\n this.baseUrl = normalizeBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL);\n this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);\n this.logger = options.logger ?? defaultLogger;\n this.headers = {\n 'content-type': 'application/json',\n authorization: `Bearer ${this.apiKey}`,\n };\n }\n\n async request<T>(path: string, body: Record<string, unknown>, signal?: AbortSignal): Promise<T> {\n let response: Response;\n try {\n response = await this.fetch(`${this.baseUrl}${path}`, {\n method: 'POST',\n headers: this.headers,\n body: JSON.stringify(body),\n signal,\n });\n } catch (err) {\n if (signal?.aborted) {\n throw err;\n }\n const message = err instanceof Error ? err.message : String(err);\n throw new ChronosNetworkError(`Chronos API request failed: ${message}`, { cause: err });\n }\n\n const payload = await parseJsonResponse<ApiResponse<T>>(response);\n const envelope = isEnvelope(payload) ? (payload as ApiResponse<T>) : undefined;\n\n if (!response.ok || (envelope && !envelope.success)) {\n throw new ChronosApiError(apiErrorMessage(response, envelope), {\n status: response.status,\n code: typeof envelope?.code === 'string' ? envelope.code : undefined,\n body: payload,\n requestId: response.headers.get('x-request-id') ?? undefined,\n });\n }\n\n if (!envelope) {\n // Plain ChronosError, not ChronosApiError — a malformed 200 body is\n // an infrastructure anomaly that should be retried, not a definitive\n // API rejection.\n throw new ChronosError('Chronos API returned an invalid response');\n }\n\n return envelope.data;\n }\n}\n\nfunction normalizeBaseUrl(baseUrl: string): string {\n const normalized = baseUrl.trim().replace(/\\/+$/, '');\n if (!normalized) {\n throw new ChronosConfigError('Chronos baseUrl is required');\n }\n return normalized;\n}\n\nasync function parseJsonResponse<T>(response: Response): Promise<T | null> {\n try {\n return (await response.json()) as T;\n } catch {\n return null;\n }\n}\n\nfunction apiErrorMessage(\n response: Response,\n envelope: Record<string, unknown> | undefined,\n): string {\n if (typeof envelope?.message === 'string' && envelope.message.trim()) {\n return envelope.message;\n }\n\n return response.ok\n ? 'Chronos API returned an invalid response'\n : `Chronos API request failed with status ${response.status}`;\n}\n\n// Malformed bodies (arrays, primitives, missing `success`) are routed to the\n// invalid-response path, not classified as API errors.\nfunction isEnvelope(value: unknown): value is Record<string, unknown> & { success: boolean } {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return false;\n }\n return typeof (value as Record<string, unknown>).success === 'boolean';\n}\n","import type { BaseClient } from './client';\nimport { ChronosApiError, ChronosError, ChronosHandlerError, ChronosNetworkError } from './errors';\nimport {\n normalizeHandlerName,\n validatePollWaitTime,\n validateRetryDelayMs,\n} from './internal/validate';\nimport type {\n ChronosContext,\n ChronosHandler,\n ChronosHandlerResult,\n ChronosOptions,\n ChronosSchedule,\n} from './types';\n\n/** Default worker long-poll wait time in seconds. Equal to the API maximum. */\nexport const DEFAULT_POLL_WAIT_TIME_SECONDS = 20;\nconst DEFAULT_RETRY_DELAY_MS = 1_000;\nconst RESULT_REPORT_MAX_ATTEMPTS = 3;\nconst MAX_REPORTED_ERROR_LENGTH = 4_096;\nconst DEFAULT_HANDLER_ERROR = 'Chronos handler failed';\n\ntype ResultStatus = 'completed' | 'failed';\n\ntype ClaimedJob = {\n job_id: string;\n execution_id: string;\n handler: string;\n scheduled_for: string;\n attempt: number;\n timeout: number;\n payload: unknown;\n schedule: ChronosSchedule | null;\n};\n\n/**\n * Long-poll worker. Claims jobs from the Chronos API, dispatches them to\n * registered handlers, and reports results.\n *\n * Construct via `new Chronos({ apiKey }).worker` rather than directly.\n */\nexport class Worker {\n private readonly client: BaseClient;\n private readonly pollWaitTimeSeconds: number;\n private readonly retryDelayMs: number;\n private readonly handlers = new Map<string, ChronosHandler<unknown>>();\n private handlerNames: string[] = [];\n private startPromise?: Promise<void>;\n private pollController?: AbortController;\n\n constructor(client: BaseClient, options: ChronosOptions) {\n this.client = client;\n this.pollWaitTimeSeconds = options.pollWaitTimeSeconds ?? DEFAULT_POLL_WAIT_TIME_SECONDS;\n this.retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;\n\n validatePollWaitTime(this.pollWaitTimeSeconds);\n validateRetryDelayMs(this.retryDelayMs);\n }\n\n /**\n * Register a handler for a named job type. Invoked when the Chronos API\n * claims a job whose `handler` field matches `name`. Names are trimmed and\n * must be 1–255 characters.\n *\n * @param name - Handler name. Must match the schedule's `handler` on the API side.\n * @param handler - Async function invoked with the job context. Return a\n * plain object to record a result, or `undefined` for none.\n * @returns The Worker, for chaining.\n * @throws {ChronosError} If the name is invalid, already registered, or `handler` is not a function.\n *\n * @example\n * ```ts\n * chronos.worker\n * .handle('send-email', async (ctx) => ({ sent: true }))\n * .handle('cleanup', async () => undefined);\n * ```\n */\n handle<TPayload = unknown>(name: string, handler: ChronosHandler<TPayload>): this {\n const normalizedName = normalizeHandlerName(name);\n\n if (this.handlers.has(normalizedName)) {\n throw new ChronosError(`Handler \"${normalizedName}\" is already registered`);\n }\n\n if (typeof handler !== 'function') {\n throw new ChronosError(`Handler \"${normalizedName}\" must be a function`);\n }\n\n this.handlers.set(normalizedName, handler as ChronosHandler<unknown>);\n this.handlerNames.push(normalizedName);\n return this;\n }\n\n /**\n * Begin long-polling for jobs. The returned promise resolves when\n * {@link Worker.stop} is called and any in-flight job completes.\n *\n * @throws {ChronosError} Synchronously, if no handlers are registered or the worker is already running.\n */\n start(): Promise<void> {\n if (this.startPromise) {\n throw new ChronosError('Chronos worker is already started');\n }\n\n if (this.handlers.size === 0) {\n throw new ChronosError('Register at least one handler before starting Chronos');\n }\n\n this.pollController = new AbortController();\n this.startPromise = this.runLoop().finally(() => {\n this.startPromise = undefined;\n this.pollController = undefined;\n });\n\n return this.startPromise;\n }\n\n /**\n * Request graceful shutdown. The poll loop is aborted immediately; any\n * in-flight handler and result-report are allowed to complete to preserve\n * at-least-once delivery. Returns the same promise as the active\n * {@link Worker.start}, or a resolved promise if the worker isn't running.\n */\n stop(): Promise<void> {\n this.pollController?.abort();\n return this.startPromise ?? Promise.resolve();\n }\n\n private get isStopped(): boolean {\n return this.pollController?.signal.aborted ?? true;\n }\n\n private async runLoop(): Promise<void> {\n while (!this.isStopped) {\n try {\n const job = await this.claimJob();\n if (job) {\n await this.processJob(job);\n }\n } catch (err) {\n if (this.isStopped) {\n break;\n }\n\n this.client.logger.error('Chronos poll loop error', { err: errorToLogValue(err) });\n await sleep(this.retryDelayMs, this.pollController?.signal);\n }\n }\n }\n\n private async claimJob(): Promise<ClaimedJob | null> {\n if (this.handlerNames.length === 0) {\n throw new ChronosError('Cannot claim jobs without registered handlers');\n }\n\n return this.client.request<ClaimedJob | null>(\n '/v1/worker/jobs/claim',\n {\n wait_time_seconds: this.pollWaitTimeSeconds,\n handlers: this.handlerNames,\n },\n this.pollController?.signal,\n );\n }\n\n private async processJob(job: ClaimedJob): Promise<void> {\n const handler = this.handlers.get(job.handler);\n if (!handler) {\n return this.handleUnregisteredJob(job);\n }\n\n let handlerResult: ChronosHandlerResult;\n try {\n handlerResult = await handler(createContext(job));\n } catch (err) {\n const message = errorMessage(err);\n const handlerErr = new ChronosHandlerError(message, { cause: err });\n this.client.logger.error('Chronos handler failed', {\n ...jobLogMeta(job),\n err: errorToLogValue(handlerErr),\n });\n await this.safeReportFailed(job, message);\n return;\n }\n\n let result: Record<string, unknown> | undefined;\n try {\n result = normalizeHandlerResult(handlerResult);\n } catch (err) {\n this.client.logger.error('Chronos handler returned invalid result', {\n ...jobLogMeta(job),\n err: errorToLogValue(err),\n });\n await this.safeReportFailed(job, errorMessage(err));\n return;\n }\n\n try {\n await this.reportCompleted(job.execution_id, result);\n } catch (err) {\n this.logResultReportFailure(err, job, 'completed');\n }\n }\n\n private async handleUnregisteredJob(job: ClaimedJob): Promise<void> {\n const message = `Chronos SDK received job for unregistered handler \"${job.handler}\"`;\n this.client.logger.error(message, jobLogMeta(job));\n await this.safeReportFailed(job, message);\n }\n\n private async safeReportFailed(job: ClaimedJob, message: string): Promise<void> {\n try {\n await this.reportFailed(job.execution_id, message);\n } catch (err) {\n this.logResultReportFailure(err, job, 'failed');\n }\n }\n\n private async reportCompleted(\n executionId: string,\n result: Record<string, unknown> | undefined,\n ): Promise<void> {\n await this.reportResultWithRetry(executionId, {\n status: 'completed',\n ...(result === undefined ? {} : { result }),\n });\n }\n\n private async reportFailed(executionId: string, error: string): Promise<void> {\n await this.reportResultWithRetry(executionId, {\n status: 'failed',\n error: truncate(error, MAX_REPORTED_ERROR_LENGTH),\n });\n }\n\n private async reportResultWithRetry(\n executionId: string,\n body: Record<string, unknown>,\n ): Promise<void> {\n let lastErr: unknown;\n\n for (let attempt = 1; attempt <= RESULT_REPORT_MAX_ATTEMPTS; attempt++) {\n try {\n await this.client.request(\n `/v1/worker/executions/${encodeURIComponent(executionId)}/result`,\n body,\n );\n return;\n } catch (err) {\n const apiErr = err instanceof ChronosApiError ? err : undefined;\n\n if (apiErr?.status === 409) {\n this.client.logger.warn('Chronos result discarded; execution already terminal', {\n executionId,\n status: body.status,\n code: apiErr.code,\n });\n return;\n }\n\n if (apiErr && isTerminalReportError(apiErr.status)) {\n throw apiErr;\n }\n\n lastErr = err;\n if (attempt === RESULT_REPORT_MAX_ATTEMPTS) {\n break;\n }\n\n this.client.logger.warn('Chronos result report failed; retrying', {\n err: errorToLogValue(err),\n executionId,\n status: body.status,\n attempt,\n maxAttempts: RESULT_REPORT_MAX_ATTEMPTS,\n });\n await sleep(this.retryDelayMs);\n }\n }\n\n throw lastErr;\n }\n\n private logResultReportFailure(err: unknown, job: ClaimedJob, status: ResultStatus): void {\n const isTerminal = err instanceof ChronosApiError && isTerminalReportError(err.status);\n this.client.logger.error(\n isTerminal\n ? 'Chronos result report rejected by API'\n : 'Chronos result report failed after retries',\n { ...jobLogMeta(job), err: errorToLogValue(err), status },\n );\n }\n}\n\nfunction createContext(job: ClaimedJob): ChronosContext<unknown> {\n return {\n jobId: job.job_id,\n executionId: job.execution_id,\n handler: job.handler,\n payload: job.payload,\n scheduledFor: new Date(job.scheduled_for),\n attempt: job.attempt,\n timeout: job.timeout,\n schedule: job.schedule,\n };\n}\n\nfunction jobLogMeta(job: ClaimedJob): Record<string, unknown> {\n return {\n jobId: job.job_id,\n executionId: job.execution_id,\n handler: job.handler,\n };\n}\n\nfunction normalizeHandlerResult(result: ChronosHandlerResult): Record<string, unknown> | undefined {\n if (result === undefined) {\n return undefined;\n }\n\n if (!isPlainObject(result)) {\n throw new ChronosError('Chronos handler result must be a plain object or undefined');\n }\n\n try {\n JSON.stringify(result);\n } catch (err) {\n const reason = err instanceof Error ? err.message : 'value is not JSON-encodable';\n throw new ChronosError(`Chronos handler result is not JSON-encodable: ${reason}`);\n }\n\n return result;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return false;\n }\n\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\nfunction errorMessage(err: unknown): string {\n const message = err instanceof Error ? err.message : String(err);\n return message.trim() || DEFAULT_HANDLER_ERROR;\n}\n\nfunction errorToLogValue(err: unknown): unknown {\n if (!(err instanceof Error)) {\n return err;\n }\n\n const base = { name: err.name, message: err.message, stack: err.stack };\n\n if (err instanceof ChronosApiError) {\n return { ...base, status: err.status, code: err.code, requestId: err.requestId };\n }\n if (err instanceof ChronosNetworkError || err instanceof ChronosHandlerError) {\n return { ...base, cause: errorToLogValue(err.cause) };\n }\n return base;\n}\n\nfunction truncate(value: string, maxLength: number): string {\n return value.length <= maxLength ? value : value.slice(0, maxLength);\n}\n\n// 409 is handled by the caller before reaching here; 408/429 are transient.\nconst NON_TERMINAL_4XX = new Set([408, 409, 429]);\n\n// 200 reaches here only via an explicit `{ success: false }` envelope from BaseClient.\nfunction isTerminalReportError(status: number): boolean {\n if (status === 200) {\n return true;\n }\n if (NON_TERMINAL_4XX.has(status)) {\n return false;\n }\n return status >= 400 && status < 500;\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.resolve();\n }\n\n return new Promise((resolve) => {\n const timeout: ReturnType<typeof setTimeout> = setTimeout(done, ms);\n\n function done() {\n clearTimeout(timeout);\n signal?.removeEventListener('abort', done);\n resolve();\n }\n\n signal?.addEventListener('abort', done, { once: true });\n });\n}\n","import { BaseClient, DEFAULT_BASE_URL } from './client';\nimport type { ChronosOptions } from './types';\nimport { DEFAULT_POLL_WAIT_TIME_SECONDS, Worker } from './worker';\n\n/**\n * The Chronos SDK client. Composes worker and (future) REST resource\n * subclients from a single instance.\n *\n * @example\n * ```ts\n * import { Chronos } from '@chronos.sh/sdk';\n *\n * const chronos = new Chronos({ apiKey: process.env.CHRONOS_API_KEY! });\n *\n * chronos.worker.handle<{ to: string }>('send-email', async (ctx) => {\n * await sendEmail(ctx.payload.to);\n * return { sent: true };\n * });\n *\n * await chronos.worker.start();\n * ```\n */\nexport class Chronos {\n /** Long-poll worker for executing pull-mode jobs. */\n readonly worker: Worker;\n\n /**\n * @param options - Client configuration. Only `apiKey` is required.\n * @throws {ChronosConfigError} If `apiKey` is missing or any option fails validation.\n */\n constructor(options: ChronosOptions) {\n const client = new BaseClient(options);\n this.worker = new Worker(client, options);\n }\n}\n\nexport {\n ChronosApiError,\n type ChronosApiErrorOptions,\n ChronosConfigError,\n ChronosError,\n ChronosHandlerError,\n ChronosNetworkError,\n} from './errors';\nexport type {\n ChronosContext,\n ChronosHandler,\n ChronosHandlerResult,\n ChronosLogger,\n ChronosOptions,\n ChronosSchedule,\n FetchLike,\n} from './types';\nexport { DEFAULT_BASE_URL, DEFAULT_POLL_WAIT_TIME_SECONDS };\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chronos.sh/sdk",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Chronos JavaScript/TypeScript SDK",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"chronos",
|
|
8
|
+
"job-scheduling",
|
|
9
|
+
"cron",
|
|
10
|
+
"background-jobs",
|
|
11
|
+
"worker",
|
|
12
|
+
"task-queue"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/halffive/chronos-node.git"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/halffive/chronos-node",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/halffive/chronos-node/issues"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./dist/index.cjs",
|
|
24
|
+
"module": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"import": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"require": {
|
|
33
|
+
"types": "./dist/index.d.cts",
|
|
34
|
+
"default": "./dist/index.cjs"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"sideEffects": false,
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist"
|
|
44
|
+
],
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsdown",
|
|
47
|
+
"check": "biome check . && pnpm typecheck && pnpm build && pnpm test && pnpm lint:pkg",
|
|
48
|
+
"lint": "biome check .",
|
|
49
|
+
"lint:fix": "biome check . --write",
|
|
50
|
+
"lint:pkg": "publint && attw --pack .",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"test:watch": "vitest",
|
|
53
|
+
"typecheck": "tsc --noEmit"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=20.0.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@arethetypeswrong/cli": "^0.18.2",
|
|
60
|
+
"@biomejs/biome": "^2.3.14",
|
|
61
|
+
"@types/node": "^24.3.0",
|
|
62
|
+
"publint": "^0.3.18",
|
|
63
|
+
"tsdown": "^0.21.10",
|
|
64
|
+
"typescript": "^5.9.3",
|
|
65
|
+
"vitest": "^4.1.5"
|
|
66
|
+
}
|
|
67
|
+
}
|