@c9up/bay 0.1.8 → 0.1.10
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 +1 -1
- package/dist/drivers/RedisDriver.d.ts +8 -1
- package/dist/drivers/RedisDriver.d.ts.map +1 -1
- package/dist/drivers/RedisDriver.js +104 -34
- package/dist/drivers/RedisDriver.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/quasar.d.ts +18 -0
- package/dist/quasar.d.ts.map +1 -0
- package/dist/quasar.js +64 -0
- package/dist/quasar.js.map +1 -0
- package/dist/services/main.d.ts.map +1 -1
- package/dist/services/main.js +9 -0
- package/dist/services/main.js.map +1 -1
- package/package.json +7 -2
- package/src/drivers/RedisDriver.ts +115 -36
- package/src/index.ts +1 -0
- package/src/quasar.ts +84 -0
- package/src/services/main.ts +9 -0
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ ream configure @c9up/bay
|
|
|
13
13
|
|
|
14
14
|
## Usage
|
|
15
15
|
|
|
16
|
-
Register the provider in your app, then configure it under `config/bay.ts
|
|
16
|
+
Register the provider in your app, then configure it under `config/queue.ts` — the provider reads `config.get('queue')`, so a `config/bay.ts` would be loaded under the key `bay` and never seen:
|
|
17
17
|
|
|
18
18
|
```ts
|
|
19
19
|
// reamrc.ts
|
|
@@ -29,9 +29,16 @@ export interface RedisClient {
|
|
|
29
29
|
set(key: string, value: string, ...args: string[]): Promise<string | null>;
|
|
30
30
|
get(key: string): Promise<string | null>;
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Where the client comes from. A resolver is what lets a queue name its
|
|
34
|
+
* connection (`quasarConnection("jobs")`) instead of being handed a client:
|
|
35
|
+
* the driver is built synchronously, the first command that needs the client
|
|
36
|
+
* is not.
|
|
37
|
+
*/
|
|
38
|
+
export type RedisClientSource = RedisClient | (() => RedisClient | Promise<RedisClient>);
|
|
32
39
|
export declare class RedisDriver implements QueueDriver {
|
|
33
40
|
#private;
|
|
34
|
-
constructor(
|
|
41
|
+
constructor(source: RedisClientSource, options?: {
|
|
35
42
|
prefix?: string;
|
|
36
43
|
visibilityTimeoutMs?: number;
|
|
37
44
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RedisDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/RedisDriver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAE3D,MAAM,WAAW,WAAW;IAC3B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACzD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1C,KAAK,CAAC,CACL,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,GAAG,OAAO,EACtB,EAAE,EAAE,MAAM,GAAG,OAAO,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1B,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnC,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACpE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC3E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACzC;AAcD,qBAAa,WAAY,YAAW,WAAW;;
|
|
1
|
+
{"version":3,"file":"RedisDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/RedisDriver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAE3D,MAAM,WAAW,WAAW;IAC3B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACzD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1C,KAAK,CAAC,CACL,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,GAAG,OAAO,EACtB,EAAE,EAAE,MAAM,GAAG,OAAO,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1B,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnC,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACpE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC3E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACzC;AAcD;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAC1B,WAAW,GACX,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;AA6B9C,qBAAa,WAAY,YAAW,WAAW;;gBAsC7C,MAAM,EAAE,iBAAiB,EACzB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;KAAE;IA8BtD,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAK7B,GAAG,IAAI,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC;IAuC1B,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAMjC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS5C,KAAK,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ9B,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IA8D/B,MAAM,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAexB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;CAI7B"}
|
|
@@ -27,13 +27,79 @@ function isValidJob(obj) {
|
|
|
27
27
|
typeof j.maxAttempts === "number" &&
|
|
28
28
|
typeof j.status === "string");
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* LMOVE (Redis 6.2+) is what makes pop() atomic. Warned once, when the client
|
|
32
|
+
* is resolved rather than at construction — a driver that names its connection
|
|
33
|
+
* has no client to inspect yet.
|
|
34
|
+
*/
|
|
35
|
+
const warned = new WeakSet();
|
|
36
|
+
function warnWithoutLmove(client) {
|
|
37
|
+
if (typeof client.lmove === "function" || warned.has(client))
|
|
38
|
+
return;
|
|
39
|
+
warned.add(client);
|
|
40
|
+
console.warn("[bay] RedisDriver: client lacks LMOVE (Redis <6.2). pop() falls back to " +
|
|
41
|
+
"a non-atomic lpop+rpush, downgrading delivery from at-least-once to " +
|
|
42
|
+
"at-most-once — a crash between the two commands loses the in-flight job.");
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A key prefix that ends in a separator.
|
|
46
|
+
*
|
|
47
|
+
* `:` is Redis's conventional namespace separator; a prefix already ending in
|
|
48
|
+
* one of the usual separators is left alone.
|
|
49
|
+
*/
|
|
50
|
+
function withSeparator(prefix) {
|
|
51
|
+
if (prefix.length === 0)
|
|
52
|
+
return prefix;
|
|
53
|
+
return /[:.\-_/]$/.test(prefix) ? prefix : `${prefix}:`;
|
|
54
|
+
}
|
|
30
55
|
export class RedisDriver {
|
|
31
|
-
#
|
|
56
|
+
#source;
|
|
57
|
+
#resolved;
|
|
58
|
+
#pending;
|
|
32
59
|
#prefix;
|
|
33
60
|
#visibilityTimeout;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
61
|
+
/**
|
|
62
|
+
* The client, resolved once. Two workers racing on a cold queue must not
|
|
63
|
+
* each open their own connection, so the in-flight promise is shared.
|
|
64
|
+
*/
|
|
65
|
+
async #client() {
|
|
66
|
+
if (this.#resolved)
|
|
67
|
+
return this.#resolved;
|
|
68
|
+
if (typeof this.#source !== "function") {
|
|
69
|
+
this.#resolved = this.#source;
|
|
70
|
+
warnWithoutLmove(this.#resolved);
|
|
71
|
+
return this.#resolved;
|
|
72
|
+
}
|
|
73
|
+
if (!this.#pending) {
|
|
74
|
+
const resolver = this.#source;
|
|
75
|
+
this.#pending = Promise.resolve(resolver())
|
|
76
|
+
.then((client) => {
|
|
77
|
+
this.#resolved = client;
|
|
78
|
+
warnWithoutLmove(client);
|
|
79
|
+
return client;
|
|
80
|
+
})
|
|
81
|
+
// Cleared on failure too. Clearing only on success left the
|
|
82
|
+
// REJECTED promise cached forever, so one transient outage at
|
|
83
|
+
// startup broke every later call for the life of the process —
|
|
84
|
+
// a permanent failure with no error of its own to explain it.
|
|
85
|
+
.finally(() => {
|
|
86
|
+
this.#pending = undefined;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return this.#pending;
|
|
90
|
+
}
|
|
91
|
+
constructor(source, options) {
|
|
92
|
+
this.#source = source;
|
|
93
|
+
// A client handed in directly can be checked now, so the warning keeps
|
|
94
|
+
// landing at construction as it always did. A named connection has no
|
|
95
|
+
// client yet — it is checked when the connection resolves.
|
|
96
|
+
if (typeof source !== "function")
|
|
97
|
+
warnWithoutLmove(source);
|
|
98
|
+
// Normalised rather than documented: every key is built by concatenation
|
|
99
|
+
// (`${prefix}pending`), so a prefix without a trailing separator yields
|
|
100
|
+
// "myapppending" — unreadable, and able to collide with a neighbouring
|
|
101
|
+
// prefix. Nothing warned, because nothing failed.
|
|
102
|
+
this.#prefix = withSeparator(options?.prefix ?? "queue:");
|
|
37
103
|
const visibilityTimeout = options?.visibilityTimeoutMs ?? 30_000;
|
|
38
104
|
// A non-positive / non-integer timeout makes pop()'s `SET … PX <ms>` fail
|
|
39
105
|
// on a real Redis; the catch then removes the job from `processing` and
|
|
@@ -43,28 +109,25 @@ export class RedisDriver {
|
|
|
43
109
|
throw new Error(`[bay] RedisDriver visibilityTimeoutMs must be a positive integer (ms), got ${visibilityTimeout}`);
|
|
44
110
|
}
|
|
45
111
|
this.#visibilityTimeout = visibilityTimeout;
|
|
46
|
-
if (typeof client.lmove !== "function") {
|
|
47
|
-
console.warn("[bay] RedisDriver: client lacks LMOVE (Redis <6.2). pop() falls back to " +
|
|
48
|
-
"a non-atomic lpop+rpush, downgrading delivery from at-least-once to " +
|
|
49
|
-
"at-most-once — a crash between the two commands loses the in-flight job.");
|
|
50
|
-
}
|
|
51
112
|
}
|
|
52
113
|
#pendingKey = () => `${this.#prefix}pending`;
|
|
53
114
|
#processingKey = () => `${this.#prefix}processing`;
|
|
54
115
|
#failedKey = () => `${this.#prefix}failed`;
|
|
55
116
|
#leaseKey = (jobId) => `${this.#prefix}lease:${jobId}`;
|
|
56
117
|
async push(job) {
|
|
57
|
-
await this.#client
|
|
118
|
+
const client = await this.#client();
|
|
119
|
+
await client.rpush(this.#pendingKey(), JSON.stringify(job));
|
|
58
120
|
}
|
|
59
121
|
async pop() {
|
|
122
|
+
const client = await this.#client();
|
|
60
123
|
let raw = null;
|
|
61
|
-
if (
|
|
62
|
-
raw = await
|
|
124
|
+
if (client.lmove) {
|
|
125
|
+
raw = await client.lmove(this.#pendingKey(), this.#processingKey(), "LEFT", "RIGHT");
|
|
63
126
|
}
|
|
64
127
|
else {
|
|
65
|
-
raw = await
|
|
128
|
+
raw = await client.lpop(this.#pendingKey());
|
|
66
129
|
if (raw)
|
|
67
|
-
await
|
|
130
|
+
await client.rpush(this.#processingKey(), raw);
|
|
68
131
|
}
|
|
69
132
|
if (!raw)
|
|
70
133
|
return null;
|
|
@@ -74,36 +137,40 @@ export class RedisDriver {
|
|
|
74
137
|
// Malformed payload — purge from `processing` so it can't sit
|
|
75
138
|
// there indefinitely as a poison pill. recoverStale() also
|
|
76
139
|
// catches survivors but pop()'s own move is the primary path.
|
|
77
|
-
await
|
|
140
|
+
await client.lrem(this.#processingKey(), 1, raw);
|
|
78
141
|
return null;
|
|
79
142
|
}
|
|
80
|
-
await
|
|
143
|
+
await client.set(this.#leaseKey(parsed.id), raw, "PX", String(this.#visibilityTimeout));
|
|
81
144
|
return parsed;
|
|
82
145
|
}
|
|
83
146
|
catch {
|
|
84
|
-
await
|
|
147
|
+
await client.lrem(this.#processingKey(), 1, raw);
|
|
85
148
|
return null;
|
|
86
149
|
}
|
|
87
150
|
}
|
|
88
151
|
async complete(job) {
|
|
152
|
+
const client = await this.#client();
|
|
89
153
|
await this.#removeFromProcessing(job);
|
|
90
|
-
await
|
|
154
|
+
await client.del(this.#leaseKey(job.id));
|
|
91
155
|
}
|
|
92
156
|
async fail(job, error) {
|
|
157
|
+
const client = await this.#client();
|
|
93
158
|
await this.#removeFromProcessing(job);
|
|
94
|
-
await
|
|
159
|
+
await client.del(this.#leaseKey(job.id));
|
|
95
160
|
job.error = error;
|
|
96
161
|
job.status = "failed";
|
|
97
|
-
await
|
|
162
|
+
await client.rpush(this.#failedKey(), JSON.stringify(job));
|
|
98
163
|
}
|
|
99
164
|
async retry(job) {
|
|
165
|
+
const client = await this.#client();
|
|
100
166
|
await this.#removeFromProcessing(job);
|
|
101
|
-
await
|
|
167
|
+
await client.del(this.#leaseKey(job.id));
|
|
102
168
|
job.status = "pending";
|
|
103
|
-
await
|
|
169
|
+
await client.rpush(this.#pendingKey(), JSON.stringify(job));
|
|
104
170
|
}
|
|
105
171
|
async recoverStale() {
|
|
106
|
-
const
|
|
172
|
+
const client = await this.#client();
|
|
173
|
+
const processing = await client.lrange(this.#processingKey(), 0, -1);
|
|
107
174
|
let recovered = 0;
|
|
108
175
|
for (const raw of processing) {
|
|
109
176
|
let parsed;
|
|
@@ -113,18 +180,18 @@ export class RedisDriver {
|
|
|
113
180
|
catch {
|
|
114
181
|
// Malformed JSON would otherwise sit in processing forever —
|
|
115
182
|
// LREM purges it so the queue makes progress.
|
|
116
|
-
await
|
|
183
|
+
await client.lrem(this.#processingKey(), 1, raw);
|
|
117
184
|
continue;
|
|
118
185
|
}
|
|
119
186
|
if (!isValidJob(parsed)) {
|
|
120
|
-
await
|
|
187
|
+
await client.lrem(this.#processingKey(), 1, raw);
|
|
121
188
|
continue;
|
|
122
189
|
}
|
|
123
|
-
const lease = await
|
|
190
|
+
const lease = await client.get(this.#leaseKey(parsed.id));
|
|
124
191
|
if (lease === null) {
|
|
125
|
-
await
|
|
192
|
+
await client.lrem(this.#processingKey(), 1, raw);
|
|
126
193
|
parsed.status = "pending";
|
|
127
|
-
await
|
|
194
|
+
await client.rpush(this.#pendingKey(), JSON.stringify(parsed));
|
|
128
195
|
recovered++;
|
|
129
196
|
}
|
|
130
197
|
}
|
|
@@ -140,15 +207,16 @@ export class RedisDriver {
|
|
|
140
207
|
* the lease has expired (e.g. recoverStale already handled it).
|
|
141
208
|
*/
|
|
142
209
|
async #removeFromProcessing(job) {
|
|
143
|
-
const
|
|
210
|
+
const client = await this.#client();
|
|
211
|
+
const stored = await client.get(this.#leaseKey(job.id));
|
|
144
212
|
if (stored !== null) {
|
|
145
|
-
const removed = await
|
|
213
|
+
const removed = await client.lrem(this.#processingKey(), 1, stored);
|
|
146
214
|
if (removed > 0)
|
|
147
215
|
return;
|
|
148
216
|
}
|
|
149
217
|
// Lease missing or already-LREM'd entry not found — best-effort scan
|
|
150
218
|
// matches by job id and removes the actual stored representation.
|
|
151
|
-
const items = await
|
|
219
|
+
const items = await client.lrange(this.#processingKey(), 0, -1);
|
|
152
220
|
for (const item of items) {
|
|
153
221
|
let parsed;
|
|
154
222
|
try {
|
|
@@ -158,13 +226,14 @@ export class RedisDriver {
|
|
|
158
226
|
continue;
|
|
159
227
|
}
|
|
160
228
|
if (isValidJob(parsed) && parsed.id === job.id) {
|
|
161
|
-
await
|
|
229
|
+
await client.lrem(this.#processingKey(), 1, item);
|
|
162
230
|
return;
|
|
163
231
|
}
|
|
164
232
|
}
|
|
165
233
|
}
|
|
166
234
|
async failed() {
|
|
167
|
-
const
|
|
235
|
+
const client = await this.#client();
|
|
236
|
+
const raws = await client.lrange(this.#failedKey(), 0, -1);
|
|
168
237
|
return raws
|
|
169
238
|
.map((r) => {
|
|
170
239
|
try {
|
|
@@ -178,7 +247,8 @@ export class RedisDriver {
|
|
|
178
247
|
.filter((j) => j !== null);
|
|
179
248
|
}
|
|
180
249
|
async size() {
|
|
181
|
-
|
|
250
|
+
const client = await this.#client();
|
|
251
|
+
return client.llen(this.#pendingKey());
|
|
182
252
|
}
|
|
183
253
|
}
|
|
184
254
|
//# sourceMappingURL=RedisDriver.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RedisDriver.js","sourceRoot":"","sources":["../../src/drivers/RedisDriver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAqBH,SAAS,UAAU,CAAC,GAAY;IAC/B,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC1D,MAAM,CAAC,GAAG,GAA8B,CAAC;IACzC,OAAO,CACN,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ;QACxB,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ;QAC1B,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ;QAC9B,OAAO,CAAC,CAAC,WAAW,KAAK,QAAQ;QACjC,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ,CAC5B,CAAC;AACH,CAAC;AAED,MAAM,OAAO,WAAW;IACvB,OAAO,
|
|
1
|
+
{"version":3,"file":"RedisDriver.js","sourceRoot":"","sources":["../../src/drivers/RedisDriver.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -17,4 +17,5 @@ import type { BayProviderConfig } from "./BayProvider.js";
|
|
|
17
17
|
* parity. Identity at runtime; the generic preserves literal types for inference.
|
|
18
18
|
*/
|
|
19
19
|
export declare function defineConfig<T extends BayProviderConfig>(config: T): T;
|
|
20
|
+
export { quasarConnection } from "./quasar.js";
|
|
20
21
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,YAAY,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,YAAY,EAAE,GAAG,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D;;;GAGG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,iBAAiB,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAEtE"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,YAAY,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,YAAY,EAAE,GAAG,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D;;;GAGG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,iBAAiB,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAEtE;AACD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAEzD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEvD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAIjD;;;GAGG;AACH,MAAM,UAAU,YAAY,CAA8B,MAAS;IAClE,OAAO,MAAM,CAAC;AACf,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAEzD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEvD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAIjD;;;GAGG;AACH,MAAM,UAAU,YAAY,CAA8B,MAAS;IAClE,OAAO,MAAM,CAAC;AACf,CAAC;AACD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/quasar.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving a Redis connection by name, from `@c9up/quasar`.
|
|
3
|
+
*
|
|
4
|
+
* Bay does not depend on quasar: it is an optional peer, and this module
|
|
5
|
+
* never imports it statically. The specifier is built at runtime so the
|
|
6
|
+
* TypeScript build stays free of it too — a hard type import would make echo
|
|
7
|
+
* unbuildable for anyone who queues in memory.
|
|
8
|
+
*
|
|
9
|
+
* The shape is checked before use rather than asserted, the same way bay
|
|
10
|
+
* duck-types its host framework.
|
|
11
|
+
*/
|
|
12
|
+
import type { RedisClient } from "./drivers/RedisDriver.js";
|
|
13
|
+
/**
|
|
14
|
+
* A resolver for `new RedisDriver(quasarConnection("jobs"))` — quasar is
|
|
15
|
+
* loaded on the first queue command, not at config time.
|
|
16
|
+
*/
|
|
17
|
+
export declare function quasarConnection(name?: string): () => Promise<RedisClient>;
|
|
18
|
+
//# sourceMappingURL=quasar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quasar.d.ts","sourceRoot":"","sources":["../src/quasar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAoC5D;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,CA+B1E"}
|
package/dist/quasar.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving a Redis connection by name, from `@c9up/quasar`.
|
|
3
|
+
*
|
|
4
|
+
* Bay does not depend on quasar: it is an optional peer, and this module
|
|
5
|
+
* never imports it statically. The specifier is built at runtime so the
|
|
6
|
+
* TypeScript build stays free of it too — a hard type import would make echo
|
|
7
|
+
* unbuildable for anyone who queues in memory.
|
|
8
|
+
*
|
|
9
|
+
* The shape is checked before use rather than asserted, the same way bay
|
|
10
|
+
* duck-types its host framework.
|
|
11
|
+
*/
|
|
12
|
+
function isConnectionSource(value) {
|
|
13
|
+
return (typeof value === "object" &&
|
|
14
|
+
value !== null &&
|
|
15
|
+
typeof Reflect.get(value, "connection") === "function");
|
|
16
|
+
}
|
|
17
|
+
function isRedisClient(value) {
|
|
18
|
+
if (typeof value !== "object" || value === null)
|
|
19
|
+
return false;
|
|
20
|
+
// The commands this driver actually issues — every non-optional member of
|
|
21
|
+
// `RedisClient`. A connection missing one would fail on the first job push,
|
|
22
|
+
// far from the cause. `lmove` is deliberately absent: the driver declares it
|
|
23
|
+
// optional and falls back when it is not there.
|
|
24
|
+
const required = [
|
|
25
|
+
"rpush",
|
|
26
|
+
"lpop",
|
|
27
|
+
"lrem",
|
|
28
|
+
"llen",
|
|
29
|
+
"lrange",
|
|
30
|
+
"del",
|
|
31
|
+
"set",
|
|
32
|
+
"get",
|
|
33
|
+
];
|
|
34
|
+
return required.every((name) => typeof Reflect.get(value, name) === "function");
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A resolver for `new RedisDriver(quasarConnection("jobs"))` — quasar is
|
|
38
|
+
* loaded on the first queue command, not at config time.
|
|
39
|
+
*/
|
|
40
|
+
export function quasarConnection(name) {
|
|
41
|
+
return async () => {
|
|
42
|
+
const specifier = "@c9up/quasar/services/main";
|
|
43
|
+
let loaded;
|
|
44
|
+
try {
|
|
45
|
+
loaded = await import(/* @vite-ignore */ specifier);
|
|
46
|
+
}
|
|
47
|
+
catch (cause) {
|
|
48
|
+
throw new Error(`Bay: the "${name ?? "default"}" queue asks for a quasar connection, but @c9up/quasar is not installed.\n` +
|
|
49
|
+
" pnpm add @c9up/quasar", { cause });
|
|
50
|
+
}
|
|
51
|
+
const manager = isConnectionSource(loaded)
|
|
52
|
+
? loaded
|
|
53
|
+
: Reflect.get(Object(loaded), "default");
|
|
54
|
+
if (!isConnectionSource(manager)) {
|
|
55
|
+
throw new Error("Bay: @c9up/quasar/services/main did not expose a connection() manager");
|
|
56
|
+
}
|
|
57
|
+
const connection = manager.connection(name);
|
|
58
|
+
if (!isRedisClient(connection)) {
|
|
59
|
+
throw new Error(`Bay: quasar connection "${name ?? "default"}" does not carry the commands this queue needs`);
|
|
60
|
+
}
|
|
61
|
+
return connection;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=quasar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quasar.js","sourceRoot":"","sources":["../src/quasar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AASH,SAAS,kBAAkB,CAAC,KAAc;IACzC,OAAO,CACN,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,YAAY,CAAC,KAAK,UAAU,CACtD,CAAC;AACH,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACpC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9D,0EAA0E;IAC1E,4EAA4E;IAC5E,6EAA6E;IAC7E,gDAAgD;IAChD,MAAM,QAAQ,GAAG;QAChB,OAAO;QACP,MAAM;QACN,MAAM;QACN,MAAM;QACN,QAAQ;QACR,KAAK;QACL,KAAK;QACL,KAAK;KACL,CAAC;IACF,OAAO,QAAQ,CAAC,KAAK,CACpB,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,UAAU,CACxD,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAa;IAC7C,OAAO,KAAK,IAAI,EAAE;QACjB,MAAM,SAAS,GAAG,4BAA4B,CAAC;QAC/C,IAAI,MAAe,CAAC;QACpB,IAAI,CAAC;YACJ,MAAM,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC;QACrD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CACd,aAAa,IAAI,IAAI,SAAS,4EAA4E;gBACzG,yBAAyB,EAC1B,EAAE,KAAK,EAAE,CACT,CAAC;QACH,CAAC;QAED,MAAM,OAAO,GAAG,kBAAkB,CAAC,MAAM,CAAC;YACzC,CAAC,CAAC,MAAM;YACR,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC,CAAC;QAC1C,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CACd,uEAAuE,CACvE,CAAC;QACH,CAAC;QAED,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QAC5C,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,EAAE,CAAC;YAChC,MAAM,IAAI,KAAK,CACd,2BAA2B,IAAI,IAAI,SAAS,gDAAgD,CAC5F,CAAC;QACH,CAAC;QACD,OAAO,UAAU,CAAC;IACnB,CAAC,CAAC;AACH,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/services/main.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAIvD,0EAA0E;AAC1E,wBAAgB,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAElD;AAED,8DAA8D;AAC9D,wBAAgB,QAAQ,IAAI,YAAY,GAAG,SAAS,CAEnD;AAED,QAAA,MAAM,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/services/main.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAIvD,0EAA0E;AAC1E,wBAAgB,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAElD;AAED,8DAA8D;AAC9D,wBAAgB,QAAQ,IAAI,YAAY,GAAG,SAAS,CAEnD;AAED,QAAA,MAAM,KAAK,EAAE,YAoBX,CAAC;AAEH,eAAe,KAAK,CAAC"}
|
package/dist/services/main.js
CHANGED
|
@@ -23,6 +23,15 @@ export function getQueue() {
|
|
|
23
23
|
}
|
|
24
24
|
const queue = new Proxy({}, {
|
|
25
25
|
get(_target, prop) {
|
|
26
|
+
// A module loader inspects what it imports before anyone uses it: it reads
|
|
27
|
+
// `then` to decide whether the namespace is thenable, and various symbols
|
|
28
|
+
// for interop and formatting. Throwing on those turns a plain
|
|
29
|
+
// `import { setX } from ".../services/main"` into a crash at import time,
|
|
30
|
+
// far from any real use. They are not members of what this stands in for,
|
|
31
|
+
// so answer undefined and let a genuine access be the one that reports.
|
|
32
|
+
if (typeof prop === "symbol" || prop === "then") {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
26
35
|
if (!instance) {
|
|
27
36
|
throw new Error("[bay] QueueManager singleton accessed before BayProvider.boot() ran " +
|
|
28
37
|
"or `setQueue(myQueue)` was called. Wire one of them first.");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/services/main.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,IAAI,QAAkC,CAAC;AAEvC,0EAA0E;AAC1E,MAAM,UAAU,QAAQ,CAAC,KAAmB;IAC3C,QAAQ,GAAG,KAAK,CAAC;AAClB,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,QAAQ;IACvB,OAAO,QAAQ,CAAC;AACjB,CAAC;AAED,MAAM,KAAK,GAAiB,IAAI,KAAK,CAAC,EAAkB,EAAE;IACzD,GAAG,CAAC,OAAO,EAAE,IAAI;QAChB,IAAI,CAAC,QAAQ,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CACd,sEAAsE;gBACrE,4DAA4D,CAC7D,CAAC;QACH,CAAC;QACD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;QACpD,OAAO,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IACnE,CAAC;CACD,CAAC,CAAC;AAEH,eAAe,KAAK,CAAC"}
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/services/main.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,IAAI,QAAkC,CAAC;AAEvC,0EAA0E;AAC1E,MAAM,UAAU,QAAQ,CAAC,KAAmB;IAC3C,QAAQ,GAAG,KAAK,CAAC;AAClB,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,QAAQ;IACvB,OAAO,QAAQ,CAAC;AACjB,CAAC;AAED,MAAM,KAAK,GAAiB,IAAI,KAAK,CAAC,EAAkB,EAAE;IACzD,GAAG,CAAC,OAAO,EAAE,IAAI;QAChB,2EAA2E;QAC3E,0EAA0E;QAC1E,8DAA8D;QAC9D,0EAA0E;QAC1E,0EAA0E;QAC1E,wEAAwE;QACxE,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;YACjD,OAAO,SAAS,CAAC;QAClB,CAAC;QACD,IAAI,CAAC,QAAQ,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CACd,sEAAsE;gBACrE,4DAA4D,CAC7D,CAAC;QACH,CAAC;QACD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;QACpD,OAAO,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IACnE,CAAC;CACD,CAAC,CAAC;AAEH,eAAe,KAAK,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c9up/bay",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Queue — pluggable job-queue contract with memory + Redis drivers for the Ream framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -25,10 +25,12 @@
|
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@c9up/ream": "^0.1.0"
|
|
28
|
+
"@c9up/ream": "^0.1.0",
|
|
29
|
+
"@c9up/quasar": "^0.1.0"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"@biomejs/biome": "^2.4.10",
|
|
33
|
+
"@c9up/quasar": "^0.1.0",
|
|
32
34
|
"@types/node": "^22.19.15",
|
|
33
35
|
"typescript": "^6.0.2",
|
|
34
36
|
"vitest": "^4.1.2"
|
|
@@ -48,6 +50,9 @@
|
|
|
48
50
|
"peerDependenciesMeta": {
|
|
49
51
|
"@c9up/ream": {
|
|
50
52
|
"optional": true
|
|
53
|
+
},
|
|
54
|
+
"@c9up/quasar": {
|
|
55
|
+
"optional": true
|
|
51
56
|
}
|
|
52
57
|
},
|
|
53
58
|
"repository": {
|
|
@@ -49,17 +49,94 @@ function isValidJob(obj: unknown): obj is Job {
|
|
|
49
49
|
);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Where the client comes from. A resolver is what lets a queue name its
|
|
54
|
+
* connection (`quasarConnection("jobs")`) instead of being handed a client:
|
|
55
|
+
* the driver is built synchronously, the first command that needs the client
|
|
56
|
+
* is not.
|
|
57
|
+
*/
|
|
58
|
+
export type RedisClientSource =
|
|
59
|
+
| RedisClient
|
|
60
|
+
| (() => RedisClient | Promise<RedisClient>);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* LMOVE (Redis 6.2+) is what makes pop() atomic. Warned once, when the client
|
|
64
|
+
* is resolved rather than at construction — a driver that names its connection
|
|
65
|
+
* has no client to inspect yet.
|
|
66
|
+
*/
|
|
67
|
+
const warned = new WeakSet<object>();
|
|
68
|
+
function warnWithoutLmove(client: RedisClient): void {
|
|
69
|
+
if (typeof client.lmove === "function" || warned.has(client)) return;
|
|
70
|
+
warned.add(client);
|
|
71
|
+
console.warn(
|
|
72
|
+
"[bay] RedisDriver: client lacks LMOVE (Redis <6.2). pop() falls back to " +
|
|
73
|
+
"a non-atomic lpop+rpush, downgrading delivery from at-least-once to " +
|
|
74
|
+
"at-most-once — a crash between the two commands loses the in-flight job.",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A key prefix that ends in a separator.
|
|
80
|
+
*
|
|
81
|
+
* `:` is Redis's conventional namespace separator; a prefix already ending in
|
|
82
|
+
* one of the usual separators is left alone.
|
|
83
|
+
*/
|
|
84
|
+
function withSeparator(prefix: string): string {
|
|
85
|
+
if (prefix.length === 0) return prefix;
|
|
86
|
+
return /[:.\-_/]$/.test(prefix) ? prefix : `${prefix}:`;
|
|
87
|
+
}
|
|
88
|
+
|
|
52
89
|
export class RedisDriver implements QueueDriver {
|
|
53
|
-
#
|
|
90
|
+
#source: RedisClientSource;
|
|
91
|
+
#resolved: RedisClient | undefined;
|
|
92
|
+
#pending: Promise<RedisClient> | undefined;
|
|
54
93
|
#prefix: string;
|
|
55
94
|
#visibilityTimeout: number;
|
|
56
95
|
|
|
96
|
+
/**
|
|
97
|
+
* The client, resolved once. Two workers racing on a cold queue must not
|
|
98
|
+
* each open their own connection, so the in-flight promise is shared.
|
|
99
|
+
*/
|
|
100
|
+
async #client(): Promise<RedisClient> {
|
|
101
|
+
if (this.#resolved) return this.#resolved;
|
|
102
|
+
if (typeof this.#source !== "function") {
|
|
103
|
+
this.#resolved = this.#source;
|
|
104
|
+
warnWithoutLmove(this.#resolved);
|
|
105
|
+
return this.#resolved;
|
|
106
|
+
}
|
|
107
|
+
if (!this.#pending) {
|
|
108
|
+
const resolver = this.#source;
|
|
109
|
+
this.#pending = Promise.resolve(resolver())
|
|
110
|
+
.then((client) => {
|
|
111
|
+
this.#resolved = client;
|
|
112
|
+
warnWithoutLmove(client);
|
|
113
|
+
return client;
|
|
114
|
+
})
|
|
115
|
+
// Cleared on failure too. Clearing only on success left the
|
|
116
|
+
// REJECTED promise cached forever, so one transient outage at
|
|
117
|
+
// startup broke every later call for the life of the process —
|
|
118
|
+
// a permanent failure with no error of its own to explain it.
|
|
119
|
+
.finally(() => {
|
|
120
|
+
this.#pending = undefined;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return this.#pending;
|
|
124
|
+
}
|
|
125
|
+
|
|
57
126
|
constructor(
|
|
58
|
-
|
|
127
|
+
source: RedisClientSource,
|
|
59
128
|
options?: { prefix?: string; visibilityTimeoutMs?: number },
|
|
60
129
|
) {
|
|
61
|
-
this.#
|
|
62
|
-
|
|
130
|
+
this.#source = source;
|
|
131
|
+
// A client handed in directly can be checked now, so the warning keeps
|
|
132
|
+
// landing at construction as it always did. A named connection has no
|
|
133
|
+
// client yet — it is checked when the connection resolves.
|
|
134
|
+
if (typeof source !== "function") warnWithoutLmove(source);
|
|
135
|
+
// Normalised rather than documented: every key is built by concatenation
|
|
136
|
+
// (`${prefix}pending`), so a prefix without a trailing separator yields
|
|
137
|
+
// "myapppending" — unreadable, and able to collide with a neighbouring
|
|
138
|
+
// prefix. Nothing warned, because nothing failed.
|
|
139
|
+
this.#prefix = withSeparator(options?.prefix ?? "queue:");
|
|
63
140
|
const visibilityTimeout = options?.visibilityTimeoutMs ?? 30_000;
|
|
64
141
|
// A non-positive / non-integer timeout makes pop()'s `SET … PX <ms>` fail
|
|
65
142
|
// on a real Redis; the catch then removes the job from `processing` and
|
|
@@ -71,13 +148,6 @@ export class RedisDriver implements QueueDriver {
|
|
|
71
148
|
);
|
|
72
149
|
}
|
|
73
150
|
this.#visibilityTimeout = visibilityTimeout;
|
|
74
|
-
if (typeof client.lmove !== "function") {
|
|
75
|
-
console.warn(
|
|
76
|
-
"[bay] RedisDriver: client lacks LMOVE (Redis <6.2). pop() falls back to " +
|
|
77
|
-
"a non-atomic lpop+rpush, downgrading delivery from at-least-once to " +
|
|
78
|
-
"at-most-once — a crash between the two commands loses the in-flight job.",
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
151
|
}
|
|
82
152
|
|
|
83
153
|
#pendingKey = () => `${this.#prefix}pending`;
|
|
@@ -86,22 +156,24 @@ export class RedisDriver implements QueueDriver {
|
|
|
86
156
|
#leaseKey = (jobId: string) => `${this.#prefix}lease:${jobId}`;
|
|
87
157
|
|
|
88
158
|
async push(job: Job): Promise<void> {
|
|
89
|
-
await this.#client
|
|
159
|
+
const client = await this.#client();
|
|
160
|
+
await client.rpush(this.#pendingKey(), JSON.stringify(job));
|
|
90
161
|
}
|
|
91
162
|
|
|
92
163
|
async pop(): Promise<Job | null> {
|
|
164
|
+
const client = await this.#client();
|
|
93
165
|
let raw: string | null = null;
|
|
94
166
|
|
|
95
|
-
if (
|
|
96
|
-
raw = await
|
|
167
|
+
if (client.lmove) {
|
|
168
|
+
raw = await client.lmove(
|
|
97
169
|
this.#pendingKey(),
|
|
98
170
|
this.#processingKey(),
|
|
99
171
|
"LEFT",
|
|
100
172
|
"RIGHT",
|
|
101
173
|
);
|
|
102
174
|
} else {
|
|
103
|
-
raw = await
|
|
104
|
-
if (raw) await
|
|
175
|
+
raw = await client.lpop(this.#pendingKey());
|
|
176
|
+
if (raw) await client.rpush(this.#processingKey(), raw);
|
|
105
177
|
}
|
|
106
178
|
|
|
107
179
|
if (!raw) return null;
|
|
@@ -111,10 +183,10 @@ export class RedisDriver implements QueueDriver {
|
|
|
111
183
|
// Malformed payload — purge from `processing` so it can't sit
|
|
112
184
|
// there indefinitely as a poison pill. recoverStale() also
|
|
113
185
|
// catches survivors but pop()'s own move is the primary path.
|
|
114
|
-
await
|
|
186
|
+
await client.lrem(this.#processingKey(), 1, raw);
|
|
115
187
|
return null;
|
|
116
188
|
}
|
|
117
|
-
await
|
|
189
|
+
await client.set(
|
|
118
190
|
this.#leaseKey(parsed.id),
|
|
119
191
|
raw,
|
|
120
192
|
"PX",
|
|
@@ -122,33 +194,37 @@ export class RedisDriver implements QueueDriver {
|
|
|
122
194
|
);
|
|
123
195
|
return parsed;
|
|
124
196
|
} catch {
|
|
125
|
-
await
|
|
197
|
+
await client.lrem(this.#processingKey(), 1, raw);
|
|
126
198
|
return null;
|
|
127
199
|
}
|
|
128
200
|
}
|
|
129
201
|
|
|
130
202
|
async complete(job: Job): Promise<void> {
|
|
203
|
+
const client = await this.#client();
|
|
131
204
|
await this.#removeFromProcessing(job);
|
|
132
|
-
await
|
|
205
|
+
await client.del(this.#leaseKey(job.id));
|
|
133
206
|
}
|
|
134
207
|
|
|
135
208
|
async fail(job: Job, error: string): Promise<void> {
|
|
209
|
+
const client = await this.#client();
|
|
136
210
|
await this.#removeFromProcessing(job);
|
|
137
|
-
await
|
|
211
|
+
await client.del(this.#leaseKey(job.id));
|
|
138
212
|
job.error = error;
|
|
139
213
|
job.status = "failed";
|
|
140
|
-
await
|
|
214
|
+
await client.rpush(this.#failedKey(), JSON.stringify(job));
|
|
141
215
|
}
|
|
142
216
|
|
|
143
217
|
async retry(job: Job): Promise<void> {
|
|
218
|
+
const client = await this.#client();
|
|
144
219
|
await this.#removeFromProcessing(job);
|
|
145
|
-
await
|
|
220
|
+
await client.del(this.#leaseKey(job.id));
|
|
146
221
|
job.status = "pending";
|
|
147
|
-
await
|
|
222
|
+
await client.rpush(this.#pendingKey(), JSON.stringify(job));
|
|
148
223
|
}
|
|
149
224
|
|
|
150
225
|
async recoverStale(): Promise<number> {
|
|
151
|
-
const
|
|
226
|
+
const client = await this.#client();
|
|
227
|
+
const processing = await client.lrange(this.#processingKey(), 0, -1);
|
|
152
228
|
let recovered = 0;
|
|
153
229
|
for (const raw of processing) {
|
|
154
230
|
let parsed: unknown;
|
|
@@ -157,18 +233,18 @@ export class RedisDriver implements QueueDriver {
|
|
|
157
233
|
} catch {
|
|
158
234
|
// Malformed JSON would otherwise sit in processing forever —
|
|
159
235
|
// LREM purges it so the queue makes progress.
|
|
160
|
-
await
|
|
236
|
+
await client.lrem(this.#processingKey(), 1, raw);
|
|
161
237
|
continue;
|
|
162
238
|
}
|
|
163
239
|
if (!isValidJob(parsed)) {
|
|
164
|
-
await
|
|
240
|
+
await client.lrem(this.#processingKey(), 1, raw);
|
|
165
241
|
continue;
|
|
166
242
|
}
|
|
167
|
-
const lease = await
|
|
243
|
+
const lease = await client.get(this.#leaseKey(parsed.id));
|
|
168
244
|
if (lease === null) {
|
|
169
|
-
await
|
|
245
|
+
await client.lrem(this.#processingKey(), 1, raw);
|
|
170
246
|
parsed.status = "pending";
|
|
171
|
-
await
|
|
247
|
+
await client.rpush(this.#pendingKey(), JSON.stringify(parsed));
|
|
172
248
|
recovered++;
|
|
173
249
|
}
|
|
174
250
|
}
|
|
@@ -185,14 +261,15 @@ export class RedisDriver implements QueueDriver {
|
|
|
185
261
|
* the lease has expired (e.g. recoverStale already handled it).
|
|
186
262
|
*/
|
|
187
263
|
async #removeFromProcessing(job: Job): Promise<void> {
|
|
188
|
-
const
|
|
264
|
+
const client = await this.#client();
|
|
265
|
+
const stored = await client.get(this.#leaseKey(job.id));
|
|
189
266
|
if (stored !== null) {
|
|
190
|
-
const removed = await
|
|
267
|
+
const removed = await client.lrem(this.#processingKey(), 1, stored);
|
|
191
268
|
if (removed > 0) return;
|
|
192
269
|
}
|
|
193
270
|
// Lease missing or already-LREM'd entry not found — best-effort scan
|
|
194
271
|
// matches by job id and removes the actual stored representation.
|
|
195
|
-
const items = await
|
|
272
|
+
const items = await client.lrange(this.#processingKey(), 0, -1);
|
|
196
273
|
for (const item of items) {
|
|
197
274
|
let parsed: unknown;
|
|
198
275
|
try {
|
|
@@ -201,14 +278,15 @@ export class RedisDriver implements QueueDriver {
|
|
|
201
278
|
continue;
|
|
202
279
|
}
|
|
203
280
|
if (isValidJob(parsed) && (parsed as { id: string }).id === job.id) {
|
|
204
|
-
await
|
|
281
|
+
await client.lrem(this.#processingKey(), 1, item);
|
|
205
282
|
return;
|
|
206
283
|
}
|
|
207
284
|
}
|
|
208
285
|
}
|
|
209
286
|
|
|
210
287
|
async failed(): Promise<Job[]> {
|
|
211
|
-
const
|
|
288
|
+
const client = await this.#client();
|
|
289
|
+
const raws = await client.lrange(this.#failedKey(), 0, -1);
|
|
212
290
|
return raws
|
|
213
291
|
.map((r) => {
|
|
214
292
|
try {
|
|
@@ -222,6 +300,7 @@ export class RedisDriver implements QueueDriver {
|
|
|
222
300
|
}
|
|
223
301
|
|
|
224
302
|
async size(): Promise<number> {
|
|
225
|
-
|
|
303
|
+
const client = await this.#client();
|
|
304
|
+
return client.llen(this.#pendingKey());
|
|
226
305
|
}
|
|
227
306
|
}
|
package/src/index.ts
CHANGED
package/src/quasar.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving a Redis connection by name, from `@c9up/quasar`.
|
|
3
|
+
*
|
|
4
|
+
* Bay does not depend on quasar: it is an optional peer, and this module
|
|
5
|
+
* never imports it statically. The specifier is built at runtime so the
|
|
6
|
+
* TypeScript build stays free of it too — a hard type import would make echo
|
|
7
|
+
* unbuildable for anyone who queues in memory.
|
|
8
|
+
*
|
|
9
|
+
* The shape is checked before use rather than asserted, the same way bay
|
|
10
|
+
* duck-types its host framework.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { RedisClient } from "./drivers/RedisDriver.js";
|
|
14
|
+
|
|
15
|
+
/** The slice of quasar's manager this needs: a connection, by name. */
|
|
16
|
+
interface ConnectionSource {
|
|
17
|
+
connection(name?: string): unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isConnectionSource(value: unknown): value is ConnectionSource {
|
|
21
|
+
return (
|
|
22
|
+
typeof value === "object" &&
|
|
23
|
+
value !== null &&
|
|
24
|
+
typeof Reflect.get(value, "connection") === "function"
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isRedisClient(value: unknown): value is RedisClient {
|
|
29
|
+
if (typeof value !== "object" || value === null) return false;
|
|
30
|
+
// The commands this driver actually issues — every non-optional member of
|
|
31
|
+
// `RedisClient`. A connection missing one would fail on the first job push,
|
|
32
|
+
// far from the cause. `lmove` is deliberately absent: the driver declares it
|
|
33
|
+
// optional and falls back when it is not there.
|
|
34
|
+
const required = [
|
|
35
|
+
"rpush",
|
|
36
|
+
"lpop",
|
|
37
|
+
"lrem",
|
|
38
|
+
"llen",
|
|
39
|
+
"lrange",
|
|
40
|
+
"del",
|
|
41
|
+
"set",
|
|
42
|
+
"get",
|
|
43
|
+
];
|
|
44
|
+
return required.every(
|
|
45
|
+
(name) => typeof Reflect.get(value, name) === "function",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A resolver for `new RedisDriver(quasarConnection("jobs"))` — quasar is
|
|
51
|
+
* loaded on the first queue command, not at config time.
|
|
52
|
+
*/
|
|
53
|
+
export function quasarConnection(name?: string): () => Promise<RedisClient> {
|
|
54
|
+
return async () => {
|
|
55
|
+
const specifier = "@c9up/quasar/services/main";
|
|
56
|
+
let loaded: unknown;
|
|
57
|
+
try {
|
|
58
|
+
loaded = await import(/* @vite-ignore */ specifier);
|
|
59
|
+
} catch (cause) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Bay: the "${name ?? "default"}" queue asks for a quasar connection, but @c9up/quasar is not installed.\n` +
|
|
62
|
+
" pnpm add @c9up/quasar",
|
|
63
|
+
{ cause },
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const manager = isConnectionSource(loaded)
|
|
68
|
+
? loaded
|
|
69
|
+
: Reflect.get(Object(loaded), "default");
|
|
70
|
+
if (!isConnectionSource(manager)) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
"Bay: @c9up/quasar/services/main did not expose a connection() manager",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const connection = manager.connection(name);
|
|
77
|
+
if (!isRedisClient(connection)) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Bay: quasar connection "${name ?? "default"}" does not carry the commands this queue needs`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return connection;
|
|
83
|
+
};
|
|
84
|
+
}
|
package/src/services/main.ts
CHANGED
|
@@ -29,6 +29,15 @@ export function getQueue(): QueueManager | undefined {
|
|
|
29
29
|
|
|
30
30
|
const queue: QueueManager = new Proxy({} as QueueManager, {
|
|
31
31
|
get(_target, prop) {
|
|
32
|
+
// A module loader inspects what it imports before anyone uses it: it reads
|
|
33
|
+
// `then` to decide whether the namespace is thenable, and various symbols
|
|
34
|
+
// for interop and formatting. Throwing on those turns a plain
|
|
35
|
+
// `import { setX } from ".../services/main"` into a crash at import time,
|
|
36
|
+
// far from any real use. They are not members of what this stands in for,
|
|
37
|
+
// so answer undefined and let a genuine access be the one that reports.
|
|
38
|
+
if (typeof prop === "symbol" || prop === "then") {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
32
41
|
if (!instance) {
|
|
33
42
|
throw new Error(
|
|
34
43
|
"[bay] QueueManager singleton accessed before BayProvider.boot() ran " +
|