@geekmidas/cli 10.0.0-alpha.72 → 10.0.0-alpha.74
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-5looQ3gB.mjs +448 -0
- package/dist/SubscriberGenerator-5looQ3gB.mjs.map +1 -0
- package/dist/SubscriberGenerator-CajwOa5W.cjs +489 -0
- package/dist/SubscriberGenerator-CajwOa5W.cjs.map +1 -0
- package/dist/{add-XTLU19_E.mjs → add-BTH7WaH3.mjs} +4 -4
- package/dist/{add-XTLU19_E.mjs.map → add-BTH7WaH3.mjs.map} +1 -1
- package/dist/{add-DeC2Kyyv.cjs → add-Djrr12Xc.cjs} +4 -4
- package/dist/{add-DeC2Kyyv.cjs.map → add-Djrr12Xc.cjs.map} +1 -1
- package/dist/{bundler-CNTVLCCA.mjs → bundler-BUojOkof.mjs} +6 -6
- package/dist/bundler-BUojOkof.mjs.map +1 -0
- package/dist/{bundler-CEaR6Dpb.cjs → bundler-utHuESvL.cjs} +6 -6
- package/dist/bundler-utHuESvL.cjs.map +1 -0
- package/dist/{cli-MMgBrHx7.cjs → cli-Bx0sUzxH.cjs} +4 -4
- package/dist/{cli-MMgBrHx7.cjs.map → cli-Bx0sUzxH.cjs.map} +1 -1
- package/dist/{cli-D1arS0p1.mjs → cli-CyYceKTS.mjs} +4 -4
- package/dist/{cli-D1arS0p1.mjs.map → cli-CyYceKTS.mjs.map} +1 -1
- package/dist/{compose-T3hnnjKZ.mjs → compose-DZp8DJ0A.mjs} +419 -70
- package/dist/compose-DZp8DJ0A.mjs.map +1 -0
- package/dist/{compose-B-MME-4C.cjs → compose-iuw8wfHk.cjs} +420 -71
- package/dist/compose-iuw8wfHk.cjs.map +1 -0
- package/dist/{config-j9zBKaEG.d.cts → config-B38YWicj.d.cts} +3 -3
- package/dist/{config-BN6iXvyR.d.mts.map → config-B38YWicj.d.cts.map} +1 -1
- package/dist/{config-BN6iXvyR.d.mts → config-DA0kRTQe.d.mts} +3 -3
- package/dist/{config-j9zBKaEG.d.cts.map → config-DA0kRTQe.d.mts.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-ClC53rc6.mjs → deploy-C17K7Imy.mjs} +12 -6
- package/dist/deploy-C17K7Imy.mjs.map +1 -0
- package/dist/{deploy-CZE0m6jd.cjs → deploy-eWorhvQp.cjs} +12 -6
- package/dist/deploy-eWorhvQp.cjs.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-DKa49Nqm.mjs} +2 -2
- package/dist/{dokploy-D1UwQKm3.mjs.map → dokploy-DKa49Nqm.mjs.map} +1 -1
- package/dist/{dokploy-lwmH05zr.cjs → dokploy-DKzcI-RL.cjs} +2 -2
- package/dist/{dokploy-lwmH05zr.cjs.map → dokploy-DKzcI-RL.cjs.map} +1 -1
- package/dist/drivers-BdvtCKc7.cjs +200 -0
- package/dist/drivers-BdvtCKc7.cjs.map +1 -0
- package/dist/drivers-Ch1rJitn.mjs +177 -0
- package/dist/drivers-Ch1rJitn.mjs.map +1 -0
- package/dist/{encryption-D05sNwED.cjs → encryption-DSkJdoyk.cjs} +2 -2
- package/dist/{encryption-D05sNwED.cjs.map → encryption-DSkJdoyk.cjs.map} +1 -1
- package/dist/{encryption-DcrxWl1s.mjs → encryption-sTMz3aWi.mjs} +2 -2
- package/dist/{encryption-DcrxWl1s.mjs.map → encryption-sTMz3aWi.mjs.map} +1 -1
- package/dist/{engine-DmQS3XHX.mjs → engine-B1b4I34Z.mjs} +117 -17
- package/dist/engine-B1b4I34Z.mjs.map +1 -0
- package/dist/{engine-Cry1vwMz.cjs → engine-Bh76sAHy.cjs} +117 -17
- package/dist/engine-Bh76sAHy.cjs.map +1 -0
- package/dist/{github-BchWoiVx.cjs → github-BvN3CfxM.cjs} +2 -2
- package/dist/{github-BchWoiVx.cjs.map → github-BvN3CfxM.cjs.map} +1 -1
- package/dist/{github-CqYOP8cI.mjs → github-C9ODoyZU.mjs} +2 -2
- package/dist/{github-CqYOP8cI.mjs.map → github-C9ODoyZU.mjs.map} +1 -1
- package/dist/{index-DvoxuTqR.d.mts → index-B-1lLGzy.d.mts} +2 -2
- package/dist/{index-Bk3NY6DN.d.cts.map → index-B-1lLGzy.d.mts.map} +1 -1
- package/dist/{index-Bk3NY6DN.d.cts → index-vMidfqhU.d.cts} +2 -2
- package/dist/{index-DvoxuTqR.d.mts.map → index-vMidfqhU.d.cts.map} +1 -1
- package/dist/index.cjs +130 -473
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +122 -465
- package/dist/index.mjs.map +1 -1
- package/dist/{migrate-qKeGYq21.cjs → migrate-B0FCZNkp.cjs} +3 -3
- package/dist/{migrate-qKeGYq21.cjs.map → migrate-B0FCZNkp.cjs.map} +1 -1
- package/dist/{migrate-guck0NJK.mjs → migrate-CTtJhGV9.mjs} +3 -3
- package/dist/{migrate-guck0NJK.mjs.map → migrate-CTtJhGV9.mjs.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-BB9xS4S9.cjs +402 -0
- package/dist/ready-BB9xS4S9.cjs.map +1 -0
- package/dist/ready-ZZoNskrr.mjs +373 -0
- package/dist/ready-ZZoNskrr.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-DSAXj_47.mjs} +3 -3
- package/dist/{resolve-BsXEXAUz.mjs.map → resolve-DSAXj_47.mjs.map} +1 -1
- package/dist/{resolve-wEjEvbJ5.cjs → resolve-D_BU-wKA.cjs} +3 -3
- package/dist/{resolve-wEjEvbJ5.cjs.map → resolve-D_BU-wKA.cjs.map} +1 -1
- package/dist/{rollback-Bk91csFg.mjs → rollback-CWYTUhfW.mjs} +4 -4
- package/dist/{rollback-Bk91csFg.mjs.map → rollback-CWYTUhfW.mjs.map} +1 -1
- package/dist/{rollback-DmcU-efZ.cjs → rollback-DJy3t696.cjs} +4 -4
- package/dist/{rollback-DmcU-efZ.cjs.map → rollback-DJy3t696.cjs.map} +1 -1
- package/dist/{runtime-DHJhe4QI.d.mts → runtime-BzZD5HHi.d.cts} +2 -2
- package/dist/{runtime-CYAO4iZG.d.cts.map → runtime-BzZD5HHi.d.cts.map} +1 -1
- package/dist/{runtime-CYAO4iZG.d.cts → runtime-Cm6J6L7n.d.mts} +2 -2
- package/dist/{runtime-DHJhe4QI.d.mts.map → runtime-Cm6J6L7n.d.mts.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-DCwRWyh-.cjs} +954 -29
- package/dist/state-DCwRWyh-.cjs.map +1 -0
- package/dist/{state-BJFherj1.mjs → state-DpkxSwzI.mjs} +889 -24
- package/dist/state-DpkxSwzI.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-Dp9lgx_S.d.mts} +22 -2
- package/dist/types-Dp9lgx_S.d.mts.map +1 -0
- package/dist/{types-BD1jgA85.d.cts → types-TzCChNrA.d.cts} +22 -2
- package/dist/types-TzCChNrA.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 +0 -872
- package/dist/ready-B-7LayMF.cjs.map +0 -1
- package/dist/ready-ULc5BTOb.mjs +0 -819
- 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-D_BU-wKA.cjs");
|
|
9
|
+
const require_SubscriberGenerator = require("./SubscriberGenerator-CajwOa5W.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
|
|
|
@@ -1551,7 +1580,7 @@ ${depsStage(options)}
|
|
|
1551
1580
|
${builderStage(options)}
|
|
1552
1581
|
|
|
1553
1582
|
# Bundle the production server, with the encrypted credentials embedded
|
|
1554
|
-
${credentialsStep(app, `cd ${app} && ${gkmCommand(
|
|
1583
|
+
${credentialsStep(app, `cd ${app} && ${gkmCommand(`build --provider server --production${options.cache ? ` --cache ${options.cache}` : ""}`)}`)}
|
|
1555
1584
|
${externals.stage}
|
|
1556
1585
|
# Production
|
|
1557
1586
|
FROM ${options.baseImage} AS runner
|
|
@@ -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
|
/**
|
|
@@ -2155,7 +2182,8 @@ const CONTAINERS = {
|
|
|
2155
2182
|
*/
|
|
2156
2183
|
const CACHE_CONTAINERS = {
|
|
2157
2184
|
upstash: "redis-http",
|
|
2158
|
-
elasticache: "redis"
|
|
2185
|
+
elasticache: "redis",
|
|
2186
|
+
redis: "redis"
|
|
2159
2187
|
};
|
|
2160
2188
|
/**
|
|
2161
2189
|
* Kinds that resolve a value without anything running.
|
|
@@ -2264,11 +2292,11 @@ function resourceName(id, kind, stage, localStage) {
|
|
|
2264
2292
|
* broker the project selected, which is why the backend is an argument here
|
|
2265
2293
|
* rather than a second pass that adds containers after the fact.
|
|
2266
2294
|
*/
|
|
2267
|
-
function containerFor(kind, events = DEFAULT_EVENTS, cache =
|
|
2295
|
+
function containerFor(kind, events = DEFAULT_EVENTS, cache = require_SubscriberGenerator.DEFAULT_CACHE.aws, derived = false, edge = true) {
|
|
2268
2296
|
if (EVENT_KINDS[kind]) return EVENT_CONTAINERS[events] ?? CONTAINERS.database;
|
|
2269
2297
|
if (kind === "file-server") return edge ? "caddy" : CONTAINERS.objects;
|
|
2270
2298
|
if (kind === "cache") {
|
|
2271
|
-
if (derived) return CONTAINERS.database;
|
|
2299
|
+
if (derived && !require_SubscriberGenerator.holdsEveryCache(cache)) return CONTAINERS.database;
|
|
2272
2300
|
return CACHE_CONTAINERS[cache] ?? CONTAINERS.database;
|
|
2273
2301
|
}
|
|
2274
2302
|
return CONTAINERS[kind];
|
|
@@ -2285,7 +2313,7 @@ function planFor(manifest, stage, order, options = {}) {
|
|
|
2285
2313
|
const resources = [];
|
|
2286
2314
|
const fakes = {};
|
|
2287
2315
|
const events = options.events ?? "pgboss";
|
|
2288
|
-
const cache = options.cache ??
|
|
2316
|
+
const cache = options.cache ?? require_SubscriberGenerator.DEFAULT_CACHE.aws;
|
|
2289
2317
|
for (const id of order) {
|
|
2290
2318
|
const declaration = manifest[id];
|
|
2291
2319
|
if (!declaration) continue;
|
|
@@ -2301,6 +2329,7 @@ function planFor(manifest, stage, order, options = {}) {
|
|
|
2301
2329
|
port: fake.port
|
|
2302
2330
|
} : {}
|
|
2303
2331
|
};
|
|
2332
|
+
const detached = declaration.kind === "cache" && require_SubscriberGenerator.holdsEveryCache(cache);
|
|
2304
2333
|
const container = fake?.image ? fake.key : containerFor(declaration.kind, events, cache, "of" in declaration && typeof declaration.of === "string", options.edge !== false);
|
|
2305
2334
|
if (!container && !CONTAINERLESS[declaration.kind]) continue;
|
|
2306
2335
|
if (container) {
|
|
@@ -2327,11 +2356,11 @@ function planFor(manifest, stage, order, options = {}) {
|
|
|
2327
2356
|
credentials: fake.credentials
|
|
2328
2357
|
} } : {},
|
|
2329
2358
|
...declaration.kind === "external-api" && !fake ? { url: declaration.url } : {},
|
|
2330
|
-
..."of" in declaration ? { of: declaration.of } : {},
|
|
2359
|
+
..."of" in declaration && !detached ? { of: declaration.of } : {},
|
|
2331
2360
|
..."schema" in declaration && declaration.schema ? { schema: declaration.schema } : {},
|
|
2332
2361
|
..."version" in declaration && declaration.version ? { version: declaration.version } : {},
|
|
2333
2362
|
..."roles" in declaration && declaration.roles === false ? { roles: false } : {},
|
|
2334
|
-
..."table" in declaration && declaration.table ? { table: declaration.table } : {}
|
|
2363
|
+
..."table" in declaration && declaration.table && !detached ? { table: declaration.table } : {}
|
|
2335
2364
|
});
|
|
2336
2365
|
}
|
|
2337
2366
|
if (options.edge !== false && resources.some((r) => EDGE_KINDS[r.kind] === true)) containers.add("caddy");
|
|
@@ -2693,7 +2722,8 @@ function urlFor(resource, plan, ports, project, addresses = {}, credential = { m
|
|
|
2693
2722
|
return url ? withTable(url, resource.table ?? (0, _geekmidas_manifest.cacheTable)(resource.id)) : void 0;
|
|
2694
2723
|
}
|
|
2695
2724
|
switch (plan.cache) {
|
|
2696
|
-
case "elasticache":
|
|
2725
|
+
case "elasticache":
|
|
2726
|
+
case "redis": return `redis://${LOCAL_HOST}:${port}`;
|
|
2697
2727
|
default: return `http://:${LOCAL_TOKEN$1}@${LOCAL_HOST}:${port}`;
|
|
2698
2728
|
}
|
|
2699
2729
|
case "queue":
|
|
@@ -2986,6 +3016,36 @@ function appEnvKeys(manifest, appName, runnables = {}) {
|
|
|
2986
3016
|
for (const key of other.provides ?? []) keys.add(key);
|
|
2987
3017
|
}
|
|
2988
3018
|
}
|
|
3019
|
+
addEdgeKeys(manifest, edges, keys);
|
|
3020
|
+
if (declaration.kind === "site" || declaration.kind === "mobile-app") for (const key of Object.keys((0, _geekmidas_manifest.publicEnvFor)(declaration, manifest))) keys.add(key);
|
|
3021
|
+
return keys;
|
|
3022
|
+
}
|
|
3023
|
+
/**
|
|
3024
|
+
* The keys a Worker's own process reads: what the worker and everything built
|
|
3025
|
+
* from it declared an edge to, the broker its crons schedule through, and the
|
|
3026
|
+
* address of every queue and topic — which of them its consumers and
|
|
3027
|
+
* subscribers are bound to is only known once a build has found them, so a
|
|
3028
|
+
* process that runs them is handed each carrier's.
|
|
3029
|
+
*
|
|
3030
|
+
* `undefined` when no worker has that id.
|
|
3031
|
+
*/
|
|
3032
|
+
function workerEnvKeys(manifest, workerId, runnables = {}) {
|
|
3033
|
+
const declaration = manifest[workerId];
|
|
3034
|
+
if (declaration?.kind !== "worker") return void 0;
|
|
3035
|
+
const keys = /* @__PURE__ */ new Set([
|
|
3036
|
+
...declaration.provides ?? [],
|
|
3037
|
+
...declaration.requires ?? [],
|
|
3038
|
+
"EVENT_PUBLISHER_CONNECTION_STRING"
|
|
3039
|
+
]);
|
|
3040
|
+
for (const other of Object.values(manifest)) {
|
|
3041
|
+
if (other.kind !== "queue" && other.kind !== "topic") continue;
|
|
3042
|
+
for (const key of other.provides ?? []) keys.add(key);
|
|
3043
|
+
}
|
|
3044
|
+
addEdgeKeys(manifest, [...(0, _geekmidas_manifest.dependenciesOf)(declaration).map((edge) => edge.target), ...runnables[workerId] ?? []], keys);
|
|
3045
|
+
return keys;
|
|
3046
|
+
}
|
|
3047
|
+
/** What each construct an edge reaches hands the process that reaches it. */
|
|
3048
|
+
function addEdgeKeys(manifest, edges, keys) {
|
|
2989
3049
|
for (const id of edges) {
|
|
2990
3050
|
const target = manifest[id];
|
|
2991
3051
|
if (!target) continue;
|
|
@@ -3002,8 +3062,6 @@ function appEnvKeys(manifest, appName, runnables = {}) {
|
|
|
3002
3062
|
keys.add("AWS_REGION");
|
|
3003
3063
|
}
|
|
3004
3064
|
}
|
|
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
3065
|
}
|
|
3008
3066
|
/** One compose service per app the workspace runs, behind the apps profile. */
|
|
3009
3067
|
function appServices(workspace, manifest, containers, runnables = {}, fakes = {}) {
|
|
@@ -4467,7 +4525,7 @@ async function reconcileWorkspace(workspace, options) {
|
|
|
4467
4525
|
runnables
|
|
4468
4526
|
});
|
|
4469
4527
|
const manifest = options.manifest ?? discovered;
|
|
4470
|
-
const fakes = fakesApply(options.stage, options.fake) ? await
|
|
4528
|
+
const fakes = fakesApply(options.stage, options.fake) ? await require_SubscriberGenerator.readFakes(workspace.root, manifest) : {};
|
|
4471
4529
|
const result = await reconcile({
|
|
4472
4530
|
root: workspace.root,
|
|
4473
4531
|
project: workspace.name,
|
|
@@ -4534,6 +4592,707 @@ function localAddress(port) {
|
|
|
4534
4592
|
return `http://localhost:${port}`;
|
|
4535
4593
|
}
|
|
4536
4594
|
//#endregion
|
|
4595
|
+
//#region src/generators/CronGenerator.ts
|
|
4596
|
+
var CronGenerator = class extends require_discover.ConstructGenerator {
|
|
4597
|
+
async build(context, constructs, outputDir, options) {
|
|
4598
|
+
const target = options?.target ?? "aws";
|
|
4599
|
+
const logger = console;
|
|
4600
|
+
const cronInfos = [];
|
|
4601
|
+
if (target === "server") {
|
|
4602
|
+
await this.generateServerCronsFile(outputDir, constructs);
|
|
4603
|
+
logger.log(`Generated server crons file with ${constructs.length} crons`);
|
|
4604
|
+
return cronInfos;
|
|
4605
|
+
}
|
|
4606
|
+
if (constructs.length === 0) return cronInfos;
|
|
4607
|
+
const cronsDir = (0, node_path.join)(outputDir, "crons");
|
|
4608
|
+
await (0, node_fs_promises.mkdir)(cronsDir, { recursive: true });
|
|
4609
|
+
for (const { key, construct, path } of constructs) {
|
|
4610
|
+
const handlerFile = await this.generateCronHandler(cronsDir, path.relative, key, context, construct.owner);
|
|
4611
|
+
cronInfos.push({
|
|
4612
|
+
name: key,
|
|
4613
|
+
handler: (0, node_path.relative)(options?.root ?? process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
4614
|
+
schedule: construct.schedule || "rate(1 hour)",
|
|
4615
|
+
timeout: construct.timeout,
|
|
4616
|
+
memorySize: construct.memorySize,
|
|
4617
|
+
environment: await construct.getEnvironment({ markOptional: context.markOptional }),
|
|
4618
|
+
dependencies: construct.constructs
|
|
4619
|
+
});
|
|
4620
|
+
logger.log(`Generated cron handler: ${key}`);
|
|
4621
|
+
}
|
|
4622
|
+
return cronInfos;
|
|
4623
|
+
}
|
|
4624
|
+
isConstruct(value) {
|
|
4625
|
+
return _geekmidas_constructs_crons.Cron.isCron(value);
|
|
4626
|
+
}
|
|
4627
|
+
/**
|
|
4628
|
+
* The crons file a server entry imports.
|
|
4629
|
+
*
|
|
4630
|
+
* Shaped after `generateServerSubscribersFile`: one module, every cron, a
|
|
4631
|
+
* `setupCrons` the generated entry calls beside `setupSubscribers`.
|
|
4632
|
+
*
|
|
4633
|
+
* It picks its scheduler from what the process can reach. With a Postgres it
|
|
4634
|
+
* uses pg-boss, whose schedules live in the database — so a deployment
|
|
4635
|
+
* running four replicas fires each job once, which is the thing a timer in
|
|
4636
|
+
* every process gets wrong and does not report. With no database it falls
|
|
4637
|
+
* back to timers and says plainly that it is doing so.
|
|
4638
|
+
*/
|
|
4639
|
+
async generateServerCronsFile(outputDir, crons) {
|
|
4640
|
+
await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
|
|
4641
|
+
const cronsPath = (0, node_path.join)(outputDir, "crons.ts");
|
|
4642
|
+
if (crons.length === 0) {
|
|
4643
|
+
await (0, node_fs_promises.writeFile)(cronsPath, `/**
|
|
4644
|
+
* Generated cron setup — this app declares none.
|
|
4645
|
+
*/
|
|
4646
|
+
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
4647
|
+
import type { Logger } from '@geekmidas/logger';
|
|
4648
|
+
|
|
4649
|
+
export async function setupCrons(
|
|
4650
|
+
_envParser: EnvironmentParser<any>,
|
|
4651
|
+
_logger: Logger,
|
|
4652
|
+
): Promise<void> {}
|
|
4653
|
+
|
|
4654
|
+
export function cronsStatus(): { failed: string[]; connections: unknown[] } {
|
|
4655
|
+
return { failed: [], connections: [] };
|
|
4656
|
+
}
|
|
4657
|
+
|
|
4658
|
+
export async function stopCrons(): Promise<void> {}
|
|
4659
|
+
`);
|
|
4660
|
+
return cronsPath;
|
|
4661
|
+
}
|
|
4662
|
+
const importsByFile = /* @__PURE__ */ new Map();
|
|
4663
|
+
for (const { path, key } of crons) {
|
|
4664
|
+
const importPath = (0, node_path.relative)((0, node_path.dirname)(cronsPath), path.relative).replace(/\.ts$/, ".js");
|
|
4665
|
+
if (!importsByFile.has(importPath)) importsByFile.set(importPath, []);
|
|
4666
|
+
importsByFile.get(importPath)?.push(key);
|
|
4667
|
+
}
|
|
4668
|
+
const content = `/**
|
|
4669
|
+
* Generated cron setup.
|
|
4670
|
+
*
|
|
4671
|
+
* Schedules every cron this app declares, in the process that serves it.
|
|
4672
|
+
*
|
|
4673
|
+
* The schedule lives in Postgres, through pg-boss, in the database the worker
|
|
4674
|
+
* named with \`.database(db)\`. That is what makes running several replicas
|
|
4675
|
+
* safe: each job fires once, where a timer in every process fires it once per
|
|
4676
|
+
* replica and reports nothing.
|
|
4677
|
+
*
|
|
4678
|
+
* No connection string appears here. The construct that owns the database is
|
|
4679
|
+
* the only thing that knows its key, and it is resolved through service
|
|
4680
|
+
* discovery like any other dependency.
|
|
4681
|
+
*/
|
|
4682
|
+
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
4683
|
+
import type { Logger } from '@geekmidas/logger';
|
|
4684
|
+
import { runCron, scheduleInProcess, toCronExpression } from '@geekmidas/constructs/crons';
|
|
4685
|
+
import { ServiceDiscovery } from '@geekmidas/services';
|
|
4686
|
+
${Array.from(importsByFile.entries()).map(([importPath, exports]) => `import { ${exports.join(", ")} } from '${importPath}';`).join("\n")}
|
|
4687
|
+
|
|
4688
|
+
const crons = [
|
|
4689
|
+
${crons.map(({ key }) => ` { name: '${key}', cron: ${key} }`).join(",\n")}
|
|
4690
|
+
];
|
|
4691
|
+
|
|
4692
|
+
type Stoppable = { stop: () => Promise<void> | void };
|
|
4693
|
+
const running: Stoppable[] = [];
|
|
4694
|
+
// What a worker's health check reads: the crons that cannot be scheduled, and
|
|
4695
|
+
// the broker connection the schedule is kept through.
|
|
4696
|
+
const failed: string[] = [];
|
|
4697
|
+
const connections: import('@geekmidas/events').EventConnection[] = [];
|
|
4698
|
+
|
|
4699
|
+
export async function setupCrons(
|
|
4700
|
+
envParser: EnvironmentParser<any>,
|
|
4701
|
+
logger: Logger,
|
|
4702
|
+
): Promise<void> {
|
|
4703
|
+
if (crons.length === 0) {
|
|
4704
|
+
return;
|
|
4705
|
+
}
|
|
4706
|
+
|
|
4707
|
+
const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
|
|
4708
|
+
|
|
4709
|
+
// Resolved once per cron rather than per firing: registering services on
|
|
4710
|
+
// every tick would reconnect a database every minute.
|
|
4711
|
+
const prepared = [];
|
|
4712
|
+
for (const { name, cron } of crons) {
|
|
4713
|
+
try {
|
|
4714
|
+
const schedule = toCronExpression(cron.schedule);
|
|
4715
|
+
const services = cron.services.length > 0
|
|
4716
|
+
? await serviceDiscovery.register(cron.services)
|
|
4717
|
+
: {};
|
|
4718
|
+
|
|
4719
|
+
prepared.push({ name, cron, schedule, services });
|
|
4720
|
+
} catch (error) {
|
|
4721
|
+
// An unrepresentable schedule is a build-time mistake, and failing the
|
|
4722
|
+
// process for it would take the HTTP server down with it.
|
|
4723
|
+
logger.error({ err: error, cron: name }, 'Cron has no runnable schedule, skipping');
|
|
4724
|
+
failed.push(name);
|
|
4725
|
+
}
|
|
4726
|
+
}
|
|
4727
|
+
|
|
4728
|
+
const run = async (entry: (typeof prepared)[number]) => {
|
|
4729
|
+
try {
|
|
4730
|
+
// The cron's own function, the way its Lambda would run it — services,
|
|
4731
|
+
// the worker's database as \`db\`, its events. It used to call a
|
|
4732
|
+
// \`handler\` a Cron does not have, so every firing logged a failure.
|
|
4733
|
+
await runCron(entry.cron, serviceDiscovery);
|
|
4734
|
+
} catch (error) {
|
|
4735
|
+
logger.error({ err: error, cron: entry.name }, 'Cron failed');
|
|
4736
|
+
}
|
|
4737
|
+
};
|
|
4738
|
+
|
|
4739
|
+
// The app's one pg-boss: the events broker's, as the broker's role, in the
|
|
4740
|
+
// broker's schema. A server fires its own crons, and a process scheduling in
|
|
4741
|
+
// memory fires each job once per replica — so the schedule lives in
|
|
4742
|
+
// Postgres. It used to be a second pg-boss on the worker's database, as the
|
|
4743
|
+
// runtime role, which can neither create a schema nor use the broker's.
|
|
4744
|
+
const { url, devPid } = envParser
|
|
4745
|
+
.create((get) => ({
|
|
4746
|
+
url: get('EVENT_PUBLISHER_CONNECTION_STRING').string().optional(),
|
|
4747
|
+
// Set by \`gkm dev\` on the server it starts, and by nothing deployed.
|
|
4748
|
+
devPid: get('GKM_DEV_PID').string().optional(),
|
|
4749
|
+
}))
|
|
4750
|
+
.parse();
|
|
4751
|
+
|
|
4752
|
+
if (!url?.startsWith('pgboss://')) {
|
|
4753
|
+
// \`gkm dev\` on a target whose crons are infrastructure once deployed —
|
|
4754
|
+
// EventBridge rules on AWS — has no broker to keep a schedule in. It is one
|
|
4755
|
+
// process, so a timer fires each job once, which is all pg-boss is for
|
|
4756
|
+
// here. Only there: a deployed server firing by timer fires once per
|
|
4757
|
+
// replica, so outside dev this stays the error below.
|
|
4758
|
+
if (devPid) {
|
|
4759
|
+
for (const entry of prepared) {
|
|
4760
|
+
running.push(scheduleInProcess(entry.schedule, () => run(entry)));
|
|
4761
|
+
logger.info({ cron: entry.name, schedule: entry.schedule }, 'Cron scheduled in this process');
|
|
4762
|
+
}
|
|
4763
|
+
return;
|
|
4764
|
+
}
|
|
4765
|
+
|
|
4766
|
+
logger.error(
|
|
4767
|
+
{ crons: prepared.map(({ name }) => name) },
|
|
4768
|
+
'These crons have nowhere to keep their schedule. On a server they are ' +
|
|
4769
|
+
'scheduled through the pg-boss events broker ' +
|
|
4770
|
+
'(EVENT_PUBLISHER_CONNECTION_STRING), and ' +
|
|
4771
|
+
(url ? 'the broker configured is not pg-boss.' : 'none is configured.'),
|
|
4772
|
+
);
|
|
4773
|
+
failed.push(...prepared.map(({ name }) => name));
|
|
4774
|
+
return;
|
|
4775
|
+
}
|
|
4776
|
+
|
|
4777
|
+
// Through the registered pg-boss driver, which the entry registers for a
|
|
4778
|
+
// server whose broker is pg-boss. Naming \`@geekmidas/events/pgboss\` here
|
|
4779
|
+
// made every bundle with a cron resolve pg-boss, whatever its broker.
|
|
4780
|
+
const { EventConnectionFactory } = await import('@geekmidas/events');
|
|
4781
|
+
const connection = (await EventConnectionFactory.fromConnectionString(
|
|
4782
|
+
url,
|
|
4783
|
+
)) as import('@geekmidas/events').EventConnection & { instance?: any };
|
|
4784
|
+
const boss = connection.instance!;
|
|
4785
|
+
|
|
4786
|
+
// What this app declares now. Anything else under the prefix belonged to a
|
|
4787
|
+
// cron since renamed or deleted, and would keep firing at a handler that has
|
|
4788
|
+
// gone.
|
|
4789
|
+
//
|
|
4790
|
+
// The prefix is \`cron.\` rather than \`cron:\`: pg-boss accepts
|
|
4791
|
+
// alphanumerics, underscores, hyphens, periods and slashes in a queue name,
|
|
4792
|
+
// and rejects a colon outright.
|
|
4793
|
+
const declared = new Set(prepared.map(({ name }) => \`cron.\${name}\`));
|
|
4794
|
+
for (const existing of await boss.getSchedules()) {
|
|
4795
|
+
if (existing.name.startsWith('cron.') && !declared.has(existing.name)) {
|
|
4796
|
+
await boss.unschedule(existing.name);
|
|
4797
|
+
logger.info({ cron: existing.name }, 'Removed a schedule nothing declares');
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
|
|
4801
|
+
for (const entry of prepared) {
|
|
4802
|
+
const queue = \`cron.\${entry.name}\`;
|
|
4803
|
+
await boss.createQueue(queue);
|
|
4804
|
+
// UTC, so this agrees with the schedule a deploy target would have made.
|
|
4805
|
+
await boss.schedule(queue, entry.schedule, undefined, { tz: 'UTC' });
|
|
4806
|
+
await boss.work(queue, () => run(entry));
|
|
4807
|
+
|
|
4808
|
+
logger.info({ cron: entry.name, schedule: entry.schedule }, 'Cron scheduled');
|
|
4809
|
+
}
|
|
4810
|
+
|
|
4811
|
+
running.push({ stop: () => connection.close() });
|
|
4812
|
+
connections.push(connection);
|
|
4813
|
+
logger.info({ count: prepared.length }, 'Crons scheduled');
|
|
4814
|
+
}
|
|
4815
|
+
|
|
4816
|
+
/** The crons that cannot be scheduled, and the connection that schedules the rest. */
|
|
4817
|
+
export function cronsStatus(): {
|
|
4818
|
+
failed: string[];
|
|
4819
|
+
connections: import('@geekmidas/events').EventConnection[];
|
|
4820
|
+
} {
|
|
4821
|
+
return { failed: [...failed], connections: [...connections] };
|
|
4822
|
+
}
|
|
4823
|
+
|
|
4824
|
+
/**
|
|
4825
|
+
* Stop scheduling, let a firing in flight finish, and close the broker
|
|
4826
|
+
* connection — pg-boss's graceful stop does all three. The caller bounds it.
|
|
4827
|
+
*/
|
|
4828
|
+
export async function stopCrons(): Promise<void> {
|
|
4829
|
+
const open = [...running];
|
|
4830
|
+
running.length = 0;
|
|
4831
|
+
connections.length = 0;
|
|
4832
|
+
await Promise.all(open.map((r) => r.stop()));
|
|
4833
|
+
}
|
|
4834
|
+
`;
|
|
4835
|
+
await (0, node_fs_promises.writeFile)(cronsPath, content);
|
|
4836
|
+
return cronsPath;
|
|
4837
|
+
}
|
|
4838
|
+
async generateCronHandler(outputDir, sourceFile, exportName, context, owner) {
|
|
4839
|
+
const handlerFileName = `${exportName}.ts`;
|
|
4840
|
+
const handlerPath = (0, node_path.join)(outputDir, handlerFileName);
|
|
4841
|
+
const importPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile).replace(/\.ts$/, ".js");
|
|
4842
|
+
const runtime = require_EndpointGenerator.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
|
|
4843
|
+
const content = `import { AWSScheduledFunction } from '@geekmidas/constructs/aws';
|
|
4844
|
+
import { ${exportName} } from '${importPath}';
|
|
4845
|
+
${runtime.imports}
|
|
4846
|
+
${context.storageDrivers?.imports ?? ""}
|
|
4847
|
+
${runtime.bindings}
|
|
4848
|
+
${context.storageDrivers?.setup ? `\n// The handler registers the drivers its target needs.\n${context.storageDrivers.setup}\n` : ""}
|
|
4849
|
+
const adapter = new AWSScheduledFunction(envParser, ${exportName});
|
|
4850
|
+
|
|
4851
|
+
export const handler = adapter.handler;
|
|
4852
|
+
`;
|
|
4853
|
+
await (0, node_fs_promises.writeFile)(handlerPath, content);
|
|
4854
|
+
return handlerPath;
|
|
4855
|
+
}
|
|
4856
|
+
};
|
|
4857
|
+
//#endregion
|
|
4858
|
+
//#region src/generators/QueueGenerator.ts
|
|
4859
|
+
/**
|
|
4860
|
+
* Generates the runtime for queue consumers (`worker.queue(…)`).
|
|
4861
|
+
*
|
|
4862
|
+
* - **server** (`gkm dev`): a single `queues.ts` exposing `setupQueues()` that
|
|
4863
|
+
* polls each queue in-process, alongside the Hono server, through the queue's
|
|
4864
|
+
* own connection string — pg-boss by default, SQS on the AWS emulator.
|
|
4865
|
+
* - **aws-lambda**: one handler file per queue wrapping `AWSLambdaQueue`, backed
|
|
4866
|
+
* by an SQS event-source mapping.
|
|
4867
|
+
*/
|
|
4868
|
+
var QueueGenerator = class extends require_discover.ConstructGenerator {
|
|
4869
|
+
isConstruct(value) {
|
|
4870
|
+
return _geekmidas_constructs_queue.Queue.isQueue(value);
|
|
4871
|
+
}
|
|
4872
|
+
async build(context, constructs, outputDir, options) {
|
|
4873
|
+
const target = options?.target ?? "aws";
|
|
4874
|
+
const logger = console;
|
|
4875
|
+
const queueInfos = [];
|
|
4876
|
+
if (target === "server") {
|
|
4877
|
+
await this.generateServerQueuesFile(outputDir, constructs);
|
|
4878
|
+
logger.log(`Generated server queues file with ${constructs.length} queues (polling mode)`);
|
|
4879
|
+
return queueInfos;
|
|
4880
|
+
}
|
|
4881
|
+
if (constructs.length === 0) return queueInfos;
|
|
4882
|
+
const queuesDir = (0, node_path.join)(outputDir, "queues");
|
|
4883
|
+
await (0, node_fs_promises.mkdir)(queuesDir, { recursive: true });
|
|
4884
|
+
for (const { key, construct, path } of constructs) {
|
|
4885
|
+
const handlerFile = await this.generateQueueHandler(queuesDir, path.relative, key, context, construct.owner);
|
|
4886
|
+
queueInfos.push({
|
|
4887
|
+
name: construct.name,
|
|
4888
|
+
handler: (0, node_path.relative)(options?.root ?? process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
4889
|
+
batchSize: construct.batchSize,
|
|
4890
|
+
fifo: construct.fifo,
|
|
4891
|
+
timeout: construct.timeout,
|
|
4892
|
+
environment: await construct.getEnvironment({ markOptional: context.markOptional }),
|
|
4893
|
+
dependencies: construct.constructs
|
|
4894
|
+
});
|
|
4895
|
+
logger.log(`Generated queue handler: ${key}`);
|
|
4896
|
+
}
|
|
4897
|
+
return queueInfos;
|
|
4898
|
+
}
|
|
4899
|
+
async generateQueueHandler(outputDir, sourceFile, exportName, context, owner) {
|
|
4900
|
+
const handlerPath = (0, node_path.join)(outputDir, `${exportName}.ts`);
|
|
4901
|
+
const importPath = (0, node_path.relative)((0, node_path.dirname)(handlerPath), sourceFile).replace(/\.ts$/, ".js");
|
|
4902
|
+
const runtime = require_EndpointGenerator.runtimeFor(context, (0, node_path.dirname)(handlerPath), owner);
|
|
4903
|
+
const content = `import { AWSLambdaQueue } from '@geekmidas/constructs/aws';
|
|
4904
|
+
import { ${exportName} } from '${importPath}';
|
|
4905
|
+
${runtime.imports}
|
|
4906
|
+
${context.storageDrivers?.imports ?? ""}
|
|
4907
|
+
${runtime.bindings}
|
|
4908
|
+
${context.storageDrivers?.setup ? `\n// The handler registers the drivers its target needs.\n${context.storageDrivers.setup}\n` : ""}
|
|
4909
|
+
const adapter = new AWSLambdaQueue(envParser, ${exportName});
|
|
4910
|
+
|
|
4911
|
+
export const handler = adapter.handler;
|
|
4912
|
+
`;
|
|
4913
|
+
await (0, node_fs_promises.writeFile)(handlerPath, content);
|
|
4914
|
+
return handlerPath;
|
|
4915
|
+
}
|
|
4916
|
+
async generateServerQueuesFile(outputDir, queues) {
|
|
4917
|
+
await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
|
|
4918
|
+
const queuesPath = (0, node_path.join)(outputDir, "queues.ts");
|
|
4919
|
+
if (queues.length === 0) {
|
|
4920
|
+
await (0, node_fs_promises.writeFile)(queuesPath, `/**
|
|
4921
|
+
* Generated queues setup — this app declares none.
|
|
4922
|
+
*/
|
|
4923
|
+
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
4924
|
+
import type { Logger } from '@geekmidas/logger';
|
|
4925
|
+
|
|
4926
|
+
export async function setupQueues(
|
|
4927
|
+
_envParser: EnvironmentParser<any>,
|
|
4928
|
+
_logger: Logger,
|
|
4929
|
+
): Promise<void> {}
|
|
4930
|
+
|
|
4931
|
+
export function queuesStatus(): { failed: string[]; connections: unknown[] } {
|
|
4932
|
+
return { failed: [], connections: [] };
|
|
4933
|
+
}
|
|
4934
|
+
|
|
4935
|
+
export async function stopQueues(): Promise<void> {}
|
|
4936
|
+
`);
|
|
4937
|
+
return queuesPath;
|
|
4938
|
+
}
|
|
4939
|
+
const importsByFile = /* @__PURE__ */ new Map();
|
|
4940
|
+
for (const { path, key } of queues) {
|
|
4941
|
+
const importPath = (0, node_path.relative)((0, node_path.dirname)(queuesPath), path.relative).replace(/\.ts$/, ".js");
|
|
4942
|
+
if (!importsByFile.has(importPath)) importsByFile.set(importPath, []);
|
|
4943
|
+
importsByFile.get(importPath)?.push(key);
|
|
4944
|
+
}
|
|
4945
|
+
const content = `/**
|
|
4946
|
+
* Generated queue consumers setup.
|
|
4947
|
+
*
|
|
4948
|
+
* Each queue's one consumer polls its own queue, through the queue's own
|
|
4949
|
+
* connection string — the one its producers publish on. SQS cannot push, so
|
|
4950
|
+
* this is a poller on every transport: pg-boss locally by default, the SQS
|
|
4951
|
+
* queue on the AWS emulator.
|
|
4952
|
+
*
|
|
4953
|
+
* Deployed on Lambda, a queue's consumer is an SQS event source instead.
|
|
4954
|
+
*/
|
|
4955
|
+
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
4956
|
+
import type { Logger } from '@geekmidas/logger';
|
|
4957
|
+
import { EventConnectionFactory, Subscriber } from '@geekmidas/events';
|
|
4958
|
+
import type { EventConnection } from '@geekmidas/events';
|
|
4959
|
+
import { ServiceDiscovery } from '@geekmidas/services';
|
|
4960
|
+
${Array.from(importsByFile.entries()).map(([importPath, exports]) => `import { ${exports.join(", ")} } from '${importPath}';`).join("\n")}
|
|
4961
|
+
|
|
4962
|
+
const queues = [
|
|
4963
|
+
${queues.map(({ key, construct }) => ` { queue: ${key}, connectionKey: '${(0, _geekmidas_manifest.provideKey)(construct.id, "publisherConnectionString")}' },`).join("\n")}
|
|
4964
|
+
];
|
|
4965
|
+
|
|
4966
|
+
// What was opened, and what could not be: a worker's health check reads both,
|
|
4967
|
+
// and its shutdown closes what was opened.
|
|
4968
|
+
const connections = new Map<string, EventConnection>();
|
|
4969
|
+
const failed: string[] = [];
|
|
4970
|
+
|
|
4971
|
+
export async function setupQueues(
|
|
4972
|
+
envParser: EnvironmentParser<any>,
|
|
4973
|
+
logger: Logger,
|
|
4974
|
+
): Promise<void> {
|
|
4975
|
+
const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
|
|
4976
|
+
|
|
4977
|
+
for (const { queue, connectionKey } of queues) {
|
|
4978
|
+
const connectionString = process.env[connectionKey];
|
|
4979
|
+
if (!connectionString) {
|
|
4980
|
+
logger.error({ queue: queue.name, connectionKey }, 'No connection string for this queue');
|
|
4981
|
+
failed.push(queue.name);
|
|
4982
|
+
continue;
|
|
4983
|
+
}
|
|
4984
|
+
|
|
4985
|
+
try {
|
|
4986
|
+
let connection = connections.get(connectionString);
|
|
4987
|
+
if (!connection) {
|
|
4988
|
+
connection = await EventConnectionFactory.fromConnectionString(connectionString);
|
|
4989
|
+
connections.set(connectionString, connection);
|
|
4990
|
+
}
|
|
4991
|
+
const eventSubscriber = await Subscriber.fromConnection(connection);
|
|
4992
|
+
const services = queue.services.length > 0
|
|
4993
|
+
? await serviceDiscovery.register(queue.services)
|
|
4994
|
+
: {};
|
|
4995
|
+
// The queue's database — the worker's, or its own — handed over as \`db\`.
|
|
4996
|
+
const db = queue.databaseService
|
|
4997
|
+
? (await serviceDiscovery.register([queue.databaseService]))[
|
|
4998
|
+
queue.databaseService.serviceName
|
|
4999
|
+
]
|
|
5000
|
+
: undefined;
|
|
5001
|
+
|
|
5002
|
+
// A queue's messages carry one type — its own name.
|
|
5003
|
+
await eventSubscriber.subscribe([queue.name], async (message) => {
|
|
5004
|
+
try {
|
|
5005
|
+
const validation = await queue.messageSchema['~standard'].validate(message.payload);
|
|
5006
|
+
if (validation.issues) {
|
|
5007
|
+
logger.error({ issues: validation.issues, queue: queue.name }, 'Queue message failed validation');
|
|
5008
|
+
return;
|
|
5009
|
+
}
|
|
5010
|
+
|
|
5011
|
+
await queue.handler({
|
|
5012
|
+
messages: [validation.value],
|
|
5013
|
+
services: services,
|
|
5014
|
+
logger: queue.logger,
|
|
5015
|
+
db,
|
|
5016
|
+
} as any);
|
|
5017
|
+
} catch (error) {
|
|
5018
|
+
logger.error({ err: error, queue: queue.name }, 'Failed to process queue message');
|
|
5019
|
+
// Rethrown so the transport keeps the message for a retry.
|
|
5020
|
+
throw error;
|
|
5021
|
+
}
|
|
5022
|
+
});
|
|
5023
|
+
logger.info({ queue: queue.name }, 'Queue consumer started polling');
|
|
5024
|
+
} catch (error) {
|
|
5025
|
+
logger.error({ err: error, queue: queue.name }, 'Failed to set up queue consumer');
|
|
5026
|
+
failed.push(queue.name);
|
|
5027
|
+
}
|
|
5028
|
+
}
|
|
5029
|
+
}
|
|
5030
|
+
|
|
5031
|
+
/** The consumers that could not start, and the connections that did. */
|
|
5032
|
+
export function queuesStatus(): { failed: string[]; connections: EventConnection[] } {
|
|
5033
|
+
return { failed: [...failed], connections: [...connections.values()] };
|
|
5034
|
+
}
|
|
5035
|
+
|
|
5036
|
+
/**
|
|
5037
|
+
* Stop polling, let the messages in flight finish, and close each broker
|
|
5038
|
+
* connection — pg-boss's graceful stop does all three. The caller bounds it.
|
|
5039
|
+
*/
|
|
5040
|
+
export async function stopQueues(): Promise<void> {
|
|
5041
|
+
const open = [...connections.values()];
|
|
5042
|
+
connections.clear();
|
|
5043
|
+
await Promise.all(open.map((connection) => connection.close()));
|
|
5044
|
+
}
|
|
5045
|
+
`;
|
|
5046
|
+
await (0, node_fs_promises.writeFile)(queuesPath, content);
|
|
5047
|
+
return queuesPath;
|
|
5048
|
+
}
|
|
5049
|
+
};
|
|
5050
|
+
//#endregion
|
|
5051
|
+
//#region src/generators/WorkerGenerator.ts
|
|
5052
|
+
/**
|
|
5053
|
+
* A worker whose topic subscribers would have to be pushed to over HTTP.
|
|
5054
|
+
*
|
|
5055
|
+
* On SNS a subscriber is an HTTP endpoint SNS delivers to, and a worker's
|
|
5056
|
+
* process serves nothing but its health check. A server target's broker is
|
|
5057
|
+
* pg-boss, where each subscriber polls a queue of its own, so this is only
|
|
5058
|
+
* reached by building server images for a project that deploys to AWS — where
|
|
5059
|
+
* a subscriber is a Lambda instead.
|
|
5060
|
+
*/
|
|
5061
|
+
var WorkerSubscribersNeedPush = class extends Error {
|
|
5062
|
+
worker;
|
|
5063
|
+
subscribers;
|
|
5064
|
+
constructor(worker, subscribers) {
|
|
5065
|
+
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.`);
|
|
5066
|
+
this.worker = worker;
|
|
5067
|
+
this.subscribers = subscribers;
|
|
5068
|
+
this.name = "WorkerSubscribersNeedPush";
|
|
5069
|
+
}
|
|
5070
|
+
};
|
|
5071
|
+
/** Where a worker's generated entry lives, under an app's `.gkm/server`. */
|
|
5072
|
+
function workerEntryDir(serverDir, workerId) {
|
|
5073
|
+
return (0, node_path.join)(serverDir, "workers", require_derive.appKey(workerId));
|
|
5074
|
+
}
|
|
5075
|
+
/** The file a worker's bundle is written to, beside the server's. */
|
|
5076
|
+
function workerBundleName(workerId) {
|
|
5077
|
+
return `worker-${require_derive.appKey(workerId)}.mjs`;
|
|
5078
|
+
}
|
|
5079
|
+
/**
|
|
5080
|
+
* A Worker's production entry: the process that runs its crons, its queue
|
|
5081
|
+
* consumers and its topic subscribers, and answers nothing but a health check.
|
|
5082
|
+
*
|
|
5083
|
+
* It is the server's runtime without the server. The `crons.ts`, `queues.ts`
|
|
5084
|
+
* and `subscribers.ts` it starts are the files `gkm dev` starts — written by
|
|
5085
|
+
* the same generators, given only this worker's constructs — so a consumer
|
|
5086
|
+
* runs the same code in development and in the image. What it adds is the
|
|
5087
|
+
* process around them: drivers, a health check that says when a consumer or
|
|
5088
|
+
* the broker is down, and a shutdown that drains.
|
|
5089
|
+
*/
|
|
5090
|
+
var WorkerGenerator = class {
|
|
5091
|
+
generators;
|
|
5092
|
+
constructor(generators = {
|
|
5093
|
+
cron: new CronGenerator(),
|
|
5094
|
+
queue: new QueueGenerator(),
|
|
5095
|
+
subscriber: new require_SubscriberGenerator.SubscriberGenerator()
|
|
5096
|
+
}) {
|
|
5097
|
+
this.generators = generators;
|
|
5098
|
+
}
|
|
5099
|
+
/** Write the worker's files; returns its entry, `worker.ts`. */
|
|
5100
|
+
async build(input) {
|
|
5101
|
+
const { context, workerId, outputDir } = input;
|
|
5102
|
+
if (input.subscribers.length > 0 && context.eventsBackends?.includes("sns")) throw new WorkerSubscribersNeedPush(workerId, input.subscribers.map(({ key }) => key));
|
|
5103
|
+
await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
|
|
5104
|
+
const options = { target: "server" };
|
|
5105
|
+
await Promise.all([
|
|
5106
|
+
this.generators.cron.build(context, input.crons, outputDir, options),
|
|
5107
|
+
this.generators.queue.build(context, input.queues, outputDir, options),
|
|
5108
|
+
this.generators.subscriber.build(context, input.subscribers, outputDir, options)
|
|
5109
|
+
]);
|
|
5110
|
+
await (0, node_fs_promises.writeFile)((0, node_path.join)(outputDir, "app.ts"), workerApp(context, workerId, outputDir));
|
|
5111
|
+
await (0, node_fs_promises.writeFile)((0, node_path.join)(outputDir, "telemetry.ts"), require_EndpointGenerator.generateTelemetryModule(context.telemetry ? {
|
|
5112
|
+
...context.telemetry,
|
|
5113
|
+
serviceName: workerId
|
|
5114
|
+
} : void 0));
|
|
5115
|
+
const entry = (0, node_path.join)(outputDir, "worker.ts");
|
|
5116
|
+
await (0, node_fs_promises.writeFile)(entry, workerEntry(context.production?.healthCheck ?? "/health"));
|
|
5117
|
+
return entry;
|
|
5118
|
+
}
|
|
5119
|
+
};
|
|
5120
|
+
/**
|
|
5121
|
+
* `app.ts`: the worker's runtime — its logger and environment parser off the
|
|
5122
|
+
* Worker construct, the drivers its target needs, and a `startWorker` that
|
|
5123
|
+
* starts every cron, consumer and subscriber it owns.
|
|
5124
|
+
*/
|
|
5125
|
+
function workerApp(context, workerId, outputDir) {
|
|
5126
|
+
const runtime = require_EndpointGenerator.runtimeFor(context, outputDir, workerId);
|
|
5127
|
+
return `/**
|
|
5128
|
+
* Generated worker runtime for ${workerId}.
|
|
5129
|
+
* Generated by 'gkm build --production'.
|
|
5130
|
+
*/
|
|
5131
|
+
import { setupCrons, cronsStatus, stopCrons } from './crons.js';
|
|
5132
|
+
import { setupQueues, queuesStatus, stopQueues } from './queues.js';
|
|
5133
|
+
import { setupSubscribers, subscribersStatus, stopSubscribers } from './subscribers.js';
|
|
5134
|
+
${runtime.imports}
|
|
5135
|
+
${context.storageDrivers?.imports ?? ""}
|
|
5136
|
+
|
|
5137
|
+
${runtime.bindings}
|
|
5138
|
+
|
|
5139
|
+
${context.storageDrivers?.setup ? `// The entry point registers the drivers its target needs.
|
|
5140
|
+
${context.storageDrivers.setup}
|
|
5141
|
+
` : ""}
|
|
5142
|
+
// Who this worker is to Postgres: application_name on every connection.
|
|
5143
|
+
process.env.GKM_APP_NAME ??= ${JSON.stringify(workerId)};
|
|
5144
|
+
|
|
5145
|
+
export { logger };
|
|
5146
|
+
|
|
5147
|
+
/** What the health check answers, and why. */
|
|
5148
|
+
export interface WorkerHealth {
|
|
5149
|
+
healthy: boolean;
|
|
5150
|
+
/** The crons, queues and subscribers that could not start. */
|
|
5151
|
+
failed: string[];
|
|
5152
|
+
/** How many broker connections did not answer. */
|
|
5153
|
+
down: number;
|
|
5154
|
+
}
|
|
5155
|
+
|
|
5156
|
+
/** Whether one broker connection answers: open, and — on pg-boss — a query. */
|
|
5157
|
+
async function answers(connection: unknown): Promise<boolean> {
|
|
5158
|
+
const c = connection as {
|
|
5159
|
+
isConnected?: () => boolean;
|
|
5160
|
+
instance?: { getDb?: () => { executeSql: (sql: string) => Promise<unknown> } };
|
|
5161
|
+
};
|
|
5162
|
+
if (c.isConnected && !c.isConnected()) return false;
|
|
5163
|
+
const db = c.instance?.getDb?.();
|
|
5164
|
+
if (!db) return true;
|
|
5165
|
+
try {
|
|
5166
|
+
await Promise.race([
|
|
5167
|
+
db.executeSql('select 1'),
|
|
5168
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 2000).unref()),
|
|
5169
|
+
]);
|
|
5170
|
+
return true;
|
|
5171
|
+
} catch {
|
|
5172
|
+
return false;
|
|
5173
|
+
}
|
|
5174
|
+
}
|
|
5175
|
+
|
|
5176
|
+
/** Start everything this worker runs. */
|
|
5177
|
+
export async function startWorker(): Promise<{
|
|
5178
|
+
health: () => Promise<WorkerHealth>;
|
|
5179
|
+
stop: () => Promise<void>;
|
|
5180
|
+
}> {
|
|
5181
|
+
// An app object with no routes: on a server target nothing is pushed to a
|
|
5182
|
+
// subscriber, so there is nothing to mount.
|
|
5183
|
+
const noRoutes = { post: () => undefined };
|
|
5184
|
+
|
|
5185
|
+
await setupSubscribers(noRoutes, envParser, logger).catch((error) => {
|
|
5186
|
+
logger.error({ err: error }, 'Failed to start subscribers');
|
|
5187
|
+
});
|
|
5188
|
+
await setupQueues(envParser, logger).catch((error) => {
|
|
5189
|
+
logger.error({ err: error }, 'Failed to start queue consumers');
|
|
5190
|
+
});
|
|
5191
|
+
await setupCrons(envParser, logger).catch((error) => {
|
|
5192
|
+
logger.error({ err: error }, 'Failed to schedule crons');
|
|
5193
|
+
});
|
|
5194
|
+
|
|
5195
|
+
const statuses = () => [subscribersStatus(), queuesStatus(), cronsStatus()];
|
|
5196
|
+
|
|
5197
|
+
return {
|
|
5198
|
+
async health() {
|
|
5199
|
+
const all = statuses();
|
|
5200
|
+
const failed = all.flatMap((s) => s.failed);
|
|
5201
|
+
const alive = await Promise.all(
|
|
5202
|
+
all.flatMap((s) => s.connections as unknown[]).map(answers),
|
|
5203
|
+
);
|
|
5204
|
+
const down = alive.filter((ok) => !ok).length;
|
|
5205
|
+
return { healthy: failed.length === 0 && down === 0, failed, down };
|
|
5206
|
+
},
|
|
5207
|
+
// Each stops pulling, waits for what it has in flight, and closes its
|
|
5208
|
+
// broker connection. Together, so one slow handler does not hold the
|
|
5209
|
+
// others' connections open.
|
|
5210
|
+
async stop() {
|
|
5211
|
+
await Promise.all([stopSubscribers(), stopQueues(), stopCrons()]);
|
|
5212
|
+
},
|
|
5213
|
+
};
|
|
5214
|
+
}
|
|
5215
|
+
`;
|
|
5216
|
+
}
|
|
5217
|
+
/**
|
|
5218
|
+
* `worker.ts`: the process. Health first, so the container answers while the
|
|
5219
|
+
* consumers connect; then the worker; then a shutdown that drains.
|
|
5220
|
+
*/
|
|
5221
|
+
function workerEntry(healthCheck) {
|
|
5222
|
+
return `#!/usr/bin/env node
|
|
5223
|
+
/**
|
|
5224
|
+
* Production worker entry point.
|
|
5225
|
+
* Generated by 'gkm build --production'.
|
|
5226
|
+
*
|
|
5227
|
+
* Runs this worker's crons, queue consumers and topic subscribers, and serves
|
|
5228
|
+
* one route: ${healthCheck}, on PORT — 200 when every consumer started and
|
|
5229
|
+
* every broker connection answers, 503 otherwise.
|
|
5230
|
+
*
|
|
5231
|
+
* On SIGTERM it stops pulling messages and scheduling crons, lets the handlers
|
|
5232
|
+
* in flight finish, closes its broker connections and database pools, and
|
|
5233
|
+
* exits 0 — or 1, at GKM_SHUTDOWN_TIMEOUT_MS (8s by default, under Docker's
|
|
5234
|
+
* 10s stop timeout).
|
|
5235
|
+
*/
|
|
5236
|
+
import { createServer } from 'node:http';
|
|
5237
|
+
import { runShutdownHooks } from '@geekmidas/constructs';
|
|
5238
|
+
import { startTelemetry } from './telemetry.js';
|
|
5239
|
+
|
|
5240
|
+
// Before the worker is imported, so the libraries it loads are instrumented.
|
|
5241
|
+
await startTelemetry({ ignorePaths: [${JSON.stringify(healthCheck)}] });
|
|
5242
|
+
|
|
5243
|
+
const { startWorker, logger } = await import('./app.js');
|
|
5244
|
+
|
|
5245
|
+
const port = Number(process.env.PORT) || 3000;
|
|
5246
|
+
|
|
5247
|
+
let worker: Awaited<ReturnType<typeof startWorker>> | undefined;
|
|
5248
|
+
let stopping = false;
|
|
5249
|
+
|
|
5250
|
+
const server = createServer((req, res) => {
|
|
5251
|
+
const path = (req.url ?? '/').split('?')[0];
|
|
5252
|
+
if (path !== ${JSON.stringify(healthCheck)} || (req.method !== 'GET' && req.method !== 'HEAD')) {
|
|
5253
|
+
res.writeHead(404).end();
|
|
5254
|
+
return;
|
|
5255
|
+
}
|
|
5256
|
+
const answer = (status: number, body: unknown) => {
|
|
5257
|
+
res.writeHead(status, { 'content-type': 'application/json' });
|
|
5258
|
+
res.end(JSON.stringify(body));
|
|
5259
|
+
};
|
|
5260
|
+
if (stopping) return answer(503, { status: 'stopping' });
|
|
5261
|
+
if (!worker) return answer(503, { status: 'starting' });
|
|
5262
|
+
worker.health().then(
|
|
5263
|
+
(health) => answer(health.healthy ? 200 : 503, { status: health.healthy ? 'ok' : 'unhealthy', ...health }),
|
|
5264
|
+
(error) => answer(503, { status: 'unhealthy', error: String(error) }),
|
|
5265
|
+
);
|
|
5266
|
+
});
|
|
5267
|
+
server.listen(port);
|
|
5268
|
+
|
|
5269
|
+
const shutdown = async () => {
|
|
5270
|
+
if (stopping) return;
|
|
5271
|
+
stopping = true;
|
|
5272
|
+
const deadline = Number(process.env.GKM_SHUTDOWN_TIMEOUT_MS) || 8000;
|
|
5273
|
+
logger.info({ deadline }, 'Graceful shutdown initiated');
|
|
5274
|
+
setTimeout(() => {
|
|
5275
|
+
logger.warn({ deadline }, 'Shutdown deadline reached, exiting');
|
|
5276
|
+
process.exit(1);
|
|
5277
|
+
}, deadline).unref();
|
|
5278
|
+
|
|
5279
|
+
// Nothing new is pulled or scheduled; what is in flight finishes.
|
|
5280
|
+
await worker?.stop().catch((error) => logger.error({ err: error }, 'Failed to stop the worker'));
|
|
5281
|
+
// Then what the handlers used: database pools.
|
|
5282
|
+
await runShutdownHooks((error) => logger.error({ err: error }, 'Shutdown hook failed'));
|
|
5283
|
+
server.close();
|
|
5284
|
+
logger.info('Worker stopped');
|
|
5285
|
+
process.exit(0);
|
|
5286
|
+
};
|
|
5287
|
+
|
|
5288
|
+
process.on('SIGTERM', shutdown);
|
|
5289
|
+
process.on('SIGINT', shutdown);
|
|
5290
|
+
|
|
5291
|
+
worker = await startWorker();
|
|
5292
|
+
logger.info({ port }, 'Worker started');
|
|
5293
|
+
`;
|
|
5294
|
+
}
|
|
5295
|
+
//#endregion
|
|
4537
5296
|
//#region src/secrets/encryption.ts
|
|
4538
5297
|
var encryption_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
4539
5298
|
encryptSecrets: () => encryptSecrets,
|
|
@@ -4586,6 +5345,74 @@ function keyFingerprint(masterKey) {
|
|
|
4586
5345
|
return (0, node_crypto.createHash)("sha256").update(masterKey).digest("hex").slice(0, 8);
|
|
4587
5346
|
}
|
|
4588
5347
|
//#endregion
|
|
5348
|
+
//#region src/build/workers.ts
|
|
5349
|
+
/** A worker with background work and no backend app to build it from. */
|
|
5350
|
+
var WorkerHasNoApp = class extends Error {
|
|
5351
|
+
worker;
|
|
5352
|
+
constructor(worker) {
|
|
5353
|
+
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.`);
|
|
5354
|
+
this.worker = worker;
|
|
5355
|
+
this.name = "WorkerHasNoApp";
|
|
5356
|
+
}
|
|
5357
|
+
};
|
|
5358
|
+
/** A worker whose service name an app already has. */
|
|
5359
|
+
var WorkerNameTaken = class extends Error {
|
|
5360
|
+
worker;
|
|
5361
|
+
service;
|
|
5362
|
+
constructor(worker, service) {
|
|
5363
|
+
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.`);
|
|
5364
|
+
this.worker = worker;
|
|
5365
|
+
this.service = service;
|
|
5366
|
+
this.name = "WorkerNameTaken";
|
|
5367
|
+
}
|
|
5368
|
+
};
|
|
5369
|
+
/**
|
|
5370
|
+
* The workers a workspace deploys — each one that has background work — and
|
|
5371
|
+
* the app each is built from.
|
|
5372
|
+
*
|
|
5373
|
+
* The host is the backend app whose directory holds the most of the worker's
|
|
5374
|
+
* files; one whose work sits outside every app (a shared `jobs/` directory)
|
|
5375
|
+
* is built by the first backend, by name, that generates a server. An app
|
|
5376
|
+
* with its own entry is never one — gkm's generators do not build it — and a
|
|
5377
|
+
* surface that serves itself, an auth server, only when it holds the work.
|
|
5378
|
+
*
|
|
5379
|
+
* @param background Each worker's files, from discovery's `background`.
|
|
5380
|
+
*/
|
|
5381
|
+
function workerUnits(workspace, manifest, background) {
|
|
5382
|
+
const hosts = Object.entries(workspace.apps).filter(([, app]) => app.type === "backend" && !app.entry).map(([name, app]) => {
|
|
5383
|
+
const surface = Object.entries(manifest).find(([id, d]) => d.kind === "rest-api" && require_derive.appKey(id) === name)?.[1];
|
|
5384
|
+
return {
|
|
5385
|
+
name,
|
|
5386
|
+
root: (0, node_path.isAbsolute)(app.path) ? app.path : (0, node_path.join)(workspace.root, app.path),
|
|
5387
|
+
generated: !(surface?.kind === "rest-api" && surface.endpoints.length > 0)
|
|
5388
|
+
};
|
|
5389
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
5390
|
+
const units = [];
|
|
5391
|
+
for (const [id, declaration] of Object.entries(manifest)) {
|
|
5392
|
+
if (declaration.kind !== "worker") continue;
|
|
5393
|
+
const files = background[id] ?? [];
|
|
5394
|
+
if (files.length === 0) continue;
|
|
5395
|
+
const name = require_derive.appKey(id);
|
|
5396
|
+
if (workspace.apps[name]) throw new WorkerNameTaken(id, name);
|
|
5397
|
+
const ranked = hosts.map((host) => ({
|
|
5398
|
+
host,
|
|
5399
|
+
holds: files.filter((file) => isUnder(file, host.root)).length
|
|
5400
|
+
})).sort((a, b) => b.holds - a.holds || Number(b.host.generated) - Number(a.host.generated));
|
|
5401
|
+
const host = ranked[0]?.host;
|
|
5402
|
+
if (!host || ranked[0].holds === 0 && !host.generated) throw new WorkerHasNoApp(id);
|
|
5403
|
+
units.push({
|
|
5404
|
+
id,
|
|
5405
|
+
name,
|
|
5406
|
+
app: host.name
|
|
5407
|
+
});
|
|
5408
|
+
}
|
|
5409
|
+
return units.sort((a, b) => a.name.localeCompare(b.name));
|
|
5410
|
+
}
|
|
5411
|
+
function isUnder(file, dir) {
|
|
5412
|
+
const rel = (0, node_path.relative)(dir, file);
|
|
5413
|
+
return rel !== "" && !rel.startsWith("..") && !(0, node_path.isAbsolute)(rel) ? !rel.startsWith(`..${node_path.sep}`) : false;
|
|
5414
|
+
}
|
|
5415
|
+
//#endregion
|
|
4589
5416
|
//#region src/deploy/devServices.ts
|
|
4590
5417
|
/**
|
|
4591
5418
|
* Mail and object storage on a deployed stage: real ones, or — only when
|
|
@@ -5043,7 +5870,7 @@ async function dockerCommand(options) {
|
|
|
5043
5870
|
const cwd = options.cwd ?? process.cwd();
|
|
5044
5871
|
const loaded = await require_config.loadWorkspaceConfig(cwd);
|
|
5045
5872
|
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);
|
|
5873
|
+
const result = await workspaceDockerCommand(loaded.workspace, loaded.manifest, names, loaded.background);
|
|
5047
5874
|
if (options.build) for (const app of result.apps) await buildDockerImage(app, result.buildRoot, options);
|
|
5048
5875
|
if (options.push) for (const app of result.apps) await pushDockerImage(app.imageName, options, result.buildRoot);
|
|
5049
5876
|
return result;
|
|
@@ -5095,7 +5922,7 @@ async function pushDockerImage(imageName, options, cwd) {
|
|
|
5095
5922
|
* Generate Dockerfiles for all apps in a workspace.
|
|
5096
5923
|
* @internal Exported for testing
|
|
5097
5924
|
*/
|
|
5098
|
-
async function workspaceDockerCommand(workspace, manifest, imageNames = {}) {
|
|
5925
|
+
async function workspaceDockerCommand(workspace, manifest, imageNames = {}, background = {}) {
|
|
5099
5926
|
const results = [];
|
|
5100
5927
|
const layout = imageLayout(workspace);
|
|
5101
5928
|
logger$1.log(`\n🐳 Generating Dockerfiles for workspace: ${workspace.name}`);
|
|
@@ -5125,6 +5952,24 @@ async function workspaceDockerCommand(workspace, manifest, imageNames = {}) {
|
|
|
5125
5952
|
imageName: imageNames[appName] ?? appName
|
|
5126
5953
|
});
|
|
5127
5954
|
}
|
|
5955
|
+
for (const worker of manifest ? workerUnits(workspace, manifest, background) : []) {
|
|
5956
|
+
logger$1.log(`\n 📄 Generating Dockerfile for ${worker.name} (worker ${worker.id}, built from ${worker.app})`);
|
|
5957
|
+
const dockerfile = workerDockerfile(worker, workspace.apps[worker.app], {
|
|
5958
|
+
layout,
|
|
5959
|
+
workspaceRoot: workspace.root
|
|
5960
|
+
});
|
|
5961
|
+
const path = workerDockerfileOf(worker.name);
|
|
5962
|
+
const dockerfilePath = (0, node_path.join)(workspace.root, path);
|
|
5963
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(dockerfilePath), { recursive: true });
|
|
5964
|
+
await (0, node_fs_promises.writeFile)(dockerfilePath, dockerfile);
|
|
5965
|
+
logger$1.log(` Generated: ${path}`);
|
|
5966
|
+
results.push({
|
|
5967
|
+
appName: worker.name,
|
|
5968
|
+
type: "backend",
|
|
5969
|
+
dockerfile: dockerfilePath,
|
|
5970
|
+
imageName: imageNames[worker.name] ?? worker.name
|
|
5971
|
+
});
|
|
5972
|
+
}
|
|
5128
5973
|
const dockerignorePath = await ensureDockerignore(layout.buildRoot);
|
|
5129
5974
|
logger$1.log(`\n Ensured: ${dockerignorePath}`);
|
|
5130
5975
|
const composePath = await writeConstructsCompose(workspace);
|
|
@@ -5161,7 +6006,27 @@ function appDockerfile(appName, app, options) {
|
|
|
5161
6006
|
});
|
|
5162
6007
|
return generateBackendDockerfile({
|
|
5163
6008
|
...image,
|
|
5164
|
-
healthCheckPath: "/health"
|
|
6009
|
+
healthCheckPath: "/health",
|
|
6010
|
+
...options.cache ? { cache: options.cache } : {}
|
|
6011
|
+
});
|
|
6012
|
+
}
|
|
6013
|
+
/** Where `gkm docker` writes a worker's Dockerfile. */
|
|
6014
|
+
function workerDockerfileOf(worker) {
|
|
6015
|
+
return `.gkm/docker/Dockerfile.${worker}`;
|
|
6016
|
+
}
|
|
6017
|
+
/**
|
|
6018
|
+
* A Worker's Dockerfile: its host app's build, and the worker's bundle as the
|
|
6019
|
+
* runner — see `generateWorkerDockerfile`.
|
|
6020
|
+
*/
|
|
6021
|
+
function workerDockerfile(worker, host, options) {
|
|
6022
|
+
return generateWorkerDockerfile({
|
|
6023
|
+
...appImageOptions(options.layout, worker.app, host, options.workspaceRoot),
|
|
6024
|
+
imageName: worker.name,
|
|
6025
|
+
port: WORKER_PORT,
|
|
6026
|
+
worker: worker.id,
|
|
6027
|
+
bundle: workerBundleName(worker.id),
|
|
6028
|
+
healthCheckPath: "/health",
|
|
6029
|
+
...options.cache ? { cache: options.cache } : {}
|
|
5165
6030
|
});
|
|
5166
6031
|
}
|
|
5167
6032
|
/**
|
|
@@ -5401,7 +6266,7 @@ async function deployDocker(options) {
|
|
|
5401
6266
|
const cwd = options.cwd ?? process.cwd();
|
|
5402
6267
|
const docker = {
|
|
5403
6268
|
cwd,
|
|
5404
|
-
dockerfile: (0, node_path.join)(cwd, dockerfileOf(config.appName ?? imageName, options.appPath ?? ".")),
|
|
6269
|
+
dockerfile: (0, node_path.join)(cwd, options.dockerfile ?? dockerfileOf(config.appName ?? imageName, options.appPath ?? ".")),
|
|
5405
6270
|
...options.signal ? { signal: options.signal } : {},
|
|
5406
6271
|
...options.stdio ? { stdio: options.stdio } : {}
|
|
5407
6272
|
};
|
|
@@ -5733,6 +6598,12 @@ function setPostgresBackupId(state, backupId) {
|
|
|
5733
6598
|
if (state.backups) state.backups.postgresBackupId = backupId;
|
|
5734
6599
|
}
|
|
5735
6600
|
//#endregion
|
|
6601
|
+
Object.defineProperty(exports, "CronGenerator", {
|
|
6602
|
+
enumerable: true,
|
|
6603
|
+
get: function() {
|
|
6604
|
+
return CronGenerator;
|
|
6605
|
+
}
|
|
6606
|
+
});
|
|
5736
6607
|
Object.defineProperty(exports, "DEFAULT_IMAGES", {
|
|
5737
6608
|
enumerable: true,
|
|
5738
6609
|
get: function() {
|
|
@@ -5787,6 +6658,12 @@ Object.defineProperty(exports, "NoDomainForStage", {
|
|
|
5787
6658
|
return NoDomainForStage;
|
|
5788
6659
|
}
|
|
5789
6660
|
});
|
|
6661
|
+
Object.defineProperty(exports, "QueueGenerator", {
|
|
6662
|
+
enumerable: true,
|
|
6663
|
+
get: function() {
|
|
6664
|
+
return QueueGenerator;
|
|
6665
|
+
}
|
|
6666
|
+
});
|
|
5790
6667
|
Object.defineProperty(exports, "STORAGE_KEY_PAIR", {
|
|
5791
6668
|
enumerable: true,
|
|
5792
6669
|
get: function() {
|
|
@@ -5805,6 +6682,18 @@ Object.defineProperty(exports, "UnknownDevService", {
|
|
|
5805
6682
|
return UnknownDevService;
|
|
5806
6683
|
}
|
|
5807
6684
|
});
|
|
6685
|
+
Object.defineProperty(exports, "WORKER_PORT", {
|
|
6686
|
+
enumerable: true,
|
|
6687
|
+
get: function() {
|
|
6688
|
+
return WORKER_PORT;
|
|
6689
|
+
}
|
|
6690
|
+
});
|
|
6691
|
+
Object.defineProperty(exports, "WorkerGenerator", {
|
|
6692
|
+
enumerable: true,
|
|
6693
|
+
get: function() {
|
|
6694
|
+
return WorkerGenerator;
|
|
6695
|
+
}
|
|
6696
|
+
});
|
|
5808
6697
|
Object.defineProperty(exports, "appDockerfile", {
|
|
5809
6698
|
enumerable: true,
|
|
5810
6699
|
get: function() {
|
|
@@ -6381,6 +7270,42 @@ Object.defineProperty(exports, "withGeneratedSecrets", {
|
|
|
6381
7270
|
return withGeneratedSecrets;
|
|
6382
7271
|
}
|
|
6383
7272
|
});
|
|
7273
|
+
Object.defineProperty(exports, "workerBundleName", {
|
|
7274
|
+
enumerable: true,
|
|
7275
|
+
get: function() {
|
|
7276
|
+
return workerBundleName;
|
|
7277
|
+
}
|
|
7278
|
+
});
|
|
7279
|
+
Object.defineProperty(exports, "workerDockerfile", {
|
|
7280
|
+
enumerable: true,
|
|
7281
|
+
get: function() {
|
|
7282
|
+
return workerDockerfile;
|
|
7283
|
+
}
|
|
7284
|
+
});
|
|
7285
|
+
Object.defineProperty(exports, "workerDockerfileOf", {
|
|
7286
|
+
enumerable: true,
|
|
7287
|
+
get: function() {
|
|
7288
|
+
return workerDockerfileOf;
|
|
7289
|
+
}
|
|
7290
|
+
});
|
|
7291
|
+
Object.defineProperty(exports, "workerEntryDir", {
|
|
7292
|
+
enumerable: true,
|
|
7293
|
+
get: function() {
|
|
7294
|
+
return workerEntryDir;
|
|
7295
|
+
}
|
|
7296
|
+
});
|
|
7297
|
+
Object.defineProperty(exports, "workerEnvKeys", {
|
|
7298
|
+
enumerable: true,
|
|
7299
|
+
get: function() {
|
|
7300
|
+
return workerEnvKeys;
|
|
7301
|
+
}
|
|
7302
|
+
});
|
|
7303
|
+
Object.defineProperty(exports, "workerUnits", {
|
|
7304
|
+
enumerable: true,
|
|
7305
|
+
get: function() {
|
|
7306
|
+
return workerUnits;
|
|
7307
|
+
}
|
|
7308
|
+
});
|
|
6384
7309
|
Object.defineProperty(exports, "workspace_exports", {
|
|
6385
7310
|
enumerable: true,
|
|
6386
7311
|
get: function() {
|
|
@@ -6388,4 +7313,4 @@ Object.defineProperty(exports, "workspace_exports", {
|
|
|
6388
7313
|
}
|
|
6389
7314
|
});
|
|
6390
7315
|
|
|
6391
|
-
//# sourceMappingURL=state-
|
|
7316
|
+
//# sourceMappingURL=state-DCwRWyh-.cjs.map
|