@geekmidas/cli 10.0.0-alpha.72 → 10.0.0-alpha.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{OpenApiTsGenerator-BlP53kf2.cjs → EndpointGenerator-CP6sBvQV.cjs} +4 -510
- package/dist/EndpointGenerator-CP6sBvQV.cjs.map +1 -0
- package/dist/{OpenApiTsGenerator-Dv0gT2DL.mjs → EndpointGenerator-DftalLYg.mjs} +3 -509
- package/dist/EndpointGenerator-DftalLYg.mjs.map +1 -0
- package/dist/{LegacyStateStore-NSHEinQi.mjs → LegacyStateStore-DTo7z924.mjs} +2 -2
- package/dist/{LegacyStateStore-NSHEinQi.mjs.map → LegacyStateStore-DTo7z924.mjs.map} +1 -1
- package/dist/{LegacyStateStore-Dd5IwiyC.cjs → LegacyStateStore-D_78eaHM.cjs} +2 -2
- package/dist/{LegacyStateStore-Dd5IwiyC.cjs.map → LegacyStateStore-D_78eaHM.cjs.map} +1 -1
- package/dist/{LocalStateStore-DTylG4T4.cjs → LocalStateStore-BhrlQIXD.cjs} +2 -2
- package/dist/{LocalStateStore-DTylG4T4.cjs.map → LocalStateStore-BhrlQIXD.cjs.map} +1 -1
- package/dist/{LocalStateStore-DUoG2wA_.mjs → LocalStateStore-CNjgkRk4.mjs} +2 -2
- package/dist/{LocalStateStore-DUoG2wA_.mjs.map → LocalStateStore-CNjgkRk4.mjs.map} +1 -1
- package/dist/OpenApiTsGenerator-CKBCckKz.cjs +514 -0
- package/dist/OpenApiTsGenerator-CKBCckKz.cjs.map +1 -0
- package/dist/OpenApiTsGenerator-nu1r1NgB.mjs +509 -0
- package/dist/OpenApiTsGenerator-nu1r1NgB.mjs.map +1 -0
- package/dist/{S3StateStore-Dxob3PNA.mjs → S3StateStore-g2P7T_Mo.mjs} +2 -2
- package/dist/{S3StateStore-Dxob3PNA.mjs.map → S3StateStore-g2P7T_Mo.mjs.map} +1 -1
- package/dist/{S3StateStore-D7hrU-sy.cjs → S3StateStore-oCxRs6HM.cjs} +2 -2
- package/dist/{S3StateStore-D7hrU-sy.cjs.map → S3StateStore-oCxRs6HM.cjs.map} +1 -1
- package/dist/{SSMStateStore-Dvl4A8Up.mjs → SSMStateStore-CYenunmK.mjs} +2 -2
- package/dist/{SSMStateStore-Dvl4A8Up.mjs.map → SSMStateStore-CYenunmK.mjs.map} +1 -1
- package/dist/{SSMStateStore-DGIKJ4NN.cjs → SSMStateStore-DXp0m7XI.cjs} +2 -2
- package/dist/{SSMStateStore-DGIKJ4NN.cjs.map → SSMStateStore-DXp0m7XI.cjs.map} +1 -1
- package/dist/SubscriberGenerator-Bixm28fN.cjs +461 -0
- package/dist/SubscriberGenerator-Bixm28fN.cjs.map +1 -0
- package/dist/SubscriberGenerator-DZk_pKDh.mjs +432 -0
- package/dist/SubscriberGenerator-DZk_pKDh.mjs.map +1 -0
- package/dist/{add-XTLU19_E.mjs → add-BiPlBUq3.mjs} +4 -4
- package/dist/{add-XTLU19_E.mjs.map → add-BiPlBUq3.mjs.map} +1 -1
- package/dist/{add-DeC2Kyyv.cjs → add-C1ojB2Yy.cjs} +4 -4
- package/dist/{add-DeC2Kyyv.cjs.map → add-C1ojB2Yy.cjs.map} +1 -1
- package/dist/{bundler-CNTVLCCA.mjs → bundler-C2LZoLdP.mjs} +6 -6
- package/dist/bundler-C2LZoLdP.mjs.map +1 -0
- package/dist/{bundler-CEaR6Dpb.cjs → bundler-C35h0WKt.cjs} +6 -6
- package/dist/bundler-C35h0WKt.cjs.map +1 -0
- package/dist/{cli-D1arS0p1.mjs → cli-BfdfyTF6.mjs} +4 -4
- package/dist/{cli-D1arS0p1.mjs.map → cli-BfdfyTF6.mjs.map} +1 -1
- package/dist/{cli-MMgBrHx7.cjs → cli-DJVL2sL9.cjs} +4 -4
- package/dist/{cli-MMgBrHx7.cjs.map → cli-DJVL2sL9.cjs.map} +1 -1
- package/dist/{compose-T3hnnjKZ.mjs → compose-9II-0IsK.mjs} +221 -64
- package/dist/compose-9II-0IsK.mjs.map +1 -0
- package/dist/{compose-B-MME-4C.cjs → compose-CNnw8meq.cjs} +223 -66
- package/dist/compose-CNnw8meq.cjs.map +1 -0
- package/dist/{config-BN6iXvyR.d.mts → config-B8OR_TrO.d.mts} +3 -3
- package/dist/{config-BN6iXvyR.d.mts.map → config-B8OR_TrO.d.mts.map} +1 -1
- package/dist/{config-j9zBKaEG.d.cts → config-RLuR2o9A.d.cts} +3 -3
- package/dist/{config-j9zBKaEG.d.cts.map → config-RLuR2o9A.d.cts.map} +1 -1
- package/dist/config.cjs +4 -1
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +3 -3
- package/dist/config.d.mts +3 -3
- package/dist/config.mjs +4 -1
- package/dist/config.mjs.map +1 -1
- package/dist/{credentialSchemas-BmYxnEz_.mjs → credentialSchemas-BJqRJNK4.mjs} +2 -2
- package/dist/{credentialSchemas-BmYxnEz_.mjs.map → credentialSchemas-BJqRJNK4.mjs.map} +1 -1
- package/dist/{credentialSchemas-BcNL1N5R.cjs → credentialSchemas-DCR4Wd1W.cjs} +2 -2
- package/dist/{credentialSchemas-BcNL1N5R.cjs.map → credentialSchemas-DCR4Wd1W.cjs.map} +1 -1
- package/dist/deploy/public.cjs +7 -7
- package/dist/deploy/public.d.cts +4 -4
- package/dist/deploy/public.d.cts.map +1 -1
- package/dist/deploy/public.d.mts +4 -4
- package/dist/deploy/public.d.mts.map +1 -1
- package/dist/deploy/public.mjs +7 -7
- package/dist/deploy/sniffer-routes-worker.cjs +2 -2
- package/dist/deploy/sniffer-routes-worker.mjs +2 -2
- package/dist/{deploy-CZE0m6jd.cjs → deploy-BitZLdnY.cjs} +12 -6
- package/dist/deploy-BitZLdnY.cjs.map +1 -0
- package/dist/{deploy-ClC53rc6.mjs → deploy-nmov4j0B.mjs} +12 -6
- package/dist/deploy-nmov4j0B.mjs.map +1 -0
- package/dist/{discover-Buwf1z4P.mjs → discover-BPelzeGW.mjs} +21 -2
- package/dist/discover-BPelzeGW.mjs.map +1 -0
- package/dist/{discover-B_IkPukx.d.cts → discover-CU0paUyH.d.cts} +10 -1
- package/dist/discover-CU0paUyH.d.cts.map +1 -0
- package/dist/{discover-B4VjaFJk.cjs → discover-D1It5r_h.cjs} +21 -2
- package/dist/discover-D1It5r_h.cjs.map +1 -0
- package/dist/{discover-B6yPpvRV.d.mts → discover-DFiJxwfv.d.mts} +10 -1
- package/dist/discover-DFiJxwfv.d.mts.map +1 -0
- package/dist/{dokploy-D1UwQKm3.mjs → dokploy-CZ1vZt21.mjs} +2 -2
- package/dist/{dokploy-D1UwQKm3.mjs.map → dokploy-CZ1vZt21.mjs.map} +1 -1
- package/dist/{dokploy-lwmH05zr.cjs → dokploy-Ce_VTytN.cjs} +2 -2
- package/dist/{dokploy-lwmH05zr.cjs.map → dokploy-Ce_VTytN.cjs.map} +1 -1
- package/dist/{encryption-D05sNwED.cjs → encryption-CDGRdcMr.cjs} +2 -2
- package/dist/{encryption-D05sNwED.cjs.map → encryption-CDGRdcMr.cjs.map} +1 -1
- package/dist/{encryption-DcrxWl1s.mjs → encryption-CmfPICSN.mjs} +2 -2
- package/dist/{encryption-DcrxWl1s.mjs.map → encryption-CmfPICSN.mjs.map} +1 -1
- package/dist/{engine-DmQS3XHX.mjs → engine-B37qBO5W.mjs} +117 -17
- package/dist/engine-B37qBO5W.mjs.map +1 -0
- package/dist/{engine-Cry1vwMz.cjs → engine-DL2-PW6m.cjs} +117 -17
- package/dist/engine-DL2-PW6m.cjs.map +1 -0
- package/dist/{github-BchWoiVx.cjs → github-C6qF8kHg.cjs} +2 -2
- package/dist/{github-BchWoiVx.cjs.map → github-C6qF8kHg.cjs.map} +1 -1
- package/dist/{github-CqYOP8cI.mjs → github-hjHHm7jM.mjs} +2 -2
- package/dist/{github-CqYOP8cI.mjs.map → github-hjHHm7jM.mjs.map} +1 -1
- package/dist/{index-DvoxuTqR.d.mts → index-DHyTqGG3.d.mts} +2 -2
- package/dist/{index-Bk3NY6DN.d.cts.map → index-DHyTqGG3.d.mts.map} +1 -1
- package/dist/{index-Bk3NY6DN.d.cts → index-Dg-UZ4Hm.d.cts} +2 -2
- package/dist/{index-DvoxuTqR.d.mts.map → index-Dg-UZ4Hm.d.cts.map} +1 -1
- package/dist/index.cjs +113 -468
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +107 -462
- package/dist/index.mjs.map +1 -1
- package/dist/{migrate-guck0NJK.mjs → migrate-CFFC3esE.mjs} +3 -3
- package/dist/{migrate-guck0NJK.mjs.map → migrate-CFFC3esE.mjs.map} +1 -1
- package/dist/{migrate-qKeGYq21.cjs → migrate-kA_j1TPK.cjs} +3 -3
- package/dist/{migrate-qKeGYq21.cjs.map → migrate-kA_j1TPK.cjs.map} +1 -1
- package/dist/openapi.cjs +3 -2
- package/dist/openapi.cjs.map +1 -1
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +2 -1
- package/dist/openapi.mjs.map +1 -1
- package/dist/{ready-B-7LayMF.cjs → ready-BIuMw6wH.cjs} +9 -297
- package/dist/ready-BIuMw6wH.cjs.map +1 -0
- package/dist/{ready-ULc5BTOb.mjs → ready-C0U0NYfB.mjs} +6 -288
- package/dist/ready-C0U0NYfB.mjs.map +1 -0
- package/dist/reconcile/public.cjs +1 -1
- package/dist/reconcile/public.d.cts +2 -2
- package/dist/reconcile/public.d.mts +2 -2
- package/dist/reconcile/public.mjs +1 -1
- package/dist/{resolve-BsXEXAUz.mjs → resolve-CV4eQkDa.mjs} +3 -3
- package/dist/{resolve-BsXEXAUz.mjs.map → resolve-CV4eQkDa.mjs.map} +1 -1
- package/dist/{resolve-wEjEvbJ5.cjs → resolve-E5RfY0bm.cjs} +3 -3
- package/dist/{resolve-wEjEvbJ5.cjs.map → resolve-E5RfY0bm.cjs.map} +1 -1
- package/dist/{rollback-Bk91csFg.mjs → rollback-C-V-Rf4a.mjs} +4 -4
- package/dist/{rollback-Bk91csFg.mjs.map → rollback-C-V-Rf4a.mjs.map} +1 -1
- package/dist/{rollback-DmcU-efZ.cjs → rollback-QGwomD64.cjs} +4 -4
- package/dist/{rollback-DmcU-efZ.cjs.map → rollback-QGwomD64.cjs.map} +1 -1
- package/dist/{runtime-DHJhe4QI.d.mts → runtime-CWYLIeCU.d.mts} +2 -2
- package/dist/{runtime-CYAO4iZG.d.cts.map → runtime-CWYLIeCU.d.mts.map} +1 -1
- package/dist/{runtime-CYAO4iZG.d.cts → runtime-CmuQOT6A.d.cts} +2 -2
- package/dist/{runtime-DHJhe4QI.d.mts.map → runtime-CmuQOT6A.d.cts.map} +1 -1
- package/dist/sandbox/credentials-worker.cjs +2 -2
- package/dist/sandbox/credentials-worker.mjs +2 -2
- package/dist/sandbox/discover-worker.cjs +6 -3
- package/dist/sandbox/discover-worker.cjs.map +1 -1
- package/dist/sandbox/discover-worker.mjs +6 -3
- package/dist/sandbox/discover-worker.mjs.map +1 -1
- package/dist/sandbox/migrate-worker.cjs +1 -1
- package/dist/sandbox/migrate-worker.mjs +1 -1
- package/dist/{sniffer-vAXsU_4G.mjs → sniffer-BFmdZpZ2.mjs} +6 -6
- package/dist/{sniffer-vAXsU_4G.mjs.map → sniffer-BFmdZpZ2.mjs.map} +1 -1
- package/dist/{sniffer-DpHpFZBD.cjs → sniffer-C-T7dSMN.cjs} +6 -6
- package/dist/{sniffer-DpHpFZBD.cjs.map → sniffer-C-T7dSMN.cjs.map} +1 -1
- package/dist/{state-CCuTJNRH.cjs → state-DMjFPOhf.cjs} +942 -22
- package/dist/state-DMjFPOhf.cjs.map +1 -0
- package/dist/{state-BJFherj1.mjs → state-DtS2HEfi.mjs} +877 -17
- package/dist/state-DtS2HEfi.mjs.map +1 -0
- package/dist/target/public.cjs +1 -1
- package/dist/target/public.d.cts +2 -2
- package/dist/target/public.d.mts +2 -2
- package/dist/target/public.mjs +1 -1
- package/dist/{terminal-CFO3xWoF.cjs → terminal-BL5E0xbR.cjs} +2 -2
- package/dist/{terminal-CFO3xWoF.cjs.map → terminal-BL5E0xbR.cjs.map} +1 -1
- package/dist/{terminal-C6TBo_SA.mjs → terminal-BdvrmkaC.mjs} +2 -2
- package/dist/{terminal-C6TBo_SA.mjs.map → terminal-BdvrmkaC.mjs.map} +1 -1
- package/dist/{types-DkG5WW8l.d.mts → types-BLdCL4fB.d.mts} +17 -1
- package/dist/types-BLdCL4fB.d.mts.map +1 -0
- package/dist/{types-BD1jgA85.d.cts → types-DwCQCkC-.d.cts} +17 -1
- package/dist/types-DwCQCkC-.d.cts.map +1 -0
- package/dist/vitest.cjs +1 -1
- package/dist/vitest.mjs +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace-C7NqCsQF.mjs.map +1 -1
- package/dist/workspace-vdrcRI-m.cjs.map +1 -1
- package/package.json +15 -15
- package/dist/OpenApiTsGenerator-BlP53kf2.cjs.map +0 -1
- package/dist/OpenApiTsGenerator-Dv0gT2DL.mjs.map +0 -1
- package/dist/bundler-CEaR6Dpb.cjs.map +0 -1
- package/dist/bundler-CNTVLCCA.mjs.map +0 -1
- package/dist/compose-B-MME-4C.cjs.map +0 -1
- package/dist/compose-T3hnnjKZ.mjs.map +0 -1
- package/dist/deploy-CZE0m6jd.cjs.map +0 -1
- package/dist/deploy-ClC53rc6.mjs.map +0 -1
- package/dist/discover-B4VjaFJk.cjs.map +0 -1
- package/dist/discover-B6yPpvRV.d.mts.map +0 -1
- package/dist/discover-B_IkPukx.d.cts.map +0 -1
- package/dist/discover-Buwf1z4P.mjs.map +0 -1
- package/dist/engine-Cry1vwMz.cjs.map +0 -1
- package/dist/engine-DmQS3XHX.mjs.map +0 -1
- package/dist/fakes-C9RiT6XG.mjs +0 -125
- package/dist/fakes-C9RiT6XG.mjs.map +0 -1
- package/dist/fakes-zW-jNAbQ.cjs +0 -148
- package/dist/fakes-zW-jNAbQ.cjs.map +0 -1
- package/dist/ready-B-7LayMF.cjs.map +0 -1
- package/dist/ready-ULc5BTOb.mjs.map +0 -1
- package/dist/state-BJFherj1.mjs.map +0 -1
- package/dist/state-CCuTJNRH.cjs.map +0 -1
- package/dist/types-BD1jgA85.d.cts.map +0 -1
- package/dist/types-DkG5WW8l.d.mts.map +0 -1
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
2
2
|
const require_run = require("./run-fVZ-rLnc.cjs");
|
|
3
|
-
const require_discover = require("./discover-
|
|
3
|
+
const require_discover = require("./discover-D1It5r_h.cjs");
|
|
4
4
|
const require_derive = require("./derive-DAbo49sm.cjs");
|
|
5
5
|
const require_workspace = require("./workspace-vdrcRI-m.cjs");
|
|
6
6
|
const require_config = require("./config.cjs");
|
|
7
|
-
const
|
|
8
|
-
const
|
|
7
|
+
const require_EndpointGenerator = require("./EndpointGenerator-CP6sBvQV.cjs");
|
|
8
|
+
const require_resolve = require("./resolve-E5RfY0bm.cjs");
|
|
9
|
+
const require_SubscriberGenerator = require("./SubscriberGenerator-Bixm28fN.cjs");
|
|
9
10
|
let node_path = require("node:path");
|
|
10
11
|
let node_fs = require("node:fs");
|
|
11
12
|
let node_fs_promises = require("node:fs/promises");
|
|
12
13
|
let node_os = require("node:os");
|
|
13
14
|
let node_util = require("node:util");
|
|
15
|
+
let _geekmidas_constructs_crons = require("@geekmidas/constructs/crons");
|
|
16
|
+
let _geekmidas_constructs_queue = require("@geekmidas/constructs/queue");
|
|
14
17
|
let _geekmidas_manifest = require("@geekmidas/manifest");
|
|
15
18
|
let node_net = require("node:net");
|
|
16
19
|
let node_child_process = require("node:child_process");
|
|
@@ -1005,11 +1008,11 @@ function providerOf(workspace) {
|
|
|
1005
1008
|
* would be worse than a slower one.
|
|
1006
1009
|
*/
|
|
1007
1010
|
function cacheBackendFor(on) {
|
|
1008
|
-
return
|
|
1011
|
+
return require_SubscriberGenerator.DEFAULT_CACHE[on];
|
|
1009
1012
|
}
|
|
1010
1013
|
/** Where a declared bucket lives on a target. */
|
|
1011
1014
|
function storageBackendFor(on) {
|
|
1012
|
-
return
|
|
1015
|
+
return require_SubscriberGenerator.DEFAULT_STORAGE[on];
|
|
1013
1016
|
}
|
|
1014
1017
|
/**
|
|
1015
1018
|
* Which broker carries declared queues and topics on a target.
|
|
@@ -1018,7 +1021,7 @@ function storageBackendFor(on) {
|
|
|
1018
1021
|
* `Queue` has no events at all, whatever the target.
|
|
1019
1022
|
*/
|
|
1020
1023
|
function eventsBackendFor(on) {
|
|
1021
|
-
return
|
|
1024
|
+
return require_SubscriberGenerator.DEFAULT_EVENTS[on];
|
|
1022
1025
|
}
|
|
1023
1026
|
//#endregion
|
|
1024
1027
|
//#region src/docker/templates.ts
|
|
@@ -1538,11 +1541,37 @@ CMD ["node", "${appPath}/server.js"]
|
|
|
1538
1541
|
* @internal Exported for testing
|
|
1539
1542
|
*/
|
|
1540
1543
|
function generateBackendDockerfile(options) {
|
|
1544
|
+
return gkmBundleDockerfile(options, {
|
|
1545
|
+
title: "Backend Dockerfile",
|
|
1546
|
+
bundle: "server.mjs",
|
|
1547
|
+
as: "server.mjs",
|
|
1548
|
+
expose: true
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
/** The port a worker answers its health check on, inside its container. */
|
|
1552
|
+
const WORKER_PORT = 3e3;
|
|
1553
|
+
/**
|
|
1554
|
+
* A Worker's image: the same build as its host app's backend — a pruned
|
|
1555
|
+
* slice, `gkm build --provider server --production` in the image, with the
|
|
1556
|
+
* encrypted credentials from the `gkm_credentials` secret — and a runner that
|
|
1557
|
+
* is the worker's bundle alone. It publishes nothing: its one route is the
|
|
1558
|
+
* health check the HEALTHCHECK asks.
|
|
1559
|
+
* @internal Exported for testing
|
|
1560
|
+
*/
|
|
1561
|
+
function generateWorkerDockerfile(options) {
|
|
1562
|
+
return gkmBundleDockerfile(options, {
|
|
1563
|
+
title: `Worker Dockerfile (${options.worker})`,
|
|
1564
|
+
bundle: options.bundle,
|
|
1565
|
+
as: "worker.mjs",
|
|
1566
|
+
expose: false
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
function gkmBundleDockerfile(options, runner) {
|
|
1541
1570
|
const { port, healthCheckPath = "/health" } = options;
|
|
1542
1571
|
const { app } = layout(options);
|
|
1543
1572
|
const externals = externalsStages(options, app);
|
|
1544
1573
|
return `# syntax=docker/dockerfile:1
|
|
1545
|
-
#
|
|
1574
|
+
# ${runner.title}: a turbo-pruned slice, bundled by gkm build in the image
|
|
1546
1575
|
|
|
1547
1576
|
${prunerStage(options)}
|
|
1548
1577
|
|
|
@@ -1563,8 +1592,8 @@ RUN apk add --no-cache tini
|
|
|
1563
1592
|
RUN addgroup --system --gid 1001 nodejs && \\
|
|
1564
1593
|
adduser --system --uid 1001 hono
|
|
1565
1594
|
|
|
1566
|
-
# The bundled server
|
|
1567
|
-
COPY --from=builder --chown=hono:nodejs ${app}/.gkm/server/dist
|
|
1595
|
+
# The bundled ${runner.as === "server.mjs" ? "server" : "worker"}
|
|
1596
|
+
COPY --from=builder --chown=hono:nodejs ${app}/.gkm/server/dist/${runner.bundle} ./${runner.as === runner.bundle ? "" : runner.as}${externals.copy}
|
|
1568
1597
|
|
|
1569
1598
|
ENV NODE_ENV=production
|
|
1570
1599
|
ENV PORT=${port}
|
|
@@ -1573,11 +1602,9 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
|
1573
1602
|
CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
|
|
1574
1603
|
|
|
1575
1604
|
USER hono
|
|
1576
|
-
|
|
1577
|
-
EXPOSE ${port}
|
|
1578
|
-
|
|
1605
|
+
${runner.expose ? `\nEXPOSE ${port}\n` : ""}
|
|
1579
1606
|
ENTRYPOINT ["/sbin/tini", "--"]
|
|
1580
|
-
CMD ["node", "
|
|
1607
|
+
CMD ["node", "${runner.as}"]
|
|
1581
1608
|
`;
|
|
1582
1609
|
}
|
|
1583
1610
|
/**
|
|
@@ -2264,7 +2291,7 @@ function resourceName(id, kind, stage, localStage) {
|
|
|
2264
2291
|
* broker the project selected, which is why the backend is an argument here
|
|
2265
2292
|
* rather than a second pass that adds containers after the fact.
|
|
2266
2293
|
*/
|
|
2267
|
-
function containerFor(kind, events = DEFAULT_EVENTS, cache =
|
|
2294
|
+
function containerFor(kind, events = DEFAULT_EVENTS, cache = require_SubscriberGenerator.DEFAULT_CACHE.aws, derived = false, edge = true) {
|
|
2268
2295
|
if (EVENT_KINDS[kind]) return EVENT_CONTAINERS[events] ?? CONTAINERS.database;
|
|
2269
2296
|
if (kind === "file-server") return edge ? "caddy" : CONTAINERS.objects;
|
|
2270
2297
|
if (kind === "cache") {
|
|
@@ -2285,7 +2312,7 @@ function planFor(manifest, stage, order, options = {}) {
|
|
|
2285
2312
|
const resources = [];
|
|
2286
2313
|
const fakes = {};
|
|
2287
2314
|
const events = options.events ?? "pgboss";
|
|
2288
|
-
const cache = options.cache ??
|
|
2315
|
+
const cache = options.cache ?? require_SubscriberGenerator.DEFAULT_CACHE.aws;
|
|
2289
2316
|
for (const id of order) {
|
|
2290
2317
|
const declaration = manifest[id];
|
|
2291
2318
|
if (!declaration) continue;
|
|
@@ -2986,6 +3013,36 @@ function appEnvKeys(manifest, appName, runnables = {}) {
|
|
|
2986
3013
|
for (const key of other.provides ?? []) keys.add(key);
|
|
2987
3014
|
}
|
|
2988
3015
|
}
|
|
3016
|
+
addEdgeKeys(manifest, edges, keys);
|
|
3017
|
+
if (declaration.kind === "site" || declaration.kind === "mobile-app") for (const key of Object.keys((0, _geekmidas_manifest.publicEnvFor)(declaration, manifest))) keys.add(key);
|
|
3018
|
+
return keys;
|
|
3019
|
+
}
|
|
3020
|
+
/**
|
|
3021
|
+
* The keys a Worker's own process reads: what the worker and everything built
|
|
3022
|
+
* from it declared an edge to, the broker its crons schedule through, and the
|
|
3023
|
+
* address of every queue and topic — which of them its consumers and
|
|
3024
|
+
* subscribers are bound to is only known once a build has found them, so a
|
|
3025
|
+
* process that runs them is handed each carrier's.
|
|
3026
|
+
*
|
|
3027
|
+
* `undefined` when no worker has that id.
|
|
3028
|
+
*/
|
|
3029
|
+
function workerEnvKeys(manifest, workerId, runnables = {}) {
|
|
3030
|
+
const declaration = manifest[workerId];
|
|
3031
|
+
if (declaration?.kind !== "worker") return void 0;
|
|
3032
|
+
const keys = /* @__PURE__ */ new Set([
|
|
3033
|
+
...declaration.provides ?? [],
|
|
3034
|
+
...declaration.requires ?? [],
|
|
3035
|
+
"EVENT_PUBLISHER_CONNECTION_STRING"
|
|
3036
|
+
]);
|
|
3037
|
+
for (const other of Object.values(manifest)) {
|
|
3038
|
+
if (other.kind !== "queue" && other.kind !== "topic") continue;
|
|
3039
|
+
for (const key of other.provides ?? []) keys.add(key);
|
|
3040
|
+
}
|
|
3041
|
+
addEdgeKeys(manifest, [...(0, _geekmidas_manifest.dependenciesOf)(declaration).map((edge) => edge.target), ...runnables[workerId] ?? []], keys);
|
|
3042
|
+
return keys;
|
|
3043
|
+
}
|
|
3044
|
+
/** What each construct an edge reaches hands the process that reaches it. */
|
|
3045
|
+
function addEdgeKeys(manifest, edges, keys) {
|
|
2989
3046
|
for (const id of edges) {
|
|
2990
3047
|
const target = manifest[id];
|
|
2991
3048
|
if (!target) continue;
|
|
@@ -3002,8 +3059,6 @@ function appEnvKeys(manifest, appName, runnables = {}) {
|
|
|
3002
3059
|
keys.add("AWS_REGION");
|
|
3003
3060
|
}
|
|
3004
3061
|
}
|
|
3005
|
-
if (declaration.kind === "site" || declaration.kind === "mobile-app") for (const key of Object.keys((0, _geekmidas_manifest.publicEnvFor)(declaration, manifest))) keys.add(key);
|
|
3006
|
-
return keys;
|
|
3007
3062
|
}
|
|
3008
3063
|
/** One compose service per app the workspace runs, behind the apps profile. */
|
|
3009
3064
|
function appServices(workspace, manifest, containers, runnables = {}, fakes = {}) {
|
|
@@ -4467,7 +4522,7 @@ async function reconcileWorkspace(workspace, options) {
|
|
|
4467
4522
|
runnables
|
|
4468
4523
|
});
|
|
4469
4524
|
const manifest = options.manifest ?? discovered;
|
|
4470
|
-
const fakes = fakesApply(options.stage, options.fake) ? await
|
|
4525
|
+
const fakes = fakesApply(options.stage, options.fake) ? await require_SubscriberGenerator.readFakes(workspace.root, manifest) : {};
|
|
4471
4526
|
const result = await reconcile({
|
|
4472
4527
|
root: workspace.root,
|
|
4473
4528
|
project: workspace.name,
|
|
@@ -4534,6 +4589,707 @@ function localAddress(port) {
|
|
|
4534
4589
|
return `http://localhost:${port}`;
|
|
4535
4590
|
}
|
|
4536
4591
|
//#endregion
|
|
4592
|
+
//#region src/generators/CronGenerator.ts
|
|
4593
|
+
var CronGenerator = class extends require_discover.ConstructGenerator {
|
|
4594
|
+
async build(context, constructs, outputDir, options) {
|
|
4595
|
+
const target = options?.target ?? "aws";
|
|
4596
|
+
const logger = console;
|
|
4597
|
+
const cronInfos = [];
|
|
4598
|
+
if (target === "server") {
|
|
4599
|
+
await this.generateServerCronsFile(outputDir, constructs);
|
|
4600
|
+
logger.log(`Generated server crons file with ${constructs.length} crons`);
|
|
4601
|
+
return cronInfos;
|
|
4602
|
+
}
|
|
4603
|
+
if (constructs.length === 0) return cronInfos;
|
|
4604
|
+
const cronsDir = (0, node_path.join)(outputDir, "crons");
|
|
4605
|
+
await (0, node_fs_promises.mkdir)(cronsDir, { recursive: true });
|
|
4606
|
+
for (const { key, construct, path } of constructs) {
|
|
4607
|
+
const handlerFile = await this.generateCronHandler(cronsDir, path.relative, key, context, construct.owner);
|
|
4608
|
+
cronInfos.push({
|
|
4609
|
+
name: key,
|
|
4610
|
+
handler: (0, node_path.relative)(options?.root ?? process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
4611
|
+
schedule: construct.schedule || "rate(1 hour)",
|
|
4612
|
+
timeout: construct.timeout,
|
|
4613
|
+
memorySize: construct.memorySize,
|
|
4614
|
+
environment: await construct.getEnvironment({ markOptional: context.markOptional }),
|
|
4615
|
+
dependencies: construct.constructs
|
|
4616
|
+
});
|
|
4617
|
+
logger.log(`Generated cron handler: ${key}`);
|
|
4618
|
+
}
|
|
4619
|
+
return cronInfos;
|
|
4620
|
+
}
|
|
4621
|
+
isConstruct(value) {
|
|
4622
|
+
return _geekmidas_constructs_crons.Cron.isCron(value);
|
|
4623
|
+
}
|
|
4624
|
+
/**
|
|
4625
|
+
* The crons file a server entry imports.
|
|
4626
|
+
*
|
|
4627
|
+
* Shaped after `generateServerSubscribersFile`: one module, every cron, a
|
|
4628
|
+
* `setupCrons` the generated entry calls beside `setupSubscribers`.
|
|
4629
|
+
*
|
|
4630
|
+
* It picks its scheduler from what the process can reach. With a Postgres it
|
|
4631
|
+
* uses pg-boss, whose schedules live in the database — so a deployment
|
|
4632
|
+
* running four replicas fires each job once, which is the thing a timer in
|
|
4633
|
+
* every process gets wrong and does not report. With no database it falls
|
|
4634
|
+
* back to timers and says plainly that it is doing so.
|
|
4635
|
+
*/
|
|
4636
|
+
async generateServerCronsFile(outputDir, crons) {
|
|
4637
|
+
await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
|
|
4638
|
+
const cronsPath = (0, node_path.join)(outputDir, "crons.ts");
|
|
4639
|
+
if (crons.length === 0) {
|
|
4640
|
+
await (0, node_fs_promises.writeFile)(cronsPath, `/**
|
|
4641
|
+
* Generated cron setup — this app declares none.
|
|
4642
|
+
*/
|
|
4643
|
+
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
4644
|
+
import type { Logger } from '@geekmidas/logger';
|
|
4645
|
+
|
|
4646
|
+
export async function setupCrons(
|
|
4647
|
+
_envParser: EnvironmentParser<any>,
|
|
4648
|
+
_logger: Logger,
|
|
4649
|
+
): Promise<void> {}
|
|
4650
|
+
|
|
4651
|
+
export function cronsStatus(): { failed: string[]; connections: unknown[] } {
|
|
4652
|
+
return { failed: [], connections: [] };
|
|
4653
|
+
}
|
|
4654
|
+
|
|
4655
|
+
export async function stopCrons(): Promise<void> {}
|
|
4656
|
+
`);
|
|
4657
|
+
return cronsPath;
|
|
4658
|
+
}
|
|
4659
|
+
const importsByFile = /* @__PURE__ */ new Map();
|
|
4660
|
+
for (const { path, key } of crons) {
|
|
4661
|
+
const importPath = (0, node_path.relative)((0, node_path.dirname)(cronsPath), path.relative).replace(/\.ts$/, ".js");
|
|
4662
|
+
if (!importsByFile.has(importPath)) importsByFile.set(importPath, []);
|
|
4663
|
+
importsByFile.get(importPath)?.push(key);
|
|
4664
|
+
}
|
|
4665
|
+
const content = `/**
|
|
4666
|
+
* Generated cron setup.
|
|
4667
|
+
*
|
|
4668
|
+
* Schedules every cron this app declares, in the process that serves it.
|
|
4669
|
+
*
|
|
4670
|
+
* The schedule lives in Postgres, through pg-boss, in the database the worker
|
|
4671
|
+
* named with \`.database(db)\`. That is what makes running several replicas
|
|
4672
|
+
* safe: each job fires once, where a timer in every process fires it once per
|
|
4673
|
+
* replica and reports nothing.
|
|
4674
|
+
*
|
|
4675
|
+
* No connection string appears here. The construct that owns the database is
|
|
4676
|
+
* the only thing that knows its key, and it is resolved through service
|
|
4677
|
+
* discovery like any other dependency.
|
|
4678
|
+
*/
|
|
4679
|
+
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
4680
|
+
import type { Logger } from '@geekmidas/logger';
|
|
4681
|
+
import { runCron, scheduleInProcess, toCronExpression } from '@geekmidas/constructs/crons';
|
|
4682
|
+
import { ServiceDiscovery } from '@geekmidas/services';
|
|
4683
|
+
${Array.from(importsByFile.entries()).map(([importPath, exports]) => `import { ${exports.join(", ")} } from '${importPath}';`).join("\n")}
|
|
4684
|
+
|
|
4685
|
+
const crons = [
|
|
4686
|
+
${crons.map(({ key }) => ` { name: '${key}', cron: ${key} }`).join(",\n")}
|
|
4687
|
+
];
|
|
4688
|
+
|
|
4689
|
+
type Stoppable = { stop: () => Promise<void> | void };
|
|
4690
|
+
const running: Stoppable[] = [];
|
|
4691
|
+
// What a worker's health check reads: the crons that cannot be scheduled, and
|
|
4692
|
+
// the broker connection the schedule is kept through.
|
|
4693
|
+
const failed: string[] = [];
|
|
4694
|
+
const connections: import('@geekmidas/events').EventConnection[] = [];
|
|
4695
|
+
|
|
4696
|
+
export async function setupCrons(
|
|
4697
|
+
envParser: EnvironmentParser<any>,
|
|
4698
|
+
logger: Logger,
|
|
4699
|
+
): Promise<void> {
|
|
4700
|
+
if (crons.length === 0) {
|
|
4701
|
+
return;
|
|
4702
|
+
}
|
|
4703
|
+
|
|
4704
|
+
const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
|
|
4705
|
+
|
|
4706
|
+
// Resolved once per cron rather than per firing: registering services on
|
|
4707
|
+
// every tick would reconnect a database every minute.
|
|
4708
|
+
const prepared = [];
|
|
4709
|
+
for (const { name, cron } of crons) {
|
|
4710
|
+
try {
|
|
4711
|
+
const schedule = toCronExpression(cron.schedule);
|
|
4712
|
+
const services = cron.services.length > 0
|
|
4713
|
+
? await serviceDiscovery.register(cron.services)
|
|
4714
|
+
: {};
|
|
4715
|
+
|
|
4716
|
+
prepared.push({ name, cron, schedule, services });
|
|
4717
|
+
} catch (error) {
|
|
4718
|
+
// An unrepresentable schedule is a build-time mistake, and failing the
|
|
4719
|
+
// process for it would take the HTTP server down with it.
|
|
4720
|
+
logger.error({ err: error, cron: name }, 'Cron has no runnable schedule, skipping');
|
|
4721
|
+
failed.push(name);
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
|
|
4725
|
+
const run = async (entry: (typeof prepared)[number]) => {
|
|
4726
|
+
try {
|
|
4727
|
+
// The cron's own function, the way its Lambda would run it — services,
|
|
4728
|
+
// the worker's database as \`db\`, its events. It used to call a
|
|
4729
|
+
// \`handler\` a Cron does not have, so every firing logged a failure.
|
|
4730
|
+
await runCron(entry.cron, serviceDiscovery);
|
|
4731
|
+
} catch (error) {
|
|
4732
|
+
logger.error({ err: error, cron: entry.name }, 'Cron failed');
|
|
4733
|
+
}
|
|
4734
|
+
};
|
|
4735
|
+
|
|
4736
|
+
// The app's one pg-boss: the events broker's, as the broker's role, in the
|
|
4737
|
+
// broker's schema. A server fires its own crons, and a process scheduling in
|
|
4738
|
+
// memory fires each job once per replica — so the schedule lives in
|
|
4739
|
+
// Postgres. It used to be a second pg-boss on the worker's database, as the
|
|
4740
|
+
// runtime role, which can neither create a schema nor use the broker's.
|
|
4741
|
+
const { url, devPid } = envParser
|
|
4742
|
+
.create((get) => ({
|
|
4743
|
+
url: get('EVENT_PUBLISHER_CONNECTION_STRING').string().optional(),
|
|
4744
|
+
// Set by \`gkm dev\` on the server it starts, and by nothing deployed.
|
|
4745
|
+
devPid: get('GKM_DEV_PID').string().optional(),
|
|
4746
|
+
}))
|
|
4747
|
+
.parse();
|
|
4748
|
+
|
|
4749
|
+
if (!url?.startsWith('pgboss://')) {
|
|
4750
|
+
// \`gkm dev\` on a target whose crons are infrastructure once deployed —
|
|
4751
|
+
// EventBridge rules on AWS — has no broker to keep a schedule in. It is one
|
|
4752
|
+
// process, so a timer fires each job once, which is all pg-boss is for
|
|
4753
|
+
// here. Only there: a deployed server firing by timer fires once per
|
|
4754
|
+
// replica, so outside dev this stays the error below.
|
|
4755
|
+
if (devPid) {
|
|
4756
|
+
for (const entry of prepared) {
|
|
4757
|
+
running.push(scheduleInProcess(entry.schedule, () => run(entry)));
|
|
4758
|
+
logger.info({ cron: entry.name, schedule: entry.schedule }, 'Cron scheduled in this process');
|
|
4759
|
+
}
|
|
4760
|
+
return;
|
|
4761
|
+
}
|
|
4762
|
+
|
|
4763
|
+
logger.error(
|
|
4764
|
+
{ crons: prepared.map(({ name }) => name) },
|
|
4765
|
+
'These crons have nowhere to keep their schedule. On a server they are ' +
|
|
4766
|
+
'scheduled through the pg-boss events broker ' +
|
|
4767
|
+
'(EVENT_PUBLISHER_CONNECTION_STRING), and ' +
|
|
4768
|
+
(url ? 'the broker configured is not pg-boss.' : 'none is configured.'),
|
|
4769
|
+
);
|
|
4770
|
+
failed.push(...prepared.map(({ name }) => name));
|
|
4771
|
+
return;
|
|
4772
|
+
}
|
|
4773
|
+
|
|
4774
|
+
// Through the registered pg-boss driver, which the entry registers for a
|
|
4775
|
+
// server whose broker is pg-boss. Naming \`@geekmidas/events/pgboss\` here
|
|
4776
|
+
// made every bundle with a cron resolve pg-boss, whatever its broker.
|
|
4777
|
+
const { EventConnectionFactory } = await import('@geekmidas/events');
|
|
4778
|
+
const connection = (await EventConnectionFactory.fromConnectionString(
|
|
4779
|
+
url,
|
|
4780
|
+
)) as import('@geekmidas/events').EventConnection & { instance?: any };
|
|
4781
|
+
const boss = connection.instance!;
|
|
4782
|
+
|
|
4783
|
+
// What this app declares now. Anything else under the prefix belonged to a
|
|
4784
|
+
// cron since renamed or deleted, and would keep firing at a handler that has
|
|
4785
|
+
// gone.
|
|
4786
|
+
//
|
|
4787
|
+
// The prefix is \`cron.\` rather than \`cron:\`: pg-boss accepts
|
|
4788
|
+
// alphanumerics, underscores, hyphens, periods and slashes in a queue name,
|
|
4789
|
+
// and rejects a colon outright.
|
|
4790
|
+
const declared = new Set(prepared.map(({ name }) => \`cron.\${name}\`));
|
|
4791
|
+
for (const existing of await boss.getSchedules()) {
|
|
4792
|
+
if (existing.name.startsWith('cron.') && !declared.has(existing.name)) {
|
|
4793
|
+
await boss.unschedule(existing.name);
|
|
4794
|
+
logger.info({ cron: existing.name }, 'Removed a schedule nothing declares');
|
|
4795
|
+
}
|
|
4796
|
+
}
|
|
4797
|
+
|
|
4798
|
+
for (const entry of prepared) {
|
|
4799
|
+
const queue = \`cron.\${entry.name}\`;
|
|
4800
|
+
await boss.createQueue(queue);
|
|
4801
|
+
// UTC, so this agrees with the schedule a deploy target would have made.
|
|
4802
|
+
await boss.schedule(queue, entry.schedule, undefined, { tz: 'UTC' });
|
|
4803
|
+
await boss.work(queue, () => run(entry));
|
|
4804
|
+
|
|
4805
|
+
logger.info({ cron: entry.name, schedule: entry.schedule }, 'Cron scheduled');
|
|
4806
|
+
}
|
|
4807
|
+
|
|
4808
|
+
running.push({ stop: () => connection.close() });
|
|
4809
|
+
connections.push(connection);
|
|
4810
|
+
logger.info({ count: prepared.length }, 'Crons scheduled');
|
|
4811
|
+
}
|
|
4812
|
+
|
|
4813
|
+
/** The crons that cannot be scheduled, and the connection that schedules the rest. */
|
|
4814
|
+
export function cronsStatus(): {
|
|
4815
|
+
failed: string[];
|
|
4816
|
+
connections: import('@geekmidas/events').EventConnection[];
|
|
4817
|
+
} {
|
|
4818
|
+
return { failed: [...failed], connections: [...connections] };
|
|
4819
|
+
}
|
|
4820
|
+
|
|
4821
|
+
/**
|
|
4822
|
+
* Stop scheduling, let a firing in flight finish, and close the broker
|
|
4823
|
+
* connection — pg-boss's graceful stop does all three. The caller bounds it.
|
|
4824
|
+
*/
|
|
4825
|
+
export async function stopCrons(): Promise<void> {
|
|
4826
|
+
const open = [...running];
|
|
4827
|
+
running.length = 0;
|
|
4828
|
+
connections.length = 0;
|
|
4829
|
+
await Promise.all(open.map((r) => r.stop()));
|
|
4830
|
+
}
|
|
4831
|
+
`;
|
|
4832
|
+
await (0, node_fs_promises.writeFile)(cronsPath, content);
|
|
4833
|
+
return cronsPath;
|
|
4834
|
+
}
|
|
4835
|
+
async generateCronHandler(outputDir, sourceFile, exportName, context, owner) {
|
|
4836
|
+
const handlerFileName = `${exportName}.ts`;
|
|
4837
|
+
const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
|
|
4838
|
+
const importPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile).replace(/\.ts$/, ".js");
|
|
4839
|
+
const runtime = require_EndpointGenerator.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
|
|
4840
|
+
const content = `import { AWSScheduledFunction } from '@geekmidas/constructs/aws';
|
|
4841
|
+
import { ${exportName} } from '${importPath}';
|
|
4842
|
+
${runtime.imports}
|
|
4843
|
+
${context.storageDrivers?.imports ?? ""}
|
|
4844
|
+
${runtime.bindings}
|
|
4845
|
+
${context.storageDrivers?.setup ? `\n// The handler registers the drivers its target needs.\n${context.storageDrivers.setup}\n` : ""}
|
|
4846
|
+
const adapter = new AWSScheduledFunction(envParser, ${exportName});
|
|
4847
|
+
|
|
4848
|
+
export const handler = adapter.handler;
|
|
4849
|
+
`;
|
|
4850
|
+
await (0, node_fs_promises.writeFile)(handlerPath, content);
|
|
4851
|
+
return handlerPath;
|
|
4852
|
+
}
|
|
4853
|
+
};
|
|
4854
|
+
//#endregion
|
|
4855
|
+
//#region src/generators/QueueGenerator.ts
|
|
4856
|
+
/**
|
|
4857
|
+
* Generates the runtime for queue consumers (`worker.queue(…)`).
|
|
4858
|
+
*
|
|
4859
|
+
* - **server** (`gkm dev`): a single `queues.ts` exposing `setupQueues()` that
|
|
4860
|
+
* polls each queue in-process, alongside the Hono server, through the queue's
|
|
4861
|
+
* own connection string — pg-boss by default, SQS on the AWS emulator.
|
|
4862
|
+
* - **aws-lambda**: one handler file per queue wrapping `AWSLambdaQueue`, backed
|
|
4863
|
+
* by an SQS event-source mapping.
|
|
4864
|
+
*/
|
|
4865
|
+
var QueueGenerator = class extends require_discover.ConstructGenerator {
|
|
4866
|
+
isConstruct(value) {
|
|
4867
|
+
return _geekmidas_constructs_queue.Queue.isQueue(value);
|
|
4868
|
+
}
|
|
4869
|
+
async build(context, constructs, outputDir, options) {
|
|
4870
|
+
const target = options?.target ?? "aws";
|
|
4871
|
+
const logger = console;
|
|
4872
|
+
const queueInfos = [];
|
|
4873
|
+
if (target === "server") {
|
|
4874
|
+
await this.generateServerQueuesFile(outputDir, constructs);
|
|
4875
|
+
logger.log(`Generated server queues file with ${constructs.length} queues (polling mode)`);
|
|
4876
|
+
return queueInfos;
|
|
4877
|
+
}
|
|
4878
|
+
if (constructs.length === 0) return queueInfos;
|
|
4879
|
+
const queuesDir = (0, node_path.join)(outputDir, "queues");
|
|
4880
|
+
await (0, node_fs_promises.mkdir)(queuesDir, { recursive: true });
|
|
4881
|
+
for (const { key, construct, path } of constructs) {
|
|
4882
|
+
const handlerFile = await this.generateQueueHandler(queuesDir, path.relative, key, context, construct.owner);
|
|
4883
|
+
queueInfos.push({
|
|
4884
|
+
name: construct.name,
|
|
4885
|
+
handler: (0, node_path.relative)(options?.root ?? process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
4886
|
+
batchSize: construct.batchSize,
|
|
4887
|
+
fifo: construct.fifo,
|
|
4888
|
+
timeout: construct.timeout,
|
|
4889
|
+
environment: await construct.getEnvironment({ markOptional: context.markOptional }),
|
|
4890
|
+
dependencies: construct.constructs
|
|
4891
|
+
});
|
|
4892
|
+
logger.log(`Generated queue handler: ${key}`);
|
|
4893
|
+
}
|
|
4894
|
+
return queueInfos;
|
|
4895
|
+
}
|
|
4896
|
+
async generateQueueHandler(outputDir, sourceFile, exportName, context, owner) {
|
|
4897
|
+
const handlerPath = (0, node_path.join)(outputDir, `${exportName}.ts`);
|
|
4898
|
+
const importPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile).replace(/\.ts$/, ".js");
|
|
4899
|
+
const runtime = require_EndpointGenerator.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
|
|
4900
|
+
const content = `import { AWSLambdaQueue } from '@geekmidas/constructs/aws';
|
|
4901
|
+
import { ${exportName} } from '${importPath}';
|
|
4902
|
+
${runtime.imports}
|
|
4903
|
+
${context.storageDrivers?.imports ?? ""}
|
|
4904
|
+
${runtime.bindings}
|
|
4905
|
+
${context.storageDrivers?.setup ? `\n// The handler registers the drivers its target needs.\n${context.storageDrivers.setup}\n` : ""}
|
|
4906
|
+
const adapter = new AWSLambdaQueue(envParser, ${exportName});
|
|
4907
|
+
|
|
4908
|
+
export const handler = adapter.handler;
|
|
4909
|
+
`;
|
|
4910
|
+
await (0, node_fs_promises.writeFile)(handlerPath, content);
|
|
4911
|
+
return handlerPath;
|
|
4912
|
+
}
|
|
4913
|
+
async generateServerQueuesFile(outputDir, queues) {
|
|
4914
|
+
await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
|
|
4915
|
+
const queuesPath = (0, node_path.join)(outputDir, "queues.ts");
|
|
4916
|
+
if (queues.length === 0) {
|
|
4917
|
+
await (0, node_fs_promises.writeFile)(queuesPath, `/**
|
|
4918
|
+
* Generated queues setup — this app declares none.
|
|
4919
|
+
*/
|
|
4920
|
+
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
4921
|
+
import type { Logger } from '@geekmidas/logger';
|
|
4922
|
+
|
|
4923
|
+
export async function setupQueues(
|
|
4924
|
+
_envParser: EnvironmentParser<any>,
|
|
4925
|
+
_logger: Logger,
|
|
4926
|
+
): Promise<void> {}
|
|
4927
|
+
|
|
4928
|
+
export function queuesStatus(): { failed: string[]; connections: unknown[] } {
|
|
4929
|
+
return { failed: [], connections: [] };
|
|
4930
|
+
}
|
|
4931
|
+
|
|
4932
|
+
export async function stopQueues(): Promise<void> {}
|
|
4933
|
+
`);
|
|
4934
|
+
return queuesPath;
|
|
4935
|
+
}
|
|
4936
|
+
const importsByFile = /* @__PURE__ */ new Map();
|
|
4937
|
+
for (const { path, key } of queues) {
|
|
4938
|
+
const importPath = (0, node_path.relative)((0, node_path.dirname)(queuesPath), path.relative).replace(/\.ts$/, ".js");
|
|
4939
|
+
if (!importsByFile.has(importPath)) importsByFile.set(importPath, []);
|
|
4940
|
+
importsByFile.get(importPath)?.push(key);
|
|
4941
|
+
}
|
|
4942
|
+
const content = `/**
|
|
4943
|
+
* Generated queue consumers setup.
|
|
4944
|
+
*
|
|
4945
|
+
* Each queue's one consumer polls its own queue, through the queue's own
|
|
4946
|
+
* connection string — the one its producers publish on. SQS cannot push, so
|
|
4947
|
+
* this is a poller on every transport: pg-boss locally by default, the SQS
|
|
4948
|
+
* queue on the AWS emulator.
|
|
4949
|
+
*
|
|
4950
|
+
* Deployed on Lambda, a queue's consumer is an SQS event source instead.
|
|
4951
|
+
*/
|
|
4952
|
+
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
4953
|
+
import type { Logger } from '@geekmidas/logger';
|
|
4954
|
+
import { EventConnectionFactory, Subscriber } from '@geekmidas/events';
|
|
4955
|
+
import type { EventConnection } from '@geekmidas/events';
|
|
4956
|
+
import { ServiceDiscovery } from '@geekmidas/services';
|
|
4957
|
+
${Array.from(importsByFile.entries()).map(([importPath, exports]) => `import { ${exports.join(", ")} } from '${importPath}';`).join("\n")}
|
|
4958
|
+
|
|
4959
|
+
const queues = [
|
|
4960
|
+
${queues.map(({ key, construct }) => ` { queue: ${key}, connectionKey: '${(0, _geekmidas_manifest.provideKey)(construct.id, "publisherConnectionString")}' },`).join("\n")}
|
|
4961
|
+
];
|
|
4962
|
+
|
|
4963
|
+
// What was opened, and what could not be: a worker's health check reads both,
|
|
4964
|
+
// and its shutdown closes what was opened.
|
|
4965
|
+
const connections = new Map<string, EventConnection>();
|
|
4966
|
+
const failed: string[] = [];
|
|
4967
|
+
|
|
4968
|
+
export async function setupQueues(
|
|
4969
|
+
envParser: EnvironmentParser<any>,
|
|
4970
|
+
logger: Logger,
|
|
4971
|
+
): Promise<void> {
|
|
4972
|
+
const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
|
|
4973
|
+
|
|
4974
|
+
for (const { queue, connectionKey } of queues) {
|
|
4975
|
+
const connectionString = process.env[connectionKey];
|
|
4976
|
+
if (!connectionString) {
|
|
4977
|
+
logger.error({ queue: queue.name, connectionKey }, 'No connection string for this queue');
|
|
4978
|
+
failed.push(queue.name);
|
|
4979
|
+
continue;
|
|
4980
|
+
}
|
|
4981
|
+
|
|
4982
|
+
try {
|
|
4983
|
+
let connection = connections.get(connectionString);
|
|
4984
|
+
if (!connection) {
|
|
4985
|
+
connection = await EventConnectionFactory.fromConnectionString(connectionString);
|
|
4986
|
+
connections.set(connectionString, connection);
|
|
4987
|
+
}
|
|
4988
|
+
const eventSubscriber = await Subscriber.fromConnection(connection);
|
|
4989
|
+
const services = queue.services.length > 0
|
|
4990
|
+
? await serviceDiscovery.register(queue.services)
|
|
4991
|
+
: {};
|
|
4992
|
+
// The queue's database — the worker's, or its own — handed over as \`db\`.
|
|
4993
|
+
const db = queue.databaseService
|
|
4994
|
+
? (await serviceDiscovery.register([queue.databaseService]))[
|
|
4995
|
+
queue.databaseService.serviceName
|
|
4996
|
+
]
|
|
4997
|
+
: undefined;
|
|
4998
|
+
|
|
4999
|
+
// A queue's messages carry one type — its own name.
|
|
5000
|
+
await eventSubscriber.subscribe([queue.name], async (message) => {
|
|
5001
|
+
try {
|
|
5002
|
+
const validation = await queue.messageSchema['~standard'].validate(message.payload);
|
|
5003
|
+
if (validation.issues) {
|
|
5004
|
+
logger.error({ issues: validation.issues, queue: queue.name }, 'Queue message failed validation');
|
|
5005
|
+
return;
|
|
5006
|
+
}
|
|
5007
|
+
|
|
5008
|
+
await queue.handler({
|
|
5009
|
+
messages: [validation.value],
|
|
5010
|
+
services: services,
|
|
5011
|
+
logger: queue.logger,
|
|
5012
|
+
db,
|
|
5013
|
+
} as any);
|
|
5014
|
+
} catch (error) {
|
|
5015
|
+
logger.error({ err: error, queue: queue.name }, 'Failed to process queue message');
|
|
5016
|
+
// Rethrown so the transport keeps the message for a retry.
|
|
5017
|
+
throw error;
|
|
5018
|
+
}
|
|
5019
|
+
});
|
|
5020
|
+
logger.info({ queue: queue.name }, 'Queue consumer started polling');
|
|
5021
|
+
} catch (error) {
|
|
5022
|
+
logger.error({ err: error, queue: queue.name }, 'Failed to set up queue consumer');
|
|
5023
|
+
failed.push(queue.name);
|
|
5024
|
+
}
|
|
5025
|
+
}
|
|
5026
|
+
}
|
|
5027
|
+
|
|
5028
|
+
/** The consumers that could not start, and the connections that did. */
|
|
5029
|
+
export function queuesStatus(): { failed: string[]; connections: EventConnection[] } {
|
|
5030
|
+
return { failed: [...failed], connections: [...connections.values()] };
|
|
5031
|
+
}
|
|
5032
|
+
|
|
5033
|
+
/**
|
|
5034
|
+
* Stop polling, let the messages in flight finish, and close each broker
|
|
5035
|
+
* connection — pg-boss's graceful stop does all three. The caller bounds it.
|
|
5036
|
+
*/
|
|
5037
|
+
export async function stopQueues(): Promise<void> {
|
|
5038
|
+
const open = [...connections.values()];
|
|
5039
|
+
connections.clear();
|
|
5040
|
+
await Promise.all(open.map((connection) => connection.close()));
|
|
5041
|
+
}
|
|
5042
|
+
`;
|
|
5043
|
+
await (0, node_fs_promises.writeFile)(queuesPath, content);
|
|
5044
|
+
return queuesPath;
|
|
5045
|
+
}
|
|
5046
|
+
};
|
|
5047
|
+
//#endregion
|
|
5048
|
+
//#region src/generators/WorkerGenerator.ts
|
|
5049
|
+
/**
|
|
5050
|
+
* A worker whose topic subscribers would have to be pushed to over HTTP.
|
|
5051
|
+
*
|
|
5052
|
+
* On SNS a subscriber is an HTTP endpoint SNS delivers to, and a worker's
|
|
5053
|
+
* process serves nothing but its health check. A server target's broker is
|
|
5054
|
+
* pg-boss, where each subscriber polls a queue of its own, so this is only
|
|
5055
|
+
* reached by building server images for a project that deploys to AWS — where
|
|
5056
|
+
* a subscriber is a Lambda instead.
|
|
5057
|
+
*/
|
|
5058
|
+
var WorkerSubscribersNeedPush = class extends Error {
|
|
5059
|
+
worker;
|
|
5060
|
+
subscribers;
|
|
5061
|
+
constructor(worker, subscribers) {
|
|
5062
|
+
super(`The worker '${worker}' runs topic subscribers (${subscribers.join(", ")}), and this build's broker is SNS, which pushes each event to its subscriber over HTTP. A worker image serves only its health check. Deploy to a server target, whose broker is pg-boss and whose subscribers poll, or to AWS, where each subscriber is a Lambda.`);
|
|
5063
|
+
this.worker = worker;
|
|
5064
|
+
this.subscribers = subscribers;
|
|
5065
|
+
this.name = "WorkerSubscribersNeedPush";
|
|
5066
|
+
}
|
|
5067
|
+
};
|
|
5068
|
+
/** Where a worker's generated entry lives, under an app's `.gkm/server`. */
|
|
5069
|
+
function workerEntryDir(serverDir, workerId) {
|
|
5070
|
+
return (0, node_path.join)(serverDir, "workers", require_derive.appKey(workerId));
|
|
5071
|
+
}
|
|
5072
|
+
/** The file a worker's bundle is written to, beside the server's. */
|
|
5073
|
+
function workerBundleName(workerId) {
|
|
5074
|
+
return `worker-${require_derive.appKey(workerId)}.mjs`;
|
|
5075
|
+
}
|
|
5076
|
+
/**
|
|
5077
|
+
* A Worker's production entry: the process that runs its crons, its queue
|
|
5078
|
+
* consumers and its topic subscribers, and answers nothing but a health check.
|
|
5079
|
+
*
|
|
5080
|
+
* It is the server's runtime without the server. The `crons.ts`, `queues.ts`
|
|
5081
|
+
* and `subscribers.ts` it starts are the files `gkm dev` starts — written by
|
|
5082
|
+
* the same generators, given only this worker's constructs — so a consumer
|
|
5083
|
+
* runs the same code in development and in the image. What it adds is the
|
|
5084
|
+
* process around them: drivers, a health check that says when a consumer or
|
|
5085
|
+
* the broker is down, and a shutdown that drains.
|
|
5086
|
+
*/
|
|
5087
|
+
var WorkerGenerator = class {
|
|
5088
|
+
generators;
|
|
5089
|
+
constructor(generators = {
|
|
5090
|
+
cron: new CronGenerator(),
|
|
5091
|
+
queue: new QueueGenerator(),
|
|
5092
|
+
subscriber: new require_SubscriberGenerator.SubscriberGenerator()
|
|
5093
|
+
}) {
|
|
5094
|
+
this.generators = generators;
|
|
5095
|
+
}
|
|
5096
|
+
/** Write the worker's files; returns its entry, `worker.ts`. */
|
|
5097
|
+
async build(input) {
|
|
5098
|
+
const { context, workerId, outputDir } = input;
|
|
5099
|
+
if (input.subscribers.length > 0 && context.eventsBackends?.includes("sns")) throw new WorkerSubscribersNeedPush(workerId, input.subscribers.map(({ key }) => key));
|
|
5100
|
+
await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
|
|
5101
|
+
const options = { target: "server" };
|
|
5102
|
+
await Promise.all([
|
|
5103
|
+
this.generators.cron.build(context, input.crons, outputDir, options),
|
|
5104
|
+
this.generators.queue.build(context, input.queues, outputDir, options),
|
|
5105
|
+
this.generators.subscriber.build(context, input.subscribers, outputDir, options)
|
|
5106
|
+
]);
|
|
5107
|
+
await (0, node_fs_promises.writeFile)((0, node_path.join)(outputDir, "app.ts"), workerApp(context, workerId, outputDir));
|
|
5108
|
+
await (0, node_fs_promises.writeFile)((0, node_path.join)(outputDir, "telemetry.ts"), require_EndpointGenerator.generateTelemetryModule(context.telemetry ? {
|
|
5109
|
+
...context.telemetry,
|
|
5110
|
+
serviceName: workerId
|
|
5111
|
+
} : void 0));
|
|
5112
|
+
const entry = (0, node_path.join)(outputDir, "worker.ts");
|
|
5113
|
+
await (0, node_fs_promises.writeFile)(entry, workerEntry(context.production?.healthCheck ?? "/health"));
|
|
5114
|
+
return entry;
|
|
5115
|
+
}
|
|
5116
|
+
};
|
|
5117
|
+
/**
|
|
5118
|
+
* `app.ts`: the worker's runtime — its logger and environment parser off the
|
|
5119
|
+
* Worker construct, the drivers its target needs, and a `startWorker` that
|
|
5120
|
+
* starts every cron, consumer and subscriber it owns.
|
|
5121
|
+
*/
|
|
5122
|
+
function workerApp(context, workerId, outputDir) {
|
|
5123
|
+
const runtime = require_EndpointGenerator.runtimeFor(context, outputDir, workerId);
|
|
5124
|
+
return `/**
|
|
5125
|
+
* Generated worker runtime for ${workerId}.
|
|
5126
|
+
* Generated by 'gkm build --production'.
|
|
5127
|
+
*/
|
|
5128
|
+
import { setupCrons, cronsStatus, stopCrons } from './crons.js';
|
|
5129
|
+
import { setupQueues, queuesStatus, stopQueues } from './queues.js';
|
|
5130
|
+
import { setupSubscribers, subscribersStatus, stopSubscribers } from './subscribers.js';
|
|
5131
|
+
${runtime.imports}
|
|
5132
|
+
${context.storageDrivers?.imports ?? ""}
|
|
5133
|
+
|
|
5134
|
+
${runtime.bindings}
|
|
5135
|
+
|
|
5136
|
+
${context.storageDrivers?.setup ? `// The entry point registers the drivers its target needs.
|
|
5137
|
+
${context.storageDrivers.setup}
|
|
5138
|
+
` : ""}
|
|
5139
|
+
// Who this worker is to Postgres: application_name on every connection.
|
|
5140
|
+
process.env.GKM_APP_NAME ??= ${JSON.stringify(workerId)};
|
|
5141
|
+
|
|
5142
|
+
export { logger };
|
|
5143
|
+
|
|
5144
|
+
/** What the health check answers, and why. */
|
|
5145
|
+
export interface WorkerHealth {
|
|
5146
|
+
healthy: boolean;
|
|
5147
|
+
/** The crons, queues and subscribers that could not start. */
|
|
5148
|
+
failed: string[];
|
|
5149
|
+
/** How many broker connections did not answer. */
|
|
5150
|
+
down: number;
|
|
5151
|
+
}
|
|
5152
|
+
|
|
5153
|
+
/** Whether one broker connection answers: open, and — on pg-boss — a query. */
|
|
5154
|
+
async function answers(connection: unknown): Promise<boolean> {
|
|
5155
|
+
const c = connection as {
|
|
5156
|
+
isConnected?: () => boolean;
|
|
5157
|
+
instance?: { getDb?: () => { executeSql: (sql: string) => Promise<unknown> } };
|
|
5158
|
+
};
|
|
5159
|
+
if (c.isConnected && !c.isConnected()) return false;
|
|
5160
|
+
const db = c.instance?.getDb?.();
|
|
5161
|
+
if (!db) return true;
|
|
5162
|
+
try {
|
|
5163
|
+
await Promise.race([
|
|
5164
|
+
db.executeSql('select 1'),
|
|
5165
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 2000).unref()),
|
|
5166
|
+
]);
|
|
5167
|
+
return true;
|
|
5168
|
+
} catch {
|
|
5169
|
+
return false;
|
|
5170
|
+
}
|
|
5171
|
+
}
|
|
5172
|
+
|
|
5173
|
+
/** Start everything this worker runs. */
|
|
5174
|
+
export async function startWorker(): Promise<{
|
|
5175
|
+
health: () => Promise<WorkerHealth>;
|
|
5176
|
+
stop: () => Promise<void>;
|
|
5177
|
+
}> {
|
|
5178
|
+
// An app object with no routes: on a server target nothing is pushed to a
|
|
5179
|
+
// subscriber, so there is nothing to mount.
|
|
5180
|
+
const noRoutes = { post: () => undefined };
|
|
5181
|
+
|
|
5182
|
+
await setupSubscribers(noRoutes, envParser, logger).catch((error) => {
|
|
5183
|
+
logger.error({ err: error }, 'Failed to start subscribers');
|
|
5184
|
+
});
|
|
5185
|
+
await setupQueues(envParser, logger).catch((error) => {
|
|
5186
|
+
logger.error({ err: error }, 'Failed to start queue consumers');
|
|
5187
|
+
});
|
|
5188
|
+
await setupCrons(envParser, logger).catch((error) => {
|
|
5189
|
+
logger.error({ err: error }, 'Failed to schedule crons');
|
|
5190
|
+
});
|
|
5191
|
+
|
|
5192
|
+
const statuses = () => [subscribersStatus(), queuesStatus(), cronsStatus()];
|
|
5193
|
+
|
|
5194
|
+
return {
|
|
5195
|
+
async health() {
|
|
5196
|
+
const all = statuses();
|
|
5197
|
+
const failed = all.flatMap((s) => s.failed);
|
|
5198
|
+
const alive = await Promise.all(
|
|
5199
|
+
all.flatMap((s) => s.connections as unknown[]).map(answers),
|
|
5200
|
+
);
|
|
5201
|
+
const down = alive.filter((ok) => !ok).length;
|
|
5202
|
+
return { healthy: failed.length === 0 && down === 0, failed, down };
|
|
5203
|
+
},
|
|
5204
|
+
// Each stops pulling, waits for what it has in flight, and closes its
|
|
5205
|
+
// broker connection. Together, so one slow handler does not hold the
|
|
5206
|
+
// others' connections open.
|
|
5207
|
+
async stop() {
|
|
5208
|
+
await Promise.all([stopSubscribers(), stopQueues(), stopCrons()]);
|
|
5209
|
+
},
|
|
5210
|
+
};
|
|
5211
|
+
}
|
|
5212
|
+
`;
|
|
5213
|
+
}
|
|
5214
|
+
/**
|
|
5215
|
+
* `worker.ts`: the process. Health first, so the container answers while the
|
|
5216
|
+
* consumers connect; then the worker; then a shutdown that drains.
|
|
5217
|
+
*/
|
|
5218
|
+
function workerEntry(healthCheck) {
|
|
5219
|
+
return `#!/usr/bin/env node
|
|
5220
|
+
/**
|
|
5221
|
+
* Production worker entry point.
|
|
5222
|
+
* Generated by 'gkm build --production'.
|
|
5223
|
+
*
|
|
5224
|
+
* Runs this worker's crons, queue consumers and topic subscribers, and serves
|
|
5225
|
+
* one route: ${healthCheck}, on PORT — 200 when every consumer started and
|
|
5226
|
+
* every broker connection answers, 503 otherwise.
|
|
5227
|
+
*
|
|
5228
|
+
* On SIGTERM it stops pulling messages and scheduling crons, lets the handlers
|
|
5229
|
+
* in flight finish, closes its broker connections and database pools, and
|
|
5230
|
+
* exits 0 — or 1, at GKM_SHUTDOWN_TIMEOUT_MS (8s by default, under Docker's
|
|
5231
|
+
* 10s stop timeout).
|
|
5232
|
+
*/
|
|
5233
|
+
import { createServer } from 'node:http';
|
|
5234
|
+
import { runShutdownHooks } from '@geekmidas/constructs';
|
|
5235
|
+
import { startTelemetry } from './telemetry.js';
|
|
5236
|
+
|
|
5237
|
+
// Before the worker is imported, so the libraries it loads are instrumented.
|
|
5238
|
+
await startTelemetry({ ignorePaths: [${JSON.stringify(healthCheck)}] });
|
|
5239
|
+
|
|
5240
|
+
const { startWorker, logger } = await import('./app.js');
|
|
5241
|
+
|
|
5242
|
+
const port = Number(process.env.PORT) || 3000;
|
|
5243
|
+
|
|
5244
|
+
let worker: Awaited<ReturnType<typeof startWorker>> | undefined;
|
|
5245
|
+
let stopping = false;
|
|
5246
|
+
|
|
5247
|
+
const server = createServer((req, res) => {
|
|
5248
|
+
const path = (req.url ?? '/').split('?')[0];
|
|
5249
|
+
if (path !== ${JSON.stringify(healthCheck)} || (req.method !== 'GET' && req.method !== 'HEAD')) {
|
|
5250
|
+
res.writeHead(404).end();
|
|
5251
|
+
return;
|
|
5252
|
+
}
|
|
5253
|
+
const answer = (status: number, body: unknown) => {
|
|
5254
|
+
res.writeHead(status, { 'content-type': 'application/json' });
|
|
5255
|
+
res.end(JSON.stringify(body));
|
|
5256
|
+
};
|
|
5257
|
+
if (stopping) return answer(503, { status: 'stopping' });
|
|
5258
|
+
if (!worker) return answer(503, { status: 'starting' });
|
|
5259
|
+
worker.health().then(
|
|
5260
|
+
(health) => answer(health.healthy ? 200 : 503, { status: health.healthy ? 'ok' : 'unhealthy', ...health }),
|
|
5261
|
+
(error) => answer(503, { status: 'unhealthy', error: String(error) }),
|
|
5262
|
+
);
|
|
5263
|
+
});
|
|
5264
|
+
server.listen(port);
|
|
5265
|
+
|
|
5266
|
+
const shutdown = async () => {
|
|
5267
|
+
if (stopping) return;
|
|
5268
|
+
stopping = true;
|
|
5269
|
+
const deadline = Number(process.env.GKM_SHUTDOWN_TIMEOUT_MS) || 8000;
|
|
5270
|
+
logger.info({ deadline }, 'Graceful shutdown initiated');
|
|
5271
|
+
setTimeout(() => {
|
|
5272
|
+
logger.warn({ deadline }, 'Shutdown deadline reached, exiting');
|
|
5273
|
+
process.exit(1);
|
|
5274
|
+
}, deadline).unref();
|
|
5275
|
+
|
|
5276
|
+
// Nothing new is pulled or scheduled; what is in flight finishes.
|
|
5277
|
+
await worker?.stop().catch((error) => logger.error({ err: error }, 'Failed to stop the worker'));
|
|
5278
|
+
// Then what the handlers used: database pools.
|
|
5279
|
+
await runShutdownHooks((error) => logger.error({ err: error }, 'Shutdown hook failed'));
|
|
5280
|
+
server.close();
|
|
5281
|
+
logger.info('Worker stopped');
|
|
5282
|
+
process.exit(0);
|
|
5283
|
+
};
|
|
5284
|
+
|
|
5285
|
+
process.on('SIGTERM', shutdown);
|
|
5286
|
+
process.on('SIGINT', shutdown);
|
|
5287
|
+
|
|
5288
|
+
worker = await startWorker();
|
|
5289
|
+
logger.info({ port }, 'Worker started');
|
|
5290
|
+
`;
|
|
5291
|
+
}
|
|
5292
|
+
//#endregion
|
|
4537
5293
|
//#region src/secrets/encryption.ts
|
|
4538
5294
|
var encryption_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
4539
5295
|
encryptSecrets: () => encryptSecrets,
|
|
@@ -4586,6 +5342,74 @@ function keyFingerprint(masterKey) {
|
|
|
4586
5342
|
return (0, node_crypto.createHash)("sha256").update(masterKey).digest("hex").slice(0, 8);
|
|
4587
5343
|
}
|
|
4588
5344
|
//#endregion
|
|
5345
|
+
//#region src/build/workers.ts
|
|
5346
|
+
/** A worker with background work and no backend app to build it from. */
|
|
5347
|
+
var WorkerHasNoApp = class extends Error {
|
|
5348
|
+
worker;
|
|
5349
|
+
constructor(worker) {
|
|
5350
|
+
super(`The worker '${worker}' has crons, queues or subscribers, and the workspace has no backend app that builds a server to carry its entry. Declare a RestApi whose app holds the worker's code.`);
|
|
5351
|
+
this.worker = worker;
|
|
5352
|
+
this.name = "WorkerHasNoApp";
|
|
5353
|
+
}
|
|
5354
|
+
};
|
|
5355
|
+
/** A worker whose service name an app already has. */
|
|
5356
|
+
var WorkerNameTaken = class extends Error {
|
|
5357
|
+
worker;
|
|
5358
|
+
service;
|
|
5359
|
+
constructor(worker, service) {
|
|
5360
|
+
super(`The worker '${worker}' runs as the service '${service}', and an app is already called that. Rename the worker — new Worker('${worker}Worker') — so each container has a name of its own.`);
|
|
5361
|
+
this.worker = worker;
|
|
5362
|
+
this.service = service;
|
|
5363
|
+
this.name = "WorkerNameTaken";
|
|
5364
|
+
}
|
|
5365
|
+
};
|
|
5366
|
+
/**
|
|
5367
|
+
* The workers a workspace deploys — each one that has background work — and
|
|
5368
|
+
* the app each is built from.
|
|
5369
|
+
*
|
|
5370
|
+
* The host is the backend app whose directory holds the most of the worker's
|
|
5371
|
+
* files; one whose work sits outside every app (a shared `jobs/` directory)
|
|
5372
|
+
* is built by the first backend, by name, that generates a server. An app
|
|
5373
|
+
* with its own entry is never one — gkm's generators do not build it — and a
|
|
5374
|
+
* surface that serves itself, an auth server, only when it holds the work.
|
|
5375
|
+
*
|
|
5376
|
+
* @param background Each worker's files, from discovery's `background`.
|
|
5377
|
+
*/
|
|
5378
|
+
function workerUnits(workspace, manifest, background) {
|
|
5379
|
+
const hosts = Object.entries(workspace.apps).filter(([, app]) => app.type === "backend" && !app.entry).map(([name, app]) => {
|
|
5380
|
+
const surface = Object.entries(manifest).find(([id, d]) => d.kind === "rest-api" && require_derive.appKey(id) === name)?.[1];
|
|
5381
|
+
return {
|
|
5382
|
+
name,
|
|
5383
|
+
root: (0, node_path.isAbsolute)(app.path) ? app.path : (0, node_path.join)(workspace.root, app.path),
|
|
5384
|
+
generated: !(surface?.kind === "rest-api" && surface.endpoints.length > 0)
|
|
5385
|
+
};
|
|
5386
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
5387
|
+
const units = [];
|
|
5388
|
+
for (const [id, declaration] of Object.entries(manifest)) {
|
|
5389
|
+
if (declaration.kind !== "worker") continue;
|
|
5390
|
+
const files = background[id] ?? [];
|
|
5391
|
+
if (files.length === 0) continue;
|
|
5392
|
+
const name = require_derive.appKey(id);
|
|
5393
|
+
if (workspace.apps[name]) throw new WorkerNameTaken(id, name);
|
|
5394
|
+
const ranked = hosts.map((host) => ({
|
|
5395
|
+
host,
|
|
5396
|
+
holds: files.filter((file) => isUnder(file, host.root)).length
|
|
5397
|
+
})).sort((a, b) => b.holds - a.holds || Number(b.host.generated) - Number(a.host.generated));
|
|
5398
|
+
const host = ranked[0]?.host;
|
|
5399
|
+
if (!host || ranked[0].holds === 0 && !host.generated) throw new WorkerHasNoApp(id);
|
|
5400
|
+
units.push({
|
|
5401
|
+
id,
|
|
5402
|
+
name,
|
|
5403
|
+
app: host.name
|
|
5404
|
+
});
|
|
5405
|
+
}
|
|
5406
|
+
return units.sort((a, b) => a.name.localeCompare(b.name));
|
|
5407
|
+
}
|
|
5408
|
+
function isUnder(file, dir) {
|
|
5409
|
+
const rel = (0, node_path.relative)(dir, file);
|
|
5410
|
+
return rel !== "" && !rel.startsWith("..") && !(0, node_path.isAbsolute)(rel) ? !rel.startsWith(`..${node_path.sep}`) : false;
|
|
5411
|
+
}
|
|
5412
|
+
//#endregion
|
|
4589
5413
|
//#region src/deploy/devServices.ts
|
|
4590
5414
|
/**
|
|
4591
5415
|
* Mail and object storage on a deployed stage: real ones, or — only when
|
|
@@ -5043,7 +5867,7 @@ async function dockerCommand(options) {
|
|
|
5043
5867
|
const cwd = options.cwd ?? process.cwd();
|
|
5044
5868
|
const loaded = await require_config.loadWorkspaceConfig(cwd);
|
|
5045
5869
|
const names = loaded.type === "single" ? Object.fromEntries(Object.keys(loaded.workspace.apps).map((name) => [name, resolveDockerConfig(loaded.raw).imageName])) : {};
|
|
5046
|
-
const result = await workspaceDockerCommand(loaded.workspace, loaded.manifest, names);
|
|
5870
|
+
const result = await workspaceDockerCommand(loaded.workspace, loaded.manifest, names, loaded.background);
|
|
5047
5871
|
if (options.build) for (const app of result.apps) await buildDockerImage(app, result.buildRoot, options);
|
|
5048
5872
|
if (options.push) for (const app of result.apps) await pushDockerImage(app.imageName, options, result.buildRoot);
|
|
5049
5873
|
return result;
|
|
@@ -5095,7 +5919,7 @@ async function pushDockerImage(imageName, options, cwd) {
|
|
|
5095
5919
|
* Generate Dockerfiles for all apps in a workspace.
|
|
5096
5920
|
* @internal Exported for testing
|
|
5097
5921
|
*/
|
|
5098
|
-
async function workspaceDockerCommand(workspace, manifest, imageNames = {}) {
|
|
5922
|
+
async function workspaceDockerCommand(workspace, manifest, imageNames = {}, background = {}) {
|
|
5099
5923
|
const results = [];
|
|
5100
5924
|
const layout = imageLayout(workspace);
|
|
5101
5925
|
logger$1.log(`\n🐳 Generating Dockerfiles for workspace: ${workspace.name}`);
|
|
@@ -5125,6 +5949,24 @@ async function workspaceDockerCommand(workspace, manifest, imageNames = {}) {
|
|
|
5125
5949
|
imageName: imageNames[appName] ?? appName
|
|
5126
5950
|
});
|
|
5127
5951
|
}
|
|
5952
|
+
for (const worker of manifest ? workerUnits(workspace, manifest, background) : []) {
|
|
5953
|
+
logger$1.log(`\n 📄 Generating Dockerfile for ${worker.name} (worker ${worker.id}, built from ${worker.app})`);
|
|
5954
|
+
const dockerfile = workerDockerfile(worker, workspace.apps[worker.app], {
|
|
5955
|
+
layout,
|
|
5956
|
+
workspaceRoot: workspace.root
|
|
5957
|
+
});
|
|
5958
|
+
const path = workerDockerfileOf(worker.name);
|
|
5959
|
+
const dockerfilePath = (0, node_path.join)(workspace.root, path);
|
|
5960
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(dockerfilePath), { recursive: true });
|
|
5961
|
+
await (0, node_fs_promises.writeFile)(dockerfilePath, dockerfile);
|
|
5962
|
+
logger$1.log(` Generated: ${path}`);
|
|
5963
|
+
results.push({
|
|
5964
|
+
appName: worker.name,
|
|
5965
|
+
type: "backend",
|
|
5966
|
+
dockerfile: dockerfilePath,
|
|
5967
|
+
imageName: imageNames[worker.name] ?? worker.name
|
|
5968
|
+
});
|
|
5969
|
+
}
|
|
5128
5970
|
const dockerignorePath = await ensureDockerignore(layout.buildRoot);
|
|
5129
5971
|
logger$1.log(`\n Ensured: ${dockerignorePath}`);
|
|
5130
5972
|
const composePath = await writeConstructsCompose(workspace);
|
|
@@ -5164,6 +6006,24 @@ function appDockerfile(appName, app, options) {
|
|
|
5164
6006
|
healthCheckPath: "/health"
|
|
5165
6007
|
});
|
|
5166
6008
|
}
|
|
6009
|
+
/** Where `gkm docker` writes a worker's Dockerfile. */
|
|
6010
|
+
function workerDockerfileOf(worker) {
|
|
6011
|
+
return `.gkm/docker/Dockerfile.${worker}`;
|
|
6012
|
+
}
|
|
6013
|
+
/**
|
|
6014
|
+
* A Worker's Dockerfile: its host app's build, and the worker's bundle as the
|
|
6015
|
+
* runner — see `generateWorkerDockerfile`.
|
|
6016
|
+
*/
|
|
6017
|
+
function workerDockerfile(worker, host, options) {
|
|
6018
|
+
return generateWorkerDockerfile({
|
|
6019
|
+
...appImageOptions(options.layout, worker.app, host, options.workspaceRoot),
|
|
6020
|
+
imageName: worker.name,
|
|
6021
|
+
port: WORKER_PORT,
|
|
6022
|
+
worker: worker.id,
|
|
6023
|
+
bundle: workerBundleName(worker.id),
|
|
6024
|
+
healthCheckPath: "/health"
|
|
6025
|
+
});
|
|
6026
|
+
}
|
|
5167
6027
|
/**
|
|
5168
6028
|
* A site's Dockerfile, with an `ARG` for every key its bundler inlines.
|
|
5169
6029
|
*
|
|
@@ -5401,7 +6261,7 @@ async function deployDocker(options) {
|
|
|
5401
6261
|
const cwd = options.cwd ?? process.cwd();
|
|
5402
6262
|
const docker = {
|
|
5403
6263
|
cwd,
|
|
5404
|
-
dockerfile: (0, node_path.join)(cwd, dockerfileOf(config.appName ?? imageName, options.appPath ?? ".")),
|
|
6264
|
+
dockerfile: (0, node_path.join)(cwd, options.dockerfile ?? dockerfileOf(config.appName ?? imageName, options.appPath ?? ".")),
|
|
5405
6265
|
...options.signal ? { signal: options.signal } : {},
|
|
5406
6266
|
...options.stdio ? { stdio: options.stdio } : {}
|
|
5407
6267
|
};
|
|
@@ -5733,6 +6593,12 @@ function setPostgresBackupId(state, backupId) {
|
|
|
5733
6593
|
if (state.backups) state.backups.postgresBackupId = backupId;
|
|
5734
6594
|
}
|
|
5735
6595
|
//#endregion
|
|
6596
|
+
Object.defineProperty(exports, "CronGenerator", {
|
|
6597
|
+
enumerable: true,
|
|
6598
|
+
get: function() {
|
|
6599
|
+
return CronGenerator;
|
|
6600
|
+
}
|
|
6601
|
+
});
|
|
5736
6602
|
Object.defineProperty(exports, "DEFAULT_IMAGES", {
|
|
5737
6603
|
enumerable: true,
|
|
5738
6604
|
get: function() {
|
|
@@ -5787,6 +6653,12 @@ Object.defineProperty(exports, "NoDomainForStage", {
|
|
|
5787
6653
|
return NoDomainForStage;
|
|
5788
6654
|
}
|
|
5789
6655
|
});
|
|
6656
|
+
Object.defineProperty(exports, "QueueGenerator", {
|
|
6657
|
+
enumerable: true,
|
|
6658
|
+
get: function() {
|
|
6659
|
+
return QueueGenerator;
|
|
6660
|
+
}
|
|
6661
|
+
});
|
|
5790
6662
|
Object.defineProperty(exports, "STORAGE_KEY_PAIR", {
|
|
5791
6663
|
enumerable: true,
|
|
5792
6664
|
get: function() {
|
|
@@ -5805,6 +6677,18 @@ Object.defineProperty(exports, "UnknownDevService", {
|
|
|
5805
6677
|
return UnknownDevService;
|
|
5806
6678
|
}
|
|
5807
6679
|
});
|
|
6680
|
+
Object.defineProperty(exports, "WORKER_PORT", {
|
|
6681
|
+
enumerable: true,
|
|
6682
|
+
get: function() {
|
|
6683
|
+
return WORKER_PORT;
|
|
6684
|
+
}
|
|
6685
|
+
});
|
|
6686
|
+
Object.defineProperty(exports, "WorkerGenerator", {
|
|
6687
|
+
enumerable: true,
|
|
6688
|
+
get: function() {
|
|
6689
|
+
return WorkerGenerator;
|
|
6690
|
+
}
|
|
6691
|
+
});
|
|
5808
6692
|
Object.defineProperty(exports, "appDockerfile", {
|
|
5809
6693
|
enumerable: true,
|
|
5810
6694
|
get: function() {
|
|
@@ -6381,6 +7265,42 @@ Object.defineProperty(exports, "withGeneratedSecrets", {
|
|
|
6381
7265
|
return withGeneratedSecrets;
|
|
6382
7266
|
}
|
|
6383
7267
|
});
|
|
7268
|
+
Object.defineProperty(exports, "workerBundleName", {
|
|
7269
|
+
enumerable: true,
|
|
7270
|
+
get: function() {
|
|
7271
|
+
return workerBundleName;
|
|
7272
|
+
}
|
|
7273
|
+
});
|
|
7274
|
+
Object.defineProperty(exports, "workerDockerfile", {
|
|
7275
|
+
enumerable: true,
|
|
7276
|
+
get: function() {
|
|
7277
|
+
return workerDockerfile;
|
|
7278
|
+
}
|
|
7279
|
+
});
|
|
7280
|
+
Object.defineProperty(exports, "workerDockerfileOf", {
|
|
7281
|
+
enumerable: true,
|
|
7282
|
+
get: function() {
|
|
7283
|
+
return workerDockerfileOf;
|
|
7284
|
+
}
|
|
7285
|
+
});
|
|
7286
|
+
Object.defineProperty(exports, "workerEntryDir", {
|
|
7287
|
+
enumerable: true,
|
|
7288
|
+
get: function() {
|
|
7289
|
+
return workerEntryDir;
|
|
7290
|
+
}
|
|
7291
|
+
});
|
|
7292
|
+
Object.defineProperty(exports, "workerEnvKeys", {
|
|
7293
|
+
enumerable: true,
|
|
7294
|
+
get: function() {
|
|
7295
|
+
return workerEnvKeys;
|
|
7296
|
+
}
|
|
7297
|
+
});
|
|
7298
|
+
Object.defineProperty(exports, "workerUnits", {
|
|
7299
|
+
enumerable: true,
|
|
7300
|
+
get: function() {
|
|
7301
|
+
return workerUnits;
|
|
7302
|
+
}
|
|
7303
|
+
});
|
|
6384
7304
|
Object.defineProperty(exports, "workspace_exports", {
|
|
6385
7305
|
enumerable: true,
|
|
6386
7306
|
get: function() {
|
|
@@ -6388,4 +7308,4 @@ Object.defineProperty(exports, "workspace_exports", {
|
|
|
6388
7308
|
}
|
|
6389
7309
|
});
|
|
6390
7310
|
|
|
6391
|
-
//# sourceMappingURL=state-
|
|
7311
|
+
//# sourceMappingURL=state-DMjFPOhf.cjs.map
|