Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 7 additions & 1 deletion sdks/node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,17 @@ worker.stop();
## Backends

```ts
new Queue({ dbPath: "taskito.db" }); // SQLite (default)
new Queue(); // SQLite at .taskito/taskito.db (default)
new Queue({ dbPath: "data/taskito.db" }); // SQLite at a custom path (dirs created)
new Queue({ backend: "postgres", dsn: process.env.PG_URL, schema: "taskito" });
new Queue({ backend: "redis", dsn: "redis://localhost", prefix: "taskito" });
```

SQLite defaults to `.taskito/taskito.db` and creates the parent directory
automatically. Postgres isolates its tables in the `schema` (default
`"taskito"`); Redis isolates its keys under `prefix`. Override either to share
or separate state.

## Enqueue options

`priority`, `maxRetries`, `timeoutMs`, `delayMs` (delayed run), `uniqueKey`
Expand Down
41 changes: 36 additions & 5 deletions sdks/node/src/queue.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { mkdirSync } from "node:fs";
import { dirname } from "node:path";
import {
JobCancelledError,
JobFailedError,
Expand Down Expand Up @@ -52,7 +54,8 @@ import { WorkflowManager } from "./workflows";

/** Construction options for a {@link Queue}. */
export interface QueueOptions {
/** SQLite file path — shorthand for `{ backend: "sqlite", dsn: path }`. */
/** SQLite file path — shorthand for `{ backend: "sqlite", dsn: path }`.
* Defaults to `.taskito/taskito.db`; missing parent directories are created. */
dbPath?: string;
/** `"sqlite"` (default), `"postgres"`, or `"redis"`. */
backend?: "sqlite" | "postgres" | "redis";
Expand Down Expand Up @@ -87,7 +90,7 @@ export class Queue<TTasks extends TaskMap = TaskMap> {
/** Built lazily — its constructor throws on addons lacking the `workflows` feature. */
private workflowManager?: WorkflowManager;

constructor(options: QueueOptions) {
constructor(options: QueueOptions = {}) {
this.native = JsQueue.open(toOpenOptions(options));
this.serializer = options.serializer ?? new JsonSerializer();
this.webhookManager = new WebhookManager(this.native, this.emitter);
Expand Down Expand Up @@ -510,18 +513,46 @@ function toNativeEnqueueOptions(options: EnqueueOptions): NativeEnqueueOptions {
return notes === undefined ? rest : { ...rest, notes: encodeNotes(notes) };
}

/** Default on-disk SQLite location — mirrors the Python SDK's `.taskito/taskito.db`. */
const DEFAULT_SQLITE_DB = ".taskito/taskito.db";

/** Resolve a {@link QueueOptions} into the native open options. */
function toOpenOptions(options: QueueOptions): OpenOptions {
const dsn = options.dsn ?? options.dbPath;
const backend = options.backend ?? "sqlite";
if (backend === "sqlite") {
// Zero-config default, like Python: an on-disk DB under `.taskito/`.
const dsn = options.dsn ?? options.dbPath ?? DEFAULT_SQLITE_DB;
ensureSqliteParentDir(dsn);
return { backend, dsn, poolSize: options.poolSize, namespace: options.namespace };
}
// Postgres/Redis have no sensible default endpoint — require an explicit dsn.
// The Postgres `schema` (default `"taskito"`, resolved in the addon) and the
// Redis `prefix` give each backend its own isolated namespace.
const dsn = options.dsn;
if (!dsn) {
throw new QueueError("Queue requires `dbPath` (SQLite) or `dsn`");
throw new QueueError(`Queue backend "${backend}" requires a \`dsn\` connection string`);
}
return {
backend: options.backend ?? (options.dbPath ? "sqlite" : undefined),
backend,
dsn,
poolSize: options.poolSize,
schema: options.schema,
prefix: options.prefix,
namespace: options.namespace,
};
}

/**
* Create the parent directory of a SQLite file path, as the Python SDK does —
* SQLite won't create missing directories itself. In-memory databases have no
* parent and are skipped.
*/
function ensureSqliteParentDir(dsn: string): void {
if (dsn === ":memory:" || dsn.startsWith("file::memory:")) {
return;
}
const dir = dirname(dsn);
if (dir && dir !== ".") {
mkdirSync(dir, { recursive: true });
}
}
4 changes: 2 additions & 2 deletions sdks/node/test/core/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ it("specific errors extend the right bases and carry a name", () => {
expect(new ResultTimeoutError("j", 50).timeoutMs).toBe(50);
});

it("Queue construction without storage throws QueueError", () => {
expect(() => new Queue({})).toThrow(QueueError);
it("Queue construction for a non-sqlite backend without a dsn throws QueueError", () => {
expect(() => new Queue({ backend: "postgres" })).toThrow(QueueError);
});

it("invalid notes throw NotesValidationError", () => {
Expand Down
30 changes: 30 additions & 0 deletions sdks/node/test/core/open.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { existsSync, mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { Queue } from "../../src/index";

describe("Queue construction", () => {
it("defaults SQLite to .taskito/taskito.db and creates the directory", () => {
const cwd = process.cwd();
const tmp = mkdtempSync(join(tmpdir(), "taskito-default-"));
try {
process.chdir(tmp);
new Queue();
expect(existsSync(join(tmp, ".taskito", "taskito.db"))).toBe(true);
} finally {
process.chdir(cwd);
}
});

it("creates missing parent directories for a nested dbPath", () => {
const dbPath = join(mkdtempSync(join(tmpdir(), "taskito-nested-")), "deep", "sub", "queue.db");
new Queue({ dbPath });
expect(existsSync(dbPath)).toBe(true);
});

it("requires a dsn for non-sqlite backends", () => {
expect(() => new Queue({ backend: "postgres" })).toThrow(/dsn/);
expect(() => new Queue({ backend: "redis" })).toThrow(/dsn/);
});
});