Skip to content

Node.js SDK

Terminal window
pnpm add @chronos.sh/sdk
  • Requires Node.js 20+
  • Dual module: ESM (import) and CJS (require) both supported
  • Zero runtime dependencies

The top-level client. The only class you construct directly.

import { Chronos } from '@chronos.sh/sdk';
const chronos = new Chronos({
apiKey: process.env.CHRONOS_API_KEY!,
});
new Chronos(options: ChronosOptions)

Throws ChronosConfigError if any option fails validation.

PropertyTypeDescription
workerWorkerRead-only. The long-poll worker instance. All functionality lives here.

None. Chronos is a composition root. Use chronos.worker for all operations.

Accessed via chronos.worker. Not directly importable.

handle<TPayload = unknown>(name: string, handler: ChronosHandler<TPayload>): this

Register a handler for a named job type. Returns this for chaining.

ParameterTypeDescription
namestringJob type name. Trimmed internally. Must be 1–255 characters.
handlerChronosHandler<TPayload>Async or sync function that processes the job.

Throws ChronosError:

  • 'Handler name is required': empty or whitespace-only name
  • 'Handler name must be 255 characters or fewer'
  • 'Handler "{name}" is already registered': duplicate name
  • 'Handler "{name}" must be a function': non-function passed
start(): Promise<void>

Begin long-polling for jobs. The returned promise resolves when stop() is called and all in-flight work completes.

Throws ChronosError (synchronously):

  • 'Chronos worker is already started'
  • 'Register at least one handler before starting Chronos'
stop(): Promise<void>

Request graceful shutdown.

  1. Aborts the current long-poll immediately
  2. Waits for any in-flight handler to finish and report its result
  3. Resolves when complete

If not running, returns a resolved promise. Never throws.

type ChronosOptions = {
apiKey: string;
baseUrl?: string;
fetch?: FetchLike;
logger?: ChronosLogger;
pollWaitTimeSeconds?: number;
retryDelayMs?: number;
};
OptionTypeDefaultValidation
apiKeystring— (required)Trimmed. Throws ChronosConfigError if empty.
baseUrlstring'https://api.chronos.sh'Trimmed, trailing slashes stripped. Throws if empty after trim.
fetchFetchLikeglobalThis.fetchNone.
loggerChronosLoggerConsole-backed loggerNone.
pollWaitTimeSecondsnumber20Integer, 0–20 inclusive. Throws ChronosConfigError.
retryDelayMsnumber1000Finite, non-negative. Throws ChronosConfigError.
type ChronosContext<TPayload = unknown> = {
jobId: string;
executionId: string;
handler: string;
payload: TPayload;
scheduledFor: Date;
attempt: number;
timeout: number;
schedule: ChronosSchedule | null;
};
FieldTypeDescription
jobIdstringStable job identifier. Same across retries. Use for idempotency keys in downstream calls.
executionIdstringUnique to this execution attempt. Use for per-attempt logs, metrics, and correlation.
handlerstringHandler name that matched this job.
payloadTPayloadJob payload. Typed via the generic on .handle<TPayload>().
scheduledForDateWhen the job was originally scheduled. Parsed from ISO string.
attemptnumberAttempt number. 1 on first try, increments on retries.
timeoutnumberSoft timeout in seconds. Informational only. The SDK does not enforce it.
scheduleChronosSchedule | nullParent schedule, or null for one-off jobs.
type ChronosSchedule = {
id: string;
name: string;
};
type ChronosHandler<TPayload = unknown> = (
ctx: ChronosContext<TPayload>,
) => ChronosHandlerResult | Promise<ChronosHandlerResult>;

Can be sync or async.

type ChronosHandlerResult = Record<string, unknown> | void;
Return valueEffect
Plain objectRecorded as execution result. Must be JSON-serializable.
undefined / voidExecution marked completed, no result data.

Rejected at runtime (throws ChronosError):

  • Arrays
  • Class instances (non-plain-object prototypes)
  • Non-JSON-serializable values
