@shivaedev/effect-pg-boss 0.8.0 → 0.8.1
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/CHANGELOG.md +12 -0
- package/README.md +2 -2
- package/dist/server/error.d.ts +1 -0
- package/dist/server/error.d.ts.map +1 -1
- package/dist/server/error.js +12 -0
- package/dist/server/error.js.map +1 -1
- package/dist/server/internal/clientCache.d.ts +7 -0
- package/dist/server/internal/clientCache.d.ts.map +1 -0
- package/dist/server/internal/clientCache.js +51 -0
- package/dist/server/internal/clientCache.js.map +1 -0
- package/dist/server/internal/clientErrors.d.ts +8 -0
- package/dist/server/internal/clientErrors.d.ts.map +1 -0
- package/dist/server/internal/clientErrors.js +49 -0
- package/dist/server/internal/clientErrors.js.map +1 -0
- package/dist/server/internal/ensureQueue.d.ts.map +1 -1
- package/dist/server/internal/ensureQueue.js +19 -2
- package/dist/server/internal/ensureQueue.js.map +1 -1
- package/dist/server/internal/lifecycle.d.ts +2 -2
- package/dist/server/internal/lifecycle.d.ts.map +1 -1
- package/dist/server/internal/lifecycle.js +36 -79
- package/dist/server/internal/lifecycle.js.map +1 -1
- package/dist/server/internal/register.d.ts +2 -2
- package/dist/server/internal/register.d.ts.map +1 -1
- package/dist/server/internal/register.js +8 -3
- package/dist/server/internal/register.js.map +1 -1
- package/dist/server/service.d.ts.map +1 -1
- package/dist/server/service.js +4 -12
- package/dist/server/service.js.map +1 -1
- package/docs/roadmap.md +6 -5
- package/package.json +6 -5
- package/src/server/error.ts +15 -1
- package/src/server/internal/clientCache.ts +61 -0
- package/src/server/internal/clientErrors.ts +64 -0
- package/src/server/internal/ensureQueue.ts +20 -2
- package/src/server/internal/lifecycle.ts +37 -97
- package/src/server/internal/register.ts +8 -4
- package/src/server/service.ts +5 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.1 - 2026-10-18
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- `PgBossError` has a message that names the operation, the queue and the original error's class and code, such as a PostgreSQL SQLSTATE, without the redacted original message. A registration failure names the queue it was preparing.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Registration creates a queue again, up to six times with a short backoff, when PostgreSQL rolls the creation back as a deadlock victim (SQLSTATE `40P01`). A partitioned queue created while other queues in the schema have traffic no longer fails Layer acquisition.
|
|
12
|
+
- Observe pg-boss connection errors throughout startup and shutdown, and log administrator-terminated connections at debug while preserving query failures and application error callbacks.
|
|
13
|
+
- Keep observing idle errors from closing sockets after shutdown, when shutdown fails, and when `stop({ close: false })` leaves the pool open.
|
|
14
|
+
|
|
3
15
|
## 0.8.0 - 2026-10-17
|
|
4
16
|
|
|
5
17
|
### Added
|
package/README.md
CHANGED
|
@@ -170,7 +170,7 @@ export const jobsLayer = (databaseUrl: string) =>
|
|
|
170
170
|
}).pipe(Layer.provide(MailLive));
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
-
The constructor options belong to pg-boss. `jobs` is the complete list of registrations for this Layer. The Layer captures `MailLive` for both the queue handler and the scheduled Effect. A startup or registration failure fails Layer acquisition with `PgBossError` or `PgBossQueueOptionsError` and releases its acquired client reference. A shared client stays running while another scope still owns a reference.
|
|
173
|
+
The constructor options belong to pg-boss. `jobs` is the complete list of registrations for this Layer. The Layer captures `MailLive` for both the queue handler and the scheduled Effect. A startup or registration failure fails Layer acquisition with `PgBossError` or `PgBossQueueOptionsError` and releases its acquired client reference. Registration creates a queue again, up to six times with a short backoff, when PostgreSQL rolls the creation back as a deadlock victim (SQLSTATE `40P01`), which a partitioned queue can meet when other queues in the schema have traffic. A shared client stays running while another scope still owns a reference.
|
|
174
174
|
|
|
175
175
|
Keep the Layer in the application's worker runtime scope. A short scope is useful for an acquisition or health check; a worker process needs its scope to remain open while it handles jobs.
|
|
176
176
|
|
|
@@ -352,7 +352,7 @@ The application's service has these operations:
|
|
|
352
352
|
|
|
353
353
|
`JobContext` has `id`, `name`, `signal`, `expireInSeconds`, `heartbeatSeconds` and `remainingBudget`. Worker registration always requests `includeMetadata: true`, including when `worker` supplies `includeMetadata: false`; custom clients must return pg-boss's `startedOn` metadata. When calling `jobContext` directly, reading `remainingBudget` requires that same metadata. `QueueHealth` has `name`, `activeCount`, `deadLetteredCount`, `failedCount`, `queuedCount` and `readyCount`. `JobsHealth` has `jobs` and the corresponding `activeTotal`, `deadLetteredTotal`, `failedTotal`, `queuedTotal` and `readyTotal` fields.
|
|
354
354
|
|
|
355
|
-
`PgBossError` has `operation`, optional `queue` and a `Redacted` `original`. `PgBossPayloadError` has `direction`, `queue` and a `Redacted` `original`. Those fields distinguish the pg-boss operation from the payload boundary.
|
|
355
|
+
`PgBossError` has `operation`, optional `queue` and a `Redacted` `original`. Its message names the operation, the queue and the original error's class and `code`, such as a PostgreSQL SQLSTATE, but never the original message, which can quote payloads or connection strings. A registration failure names the queue it was preparing. `PgBossPayloadError` has `direction`, `queue` and a `Redacted` `original`. Those fields distinguish the pg-boss operation from the payload boundary.
|
|
356
356
|
|
|
357
357
|
### Install, setup and limits
|
|
358
358
|
|
package/dist/server/error.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare class PgBossError extends PgBossError_base<{
|
|
|
8
8
|
readonly queue?: string | undefined;
|
|
9
9
|
readonly original: Redacted.Redacted<unknown>;
|
|
10
10
|
}> {
|
|
11
|
+
get message(): string;
|
|
11
12
|
}
|
|
12
13
|
declare const PgBossQueueOptionsError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
13
14
|
readonly _tag: "PgBossQueueOptionsError";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../src/server/error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAExC,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;;;;AAEnH,qBAAa,WAAY,SAAQ,iBAAgC;IAChE,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;IACpC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;CAC9C,CAAC;
|
|
1
|
+
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../src/server/error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAExC,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;;;;AAEnH,qBAAa,WAAY,SAAQ,iBAAgC;IAChE,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;IACpC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;CAC9C,CAAC;IAED,IAAa,OAAO,IAAI,MAAM,CAG7B;CACD;;;;AAUD,qBAAa,uBAAwB,SAAQ,6BAA4C;IACxF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC,WAAW,GAAG,QAAQ,CAAC,EAAE,CAAC;CACtD,CAAC;IACD,IAAa,OAAO,IAAI,MAAM,CAE7B;CACD;;;;AAED,qBAAa,kBAAmB,SAAQ,wBAAuC;IAC9E,QAAQ,CAAC,SAAS,EAAE,QAAQ,GAAG,QAAQ,CAAC;IACxC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;CAC9C,CAAC;CAAG;AAEL,eAAO,MAAM,aAAa,cAAe,eAAe,SAAS,OAAO,UAAU,MAAM,KAAG,WAKxF,CAAC"}
|
package/dist/server/error.js
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { Data, Redacted } from "effect";
|
|
2
2
|
export class PgBossError extends Data.TaggedError("PgBossError") {
|
|
3
|
+
// The original's message can quote payloads or connection strings, so only its class and code are shown.
|
|
4
|
+
get message() {
|
|
5
|
+
const target = this.queue === undefined ? "" : ` for queue ${this.queue}`;
|
|
6
|
+
return `pg-boss ${this.operation}${target} failed: ${originalKind(Redacted.value(this.original))}`;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function originalKind(original) {
|
|
10
|
+
if (!(original instanceof Error)) {
|
|
11
|
+
return typeof original;
|
|
12
|
+
}
|
|
13
|
+
const code = Reflect.get(original, "code");
|
|
14
|
+
return typeof code === "string" ? `${original.constructor.name} ${code}` : original.constructor.name;
|
|
3
15
|
}
|
|
4
16
|
export class PgBossQueueOptionsError extends Data.TaggedError("PgBossQueueOptionsError") {
|
|
5
17
|
get message() {
|
package/dist/server/error.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error.js","sourceRoot":"","sources":["../../src/server/error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAIxC,MAAM,OAAO,WAAY,SAAQ,IAAI,CAAC,WAAW,CAAC,aAAa,CAI7D;
|
|
1
|
+
{"version":3,"file":"error.js","sourceRoot":"","sources":["../../src/server/error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAIxC,MAAM,OAAO,WAAY,SAAQ,IAAI,CAAC,WAAW,CAAC,aAAa,CAI7D;IACD,yGAAyG;IACzG,IAAa,OAAO;QACnB,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,cAAc,IAAI,CAAC,KAAK,EAAE,CAAC;QAC1E,OAAO,WAAW,IAAI,CAAC,SAAS,GAAG,MAAM,YAAY,YAAY,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;IACpG,CAAC;CACD;AAED,SAAS,YAAY,CAAC,QAAiB;IACtC,IAAI,CAAC,CAAC,QAAQ,YAAY,KAAK,CAAC,EAAE,CAAC;QAClC,OAAO,OAAO,QAAQ,CAAC;IACxB,CAAC;IACD,MAAM,IAAI,GAAY,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACpD,OAAO,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,WAAW,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC;AACtG,CAAC;AAED,MAAM,OAAO,uBAAwB,SAAQ,IAAI,CAAC,WAAW,CAAC,yBAAyB,CAGrF;IACD,IAAa,OAAO;QACnB,OAAO,SAAS,IAAI,CAAC,KAAK,0CAA0C,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IAC/F,CAAC;CACD;AAED,MAAM,OAAO,kBAAmB,SAAQ,IAAI,CAAC,WAAW,CAAC,oBAAoB,CAI3E;CAAG;AAEL,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,SAA0B,EAAE,KAAc,EAAE,KAAc,EAAe,EAAE,CACxG,IAAI,WAAW,CAAC;IACf,SAAS;IACT,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC;IAC9B,KAAK;CACL,CAAC,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PgBossClient } from "#client.ts";
|
|
2
|
+
export declare function acquireCachedClient(key: string | symbol, start: () => Promise<PgBossClient>): Promise<{
|
|
3
|
+
client: PgBossClient;
|
|
4
|
+
reused: boolean;
|
|
5
|
+
}>;
|
|
6
|
+
export declare function releaseCachedClient(key: string | symbol): Promise<PgBossClient> | undefined;
|
|
7
|
+
//# sourceMappingURL=clientCache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clientCache.d.ts","sourceRoot":"","sources":["../../../src/server/internal/clientCache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AA0B/C,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC;;;GAqBjG;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,GAAG,SAAS,CAW3F"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const cacheSymbol = Symbol.for("@shivaedev/effect-pg-boss/client-cache");
|
|
2
|
+
const shared = Reflect.get(globalThis, cacheSymbol);
|
|
3
|
+
const clients = shared instanceof Map ? shared : new Map();
|
|
4
|
+
Reflect.set(globalThis, cacheSymbol, clients);
|
|
5
|
+
function entryFor(key) {
|
|
6
|
+
const value = clients.get(key);
|
|
7
|
+
return isCachedClient(value) ? value : undefined;
|
|
8
|
+
}
|
|
9
|
+
function isCachedClient(value) {
|
|
10
|
+
return (typeof value === "object"
|
|
11
|
+
&& value !== null
|
|
12
|
+
&& Reflect.get(value, "client") instanceof Promise
|
|
13
|
+
&& typeof Reflect.get(value, "references") === "number");
|
|
14
|
+
}
|
|
15
|
+
export async function acquireCachedClient(key, start) {
|
|
16
|
+
const existing = entryFor(key);
|
|
17
|
+
if (existing !== undefined) {
|
|
18
|
+
existing.references += 1;
|
|
19
|
+
try {
|
|
20
|
+
return { client: await existing.client, reused: true };
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
existing.references -= 1;
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const entry = { client: start(), references: 1 };
|
|
28
|
+
clients.set(key, entry);
|
|
29
|
+
try {
|
|
30
|
+
return { client: await entry.client, reused: false };
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (clients.get(key) === entry) {
|
|
34
|
+
clients.delete(key);
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function releaseCachedClient(key) {
|
|
40
|
+
const entry = entryFor(key);
|
|
41
|
+
if (entry === undefined) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
entry.references -= 1;
|
|
45
|
+
if (entry.references > 0) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
clients.delete(key);
|
|
49
|
+
return entry.client;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=clientCache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clientCache.js","sourceRoot":"","sources":["../../../src/server/internal/clientCache.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAC,wCAAwC,CAAC,CAAC;AACzE,MAAM,MAAM,GAAY,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;AAC7D,MAAM,OAAO,GAAkC,MAAM,YAAY,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,GAAG,EAA4B,CAAC;AACpH,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC;AAE9C,SAAS,QAAQ,CAAC,GAAoB;IACrC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC/B,OAAO,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AAClD,CAAC;AAED,SAAS,cAAc,CAAC,KAAc;IACrC,OAAO,CACN,OAAO,KAAK,KAAK,QAAQ;WACtB,KAAK,KAAK,IAAI;WACd,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,YAAY,OAAO;WAC/C,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,YAAY,CAAC,KAAK,QAAQ,CACvD,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,GAAoB,EAAE,KAAkC;IACjG,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC/B,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC5B,QAAQ,CAAC,UAAU,IAAI,CAAC,CAAC;QACzB,IAAI,CAAC;YACJ,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;QACxD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,QAAQ,CAAC,UAAU,IAAI,CAAC,CAAC;YACzB,MAAM,KAAK,CAAC;QACb,CAAC;IACF,CAAC;IACD,MAAM,KAAK,GAAiB,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;IAC/D,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACxB,IAAI,CAAC;QACJ,OAAO,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IACtD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,KAAK,EAAE,CAAC;YAChC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACrB,CAAC;QACD,MAAM,KAAK,CAAC;IACb,CAAC;AACF,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,GAAoB;IACvD,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC5B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO;IACR,CAAC;IACD,KAAK,CAAC,UAAU,IAAI,CAAC,CAAC;IACtB,IAAI,KAAK,CAAC,UAAU,GAAG,CAAC,EAAE,CAAC;QAC1B,OAAO;IACR,CAAC;IACD,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACpB,OAAO,KAAK,CAAC,MAAM,CAAC;AACrB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { PgBossClient } from "#client.ts";
|
|
3
|
+
type Listener = (error: Error) => void;
|
|
4
|
+
export declare function logClientError(error: Error): Effect.Effect<void>;
|
|
5
|
+
export declare function observeClientErrors(client: PgBossClient, fallback: Listener): void;
|
|
6
|
+
export declare function subscribeClientErrors(client: PgBossClient, listener: Listener): () => void;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=clientErrors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clientErrors.d.ts","sourceRoot":"","sources":["../../../src/server/internal/clientErrors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,KAAK,QAAQ,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;AAYvC,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAGhE;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAelF;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,QAAQ,GAAG,MAAM,IAAI,CAO1F"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
const key = Symbol.for("@shivaedev/effect-pg-boss/error-observers");
|
|
3
|
+
const shared = Reflect.get(globalThis, key);
|
|
4
|
+
const observers = shared instanceof WeakMap ? shared : new WeakMap();
|
|
5
|
+
Reflect.set(globalThis, key, observers);
|
|
6
|
+
export function logClientError(error) {
|
|
7
|
+
const message = { event: "pg_boss_error", message: error.message };
|
|
8
|
+
return Reflect.get(error, "code") === "57P01" ? Effect.logDebug(message) : Effect.logError(message);
|
|
9
|
+
}
|
|
10
|
+
export function observeClientErrors(client, fallback) {
|
|
11
|
+
closeClientErrors(client);
|
|
12
|
+
const listeners = new Set();
|
|
13
|
+
const listener = (error) => {
|
|
14
|
+
if (listeners.size === 0) {
|
|
15
|
+
fallback(error);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
for (const report of listeners) {
|
|
19
|
+
report(error);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
// Pool shutdown can resolve before its clients finish disconnecting.
|
|
24
|
+
client.on("error", listener);
|
|
25
|
+
observers.set(client, { dispose: () => client.off("error", listener), listeners });
|
|
26
|
+
}
|
|
27
|
+
export function subscribeClientErrors(client, listener) {
|
|
28
|
+
const observer = observerFor(client);
|
|
29
|
+
if (observer === undefined) {
|
|
30
|
+
throw new Error("pg-boss client has no error observer");
|
|
31
|
+
}
|
|
32
|
+
observer.listeners.add(listener);
|
|
33
|
+
return () => observer.listeners.delete(listener);
|
|
34
|
+
}
|
|
35
|
+
function closeClientErrors(client) {
|
|
36
|
+
observerFor(client)?.dispose();
|
|
37
|
+
observers.delete(client);
|
|
38
|
+
}
|
|
39
|
+
function observerFor(client) {
|
|
40
|
+
const value = observers.get(client);
|
|
41
|
+
return isObserver(value) ? value : undefined;
|
|
42
|
+
}
|
|
43
|
+
function isObserver(value) {
|
|
44
|
+
return (typeof value === "object"
|
|
45
|
+
&& value !== null
|
|
46
|
+
&& typeof Reflect.get(value, "dispose") === "function"
|
|
47
|
+
&& Reflect.get(value, "listeners") instanceof Set);
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=clientErrors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clientErrors.js","sourceRoot":"","sources":["../../../src/server/internal/clientErrors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAUhC,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAC;AACpE,MAAM,MAAM,GAAY,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;AACrD,MAAM,SAAS,GAA8B,MAAM,YAAY,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,OAAO,EAAoB,CAAC;AAClH,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,EAAE,SAAS,CAAC,CAAC;AAExC,MAAM,UAAU,cAAc,CAAC,KAAY;IAC1C,MAAM,OAAO,GAAG,EAAE,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;IACnE,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;AACrG,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,MAAoB,EAAE,QAAkB;IAC3E,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC1B,MAAM,SAAS,GAAG,IAAI,GAAG,EAAY,CAAC;IACtC,MAAM,QAAQ,GAAa,CAAC,KAAK,EAAE,EAAE;QACpC,IAAI,SAAS,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YAC1B,QAAQ,CAAC,KAAK,CAAC,CAAC;QACjB,CAAC;aAAM,CAAC;YACP,KAAK,MAAM,MAAM,IAAI,SAAS,EAAE,CAAC;gBAChC,MAAM,CAAC,KAAK,CAAC,CAAC;YACf,CAAC;QACF,CAAC;IACF,CAAC,CAAC;IACF,qEAAqE;IACrE,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAC7B,SAAS,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC;AACpF,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,MAAoB,EAAE,QAAkB;IAC7E,MAAM,QAAQ,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IACrC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IACzD,CAAC;IACD,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACjC,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAClD,CAAC;AAED,SAAS,iBAAiB,CAAC,MAAoB;IAC9C,WAAW,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;IAC/B,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AAC1B,CAAC;AAED,SAAS,WAAW,CAAC,MAAoB;IACxC,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACpC,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AAC9C,CAAC;AAED,SAAS,UAAU,CAAC,KAAc;IACjC,OAAO,CACN,OAAO,KAAK,KAAK,QAAQ;WACtB,KAAK,KAAK,IAAI;WACd,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,SAAS,CAAC,KAAK,UAAU;WACnD,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,WAAW,CAAC,YAAY,GAAG,CACjD,CAAC;AACH,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ensureQueue.d.ts","sourceRoot":"","sources":["../../../src/server/internal/ensureQueue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"ensureQueue.d.ts","sourceRoot":"","sources":["../../../src/server/internal/ensureQueue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAsB/C,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBhI"}
|
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
import { PgBossQueueOptionsError } from "#error.ts";
|
|
2
2
|
import { deadLetterQueueName } from "#health.ts";
|
|
3
|
+
const DEADLOCK_DETECTED = "40P01";
|
|
4
|
+
const CREATE_QUEUE_ATTEMPTS = 6;
|
|
5
|
+
// A partitioned queue attaches its table to pg-boss's job table, which can deadlock with job traffic on other queues.
|
|
6
|
+
// PostgreSQL rolls the whole creation back, so trying it again is safe.
|
|
7
|
+
async function createQueue(client, name, options) {
|
|
8
|
+
for (let attempt = 1;; attempt += 1) {
|
|
9
|
+
try {
|
|
10
|
+
return await client.createQueue(name, options);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (attempt === CREATE_QUEUE_ATTEMPTS || !(error instanceof Error) || Reflect.get(error, "code") !== DEADLOCK_DETECTED) {
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
await new Promise((resolve) => setTimeout(resolve, 25 * 2 ** (attempt - 1)));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
3
20
|
export async function ensureQueue(client, name, options) {
|
|
4
21
|
const deadLetter = deadLetterQueueName(name);
|
|
5
22
|
const desired = { retryBackoff: true, retryLimit: 3, ...options, deadLetter };
|
|
6
|
-
await
|
|
7
|
-
await
|
|
23
|
+
await createQueue(client, deadLetter);
|
|
24
|
+
await createQueue(client, name, desired);
|
|
8
25
|
const existing = await client.getQueue(name);
|
|
9
26
|
if (existing === null) {
|
|
10
27
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ensureQueue.js","sourceRoot":"","sources":["../../../src/server/internal/ensureQueue.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAoB,EAAE,IAAY,EAAE,OAA2C;IAChH,MAAM,UAAU,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAC7C,MAAM,OAAO,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,GAAG,OAAO,EAAE,UAAU,EAAE,CAAC;IAC9E,MAAM,
|
|
1
|
+
{"version":3,"file":"ensureQueue.js","sourceRoot":"","sources":["../../../src/server/internal/ensureQueue.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,iBAAiB,GAAG,OAAO,CAAC;AAClC,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAEhC,sHAAsH;AACtH,wEAAwE;AACxE,KAAK,UAAU,WAAW,CAAC,MAAoB,EAAE,IAAY,EAAE,OAA6B;IAC3F,KAAK,IAAI,OAAO,GAAG,CAAC,GAAI,OAAO,IAAI,CAAC,EAAE,CAAC;QACtC,IAAI,CAAC;YACJ,OAAO,MAAM,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAChD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,OAAO,KAAK,qBAAqB,IAAI,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,iBAAiB,EAAE,CAAC;gBACxH,MAAM,KAAK,CAAC;YACb,CAAC;YACD,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,GAAG,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9E,CAAC;IACF,CAAC;AACF,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAoB,EAAE,IAAY,EAAE,OAA2C;IAChH,MAAM,UAAU,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAC7C,MAAM,OAAO,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,GAAG,OAAO,EAAE,UAAU,EAAE,CAAC;IAC9E,MAAM,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACtC,MAAM,WAAW,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IACzC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC7C,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACvB,OAAO;IACR,CAAC;IACD,MAAM,SAAS,GAAI,CAAC,WAAW,EAAE,QAAQ,CAAW,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;IACnI,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,uBAAuB,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACxE,CAAC;IACD,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,GAAG,OAAO,CAAC;IACvE,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC;IAC5H,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,OAAO;IACR,CAAC;IACD,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QACtC,MAAM,IAAI,SAAS,CAAC,kDAAkD,CAAC,CAAC;IACzE,CAAC;IACD,2EAA2E;IAC3E,MAAM,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AACzC,CAAC"}
|
|
@@ -11,6 +11,6 @@ export interface AcquiredClient {
|
|
|
11
11
|
readonly client: PgBossClient;
|
|
12
12
|
readonly reused: boolean;
|
|
13
13
|
}
|
|
14
|
-
export declare
|
|
15
|
-
export declare
|
|
14
|
+
export declare function acquireClient(options: AcquireClientOptions): Effect.Effect<AcquiredClient, import("#error.ts").PgBossError>;
|
|
15
|
+
export declare function releaseClient(acquired: AcquiredClient, stopOptions?: StopOptions): Effect.Effect<void>;
|
|
16
16
|
//# sourceMappingURL=lifecycle.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../src/server/internal/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,KAAK,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAwB,KAAK,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../src/server/internal/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,KAAK,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAwB,KAAK,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAK/F,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACtD,QAAQ,CAAC,aAAa,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC;IACzD,QAAQ,CAAC,WAAW,EAAE,kBAAkB,CAAC;CACzC;AAED,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CACzB;AAkBD,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,WAAW,EAAE,WAAW,CAAC,CAgB3H;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,cAAc,EAAE,WAAW,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAUtG"}
|
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
2
|
import { defaultClientFactory } from "#client.ts";
|
|
3
3
|
import { toPgBossError } from "#error.ts";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Reflect.set(globalThis, CacheKey, clientCache);
|
|
8
|
-
function isCachedClient(value) {
|
|
9
|
-
return (typeof value === "object"
|
|
10
|
-
&& value !== null
|
|
11
|
-
&& Reflect.get(value, "client") instanceof Promise
|
|
12
|
-
&& typeof Reflect.get(value, "references") === "number");
|
|
13
|
-
}
|
|
14
|
-
function cachedClient(key) {
|
|
15
|
-
const entry = clientCache.get(key);
|
|
16
|
-
return isCachedClient(entry) ? entry : undefined;
|
|
17
|
-
}
|
|
18
|
-
function startClient(options) {
|
|
4
|
+
import { acquireCachedClient, releaseCachedClient } from "./clientCache.js";
|
|
5
|
+
import { logClientError, observeClientErrors } from "./clientErrors.js";
|
|
6
|
+
async function startClient(options, report) {
|
|
19
7
|
const client = (options.clientFactory ?? defaultClientFactory)(options.constructor);
|
|
20
|
-
|
|
8
|
+
observeClientErrors(client, report);
|
|
9
|
+
try {
|
|
10
|
+
await client.start();
|
|
11
|
+
return client;
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
21
14
|
try {
|
|
22
15
|
await client.stop();
|
|
23
16
|
}
|
|
@@ -25,70 +18,34 @@ function startClient(options) {
|
|
|
25
18
|
// Preserve the startup failure; stop is best effort on failed acquisition.
|
|
26
19
|
}
|
|
27
20
|
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function acquireClient(options) {
|
|
24
|
+
return Effect.contextWith((context) => {
|
|
25
|
+
const run = Effect.runForkWith(context);
|
|
26
|
+
function report(error) {
|
|
27
|
+
run(logClientError(error));
|
|
28
|
+
}
|
|
29
|
+
return Effect.tryPromise({
|
|
30
|
+
catch: (error) => toPgBossError("start", error),
|
|
31
|
+
try: async () => {
|
|
32
|
+
const cacheKey = options.clientCacheKey;
|
|
33
|
+
return cacheKey === undefined
|
|
34
|
+
? { client: await startClient(options, report), reused: false }
|
|
35
|
+
: { cacheKey, ...(await acquireCachedClient(cacheKey, () => startClient(options, report))) };
|
|
36
|
+
},
|
|
37
|
+
});
|
|
28
38
|
});
|
|
29
39
|
}
|
|
30
|
-
export
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
reused: false,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
const existing = cachedClient(cacheKey);
|
|
41
|
-
if (existing !== undefined) {
|
|
42
|
-
existing.references += 1;
|
|
43
|
-
try {
|
|
44
|
-
return {
|
|
45
|
-
cacheKey,
|
|
46
|
-
client: await existing.client,
|
|
47
|
-
reused: true,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
existing.references -= 1;
|
|
52
|
-
throw error;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
const entry = {
|
|
56
|
-
client: startClient(options),
|
|
57
|
-
references: 1,
|
|
58
|
-
};
|
|
59
|
-
clientCache.set(cacheKey, entry);
|
|
60
|
-
try {
|
|
61
|
-
return {
|
|
62
|
-
cacheKey,
|
|
63
|
-
client: await entry.client,
|
|
64
|
-
reused: false,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
catch (error) {
|
|
68
|
-
if (clientCache.get(cacheKey) === entry) {
|
|
69
|
-
clientCache.delete(cacheKey);
|
|
40
|
+
export function releaseClient(acquired, stopOptions) {
|
|
41
|
+
return Effect.tryPromise({
|
|
42
|
+
catch: (error) => toPgBossError("stop", error),
|
|
43
|
+
try: async () => {
|
|
44
|
+
const client = acquired.cacheKey === undefined ? acquired.client : await releaseCachedClient(acquired.cacheKey);
|
|
45
|
+
if (client !== undefined) {
|
|
46
|
+
await client.stop(stopOptions);
|
|
70
47
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
});
|
|
75
|
-
export const releaseClient = (acquired, stopOptions) => Effect.tryPromise({
|
|
76
|
-
catch: (error) => toPgBossError("stop", error),
|
|
77
|
-
try: async () => {
|
|
78
|
-
if (acquired.cacheKey === undefined) {
|
|
79
|
-
await acquired.client.stop(stopOptions);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
const entry = cachedClient(acquired.cacheKey);
|
|
83
|
-
if (entry === undefined) {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
entry.references -= 1;
|
|
87
|
-
if (entry.references > 0) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
clientCache.delete(acquired.cacheKey);
|
|
91
|
-
await acquired.client.stop(stopOptions);
|
|
92
|
-
},
|
|
93
|
-
}).pipe(Effect.catch((error) => Effect.logError(error)));
|
|
48
|
+
},
|
|
49
|
+
}).pipe(Effect.catch((error) => Effect.logError(error)));
|
|
50
|
+
}
|
|
94
51
|
//# sourceMappingURL=lifecycle.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../../../src/server/internal/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,oBAAoB,EAA+C,MAAM,YAAY,CAAC;AAC/F,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../../../src/server/internal/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,oBAAoB,EAA+C,MAAM,YAAY,CAAC;AAC/F,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAcxE,KAAK,UAAU,WAAW,CAAC,OAA6B,EAAE,MAA8B;IACvF,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,aAAa,IAAI,oBAAoB,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACpF,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,IAAI,CAAC;QACJ,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,OAAO,MAAM,CAAC;IACf,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,IAAI,CAAC;YACJ,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC;YACR,2EAA2E;QAC5E,CAAC;QACD,MAAM,KAAK,CAAC;IACb,CAAC;AACF,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,OAA6B;IAC1D,OAAO,MAAM,CAAC,WAAW,CAAC,CAAC,OAAO,EAAE,EAAE;QACrC,MAAM,GAAG,GAAG,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QACxC,SAAS,MAAM,CAAC,KAAY;YAC3B,GAAG,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5B,CAAC;QACD,OAAO,MAAM,CAAC,UAAU,CAAC;YACxB,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC;YAC/C,GAAG,EAAE,KAAK,IAAI,EAAE;gBACf,MAAM,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC;gBACxC,OAAO,QAAQ,KAAK,SAAS;oBAC5B,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE;oBAC/D,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,mBAAmB,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/F,CAAC;SACD,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,QAAwB,EAAE,WAAyB;IAChF,OAAO,MAAM,CAAC,UAAU,CAAC;QACxB,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,aAAa,CAAC,MAAM,EAAE,KAAK,CAAC;QAC9C,GAAG,EAAE,KAAK,IAAI,EAAE;YACf,MAAM,MAAM,GAAG,QAAQ,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,mBAAmB,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAChH,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBAC1B,MAAM,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YAChC,CAAC;QACF,CAAC;KACD,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAC1D,CAAC"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type Context, Effect } from "effect";
|
|
2
2
|
import type { PgBossClient } from "#client.ts";
|
|
3
3
|
import { type JobRegistration } from "#definition.ts";
|
|
4
|
-
import { PgBossQueueOptionsError } from "#error.ts";
|
|
4
|
+
import { PgBossError, PgBossQueueOptionsError } from "#error.ts";
|
|
5
5
|
export declare function registrationName(registration: JobRegistration): string;
|
|
6
6
|
export declare const registrationNames: (registrations: readonly JobRegistration[]) => readonly string[];
|
|
7
|
-
export declare const registerJobs: <R>(client: PgBossClient, registrations: readonly JobRegistration[], context: Context.Context<R>, replaceWorkers: boolean) => Effect.Effect<void,
|
|
7
|
+
export declare const registerJobs: <R>(client: PgBossClient, registrations: readonly JobRegistration[], context: Context.Context<R>, replaceWorkers: boolean) => Effect.Effect<void, PgBossError | PgBossQueueOptionsError>;
|
|
8
8
|
//# sourceMappingURL=register.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../../../src/server/internal/register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,EAAoB,MAAM,QAAQ,CAAC;AAEhE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EACN,KAAK,eAAe,EAMpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAsB,uBAAuB,EAAiB,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../../../src/server/internal/register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,EAAoB,MAAM,QAAQ,CAAC;AAEhE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EACN,KAAK,eAAe,EAMpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,WAAW,EAAsB,uBAAuB,EAAiB,MAAM,WAAW,CAAC;AA8BpG,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,eAAe,GAAG,MAAM,CAEtE;AA+DD,eAAO,MAAM,iBAAiB,kBAAmB,SAAS,eAAe,EAAE,KAAG,SAAS,MAAM,EAM5F,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,CAAC,UACrB,YAAY,iBACL,SAAS,eAAe,EAAE,WAChC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,kBACX,OAAO,KACrB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,GAAG,uBAAuB,CAezD,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Effect, Redacted, Schema } from "effect";
|
|
2
2
|
import { jobContext, } from "#definition.ts";
|
|
3
|
-
import { PgBossPayloadError, PgBossQueueOptionsError, toPgBossError } from "#error.ts";
|
|
3
|
+
import { PgBossError, PgBossPayloadError, PgBossQueueOptionsError, toPgBossError } from "#error.ts";
|
|
4
4
|
import { batchHandler } from "#internal/batchHandler.ts";
|
|
5
5
|
import { ensureQueue } from "#internal/ensureQueue.ts";
|
|
6
6
|
import { payloadOf, traceJob } from "#internal/tracing.ts";
|
|
@@ -36,7 +36,12 @@ async function prepareQueues(client, registrations, replaceWorkers) {
|
|
|
36
36
|
}
|
|
37
37
|
for (const registration of registrations) {
|
|
38
38
|
const definition = registrationDefinition(registration);
|
|
39
|
-
|
|
39
|
+
try {
|
|
40
|
+
await ensureQueue(client, definition.name, definition.queueOptions);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw error instanceof PgBossQueueOptionsError ? error : toPgBossError("register", error, definition.name);
|
|
44
|
+
}
|
|
40
45
|
}
|
|
41
46
|
}
|
|
42
47
|
function workerContext(context) {
|
|
@@ -70,7 +75,7 @@ export const registrationNames = (registrations) => {
|
|
|
70
75
|
return names;
|
|
71
76
|
};
|
|
72
77
|
export const registerJobs = (client, registrations, context, replaceWorkers) => Effect.tryPromise({
|
|
73
|
-
catch: (error) => (error instanceof PgBossQueueOptionsError ? error : toPgBossError("register", error)),
|
|
78
|
+
catch: (error) => (error instanceof PgBossQueueOptionsError || error instanceof PgBossError ? error : toPgBossError("register", error)),
|
|
74
79
|
try: async () => {
|
|
75
80
|
registrationNames(registrations);
|
|
76
81
|
await prepareQueues(client, registrations, replaceWorkers);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register.js","sourceRoot":"","sources":["../../../src/server/internal/register.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGhE,OAAO,EAEN,UAAU,GAKV,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"register.js","sourceRoot":"","sources":["../../../src/server/internal/register.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/server/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC1E,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC7F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AACnH,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AAC1F,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/server/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC1E,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC7F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AACnH,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AAC1F,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAM1D,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;IACrF,QAAQ,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,KAAK,SAAS,MAAM,EAAE,KAAK,CAAC,QAAQ,SAAS,gBAAgB,EACrF,KAAK,EAAE,eAAe,CAAC,KAAK,EAAE,QAAQ,CAAC,EACvC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,EACrC,OAAO,CAAC,EAAE,WAAW,KACjB,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,kBAAkB,EAAE,QAAQ,CAAC,kBAAkB,CAAC,CAAC,CAAC;IAC1G,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,KAAK,SAAS,MAAM,EAAE,KAAK,CAAC,QAAQ,SAAS,gBAAgB,EACpF,KAAK,EAAE,eAAe,CAAC,KAAK,EAAE,QAAQ,CAAC,EACvC,EAAE,EAAE,MAAM,KACN,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,WAAW,GAAG,kBAAkB,EAAE,QAAQ,CAAC,kBAAkB,CAAC,CAAC,CAAC;IACrI,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;IACxD,QAAQ,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,eAAe,EAAE,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;CACzF;AAED,MAAM,MAAM,kBAAkB,CAAC,cAAc,SAAS,SAAS,eAAe,EAAE,EAAE,kBAAkB,IAAI,kBAAkB,GAAG;IAC5H,QAAQ,CAAC,aAAa,CAAC,EAAE,mBAAmB,CAAC;IAE7C,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACtD,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,kBAAkB,CAAC,CAAC;IACvF,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAC;CAC5B,CAAC;AAEF,MAAM,WAAW,gBAAgB,CAAC,WAAW,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,aAAa,CAAC;IACzH,QAAQ,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,cAAc,SAAS,SAAS,eAAe,EAAE,EAAE,kBAAkB,GAAG,KAAK,EACnG,OAAO,EAAE,kBAAkB,CAAC,cAAc,EAAE,kBAAkB,CAAC,KAC3D,KAAK,CAAC,KAAK,CACf,WAAW,EACX,WAAW,GAAG,uBAAuB,EACrC,OAAO,CAAC,wBAAwB,CAAC,cAAc,CAAC,EAAE,WAAW,CAAC,GAAG,kBAAkB,CACnF,CAAC;CACF;AAED,eAAO,MAAM,UAAU,GAAI,KAAK,CAAC,WAAW,SAAS,MAAM,cAAc,WAAW,KAAG,gBAAgB,CAAC,WAAW,CA6ClH,CAAC"}
|
package/dist/server/service.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
import { logClientError, subscribeClientErrors } from "#internal/clientErrors.ts";
|
|
2
3
|
import { acquireClient, releaseClient } from "#internal/lifecycle.ts";
|
|
3
4
|
import { registerJobs, registrationName } from "#internal/register.ts";
|
|
4
5
|
import { makeService } from "#internal/service.ts";
|
|
5
|
-
function logClientError(error) {
|
|
6
|
-
return Effect.logError({ event: "pg_boss_error", message: error.message });
|
|
7
|
-
}
|
|
8
6
|
export const makePgBoss = (identifier) => {
|
|
9
7
|
const Service = Context.Service(identifier);
|
|
10
8
|
const layer = (options) => {
|
|
@@ -23,16 +21,10 @@ export const makePgBoss = (identifier) => {
|
|
|
23
21
|
const errorListener = (error) => {
|
|
24
22
|
void Effect.runPromise(Effect.exit(Effect.provide(reportError(error), context)));
|
|
25
23
|
};
|
|
26
|
-
acquired.client
|
|
24
|
+
const unsubscribe = subscribeClientErrors(acquired.client, errorListener);
|
|
27
25
|
const registration = registerJobs(acquired.client, jobs, context, acquired.reused);
|
|
28
|
-
return yield* registration.pipe(Effect.as({ acquired,
|
|
29
|
-
|
|
30
|
-
return releaseClient(acquired, stop);
|
|
31
|
-
}));
|
|
32
|
-
}), (resource) => {
|
|
33
|
-
resource.acquired.client.off("error", resource.errorListener);
|
|
34
|
-
return releaseClient(resource.acquired, stop);
|
|
35
|
-
});
|
|
26
|
+
return yield* registration.pipe(Effect.as({ acquired, service, unsubscribe }), Effect.onError(() => releaseClient(acquired, stop).pipe(Effect.ensuring(Effect.sync(unsubscribe)))));
|
|
27
|
+
}), (resource) => releaseClient(resource.acquired, stop).pipe(Effect.ensuring(Effect.sync(resource.unsubscribe))));
|
|
36
28
|
return Layer.effect(Service, Effect.map(acquire, ({ service }) => service));
|
|
37
29
|
};
|
|
38
30
|
return Object.assign(Service, { layer });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","sourceRoot":"","sources":["../../src/server/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAA4B,MAAM,QAAQ,CAAC;AAM1E,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../../src/server/service.ts"],"names":[],"mappings":"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"}
|
package/docs/roadmap.md
CHANGED
|
@@ -4,10 +4,10 @@ This file owns the package's implementation inventory and open API choices. The
|
|
|
4
4
|
|
|
5
5
|
## Built
|
|
6
6
|
|
|
7
|
-
- [x] `makePgBoss` retains its literal service identity in `PgBossDefinition` and its Layer type, so a different jobs Layer does not satisfy the required service. [Compiler evidence](https://github.com/ShivaeDev/platform/blob/main/packages/effect-pg-boss/src/service.typecheck.test.ts).
|
|
7
|
+
- [x] `makePgBoss` retains its literal service identity in `PgBossDefinition` and its Layer type, so a different jobs Layer does not satisfy the required service. [Compiler evidence](https://github.com/ShivaeDev/platform/blob/main/packages/effect-pg-boss/src/server/service.typecheck.test.ts).
|
|
8
8
|
- [x] Object payload Schema definitions, decoded enqueue types and queue workers. Type tests reject primitive schemas, missing fields and encoded values passed as decoded payloads.
|
|
9
9
|
- [x] Encoding at enqueue and decoding before the handler. The PostgreSQL test round-trips a transformed payload and checks that malformed stored data reaches the dead-letter queue without calling the domain handler.
|
|
10
|
-
- [x] Queue and schedule registration, with dead-letter queues, default retry settings and UTC schedule registration.
|
|
10
|
+
- [x] Queue and schedule registration, with dead-letter queues, default retry settings and UTC schedule registration. The [PostgreSQL schedule test](../src/server/schedulePostgres.spec.ts) reads the stored cron, explicit UTC option, default retries and dead-letter queue through pg-boss. Fake-client tests also verify a retry-limit override.
|
|
11
11
|
- [x] Producer-only queue definitions create queues and dead-letter queues through `Jobs.layer({ jobs: [Queue] })`, without starting a consumer. The [PostgreSQL producer test](../src/server/producerPostgres.spec.ts) enqueues a transformed payload and observes worker retry exhaustion and dead-letter delivery. The [starter producer](../../../examples/starter/src/server/noteProducer.ts) uses this API.
|
|
12
12
|
- [x] Native pg-boss burst defaults (`batchSize: 10`, `burstWhenBatchFull: true`) and configurable batches and local concurrency. [PostgreSQL backlog evidence](../src/server/throughputPostgres.spec.ts) drains 30 jobs within 2.5 seconds.
|
|
13
13
|
- [x] Handlers and scheduled effects can yield their owning jobs service without an external self requirement. [PostgreSQL follow-up evidence](../src/server/followUpPostgres.spec.ts) completes a worker chain and enqueues to a producer-only queue using one scoped client. All declared queues exist before consumers start.
|
|
@@ -15,9 +15,10 @@ This file owns the package's implementation inventory and open API choices. The
|
|
|
15
15
|
- [x] Scoped startup and shutdown, including cleanup when startup or registration fails.
|
|
16
16
|
- [x] Explicit keyed client reuse and unkeyed per-build acquisition. Lifecycle tests cover two builds sharing one client and replacing the registered worker.
|
|
17
17
|
- [x] Full pg-boss queue creation options, excluding the adapter's name and dead-letter setup. [PostgreSQL evidence](../src/server/notifyPostgres.spec.ts) verifies notification delivery to a waiting worker and immediate live counts after completion; [type tests](../src/server/service.typecheck.test.ts) enforce option ownership.
|
|
18
|
-
- [x] Schema-decoded lookup by job ID and retry under the same ID for failed or retry-waiting jobs. [PostgreSQL evidence](../src/server/jobControlPostgres.spec.ts) verifies both states complete after a manual retry
|
|
18
|
+
- [x] Schema-decoded lookup by job ID and retry under the same ID for failed or retry-waiting jobs. [PostgreSQL evidence](../src/server/jobControlPostgres.spec.ts) verifies both states complete after a manual retry, missing lookups return `None` and duplicate-ID sends return `None`.
|
|
19
19
|
- [x] Startup reconciles declared mutable queue settings and rejects immutable policy or partition changes with a typed error. [PostgreSQL restart evidence](../src/server/queueOptionsPostgres.spec.ts) checks expiry and retry-limit changes on an existing queue.
|
|
20
|
-
- [x] Queue health totals for active, failed, queued, ready and dead-lettered counts
|
|
20
|
+
- [x] Queue health totals for active, failed, queued, ready and dead-lettered counts. The [PostgreSQL health test](../src/server/healthPostgres.spec.ts) checks two populated queues and their totals after the pg-boss monitor refreshes its cached counts, including delayed jobs in queued counts.
|
|
21
|
+
- [x] Background error events reach the captured reporting service and the listener is removed on release, verified with a [fake client](../src/server/edgeCases.spec.ts). Job AbortSignal cancellation interrupts the handler and leaves the rest of the batch to pg-boss expiry, verified with a [fake client](../src/server/settlement.spec.ts).
|
|
21
22
|
- [x] Public defining-module imports and an installed-package type fixture.
|
|
22
23
|
|
|
23
24
|
## Next
|
|
@@ -31,4 +32,4 @@ Enqueueing is separate from application SQL transactions. The package supplies n
|
|
|
31
32
|
## Open questions
|
|
32
33
|
|
|
33
34
|
- Should a shared client key be checked against constructor settings and registration changes? The lifecycle fixture uses the same Layer twice; it does not settle incompatible configurations under one key.
|
|
34
|
-
-
|
|
35
|
+
- What evidence is needed before the guide makes stronger claims about actual cron execution? PostgreSQL tests verify schedule registration; cron execution timing remains untested.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/effect-pg-boss",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Effect-native jobs and lifecycle management for pg-boss",
|
|
5
5
|
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/effect-pg-boss#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -43,17 +43,18 @@
|
|
|
43
43
|
"README.md"
|
|
44
44
|
],
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@shivaedev/
|
|
47
|
-
"@
|
|
46
|
+
"@shivaedev/effect-test-sql": "0.5.3",
|
|
47
|
+
"@shivaedev/quality": "0.23.0",
|
|
48
|
+
"@types/node": "24.19.2",
|
|
48
49
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
49
50
|
"effect": "4.0.4",
|
|
50
|
-
"pg-boss": "12.37.
|
|
51
|
+
"pg-boss": "12.37.1",
|
|
51
52
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
52
53
|
"vitest": "5.0.3"
|
|
53
54
|
},
|
|
54
55
|
"peerDependencies": {
|
|
55
56
|
"effect": "4.0.4",
|
|
56
|
-
"pg-boss": "12.37.
|
|
57
|
+
"pg-boss": "12.37.1"
|
|
57
58
|
},
|
|
58
59
|
"engines": {
|
|
59
60
|
"node": ">=24"
|
package/src/server/error.ts
CHANGED
|
@@ -6,7 +6,21 @@ export class PgBossError extends Data.TaggedError("PgBossError")<{
|
|
|
6
6
|
readonly operation: PgBossOperation;
|
|
7
7
|
readonly queue?: string | undefined;
|
|
8
8
|
readonly original: Redacted.Redacted<unknown>;
|
|
9
|
-
}> {
|
|
9
|
+
}> {
|
|
10
|
+
// The original's message can quote payloads or connection strings, so only its class and code are shown.
|
|
11
|
+
override get message(): string {
|
|
12
|
+
const target = this.queue === undefined ? "" : ` for queue ${this.queue}`;
|
|
13
|
+
return `pg-boss ${this.operation}${target} failed: ${originalKind(Redacted.value(this.original))}`;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function originalKind(original: unknown): string {
|
|
18
|
+
if (!(original instanceof Error)) {
|
|
19
|
+
return typeof original;
|
|
20
|
+
}
|
|
21
|
+
const code: unknown = Reflect.get(original, "code");
|
|
22
|
+
return typeof code === "string" ? `${original.constructor.name} ${code}` : original.constructor.name;
|
|
23
|
+
}
|
|
10
24
|
|
|
11
25
|
export class PgBossQueueOptionsError extends Data.TaggedError("PgBossQueueOptionsError")<{
|
|
12
26
|
readonly queue: string;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { PgBossClient } from "#client.ts";
|
|
2
|
+
|
|
3
|
+
interface CachedClient {
|
|
4
|
+
readonly client: Promise<PgBossClient>;
|
|
5
|
+
references: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const cacheSymbol = Symbol.for("@shivaedev/effect-pg-boss/client-cache");
|
|
9
|
+
const shared: unknown = Reflect.get(globalThis, cacheSymbol);
|
|
10
|
+
const clients: Map<string | symbol, unknown> = shared instanceof Map ? shared : new Map<string | symbol, unknown>();
|
|
11
|
+
Reflect.set(globalThis, cacheSymbol, clients);
|
|
12
|
+
|
|
13
|
+
function entryFor(key: string | symbol): CachedClient | undefined {
|
|
14
|
+
const value = clients.get(key);
|
|
15
|
+
return isCachedClient(value) ? value : undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isCachedClient(value: unknown): value is CachedClient {
|
|
19
|
+
return (
|
|
20
|
+
typeof value === "object"
|
|
21
|
+
&& value !== null
|
|
22
|
+
&& Reflect.get(value, "client") instanceof Promise
|
|
23
|
+
&& typeof Reflect.get(value, "references") === "number"
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function acquireCachedClient(key: string | symbol, start: () => Promise<PgBossClient>) {
|
|
28
|
+
const existing = entryFor(key);
|
|
29
|
+
if (existing !== undefined) {
|
|
30
|
+
existing.references += 1;
|
|
31
|
+
try {
|
|
32
|
+
return { client: await existing.client, reused: true };
|
|
33
|
+
} catch (error) {
|
|
34
|
+
existing.references -= 1;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const entry: CachedClient = { client: start(), references: 1 };
|
|
39
|
+
clients.set(key, entry);
|
|
40
|
+
try {
|
|
41
|
+
return { client: await entry.client, reused: false };
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (clients.get(key) === entry) {
|
|
44
|
+
clients.delete(key);
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function releaseCachedClient(key: string | symbol): Promise<PgBossClient> | undefined {
|
|
51
|
+
const entry = entryFor(key);
|
|
52
|
+
if (entry === undefined) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
entry.references -= 1;
|
|
56
|
+
if (entry.references > 0) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
clients.delete(key);
|
|
60
|
+
return entry.client;
|
|
61
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { PgBossClient } from "#client.ts";
|
|
3
|
+
|
|
4
|
+
type Listener = (error: Error) => void;
|
|
5
|
+
|
|
6
|
+
interface Observer {
|
|
7
|
+
readonly dispose: () => void;
|
|
8
|
+
readonly listeners: Set<Listener>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const key = Symbol.for("@shivaedev/effect-pg-boss/error-observers");
|
|
12
|
+
const shared: unknown = Reflect.get(globalThis, key);
|
|
13
|
+
const observers: WeakMap<WeakKey, unknown> = shared instanceof WeakMap ? shared : new WeakMap<WeakKey, unknown>();
|
|
14
|
+
Reflect.set(globalThis, key, observers);
|
|
15
|
+
|
|
16
|
+
export function logClientError(error: Error): Effect.Effect<void> {
|
|
17
|
+
const message = { event: "pg_boss_error", message: error.message };
|
|
18
|
+
return Reflect.get(error, "code") === "57P01" ? Effect.logDebug(message) : Effect.logError(message);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function observeClientErrors(client: PgBossClient, fallback: Listener): void {
|
|
22
|
+
closeClientErrors(client);
|
|
23
|
+
const listeners = new Set<Listener>();
|
|
24
|
+
const listener: Listener = (error) => {
|
|
25
|
+
if (listeners.size === 0) {
|
|
26
|
+
fallback(error);
|
|
27
|
+
} else {
|
|
28
|
+
for (const report of listeners) {
|
|
29
|
+
report(error);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
// Pool shutdown can resolve before its clients finish disconnecting.
|
|
34
|
+
client.on("error", listener);
|
|
35
|
+
observers.set(client, { dispose: () => client.off("error", listener), listeners });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function subscribeClientErrors(client: PgBossClient, listener: Listener): () => void {
|
|
39
|
+
const observer = observerFor(client);
|
|
40
|
+
if (observer === undefined) {
|
|
41
|
+
throw new Error("pg-boss client has no error observer");
|
|
42
|
+
}
|
|
43
|
+
observer.listeners.add(listener);
|
|
44
|
+
return () => observer.listeners.delete(listener);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function closeClientErrors(client: PgBossClient): void {
|
|
48
|
+
observerFor(client)?.dispose();
|
|
49
|
+
observers.delete(client);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function observerFor(client: PgBossClient): Observer | undefined {
|
|
53
|
+
const value = observers.get(client);
|
|
54
|
+
return isObserver(value) ? value : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isObserver(value: unknown): value is Observer {
|
|
58
|
+
return (
|
|
59
|
+
typeof value === "object"
|
|
60
|
+
&& value !== null
|
|
61
|
+
&& typeof Reflect.get(value, "dispose") === "function"
|
|
62
|
+
&& Reflect.get(value, "listeners") instanceof Set
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -3,11 +3,29 @@ import type { PgBossClient } from "#client.ts";
|
|
|
3
3
|
import { PgBossQueueOptionsError } from "#error.ts";
|
|
4
4
|
import { deadLetterQueueName } from "#health.ts";
|
|
5
5
|
|
|
6
|
+
const DEADLOCK_DETECTED = "40P01";
|
|
7
|
+
const CREATE_QUEUE_ATTEMPTS = 6;
|
|
8
|
+
|
|
9
|
+
// A partitioned queue attaches its table to pg-boss's job table, which can deadlock with job traffic on other queues.
|
|
10
|
+
// PostgreSQL rolls the whole creation back, so trying it again is safe.
|
|
11
|
+
async function createQueue(client: PgBossClient, name: string, options?: Omit<Queue, "name">): Promise<void> {
|
|
12
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
13
|
+
try {
|
|
14
|
+
return await client.createQueue(name, options);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (attempt === CREATE_QUEUE_ATTEMPTS || !(error instanceof Error) || Reflect.get(error, "code") !== DEADLOCK_DETECTED) {
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
await new Promise((resolve) => setTimeout(resolve, 25 * 2 ** (attempt - 1)));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
6
24
|
export async function ensureQueue(client: PgBossClient, name: string, options: Omit<Queue, "name" | "deadLetter">): Promise<void> {
|
|
7
25
|
const deadLetter = deadLetterQueueName(name);
|
|
8
26
|
const desired = { retryBackoff: true, retryLimit: 3, ...options, deadLetter };
|
|
9
|
-
await
|
|
10
|
-
await
|
|
27
|
+
await createQueue(client, deadLetter);
|
|
28
|
+
await createQueue(client, name, desired);
|
|
11
29
|
const existing = await client.getQueue(name);
|
|
12
30
|
if (existing === null) {
|
|
13
31
|
return;
|
|
@@ -2,30 +2,8 @@ import { Effect } from "effect";
|
|
|
2
2
|
import type { ConstructorOptions, StopOptions } from "pg-boss";
|
|
3
3
|
import { defaultClientFactory, type PgBossClient, type PgBossClientFactory } from "#client.ts";
|
|
4
4
|
import { toPgBossError } from "#error.ts";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
readonly client: Promise<PgBossClient>;
|
|
8
|
-
references: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const CacheKey = Symbol.for("@shivaedev/effect-pg-boss/client-cache");
|
|
12
|
-
const sharedCache: unknown = Reflect.get(globalThis, CacheKey);
|
|
13
|
-
const clientCache: Map<string | symbol, unknown> = sharedCache instanceof Map ? sharedCache : new Map<string | symbol, unknown>();
|
|
14
|
-
Reflect.set(globalThis, CacheKey, clientCache);
|
|
15
|
-
|
|
16
|
-
function isCachedClient(value: unknown): value is CachedClient {
|
|
17
|
-
return (
|
|
18
|
-
typeof value === "object"
|
|
19
|
-
&& value !== null
|
|
20
|
-
&& Reflect.get(value, "client") instanceof Promise
|
|
21
|
-
&& typeof Reflect.get(value, "references") === "number"
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function cachedClient(key: string | symbol): CachedClient | undefined {
|
|
26
|
-
const entry = clientCache.get(key);
|
|
27
|
-
return isCachedClient(entry) ? entry : undefined;
|
|
28
|
-
}
|
|
5
|
+
import { acquireCachedClient, releaseCachedClient } from "./clientCache.ts";
|
|
6
|
+
import { logClientError, observeClientErrors } from "./clientErrors.ts";
|
|
29
7
|
|
|
30
8
|
export interface AcquireClientOptions {
|
|
31
9
|
readonly clientCacheKey?: string | symbol | undefined;
|
|
@@ -39,86 +17,48 @@ export interface AcquiredClient {
|
|
|
39
17
|
readonly reused: boolean;
|
|
40
18
|
}
|
|
41
19
|
|
|
42
|
-
function startClient(options: AcquireClientOptions): Promise<PgBossClient> {
|
|
20
|
+
async function startClient(options: AcquireClientOptions, report: (error: Error) => void): Promise<PgBossClient> {
|
|
43
21
|
const client = (options.clientFactory ?? defaultClientFactory)(options.constructor);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
22
|
+
observeClientErrors(client, report);
|
|
23
|
+
try {
|
|
24
|
+
await client.start();
|
|
25
|
+
return client;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
try {
|
|
28
|
+
await client.stop();
|
|
29
|
+
} catch {
|
|
30
|
+
// Preserve the startup failure; stop is best effort on failed acquisition.
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
55
34
|
}
|
|
56
35
|
|
|
57
|
-
export
|
|
58
|
-
Effect.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
return {
|
|
74
|
-
cacheKey,
|
|
75
|
-
client: await existing.client,
|
|
76
|
-
reused: true,
|
|
77
|
-
};
|
|
78
|
-
} catch (error) {
|
|
79
|
-
existing.references -= 1;
|
|
80
|
-
throw error;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const entry: CachedClient = {
|
|
85
|
-
client: startClient(options),
|
|
86
|
-
references: 1,
|
|
87
|
-
};
|
|
88
|
-
clientCache.set(cacheKey, entry);
|
|
89
|
-
try {
|
|
90
|
-
return {
|
|
91
|
-
cacheKey,
|
|
92
|
-
client: await entry.client,
|
|
93
|
-
reused: false,
|
|
94
|
-
};
|
|
95
|
-
} catch (error) {
|
|
96
|
-
if (clientCache.get(cacheKey) === entry) {
|
|
97
|
-
clientCache.delete(cacheKey);
|
|
98
|
-
}
|
|
99
|
-
throw error;
|
|
100
|
-
}
|
|
101
|
-
},
|
|
36
|
+
export function acquireClient(options: AcquireClientOptions): Effect.Effect<AcquiredClient, import("#error.ts").PgBossError> {
|
|
37
|
+
return Effect.contextWith((context) => {
|
|
38
|
+
const run = Effect.runForkWith(context);
|
|
39
|
+
function report(error: Error) {
|
|
40
|
+
run(logClientError(error));
|
|
41
|
+
}
|
|
42
|
+
return Effect.tryPromise({
|
|
43
|
+
catch: (error) => toPgBossError("start", error),
|
|
44
|
+
try: async () => {
|
|
45
|
+
const cacheKey = options.clientCacheKey;
|
|
46
|
+
return cacheKey === undefined
|
|
47
|
+
? { client: await startClient(options, report), reused: false }
|
|
48
|
+
: { cacheKey, ...(await acquireCachedClient(cacheKey, () => startClient(options, report))) };
|
|
49
|
+
},
|
|
50
|
+
});
|
|
102
51
|
});
|
|
52
|
+
}
|
|
103
53
|
|
|
104
|
-
export
|
|
105
|
-
Effect.tryPromise({
|
|
54
|
+
export function releaseClient(acquired: AcquiredClient, stopOptions?: StopOptions): Effect.Effect<void> {
|
|
55
|
+
return Effect.tryPromise({
|
|
106
56
|
catch: (error) => toPgBossError("stop", error),
|
|
107
57
|
try: async () => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const entry = cachedClient(acquired.cacheKey);
|
|
114
|
-
if (entry === undefined) {
|
|
115
|
-
return;
|
|
58
|
+
const client = acquired.cacheKey === undefined ? acquired.client : await releaseCachedClient(acquired.cacheKey);
|
|
59
|
+
if (client !== undefined) {
|
|
60
|
+
await client.stop(stopOptions);
|
|
116
61
|
}
|
|
117
|
-
entry.references -= 1;
|
|
118
|
-
if (entry.references > 0) {
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
clientCache.delete(acquired.cacheKey);
|
|
122
|
-
await acquired.client.stop(stopOptions);
|
|
123
62
|
},
|
|
124
63
|
}).pipe(Effect.catch((error) => Effect.logError(error)));
|
|
64
|
+
}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
type ScheduleDefinition,
|
|
10
10
|
type ScheduledWorker,
|
|
11
11
|
} from "#definition.ts";
|
|
12
|
-
import { PgBossPayloadError, PgBossQueueOptionsError, toPgBossError } from "#error.ts";
|
|
12
|
+
import { PgBossError, PgBossPayloadError, PgBossQueueOptionsError, toPgBossError } from "#error.ts";
|
|
13
13
|
import { batchHandler } from "#internal/batchHandler.ts";
|
|
14
14
|
import { ensureQueue } from "#internal/ensureQueue.ts";
|
|
15
15
|
import { payloadOf, traceJob } from "#internal/tracing.ts";
|
|
@@ -51,7 +51,11 @@ async function prepareQueues(client: PgBossClient, registrations: readonly JobRe
|
|
|
51
51
|
}
|
|
52
52
|
for (const registration of registrations) {
|
|
53
53
|
const definition = registrationDefinition(registration);
|
|
54
|
-
|
|
54
|
+
try {
|
|
55
|
+
await ensureQueue(client, definition.name, definition.queueOptions);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw error instanceof PgBossQueueOptionsError ? error : toPgBossError("register", error, definition.name);
|
|
58
|
+
}
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
61
|
|
|
@@ -113,9 +117,9 @@ export const registerJobs = <R>(
|
|
|
113
117
|
registrations: readonly JobRegistration[],
|
|
114
118
|
context: Context.Context<R>,
|
|
115
119
|
replaceWorkers: boolean,
|
|
116
|
-
): Effect.Effect<void,
|
|
120
|
+
): Effect.Effect<void, PgBossError | PgBossQueueOptionsError> =>
|
|
117
121
|
Effect.tryPromise({
|
|
118
|
-
catch: (error) => (error instanceof PgBossQueueOptionsError ? error : toPgBossError("register", error)),
|
|
122
|
+
catch: (error) => (error instanceof PgBossQueueOptionsError || error instanceof PgBossError ? error : toPgBossError("register", error)),
|
|
119
123
|
try: async () => {
|
|
120
124
|
registrationNames(registrations);
|
|
121
125
|
await prepareQueues(client, registrations, replaceWorkers);
|
package/src/server/service.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { PgBossClientFactory } from "#client.ts";
|
|
|
4
4
|
import type { JobPayloadSchema, JobRegistration, QueueDefinition, RegistrationRequirements } from "#definition.ts";
|
|
5
5
|
import type { PgBossError, PgBossPayloadError, PgBossQueueOptionsError } from "#error.ts";
|
|
6
6
|
import type { JobsHealth, QueueCounts } from "#health.ts";
|
|
7
|
+
import { logClientError, subscribeClientErrors } from "#internal/clientErrors.ts";
|
|
7
8
|
import { acquireClient, releaseClient } from "#internal/lifecycle.ts";
|
|
8
9
|
import { registerJobs, registrationName } from "#internal/register.ts";
|
|
9
10
|
import { makeService } from "#internal/service.ts";
|
|
@@ -42,10 +43,6 @@ export interface PgBossDefinition<TIdentifier extends string = string> extends C
|
|
|
42
43
|
>;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
function logClientError(error: Error): Effect.Effect<void> {
|
|
46
|
-
return Effect.logError({ event: "pg_boss_error", message: error.message });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
46
|
export const makePgBoss = <const TIdentifier extends string>(identifier: TIdentifier): PgBossDefinition<TIdentifier> => {
|
|
50
47
|
const Service = Context.Service<TIdentifier, PgBossService>(identifier);
|
|
51
48
|
|
|
@@ -74,20 +71,14 @@ export const makePgBoss = <const TIdentifier extends string>(identifier: TIdenti
|
|
|
74
71
|
const errorListener = (error: Error) => {
|
|
75
72
|
void Effect.runPromise(Effect.exit(Effect.provide(reportError(error), context)));
|
|
76
73
|
};
|
|
77
|
-
acquired.client
|
|
74
|
+
const unsubscribe = subscribeClientErrors(acquired.client, errorListener);
|
|
78
75
|
const registration = registerJobs(acquired.client, jobs, context, acquired.reused);
|
|
79
76
|
return yield* registration.pipe(
|
|
80
|
-
Effect.as({ acquired,
|
|
81
|
-
Effect.onError(() =>
|
|
82
|
-
acquired.client.off("error", errorListener);
|
|
83
|
-
return releaseClient(acquired, stop);
|
|
84
|
-
}),
|
|
77
|
+
Effect.as({ acquired, service, unsubscribe }),
|
|
78
|
+
Effect.onError(() => releaseClient(acquired, stop).pipe(Effect.ensuring(Effect.sync(unsubscribe)))),
|
|
85
79
|
);
|
|
86
80
|
}),
|
|
87
|
-
(resource) =>
|
|
88
|
-
resource.acquired.client.off("error", resource.errorListener);
|
|
89
|
-
return releaseClient(resource.acquired, stop);
|
|
90
|
-
},
|
|
81
|
+
(resource) => releaseClient(resource.acquired, stop).pipe(Effect.ensuring(Effect.sync(resource.unsubscribe))),
|
|
91
82
|
);
|
|
92
83
|
|
|
93
84
|
return Layer.effect(
|