@c9up/bay 0.1.7 → 0.1.9
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/dist/BayProvider.d.ts +1 -1
- package/dist/BayProvider.d.ts.map +1 -1
- package/dist/BayProvider.js +1 -1
- package/dist/BayProvider.js.map +1 -1
- package/dist/drivers/RedisDriver.d.ts +8 -1
- package/dist/drivers/RedisDriver.d.ts.map +1 -1
- package/dist/drivers/RedisDriver.js +81 -33
- 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 +6 -2
- package/src/BayProvider.ts +2 -2
- package/src/drivers/RedisDriver.ts +92 -35
- package/src/index.ts +1 -0
- package/src/quasar.ts +84 -0
- package/src/services/main.ts +9 -0
package/dist/BayProvider.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
interface BayContainer {
|
|
8
8
|
singleton(token: unknown, factory: () => unknown): void;
|
|
9
|
-
resolve<T = unknown>(token: unknown): T
|
|
9
|
+
resolve<T = unknown>(token: unknown): Promise<T>;
|
|
10
10
|
}
|
|
11
11
|
interface BayConfigStore {
|
|
12
12
|
get<T = unknown>(key: string): T | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BayProvider.d.ts","sourceRoot":"","sources":["../src/BayProvider.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,UAAU,YAAY;IACrB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,GAAG,IAAI,CAAC;IACxD,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"BayProvider.d.ts","sourceRoot":"","sources":["../src/BayProvider.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,UAAU,YAAY;IACrB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,GAAG,IAAI,CAAC;IACxD,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACjD;AACD,UAAU,cAAc;IACvB,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC;CAC7C;AACD,MAAM,WAAW,aAAa;IAC7B,SAAS,EAAE,YAAY,CAAC;IACxB,MAAM,EAAE,cAAc,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IACjC;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,OAAO,OAAO,WAAW;IACnB,SAAS,CAAC,GAAG,EAAE,aAAa;gBAAlB,GAAG,EAAE,aAAa;IAExC,QAAQ,IAAI,IAAI;IAiBV,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAMrB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAC/B"}
|
package/dist/BayProvider.js
CHANGED
|
@@ -43,7 +43,7 @@ export default class BayProvider {
|
|
|
43
43
|
async boot() {
|
|
44
44
|
// Populate the `@c9up/bay/services/main` singleton so apps can
|
|
45
45
|
// `import queue from '@c9up/bay/services/main'` from anywhere.
|
|
46
|
-
setQueue(this.app.container.resolve(QueueManager));
|
|
46
|
+
setQueue(await this.app.container.resolve(QueueManager));
|
|
47
47
|
}
|
|
48
48
|
async shutdown() { }
|
|
49
49
|
}
|
package/dist/BayProvider.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BayProvider.js","sourceRoot":"","sources":["../src/BayProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAkC9C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,OAAO,OAAO,WAAW;IACT;IAAtB,YAAsB,GAAkB;QAAlB,QAAG,GAAH,GAAG,CAAe;IAAG,CAAC;IAE5C,QAAQ;QACP,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,YAAY,EAAE,GAAG,EAAE;YAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAoB,OAAO,CAAC,CAAC;YAC/D,MAAM,UAAU,GAAG,MAAM,EAAE,MAAM,IAAI,QAAQ,CAAC;YAC9C,IAAI,UAAU,KAAK,QAAQ,EAAE,CAAC;gBAC7B,MAAM,IAAI,KAAK,CACd,6BAA6B,UAAU,2BAA2B;oBACjE,sEAAsE,CACvE,CAAC;YACH,CAAC;YACD,OAAO,IAAI,YAAY,CAAC,IAAI,YAAY,EAAE,CAAC,CAAC;QAC7C,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,EAAE,CAC1C,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAe,YAAY,CAAC,CACtD,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI;QACT,+DAA+D;QAC/D,+DAA+D;QAC/D,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAe,YAAY,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"BayProvider.js","sourceRoot":"","sources":["../src/BayProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAkC9C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,OAAO,OAAO,WAAW;IACT;IAAtB,YAAsB,GAAkB;QAAlB,QAAG,GAAH,GAAG,CAAe;IAAG,CAAC;IAE5C,QAAQ;QACP,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,YAAY,EAAE,GAAG,EAAE;YAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAoB,OAAO,CAAC,CAAC;YAC/D,MAAM,UAAU,GAAG,MAAM,EAAE,MAAM,IAAI,QAAQ,CAAC;YAC9C,IAAI,UAAU,KAAK,QAAQ,EAAE,CAAC;gBAC7B,MAAM,IAAI,KAAK,CACd,6BAA6B,UAAU,2BAA2B;oBACjE,sEAAsE,CACvE,CAAC;YACH,CAAC;YACD,OAAO,IAAI,YAAY,CAAC,IAAI,YAAY,EAAE,CAAC,CAAC;QAC7C,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,EAAE,CAC1C,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAe,YAAY,CAAC,CACtD,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI;QACT,+DAA+D;QAC/D,+DAA+D;QAC/D,QAAQ,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAe,YAAY,CAAC,CAAC,CAAC;IACxE,CAAC;IAED,KAAK,CAAC,QAAQ,KAAmB,CAAC;CAClC"}
|
|
@@ -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;AAkB9C,qBAAa,WAAY,YAAW,WAAW;;gBA+B7C,MAAM,EAAE,iBAAiB,EACzB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;KAAE;IA0BtD,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,12 +27,56 @@ 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
|
+
}
|
|
30
44
|
export class RedisDriver {
|
|
31
|
-
#
|
|
45
|
+
#source;
|
|
46
|
+
#resolved;
|
|
47
|
+
#pending;
|
|
32
48
|
#prefix;
|
|
33
49
|
#visibilityTimeout;
|
|
34
|
-
|
|
35
|
-
|
|
50
|
+
/**
|
|
51
|
+
* The client, resolved once. Two workers racing on a cold queue must not
|
|
52
|
+
* each open their own connection, so the in-flight promise is shared.
|
|
53
|
+
*/
|
|
54
|
+
async #client() {
|
|
55
|
+
if (this.#resolved)
|
|
56
|
+
return this.#resolved;
|
|
57
|
+
if (typeof this.#source !== "function") {
|
|
58
|
+
this.#resolved = this.#source;
|
|
59
|
+
warnWithoutLmove(this.#resolved);
|
|
60
|
+
return this.#resolved;
|
|
61
|
+
}
|
|
62
|
+
if (!this.#pending) {
|
|
63
|
+
const resolver = this.#source;
|
|
64
|
+
this.#pending = Promise.resolve(resolver()).then((client) => {
|
|
65
|
+
this.#resolved = client;
|
|
66
|
+
this.#pending = undefined;
|
|
67
|
+
warnWithoutLmove(client);
|
|
68
|
+
return client;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return this.#pending;
|
|
72
|
+
}
|
|
73
|
+
constructor(source, options) {
|
|
74
|
+
this.#source = source;
|
|
75
|
+
// A client handed in directly can be checked now, so the warning keeps
|
|
76
|
+
// landing at construction as it always did. A named connection has no
|
|
77
|
+
// client yet — it is checked when the connection resolves.
|
|
78
|
+
if (typeof source !== "function")
|
|
79
|
+
warnWithoutLmove(source);
|
|
36
80
|
this.#prefix = options?.prefix ?? "queue:";
|
|
37
81
|
const visibilityTimeout = options?.visibilityTimeoutMs ?? 30_000;
|
|
38
82
|
// A non-positive / non-integer timeout makes pop()'s `SET … PX <ms>` fail
|
|
@@ -43,28 +87,25 @@ export class RedisDriver {
|
|
|
43
87
|
throw new Error(`[bay] RedisDriver visibilityTimeoutMs must be a positive integer (ms), got ${visibilityTimeout}`);
|
|
44
88
|
}
|
|
45
89
|
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
90
|
}
|
|
52
91
|
#pendingKey = () => `${this.#prefix}pending`;
|
|
53
92
|
#processingKey = () => `${this.#prefix}processing`;
|
|
54
93
|
#failedKey = () => `${this.#prefix}failed`;
|
|
55
94
|
#leaseKey = (jobId) => `${this.#prefix}lease:${jobId}`;
|
|
56
95
|
async push(job) {
|
|
57
|
-
await this.#client
|
|
96
|
+
const client = await this.#client();
|
|
97
|
+
await client.rpush(this.#pendingKey(), JSON.stringify(job));
|
|
58
98
|
}
|
|
59
99
|
async pop() {
|
|
100
|
+
const client = await this.#client();
|
|
60
101
|
let raw = null;
|
|
61
|
-
if (
|
|
62
|
-
raw = await
|
|
102
|
+
if (client.lmove) {
|
|
103
|
+
raw = await client.lmove(this.#pendingKey(), this.#processingKey(), "LEFT", "RIGHT");
|
|
63
104
|
}
|
|
64
105
|
else {
|
|
65
|
-
raw = await
|
|
106
|
+
raw = await client.lpop(this.#pendingKey());
|
|
66
107
|
if (raw)
|
|
67
|
-
await
|
|
108
|
+
await client.rpush(this.#processingKey(), raw);
|
|
68
109
|
}
|
|
69
110
|
if (!raw)
|
|
70
111
|
return null;
|
|
@@ -74,36 +115,40 @@ export class RedisDriver {
|
|
|
74
115
|
// Malformed payload — purge from `processing` so it can't sit
|
|
75
116
|
// there indefinitely as a poison pill. recoverStale() also
|
|
76
117
|
// catches survivors but pop()'s own move is the primary path.
|
|
77
|
-
await
|
|
118
|
+
await client.lrem(this.#processingKey(), 1, raw);
|
|
78
119
|
return null;
|
|
79
120
|
}
|
|
80
|
-
await
|
|
121
|
+
await client.set(this.#leaseKey(parsed.id), raw, "PX", String(this.#visibilityTimeout));
|
|
81
122
|
return parsed;
|
|
82
123
|
}
|
|
83
124
|
catch {
|
|
84
|
-
await
|
|
125
|
+
await client.lrem(this.#processingKey(), 1, raw);
|
|
85
126
|
return null;
|
|
86
127
|
}
|
|
87
128
|
}
|
|
88
129
|
async complete(job) {
|
|
130
|
+
const client = await this.#client();
|
|
89
131
|
await this.#removeFromProcessing(job);
|
|
90
|
-
await
|
|
132
|
+
await client.del(this.#leaseKey(job.id));
|
|
91
133
|
}
|
|
92
134
|
async fail(job, error) {
|
|
135
|
+
const client = await this.#client();
|
|
93
136
|
await this.#removeFromProcessing(job);
|
|
94
|
-
await
|
|
137
|
+
await client.del(this.#leaseKey(job.id));
|
|
95
138
|
job.error = error;
|
|
96
139
|
job.status = "failed";
|
|
97
|
-
await
|
|
140
|
+
await client.rpush(this.#failedKey(), JSON.stringify(job));
|
|
98
141
|
}
|
|
99
142
|
async retry(job) {
|
|
143
|
+
const client = await this.#client();
|
|
100
144
|
await this.#removeFromProcessing(job);
|
|
101
|
-
await
|
|
145
|
+
await client.del(this.#leaseKey(job.id));
|
|
102
146
|
job.status = "pending";
|
|
103
|
-
await
|
|
147
|
+
await client.rpush(this.#pendingKey(), JSON.stringify(job));
|
|
104
148
|
}
|
|
105
149
|
async recoverStale() {
|
|
106
|
-
const
|
|
150
|
+
const client = await this.#client();
|
|
151
|
+
const processing = await client.lrange(this.#processingKey(), 0, -1);
|
|
107
152
|
let recovered = 0;
|
|
108
153
|
for (const raw of processing) {
|
|
109
154
|
let parsed;
|
|
@@ -113,18 +158,18 @@ export class RedisDriver {
|
|
|
113
158
|
catch {
|
|
114
159
|
// Malformed JSON would otherwise sit in processing forever —
|
|
115
160
|
// LREM purges it so the queue makes progress.
|
|
116
|
-
await
|
|
161
|
+
await client.lrem(this.#processingKey(), 1, raw);
|
|
117
162
|
continue;
|
|
118
163
|
}
|
|
119
164
|
if (!isValidJob(parsed)) {
|
|
120
|
-
await
|
|
165
|
+
await client.lrem(this.#processingKey(), 1, raw);
|
|
121
166
|
continue;
|
|
122
167
|
}
|
|
123
|
-
const lease = await
|
|
168
|
+
const lease = await client.get(this.#leaseKey(parsed.id));
|
|
124
169
|
if (lease === null) {
|
|
125
|
-
await
|
|
170
|
+
await client.lrem(this.#processingKey(), 1, raw);
|
|
126
171
|
parsed.status = "pending";
|
|
127
|
-
await
|
|
172
|
+
await client.rpush(this.#pendingKey(), JSON.stringify(parsed));
|
|
128
173
|
recovered++;
|
|
129
174
|
}
|
|
130
175
|
}
|
|
@@ -140,15 +185,16 @@ export class RedisDriver {
|
|
|
140
185
|
* the lease has expired (e.g. recoverStale already handled it).
|
|
141
186
|
*/
|
|
142
187
|
async #removeFromProcessing(job) {
|
|
143
|
-
const
|
|
188
|
+
const client = await this.#client();
|
|
189
|
+
const stored = await client.get(this.#leaseKey(job.id));
|
|
144
190
|
if (stored !== null) {
|
|
145
|
-
const removed = await
|
|
191
|
+
const removed = await client.lrem(this.#processingKey(), 1, stored);
|
|
146
192
|
if (removed > 0)
|
|
147
193
|
return;
|
|
148
194
|
}
|
|
149
195
|
// Lease missing or already-LREM'd entry not found — best-effort scan
|
|
150
196
|
// matches by job id and removes the actual stored representation.
|
|
151
|
-
const items = await
|
|
197
|
+
const items = await client.lrange(this.#processingKey(), 0, -1);
|
|
152
198
|
for (const item of items) {
|
|
153
199
|
let parsed;
|
|
154
200
|
try {
|
|
@@ -158,13 +204,14 @@ export class RedisDriver {
|
|
|
158
204
|
continue;
|
|
159
205
|
}
|
|
160
206
|
if (isValidJob(parsed) && parsed.id === job.id) {
|
|
161
|
-
await
|
|
207
|
+
await client.lrem(this.#processingKey(), 1, item);
|
|
162
208
|
return;
|
|
163
209
|
}
|
|
164
210
|
}
|
|
165
211
|
}
|
|
166
212
|
async failed() {
|
|
167
|
-
const
|
|
213
|
+
const client = await this.#client();
|
|
214
|
+
const raws = await client.lrange(this.#failedKey(), 0, -1);
|
|
168
215
|
return raws
|
|
169
216
|
.map((r) => {
|
|
170
217
|
try {
|
|
@@ -178,7 +225,8 @@ export class RedisDriver {
|
|
|
178
225
|
.filter((j) => j !== null);
|
|
179
226
|
}
|
|
180
227
|
async size() {
|
|
181
|
-
|
|
228
|
+
const client = await this.#client();
|
|
229
|
+
return client.llen(this.#pendingKey());
|
|
182
230
|
}
|
|
183
231
|
}
|
|
184
232
|
//# 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. A connection missing one of
|
|
21
|
+
// them would fail on the first job push, far from the cause.
|
|
22
|
+
const required = [
|
|
23
|
+
"get",
|
|
24
|
+
"set",
|
|
25
|
+
"del",
|
|
26
|
+
"exists",
|
|
27
|
+
"keys",
|
|
28
|
+
"sadd",
|
|
29
|
+
"srem",
|
|
30
|
+
"smembers",
|
|
31
|
+
"expire",
|
|
32
|
+
"ttl",
|
|
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,wEAAwE;IACxE,6DAA6D;IAC7D,MAAM,QAAQ,GAAG;QAChB,KAAK;QACL,KAAK;QACL,KAAK;QACL,QAAQ;QACR,MAAM;QACN,MAAM;QACN,MAAM;QACN,UAAU;QACV,QAAQ;QACR,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.9",
|
|
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,7 +25,8 @@
|
|
|
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",
|
|
@@ -48,6 +49,9 @@
|
|
|
48
49
|
"peerDependenciesMeta": {
|
|
49
50
|
"@c9up/ream": {
|
|
50
51
|
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"@c9up/quasar": {
|
|
54
|
+
"optional": true
|
|
51
55
|
}
|
|
52
56
|
},
|
|
53
57
|
"repository": {
|
package/src/BayProvider.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { setQueue } from "./services/main.js";
|
|
|
10
10
|
*/
|
|
11
11
|
interface BayContainer {
|
|
12
12
|
singleton(token: unknown, factory: () => unknown): void;
|
|
13
|
-
resolve<T = unknown>(token: unknown): T
|
|
13
|
+
resolve<T = unknown>(token: unknown): Promise<T>;
|
|
14
14
|
}
|
|
15
15
|
interface BayConfigStore {
|
|
16
16
|
get<T = unknown>(key: string): T | undefined;
|
|
@@ -79,7 +79,7 @@ export default class BayProvider {
|
|
|
79
79
|
async boot(): Promise<void> {
|
|
80
80
|
// Populate the `@c9up/bay/services/main` singleton so apps can
|
|
81
81
|
// `import queue from '@c9up/bay/services/main'` from anywhere.
|
|
82
|
-
setQueue(this.app.container.resolve<QueueManager>(QueueManager));
|
|
82
|
+
setQueue(await this.app.container.resolve<QueueManager>(QueueManager));
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
async shutdown(): Promise<void> {}
|
|
@@ -49,16 +49,71 @@ 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
|
+
|
|
52
78
|
export class RedisDriver implements QueueDriver {
|
|
53
|
-
#
|
|
79
|
+
#source: RedisClientSource;
|
|
80
|
+
#resolved: RedisClient | undefined;
|
|
81
|
+
#pending: Promise<RedisClient> | undefined;
|
|
54
82
|
#prefix: string;
|
|
55
83
|
#visibilityTimeout: number;
|
|
56
84
|
|
|
85
|
+
/**
|
|
86
|
+
* The client, resolved once. Two workers racing on a cold queue must not
|
|
87
|
+
* each open their own connection, so the in-flight promise is shared.
|
|
88
|
+
*/
|
|
89
|
+
async #client(): Promise<RedisClient> {
|
|
90
|
+
if (this.#resolved) return this.#resolved;
|
|
91
|
+
if (typeof this.#source !== "function") {
|
|
92
|
+
this.#resolved = this.#source;
|
|
93
|
+
warnWithoutLmove(this.#resolved);
|
|
94
|
+
return this.#resolved;
|
|
95
|
+
}
|
|
96
|
+
if (!this.#pending) {
|
|
97
|
+
const resolver = this.#source;
|
|
98
|
+
this.#pending = Promise.resolve(resolver()).then((client) => {
|
|
99
|
+
this.#resolved = client;
|
|
100
|
+
this.#pending = undefined;
|
|
101
|
+
warnWithoutLmove(client);
|
|
102
|
+
return client;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return this.#pending;
|
|
106
|
+
}
|
|
107
|
+
|
|
57
108
|
constructor(
|
|
58
|
-
|
|
109
|
+
source: RedisClientSource,
|
|
59
110
|
options?: { prefix?: string; visibilityTimeoutMs?: number },
|
|
60
111
|
) {
|
|
61
|
-
this.#
|
|
112
|
+
this.#source = source;
|
|
113
|
+
// A client handed in directly can be checked now, so the warning keeps
|
|
114
|
+
// landing at construction as it always did. A named connection has no
|
|
115
|
+
// client yet — it is checked when the connection resolves.
|
|
116
|
+
if (typeof source !== "function") warnWithoutLmove(source);
|
|
62
117
|
this.#prefix = options?.prefix ?? "queue:";
|
|
63
118
|
const visibilityTimeout = options?.visibilityTimeoutMs ?? 30_000;
|
|
64
119
|
// A non-positive / non-integer timeout makes pop()'s `SET … PX <ms>` fail
|
|
@@ -71,13 +126,6 @@ export class RedisDriver implements QueueDriver {
|
|
|
71
126
|
);
|
|
72
127
|
}
|
|
73
128
|
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
129
|
}
|
|
82
130
|
|
|
83
131
|
#pendingKey = () => `${this.#prefix}pending`;
|
|
@@ -86,22 +134,24 @@ export class RedisDriver implements QueueDriver {
|
|
|
86
134
|
#leaseKey = (jobId: string) => `${this.#prefix}lease:${jobId}`;
|
|
87
135
|
|
|
88
136
|
async push(job: Job): Promise<void> {
|
|
89
|
-
await this.#client
|
|
137
|
+
const client = await this.#client();
|
|
138
|
+
await client.rpush(this.#pendingKey(), JSON.stringify(job));
|
|
90
139
|
}
|
|
91
140
|
|
|
92
141
|
async pop(): Promise<Job | null> {
|
|
142
|
+
const client = await this.#client();
|
|
93
143
|
let raw: string | null = null;
|
|
94
144
|
|
|
95
|
-
if (
|
|
96
|
-
raw = await
|
|
145
|
+
if (client.lmove) {
|
|
146
|
+
raw = await client.lmove(
|
|
97
147
|
this.#pendingKey(),
|
|
98
148
|
this.#processingKey(),
|
|
99
149
|
"LEFT",
|
|
100
150
|
"RIGHT",
|
|
101
151
|
);
|
|
102
152
|
} else {
|
|
103
|
-
raw = await
|
|
104
|
-
if (raw) await
|
|
153
|
+
raw = await client.lpop(this.#pendingKey());
|
|
154
|
+
if (raw) await client.rpush(this.#processingKey(), raw);
|
|
105
155
|
}
|
|
106
156
|
|
|
107
157
|
if (!raw) return null;
|
|
@@ -111,10 +161,10 @@ export class RedisDriver implements QueueDriver {
|
|
|
111
161
|
// Malformed payload — purge from `processing` so it can't sit
|
|
112
162
|
// there indefinitely as a poison pill. recoverStale() also
|
|
113
163
|
// catches survivors but pop()'s own move is the primary path.
|
|
114
|
-
await
|
|
164
|
+
await client.lrem(this.#processingKey(), 1, raw);
|
|
115
165
|
return null;
|
|
116
166
|
}
|
|
117
|
-
await
|
|
167
|
+
await client.set(
|
|
118
168
|
this.#leaseKey(parsed.id),
|
|
119
169
|
raw,
|
|
120
170
|
"PX",
|
|
@@ -122,33 +172,37 @@ export class RedisDriver implements QueueDriver {
|
|
|
122
172
|
);
|
|
123
173
|
return parsed;
|
|
124
174
|
} catch {
|
|
125
|
-
await
|
|
175
|
+
await client.lrem(this.#processingKey(), 1, raw);
|
|
126
176
|
return null;
|
|
127
177
|
}
|
|
128
178
|
}
|
|
129
179
|
|
|
130
180
|
async complete(job: Job): Promise<void> {
|
|
181
|
+
const client = await this.#client();
|
|
131
182
|
await this.#removeFromProcessing(job);
|
|
132
|
-
await
|
|
183
|
+
await client.del(this.#leaseKey(job.id));
|
|
133
184
|
}
|
|
134
185
|
|
|
135
186
|
async fail(job: Job, error: string): Promise<void> {
|
|
187
|
+
const client = await this.#client();
|
|
136
188
|
await this.#removeFromProcessing(job);
|
|
137
|
-
await
|
|
189
|
+
await client.del(this.#leaseKey(job.id));
|
|
138
190
|
job.error = error;
|
|
139
191
|
job.status = "failed";
|
|
140
|
-
await
|
|
192
|
+
await client.rpush(this.#failedKey(), JSON.stringify(job));
|
|
141
193
|
}
|
|
142
194
|
|
|
143
195
|
async retry(job: Job): Promise<void> {
|
|
196
|
+
const client = await this.#client();
|
|
144
197
|
await this.#removeFromProcessing(job);
|
|
145
|
-
await
|
|
198
|
+
await client.del(this.#leaseKey(job.id));
|
|
146
199
|
job.status = "pending";
|
|
147
|
-
await
|
|
200
|
+
await client.rpush(this.#pendingKey(), JSON.stringify(job));
|
|
148
201
|
}
|
|
149
202
|
|
|
150
203
|
async recoverStale(): Promise<number> {
|
|
151
|
-
const
|
|
204
|
+
const client = await this.#client();
|
|
205
|
+
const processing = await client.lrange(this.#processingKey(), 0, -1);
|
|
152
206
|
let recovered = 0;
|
|
153
207
|
for (const raw of processing) {
|
|
154
208
|
let parsed: unknown;
|
|
@@ -157,18 +211,18 @@ export class RedisDriver implements QueueDriver {
|
|
|
157
211
|
} catch {
|
|
158
212
|
// Malformed JSON would otherwise sit in processing forever —
|
|
159
213
|
// LREM purges it so the queue makes progress.
|
|
160
|
-
await
|
|
214
|
+
await client.lrem(this.#processingKey(), 1, raw);
|
|
161
215
|
continue;
|
|
162
216
|
}
|
|
163
217
|
if (!isValidJob(parsed)) {
|
|
164
|
-
await
|
|
218
|
+
await client.lrem(this.#processingKey(), 1, raw);
|
|
165
219
|
continue;
|
|
166
220
|
}
|
|
167
|
-
const lease = await
|
|
221
|
+
const lease = await client.get(this.#leaseKey(parsed.id));
|
|
168
222
|
if (lease === null) {
|
|
169
|
-
await
|
|
223
|
+
await client.lrem(this.#processingKey(), 1, raw);
|
|
170
224
|
parsed.status = "pending";
|
|
171
|
-
await
|
|
225
|
+
await client.rpush(this.#pendingKey(), JSON.stringify(parsed));
|
|
172
226
|
recovered++;
|
|
173
227
|
}
|
|
174
228
|
}
|
|
@@ -185,14 +239,15 @@ export class RedisDriver implements QueueDriver {
|
|
|
185
239
|
* the lease has expired (e.g. recoverStale already handled it).
|
|
186
240
|
*/
|
|
187
241
|
async #removeFromProcessing(job: Job): Promise<void> {
|
|
188
|
-
const
|
|
242
|
+
const client = await this.#client();
|
|
243
|
+
const stored = await client.get(this.#leaseKey(job.id));
|
|
189
244
|
if (stored !== null) {
|
|
190
|
-
const removed = await
|
|
245
|
+
const removed = await client.lrem(this.#processingKey(), 1, stored);
|
|
191
246
|
if (removed > 0) return;
|
|
192
247
|
}
|
|
193
248
|
// Lease missing or already-LREM'd entry not found — best-effort scan
|
|
194
249
|
// matches by job id and removes the actual stored representation.
|
|
195
|
-
const items = await
|
|
250
|
+
const items = await client.lrange(this.#processingKey(), 0, -1);
|
|
196
251
|
for (const item of items) {
|
|
197
252
|
let parsed: unknown;
|
|
198
253
|
try {
|
|
@@ -201,14 +256,15 @@ export class RedisDriver implements QueueDriver {
|
|
|
201
256
|
continue;
|
|
202
257
|
}
|
|
203
258
|
if (isValidJob(parsed) && (parsed as { id: string }).id === job.id) {
|
|
204
|
-
await
|
|
259
|
+
await client.lrem(this.#processingKey(), 1, item);
|
|
205
260
|
return;
|
|
206
261
|
}
|
|
207
262
|
}
|
|
208
263
|
}
|
|
209
264
|
|
|
210
265
|
async failed(): Promise<Job[]> {
|
|
211
|
-
const
|
|
266
|
+
const client = await this.#client();
|
|
267
|
+
const raws = await client.lrange(this.#failedKey(), 0, -1);
|
|
212
268
|
return raws
|
|
213
269
|
.map((r) => {
|
|
214
270
|
try {
|
|
@@ -222,6 +278,7 @@ export class RedisDriver implements QueueDriver {
|
|
|
222
278
|
}
|
|
223
279
|
|
|
224
280
|
async size(): Promise<number> {
|
|
225
|
-
|
|
281
|
+
const client = await this.#client();
|
|
282
|
+
return client.llen(this.#pendingKey());
|
|
226
283
|
}
|
|
227
284
|
}
|
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. A connection missing one of
|
|
31
|
+
// them would fail on the first job push, far from the cause.
|
|
32
|
+
const required = [
|
|
33
|
+
"get",
|
|
34
|
+
"set",
|
|
35
|
+
"del",
|
|
36
|
+
"exists",
|
|
37
|
+
"keys",
|
|
38
|
+
"sadd",
|
|
39
|
+
"srem",
|
|
40
|
+
"smembers",
|
|
41
|
+
"expire",
|
|
42
|
+
"ttl",
|
|
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 " +
|