type ChronosLogger = {
debug(message: string, meta?: Record<string, unknown>): void;
info(message: string, meta?: Record<string, unknown>): void;
warn(message: string, meta?: Record<string, unknown>): void;
error(message: string, meta?: Record<string, unknown>): void;
};

Signature is (message, meta?): message first, metadata second.

Adapting pino (which uses obj, message order):

import pino from 'pino';
const log = pino();
const chronos = new Chronos({
apiKey: process.env.CHRONOS_API_KEY!,
logger: {
debug: (msg, meta) => log.debug(meta ?? {}, msg),
info: (msg, meta) => log.info(meta ?? {}, msg),
warn: (msg, meta) => log.warn(meta ?? {}, msg),
error: (msg, meta) => log.error(meta ?? {}, msg),
},
});
type FetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>;

Compatible with globalThis.fetch. Use cases: inject tracing headers, custom timeouts, test mocking.

Error
└── ChronosError
├── ChronosConfigError
├── ChronosApiError
├── ChronosNetworkError
└── ChronosHandlerError
class ChronosError extends Error {
readonly cause?: unknown;
constructor(message: string, options?: { cause?: unknown });
}

Base class for all SDK errors. Catch this to handle any SDK error generically.

Thrown for operational issues: duplicate handler names, invalid return values, calling start() twice.

class ChronosConfigError extends ChronosError {
constructor(message: string);
}

Invalid options passed to new Chronos(). Only thrown during construction. If the constructor succeeds, config is valid.

Messages:

  • 'Chronos apiKey is required'
  • 'Chronos baseUrl is required'
  • 'pollWaitTimeSeconds must be an integer between 0 and 20'
  • 'retryDelayMs must be a non-negative number'
class ChronosApiError extends ChronosError {
readonly status: number;
readonly code?: string;
readonly body?: unknown;
readonly requestId?: string;
constructor(message: string, options: ChronosApiErrorOptions);
}
PropertyTypeDescription
statusnumberHTTP status code. Can be 200 if the API returned { success: false }.
codestring | undefinedApplication error code (e.g., 'rate_limit_exceeded').
bodyunknownFull parsed response body.
requestIdstring | undefinedValue of the X-Request-Id response header.

Thrown when the API responds with a non-2xx status or a 2xx response with { success: false }.

try {
await chronos.worker.start();
} catch (err) {
if (err instanceof ChronosApiError) {
console.error(`API error ${err.status}: ${err.code}`);
}
}
class ChronosNetworkError extends ChronosError {
readonly cause: unknown;
constructor(message: string, options: { cause: unknown });
}

Thrown when the HTTP request fails before reaching the server: DNS resolution failure, TCP connection refused, TLS errors.

cause contains the original error from fetch.

Not thrown for abort signals. If stop() aborts a poll, the raw abort error propagates (not wrapped in ChronosNetworkError).

class ChronosHandlerError extends ChronosError {
readonly cause: unknown;
constructor(message: string, options: { cause: unknown });
}

Wraps any exception thrown inside your handler. The message is copied from the original error (or stringified). If empty, defaults to 'Chronos handler failed'.

This error is never rethrown to your code. It’s logged internally and the failure is reported to the API. The error message (truncated to 4KB) is sent as the execution failure reason.

Every named export from @chronos.sh/sdk:

ExportKindDescription
ChronosClassTop-level client. The only class you construct.
ChronosErrorClassBase error class for all SDK errors.
ChronosConfigErrorClassInvalid constructor options.
ChronosApiErrorClassAPI responded with an error.
ChronosApiErrorOptionsTypeOptions for constructing ChronosApiError.
ChronosNetworkErrorClassNetwork/transport failure.
ChronosHandlerErrorClassHandler threw an exception.
ChronosContextTypeContext object passed to handlers.
ChronosHandlerTypeHandler function signature.
ChronosHandlerResultTypeValid handler return types.
ChronosLoggerTypeLogger interface.
ChronosOptionsTypeConstructor options.
ChronosScheduleTypeSchedule reference on context.
FetchLikeTypeCustom fetch signature.
DEFAULT_BASE_URLConstant'https://api.chronos.sh'
DEFAULT_POLL_WAIT_TIME_SECONDSConstant20