Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
c6c56d5
feat(node): resource DI and enqueue interception
pratyush618 Jun 21, 2026
b912431
docs(node): add resources guide for DI + interception
pratyush618 Jun 21, 2026
1301275
docs(node): correct middleware hook table
pratyush618 Jun 21, 2026
70441c6
docs(node): add observability section (monitoring + logging)
pratyush618 Jun 21, 2026
cf768a1
docs(node): move stats/metrics into observability
pratyush618 Jun 21, 2026
a33883b
docs(node): remove fiction — enqueueMany, fix rateLimit type
pratyush618 Jun 21, 2026
c38be1e
docs(node): fix rateLimit examples to string form
pratyush618 Jun 21, 2026
82e9699
docs(node): fix Serializer methods to serialize/deserialize
pratyush618 Jun 21, 2026
66955fb
docs(node): fix on_failure condition wording
pratyush618 Jun 21, 2026
7cda0c8
docs(node): complete api-reference option tables
pratyush618 Jun 21, 2026
17cac4d
docs(node): add error-handling + delivery guarantees guides
pratyush618 Jun 21, 2026
c4c6095
docs(node): add dashboard REST API + testing guides
pratyush618 Jun 21, 2026
b3223f4
docs(node): add capabilities overview page
pratyush618 Jun 21, 2026
ebd4e0d
docs(node): add execution-model guide
pratyush618 Jun 21, 2026
92724c1
docs(node): add security, troubleshooting, migration guides
pratyush618 Jun 21, 2026
7835464
feat(node): batch enqueue and notes at enqueue
pratyush618 Jun 21, 2026
3e71b81
feat(node): HMAC-signed serializer
pratyush618 Jun 21, 2026
85c23a9
docs(node): document batch enqueue + notes option
pratyush618 Jun 21, 2026
18d58e1
docs(node): document SignedSerializer
pratyush618 Jun 21, 2026
c100f3e
feat(node): specific error classes over bare TaskitoError
pratyush618 Jun 21, 2026
0db0a63
docs(node): add error hierarchy reference
pratyush618 Jun 21, 2026
e78bba7
feat(node): AES-256-GCM EncryptedSerializer
pratyush618 Jun 21, 2026
6c7065d
docs(node): document EncryptedSerializer
pratyush618 Jun 21, 2026
7e19c8b
feat(node): workflow introspection (WorkflowAnalysis)
pratyush618 Jun 21, 2026
00140da
docs(node): document workflow analysis
pratyush618 Jun 21, 2026
afe0124
feat(node): resource metrics + mockResource test helper
pratyush618 Jun 21, 2026
edbf00b
docs(node): document resource metrics + testing
pratyush618 Jun 21, 2026
822da45
feat(node): predicate gates for enqueue
pratyush618 Jun 21, 2026
94826ce
docs(node): document predicate gates
pratyush618 Jun 21, 2026
c5fb8fc
feat(node): dashboard bearer-token auth
pratyush618 Jun 21, 2026
ef35020
docs(node): document dashboard token auth
pratyush618 Jun 21, 2026
286dd7f
feat(node): workflow canvas (chain/group/chord)
pratyush618 Jun 21, 2026
5b6a77b
docs(node): document workflow canvas
pratyush618 Jun 21, 2026
0538e9f
feat(node): workflow step result caching
pratyush618 Jun 21, 2026
ff473b8
docs(node): document workflow caching
pratyush618 Jun 21, 2026
9fab724
feat(node): streaming partial results
pratyush618 Jun 21, 2026
481aa62
docs(node): document streaming partial results
pratyush618 Jun 21, 2026
db253f0
feat(node): KEDA scaler server + CLI
pratyush618 Jun 21, 2026
706000d
docs(node): document KEDA scaler
pratyush618 Jun 21, 2026
10fd0d2
fix(node): propagate validation errors from enqueue_many
pratyush618 Jun 21, 2026
73b0cb1
fix(node): validate scaler depth, stop leaking errors to clients
pratyush618 Jun 21, 2026
3a962d7
fix(node): refcount worker resources, cache promise before factory
pratyush618 Jun 21, 2026
9e3574d
fix(node): degrade to cache miss on malformed cache entries
pratyush618 Jun 21, 2026
243911f
fix(node): chord respects after on empty group, typed fan-in error
pratyush618 Jun 21, 2026
e50c576
fix(node): exclude skipped nodes from workflow pending count
pratyush618 Jun 21, 2026
a170e8b
fix(node): wrap non-serializable notes in NotesValidationError
pratyush618 Jun 21, 2026
d9ce428
test(node): assert enqueueMany id order maps to input rows
pratyush618 Jun 21, 2026
6ed0791
docs(node): fix queue/worker mismatch in migration example
pratyush618 Jun 21, 2026
8e93d94
docs(node): clarify analyze and wait are separate calls
pratyush618 Jun 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions crates/taskito-node/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Plain option objects passed from JavaScript. napi maps snake_case Rust
//! fields to camelCase JS keys (`maxRetries`, `timeoutMs`).

