@pattern-stack/codegen 0.17.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +106 -2
- package/dist/{chunk-SFQRETXJ.js → chunk-2VGVSL2D.js} +6 -6
- package/dist/{chunk-VNBC3VXM.js → chunk-3A34R6CI.js} +7 -7
- package/dist/{chunk-FVNAU7VO.js → chunk-7MMS36AN.js} +6 -6
- package/dist/{chunk-FWRL7BZ5.js → chunk-C5E7H553.js} +25 -15
- package/dist/chunk-C5E7H553.js.map +1 -0
- package/dist/{chunk-IOQMMH6C.js → chunk-CFFTPWHM.js} +79 -4
- package/dist/chunk-CFFTPWHM.js.map +1 -0
- package/dist/{chunk-HOIRY5XP.js → chunk-EWYI5GGJ.js} +10 -10
- package/dist/{chunk-BHZP6LOV.js → chunk-IN3EWFB4.js} +4 -4
- package/dist/{chunk-CZQUOIDY.js → chunk-J7JMVS2B.js} +4 -4
- package/dist/{chunk-KSTZIULO.js → chunk-K2I6XIK5.js} +4 -4
- package/dist/{chunk-T6SCOJF4.js → chunk-NXHL5YII.js} +4 -4
- package/dist/{chunk-JA7GJDNI.js → chunk-PKDS6QIJ.js} +4 -4
- package/dist/{chunk-MYQIQ27N.js → chunk-Q6LRJ4VI.js} +51 -2
- package/dist/chunk-Q6LRJ4VI.js.map +1 -0
- package/dist/{chunk-EJBK7I4F.js → chunk-R4BPUUB5.js} +3 -3
- package/dist/{chunk-4PFF3ED4.js → chunk-RKNW56RU.js} +5 -5
- package/dist/{chunk-SGSWVNNB.js → chunk-TBGTMALE.js} +4 -4
- package/dist/{chunk-GM3RMJIJ.js → chunk-VHAR2BGH.js} +4 -4
- package/dist/{chunk-DUMI2J5M.js → chunk-VQOAATIG.js} +4 -4
- package/dist/{chunk-HPS554L4.js → chunk-X6BP6LI5.js} +6 -6
- package/dist/{chunk-PSDVGPQR.js → chunk-YZLBU6O2.js} +9 -9
- package/dist/runtime/shared/openapi/index.js +3 -3
- package/dist/runtime/subsystems/analytics/analytics.module.js +2 -2
- package/dist/runtime/subsystems/analytics/index.js +4 -4
- package/dist/runtime/subsystems/auth/auth.module.js +3 -3
- package/dist/runtime/subsystems/auth/index.js +10 -10
- package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js +2 -2
- package/dist/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.js +2 -2
- package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js +6 -6
- package/dist/runtime/subsystems/bridge/bridge.module.js +17 -17
- package/dist/runtime/subsystems/bridge/index.js +24 -24
- package/dist/runtime/subsystems/cache/cache.module.js +1 -1
- package/dist/runtime/subsystems/cache/index.js +3 -3
- package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +3 -3
- package/dist/runtime/subsystems/events/events.module.js +6 -6
- package/dist/runtime/subsystems/events/generated/bus.js +2 -2
- package/dist/runtime/subsystems/events/generated/index.js +2 -2
- package/dist/runtime/subsystems/events/index.js +10 -10
- package/dist/runtime/subsystems/index.js +64 -64
- package/dist/runtime/subsystems/integration/index.js +10 -10
- package/dist/runtime/subsystems/integration/integration.module.js +2 -2
- package/dist/runtime/subsystems/jobs/index.js +21 -21
- package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +5 -5
- package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.js +3 -3
- package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js +2 -2
- package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +3 -3
- package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +2 -2
- package/dist/runtime/subsystems/jobs/job-worker.d.ts +8 -0
- package/dist/runtime/subsystems/jobs/job-worker.js +3 -3
- package/dist/runtime/subsystems/jobs/job-worker.module.js +11 -11
- package/dist/runtime/subsystems/jobs/jobs-domain.module.js +9 -9
- package/dist/runtime/subsystems/jobs/pg-notify.d.ts +25 -1
- package/dist/runtime/subsystems/jobs/pg-notify.js +1 -1
- package/dist/src/cli/index.js +1408 -245
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/index.d.ts +18 -0
- package/dist/src/index.js +5 -5
- package/package.json +1 -1
- package/runtime/subsystems/jobs/job-worker.ts +29 -11
- package/runtime/subsystems/jobs/pg-notify.ts +63 -3
- package/src/config/locations.mjs +0 -6
- package/src/config/paths.mjs +0 -13
- package/templates/entity/new/prompt.js +12 -88
- package/dist/chunk-FWRL7BZ5.js.map +0 -1
- package/dist/chunk-IOQMMH6C.js.map +0 -1
- package/dist/chunk-MYQIQ27N.js.map +0 -1
- package/templates/entity/new/frontend/_inject-entities-entry.ejs.t +0 -7
- package/templates/entity/new/frontend/_inject-entities-import.ejs.t +0 -7
- package/templates/entity/new/frontend/collections/_ensure-anchor-collections.ejs.t +0 -10
- package/templates/entity/new/frontend/collections/_inject-index.ejs.t +0 -9
- package/templates/entity/new/frontend/collections/_inject-schema-import.ejs.t +0 -9
- package/templates/entity/new/frontend/collections/collection.ejs.t +0 -86
- package/templates/entity/new/frontend/collections/collections-base.ejs.t +0 -35
- package/templates/entity/new/frontend/entity/collection.ejs.t +0 -173
- package/templates/entity/new/frontend/entity/combined.ejs.t +0 -505
- package/templates/entity/new/frontend/entity/fields.ejs.t +0 -105
- package/templates/entity/new/frontend/entity/hooks.ejs.t +0 -74
- package/templates/entity/new/frontend/entity/index.ejs.t +0 -22
- package/templates/entity/new/frontend/entity/mutation-hooks.ejs.t +0 -85
- package/templates/entity/new/frontend/entity/mutations.ejs.t +0 -39
- package/templates/entity/new/frontend/entity/types.ejs.t +0 -60
- package/templates/entity/new/frontend/generated/_inject-index-export.ejs.t +0 -7
- package/templates/entity/new/frontend/generated/_inject-index-import.ejs.t +0 -7
- package/templates/entity/new/frontend/generated/_inject-index-registry.ejs.t +0 -7
- package/templates/entity/new/frontend/store/_inject-collection-import.ejs.t +0 -9
- package/templates/entity/new/frontend/store/_inject-collections.ejs.t +0 -9
- package/templates/entity/new/frontend/store/_inject-entity.ejs.t +0 -9
- package/templates/entity/new/frontend/store/_inject-import.ejs.t +0 -9
- package/templates/entity/new/frontend/store/_inject-lookups.ejs.t +0 -9
- package/templates/entity/new/frontend/store/_inject-resolve.ejs.t +0 -10
- package/templates/entity/new/frontend/store/hooks.ejs.t +0 -73
- package/templates/entity/new/frontend/unified-entity.ejs.t +0 -29
- /package/dist/{chunk-SFQRETXJ.js.map → chunk-2VGVSL2D.js.map} +0 -0
- /package/dist/{chunk-VNBC3VXM.js.map → chunk-3A34R6CI.js.map} +0 -0
- /package/dist/{chunk-FVNAU7VO.js.map → chunk-7MMS36AN.js.map} +0 -0
- /package/dist/{chunk-HOIRY5XP.js.map → chunk-EWYI5GGJ.js.map} +0 -0
- /package/dist/{chunk-BHZP6LOV.js.map → chunk-IN3EWFB4.js.map} +0 -0
- /package/dist/{chunk-CZQUOIDY.js.map → chunk-J7JMVS2B.js.map} +0 -0
- /package/dist/{chunk-KSTZIULO.js.map → chunk-K2I6XIK5.js.map} +0 -0
- /package/dist/{chunk-T6SCOJF4.js.map → chunk-NXHL5YII.js.map} +0 -0
- /package/dist/{chunk-JA7GJDNI.js.map → chunk-PKDS6QIJ.js.map} +0 -0
- /package/dist/{chunk-EJBK7I4F.js.map → chunk-R4BPUUB5.js.map} +0 -0
- /package/dist/{chunk-4PFF3ED4.js.map → chunk-RKNW56RU.js.map} +0 -0
- /package/dist/{chunk-SGSWVNNB.js.map → chunk-TBGTMALE.js.map} +0 -0
- /package/dist/{chunk-GM3RMJIJ.js.map → chunk-VHAR2BGH.js.map} +0 -0
- /package/dist/{chunk-DUMI2J5M.js.map → chunk-VQOAATIG.js.map} +0 -0
- /package/dist/{chunk-HPS554L4.js.map → chunk-X6BP6LI5.js.map} +0 -0
- /package/dist/{chunk-PSDVGPQR.js.map → chunk-YZLBU6O2.js.map} +0 -0
package/dist/src/index.d.ts
CHANGED
|
@@ -89,6 +89,13 @@ interface ParsedEntity {
|
|
|
89
89
|
patternConfig?: Record<string, unknown>;
|
|
90
90
|
/** Whether this entity is a valid scope target for job scoping (JOB-7). */
|
|
91
91
|
scopeable?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Which layers to generate (entity `expose:`, default
|
|
94
|
+
* `['repository', 'rest', 'trpc']`). The frontend field-metadata emitter
|
|
95
|
+
* (FE-3) derives write `capabilities` from whether `repository` or `trpc`
|
|
96
|
+
* is exposed.
|
|
97
|
+
*/
|
|
98
|
+
expose: ('repository' | 'rest' | 'trpc' | 'electric')[];
|
|
92
99
|
folderStructure: 'nested' | 'flat';
|
|
93
100
|
fields: Map<string, ParsedField>;
|
|
94
101
|
relationships: Map<string, ParsedRelationship>;
|
|
@@ -284,6 +291,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
|
284
291
|
scopeable: z.ZodOptional<z.ZodBoolean>;
|
|
285
292
|
surface: z.ZodOptional<z.ZodString>;
|
|
286
293
|
context: z.ZodOptional<z.ZodString>;
|
|
294
|
+
sync: z.ZodOptional<z.ZodEnum<["api", "electric"]>>;
|
|
287
295
|
}, "strict", z.ZodTypeAny, {
|
|
288
296
|
name: string;
|
|
289
297
|
table: string;
|
|
@@ -298,6 +306,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
|
298
306
|
scopeable?: boolean | undefined;
|
|
299
307
|
surface?: string | undefined;
|
|
300
308
|
context?: string | undefined;
|
|
309
|
+
sync?: "electric" | "api" | undefined;
|
|
301
310
|
}, {
|
|
302
311
|
name: string;
|
|
303
312
|
table: string;
|
|
@@ -312,6 +321,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
|
312
321
|
scopeable?: boolean | undefined;
|
|
313
322
|
surface?: string | undefined;
|
|
314
323
|
context?: string | undefined;
|
|
324
|
+
sync?: "electric" | "api" | undefined;
|
|
315
325
|
}>, {
|
|
316
326
|
name: string;
|
|
317
327
|
table: string;
|
|
@@ -326,6 +336,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
|
326
336
|
scopeable?: boolean | undefined;
|
|
327
337
|
surface?: string | undefined;
|
|
328
338
|
context?: string | undefined;
|
|
339
|
+
sync?: "electric" | "api" | undefined;
|
|
329
340
|
}, {
|
|
330
341
|
name: string;
|
|
331
342
|
table: string;
|
|
@@ -340,6 +351,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
|
340
351
|
scopeable?: boolean | undefined;
|
|
341
352
|
surface?: string | undefined;
|
|
342
353
|
context?: string | undefined;
|
|
354
|
+
sync?: "electric" | "api" | undefined;
|
|
343
355
|
}>;
|
|
344
356
|
fields: z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodEffects<z.ZodEffects<z.ZodEffects<z.ZodEffects<z.ZodEffects<z.ZodEffects<z.ZodEffects<z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
345
357
|
type: z.ZodEnum<["string", "integer", "decimal", "boolean", "uuid", "date", "datetime", "json", "entity_ref", "string_array", "enum"]>;
|
|
@@ -2076,6 +2088,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
|
2076
2088
|
scopeable?: boolean | undefined;
|
|
2077
2089
|
surface?: string | undefined;
|
|
2078
2090
|
context?: string | undefined;
|
|
2091
|
+
sync?: "electric" | "api" | undefined;
|
|
2079
2092
|
};
|
|
2080
2093
|
behaviors: (string | {
|
|
2081
2094
|
name: string;
|
|
@@ -2286,6 +2299,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
|
2286
2299
|
scopeable?: boolean | undefined;
|
|
2287
2300
|
surface?: string | undefined;
|
|
2288
2301
|
context?: string | undefined;
|
|
2302
|
+
sync?: "electric" | "api" | undefined;
|
|
2289
2303
|
};
|
|
2290
2304
|
relationships?: Record<string, {
|
|
2291
2305
|
type: "belongs_to" | "has_many" | "has_one";
|
|
@@ -2496,6 +2510,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
|
2496
2510
|
scopeable?: boolean | undefined;
|
|
2497
2511
|
surface?: string | undefined;
|
|
2498
2512
|
context?: string | undefined;
|
|
2513
|
+
sync?: "electric" | "api" | undefined;
|
|
2499
2514
|
};
|
|
2500
2515
|
behaviors: (string | {
|
|
2501
2516
|
name: string;
|
|
@@ -2706,6 +2721,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
|
2706
2721
|
scopeable?: boolean | undefined;
|
|
2707
2722
|
surface?: string | undefined;
|
|
2708
2723
|
context?: string | undefined;
|
|
2724
|
+
sync?: "electric" | "api" | undefined;
|
|
2709
2725
|
};
|
|
2710
2726
|
relationships?: Record<string, {
|
|
2711
2727
|
type: "belongs_to" | "has_many" | "has_one";
|
|
@@ -2916,6 +2932,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
|
2916
2932
|
scopeable?: boolean | undefined;
|
|
2917
2933
|
surface?: string | undefined;
|
|
2918
2934
|
context?: string | undefined;
|
|
2935
|
+
sync?: "electric" | "api" | undefined;
|
|
2919
2936
|
};
|
|
2920
2937
|
behaviors: (string | {
|
|
2921
2938
|
name: string;
|
|
@@ -3126,6 +3143,7 @@ declare const EntityDefinitionSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
|
3126
3143
|
scopeable?: boolean | undefined;
|
|
3127
3144
|
surface?: string | undefined;
|
|
3128
3145
|
context?: string | undefined;
|
|
3146
|
+
sync?: "electric" | "api" | undefined;
|
|
3129
3147
|
};
|
|
3130
3148
|
relationships?: Record<string, {
|
|
3131
3149
|
type: "belongs_to" | "has_many" | "has_one";
|
package/dist/src/index.js
CHANGED
|
@@ -44,27 +44,27 @@ import {
|
|
|
44
44
|
validateOrchestrationProject,
|
|
45
45
|
validatePatternComposition,
|
|
46
46
|
validatePatternProject
|
|
47
|
-
} from "../chunk-
|
|
47
|
+
} from "../chunk-CFFTPWHM.js";
|
|
48
48
|
import "../chunk-KVOWSC5S.js";
|
|
49
|
-
import "../chunk-
|
|
49
|
+
import "../chunk-PKDS6QIJ.js";
|
|
50
50
|
import "../chunk-PRWIX6UW.js";
|
|
51
|
-
import "../chunk-AHV4GDYM.js";
|
|
52
51
|
import "../chunk-YK5JEVLX.js";
|
|
53
52
|
import "../chunk-EO2QPOKH.js";
|
|
54
53
|
import "../chunk-SQDOBLBP.js";
|
|
55
|
-
import "../chunk-4KNXX6TI.js";
|
|
56
|
-
import "../chunk-3CJFPU6Q.js";
|
|
57
54
|
import "../chunk-TDEHU73T.js";
|
|
58
55
|
import "../chunk-LG57S2SC.js";
|
|
59
56
|
import "../chunk-XWBK3XJK.js";
|
|
60
57
|
import "../chunk-S7C6TIIF.js";
|
|
61
58
|
import "../chunk-MZ6GV4YF.js";
|
|
62
59
|
import "../chunk-HNWZFNKP.js";
|
|
60
|
+
import "../chunk-AHV4GDYM.js";
|
|
63
61
|
import "../chunk-43SBT72G.js";
|
|
64
62
|
import "../chunk-4MF3HKJA.js";
|
|
65
63
|
import "../chunk-TIZXQU26.js";
|
|
66
64
|
import "../chunk-JEINYUJH.js";
|
|
67
65
|
import "../chunk-5TK7MEN4.js";
|
|
66
|
+
import "../chunk-4KNXX6TI.js";
|
|
67
|
+
import "../chunk-3CJFPU6Q.js";
|
|
68
68
|
import "../chunk-U64T4YZE.js";
|
|
69
69
|
import "../chunk-2E224ZSN.js";
|
|
70
70
|
export {
|
package/package.json
CHANGED
|
@@ -317,6 +317,17 @@ export class JobWorker implements OnModuleInit, OnModuleDestroy {
|
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
async onModuleDestroy(): Promise<void> {
|
|
320
|
+
// LISTEN-NOTIFY-2 — release the listener connection on EVERY destroy path,
|
|
321
|
+
// including the `shuttingDown` early-return below (reached when SIGTERM and
|
|
322
|
+
// Nest's onModuleDestroy both fire) and a destroy with no prior SIGTERM.
|
|
323
|
+
// Previously this lived only on the first (non-shuttingDown) branch, so a
|
|
324
|
+
// double-fire — or a stop() that raced the listener's own in-flight
|
|
325
|
+
// connect() — left a `LISTEN codegen_jobs_wake` socket open past
|
|
326
|
+
// `app.close()`, hanging the process. `PgNotifyListener.stop()` is itself
|
|
327
|
+
// idempotent + connect-race-safe (LISTEN-NOTIFY-2). Best-effort; a failure
|
|
328
|
+
// here doesn't block the drain.
|
|
329
|
+
await this.stopNotifyListener();
|
|
330
|
+
|
|
320
331
|
if (this.shuttingDown) {
|
|
321
332
|
// Still drain, but don't tear intervals down twice.
|
|
322
333
|
await this.drainInFlight();
|
|
@@ -333,17 +344,6 @@ export class JobWorker implements OnModuleInit, OnModuleDestroy {
|
|
|
333
344
|
}
|
|
334
345
|
process.removeListener('SIGTERM', this.sigtermHandler);
|
|
335
346
|
|
|
336
|
-
// LISTEN-NOTIFY-1 — release the listener connection so the process can exit
|
|
337
|
-
// cleanly. Best-effort; a failure here doesn't block the drain.
|
|
338
|
-
if (this.notifyListener) {
|
|
339
|
-
try {
|
|
340
|
-
await this.notifyListener.stop();
|
|
341
|
-
} catch (err) {
|
|
342
|
-
this.logger.error(`notify listener stop failed: ${(err as Error).message}`);
|
|
343
|
-
}
|
|
344
|
-
this.notifyListener = null;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
347
|
await this.drainInFlight();
|
|
348
348
|
|
|
349
349
|
// Any rows still `running` past timeout → release back to pending.
|
|
@@ -370,6 +370,24 @@ export class JobWorker implements OnModuleInit, OnModuleDestroy {
|
|
|
370
370
|
]);
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
+
/**
|
|
374
|
+
* LISTEN-NOTIFY-2 — stop + drop the wake listener. Idempotent: a second call
|
|
375
|
+
* (SIGTERM + Nest destroy) finds `notifyListener` already null and no-ops.
|
|
376
|
+
* `PgNotifyListener.stop()` is itself race-safe against an in-flight
|
|
377
|
+
* `connect()`, so even a destroy that arrives microseconds after `start()`
|
|
378
|
+
* releases the listener socket rather than leaking it.
|
|
379
|
+
*/
|
|
380
|
+
private async stopNotifyListener(): Promise<void> {
|
|
381
|
+
const listener = this.notifyListener;
|
|
382
|
+
if (!listener) return;
|
|
383
|
+
this.notifyListener = null;
|
|
384
|
+
try {
|
|
385
|
+
await listener.stop();
|
|
386
|
+
} catch (err) {
|
|
387
|
+
this.logger.error(`notify listener stop failed: ${(err as Error).message}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
373
391
|
// ============================================================================
|
|
374
392
|
// Poll loop
|
|
375
393
|
// ============================================================================
|
|
@@ -111,6 +111,17 @@ export class PgNotifyListener {
|
|
|
111
111
|
private readonly backoffMaxMs: number;
|
|
112
112
|
/** WARN-once gate so a flapping listener doesn't spam the log. */
|
|
113
113
|
private warnedDown = false;
|
|
114
|
+
/**
|
|
115
|
+
* LISTEN-NOTIFY-2 — the in-flight `connect()` promise, set while a checkout is
|
|
116
|
+
* mid-`await`. `stop()` awaits it so a `stop()` that races a still-resolving
|
|
117
|
+
* `connect()` can't return before the connect either assigns `this.client`
|
|
118
|
+
* (then released by `releaseClient`) or self-releases the checked-out client.
|
|
119
|
+
* Without this, a `stop()` arriving during `pool.connect()`'s await saw
|
|
120
|
+
* `this.client === null` (nothing to release), then `connect()` resumed,
|
|
121
|
+
* assigned the client, and issued `LISTEN` — leaking an ESTABLISHED socket
|
|
122
|
+
* holding `LISTEN <channel>` forever past `app.close()`.
|
|
123
|
+
*/
|
|
124
|
+
private connecting: Promise<void> | null = null;
|
|
114
125
|
|
|
115
126
|
constructor(private readonly opts: PgNotifyListenerOptions) {
|
|
116
127
|
this.logger = new Logger(`PgNotifyListener(${opts.label})`);
|
|
@@ -125,20 +136,56 @@ export class PgNotifyListener {
|
|
|
125
136
|
await this.connect();
|
|
126
137
|
}
|
|
127
138
|
|
|
128
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* Stop listening + release the connection. Safe to call repeatedly and
|
|
141
|
+
* race-safe against an in-flight `connect()` (LISTEN-NOTIFY-2): it sets
|
|
142
|
+
* `stopped` first (so a resuming `connect()` self-releases its checkout),
|
|
143
|
+
* then awaits any in-flight connect, then releases whatever client landed.
|
|
144
|
+
*/
|
|
129
145
|
async stop(): Promise<void> {
|
|
130
146
|
this.stopped = true;
|
|
131
147
|
if (this.reconnectTimer) {
|
|
132
148
|
clearTimeout(this.reconnectTimer);
|
|
133
149
|
this.reconnectTimer = null;
|
|
134
150
|
}
|
|
151
|
+
// Await an in-flight checkout so we don't return while a client is still
|
|
152
|
+
// mid-`pool.connect()`. The resuming `connect()` sees `stopped` and either
|
|
153
|
+
// self-releases its checkout or assigns `this.client`; either way the
|
|
154
|
+
// `releaseClient()` below mops up.
|
|
155
|
+
const inflight = this.connecting;
|
|
156
|
+
if (inflight) {
|
|
157
|
+
try {
|
|
158
|
+
await inflight;
|
|
159
|
+
} catch {
|
|
160
|
+
// connect failures are handled inside connect(); ignore here.
|
|
161
|
+
}
|
|
162
|
+
}
|
|
135
163
|
await this.releaseClient();
|
|
136
164
|
}
|
|
137
165
|
|
|
138
166
|
private async connect(): Promise<void> {
|
|
139
167
|
if (this.stopped) return;
|
|
168
|
+
// Track this checkout so a racing stop() can await it (LISTEN-NOTIFY-2).
|
|
169
|
+
const attempt = this.doConnect();
|
|
170
|
+
this.connecting = attempt;
|
|
171
|
+
try {
|
|
172
|
+
await attempt;
|
|
173
|
+
} finally {
|
|
174
|
+
if (this.connecting === attempt) this.connecting = null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private async doConnect(): Promise<void> {
|
|
140
179
|
try {
|
|
141
180
|
const client = await this.opts.pool.connect();
|
|
181
|
+
// Re-check AFTER the await resolves: a stop() may have fired while this
|
|
182
|
+
// checkout was in flight. If so, release the just-checked-out client
|
|
183
|
+
// right here and bail BEFORE wiring handlers / issuing LISTEN — otherwise
|
|
184
|
+
// we'd leak an ESTABLISHED listener socket past shutdown (LISTEN-NOTIFY-2).
|
|
185
|
+
if (this.stopped) {
|
|
186
|
+
await this.releaseRawClient(client);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
142
189
|
client.on('notification', (msg) => {
|
|
143
190
|
if (msg.channel !== this.opts.channel) return;
|
|
144
191
|
try {
|
|
@@ -154,6 +201,11 @@ export class PgNotifyListener {
|
|
|
154
201
|
this.handleDrop();
|
|
155
202
|
});
|
|
156
203
|
await client.query(`LISTEN ${this.opts.channel}`);
|
|
204
|
+
// A stop() could have fired during the LISTEN round-trip too — same guard.
|
|
205
|
+
if (this.stopped) {
|
|
206
|
+
await this.releaseRawClient(client);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
157
209
|
this.client = client;
|
|
158
210
|
// Recovery: only announce if we had previously warned about being down.
|
|
159
211
|
if (this.warnedDown) {
|
|
@@ -202,11 +254,19 @@ export class PgNotifyListener {
|
|
|
202
254
|
const client = this.client;
|
|
203
255
|
this.client = null;
|
|
204
256
|
if (!client) return;
|
|
257
|
+
await this.releaseRawClient(client);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Tear down a raw checked-out client (LISTEN-NOTIFY-2). Used both by the
|
|
262
|
+
* normal `releaseClient()` path and by the connect-vs-stop race bail-outs,
|
|
263
|
+
* where the client was checked out but never assigned to `this.client`.
|
|
264
|
+
* Destroys (`release(true)`) so a half-listening socket is never reused.
|
|
265
|
+
*/
|
|
266
|
+
private async releaseRawClient(client: PgListenClient): Promise<void> {
|
|
205
267
|
try {
|
|
206
268
|
client.removeAllListeners?.('notification');
|
|
207
269
|
client.removeAllListeners?.('error');
|
|
208
|
-
// A listener client is a checked-out pool connection; release it back
|
|
209
|
-
// with `release(true)` (destroy) so a half-broken socket isn't reused.
|
|
210
270
|
if (client.release) client.release(true);
|
|
211
271
|
else if (client.end) await client.end();
|
|
212
272
|
} catch {
|
package/src/config/locations.mjs
CHANGED
|
@@ -113,12 +113,6 @@ const DEFAULT_LOCATIONS = {
|
|
|
113
113
|
import: '@/generated/entity-metadata',
|
|
114
114
|
},
|
|
115
115
|
|
|
116
|
-
/** Field meta type definitions */
|
|
117
|
-
frontendFieldMetaTypes: {
|
|
118
|
-
path: 'apps/frontend/src/lib/types',
|
|
119
|
-
import: '@/lib/types',
|
|
120
|
-
},
|
|
121
|
-
|
|
122
116
|
/** Auth helpers (for collections) */
|
|
123
117
|
frontendCollectionsAuth: {
|
|
124
118
|
path: 'apps/frontend/src/lib/collections/auth',
|
package/src/config/paths.mjs
CHANGED
|
@@ -611,18 +611,6 @@ export function getProjectConfig() {
|
|
|
611
611
|
return projectConfig;
|
|
612
612
|
}
|
|
613
613
|
|
|
614
|
-
/**
|
|
615
|
-
* Get pipelines configuration from project config.
|
|
616
|
-
* Returns the pipelines block (or an empty object if not configured).
|
|
617
|
-
*
|
|
618
|
-
* Usage:
|
|
619
|
-
* const pipelines = getPipelinesConfig();
|
|
620
|
-
* const arch = pipelines?.backend?.architecture ?? 'clean';
|
|
621
|
-
*/
|
|
622
|
-
export function getPipelinesConfig() {
|
|
623
|
-
return projectConfig?.pipelines ?? {};
|
|
624
|
-
}
|
|
625
|
-
|
|
626
614
|
/**
|
|
627
615
|
* Resolve the directory codegen writes barrel files to (modules.ts, schema.ts).
|
|
628
616
|
*
|
|
@@ -681,7 +669,6 @@ export default {
|
|
|
681
669
|
getLayoutConfig,
|
|
682
670
|
getDatabaseDialect,
|
|
683
671
|
getProjectConfig,
|
|
684
|
-
getPipelinesConfig,
|
|
685
672
|
getGenerateConfig,
|
|
686
673
|
getGeneratedDir,
|
|
687
674
|
// NEW: Config-driven naming functions
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
getLayoutConfig,
|
|
21
21
|
getDatabaseDialect,
|
|
22
22
|
getProjectConfig,
|
|
23
|
-
getPipelinesConfig,
|
|
24
23
|
getGenerateConfig,
|
|
25
24
|
} from "../../../src/config/paths.mjs";
|
|
26
25
|
import { getNamingConfig } from "../../../src/config/naming-config.mjs";
|
|
@@ -303,10 +302,6 @@ export default {
|
|
|
303
302
|
// specifier the generated entity code carries.
|
|
304
303
|
const runtimeMode = loadRuntimeMode(process.cwd());
|
|
305
304
|
|
|
306
|
-
// Load frontend config from project config (used for auth, sync, parsers)
|
|
307
|
-
const frontendConfig = getProjectConfig()?.frontend ?? {};
|
|
308
|
-
const frontendSync = frontendConfig.sync ?? {};
|
|
309
|
-
|
|
310
305
|
// Prepare locals for templates
|
|
311
306
|
const entity = definition.entity;
|
|
312
307
|
const fields = definition.fields || {};
|
|
@@ -463,12 +458,6 @@ export default {
|
|
|
463
458
|
const camelName = camelCase(name); // opportunity
|
|
464
459
|
const repositoryToken = `${pascalCase(name).toUpperCase()}_REPOSITORY`; // OPPORTUNITY_REPOSITORY
|
|
465
460
|
|
|
466
|
-
// Frontend store naming
|
|
467
|
-
const singularCamelName = camelCase(name); // "dealState" from "deal_state"
|
|
468
|
-
const pluralCamelName = camelCase(plural); // "dealStates" from "deal_states"
|
|
469
|
-
const collectionVarName = singularCamelName + "Collection"; // "dealStateCollection"
|
|
470
|
-
const collectionVarNamePlural = pluralCamelName + "Collection"; // "dealStatesCollection"
|
|
471
|
-
|
|
472
461
|
// Layout configuration (folder structure + file grouping)
|
|
473
462
|
// See tools/codegen/config/paths.js for options
|
|
474
463
|
const layout = getLayoutConfig(entity);
|
|
@@ -1026,7 +1015,6 @@ export default {
|
|
|
1026
1015
|
const architectureTarget = generateConfig.architecture;
|
|
1027
1016
|
const isCleanArchitecture = architectureTarget === 'clean';
|
|
1028
1017
|
const isCleanLitePs = architectureTarget === 'clean-lite-ps';
|
|
1029
|
-
const frontendEnabled = generateConfig.frontend === true;
|
|
1030
1018
|
|
|
1031
1019
|
// ============================================================================
|
|
1032
1020
|
// v2: Queries
|
|
@@ -1452,12 +1440,6 @@ export default {
|
|
|
1452
1440
|
camelName,
|
|
1453
1441
|
repositoryToken,
|
|
1454
1442
|
|
|
1455
|
-
// Frontend store naming
|
|
1456
|
-
singularCamelName,
|
|
1457
|
-
pluralCamelName,
|
|
1458
|
-
collectionVarName,
|
|
1459
|
-
collectionVarNamePlural,
|
|
1460
|
-
|
|
1461
1443
|
// Fields
|
|
1462
1444
|
fields: processedFields,
|
|
1463
1445
|
requiredFields,
|
|
@@ -1510,50 +1492,11 @@ export default {
|
|
|
1510
1492
|
// Usage: locations.dbEntities.path, locations.dbEntities.import
|
|
1511
1493
|
locations: LOCATIONS,
|
|
1512
1494
|
|
|
1513
|
-
//
|
|
1514
|
-
//
|
|
1515
|
-
frontend
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
function: frontendConfig.auth?.hasOwnProperty?.('function')
|
|
1519
|
-
? frontendConfig.auth.function
|
|
1520
|
-
: 'getAuthorizationHeader',
|
|
1521
|
-
},
|
|
1522
|
-
sync: {
|
|
1523
|
-
// Read transport for the generated collection:
|
|
1524
|
-
// 'electric' (default) → electricCollectionOptions (real-time shape sync)
|
|
1525
|
-
// 'api' → queryCollectionOptions (REST via TanStack Query)
|
|
1526
|
-
// Additive + default-off: existing consumers are unaffected. The rest of
|
|
1527
|
-
// the store (hooks/useLiveQuery, resolve, mutations) is mode-agnostic.
|
|
1528
|
-
mode: frontendSync.mode ?? 'electric',
|
|
1529
|
-
// 'api' mode only: REST base path; the list endpoint is `${apiUrl}/${plural}`
|
|
1530
|
-
// (or `${API_BASE_URL}/${plural}` when apiBaseUrlImport is set).
|
|
1531
|
-
apiUrl: frontendSync.apiUrl ?? '/api',
|
|
1532
|
-
// 'api' mode only: import path for the shared TanStack QueryClient.
|
|
1533
|
-
// null → a sibling './query-client' is assumed (consumer-provided).
|
|
1534
|
-
queryClientImport: frontendSync.queryClientImport ?? null,
|
|
1535
|
-
shapeUrl: frontendSync.shapeUrl ?? '/v1/shape',
|
|
1536
|
-
useTableParam: frontendSync.useTableParam ?? true,
|
|
1537
|
-
// Column mapper for snake_case to camelCase conversion (e.g., 'snakeCamelMapper')
|
|
1538
|
-
// Set to null/undefined if DB columns already match JS property names
|
|
1539
|
-
columnMapper: frontendSync.hasOwnProperty?.('columnMapper')
|
|
1540
|
-
? frontendSync.columnMapper
|
|
1541
|
-
: 'snakeCamelMapper',
|
|
1542
|
-
// Whether to wrap shapeUrl in new URL() constructor
|
|
1543
|
-
wrapInUrlConstructor: frontendSync.wrapInUrlConstructor ?? true,
|
|
1544
|
-
// Whether columnMapper needs () to call (true for functions, false for objects)
|
|
1545
|
-
columnMapperNeedsCall: frontendSync.columnMapperNeedsCall ?? true,
|
|
1546
|
-
// Import path for API_BASE_URL (if needed)
|
|
1547
|
-
apiBaseUrlImport: frontendSync.apiBaseUrlImport ?? null,
|
|
1548
|
-
},
|
|
1549
|
-
parsers: frontendConfig.parsers ?? {
|
|
1550
|
-
timestamptz: '(date: string) => new Date(date)',
|
|
1551
|
-
},
|
|
1552
|
-
collections: {
|
|
1553
|
-
// Schema prefix: 'schema.' for namespace import, '' for direct import
|
|
1554
|
-
schemaPrefix: frontendConfig.collections?.schemaPrefix ?? 'schema.',
|
|
1555
|
-
},
|
|
1556
|
-
},
|
|
1495
|
+
// NOTE: the `frontend:` locals block (auth/sync/parsers/collections) was
|
|
1496
|
+
// deleted with the hygen frontend templates (FE-3). The frontend emitter
|
|
1497
|
+
// (src/emitters/frontend/) now reads `frontend.*` from codegen.config.yaml
|
|
1498
|
+
// directly into its own FrontendEmitConfig — no template ever consumed
|
|
1499
|
+
// these locals after the templates were removed.
|
|
1557
1500
|
|
|
1558
1501
|
// Naming configuration (for templates that need it)
|
|
1559
1502
|
namingConfig,
|
|
@@ -1567,15 +1510,14 @@ export default {
|
|
|
1567
1510
|
getByIdQueryClass,
|
|
1568
1511
|
listQueryClass,
|
|
1569
1512
|
|
|
1570
|
-
// Generation toggles (what to generate)
|
|
1513
|
+
// Generation toggles (backend only — what to generate).
|
|
1514
|
+
//
|
|
1515
|
+
// The frontend toggles (fieldMetadata/collections/collectionsIndex/hooks/
|
|
1516
|
+
// mutations/hookStyle/structure/typeNaming/fkResolution/collectionNaming/
|
|
1517
|
+
// fileNaming/hookReturnStyle) were deleted with the hygen frontend
|
|
1518
|
+
// templates (FE-3). The frontend tree is now emitted by
|
|
1519
|
+
// src/emitters/frontend/ and gated solely by `generate.frontend`.
|
|
1571
1520
|
generate: {
|
|
1572
|
-
fieldMetadata: getProjectConfig()?.generate?.fieldMetadata ?? true,
|
|
1573
|
-
collections: getProjectConfig()?.generate?.collections ?? true,
|
|
1574
|
-
// Whether to generate index.ts in collections folder (for multi-file collection structure)
|
|
1575
|
-
collectionsIndex: getProjectConfig()?.generate?.collectionsIndex ?? false,
|
|
1576
|
-
hooks: getProjectConfig()?.generate?.hooks ?? true,
|
|
1577
|
-
mutations: getProjectConfig()?.generate?.mutations ?? true,
|
|
1578
|
-
// Backend toggles
|
|
1579
1521
|
drizzleSchema: getProjectConfig()?.generate?.drizzleSchema ?? true,
|
|
1580
1522
|
commands: getProjectConfig()?.generate?.commands ?? true,
|
|
1581
1523
|
queries: getProjectConfig()?.generate?.queries ?? true,
|
|
@@ -1583,23 +1525,6 @@ export default {
|
|
|
1583
1525
|
schemaServer: getProjectConfig()?.generate?.schemaServer ?? false,
|
|
1584
1526
|
schemaClient: getProjectConfig()?.generate?.schemaClient ?? false,
|
|
1585
1527
|
electricMigrations: getProjectConfig()?.generate?.electricMigrations ?? false,
|
|
1586
|
-
// Hook style: 'collection' uses collection.useMany(), 'useLiveQuery' uses TanStack DB pattern
|
|
1587
|
-
hookStyle: getProjectConfig()?.generate?.hookStyle ?? 'collection',
|
|
1588
|
-
// Output structure mode: 'entity-first' | 'concern-first' | 'monolithic'
|
|
1589
|
-
// entity-first: generated/{entity}/types.ts, collection.ts, hooks.ts...
|
|
1590
|
-
// concern-first: generated/types/{entity}.ts, collections/{entity}.ts...
|
|
1591
|
-
// monolithic: generated/{entity}.ts (single file per entity)
|
|
1592
|
-
structure: getProjectConfig()?.generate?.structure ?? 'monolithic',
|
|
1593
|
-
// Type naming: 'plain' = Opportunity, 'entity' = OpportunityEntity
|
|
1594
|
-
typeNaming: getProjectConfig()?.generate?.typeNaming ?? 'plain',
|
|
1595
|
-
// FK resolution: true = import related collections, false = skip (useful when collections don't exist)
|
|
1596
|
-
fkResolution: getProjectConfig()?.generate?.fkResolution ?? true,
|
|
1597
|
-
// Collection variable naming: 'singular' = opportunityCollection, 'plural' = opportunitiesCollection
|
|
1598
|
-
collectionNaming: getProjectConfig()?.generate?.collectionNaming ?? 'singular',
|
|
1599
|
-
// File naming: 'singular' = opportunity.ts, 'plural' = opportunities.ts
|
|
1600
|
-
fileNaming: getProjectConfig()?.generate?.fileNaming ?? 'singular',
|
|
1601
|
-
// Hook return style: 'generic' = { data }, 'named' = { opportunities }
|
|
1602
|
-
hookReturnStyle: getProjectConfig()?.generate?.hookReturnStyle ?? 'generic',
|
|
1603
1528
|
},
|
|
1604
1529
|
|
|
1605
1530
|
// Pre-computed output paths for templates (avoids ternary in YAML frontmatter)
|
|
@@ -1643,7 +1568,6 @@ export default {
|
|
|
1643
1568
|
architectureTarget,
|
|
1644
1569
|
isCleanArchitecture,
|
|
1645
1570
|
isCleanLitePs,
|
|
1646
|
-
frontendEnabled,
|
|
1647
1571
|
|
|
1648
1572
|
// Queries
|
|
1649
1573
|
hasQueries,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../runtime/subsystems/jobs/job-worker.ts"],"sourcesContent":["/**\n * JobWorker — backend-agnostic tick loop for the job orchestration domain\n * (ADR-022, JOB-3).\n *\n * One worker instance per active pool. On `onModuleInit` it starts two\n * intervals: the poll loop (claim → process → repeat) and the stale-claim\n * sweeper. On `onModuleDestroy` / SIGTERM it drains in-flight work and\n * releases still-`running` rows back to `pending` so a replacement worker\n * can resume with step memoization intact.\n *\n * The claim query is the beating heart: `SELECT … FOR UPDATE SKIP LOCKED`\n * inside a single transaction. Multiple worker processes share the table\n * without serialising on row locks.\n */\n// TODO(logging-subsystem): swap to ILogger once ADR-028 lands\nimport { Inject, Injectable, Logger, type OnModuleDestroy, type OnModuleInit } from '@nestjs/common';\nimport { ModuleRef } from '@nestjs/core';\nimport { and, asc, desc, eq, inArray, lt, lte, sql } from 'drizzle-orm';\nimport type { DrizzleClient } from '../../types/drizzle';\nimport { tokenKey } from '../token-key';\nimport { DRIZZLE } from '../../constants/tokens';\nimport { jobRuns, type JobRunRow } from './job-orchestration.schema';\nimport type { IJobOrchestrator, JobRun } from './job-orchestrator.protocol';\nimport type { IJobRunService } from './job-run-service.protocol';\nimport type { IJobStepService } from './job-step-service.protocol';\nimport {\n JOB_ORCHESTRATOR,\n JOB_RUN_SERVICE,\n JOB_STEP_SERVICE,\n} from './jobs-domain.tokens';\nimport {\n JOB_HANDLER_REGISTRY,\n JobHandlerBase,\n type JobContext,\n type JobHandlerMeta,\n type RetryPolicy,\n type SpawnChildOptions,\n type StepOptions,\n} from './job-handler.base';\nimport { JOBS_WAKE_CHANNEL, PgNotifyListener } from './pg-notify';\n\n/**\n * Options accepted by `JobWorker`. JOB-5 threads these through module\n * `.forRoot()` config; supplied here as a plain DI-constructor argument\n * so the worker compiles standalone.\n */\nexport interface JobWorkerOptions {\n /** Pool name this worker claims from. Matches `job.pool`. */\n pool: string;\n /** Max concurrent in-flight `processRun` calls. */\n concurrency: number;\n /** Poll interval in ms. Default 1000. */\n pollIntervalMs?: number;\n /** Stale sweep interval in ms. Default 60_000. */\n staleSweeperIntervalMs?: number;\n /**\n * Threshold beyond which a `running` row is presumed stranded by a\n * crashed worker. Default 5 min. Must be >= 2× max handler duration.\n */\n staleThresholdMs?: number;\n /** Max ms to wait for in-flight drain on SIGTERM. Default 30_000. */\n shutdownTimeoutMs?: number;\n /**\n * LISTEN-NOTIFY-1 — when true, hold a dedicated listener connection and\n * LISTEN on `codegen_jobs_wake`. A notification naming this worker's `pool`\n * triggers an immediate (debounced) claim cycle, so an enqueue is claimed in\n * milliseconds instead of waiting for the next `pollIntervalMs` tick. Polling\n * continues unchanged as the fallback heartbeat. Default false.\n */\n listenNotify?: boolean;\n}\n\n// ADR-037: namespaced `Symbol.for(...)` (via `tokenKey()`) — matches by value\n// across runtime copies.\nexport const JOB_WORKER_OPTIONS = Symbol.for(tokenKey('jobs', 'worker-options'));\n\nconst DEFAULT_POLL_INTERVAL_MS = 1_000;\nconst DEFAULT_STALE_SWEEPER_INTERVAL_MS = 60_000;\nconst DEFAULT_STALE_THRESHOLD_MS = 5 * 60_000;\nconst DEFAULT_SHUTDOWN_TIMEOUT_MS = 30_000;\n\nconst TERMINAL_STATUSES: JobRunRow['status'][] = [\n 'completed',\n 'failed',\n 'timed_out',\n 'canceled',\n];\n\n// ─── Pure helpers (exported for unit tests) ────────────────────────────────\n\n/**\n * Backoff delay in ms for the Nth attempt (1-indexed). Supports both\n * policy modes. Exponential is capped at `Number.MAX_SAFE_INTEGER` so\n * pathological attempt counts don't overflow.\n */\nexport function computeBackoff(policy: RetryPolicy, attempts: number): number {\n const base = Math.max(policy.baseMs, 0);\n if (policy.backoff === 'fixed') {\n return base;\n }\n // exponential: baseMs * 2^(attempts-1)\n const exponent = Math.max(attempts - 1, 0);\n if (exponent >= 53) return Number.MAX_SAFE_INTEGER; // 2^53 overflow guard\n const raw = base * Math.pow(2, exponent);\n if (!Number.isFinite(raw) || raw >= Number.MAX_SAFE_INTEGER) {\n return Number.MAX_SAFE_INTEGER;\n }\n return raw;\n}\n\n/**\n * Decide whether an error should be retried under the given policy.\n * Matches `nonRetryableErrors` by `.name` OR `.code`. Returns\n * - `'retry'` if attempts remain and the error isn't blacklisted,\n * - `'fail'` otherwise (terminal failure).\n */\nexport function classifyError(\n err: unknown,\n policy: RetryPolicy | undefined,\n currentAttempts: number,\n): 'retry' | 'fail' {\n if (!policy) return 'fail';\n const errObj = err as { name?: string; code?: string } | undefined;\n const name = errObj?.name;\n const code = errObj?.code;\n const nonRetryable = policy.nonRetryableErrors ?? [];\n if (nonRetryable.some((n) => n === name || n === code)) return 'fail';\n if (currentAttempts + 1 >= policy.attempts) return 'fail';\n return 'retry';\n}\n\n/**\n * Build the raw claim-candidate select. Exported so tests can inspect\n * `.toSQL()` without spinning up the full worker. Matches JOB-3 §4 and\n * ADR-022 \"Claim query (Drizzle backend)\".\n */\nexport function buildClaimQuery(db: DrizzleClient, pool: string) {\n return db\n .select({ id: jobRuns.id })\n .from(jobRuns)\n .where(\n and(\n eq(jobRuns.status, 'pending'),\n eq(jobRuns.pool, pool),\n lte(jobRuns.runAt, new Date()),\n ),\n )\n .orderBy(desc(jobRuns.priority), asc(jobRuns.runAt))\n .limit(1)\n .for('update', { skipLocked: true });\n}\n\n/**\n * Build the stale-claim sweep candidate select. `FOR UPDATE SKIP LOCKED`\n * per OQ-2 resolution (2026-04-19): per-worker sweeper, safe without\n * leader election because the update is self-gating.\n */\nexport function buildStaleSweepQuery(\n db: DrizzleClient,\n staleThresholdMs: number,\n) {\n const threshold = new Date(Date.now() - staleThresholdMs);\n return db\n .select({ id: jobRuns.id })\n .from(jobRuns)\n .where(\n and(\n eq(jobRuns.status, 'running'),\n lt(jobRuns.claimedAt, threshold),\n ),\n )\n .for('update', { skipLocked: true });\n}\n\n// ─── Error serialisation ───────────────────────────────────────────────────\n\nfunction serialiseError(err: unknown, attempt: number, retryable: boolean) {\n const e = err as { message?: string; stack?: string; code?: string } | undefined;\n return {\n message: (e?.message ?? String(err)) as string,\n stack: e?.stack,\n retryable,\n attempt,\n };\n}\n\n// ─── JobWorker ─────────────────────────────────────────────────────────────\n\n@Injectable()\nexport class JobWorker implements OnModuleInit, OnModuleDestroy {\n private readonly logger = new Logger(JobWorker.name);\n private shuttingDown = false;\n private readonly inFlight = new Set<Promise<void>>();\n private pollTimer: ReturnType<typeof setInterval> | null = null;\n private sweeperTimer: ReturnType<typeof setInterval> | null = null;\n private sigtermHandled = false;\n private readonly sigtermHandler: () => void;\n\n private readonly pollIntervalMs: number;\n private readonly staleSweeperIntervalMs: number;\n private readonly staleThresholdMs: number;\n private readonly shutdownTimeoutMs: number;\n\n // LISTEN-NOTIFY-1 — dedicated listener + debounce state. `null` when\n // `listenNotify` is off (the common case); polling is the only driver then.\n private readonly listenNotifyEnabled: boolean;\n private notifyListener: PgNotifyListener | null = null;\n /** True while a wake-driven claim cycle is in flight (debounce gate). */\n private wakeDraining = false;\n /** A notify arrived mid-cycle → re-check once when the cycle ends. */\n private wakeRecheckPending = false;\n\n constructor(\n @Inject(DRIZZLE) private readonly db: DrizzleClient,\n @Inject(JOB_ORCHESTRATOR) private readonly orchestrator: IJobOrchestrator,\n @Inject(JOB_RUN_SERVICE) private readonly runService: IJobRunService,\n @Inject(JOB_STEP_SERVICE) private readonly stepService: IJobStepService,\n @Inject(JOB_WORKER_OPTIONS) private readonly options: JobWorkerOptions,\n private readonly moduleRef: ModuleRef,\n ) {\n this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n this.staleSweeperIntervalMs =\n options.staleSweeperIntervalMs ?? DEFAULT_STALE_SWEEPER_INTERVAL_MS;\n this.staleThresholdMs = options.staleThresholdMs ?? DEFAULT_STALE_THRESHOLD_MS;\n this.shutdownTimeoutMs =\n options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;\n this.listenNotifyEnabled = options.listenNotify ?? false;\n\n this.sigtermHandler = () => {\n if (this.sigtermHandled) return;\n this.sigtermHandled = true;\n void this.onModuleDestroy();\n };\n void this.runService; // reserved for future scope-aware cancellation paths\n }\n\n // ============================================================================\n // Lifecycle\n // ============================================================================\n\n onModuleInit(): void {\n this.pollTimer = setInterval(() => {\n void this.pollAndProcess();\n }, this.pollIntervalMs);\n this.sweeperTimer = setInterval(() => {\n void this.sweepStaleClaims();\n }, this.staleSweeperIntervalMs);\n process.on('SIGTERM', this.sigtermHandler);\n\n // LISTEN-NOTIFY-1 — start the wake listener ALONGSIDE the poll timer (never\n // instead). A notify for this worker's pool drives an immediate claim cycle;\n // the interval timer above stays the durability heartbeat. Listener startup\n // is fire-and-forget: a connect failure self-heals via the listener's own\n // backoff, and until it's up the poll loop is the sole driver.\n if (this.listenNotifyEnabled) {\n // The DRIZZLE provider wraps a `pg.Pool`, exposed by drizzle as `$client`.\n const pool = (this.db as unknown as { $client?: unknown }).$client;\n if (!pool || typeof (pool as { connect?: unknown }).connect !== 'function') {\n this.logger.warn(\n `listen_notify enabled but the Drizzle client exposes no pg Pool ` +\n `($client.connect missing) — falling back to interval polling only.`,\n );\n } else {\n this.notifyListener = new PgNotifyListener({\n channel: JOBS_WAKE_CHANNEL,\n pool: pool as { connect(): Promise<never> },\n label: `jobs:${this.options.pool}`,\n onNotify: (payload) => this.onWake(payload),\n });\n void this.notifyListener.start();\n }\n }\n }\n\n /**\n * Wake handler — a `codegen_jobs_wake` notification arrived. Only payloads\n * naming THIS worker's pool are relevant (other pools have their own workers).\n * Debounced: if a claim cycle is already running we just flag a re-check so a\n * burst of N enqueues collapses to at most one extra cycle (D3).\n */\n private onWake(payload: string): void {\n if (this.shuttingDown) return;\n if (payload !== this.options.pool) return;\n if (this.wakeDraining) {\n this.wakeRecheckPending = true;\n return;\n }\n void this.drainOnWake();\n }\n\n /**\n * Claim-until-empty on a wake. Unlike the interval `pollAndProcess` (one\n * claim per tick), a wake drains greedily up to the concurrency ceiling so a\n * burst that arrived together is dispatched without waiting for N ticks. The\n * `wakeRecheckPending` flag coalesces notifies that land mid-drain.\n */\n private async drainOnWake(): Promise<void> {\n this.wakeDraining = true;\n try {\n do {\n this.wakeRecheckPending = false;\n // Claim while there's capacity; pollAndProcess no-ops at the ceiling.\n let progressed = true;\n while (\n progressed &&\n !this.shuttingDown &&\n this.inFlight.size < this.options.concurrency\n ) {\n const before = this.inFlight.size;\n await this.pollAndProcess();\n progressed = this.inFlight.size > before;\n }\n } while (this.wakeRecheckPending && !this.shuttingDown);\n } finally {\n this.wakeDraining = false;\n }\n }\n\n async onModuleDestroy(): Promise<void> {\n if (this.shuttingDown) {\n // Still drain, but don't tear intervals down twice.\n await this.drainInFlight();\n return;\n }\n this.shuttingDown = true;\n if (this.pollTimer) {\n clearInterval(this.pollTimer);\n this.pollTimer = null;\n }\n if (this.sweeperTimer) {\n clearInterval(this.sweeperTimer);\n this.sweeperTimer = null;\n }\n process.removeListener('SIGTERM', this.sigtermHandler);\n\n // LISTEN-NOTIFY-1 — release the listener connection so the process can exit\n // cleanly. Best-effort; a failure here doesn't block the drain.\n if (this.notifyListener) {\n try {\n await this.notifyListener.stop();\n } catch (err) {\n this.logger.error(`notify listener stop failed: ${(err as Error).message}`);\n }\n this.notifyListener = null;\n }\n\n await this.drainInFlight();\n\n // Any rows still `running` past timeout → release back to pending.\n try {\n await this.db\n .update(jobRuns)\n .set({ status: 'pending', claimedAt: null, startedAt: null })\n .where(\n and(eq(jobRuns.status, 'running'), eq(jobRuns.pool, this.options.pool)),\n );\n } catch (err) {\n this.logger.error(`shutdown reset failed: ${(err as Error).message}`);\n }\n }\n\n private async drainInFlight(): Promise<void> {\n if (this.inFlight.size === 0) return;\n const timeout = new Promise<void>((resolve) =>\n setTimeout(resolve, this.shutdownTimeoutMs),\n );\n await Promise.race([\n Promise.allSettled([...this.inFlight]).then(() => undefined),\n timeout,\n ]);\n }\n\n // ============================================================================\n // Poll loop\n // ============================================================================\n\n async pollAndProcess(): Promise<void> {\n if (this.shuttingDown) return;\n if (this.inFlight.size >= this.options.concurrency) return;\n\n let claimed: JobRunRow | null;\n try {\n claimed = await this.claimNext(this.options.pool);\n } catch (err) {\n this.logger.error(`claimNext failed: ${(err as Error).message}`);\n return;\n }\n if (!claimed) return;\n\n const run = claimed;\n const promise = this.processRun(run).catch((err) => {\n this.logger.error(\n `processRun(${run.id}) unhandled: ${(err as Error).message}`,\n );\n });\n this.inFlight.add(promise);\n promise.finally(() => {\n this.inFlight.delete(promise);\n });\n }\n\n /**\n * Claim the next runnable row from the pool. Transaction ensures the\n * select-candidate + update-to-running pair is atomic; FOR UPDATE SKIP\n * LOCKED lets multiple workers share the table without serialising.\n */\n async claimNext(pool: string): Promise<JobRunRow | null> {\n return this.db.transaction(async (tx) => {\n const candidates = await tx\n .select({ id: jobRuns.id })\n .from(jobRuns)\n .where(\n and(\n eq(jobRuns.status, 'pending'),\n eq(jobRuns.pool, pool),\n lte(jobRuns.runAt, new Date()),\n ),\n )\n .orderBy(desc(jobRuns.priority), asc(jobRuns.runAt))\n .limit(1)\n .for('update', { skipLocked: true });\n const candidate = candidates[0];\n if (!candidate) return null;\n\n const [claimed] = await tx\n .update(jobRuns)\n .set({\n status: 'running',\n claimedAt: new Date(),\n startedAt: new Date(),\n updatedAt: new Date(),\n })\n .where(eq(jobRuns.id, candidate.id))\n .returning();\n return (claimed ?? null) as JobRunRow | null;\n });\n }\n\n // ============================================================================\n // Stale claim sweeper\n // ============================================================================\n\n /**\n * Release rows whose `claimed_at` is older than the threshold. Safe to\n * run concurrently across workers — the two-phase tx (select-for-update\n * then update) guarantees each stranded row is only reset once.\n */\n async sweepStaleClaims(): Promise<void> {\n if (this.shuttingDown) return;\n try {\n await this.db.transaction(async (tx) => {\n const threshold = new Date(Date.now() - this.staleThresholdMs);\n const stale = await tx\n .select({ id: jobRuns.id })\n .from(jobRuns)\n .where(\n and(eq(jobRuns.status, 'running'), lt(jobRuns.claimedAt, threshold)),\n )\n .for('update', { skipLocked: true });\n if (stale.length === 0) return;\n const ids = stale.map((r) => r.id);\n await tx\n .update(jobRuns)\n .set({ status: 'pending', claimedAt: null, startedAt: null })\n .where(inArray(jobRuns.id, ids));\n for (const id of ids) {\n this.logger.warn(`Recovered stale claim on run ${id}`);\n }\n });\n } catch (err) {\n this.logger.error(`sweepStaleClaims failed: ${(err as Error).message}`);\n }\n }\n\n // ============================================================================\n // processRun\n // ============================================================================\n\n private async processRun(claimed: JobRunRow): Promise<void> {\n const registryEntry = JOB_HANDLER_REGISTRY.get(claimed.jobType);\n\n // (a) Missing handler — defensive; JOB-5 boot validator should have caught.\n if (!registryEntry) {\n this.logger.error(\n `No handler registered for jobType='${claimed.jobType}' (run ${claimed.id})`,\n );\n await this.markFailed(\n claimed,\n new Error(`No handler registered for jobType='${claimed.jobType}'`),\n /*finalAttempts*/ (claimed.attempts ?? 0) + 1,\n );\n return;\n }\n\n // (b) Concurrency-queue release gate — defer if another run with the\n // same key is already `running`.\n if (claimed.concurrencyKey) {\n const inflight = await this.db\n .select({ id: jobRuns.id })\n .from(jobRuns)\n .where(\n and(\n eq(jobRuns.concurrencyKey, claimed.concurrencyKey),\n eq(jobRuns.status, 'running'),\n ),\n );\n const other = inflight.find((r) => r.id !== claimed.id);\n if (other) {\n await this.db\n .update(jobRuns)\n .set({\n status: 'pending',\n claimedAt: null,\n startedAt: null,\n updatedAt: new Date(),\n })\n .where(eq(jobRuns.id, claimed.id));\n return;\n }\n }\n\n const meta = registryEntry.meta as JobHandlerMeta<unknown>;\n const HandlerClass = registryEntry.handlerClass;\n\n // (c) Build JobContext. Resolve the handler instance from Nest's DI\n // graph so its `@Inject` constructor params (which may come from\n // any module in the app graph) are satisfied. `moduleRef.create()`\n // would otherwise instantiate a fresh class within JobWorkerModule's\n // scope only — which blows up with \"not a provider of the current\n // module\" for any handler that consumes a service from a peer\n // module (e.g. CrmSyncJob injecting CrmSyncFactory from CrmModule).\n // Consequence: handlers MUST be registered as providers in their\n // owning module (@Injectable + `providers: [HandlerClass]`). The\n // @JobHandler decorator handles registry registration only, not DI.\n // See the jobs skill's handler-authoring.md for the registration\n // rule.\n const handler = this.moduleRef.get(\n HandlerClass as unknown as new (...args: unknown[]) => unknown,\n { strict: false },\n ) as JobHandlerBase<unknown>;\n const ctx: JobContext<unknown> = {\n input: claimed.input,\n run: claimed as JobRun,\n step: this.makeStepFn(claimed),\n spawnChild: this.makeSpawnFn(claimed),\n logger: new Logger(`JobRun:${claimed.id}`),\n };\n\n const attemptsBefore = claimed.attempts ?? 0;\n try {\n // (d) Run the handler.\n const output = (await handler.run(ctx)) as Record<string, unknown> | undefined;\n // (e) Success.\n await this.db\n .update(jobRuns)\n .set({\n status: 'completed',\n output: (output ?? {}) as Record<string, unknown>,\n finishedAt: new Date(),\n updatedAt: new Date(),\n attempts: attemptsBefore + 1,\n })\n .where(eq(jobRuns.id, claimed.id));\n } catch (err) {\n // (f) Error classification + retry/fail.\n const policy = meta.retry;\n const decision = classifyError(err, policy, attemptsBefore);\n const nextAttempts = attemptsBefore + 1;\n if (decision === 'retry' && policy) {\n const delay = computeBackoff(policy, nextAttempts);\n await this.db\n .update(jobRuns)\n .set({\n status: 'pending',\n attempts: nextAttempts,\n runAt: new Date(Date.now() + delay),\n startedAt: null,\n claimedAt: null,\n error: serialiseError(err, nextAttempts, true),\n updatedAt: new Date(),\n })\n .where(eq(jobRuns.id, claimed.id));\n } else {\n await this.markFailed(claimed, err, nextAttempts);\n }\n }\n }\n\n private async markFailed(\n claimed: JobRunRow,\n err: unknown,\n finalAttempts: number,\n ): Promise<void> {\n await this.db\n .update(jobRuns)\n .set({\n status: 'failed',\n attempts: finalAttempts,\n finishedAt: new Date(),\n error: serialiseError(err, finalAttempts, false),\n updatedAt: new Date(),\n })\n .where(eq(jobRuns.id, claimed.id));\n\n // Parent-close-policy cascade: if this run has children under the same\n // root_run_id and this run's own parentClosePolicy is 'terminate', cascade.\n if (claimed.parentClosePolicy === 'terminate') {\n try {\n // JOB-8 — thread the run's own tenantId so the orchestrator's\n // multi-tenant gate passes. Without this, every terminate-policy\n // cascade throws MissingTenantIdError under multiTenant=true and\n // the outer catch silently swallows it — children never cancel.\n await this.orchestrator.cancel(claimed.id, {\n cascade: true,\n reason: 'parent-failed',\n tenantId: claimed.tenantId,\n });\n } catch (cascadeErr) {\n // cancel is idempotent; failure here is unusual but not fatal.\n this.logger.warn(\n `cascade on failed run ${claimed.id}: ${(cascadeErr as Error).message}`,\n );\n }\n }\n }\n\n // ============================================================================\n // ctx.step / ctx.spawnChild builders\n // ============================================================================\n\n private makeStepFn(run: JobRunRow) {\n return async <TOutput>(\n stepId: string,\n fn: () => Promise<TOutput>,\n _opts?: StepOptions,\n ): Promise<TOutput> => {\n void _opts;\n const existing = await this.stepService.findStep(run.id, stepId);\n if (existing?.status === 'completed') {\n return existing.output as TOutput;\n }\n\n const seq = await this.nextStepSeq(run.id);\n const startedAt = new Date();\n const nextAttempts = (existing?.attempts ?? 0) + 1;\n await this.stepService.recordStep({\n jobRunId: run.id,\n stepId,\n kind: 'task',\n seq,\n status: 'running',\n startedAt,\n attempts: nextAttempts,\n });\n try {\n const output = await fn();\n await this.stepService.recordStep({\n jobRunId: run.id,\n stepId,\n kind: 'task',\n seq,\n status: 'completed',\n output: output as Record<string, unknown> | undefined,\n finishedAt: new Date(),\n attempts: nextAttempts,\n });\n return output;\n } catch (err) {\n await this.stepService.recordStep({\n jobRunId: run.id,\n stepId,\n kind: 'task',\n seq,\n status: 'failed',\n error: serialiseError(err, nextAttempts, false),\n finishedAt: new Date(),\n attempts: nextAttempts,\n });\n throw err;\n }\n };\n }\n\n private makeSpawnFn(run: JobRunRow) {\n return async (\n type: string,\n input: unknown,\n opts?: SpawnChildOptions,\n ): Promise<JobRun> => {\n return this.orchestrator.start(type, input, {\n parentRunId: run.id,\n parentClosePolicy: opts?.closePolicy,\n runAt: opts?.runAt,\n priority: opts?.priority,\n tags: opts?.tags,\n triggerSource: 'parent',\n triggerRef: run.id,\n });\n };\n }\n\n /**\n * Allocate the next `seq` for a given run. SELECT-max approach — runs\n * typically have <100 steps so the scan is cheap, and correctness across\n * retries is more important than the microseconds saved by an in-memory\n * counter (which would drift if the worker crashes mid-run and another\n * worker resumes via stale-claim sweep).\n */\n private async nextStepSeq(runId: string): Promise<number> {\n const result = await this.db.execute(\n sql`SELECT COALESCE(MAX(seq), 0) + 1 AS next FROM job_step WHERE job_run_id = ${runId}`,\n );\n // Driver shape varies and is NOT uniformly array-iterable, so we must\n // never array-destructure the raw result (that throws `{} is not iterable`\n // on the node-postgres `Result` object, which exposes `.rows` instead of\n // being an array — first hit by package-mode bridge deliveries on\n // `drizzle-orm/node-postgres`). Normalise to a row array first, then read.\n // - node-postgres `db.execute(sql)` → `{ rows: [{ next }], ... }`\n // - some drivers / future shapes → a plain `[{ next }]` array\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const raw = result as any;\n const rows: Array<{ next?: unknown }> = Array.isArray(raw)\n ? raw\n : Array.isArray(raw?.rows)\n ? raw.rows\n : [];\n const next = rows[0]?.next;\n return typeof next === 'undefined' ? 1 : Number(next);\n }\n\n // ============================================================================\n // (suppress unused-import noise)\n // ============================================================================\n}\n\n// Terminal statuses re-exported for JOB-4 parity imports.\nexport { TERMINAL_STATUSES };\n"],"mappings":"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","names":[]}
|