@superfunctions/queue 0.1.0 → 0.2.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ Breaking: `QueueAdapter` is now a lease-based, fully async interface, and a durable SQL adapter is added.
6
+
7
+ - `QueueAdapter<T>` methods are `enqueue(queue, payload, { delayMs?, dedupeKey? })`, `claim(queue, { leaseMs })`, `ack(id)`, `fail(id, { retryAt } | { deadLetter: true, reason })`, `extendLease(id, leaseMs)`, `peek(queue, limit)` and `size(queue)`. All return Promises.
8
+ - `ack`, `fail` and `extendLease` accept an optional `{ attempt }` fence (the `attempt` returned by `claim`), so a claimer whose lease expired and was reclaimed cannot act on the new lease.
9
+ - New `DatabaseQueueAdapter` stores jobs in a `queue_jobs` table on SQLite (SQLite >= 3.35), Cloudflare D1 and Postgres. Construct it with `{ db }`, a `@superfunctions/db` 0.3.1 `drizzleAdapter` or `kyselyAdapter`, whose new `rawSql` executor runs the statements; or with `{ client }` from `createSqliteQueueClient`, `createD1QueueClient` or `createPostgresQueueClient`. `@superfunctions/db` is an optional peer dependency; the `db` option is typed structurally (`QueueRawSqlSource`), so the published declarations do not import it. `claim` is one atomic `UPDATE … WHERE id = (SELECT … LIMIT 1) RETURNING` statement (`FOR UPDATE SKIP LOCKED` on Postgres). Migrations ship in `migrations/sqlite` and `migrations/postgres`, and `adapter.migrate()` applies the same DDL.
10
+ - `MemoryQueueAdapter` implements the new interface. `maxSize` now counts pending and leased jobs.
11
+ - Errors are `QueueError` instances whose `message` and `code` are the error code, for example `FLOWFN_QUEUE_JOB_NOT_LEASED`.
12
+ - `FlowFnQueueAdapter` no longer implements `QueueAdapter`, because the FlowFn HTTP API has no leases. It keeps `enqueue(queue, payload)` and `dequeue(queue)`; `dequeueMatching`, `peek` and `size`, which always threw, are removed.
13
+
14
+ ### Migrating from 0.1
15
+
16
+ | 0.1 | 0.2 |
17
+ | --- | --- |
18
+ | `await queue.enqueue(name, payload)` | Unchanged; now resolves `{ id, deduplicated }` |
19
+ | `const payload = await queue.dequeue(name)` | `const job = await queue.claim(name, { leaseMs })`, use `job.payload`, then `await queue.ack(job.id, { attempt: job.attempt })` |
20
+ | Retrying by enqueueing again | `await queue.fail(job.id, { retryAt, attempt: job.attempt })` |
21
+ | Dropping a poison message | `await queue.fail(job.id, { deadLetter: true, reason, attempt: job.attempt })` |
22
+ | `queue.dequeueMatching(name, matcher)` | Removed; use separate queue names or filter after `claim` and `fail` with an immediate `retryAt` |
23
+ | `queue.peek(name)` returning payloads | `await queue.peek(name, limit)` returning `QueueJob` records; use `.map((job) => job.payload)` |
24
+ | `queue.size(name)` | `await queue.size(name)` |
25
+
26
+ A job that is claimed but never acked or failed becomes claimable again when its lease expires, with `attempt` incremented. Callers that do not want redelivery should dead-letter when `attempt` exceeds their limit.
27
+
28
+ ## 0.1.0
29
+
30
+ - Initial `MemoryQueueAdapter` and `FlowFnQueueAdapter`.
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # @superfunctions/queue
2
+
3
+ Lease-based job queues for Superfunctions libraries. `MemoryQueueAdapter` keeps jobs in process; `DatabaseQueueAdapter` stores them in a SQL table so they survive restarts on SQLite, Cloudflare D1 and Postgres. Callers drive `claim`; there is no worker loop or scheduler.
4
+
5
+ ```ts
6
+ interface QueueAdapter<T> {
7
+ enqueue(queue: string, payload: T, options?: { delayMs?: number; dedupeKey?: string }): Promise<{ id: string; deduplicated: boolean }>;
8
+ claim(queue: string, options: { leaseMs: number }): Promise<{ id: string; payload: T; attempt: number } | null>;
9
+ ack(id: string, fence?: { attempt?: number }): Promise<void>;
10
+ fail(id: string, options: { retryAt: Date | number; reason?: string; attempt?: number } | { deadLetter: true; reason: string; attempt?: number }): Promise<void>;
11
+ extendLease(id: string, leaseMs: number, fence?: { attempt?: number }): Promise<void>;
12
+ peek(queue: string, limit: number): Promise<QueueJob<T>[]>;
13
+ size(queue: string): Promise<number>;
14
+ }
15
+ ```
16
+
17
+ ## Semantics
18
+
19
+ - A job is held by at most one claimer at a time. `claim` leases the oldest available job (by `availableAt`, then `id`) for `leaseMs` and returns it with `attempt` incremented, starting at 1.
20
+ - If the lease expires before `ack` or `fail`, the job can be claimed again with the next `attempt`. Use `extendLease` for long work.
21
+ - Pass `{ attempt: job.attempt }` to `ack`, `fail` and `extendLease`. Without it, a claimer whose lease expired and was reclaimed could act on the new claimer's lease. All three reject with `FLOWFN_QUEUE_JOB_NOT_LEASED` when the job is not leased (or the fence does not match).
22
+ - `ack` removes the job. `fail` with `retryAt` makes it pending again at that time; `fail` with `deadLetter` keeps it in the `dead` state (database adapter only) and it is never claimed again.
23
+ - `dedupeKey`: while a job with the same key is pending or leased in the same queue, `enqueue` is a no-op that returns the existing id with `deduplicated: true`. Acked or dead-lettered jobs release the key.
24
+ - `peek` and `size` cover pending and leased jobs.
25
+ - `delayMs` and `leaseMs` are integer milliseconds up to 100 years; invalid input rejects with a `QueueError` code such as `FLOWFN_QUEUE_LEASE_INVALID` before storage is touched.
26
+ - Delivery is at least once: a claimer can crash after its side effects and before `ack`. Make handlers idempotent.
27
+
28
+ ## Durable adapter
29
+
30
+ `DatabaseQueueAdapter` runs on a `@superfunctions/db` SQL adapter. `drizzleAdapter` and `kyselyAdapter` (0.3.1 or later) expose `rawSql`, a parameterized-statement executor on the adapter's own connection, because the CRUD API cannot express the single-statement claim. Pass the same adapter the rest of the app uses:
31
+
32
+ ```ts
33
+ import Database from 'better-sqlite3';
34
+ import { drizzle } from 'drizzle-orm/better-sqlite3';
35
+ import { drizzleAdapter } from '@superfunctions/db/adapters/drizzle';
36
+ import { DatabaseQueueAdapter } from '@superfunctions/queue';
37
+
38
+ const sqlite = new Database('app.db');
39
+ sqlite.pragma('journal_mode = WAL');
40
+ sqlite.pragma('busy_timeout = 5000');
41
+ const db = drizzleAdapter({ db: drizzle(sqlite), dialect: 'sqlite' });
42
+
43
+ const queue = new DatabaseQueueAdapter<{ url: string }>({ db });
44
+ await queue.migrate();
45
+
46
+ await queue.enqueue('fetch', { url: 'https://example.com' }, { dedupeKey: 'https://example.com' });
47
+
48
+ const job = await queue.claim('fetch', { leaseMs: 30_000 });
49
+ if (job) {
50
+ try {
51
+ await handle(job.payload);
52
+ await queue.ack(job.id, { attempt: job.attempt });
53
+ } catch (error) {
54
+ await queue.fail(job.id, job.attempt >= 5
55
+ ? { deadLetter: true, reason: String(error), attempt: job.attempt }
56
+ : { retryAt: Date.now() + 2 ** job.attempt * 1_000, reason: String(error), attempt: job.attempt });
57
+ }
58
+ }
59
+ ```
60
+
61
+ The same works for the other backends:
62
+
63
+ - D1: `drizzleAdapter({ db: drizzle(env.DB), dialect: 'sqlite' })` with `drizzle-orm/d1`.
64
+ - Postgres: `drizzleAdapter({ db: drizzle(pool), dialect: 'postgres' })` with `drizzle-orm/node-postgres` (or `drizzle-orm/pglite`), or `kyselyAdapter({ db: new Kysely({ dialect: new PostgresDialect({ pool }) }), dialect: 'postgres', schema: {} })`.
65
+ - SQLite through Kysely: `kyselyAdapter({ db: new Kysely({ dialect: new SqliteDialect({ database }) }), dialect: 'sqlite', schema: {} })`.
66
+
67
+ An adapter without `rawSql` (memory, Prisma, DynamoDB, Cloudflare KV) is rejected with `FLOWFN_QUEUE_RAW_SQL_UNAVAILABLE`, and a MySQL one with `FLOWFN_QUEUE_DIALECT_INVALID`. `rawSql` bypasses the db adapter's schema mapping and row-level namespaces, so the queue table is shared by every namespace; use `tableName` to separate queues per app.
68
+
69
+ Without `@superfunctions/db`, pass a driver client instead of `db`:
70
+
71
+ - `createSqliteQueueClient(db)` for a better-sqlite3 database (SQLite >= 3.35 for `RETURNING`). Use WAL and a `busy_timeout` when several processes share the file.
72
+ - `createD1QueueClient(env.DB)` for a Cloudflare D1 binding.
73
+ - `createPostgresQueueClient(pool)` for a node-postgres `Pool` or `Client`, or anything with `query(text, values) => { rows }`, such as PGlite.
74
+
75
+ Any other driver can implement `QueueSqlClient`, which has the shape of `rawSql`: `dialect`, `query(sql, params)` returning rows and `execute(sql, params)`, where each `?` is a positional parameter.
76
+
77
+ `claim` is one statement, `UPDATE … WHERE id = (SELECT … ORDER BY available_at, id LIMIT 1) RETURNING …`. On Postgres the subquery uses `FOR UPDATE SKIP LOCKED`, so concurrent claimers take different rows without waiting; SQLite and D1 run the statement under their single writer lock.
78
+
79
+ Lease times come from the adapter's `now` option (default `Date.now`), not the database clock. Processes sharing a table need synchronized clocks, with leases much longer than any skew.
80
+
81
+ ### Migrations
82
+
83
+ The `queue_jobs` table (`id`, `queue`, `payload` JSON, `state`, `attempt`, `available_at`, `lease_until`, `dedupe_key`, `last_error`, `created_at`, `updated_at`; times in epoch milliseconds) is created by:
84
+
85
+ - `await queue.migrate()`, which runs idempotent `CREATE … IF NOT EXISTS` statements. Run it once at deploy time, not from many processes at once.
86
+ - The shipped files `@superfunctions/queue/migrations/sqlite/0001_queue_jobs.sql` (SQLite and D1, for example with `wrangler d1 migrations`) and `@superfunctions/queue/migrations/postgres/0001_queue_jobs.sql`.
87
+ - `queueJobsMigrationSql(dialect, tableName)` for a custom table name, which must match the adapter's `tableName` option.
88
+
89
+ Dead-lettered rows stay in the table with `state = 'dead'` and `last_error`; delete them when they are no longer needed.
90
+
91
+ ## Memory adapter
92
+
93
+ `new MemoryQueueAdapter({ maxSize?, now? })` has the same semantics in one process. Jobs are lost on restart, payloads are kept by reference, and dead-lettered jobs are discarded. `maxSize` caps pending and leased jobs per queue (`FLOWFN_QUEUE_FULL`).
94
+
95
+ ## FlowFn client
96
+
97
+ `FlowFnQueueAdapter` calls a hosted FlowFn queue over HTTP with `enqueue(queue, payload)` and a destructive `dequeue(queue)`. The FlowFn API has no leases, so it does not implement `QueueAdapter`.
98
+
99
+ ## Tests
100
+
101
+ `npm test` runs the shared contract against memory, a better-sqlite3 file, D1 under workerd (Miniflare) and Postgres via PGlite, each through a driver client and through `@superfunctions/db` (`drizzleAdapter`, plus `kyselyAdapter` on SQLite), and 8 claimer threads against 500 jobs on SQLite through both paths. Set `SUPERFUNCTIONS_QUEUE_POSTGRES_URL` to also run the contract (client, Drizzle and Kysely), an 8-connection concurrency test on each path and concurrent dedupe against a Postgres server; each test uses its own table and drops it.
@@ -0,0 +1,49 @@
1
+ import type { QueueRawSqlSource, QueueSqlClient } from './sql-client.js';
2
+ import type { ClaimOptions, ClaimedJob, EnqueueOptions, EnqueueResult, FailOptions, LeaseFence, QueueAdapter, QueueJob } from './types.js';
3
+ interface DatabaseQueueAdapterBaseOptions {
4
+ /** Defaults to `queue_jobs`. */
5
+ tableName?: string;
6
+ /**
7
+ * Clock in epoch milliseconds, defaults to `Date.now`. Leases are compared
8
+ * against this clock, so processes sharing a table need synchronized clocks
9
+ * and leases longer than their skew.
10
+ */
11
+ now?: () => number;
12
+ }
13
+ export type DatabaseQueueAdapterOptions = DatabaseQueueAdapterBaseOptions & ({
14
+ /**
15
+ * A `@superfunctions/db` SQL adapter (`drizzleAdapter` or `kyselyAdapter`
16
+ * on SQLite, D1 or Postgres). Statements run through its `rawSql`.
17
+ */
18
+ db: QueueRawSqlSource;
19
+ client?: never;
20
+ } | {
21
+ /** A driver client from `createSqliteQueueClient`, `createD1QueueClient` or `createPostgresQueueClient`. */
22
+ client: QueueSqlClient;
23
+ db?: never;
24
+ });
25
+ /**
26
+ * Durable queue on a SQL table, for SQLite (>= 3.35), D1 and Postgres, run
27
+ * through a `@superfunctions/db` SQL adapter or a driver client. Create the table with `migrate()` or the shipped migration files
28
+ * before use. Payloads are stored as JSON, so they must be JSON-serializable
29
+ * and are returned as their JSON round trip.
30
+ */
31
+ export declare class DatabaseQueueAdapter<TPayload> implements QueueAdapter<TPayload> {
32
+ private readonly client;
33
+ private readonly tableName;
34
+ private readonly statements;
35
+ private readonly now;
36
+ constructor(options: DatabaseQueueAdapterOptions);
37
+ /** Creates the jobs table and its indexes if they do not exist. */
38
+ migrate(): Promise<void>;
39
+ enqueue(queueName: string, payload: TPayload, options?: EnqueueOptions): Promise<EnqueueResult>;
40
+ claim(queueName: string, options: ClaimOptions): Promise<ClaimedJob<TPayload> | null>;
41
+ ack(id: string, fence?: LeaseFence): Promise<void>;
42
+ fail(id: string, options: FailOptions): Promise<void>;
43
+ extendLease(id: string, leaseMs: number, fence?: LeaseFence): Promise<void>;
44
+ peek(queueName: string, limit: number): Promise<QueueJob<TPayload>[]>;
45
+ size(queueName: string): Promise<number>;
46
+ private requireLeasedChange;
47
+ }
48
+ export {};
49
+ //# sourceMappingURL=database.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAiB,MAAM,iBAAiB,CAAC;AACxF,OAAO,KAAK,EACV,YAAY,EACZ,UAAU,EACV,cAAc,EACd,aAAa,EACb,WAAW,EACX,UAAU,EACV,YAAY,EACZ,QAAQ,EAET,MAAM,YAAY,CAAC;AAapB,UAAU,+BAA+B;IACvC,gCAAgC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,2BAA2B,GAAG,+BAA+B,GACvE,CACI;IACE;;;OAGG;IACH,EAAE,EAAE,iBAAiB,CAAC;IACtB,MAAM,CAAC,EAAE,KAAK,CAAC;CAChB,GACD;IACE,4GAA4G;IAC5G,MAAM,EAAE,cAAc,CAAC;IACvB,EAAE,CAAC,EAAE,KAAK,CAAC;CACZ,CACJ,CAAC;AA0GJ;;;;;GAKG;AACH,qBAAa,oBAAoB,CAAC,QAAQ,CAAE,YAAW,YAAY,CAAC,QAAQ,CAAC;IAC3E,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;IACxC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEvB,OAAO,EAAE,2BAA2B;IAOhD,mEAAmE;IAC7D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAMxB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IA+B/F,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC;IAYrF,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAWlD,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBrD,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAa3E,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;IAyBrE,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAMhC,mBAAmB;CAYlC"}
@@ -0,0 +1,204 @@
1
+ import { DEFAULT_QUEUE_JOBS_TABLE, queueJobsMigrationStatements, quoteQueueTableName } from './migrations.js';
2
+ import { QueueError, normalizeEnqueueOptions, normalizeFailOptions, normalizeFence, normalizeJobId, normalizeLeaseMs, normalizeLimit, normalizeQueueName, readClock, } from './validation.js';
3
+ function resolveClient(options) {
4
+ if (options?.db !== undefined) {
5
+ const rawSql = options.db?.rawSql;
6
+ if (!rawSql) {
7
+ throw new QueueError('FLOWFN_QUEUE_RAW_SQL_UNAVAILABLE');
8
+ }
9
+ if (rawSql.dialect !== 'sqlite' && rawSql.dialect !== 'postgres') {
10
+ throw new QueueError('FLOWFN_QUEUE_DIALECT_INVALID');
11
+ }
12
+ return rawSql;
13
+ }
14
+ const client = options?.client;
15
+ if (client?.dialect !== 'sqlite' && client?.dialect !== 'postgres') {
16
+ throw new QueueError('FLOWFN_QUEUE_DIALECT_INVALID');
17
+ }
18
+ return client;
19
+ }
20
+ const LIVE_STATES = "state IN ('pending', 'leased')";
21
+ const MAX_DEDUPE_ATTEMPTS = 3;
22
+ function buildStatements(client, table) {
23
+ const postgres = client.dialect === 'postgres';
24
+ const payload = postgres ? 'payload::text AS payload' : 'payload';
25
+ const columns = `id, queue, ${payload}, state, attempt, available_at, lease_until, dedupe_key, last_error, created_at, updated_at`;
26
+ const leased = (fenced) => `id = ? AND state = 'leased'${fenced ? ' AND attempt = ?' : ''} RETURNING id`;
27
+ return {
28
+ insert: `INSERT INTO ${table} (queue, payload, state, attempt, available_at, dedupe_key, created_at, updated_at) ` +
29
+ `VALUES (?, ?, 'pending', 0, ?, ?, ?, ?) ` +
30
+ `ON CONFLICT (queue, dedupe_key) WHERE dedupe_key IS NOT NULL AND ${LIVE_STATES} DO NOTHING RETURNING id`,
31
+ findDeduped: `SELECT id FROM ${table} WHERE queue = ? AND dedupe_key = ? AND ${LIVE_STATES}`,
32
+ // One statement: the subquery picks the next claimable job and the UPDATE
33
+ // leases it, so two claimers can never lease the same row. Postgres skips
34
+ // rows another transaction has locked instead of waiting on them; SQLite
35
+ // runs the whole statement under its single writer lock.
36
+ claim: `UPDATE ${table} SET state = 'leased', attempt = attempt + 1, lease_until = ?, updated_at = ? ` +
37
+ `WHERE id = (SELECT id FROM ${table} WHERE queue = ? AND ` +
38
+ `((state = 'pending' AND available_at <= ?) OR (state = 'leased' AND lease_until <= ?)) ` +
39
+ `ORDER BY available_at, id LIMIT 1${postgres ? ' FOR UPDATE SKIP LOCKED' : ''}) ` +
40
+ `RETURNING id, ${payload}, attempt`,
41
+ ack: `DELETE FROM ${table} WHERE ${leased(false)}`,
42
+ ackFenced: `DELETE FROM ${table} WHERE ${leased(true)}`,
43
+ retry: `UPDATE ${table} SET state = 'pending', available_at = ?, lease_until = NULL, last_error = ?, updated_at = ? WHERE ${leased(false)}`,
44
+ retryFenced: `UPDATE ${table} SET state = 'pending', available_at = ?, lease_until = NULL, last_error = ?, updated_at = ? WHERE ${leased(true)}`,
45
+ deadLetter: `UPDATE ${table} SET state = 'dead', lease_until = NULL, last_error = ?, updated_at = ? WHERE ${leased(false)}`,
46
+ deadLetterFenced: `UPDATE ${table} SET state = 'dead', lease_until = NULL, last_error = ?, updated_at = ? WHERE ${leased(true)}`,
47
+ extend: `UPDATE ${table} SET lease_until = ?, updated_at = ? WHERE ${leased(false)}`,
48
+ extendFenced: `UPDATE ${table} SET lease_until = ?, updated_at = ? WHERE ${leased(true)}`,
49
+ peek: `SELECT ${columns} FROM ${table} WHERE queue = ? AND ${LIVE_STATES} ORDER BY available_at, id LIMIT ?`,
50
+ size: `SELECT COUNT(*) AS count FROM ${table} WHERE queue = ? AND ${LIVE_STATES}`,
51
+ };
52
+ }
53
+ function toNumber(value) {
54
+ const result = typeof value === 'number' ? value : Number(value);
55
+ if (!Number.isFinite(result)) {
56
+ throw new QueueError('FLOWFN_QUEUE_ROW_INVALID');
57
+ }
58
+ return result;
59
+ }
60
+ function toNullableNumber(value) {
61
+ return value === null || value === undefined ? null : toNumber(value);
62
+ }
63
+ function parsePayload(value) {
64
+ if (typeof value !== 'string') {
65
+ throw new QueueError('FLOWFN_QUEUE_ROW_INVALID');
66
+ }
67
+ return JSON.parse(value);
68
+ }
69
+ function serializePayload(payload) {
70
+ let serialized;
71
+ try {
72
+ serialized = JSON.stringify(payload);
73
+ }
74
+ catch {
75
+ throw new QueueError('FLOWFN_QUEUE_PAYLOAD_INVALID');
76
+ }
77
+ if (serialized === undefined) {
78
+ throw new QueueError('FLOWFN_QUEUE_PAYLOAD_INVALID');
79
+ }
80
+ return serialized;
81
+ }
82
+ /**
83
+ * Durable queue on a SQL table, for SQLite (>= 3.35), D1 and Postgres, run
84
+ * through a `@superfunctions/db` SQL adapter or a driver client. Create the table with `migrate()` or the shipped migration files
85
+ * before use. Payloads are stored as JSON, so they must be JSON-serializable
86
+ * and are returned as their JSON round trip.
87
+ */
88
+ export class DatabaseQueueAdapter {
89
+ client;
90
+ tableName;
91
+ statements;
92
+ now;
93
+ constructor(options) {
94
+ this.client = resolveClient(options);
95
+ this.tableName = options.tableName ?? DEFAULT_QUEUE_JOBS_TABLE;
96
+ this.statements = buildStatements(this.client, quoteQueueTableName(this.tableName));
97
+ this.now = options.now ?? Date.now;
98
+ }
99
+ /** Creates the jobs table and its indexes if they do not exist. */
100
+ async migrate() {
101
+ for (const statement of queueJobsMigrationStatements(this.client.dialect, this.tableName)) {
102
+ await this.client.execute(statement, []);
103
+ }
104
+ }
105
+ async enqueue(queueName, payload, options) {
106
+ const queue = normalizeQueueName(queueName);
107
+ const { delayMs, dedupeKey } = normalizeEnqueueOptions(options);
108
+ const serialized = serializePayload(payload);
109
+ // A conflicting live job can be acked between the insert and the lookup;
110
+ // the next insert then succeeds, so a few rounds always settle.
111
+ for (let round = 0; round < MAX_DEDUPE_ATTEMPTS; round += 1) {
112
+ const now = readClock(this.now);
113
+ const inserted = await this.client.query(this.statements.insert, [
114
+ queue,
115
+ serialized,
116
+ now + delayMs,
117
+ dedupeKey,
118
+ now,
119
+ now,
120
+ ]);
121
+ if (inserted.length > 0) {
122
+ return { id: String(inserted[0].id), deduplicated: false };
123
+ }
124
+ if (dedupeKey === null) {
125
+ break;
126
+ }
127
+ const existing = await this.client.query(this.statements.findDeduped, [queue, dedupeKey]);
128
+ if (existing.length > 0) {
129
+ return { id: String(existing[0].id), deduplicated: true };
130
+ }
131
+ }
132
+ throw new QueueError('FLOWFN_QUEUE_ENQUEUE_CONFLICT');
133
+ }
134
+ async claim(queueName, options) {
135
+ const queue = normalizeQueueName(queueName);
136
+ const leaseMs = normalizeLeaseMs(options?.leaseMs);
137
+ const now = readClock(this.now);
138
+ const rows = await this.client.query(this.statements.claim, [now + leaseMs, now, queue, now, now]);
139
+ if (rows.length === 0) {
140
+ return null;
141
+ }
142
+ const row = rows[0];
143
+ return { id: String(row.id), payload: parsePayload(row.payload), attempt: toNumber(row.attempt) };
144
+ }
145
+ async ack(id, fence) {
146
+ const jobId = normalizeJobId(id);
147
+ const attempt = normalizeFence(fence);
148
+ await this.requireLeasedChange(attempt === null ? this.statements.ack : this.statements.ackFenced, [], jobId, attempt);
149
+ }
150
+ async fail(id, options) {
151
+ const normalized = normalizeFailOptions(options);
152
+ const jobId = normalizeJobId(id);
153
+ const now = readClock(this.now);
154
+ if (normalized.kind === 'dead') {
155
+ await this.requireLeasedChange(normalized.attempt === null ? this.statements.deadLetter : this.statements.deadLetterFenced, [normalized.reason, now], jobId, normalized.attempt);
156
+ return;
157
+ }
158
+ await this.requireLeasedChange(normalized.attempt === null ? this.statements.retry : this.statements.retryFenced, [normalized.retryAt, normalized.reason, now], jobId, normalized.attempt);
159
+ }
160
+ async extendLease(id, leaseMs, fence) {
161
+ const normalizedLeaseMs = normalizeLeaseMs(leaseMs);
162
+ const jobId = normalizeJobId(id);
163
+ const attempt = normalizeFence(fence);
164
+ const now = readClock(this.now);
165
+ await this.requireLeasedChange(attempt === null ? this.statements.extend : this.statements.extendFenced, [now + normalizedLeaseMs, now], jobId, attempt);
166
+ }
167
+ async peek(queueName, limit) {
168
+ const queue = normalizeQueueName(queueName);
169
+ const normalizedLimit = normalizeLimit(limit);
170
+ if (normalizedLimit === 0) {
171
+ return [];
172
+ }
173
+ const rows = await this.client.query(this.statements.peek, [queue, normalizedLimit]);
174
+ return rows.map((row) => {
175
+ const leaseUntil = toNullableNumber(row.lease_until);
176
+ return {
177
+ id: String(row.id),
178
+ queue: String(row.queue),
179
+ payload: parsePayload(row.payload),
180
+ state: row.state,
181
+ attempt: toNumber(row.attempt),
182
+ availableAt: new Date(toNumber(row.available_at)),
183
+ leaseUntil: leaseUntil === null ? null : new Date(leaseUntil),
184
+ dedupeKey: row.dedupe_key ?? null,
185
+ lastError: row.last_error ?? null,
186
+ createdAt: new Date(toNumber(row.created_at)),
187
+ updatedAt: new Date(toNumber(row.updated_at)),
188
+ };
189
+ });
190
+ }
191
+ async size(queueName) {
192
+ const queue = normalizeQueueName(queueName);
193
+ const rows = await this.client.query(this.statements.size, [queue]);
194
+ return toNumber(rows[0]?.count ?? 0);
195
+ }
196
+ async requireLeasedChange(sql, values, jobId, attempt) {
197
+ const params = attempt === null ? [...values, jobId] : [...values, jobId, attempt];
198
+ const rows = await this.client.query(sql, params);
199
+ if (rows.length === 0) {
200
+ throw new QueueError('FLOWFN_QUEUE_JOB_NOT_LEASED');
201
+ }
202
+ }
203
+ }
204
+ //# sourceMappingURL=database.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.js","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ export interface FlowFnQueueAdapterConfig {
2
+ baseUrl: string;
3
+ apiKey: string;
4
+ fetch?: typeof globalThis.fetch;
5
+ timeoutMs?: number;
6
+ }
7
+ /**
8
+ * HTTP client for a hosted FlowFn queue. The FlowFn API only offers enqueue
9
+ * and a destructive dequeue, without leases, acknowledgements or retries, so
10
+ * this client does not implement `QueueAdapter`.
11
+ */
12
+ export declare class FlowFnQueueAdapter<TPayload> {
13
+ private readonly config;
14
+ private readonly fetcher;
15
+ private readonly timeoutMs;
16
+ private readonly baseUrl;
17
+ constructor(config: FlowFnQueueAdapterConfig);
18
+ enqueue(queueName: string, payload: TPayload): Promise<void>;
19
+ dequeue(queueName: string): Promise<TPayload | null>;
20
+ private fetchWithTimeout;
21
+ }
22
+ //# sourceMappingURL=flowfn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flowfn.d.ts","sourceRoot":"","sources":["../src/flowfn.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAwBD;;;;GAIG;AACH,qBAAa,kBAAkB,CAAC,QAAQ;IAK1B,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAClD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAEJ,MAAM,EAAE,wBAAwB;IAMvD,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB5D,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;YAqB5C,gBAAgB;CAgB/B"}
package/dist/flowfn.js ADDED
@@ -0,0 +1,84 @@
1
+ import { QueueError, normalizeQueueName } from './validation.js';
2
+ function normalizeBaseUrl(baseUrl) {
3
+ let normalized = baseUrl;
4
+ while (normalized.endsWith('/')) {
5
+ normalized = normalized.slice(0, -1);
6
+ }
7
+ return normalized;
8
+ }
9
+ function encodeQueueName(queueName) {
10
+ return encodeURIComponent(normalizeQueueName(queueName));
11
+ }
12
+ function normalizeTimeoutMs(value) {
13
+ if (value === undefined) {
14
+ return 15_000;
15
+ }
16
+ if (!Number.isFinite(value)) {
17
+ throw new QueueError('FLOWFN_QUEUE_TIMEOUT_INVALID');
18
+ }
19
+ return Math.max(0, value);
20
+ }
21
+ /**
22
+ * HTTP client for a hosted FlowFn queue. The FlowFn API only offers enqueue
23
+ * and a destructive dequeue, without leases, acknowledgements or retries, so
24
+ * this client does not implement `QueueAdapter`.
25
+ */
26
+ export class FlowFnQueueAdapter {
27
+ config;
28
+ fetcher;
29
+ timeoutMs;
30
+ baseUrl;
31
+ constructor(config) {
32
+ this.config = config;
33
+ this.fetcher = config.fetch ?? globalThis.fetch;
34
+ this.timeoutMs = normalizeTimeoutMs(config.timeoutMs);
35
+ this.baseUrl = normalizeBaseUrl(config.baseUrl);
36
+ }
37
+ async enqueue(queueName, payload) {
38
+ const encodedQueueName = encodeQueueName(queueName);
39
+ const response = await this.fetchWithTimeout(`${this.baseUrl}/queues/${encodedQueueName}/enqueue`, {
40
+ method: 'POST',
41
+ headers: {
42
+ 'Content-Type': 'application/json',
43
+ Authorization: `Bearer ${this.config.apiKey}`,
44
+ },
45
+ body: JSON.stringify(payload),
46
+ });
47
+ if (!response.ok) {
48
+ throw new Error(`FlowFn enqueue failed: ${response.status} ${response.statusText}`);
49
+ }
50
+ }
51
+ async dequeue(queueName) {
52
+ const encodedQueueName = encodeQueueName(queueName);
53
+ const response = await this.fetchWithTimeout(`${this.baseUrl}/queues/${encodedQueueName}/dequeue`, {
54
+ method: 'POST',
55
+ headers: {
56
+ Authorization: `Bearer ${this.config.apiKey}`,
57
+ },
58
+ });
59
+ if (response.status === 404) {
60
+ return null;
61
+ }
62
+ if (!response.ok) {
63
+ throw new Error(`FlowFn dequeue failed: ${response.status} ${response.statusText}`);
64
+ }
65
+ const data = (await response.json());
66
+ return 'payload' in data ? data.payload : null;
67
+ }
68
+ async fetchWithTimeout(url, init) {
69
+ const controller = typeof AbortController === 'function' ? new AbortController() : undefined;
70
+ const timeoutId = controller && this.timeoutMs > 0 ? setTimeout(() => controller.abort(), this.timeoutMs) : undefined;
71
+ try {
72
+ return await this.fetcher(url, {
73
+ ...init,
74
+ signal: controller?.signal,
75
+ });
76
+ }
77
+ finally {
78
+ if (timeoutId) {
79
+ clearTimeout(timeoutId);
80
+ }
81
+ }
82
+ }
83
+ }
84
+ //# sourceMappingURL=flowfn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flowfn.js","sourceRoot":"","sources":["../src/flowfn.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,46 +1,12 @@
1
- export interface QueueAdapter<TPayload> {
2
- enqueue(queueName: string, payload: TPayload): Promise<void>;
3
- dequeue(queueName: string): Promise<TPayload | null>;
4
- dequeueMatching(queueName: string, matcher: (payload: TPayload) => boolean): Promise<TPayload | null>;
5
- peek(queueName: string): TPayload[];
6
- size(queueName: string): number;
7
- }
8
- export interface MemoryQueueAdapterOptions {
9
- /**
10
- * Maximum number of items retained per queue. When reached, further
11
- * `enqueue` calls reject with `FLOWFN_QUEUE_FULL` instead of growing the
12
- * backing array without bound (which would leak memory until OOM if
13
- * producers outpace consumers). Unset means unbounded (legacy behaviour).
14
- */
15
- maxSize?: number;
16
- }
17
- export declare class MemoryQueueAdapter<TPayload> implements QueueAdapter<TPayload> {
18
- private readonly queues;
19
- private readonly maxSize?;
20
- constructor(options?: MemoryQueueAdapterOptions);
21
- enqueue(queueName: string, payload: TPayload): Promise<void>;
22
- dequeue(queueName: string): Promise<TPayload | null>;
23
- dequeueMatching(queueName: string, matcher: (payload: TPayload) => boolean): Promise<TPayload | null>;
24
- peek(queueName: string): TPayload[];
25
- size(queueName: string): number;
26
- }
27
- export interface FlowFnQueueAdapterConfig {
28
- baseUrl: string;
29
- apiKey: string;
30
- fetch?: typeof globalThis.fetch;
31
- timeoutMs?: number;
32
- }
33
- export declare class FlowFnQueueAdapter<TPayload> implements QueueAdapter<TPayload> {
34
- private readonly config;
35
- private readonly fetcher;
36
- private readonly timeoutMs;
37
- private readonly baseUrl;
38
- constructor(config: FlowFnQueueAdapterConfig);
39
- enqueue(queueName: string, payload: TPayload): Promise<void>;
40
- dequeue(queueName: string): Promise<TPayload | null>;
41
- dequeueMatching(queueName: string, matcher: (payload: TPayload) => boolean): Promise<TPayload | null>;
42
- peek(queueName: string): TPayload[];
43
- size(queueName: string): number;
44
- private fetchWithTimeout;
45
- }
1
+ export type { ClaimOptions, ClaimedJob, EnqueueOptions, EnqueueResult, FailOptions, LeaseFence, QueueAdapter, QueueJob, QueueJobState, } from './types.js';
2
+ export { QueueError } from './validation.js';
3
+ export { MemoryQueueAdapter } from './memory.js';
4
+ export type { MemoryQueueAdapterOptions } from './memory.js';
5
+ export { DatabaseQueueAdapter } from './database.js';
6
+ export type { DatabaseQueueAdapterOptions } from './database.js';
7
+ export { createD1QueueClient, createPostgresQueueClient, createSqliteQueueClient } from './sql-client.js';
8
+ export type { BetterSqlite3DatabaseLike, D1DatabaseLike, PostgresQueryableLike, QueueRawSqlExecutor, QueueRawSqlSource, QueueSqlClient, QueueSqlDialect, QueueSqlValue, } from './sql-client.js';
9
+ export { DEFAULT_QUEUE_JOBS_TABLE, queueJobsMigrationSql, queueJobsMigrationStatements } from './migrations.js';
10
+ export { FlowFnQueueAdapter } from './flowfn.js';
11
+ export type { FlowFnQueueAdapterConfig } from './flowfn.js';
46
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY,CAAC,QAAQ;IACpC,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7D,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IACrD,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IACtG,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,QAAQ,EAAE,CAAC;IACpC,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;CACjC;AAUD,MAAM,WAAW,yBAAyB;IACxC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,kBAAkB,CAAC,QAAQ,CAAE,YAAW,YAAY,CAAC,QAAQ,CAAC;IACzE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiC;IACxD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAS;gBAEtB,OAAO,GAAE,yBAA8B;IAS7C,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAU5D,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAUpD,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAgB3G,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,QAAQ,EAAE;IAKnC,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;CAIhC;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAwBD,qBAAa,kBAAkB,CAAC,QAAQ,CAAE,YAAW,YAAY,CAAC,QAAQ,CAAC;IAK7D,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAClD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAEJ,MAAM,EAAE,wBAAwB;IAMvD,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB5D,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAqBpD,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAM3G,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,QAAQ,EAAE;IAKnC,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;YAKjB,gBAAgB;CAgB/B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,YAAY,EACZ,UAAU,EACV,cAAc,EACd,aAAa,EACb,WAAW,EACX,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,YAAY,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,YAAY,EAAE,2BAA2B,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAC1G,YAAY,EACV,yBAAyB,EACzB,cAAc,EACd,qBAAqB,EACrB,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,aAAa,GACd,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAC;AAChH,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,YAAY,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC"}