use napi::bindgen_prelude::Buffer;
use napi_derive::napi;

/// How to open a queue's storage backend.
Expand Down Expand Up @@ -37,10 +38,20 @@ pub struct EnqueueOptions {
pub unique_key: Option<String>,
/// Free-form metadata string stored with the job.
pub metadata: Option<String>,
/// Structured notes — a pre-encoded canonical JSON object (the TS shell
/// validates the bounds and stringifies before passing it here).
pub notes: Option<String>,
/// Namespace override for this job.
pub namespace: Option<String>,
}

/// One entry in a batch enqueue: an opaque payload plus its own options.
#[napi(object)]
pub struct EnqueueJob {
pub payload: Buffer,
pub options: Option<EnqueueOptions>,
}

/// Filter for [`crate::queue::JsQueue::list_jobs`]. All fields optional.
#[napi(object)]
#[derive(Default)]
Expand Down
2 changes: 1 addition & 1 deletion crates/taskito-node/src/convert/job.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub fn build_new_job(
timeout_ms,
unique_key: opts.unique_key,
metadata: opts.metadata,
notes: None,
notes: opts.notes,
depends_on: Vec::new(),
expires_at: None,
result_ttl_ms: None,
Expand Down
29 changes: 29 additions & 0 deletions crates/taskito-node/src/convert/log.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//! Marshalling for task logs / published partial results.

use napi_derive::napi;
use taskito_core::storage::models::TaskLogRow;

/// JS-facing view of a task log entry. A published partial result is a log with
/// `level === "result"` and the value in `extra`.
#[napi(object)]
pub struct JsTaskLog {
pub id: String,
pub job_id: String,
pub task_name: String,
pub level: String,
pub message: String,
pub extra: Option<String>,
pub logged_at: i64,
}

pub fn log_to_js(row: TaskLogRow) -> JsTaskLog {
JsTaskLog {
id: row.id,
job_id: row.job_id,
task_name: row.task_name,
level: row.level,
message: row.message,
extra: row.extra,
logged_at: row.logged_at,
}
}
2 changes: 2 additions & 0 deletions crates/taskito-node/src/convert/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

mod job;
mod lock;
mod log;
mod outcome;
mod stats;
mod task_config;
Expand All @@ -11,6 +12,7 @@ mod workflow;

pub use job::{build_new_job, job_to_js, JsJob, JsTaskInvocation};
pub use lock::{lock_info_to_js, JsLockInfo};
pub use log::{log_to_js, JsTaskLog};
pub use outcome::{outcome_to_js, JsOutcome};
pub use stats::{
dead_job_to_js, job_error_to_js, metric_to_js, stats_to_js, status_code, worker_to_js,
Expand Down
36 changes: 36 additions & 0 deletions crates/taskito-node/src/queue/logs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//! Task-log methods on `JsQueue`: the storage behind published partial results
//! (`currentJob().publish()`) and the stream consumer (`queue.stream()`).

use napi::bindgen_prelude::Result;
use napi_derive::napi;
use taskito_core::Storage;

use super::JsQueue;
use crate::convert::{log_to_js, JsTaskLog};
use crate::error::to_napi_err;

#[napi]
impl JsQueue {
/// Append a task-log entry for a job. A published partial result uses
/// `level = "result"` with the value JSON-encoded in `extra`.
#[napi]
pub fn write_task_log(
&self,
job_id: String,
task_name: String,
level: String,
message: String,
extra: Option<String>,
) -> Result<()> {
self.storage
.write_task_log(&job_id, &task_name, &level, &message, extra.as_deref())
.map_err(to_napi_err)
}

/// All task-log entries for a job, oldest first.
#[napi]
pub fn get_task_logs(&self, job_id: String) -> Result<Vec<JsTaskLog>> {
let rows = self.storage.get_task_logs(&job_id).map_err(to_napi_err)?;
Ok(rows.into_iter().map(log_to_js).collect())
}
}
21 changes: 20 additions & 1 deletion crates/taskito-node/src/queue/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ use napi::threadsafe_function::{ErrorStrategy, ThreadsafeFunction};
use napi_derive::napi;
use taskito_core::{Storage, StorageBackend};

use crate::config::{EnqueueOptions, OpenOptions, WorkerOptions};
use crate::config::{EnqueueJob, EnqueueOptions, OpenOptions, WorkerOptions};
use crate::convert::{build_new_job, job_to_js, JsJob, JsOutcome, JsTaskInvocation};
use crate::error::to_napi_err;
use crate::worker::{start_worker, JsWorker};

mod admin;
mod inspect;
mod locks;
mod logs;
mod periodic;
#[cfg(feature = "workflows")]
mod workflows;
Expand Down Expand Up @@ -65,6 +66,24 @@ impl JsQueue {
Ok(job.id)
}

/// Enqueue a batch of jobs for one `task_name` in a single storage call.
/// Each entry carries its own payload and options. Returns the new job ids
/// in input order. Unlike `enqueue`, the batch path does not apply
/// `uniqueKey` dedup — it is a plain bulk insert for throughput.
#[napi]
pub fn enqueue_many(&self, task_name: String, jobs: Vec<EnqueueJob>) -> Result<Vec<String>> {
let namespace = self.namespace.as_deref();
let new_jobs = jobs
.into_iter()
.map(|job| {
let opts = job.options.unwrap_or_default();
build_new_job(task_name.clone(), job.payload.to_vec(), opts, namespace)
})
.collect::<Result<Vec<_>>>()?;
let created = self.storage.enqueue_batch(new_jobs).map_err(to_napi_err)?;
Ok(created.into_iter().map(|job| job.id).collect())
}

/// Fetch a job by id, or `null` if no such job exists.
#[napi]
pub fn get_job(&self, id: String) -> Result<Option<JsJob>> {
Expand Down
5 changes: 5 additions & 0 deletions crates/taskito-workflows/src/definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ pub struct StepMetadata {
/// (in reverse-dependency order) by running this task with the node's result.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compensate: Option<String>,
/// JSON `{ttlMs}` marking a cacheable node — its result is reused across runs
/// when its task, args, and upstream results are unchanged. Opaque to the
/// core; the shell's tracker reads it.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache: Option<String>,
}

/// A persisted workflow definition: the DAG structure plus per-step metadata.
Expand Down
13 changes: 13 additions & 0 deletions docs/content/docs/node/api-reference/context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Returns the running task's context, or `undefined` outside a task. Backed by
| `jobId` | `string` | The running job's id. |
| `signal` | `AbortSignal` | Aborted on cancel or timeout — pass to `fetch`/streams. |
| `setProgress(n)` | `(0–100) => void` | Report progress for inspection / dashboard. |
| `publish(value)` | `(unknown) => void` | Emit a partial result for [`queue.stream`](/node/guides/core/streaming) (JSON-serializable). |

```ts
queue.task("download", async (url: string) => {
Expand All @@ -31,3 +32,15 @@ queue.task("download", async (url: string) => {

See [Cancellation](/node/guides/core/cancellation) for the cooperative
cancellation flow.

## `useResource`

```ts
import { useResource } from "taskito";

const db = await useResource<Pool>("db"); // inside a running task
```

Resolves a [registered resource](/node/guides/resources/dependency-injection) by
name against the current job's scope. Also `AsyncLocalStorage`-backed, so it
works across `await` boundaries. Throws if called outside a task.
45 changes: 45 additions & 0 deletions docs/content/docs/node/api-reference/errors.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
title: Errors
description: "The Taskito error hierarchy — catch by specific type or by the TaskitoError base."
---

Every error the SDK throws extends `TaskitoError`, so a single `catch` can scope
all of them; the specific subclasses let you branch on what went wrong.

```ts
import { TaskitoError, ResourceNotFoundError } from "taskito";

try {
await runJob();
} catch (err) {
if (err instanceof ResourceNotFoundError) reRegister(err.resourceName);
else if (err instanceof TaskitoError) report(err);
else throw err;
}
```

## Hierarchy

| Class | Extends | Thrown when |
|---|---|---|
| `TaskitoError` | `Error` | Base — never thrown directly. |
| `TaskNotRegisteredError` | `TaskitoError` | A worker dequeues a job whose task name isn't registered. |
| `JobFailedError` | `TaskitoError` | `result()` awaits a job that failed or dead-lettered. |
| `JobCancelledError` | `TaskitoError` | `result()` awaits a cancelled job. |
| `ResultTimeoutError` | `TaskitoError` | `result()` times out before the job settles. |
| `QueueError` | `TaskitoError` | Queue construction / operational error (e.g. no `dbPath`/`dsn`). |
| `LockNotAcquiredError` | `TaskitoError` | `withLock` can't acquire a held lock. |
| `SerializationError` | `TaskitoError` | (De)serialization or payload-integrity failure (e.g. a bad `SignedSerializer` signature). |
| `NotesValidationError` | `TaskitoError` | A `notes` object breaks the contract (>15 fields or >4 KiB). |
| `WorkflowError` | `TaskitoError` | Workflow definition, submission, or query error. |
| `PredicateRejectedError` | `TaskitoError` | An enqueue-time [gate](/node/guides/core/predicates) rejected the submission. |
| `ResourceError` | `TaskitoError` | Base for resource dependency-injection errors. |
| `ResourceNotFoundError` | `ResourceError` | Resolving a resource name that was never registered. |
| `ResourceScopeError` | `ResourceError` | Resolving a task-scoped resource at worker scope. |

Selected errors carry context: `TaskNotRegisteredError.taskName`,
`JobFailedError.jobId`, `ResultTimeoutError.timeoutMs`,
`LockNotAcquiredError.lockName`, `ResourceNotFoundError.resourceName`.

See [error handling](/node/guides/reliability/error-handling) for retry / timeout
/ dead-letter behavior around a failing task.
1 change: 1 addition & 0 deletions docs/content/docs/node/api-reference/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ The public API exported from the `taskito` barrel. Contrib helpers live under
| [Context](/node/api-reference/context) | `currentJob()` — signal, progress, ids. |
| [Serializers](/node/api-reference/serializers) | `JsonSerializer`, `MsgpackSerializer`, the `Serializer` interface. |
| [Workflows](/node/api-reference/workflows) | The workflow builder + run handle. |
| [Errors](/node/api-reference/errors) | The `TaskitoError` hierarchy. |
| [CLI](/node/api-reference/cli) | The `taskito` command. |
2 changes: 1 addition & 1 deletion docs/content/docs/node/api-reference/meta.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{ "title": "API Reference", "root": true, "pages": ["index", "queue", "task", "worker", "result", "context", "serializers", "workflows", "cli"] }
{ "title": "API Reference", "root": true, "pages": ["index", "queue", "task", "worker", "result", "context", "serializers", "workflows", "errors", "cli"] }
9 changes: 7 additions & 2 deletions docs/content/docs/node/api-reference/queue.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ const queue = new Queue(options);
| `dbPath` | `string` | SQLite file path (default backend). |
| `backend` | `"sqlite" \| "postgres" \| "redis"` | Storage backend. |
| `dsn` | `string` | Connection string for postgres/redis. |
| `poolSize` | `number` | Connection pool size (sqlite/postgres). |
| `schema` | `string` | Postgres schema (default `taskito`). |
| `prefix` | `string` | Redis key prefix. |
| `namespace` | `string` | Namespace applied to enqueued jobs + the worker scheduler. |
| `serializer` | `Serializer` | Arg/result serializer (default `JsonSerializer`). |

## Tasks & enqueue
Expand All @@ -27,13 +29,15 @@ const queue = new Queue(options);
| `task(name, fn, config?)` | Register a task. See [Task](/node/api-reference/task). |
| `configureQueue(name, config)` | Set per-queue rate limit / concurrency. |
| `enqueue(task, args?, options?)` | Enqueue a job → job id. |
| `enqueueMany(task, jobs[])` | Enqueue a batch. |
| `enqueueMany(task, jobs[])` | Batch-enqueue `{ args?, options? }[]` → job ids (no dedup). |

## Results & cancellation

| Method | Description |
|---|---|
| `result(id, { timeoutMs? })` | Await a job's result. |
| `stream(id, { timeoutMs?, pollMs? })` | Async-iterate a job's [published partials](/node/guides/core/streaming). |
| `taskLogs(id)` | Raw task-log entries for a job. |
| `requestCancel(id)` | Abort a running job's signal. |
| `cancelJob(id)` | Cancel a pending job. |

Expand All @@ -56,7 +60,8 @@ const queue = new Queue(options);
|---|---|
| `runWorker(options?)` | Start a [worker](/node/api-reference/worker). |
| `on(event, handler)` | [Events](/node/guides/extensibility/events). |
| `use(middleware)` | [Middleware](/node/guides/extensibility/middleware). |
| `use(middleware)` | [Middleware](/node/guides/extensibility/middleware) (incl. the `onEnqueue` [interception](/node/guides/resources/interception) hook). |
| `resource(name, factory, opts?)` | Register an injectable [resource](/node/guides/resources/dependency-injection) (`scope`, `dispose`). |
| `webhooks` | [Webhooks](/node/guides/extensibility/webhooks) manager. |
| `workflows` | [Workflow](/node/api-reference/workflows) builder + queries. |
| `lock(name, opts)` / `withLock(name, fn)` | [Distributed locks](/node/guides/reliability/locks). |
Expand Down
13 changes: 9 additions & 4 deletions docs/content/docs/node/api-reference/result.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ description: "Awaiting job results."
---

```ts
const value = await queue.result(id: string, options?: { timeoutMs?: number }): Promise<R>;
const value = await queue.result(
id: string,
options?: { timeoutMs?: number; pollMs?: number },
): Promise<R>;
```

Resolves with the task's return value once the job finishes. Rejects if the job
dead-letters or the optional `timeoutMs` elapses before completion.
dead-letters or the optional `timeoutMs` (default 30000) elapses before
completion. `pollMs` (default 50) sets the store poll interval.

```ts
const id = queue.enqueue("add", [2, 3]);
Expand All @@ -21,6 +25,7 @@ The result is deserialized with the queue's
can await a result by id — the waiter polls the store.

<Callout type="info">
`result` is for awaiting one job. To scan many jobs or read outcomes in bulk,
use [`listJobs` / `getMetrics`](/node/guides/operations/inspection).
`result` is for awaiting one job. To scan many jobs use
[`listJobs`](/node/guides/operations/inspection); for aggregate outcomes use
[`getMetrics`](/node/guides/observability/monitoring).
</Callout>
19 changes: 17 additions & 2 deletions docs/content/docs/node/api-reference/serializers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,32 @@ import type { Serializer } from "taskito";
|---|---|
| `JsonSerializer` | Default. Human-readable JSON bytes. |
| `MsgpackSerializer` | Compact binary (msgpack). |
| `SignedSerializer` | Wraps another codec with an HMAC-SHA256 tag; rejects tampered payloads. |
| `EncryptedSerializer` | Wraps another codec with AES-256-GCM; encrypts + authenticates. |

```ts
new Queue({ dbPath: "taskito.db", serializer: new MsgpackSerializer() });
```

`SignedSerializer(secret, inner?)` prefixes each payload with a 32-byte HMAC tag
and verifies it (timing-safe) on read — authentication, not encryption.
`EncryptedSerializer(secret, inner?)` goes further: AES-256-GCM gives
confidentiality **and** integrity. Both derive their key from `secret`, so
producers and workers must share it.

```ts
new Queue({
dbPath: "taskito.db",
serializer: new EncryptedSerializer(process.env.TASKITO_SECRET!),
});
```

## `Serializer` interface

```ts
interface Serializer {
dumps(value: unknown): Uint8Array | Buffer;
loads(bytes: Uint8Array): unknown;
serialize(value: unknown): Uint8Array;
deserialize(bytes: Uint8Array): unknown;
}
```

Expand Down
3 changes: 2 additions & 1 deletion docs/content/docs/node/api-reference/task.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ result. Re-registering a name replaces the function and config.
| `retryBackoff` | `{ baseMs, maxMs }` | Exponential backoff bounds. |
| `timeoutMs` | `number` | Per-attempt timeout. |
| `maxConcurrent` | `number` | Max simultaneous running jobs of this task. |
| `rateLimit` | `{ limit, windowMs }` | Token-bucket rate limit. |
| `rateLimit` | `` `"100/m"` `` | Rate limit as `count/unit`, unit `s` \| `m` \| `h`. |
| `circuitBreaker` | `{ threshold, windowMs, cooldownMs }` | Trip after repeated failures. |
| `inject` | `string[]` | Resource names injected as a trailing `deps` argument. See [Dependency injection](/node/guides/resources/dependency-injection). |

```ts
queue.task("add", (a: number, b: number) => a + b, {
Expand Down
10 changes: 6 additions & 4 deletions docs/content/docs/node/api-reference/worker.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@ description: "runWorker options and the worker handle."
---

```ts
const worker = queue.runWorker(options?: WorkerOptions): Worker;
const worker = queue.runWorker(options?: WorkerRunOptions): Worker;
```

Starts a background worker over the Rust core and returns a handle.

## `WorkerOptions`
## `WorkerRunOptions`

| Option | Type | Default | Description |
|---|---|---|---|
| `queues` | `string[]` | `["default"]` | Queue names to serve. |
| `channelCapacity` | `number` | `128` | In-flight dispatch channel capacity. |
| `batchSize` | `number` | `1` | Jobs claimed per scheduler poll. |
| `advanceWorkflows` | `boolean` | `true` | Drive workflow bookkeeping. |
| `mesh` | `MeshOptions` | — | Join a [mesh](/node/guides/operations/mesh). |
| `mesh` | `MeshWorkerConfig` | — | Join a [mesh](/node/guides/operations/mesh). |

## `MeshOptions`
## `MeshWorkerConfig`

`port`, `seeds`, `steal`, `encryptionKey`, `bindAddr`, `advertiseAddr`,
`affinityWeight`, `localBuffer`, `stealBatch`, `stealThreshold`, `virtualNodes`,
Expand Down
Loading