@sqlane/core 0.0.1 → 0.1.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/README.md +44 -6
- package/package.json +20 -2
- package/src/adapters/mysql2.js +96 -0
- package/src/adapters/pg.js +164 -0
- package/src/backoff.js +24 -0
- package/src/create_sqlane.js +210 -0
- package/src/defaults.js +38 -0
- package/src/define_job.js +40 -0
- package/src/duration.js +27 -0
- package/src/enqueue.js +132 -0
- package/src/error_json.js +30 -0
- package/src/errors.js +188 -0
- package/src/hint_channel.js +107 -0
- package/src/index.js +64 -2
- package/src/migrations.js +163 -0
- package/src/mutex_keys.js +34 -0
- package/src/new_job.js +128 -0
- package/src/payload.js +54 -0
- package/src/registry.js +169 -0
- package/src/runner.js +974 -0
- package/src/schema/mysql.js +74 -0
- package/src/schema/postgres.js +67 -0
- package/src/store/mysql_enqueue.js +98 -0
- package/src/store/mysql_final.js +424 -0
- package/src/store/mysql_worker.js +250 -0
- package/src/store/postgres_enqueue.js +54 -0
- package/src/store/postgres_final.js +407 -0
- package/src/store/postgres_worker.js +224 -0
- package/src/store/store.js +136 -0
- package/src/transient_error.js +22 -0
- package/src/types.js +300 -0
- package/src/validate.js +181 -0
package/src/duration.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import parse from 'parse-duration';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parses a duration: milliseconds (a non-negative safe integer), or a string that parse-duration reads, such as
|
|
5
|
+
* `'500ms'`, `'5s'`, `'1.5h'`, `'4m30s'`, `'2 days'`.
|
|
6
|
+
* @param {unknown} value
|
|
7
|
+
* @returns {number | null} The milliseconds, rounded. Null: not a duration, or negative.
|
|
8
|
+
*/
|
|
9
|
+
export function parseDuration(value) {
|
|
10
|
+
if (typeof value === 'number') {
|
|
11
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (typeof value !== 'string') {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ms = parse(value);
|
|
19
|
+
|
|
20
|
+
if (typeof ms !== 'number' || !(ms >= 0)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const rounded = Math.round(ms);
|
|
25
|
+
|
|
26
|
+
return Number.isSafeInteger(rounded) ? rounded : null;
|
|
27
|
+
}
|
package/src/enqueue.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { monotonicFactory } from 'ulid';
|
|
2
|
+
import { InvalidOptionsError, UnknownJobError } from './errors.js';
|
|
3
|
+
import { buildNewJob } from './new_job.js';
|
|
4
|
+
import { insertJobsMysql } from './store/mysql_enqueue.js';
|
|
5
|
+
import { insertJobsPostgres } from './store/postgres_enqueue.js';
|
|
6
|
+
import { isPlainObject } from './validate.js';
|
|
7
|
+
|
|
8
|
+
const enqueueToken = monotonicFactory();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @template C
|
|
12
|
+
* @typedef {object} EnqueuerOptions
|
|
13
|
+
* @property {import('./types.js').Adapter<C>} adapter
|
|
14
|
+
* @property {string} tablePrefix Checked.
|
|
15
|
+
* @property {(name: string) => import('./types.js').JobDefinition | null} findDefinition The known definition of
|
|
16
|
+
* a name.
|
|
17
|
+
* @property {'allow' | 'reject'} unknownJobs
|
|
18
|
+
* @property {import('./defaults.js').ResolvedDefaults} defaults
|
|
19
|
+
* @property {number} maxPayloadBytes
|
|
20
|
+
* @property {string | null} notifyChannel PostgreSQL NOTIFY: `pg_notify` in the enqueue transaction, delivered on
|
|
21
|
+
* commit. Null: no NOTIFY.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @template C
|
|
26
|
+
* @typedef {object} Enqueuer
|
|
27
|
+
* @property {import('./types.js').Sqlane<C, never>['enqueue']} enqueue
|
|
28
|
+
* @property {import('./types.js').Sqlane<C, never>['enqueueMany']} enqueueMany
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The enqueue side of an instance.
|
|
33
|
+
* @template C
|
|
34
|
+
* @param {EnqueuerOptions<C>} options
|
|
35
|
+
* @returns {Enqueuer<C>}
|
|
36
|
+
*/
|
|
37
|
+
export function createEnqueuer({
|
|
38
|
+
adapter,
|
|
39
|
+
tablePrefix,
|
|
40
|
+
findDefinition,
|
|
41
|
+
unknownJobs,
|
|
42
|
+
defaults,
|
|
43
|
+
maxPayloadBytes,
|
|
44
|
+
notifyChannel,
|
|
45
|
+
}) {
|
|
46
|
+
const insertJobs = adapter.dialect === 'mysql' ? insertJobsMysql : insertJobsPostgres;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {import('./types.js').JobDefinition | string} job
|
|
50
|
+
* @returns {{ name: string, definition: import('./types.js').JobDefinition | null }}
|
|
51
|
+
*/
|
|
52
|
+
function resolveJob(job) {
|
|
53
|
+
if (isPlainObject(job) && typeof job.name === 'string') {
|
|
54
|
+
return { name: job.name, definition: job };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (typeof job !== 'string') {
|
|
58
|
+
throw new InvalidOptionsError('enqueue: the job must be a job definition or a job name');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const definition = findDefinition(job);
|
|
62
|
+
|
|
63
|
+
if (!definition && unknownJobs === 'reject') {
|
|
64
|
+
throw new UnknownJobError(`enqueue("${job}"): the job is not in "jobs" and not handled`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { name: job, definition };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {C} connection
|
|
72
|
+
* @param {import('./types.js').JobDefinition | string} job
|
|
73
|
+
* @param {readonly { payload: unknown, options?: import('./types.js').EnqueueOptions }[]} items
|
|
74
|
+
* @returns {Promise<import('./types.js').EnqueueResult[]>}
|
|
75
|
+
*/
|
|
76
|
+
async function insert(connection, job, items) {
|
|
77
|
+
const { name, definition } = resolveJob(job);
|
|
78
|
+
// Checks all the items before the INSERT: a bad item adds no job.
|
|
79
|
+
const newJobs = items.map(item => buildNewJob({
|
|
80
|
+
name,
|
|
81
|
+
definition,
|
|
82
|
+
defaults,
|
|
83
|
+
payload: item.payload,
|
|
84
|
+
options: item.options,
|
|
85
|
+
maxPayloadBytes,
|
|
86
|
+
}));
|
|
87
|
+
// A jobId repeated in the call: its first item is the job, the next ones are duplicates.
|
|
88
|
+
const firstById = new Map();
|
|
89
|
+
|
|
90
|
+
for (const newJob of newJobs) {
|
|
91
|
+
if (!firstById.has(newJob.id)) {
|
|
92
|
+
firstById.set(newJob.id, newJob);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!firstById.size) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const token = enqueueToken();
|
|
101
|
+
const created = await adapter.inTransaction(connection, async (executor) => {
|
|
102
|
+
const createdIds = await insertJobs(executor, tablePrefix, [...firstById.values()], token);
|
|
103
|
+
|
|
104
|
+
if (notifyChannel && createdIds.size) {
|
|
105
|
+
// One NOTIFY per transaction and name: PostgreSQL drops the repeated ones
|
|
106
|
+
await executor.query('SELECT pg_notify($1, $2)', [notifyChannel, name]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return createdIds;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return newJobs.map(newJob => ({
|
|
113
|
+
id: newJob.id,
|
|
114
|
+
created: created.has(newJob.id) && firstById.get(newJob.id) === newJob,
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
enqueue: async (connection, job, payload, options) => {
|
|
120
|
+
const [result] = await insert(connection, job, [{ payload, options }]);
|
|
121
|
+
|
|
122
|
+
return result;
|
|
123
|
+
},
|
|
124
|
+
enqueueMany: (connection, job, items) => {
|
|
125
|
+
if (!Array.isArray(items)) {
|
|
126
|
+
return Promise.reject(new InvalidOptionsError('enqueueMany: "items" must be an array'));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return insert(connection, job, items);
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** The longest stack kept with a job error: the error row and the failure job stay small. */
|
|
2
|
+
export const MAX_STACK_LENGTH = 8 * 1024;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {object} ErrorJson
|
|
6
|
+
* @property {string} name
|
|
7
|
+
* @property {string} message
|
|
8
|
+
* @property {string} [stack]
|
|
9
|
+
* @property {string} [code]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The JSON of an error, for `last_error` and the failure job. A thrown non-Error becomes its string.
|
|
14
|
+
* @param {unknown} error
|
|
15
|
+
* @returns {ErrorJson}
|
|
16
|
+
*/
|
|
17
|
+
export function toErrorJson(error) {
|
|
18
|
+
if (!(error instanceof Error)) {
|
|
19
|
+
return { name: 'Error', message: String(error) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { code } = /** @type {{ code?: unknown }} */ (error);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
name: error.name,
|
|
26
|
+
message: error.message,
|
|
27
|
+
...(error.stack && { stack: error.stack.slice(0, MAX_STACK_LENGTH) }),
|
|
28
|
+
...(typeof code === 'string' && { code }),
|
|
29
|
+
};
|
|
30
|
+
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
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
|
+
}
|
|
177
|
+
|
|
178
|
+
/** `migrate()` did not get the migration lock in its `lockTimeout`. */
|
|
179
|
+
export class MigrationLockTimeoutError extends SqlaneError {
|
|
180
|
+
/**
|
|
181
|
+
* @param {string} message
|
|
182
|
+
* @param {ErrorOptions} [options]
|
|
183
|
+
*/
|
|
184
|
+
constructor(message, options) {
|
|
185
|
+
super('MIGRATION_LOCK_TIMEOUT', message, options);
|
|
186
|
+
this.name = 'MigrationLockTimeoutError';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Hints between processes: "jobs of this name are ready". Only an optimization: a lost hint delays a job until the
|
|
2
|
+
// next poll, it never loses one.
|
|
3
|
+
|
|
4
|
+
/** At most one message per name in this window; the messages in it make one more at its end */
|
|
5
|
+
const THROTTLE_MS = 100;
|
|
6
|
+
/** The message after a release of mutex keys: every process probes all its names */
|
|
7
|
+
export const ANY_NAME = '*';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {object} HintChannel
|
|
11
|
+
* @property {(name: string) => void} publish Fire-and-forget: never throws, never waits.
|
|
12
|
+
* @property {(onMessage: (name: string) => void) => Promise<void>} listen
|
|
13
|
+
* @property {() => Promise<void>} stop Stops listening; drops the pending messages.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {object} params
|
|
18
|
+
* @param {import('./types.js').Hint | null} params.hint
|
|
19
|
+
* @param {string} params.channel
|
|
20
|
+
* @param {(operation: string, error: unknown) => void} params.reportError
|
|
21
|
+
* @returns {HintChannel}
|
|
22
|
+
*/
|
|
23
|
+
export function createHintChannel({ hint, channel, reportError }) {
|
|
24
|
+
/** @type {Map<string, { timer: NodeJS.Timeout, again: boolean }>} */
|
|
25
|
+
const windows = new Map();
|
|
26
|
+
/** @type {(() => Promise<void>) | null} */
|
|
27
|
+
let unsubscribe = null;
|
|
28
|
+
let stopped = false;
|
|
29
|
+
|
|
30
|
+
/** @param {string} name */
|
|
31
|
+
function send(name) {
|
|
32
|
+
if (!hint || stopped) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { publish } = hint;
|
|
37
|
+
|
|
38
|
+
// Not awaited: the enqueue does not wait for the hint
|
|
39
|
+
(async () => {
|
|
40
|
+
try {
|
|
41
|
+
await publish(channel, name);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
reportError('hint_publish', error);
|
|
44
|
+
}
|
|
45
|
+
})();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @param {string} name */
|
|
49
|
+
function openWindow(name) {
|
|
50
|
+
const timer = setTimeout(() => {
|
|
51
|
+
const window = windows.get(name);
|
|
52
|
+
|
|
53
|
+
windows.delete(name);
|
|
54
|
+
|
|
55
|
+
if (window?.again) {
|
|
56
|
+
send(name);
|
|
57
|
+
openWindow(name);
|
|
58
|
+
}
|
|
59
|
+
}, THROTTLE_MS);
|
|
60
|
+
|
|
61
|
+
timer.unref();
|
|
62
|
+
windows.set(name, { timer, again: false });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
publish: (name) => {
|
|
67
|
+
const window = windows.get(name);
|
|
68
|
+
|
|
69
|
+
if (window) {
|
|
70
|
+
window.again = true;
|
|
71
|
+
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
send(name);
|
|
76
|
+
openWindow(name);
|
|
77
|
+
},
|
|
78
|
+
listen: async (onMessage) => {
|
|
79
|
+
if (!hint) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
unsubscribe = await hint.subscribe(channel, (message) => {
|
|
84
|
+
try {
|
|
85
|
+
onMessage(message);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
reportError('hint_message', error);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
stop: async () => {
|
|
92
|
+
stopped = true;
|
|
93
|
+
|
|
94
|
+
for (const { timer } of windows.values()) {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
windows.clear();
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await unsubscribe?.();
|
|
102
|
+
} catch (error) {
|
|
103
|
+
reportError('hint_unsubscribe', error);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,2 +1,64 @@
|
|
|
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
|
+
MigrationLockTimeoutError,
|
|
15
|
+
MissingPayloadError,
|
|
16
|
+
PayloadTooLargeError,
|
|
17
|
+
RetryJobError,
|
|
18
|
+
ShutdownError,
|
|
19
|
+
SqlaneError,
|
|
20
|
+
UnknownJobError,
|
|
21
|
+
} from './errors.js';
|
|
22
|
+
export { getMigrations } from './migrations.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {import('./types.js').Adapter<C>} Adapter
|
|
26
|
+
* @template C
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {import('./types.js').Sqlane<C, L>} Sqlane
|
|
30
|
+
* @template C
|
|
31
|
+
* @template {string} L
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {import('./types.js').SqlaneOptions<C, L>} SqlaneOptions
|
|
35
|
+
* @template C
|
|
36
|
+
* @template {string} L
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* @typedef {import('./types.js').Jsonable<T>} Jsonable
|
|
40
|
+
* @template T
|
|
41
|
+
*/
|
|
42
|
+
/** @typedef {import('./types.js').Backoff} Backoff */
|
|
43
|
+
/** @typedef {import('./types.js').CloseOptions} CloseOptions */
|
|
44
|
+
/** @typedef {import('./types.js').DrainOptions} DrainOptions */
|
|
45
|
+
/** @typedef {import('./types.js').DrainResult} DrainResult */
|
|
46
|
+
/** @typedef {import('./types.js').Duration} Duration */
|
|
47
|
+
/** @typedef {import('./types.js').EnqueueOptions} EnqueueOptions */
|
|
48
|
+
/** @typedef {import('./types.js').EnqueueResult} EnqueueResult */
|
|
49
|
+
/** @typedef {import('./types.js').Executor} Executor */
|
|
50
|
+
/** @typedef {import('./types.js').FailedJobPayload} FailedJobPayload */
|
|
51
|
+
/** @typedef {import('./types.js').Handler} Handler */
|
|
52
|
+
/** @typedef {import('./types.js').HandlerContext} HandlerContext */
|
|
53
|
+
/** @typedef {import('./types.js').Hint} Hint */
|
|
54
|
+
/** @typedef {import('./types.js').JobDefaults} JobDefaults */
|
|
55
|
+
/** @typedef {import('./types.js').JobDefinition} JobDefinition */
|
|
56
|
+
/** @typedef {import('./types.js').JobInfo} JobInfo */
|
|
57
|
+
/** @typedef {import('./types.js').JobOptions} JobOptions */
|
|
58
|
+
/** @typedef {import('./types.js').JobStats} JobStats */
|
|
59
|
+
/** @typedef {import('./types.js').LaneOptions} LaneOptions */
|
|
60
|
+
/** @typedef {import('./types.js').Logger} Logger */
|
|
61
|
+
/** @typedef {import('./types.js').MigrateOptions} MigrateOptions */
|
|
62
|
+
/** @typedef {import('./types.js').Migration} Migration */
|
|
63
|
+
/** @typedef {import('./types.js').Retention} Retention */
|
|
64
|
+
/** @typedef {import('./types.js').SqlaneEvent} SqlaneEvent */
|