@sqlane/core 0.0.0 → 0.0.2

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/README.md CHANGED
@@ -24,16 +24,18 @@ pnpm install
24
24
  hk install --global --mise # Git hooks, also for GUI Git clients: lint, format, type check, secrets, typos, Conventional Commits
25
25
  ```
26
26
 
27
- | Command | Does |
28
- | ---------------- | ---------------------------------------------------------- |
29
- | `pnpm test` | Run the tests (Vitest) |
30
- | `pnpm lint` | Lint, including formatting (oxlint, @stylistic) |
31
- | `pnpm lint:fix` | Fix lint and formatting |
32
- | `pnpm format` | Format the other files: JSON, YAML, Markdown, TOML (oxfmt) |
33
- | `pnpm typecheck` | Type-check the JSDoc (`tsc`, no emit) |
34
- | `pnpm check` | Run all hook checks on all files (`hk check --all`) |
35
-
36
- Commit messages follow [Conventional Commits](https://www.conventionalcommits.org).
27
+ | Command | Does |
28
+ | ------------------- | ---------------------------------------------------------- |
29
+ | `pnpm test` | Run the tests (Vitest) |
30
+ | `pnpm lint` | Lint, including formatting (oxlint, @stylistic) |
31
+ | `pnpm lint:fix` | Fix lint and formatting |
32
+ | `pnpm format` | Format the other files: JSON, YAML, Markdown, TOML (oxfmt) |
33
+ | `pnpm lint:package` | Check the package that npm would publish (publint) |
34
+ | `pnpm typecheck` | Type-check the JSDoc (`tsc`, no emit) |
35
+ | `pnpm check` | Run all hook checks on all files (`hk check --all`) |
36
+
37
+ Commit messages follow [Conventional Commits](https://www.conventionalcommits.org). A change of the package adds a
38
+ changeset (`pnpm changeset`); see [releasing](docs/releasing.md).
37
39
 
38
40
  ## License
39
41
 
package/package.json CHANGED
@@ -1,58 +1,85 @@
1
1
  {
2
- "name": "@sqlane/core",
3
- "version": "0.0.0",
4
- "description": "Transactional SQL job queue for Node.js (MySQL, PostgreSQL)",
5
- "keywords": [
6
- "background-jobs",
7
- "job",
8
- "mysql",
9
- "outbox",
10
- "postgres",
11
- "postgresql",
12
- "queue",
13
- "transactional"
14
- ],
15
- "homepage": "https://github.com/sqlane/sqlane-node#readme",
16
- "bugs": "https://github.com/sqlane/sqlane-node/issues",
17
- "license": "MIT",
18
- "author": "Klim Pal",
19
- "repository": {
20
- "type": "git",
21
- "url": "git+https://github.com/sqlane/sqlane-node.git"
2
+ "name": "@sqlane/core",
3
+ "version": "0.0.2",
4
+ "description": "Transactional SQL job queue for Node.js (MySQL, PostgreSQL)",
5
+ "keywords": [
6
+ "background-jobs",
7
+ "job",
8
+ "mysql",
9
+ "outbox",
10
+ "postgres",
11
+ "postgresql",
12
+ "queue",
13
+ "transactional"
14
+ ],
15
+ "homepage": "https://github.com/sqlane/sqlane-node#readme",
16
+ "bugs": "https://github.com/sqlane/sqlane-node/issues",
17
+ "license": "MIT",
18
+ "author": "Klim Pal",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/sqlane/sqlane-node.git"
22
+ },
23
+ "files": [
24
+ "src",
25
+ "!src/**/*.test.js"
26
+ ],
27
+ "type": "module",
28
+ "exports": {
29
+ ".": "./src/index.js",
30
+ "./mysql2": "./src/adapters/mysql2.js",
31
+ "./pg": "./src/adapters/pg.js"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "devDependencies": {
37
+ "@changesets/changelog-github": "1.0.1",
38
+ "@changesets/cli": "3.0.3",
39
+ "@stylistic/eslint-plugin": "5.10.0",
40
+ "@testcontainers/mysql": "12.2.1",
41
+ "@testcontainers/postgresql": "12.2.1",
42
+ "@types/node": "22.20.5",
43
+ "@types/pg": "8.23.1",
44
+ "@vitest/coverage-v8": "5.0.5",
45
+ "mysql2": "3.24.6",
46
+ "oxfmt": "0.75.0",
47
+ "oxlint": "1.90.0",
48
+ "pg": "8.23.1",
49
+ "publint": "0.3.25",
50
+ "testcontainers": "12.2.1",
51
+ "typescript": "7.0.2",
52
+ "vitest": "5.0.5"
53
+ },
54
+ "peerDependencies": {
55
+ "mysql2": "^3.11.0",
56
+ "pg": "^8.13.0"
57
+ },
58
+ "peerDependenciesMeta": {
59
+ "mysql2": {
60
+ "optional": true
22
61
  },
23
- "files": [
24
- "src",
25
- "!src/**/*.test.js"
26
- ],
27
- "type": "module",
28
- "exports": {
29
- ".": "./src/index.js"
30
- },
31
- "publishConfig": {
32
- "access": "public"
33
- },
34
- "scripts": {
35
- "lint": "oxlint --deny-warnings",
36
- "lint:fix": "oxlint --fix",
37
- "format": "oxfmt",
38
- "format:check": "oxfmt --check",
39
- "typecheck": "tsc -p tsconfig.json",
40
- "test": "vitest run",
41
- "test:watch": "vitest",
42
- "test:coverage": "vitest run --coverage",
43
- "check": "hk check --all"
44
- },
45
- "devDependencies": {
46
- "@stylistic/eslint-plugin": "^5.10.0",
47
- "@types/node": "^22.20.5",
48
- "@vitest/coverage-v8": "^5.0.5",
49
- "oxfmt": "^0.75.0",
50
- "oxlint": "^1.90.0",
51
- "typescript": "^7.0.2",
52
- "vitest": "^5.0.5"
53
- },
54
- "engines": {
55
- "node": ">=22.12.0"
56
- },
57
- "packageManager": "pnpm@12.16.1"
58
- }
62
+ "pg": {
63
+ "optional": true
64
+ }
65
+ },
66
+ "engines": {
67
+ "node": ">=22.12.0"
68
+ },
69
+ "scripts": {
70
+ "lint": "oxlint --deny-warnings",
71
+ "lint:fix": "oxlint --fix",
72
+ "lint:package": "publint --strict",
73
+ "format": "oxfmt",
74
+ "format:check": "oxfmt --check",
75
+ "typecheck": "tsc -p tsconfig.json",
76
+ "test": "vitest run",
77
+ "test:unit": "vitest run --project unit",
78
+ "test:integration": "vitest run --project 'integration:*'",
79
+ "test:watch": "vitest",
80
+ "test:coverage": "vitest run --coverage",
81
+ "check": "hk check --all",
82
+ "changeset": "changeset",
83
+ "changeset:version": "changeset version && oxfmt --no-error-on-unmatched-pattern CHANGELOG.md"
84
+ }
85
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @typedef {import('mysql2/promise').Pool
3
+ * | import('mysql2/promise').PoolConnection
4
+ * | import('mysql2/promise').Connection} Mysql2Connection
5
+ */
6
+
7
+ /**
8
+ * @param {Mysql2Connection} connection
9
+ * @returns {import('../types.js').Executor}
10
+ */
11
+ function toExecutor(connection) {
12
+ return {
13
+ query: async (sql, params) => {
14
+ // The three types have the same query(); TS cannot call a method of a union of overloads.
15
+ const queryable = /** @type {import('mysql2/promise').Connection} */ (connection);
16
+ const [rows] = await queryable.query(sql, [...params]);
17
+
18
+ return Array.isArray(rows) ? /** @type {Record<string, unknown>[]} */ (rows) : [];
19
+ },
20
+ };
21
+ }
22
+
23
+ /**
24
+ * The adapter for mysql2 (its promise API). `enqueue` then takes a `Connection`, a `PoolConnection` (in a
25
+ * transaction) or a `Pool`.
26
+ * @param {import('mysql2/promise').Pool} pool The queue's own pool: claims, leases, results.
27
+ * @returns {import('../types.js').Adapter<Mysql2Connection>}
28
+ */
29
+ export function mysql2Adapter(pool) {
30
+ return {
31
+ dialect: 'mysql',
32
+ wrap: toExecutor,
33
+ queue: toExecutor(pool),
34
+ };
35
+ }
@@ -0,0 +1,36 @@
1
+ /** @typedef {import('pg').Pool | import('pg').PoolClient | import('pg').Client} PgConnection */
2
+
3
+ /**
4
+ * @typedef {object} PgAdapterOptions
5
+ * @property {boolean} [notify] Hints by LISTEN/NOTIFY: `pg_notify` in the enqueue transaction, delivered on commit.
6
+ * Default: false.
7
+ */
8
+
9
+ /**
10
+ * @param {PgConnection} connection
11
+ * @returns {import('../types.js').Executor}
12
+ */
13
+ function toExecutor(connection) {
14
+ return {
15
+ query: async (sql, params) => {
16
+ const result = await connection.query(sql, [...params]);
17
+
18
+ return result.rows;
19
+ },
20
+ };
21
+ }
22
+
23
+ /**
24
+ * The adapter for pg. `enqueue` then takes a `Client`, a `PoolClient` (in a transaction) or a `Pool`.
25
+ * @param {import('pg').Pool} pool The queue's own pool: claims, leases, results.
26
+ * @param {PgAdapterOptions} [options]
27
+ * @returns {import('../types.js').Adapter<PgConnection> & { notify: boolean }}
28
+ */
29
+ export function pgAdapter(pool, options = {}) {
30
+ return {
31
+ dialect: 'postgres',
32
+ wrap: toExecutor,
33
+ queue: toExecutor(pool),
34
+ notify: options.notify ?? false,
35
+ };
36
+ }
@@ -0,0 +1,34 @@
1
+ import { ConfigError } from './errors.js';
2
+
3
+ /**
4
+ * @param {string} method
5
+ * @returns {never}
6
+ */
7
+ function notImplemented(method) {
8
+ throw new Error(`sqlane: ${method}() is not implemented yet`);
9
+ }
10
+
11
+ /**
12
+ * Creates a sqlane instance. It connects, migrates and starts nothing.
13
+ * @template C The connections that `enqueue` accepts, from the adapter.
14
+ * @template {string} [L=never] The lane names, from `lanes`.
15
+ * @param {import('./types.js').SqlaneOptions<C, L>} options
16
+ * @returns {import('./types.js').Sqlane<C, L>}
17
+ */
18
+ export function createSqlane(options) {
19
+ if (!options?.adapter) {
20
+ throw new ConfigError('createSqlane: "adapter" is required');
21
+ }
22
+
23
+ return {
24
+ enqueue: () => notImplemented('enqueue'),
25
+ enqueueMany: () => notImplemented('enqueueMany'),
26
+ handle: () => notImplemented('handle'),
27
+ start: () => notImplemented('start'),
28
+ drain: () => notImplemented('drain'),
29
+ close: () => notImplemented('close'),
30
+ migrate: () => notImplemented('migrate'),
31
+ getStats: () => notImplemented('getStats'),
32
+ wake: () => notImplemented('wake'),
33
+ };
34
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Defines a job name and its options. Producers and workers import the definition.
3
+ * @param {import('./types.js').JobOptions} options
4
+ * @returns {import('./types.js').JobDefinition}
5
+ */
6
+ export function defineJob(options) {
7
+ // The validation of the options comes with the implementation.
8
+ return Object.freeze({ ...options });
9
+ }
package/src/errors.js ADDED
@@ -0,0 +1,176 @@
1
+ /**
2
+ * The base of the errors of sqlane.
3
+ */
4
+ export class SqlaneError extends Error {
5
+ /**
6
+ * @param {string} code A stable code, e.g. `CONFIG`.
7
+ * @param {string} message
8
+ * @param {ErrorOptions} [options]
9
+ */
10
+ constructor(code, message, options) {
11
+ super(message, options);
12
+ this.name = 'SqlaneError';
13
+ /** @readonly */
14
+ this.code = code;
15
+ }
16
+ }
17
+
18
+ /** Bad `createSqlane`, `defineJob` or `handle()` options. */
19
+ export class ConfigError extends SqlaneError {
20
+ /**
21
+ * @param {string} message
22
+ * @param {ErrorOptions} [options]
23
+ */
24
+ constructor(message, options) {
25
+ super('CONFIG', message, options);
26
+ this.name = 'ConfigError';
27
+ }
28
+ }
29
+
30
+ /** Bad enqueue options: a bad name or `jobId`, `delay` with `runAt`. */
31
+ export class InvalidOptionsError extends SqlaneError {
32
+ /**
33
+ * @param {string} message
34
+ * @param {ErrorOptions} [options]
35
+ */
36
+ constructor(message, options) {
37
+ super('INVALID_OPTIONS', message, options);
38
+ this.name = 'InvalidOptionsError';
39
+ }
40
+ }
41
+
42
+ /** The payload is above `maxPayloadBytes`. */
43
+ export class PayloadTooLargeError extends SqlaneError {
44
+ /**
45
+ * @param {string} message
46
+ * @param {ErrorOptions} [options]
47
+ */
48
+ constructor(message, options) {
49
+ super('PAYLOAD_TOO_LARGE', message, options);
50
+ this.name = 'PayloadTooLargeError';
51
+ }
52
+ }
53
+
54
+ /** More than 10 mutex keys, or a key that breaks the rules. */
55
+ export class InvalidMutexKeyError extends SqlaneError {
56
+ /**
57
+ * @param {string} message
58
+ * @param {ErrorOptions} [options]
59
+ */
60
+ constructor(message, options) {
61
+ super('INVALID_MUTEX_KEY', message, options);
62
+ this.name = 'InvalidMutexKeyError';
63
+ }
64
+ }
65
+
66
+ /** Enqueue of an unknown name with `unknownJobs: 'reject'`. */
67
+ export class UnknownJobError extends SqlaneError {
68
+ /**
69
+ * @param {string} message
70
+ * @param {ErrorOptions} [options]
71
+ */
72
+ constructor(message, options) {
73
+ super('UNKNOWN_JOB', message, options);
74
+ this.name = 'UnknownJobError';
75
+ }
76
+ }
77
+
78
+ /** `start`, `handle` or `drain` after `close()`. */
79
+ export class ClosedError extends SqlaneError {
80
+ /**
81
+ * @param {string} message
82
+ * @param {ErrorOptions} [options]
83
+ */
84
+ constructor(message, options) {
85
+ super('CLOSED', message, options);
86
+ this.name = 'ClosedError';
87
+ }
88
+ }
89
+
90
+ /** `drain()` did not finish in its `timeout`. */
91
+ export class DrainTimeoutError extends SqlaneError {
92
+ /**
93
+ * @param {string} message
94
+ * @param {ErrorOptions} [options]
95
+ */
96
+ constructor(message, options) {
97
+ super('DRAIN_TIMEOUT', message, options);
98
+ this.name = 'DrainTimeoutError';
99
+ }
100
+ }
101
+
102
+ /** The `signal.reason` on a timeout; the error of the attempt. */
103
+ export class JobTimeoutError extends SqlaneError {
104
+ /**
105
+ * @param {string} message
106
+ * @param {ErrorOptions} [options]
107
+ */
108
+ constructor(message, options) {
109
+ super('JOB_TIMEOUT', message, options);
110
+ this.name = 'JobTimeoutError';
111
+ }
112
+ }
113
+
114
+ /** The `signal.reason` at the close deadline. */
115
+ export class ShutdownError extends SqlaneError {
116
+ /**
117
+ * @param {string} message
118
+ * @param {ErrorOptions} [options]
119
+ */
120
+ constructor(message, options) {
121
+ super('SHUTDOWN', message, options);
122
+ this.name = 'ShutdownError';
123
+ }
124
+ }
125
+
126
+ /** The `signal.reason` when the lease could not be renewed in time. */
127
+ export class LeaseLostError extends SqlaneError {
128
+ /**
129
+ * @param {string} message
130
+ * @param {ErrorOptions} [options]
131
+ */
132
+ constructor(message, options) {
133
+ super('LEASE_LOST', message, options);
134
+ this.name = 'LeaseLostError';
135
+ }
136
+ }
137
+
138
+ /** The error of a job without its payload. */
139
+ export class MissingPayloadError extends SqlaneError {
140
+ /**
141
+ * @param {string} message
142
+ * @param {ErrorOptions} [options]
143
+ */
144
+ constructor(message, options) {
145
+ super('MISSING_PAYLOAD', message, options);
146
+ this.name = 'MissingPayloadError';
147
+ }
148
+ }
149
+
150
+ /** Thrown by a handler: the job fails at once, without retries; its failure job runs. */
151
+ export class FatalJobError extends SqlaneError {
152
+ /**
153
+ * @param {string} message
154
+ * @param {ErrorOptions} [options]
155
+ */
156
+ constructor(message, options) {
157
+ super('FATAL_JOB', message, options);
158
+ this.name = 'FatalJobError';
159
+ }
160
+ }
161
+
162
+ /** Thrown by a handler: the job is retried after `delay` instead of the backoff. It still uses an attempt. */
163
+ export class RetryJobError extends SqlaneError {
164
+ /**
165
+ * @param {string} message
166
+ * @param {ErrorOptions & { delay: import('./types.js').Duration }} options
167
+ */
168
+ constructor(message, options) {
169
+ const { delay, ...errorOptions } = options;
170
+
171
+ super('RETRY_JOB', message, errorOptions);
172
+ this.name = 'RetryJobError';
173
+ /** @readonly */
174
+ this.delay = delay;
175
+ }
176
+ }
package/src/index.js CHANGED
@@ -1,2 +1,63 @@
1
- /* oxlint-disable unicorn/no-empty-file -- placeholder until the first module */
2
- // Public API of sqlane.
1
+ // The public API of sqlane. See docs/design.md.
2
+
3
+ export { createSqlane } from './create_sqlane.js';
4
+ export { defineJob } from './define_job.js';
5
+ export {
6
+ ClosedError,
7
+ ConfigError,
8
+ DrainTimeoutError,
9
+ FatalJobError,
10
+ InvalidMutexKeyError,
11
+ InvalidOptionsError,
12
+ JobTimeoutError,
13
+ LeaseLostError,
14
+ MissingPayloadError,
15
+ PayloadTooLargeError,
16
+ RetryJobError,
17
+ ShutdownError,
18
+ SqlaneError,
19
+ UnknownJobError,
20
+ } from './errors.js';
21
+ export { getMigrations } from './migrations.js';
22
+
23
+ /**
24
+ * @typedef {import('./types.js').Adapter<C>} Adapter
25
+ * @template C
26
+ */
27
+ /**
28
+ * @typedef {import('./types.js').Sqlane<C, L>} Sqlane
29
+ * @template C
30
+ * @template {string} L
31
+ */
32
+ /**
33
+ * @typedef {import('./types.js').SqlaneOptions<C, L>} SqlaneOptions
34
+ * @template C
35
+ * @template {string} L
36
+ */
37
+ /**
38
+ * @typedef {import('./types.js').Jsonable<T>} Jsonable
39
+ * @template T
40
+ */
41
+ /** @typedef {import('./types.js').Backoff} Backoff */
42
+ /** @typedef {import('./types.js').CloseOptions} CloseOptions */
43
+ /** @typedef {import('./types.js').DrainOptions} DrainOptions */
44
+ /** @typedef {import('./types.js').DrainResult} DrainResult */
45
+ /** @typedef {import('./types.js').Duration} Duration */
46
+ /** @typedef {import('./types.js').EnqueueOptions} EnqueueOptions */
47
+ /** @typedef {import('./types.js').EnqueueResult} EnqueueResult */
48
+ /** @typedef {import('./types.js').Executor} Executor */
49
+ /** @typedef {import('./types.js').FailedJobPayload} FailedJobPayload */
50
+ /** @typedef {import('./types.js').Handler} Handler */
51
+ /** @typedef {import('./types.js').HandlerContext} HandlerContext */
52
+ /** @typedef {import('./types.js').Hint} Hint */
53
+ /** @typedef {import('./types.js').JobDefaults} JobDefaults */
54
+ /** @typedef {import('./types.js').JobDefinition} JobDefinition */
55
+ /** @typedef {import('./types.js').JobInfo} JobInfo */
56
+ /** @typedef {import('./types.js').JobOptions} JobOptions */
57
+ /** @typedef {import('./types.js').JobStats} JobStats */
58
+ /** @typedef {import('./types.js').LaneOptions} LaneOptions */
59
+ /** @typedef {import('./types.js').Logger} Logger */
60
+ /** @typedef {import('./types.js').MigrateOptions} MigrateOptions */
61
+ /** @typedef {import('./types.js').Migration} Migration */
62
+ /** @typedef {import('./types.js').Retention} Retention */
63
+ /** @typedef {import('./types.js').SqlaneEvent} SqlaneEvent */
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The schema of sqlane as versioned SQL, for the app's own migration tool.
3
+ * @param {object} options
4
+ * @param {'mysql' | 'postgres'} options.dialect
5
+ * @param {string} [options.tablePrefix] Default: `'sqlane_'`.
6
+ * @returns {import('./types.js').Migration[]}
7
+ */
8
+ export function getMigrations(options) {
9
+ throw new Error(`sqlane: getMigrations(${options.dialect}) is not implemented yet`);
10
+ }
package/src/types.js ADDED
@@ -0,0 +1,288 @@
1
+ // The public types of sqlane. See docs/design.md.
2
+
3
+ /**
4
+ * A duration: a string (`'500ms'`, `'5s'`, `'10m'`, `'1h'`, `'30d'`) or milliseconds.
5
+ * @typedef {string | number} Duration
6
+ */
7
+
8
+ /**
9
+ * The JSON form of T: T itself if it is JSON, else a type that T does not match. Optional fields and interfaces pass;
10
+ * a `Date`, a `Map`, a `Set`, a function, a `bigint` or a class instance with methods does not.
11
+ * @template T
12
+ * @typedef {T extends string | number | boolean | null ? T
13
+ * : T extends undefined | bigint | symbol | Function | Date | RegExp | Map<any, any> | Set<any> ? never
14
+ * : T extends readonly (infer U)[] ? readonly Jsonable<U>[]
15
+ * : T extends object ? { [K in keyof T]: Jsonable<T[K]> }
16
+ * : never} Jsonable
17
+ */
18
+
19
+ /**
20
+ * @typedef {object} ExponentialBackoff
21
+ * @property {'exponential'} type `delay × 2^(attempt − 1)`, at most `maxDelay`.
22
+ * @property {Duration} delay
23
+ * @property {Duration} [maxDelay]
24
+ * @property {number} [jitter] 0–1: the delay is `delay × (1 − jitter × random())`. Default: 0.2.
25
+ */
26
+
27
+ /**
28
+ * @typedef {object} FixedBackoff
29
+ * @property {'fixed'} type
30
+ * @property {Duration} delay
31
+ * @property {number} [jitter] 0–1. Default: 0.2.
32
+ */
33
+
34
+ /** @typedef {ExponentialBackoff | FixedBackoff} Backoff */
35
+
36
+ /**
37
+ * How long a finished job is kept. It is also the dedupe window of its `jobId`.
38
+ * @typedef {object} Retention
39
+ * @property {Duration} [completed] Default: `'1h'`.
40
+ * @property {Duration} [failed] Default: `'30d'`.
41
+ */
42
+
43
+ /**
44
+ * The options of a job name, shared by its producers and workers. A definition overrides the instance `defaults`.
45
+ * @typedef {object} JobOptions
46
+ * @property {string} name Matches `^[a-z][a-z0-9_.-]{0,127}$`.
47
+ * @property {number} [maxAttempts] Default: 3.
48
+ * @property {number} [maxStalls] Default: 2.
49
+ * @property {Backoff} [backoff]
50
+ * @property {Retention} [retention]
51
+ * @property {Duration} [timeout] No default. A job with mutex keys needs one.
52
+ * @property {(payload: any) => readonly string[]} [mutexKeys] Sync, from the payload only.
53
+ * @property {string | JobDefinition} [failureJob] Added when a job fails for good.
54
+ */
55
+
56
+ /**
57
+ * The result of `defineJob()`. Frozen.
58
+ * @typedef {Readonly<JobOptions>} JobDefinition
59
+ */
60
+
61
+ /**
62
+ * @typedef {object} EnqueueBaseOptions
63
+ * @property {string} [jobId] 1–120 printable ASCII characters. Dedupe: a job with this id is added once while its row
64
+ * exists.
65
+ * @property {number} [maxAttempts]
66
+ * @property {number} [maxStalls]
67
+ * @property {Retention} [retention]
68
+ * @property {string | JobDefinition} [failureJob]
69
+ * @property {readonly string[]} [mutexKeys] Replace the keys of the definition; `[]` clears them.
70
+ */
71
+
72
+ /**
73
+ * The options of an enqueue: `delay` or `runAt`, not both.
74
+ * @typedef {EnqueueBaseOptions & ({ delay?: Duration, runAt?: never } | { runAt: Date, delay?: never })}
75
+ * EnqueueOptions
76
+ */
77
+
78
+ /**
79
+ * @typedef {object} EnqueueResult
80
+ * @property {string} id `<job_name>:<jobId>`, or a new ULID without a `jobId`.
81
+ * @property {boolean} created False: a job with this `jobId` exists already, nothing was added.
82
+ */
83
+
84
+ /**
85
+ * @template P
86
+ * @typedef {object} EnqueueItem
87
+ * @property {P} payload
88
+ * @property {EnqueueOptions} [options]
89
+ */
90
+
91
+ /**
92
+ * @typedef {object} JobInfo
93
+ * @property {string} id
94
+ * @property {string} name
95
+ * @property {number} attempt 1 for the first run.
96
+ * @property {number} maxAttempts
97
+ * @property {Date} enqueuedAt
98
+ * @property {readonly string[]} mutexKeys
99
+ */
100
+
101
+ /**
102
+ * @typedef {object} HandlerContext
103
+ * @property {any} payload As JSON gives it back.
104
+ * @property {JobInfo} job
105
+ * @property {AbortSignal} signal Aborted with a `JobTimeoutError`, a `LeaseLostError` or a `ShutdownError`.
106
+ */
107
+
108
+ /**
109
+ * Runs a job. Its return value is ignored.
110
+ * @typedef {(context: HandlerContext) => unknown} Handler
111
+ */
112
+
113
+ /**
114
+ * A private lane (`concurrency`) or a shared lane of `lanes` (`lane`), not both.
115
+ * @template {string} L
116
+ * @typedef {{ timeout?: Duration } & (
117
+ * { concurrency?: number, lane?: never } | { lane: L, concurrency?: never }
118
+ * )} HandleOptions
119
+ */
120
+
121
+ /**
122
+ * @typedef {object} LaneOptions
123
+ * @property {number} concurrency The handlers of the lane that run at the same time in one process.
124
+ */
125
+
126
+ /**
127
+ * The instance defaults; a definition overrides them.
128
+ * @typedef {object} JobDefaults
129
+ * @property {number} [maxAttempts]
130
+ * @property {number} [maxStalls]
131
+ * @property {Backoff} [backoff]
132
+ * @property {Retention} [retention]
133
+ * @property {Duration} [timeout]
134
+ */
135
+
136
+ /**
137
+ * A pub/sub that wakes the other processes. Only an optimization: a lost message delays a job until the next poll.
138
+ * @typedef {object} Hint
139
+ * @property {(channel: string, message: string) => Promise<void>} publish
140
+ * @property {(channel: string, onMessage: (message: string) => void) => Promise<() => Promise<void>>} subscribe
141
+ * Resolves to the unsubscribe function.
142
+ */
143
+
144
+ /**
145
+ * @typedef {(object: object, message?: string) => void} LogFn
146
+ */
147
+
148
+ /**
149
+ * A pino-compatible logger.
150
+ * @typedef {object} Logger
151
+ * @property {LogFn} debug
152
+ * @property {LogFn} info
153
+ * @property {LogFn} warn
154
+ * @property {LogFn} error
155
+ */
156
+
157
+ /**
158
+ * @typedef {object} JobEventBase
159
+ * @property {string} jobId
160
+ * @property {string} jobName
161
+ * @property {number} attempt
162
+ */
163
+
164
+ /**
165
+ * An event of the job lifecycle, by `type`.
166
+ * @typedef {(JobEventBase & (
167
+ * | { type: 'claimed' }
168
+ * | { type: 'completed', durationMs: number }
169
+ * | { type: 'retried', durationMs: number, delayMs: number, error: unknown }
170
+ * | { type: 'failed', durationMs: number, error: unknown }
171
+ * | { type: 'timed_out', durationMs: number }
172
+ * | { type: 'stalled' }
173
+ * | { type: 'released' }
174
+ * | { type: 'lease_lost' }
175
+ * | { type: 'failure_job_not_added', failureJob: string, error: unknown }
176
+ * )) | { type: 'error', operation: string, error: unknown }} SqlaneEvent
177
+ */
178
+
179
+ /**
180
+ * The connections that `enqueue` accepts (C), and the queue's own pool. Made by `mysql2Adapter()` or `pgAdapter()`.
181
+ * @template C
182
+ * @typedef {object} Adapter
183
+ * @property {'mysql' | 'postgres'} dialect
184
+ * @property {(connection: C) => Executor} wrap Used by sqlane: wraps a connection given to `enqueue`.
185
+ * @property {Executor} queue Used by sqlane: the queue's own pool.
186
+ */
187
+
188
+ /**
189
+ * Runs SQL. Used by sqlane, through an adapter.
190
+ * @typedef {object} Executor
191
+ * @property {(sql: string, params: readonly unknown[]) => Promise<readonly Record<string, unknown>[]>} query
192
+ */
193
+
194
+ /**
195
+ * @template C
196
+ * @template {string} L
197
+ * @typedef {object} SqlaneOptions
198
+ * @property {Adapter<C>} adapter
199
+ * @property {readonly JobDefinition[]} [jobs] The definitions known by name.
200
+ * @property {number} [concurrency] The handlers that run at the same time in this process. Default: 10.
201
+ * @property {Record<L, LaneOptions>} [lanes] Limits shared by several job names.
202
+ * @property {JobDefaults} [defaults]
203
+ * @property {'allow' | 'reject'} [unknownJobs] Default: `'allow'`. `'reject'`: `enqueue` of a name that is not in
204
+ * `jobs` and not handled throws an `UnknownJobError`.
205
+ * @property {Hint | null} [hint] Default: null: polling only.
206
+ * @property {Logger} [logger] Default: no logs.
207
+ * @property {(event: SqlaneEvent) => void} [onEvent]
208
+ * @property {string} [tablePrefix] Default: `'sqlane_'`.
209
+ * @property {Duration} [pollInterval] Default: `'5s'`.
210
+ * @property {number} [maxPayloadBytes] Default: 256 KiB.
211
+ * @property {boolean} [cleanup] Built-in deletion of the expired jobs. Default: true.
212
+ */
213
+
214
+ /**
215
+ * @typedef {object} DrainOptions
216
+ * @property {Duration} [timeout] Default: `'10s'`.
217
+ * @property {boolean} [includeDelayed] Also runs the delayed jobs and the retries in backoff at once.
218
+ */
219
+
220
+ /**
221
+ * @typedef {object} DrainResult
222
+ * @property {number} completed
223
+ * @property {number} retried Failed attempts that were retried.
224
+ * @property {number} failed Jobs that failed for good.
225
+ */
226
+
227
+ /**
228
+ * @typedef {object} CloseOptions
229
+ * @property {Duration} [deadline] From the call. Default: `'30s'`.
230
+ * @property {'release' | 'abandon'} [mode] Default: `'release'`.
231
+ * @property {Duration} [exitsBy] From the call: the latest time the process exits. Default: the deadline plus one
232
+ * lease.
233
+ */
234
+
235
+ /**
236
+ * @typedef {object} MigrateOptions
237
+ * @property {Duration} [lockTimeout] Default: `'10m'`.
238
+ */
239
+
240
+ /**
241
+ * @typedef {object} JobStats
242
+ * @property {string} name
243
+ * @property {number} pending
244
+ * @property {number} due
245
+ * @property {number} running
246
+ * @property {number} stuck Timed out, the handler has not settled.
247
+ * @property {number} failed
248
+ * @property {number | null} oldestDueAgeMs
249
+ */
250
+
251
+ /**
252
+ * The payload of a failure job.
253
+ * @typedef {object} FailedJobPayload
254
+ * @property {{ id: string, name: string, payload: any, attempt: number, maxAttempts: number }} job
255
+ * @property {{ name: string, message: string, stack?: string }} error
256
+ */
257
+
258
+ /**
259
+ * @template C The connections that `enqueue` accepts.
260
+ * @template {string} L The lane names.
261
+ * @typedef {object} Sqlane
262
+ * @property {<P>(
263
+ * connection: C, job: JobDefinition | string, payload: P & NoInfer<Jsonable<P>>, options?: EnqueueOptions,
264
+ * ) => Promise<EnqueueResult>} enqueue Adds a job with the caller's connection: it must be in a transaction (or a
265
+ * pool: then sqlane opens its own transaction).
266
+ * @property {<P>(
267
+ * connection: C,
268
+ * job: JobDefinition | string,
269
+ * items: readonly (EnqueueItem<P> & NoInfer<EnqueueItem<Jsonable<P>>>)[],
270
+ * ) => Promise<EnqueueResult[]>} enqueueMany The results are in the order of the items.
271
+ * @property {(job: JobDefinition | string, handler: Handler, options?: HandleOptions<L>) => void} handle
272
+ * @property {() => Promise<void>} start
273
+ * @property {(options?: DrainOptions) => Promise<DrainResult>} drain For tests: runs the due jobs of the handled
274
+ * names until none is left, without `start()`.
275
+ * @property {(options?: CloseOptions) => Promise<void>} close
276
+ * @property {(options?: MigrateOptions) => Promise<void>} migrate
277
+ * @property {() => Promise<JobStats[]>} getStats
278
+ * @property {(jobName: string) => void} wake Low level: probes this name now.
279
+ */
280
+
281
+ /**
282
+ * @typedef {object} Migration
283
+ * @property {string} id
284
+ * @property {readonly string[]} statements
285
+ */
286
+
287
+ // oxlint-disable-next-line unicorn/require-module-specifiers -- makes the file a module: its typedefs are exported
288
+ export {};