@sqlane/core 0.0.2 → 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.
@@ -0,0 +1,169 @@
1
+ // The handlers of an instance, their lanes and the known definitions.
2
+ import { parseDuration } from './duration.js';
3
+ import { ConfigError } from './errors.js';
4
+ import { checkDuration, checkInteger, checkObject, isPlainObject, JOB_NAME_REGEXP } from './validate.js';
5
+
6
+ /** @typedef {import('./types.js').JobDefinition} JobDefinition */
7
+
8
+ /**
9
+ * A concurrency limit in this process.
10
+ * @typedef {object} Lane
11
+ * @property {string} name
12
+ * @property {number} concurrency
13
+ * @property {number} running
14
+ */
15
+
16
+ /**
17
+ * @typedef {object} HandlerEntry
18
+ * @property {string} name
19
+ * @property {JobDefinition | null} definition
20
+ * @property {import('./types.js').Handler} handler
21
+ * @property {Lane} lane
22
+ * @property {number | null} timeoutMs Resolved: the handle() options, else the definition, else the defaults.
23
+ */
24
+
25
+ /**
26
+ * @typedef {object} Registry
27
+ * @property {(job: JobDefinition | string, handler: import('./types.js').Handler, options?: object) => void} handle
28
+ * @property {(name: string) => HandlerEntry | undefined} getEntry
29
+ * @property {() => HandlerEntry[]} entries
30
+ * @property {(name: string) => JobDefinition | null} findDefinition In `jobs`, else handled.
31
+ * @property {() => string[]} keyedNames The handled names whose definition has mutex keys.
32
+ * @property {() => void} validate Checks the handlers together: each failure job resolves and has none of its own.
33
+ */
34
+
35
+ /**
36
+ * @param {object} params
37
+ * @param {Map<string, JobDefinition>} params.jobsByName
38
+ * @param {unknown} params.lanes The `lanes` option.
39
+ * @param {number} params.concurrency The process concurrency.
40
+ * @param {number | null} params.defaultTimeoutMs
41
+ * @returns {Registry}
42
+ */
43
+ export function createRegistry({ jobsByName, lanes, concurrency, defaultTimeoutMs }) {
44
+ const scope = { where: 'createSqlane', ErrorClass: ConfigError };
45
+ /** @type {Map<string, Lane>} */
46
+ const namedLanes = new Map();
47
+ /** The handlers without a lane: the process concurrency only */
48
+ const defaultLane = { name: '', concurrency, running: 0 };
49
+ /** @type {Map<string, HandlerEntry>} */
50
+ const entriesByName = new Map();
51
+
52
+ if (lanes !== undefined && !isPlainObject(lanes)) {
53
+ throw new ConfigError('createSqlane: "lanes" must be an object of lane options');
54
+ }
55
+
56
+ for (const [name, options] of Object.entries(lanes ?? {})) {
57
+ const laneOptions = checkObject(options, ['concurrency'], `lane "${name}" options`, scope);
58
+
59
+ checkInteger(laneOptions.concurrency, `lane "${name}" concurrency`, scope);
60
+ namedLanes.set(name, { name, concurrency: Math.min(Number(laneOptions.concurrency), concurrency), running: 0 });
61
+ }
62
+
63
+ /** @type {Registry['findDefinition']} */
64
+ function findDefinition(name) {
65
+ return jobsByName.get(name) ?? entriesByName.get(name)?.definition ?? null;
66
+ }
67
+
68
+ /** @type {Registry['handle']} */
69
+ function handle(job, handler, options = {}) {
70
+ const name = isPlainObject(job) ? job.name : job;
71
+ const where = `handle(${JSON.stringify(name)})`;
72
+ const handleScope = { where, ErrorClass: ConfigError };
73
+
74
+ if (typeof name !== 'string' || !JOB_NAME_REGEXP.test(name)) {
75
+ throw new ConfigError(`${where}: the job must be a job definition or a job name`);
76
+ }
77
+
78
+ if (typeof handler !== 'function') {
79
+ throw new ConfigError(`${where}: the handler must be a function`);
80
+ }
81
+
82
+ const handleOptions = checkObject(options, ['concurrency', 'lane', 'timeout'], 'options', handleScope);
83
+
84
+ if (entriesByName.has(name)) {
85
+ throw new ConfigError(`${where}: the job is handled already`);
86
+ }
87
+
88
+ const known = jobsByName.get(name);
89
+ const definition = isPlainObject(job) ? /** @type {JobDefinition} */ (job) : known ?? null;
90
+
91
+ if (known && definition !== known) {
92
+ throw new ConfigError(`${where}: another definition of "${name}" is in "jobs"`);
93
+ }
94
+
95
+ if (handleOptions.lane !== undefined && handleOptions.concurrency !== undefined) {
96
+ throw new ConfigError(`${where}: "lane" or "concurrency", not both`);
97
+ }
98
+
99
+ /** @type {Lane} */
100
+ let lane = defaultLane;
101
+
102
+ if (handleOptions.lane !== undefined) {
103
+ const named = namedLanes.get(String(handleOptions.lane));
104
+
105
+ if (!named) {
106
+ throw new ConfigError(`${where}: unknown lane ${JSON.stringify(handleOptions.lane)}`);
107
+ }
108
+
109
+ lane = named;
110
+ } else if (handleOptions.concurrency !== undefined) {
111
+ checkInteger(handleOptions.concurrency, '"concurrency"', handleScope);
112
+ lane = {
113
+ name: `handler:${name}`,
114
+ concurrency: Math.min(Number(handleOptions.concurrency), concurrency),
115
+ running: 0,
116
+ };
117
+ }
118
+
119
+ if (handleOptions.timeout !== undefined) {
120
+ checkDuration(handleOptions.timeout, '"timeout"', handleScope, true);
121
+ }
122
+
123
+ const timeoutMs = parseDuration(handleOptions.timeout ?? definition?.timeout) ?? defaultTimeoutMs;
124
+
125
+ if (definition?.mutexKeys && timeoutMs === null) {
126
+ throw new ConfigError(
127
+ `${where}: a job with mutex keys needs a timeout (in handle(), the definition or "defaults")`,
128
+ );
129
+ }
130
+
131
+ entriesByName.set(name, { name, definition, handler, lane, timeoutMs });
132
+ }
133
+
134
+ /** @type {Registry['validate']} */
135
+ function validate() {
136
+ for (const entry of entriesByName.values()) {
137
+ const failureJob = entry.definition?.failureJob;
138
+
139
+ if (failureJob === undefined) {
140
+ continue;
141
+ }
142
+
143
+ const where = `handle(${JSON.stringify(entry.name)})`;
144
+ const known = typeof failureJob !== 'string' || jobsByName.has(failureJob) || entriesByName.has(failureJob);
145
+
146
+ if (!known) {
147
+ throw new ConfigError(`${where}: the failure job "${failureJob}" is not in "jobs" and not handled`);
148
+ }
149
+
150
+ // Handled by name only: no definition, so no failure job of its own
151
+ const failureDefinition = typeof failureJob === 'string' ? findDefinition(failureJob) : failureJob;
152
+
153
+ if (failureDefinition?.failureJob !== undefined) {
154
+ throw new ConfigError(`${where}: the failure job "${failureDefinition?.name}" has a failure job of its own`);
155
+ }
156
+ }
157
+ }
158
+
159
+ return {
160
+ handle,
161
+ getEntry: name => entriesByName.get(name),
162
+ entries: () => [...entriesByName.values()],
163
+ findDefinition,
164
+ keyedNames: () => [...entriesByName.values()]
165
+ .filter(entry => entry.definition?.mutexKeys)
166
+ .map(entry => entry.name),
167
+ validate,
168
+ };
169
+ }