@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 +12 -10
- package/package.json +83 -56
- package/src/adapters/mysql2.js +35 -0
- package/src/adapters/pg.js +36 -0
- package/src/create_sqlane.js +34 -0
- package/src/define_job.js +9 -0
- package/src/errors.js +176 -0
- package/src/index.js +63 -2
- package/src/migrations.js +10 -0
- package/src/types.js +288 -0
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
|
|
28
|
-
|
|
|
29
|
-
| `pnpm test`
|
|
30
|
-
| `pnpm lint`
|
|
31
|
-
| `pnpm lint:fix`
|
|
32
|
-
| `pnpm format`
|
|
33
|
-
| `pnpm
|
|
34
|
-
| `pnpm
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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 {};
|