@geekmidas/cli 10.0.0-alpha.71 → 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-DZZwHdDC.cjs → EndpointGenerator-CP6sBvQV.cjs} +7 -513
- package/dist/EndpointGenerator-CP6sBvQV.cjs.map +1 -0
- package/dist/{OpenApiTsGenerator-Bhr_XWWN.mjs → EndpointGenerator-DftalLYg.mjs} +6 -512
- 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 +124 -448
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +118 -442
- 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-Dp6arkI4.cjs → ready-BIuMw6wH.cjs} +66 -289
- package/dist/ready-BIuMw6wH.cjs.map +1 -0
- package/dist/{ready-xpoX11uu.mjs → ready-C0U0NYfB.mjs} +58 -281
- 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-Bhr_XWWN.mjs.map +0 -1
- package/dist/OpenApiTsGenerator-DZZwHdDC.cjs.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-Dp6arkI4.cjs.map +0 -1
- package/dist/ready-xpoX11uu.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
|
import { t as __exportAll } from "./rolldown-runtime-BhDjJH2R.mjs";
|
|
2
2
|
import { a as runOutput, i as run$1, s as gkmHome } from "./run-BJLIerMM.mjs";
|
|
3
|
-
import { d as output, r as discover } from "./discover-
|
|
3
|
+
import { d as output, r as discover, u as ConstructGenerator } from "./discover-BPelzeGW.mjs";
|
|
4
4
|
import { t as appKey } from "./derive-B4vDpzgP.mjs";
|
|
5
5
|
import { O as TEST_STAGE, h as getPublicEnvPrefix, n as allConstructGlobs } from "./workspace-C7NqCsQF.mjs";
|
|
6
6
|
import { ConfigNotFound, NotInAnApp, loadWorkspaceAppInfo, loadWorkspaceConfig } from "./config.mjs";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { n as runtimeFor, r as generateTelemetryModule } from "./EndpointGenerator-DftalLYg.mjs";
|
|
8
|
+
import { c as runtimeOf } from "./resolve-CV4eQkDa.mjs";
|
|
9
|
+
import { a as DEFAULT_STORAGE, i as DEFAULT_EVENTS$1, n as readFakes, r as DEFAULT_CACHE, t as SubscriberGenerator } from "./SubscriberGenerator-DZk_pKDh.mjs";
|
|
9
10
|
import { basename, dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
|
|
10
11
|
import { constants, existsSync, readFileSync } from "node:fs";
|
|
11
12
|
import { appendFile, chmod, copyFile, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
12
13
|
import { homedir, networkInterfaces, tmpdir } from "node:os";
|
|
13
14
|
import { promisify } from "node:util";
|
|
15
|
+
import { Cron } from "@geekmidas/constructs/crons";
|
|
16
|
+
import { Queue } from "@geekmidas/constructs/queue";
|
|
14
17
|
import { DEFAULT_POSTGRES_VERSION, cacheTable, cookieDomain, dependenciesOf, dependentsOf, externalApiUrl, kebabCase, mobileOrigins, provideKey, providedKeyFor, provisionOrder, publicEnvFor, schemeBase, scopedName } from "@geekmidas/manifest";
|
|
15
18
|
import { createServer } from "node:net";
|
|
16
19
|
import { execFile, spawn } from "node:child_process";
|
|
@@ -1536,11 +1539,37 @@ CMD ["node", "${appPath}/server.js"]
|
|
|
1536
1539
|
* @internal Exported for testing
|
|
1537
1540
|
*/
|
|
1538
1541
|
function generateBackendDockerfile(options) {
|
|
1542
|
+
return gkmBundleDockerfile(options, {
|
|
1543
|
+
title: "Backend Dockerfile",
|
|
1544
|
+
bundle: "server.mjs",
|
|
1545
|
+
as: "server.mjs",
|
|
1546
|
+
expose: true
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
/** The port a worker answers its health check on, inside its container. */
|
|
1550
|
+
const WORKER_PORT = 3e3;
|
|
1551
|
+
/**
|
|
1552
|
+
* A Worker's image: the same build as its host app's backend — a pruned
|
|
1553
|
+
* slice, `gkm build --provider server --production` in the image, with the
|
|
1554
|
+
* encrypted credentials from the `gkm_credentials` secret — and a runner that
|
|
1555
|
+
* is the worker's bundle alone. It publishes nothing: its one route is the
|
|
1556
|
+
* health check the HEALTHCHECK asks.
|
|
1557
|
+
* @internal Exported for testing
|
|
1558
|
+
*/
|
|
1559
|
+
function generateWorkerDockerfile(options) {
|
|
1560
|
+
return gkmBundleDockerfile(options, {
|
|
1561
|
+
title: `Worker Dockerfile (${options.worker})`,
|
|
1562
|
+
bundle: options.bundle,
|
|
1563
|
+
as: "worker.mjs",
|
|
1564
|
+
expose: false
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
function gkmBundleDockerfile(options, runner) {
|
|
1539
1568
|
const { port, healthCheckPath = "/health" } = options;
|
|
1540
1569
|
const { app } = layout(options);
|
|
1541
1570
|
const externals = externalsStages(options, app);
|
|
1542
1571
|
return `# syntax=docker/dockerfile:1
|
|
1543
|
-
#
|
|
1572
|
+
# ${runner.title}: a turbo-pruned slice, bundled by gkm build in the image
|
|
1544
1573
|
|
|
1545
1574
|
${prunerStage(options)}
|
|
1546
1575
|
|
|
@@ -1561,8 +1590,8 @@ RUN apk add --no-cache tini
|
|
|
1561
1590
|
RUN addgroup --system --gid 1001 nodejs && \\
|
|
1562
1591
|
adduser --system --uid 1001 hono
|
|
1563
1592
|
|
|
1564
|
-
# The bundled server
|
|
1565
|
-
COPY --from=builder --chown=hono:nodejs ${app}/.gkm/server/dist
|
|
1593
|
+
# The bundled ${runner.as === "server.mjs" ? "server" : "worker"}
|
|
1594
|
+
COPY --from=builder --chown=hono:nodejs ${app}/.gkm/server/dist/${runner.bundle} ./${runner.as === runner.bundle ? "" : runner.as}${externals.copy}
|
|
1566
1595
|
|
|
1567
1596
|
ENV NODE_ENV=production
|
|
1568
1597
|
ENV PORT=${port}
|
|
@@ -1571,11 +1600,9 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \\
|
|
|
1571
1600
|
CMD wget -qO- http://localhost:${port}${healthCheckPath} > /dev/null 2>&1 || exit 1
|
|
1572
1601
|
|
|
1573
1602
|
USER hono
|
|
1574
|
-
|
|
1575
|
-
EXPOSE ${port}
|
|
1576
|
-
|
|
1603
|
+
${runner.expose ? `\nEXPOSE ${port}\n` : ""}
|
|
1577
1604
|
ENTRYPOINT ["/sbin/tini", "--"]
|
|
1578
|
-
CMD ["node", "
|
|
1605
|
+
CMD ["node", "${runner.as}"]
|
|
1579
1606
|
`;
|
|
1580
1607
|
}
|
|
1581
1608
|
/**
|
|
@@ -2984,6 +3011,36 @@ function appEnvKeys(manifest, appName, runnables = {}) {
|
|
|
2984
3011
|
for (const key of other.provides ?? []) keys.add(key);
|
|
2985
3012
|
}
|
|
2986
3013
|
}
|
|
3014
|
+
addEdgeKeys(manifest, edges, keys);
|
|
3015
|
+
if (declaration.kind === "site" || declaration.kind === "mobile-app") for (const key of Object.keys(publicEnvFor(declaration, manifest))) keys.add(key);
|
|
3016
|
+
return keys;
|
|
3017
|
+
}
|
|
3018
|
+
/**
|
|
3019
|
+
* The keys a Worker's own process reads: what the worker and everything built
|
|
3020
|
+
* from it declared an edge to, the broker its crons schedule through, and the
|
|
3021
|
+
* address of every queue and topic — which of them its consumers and
|
|
3022
|
+
* subscribers are bound to is only known once a build has found them, so a
|
|
3023
|
+
* process that runs them is handed each carrier's.
|
|
3024
|
+
*
|
|
3025
|
+
* `undefined` when no worker has that id.
|
|
3026
|
+
*/
|
|
3027
|
+
function workerEnvKeys(manifest, workerId, runnables = {}) {
|
|
3028
|
+
const declaration = manifest[workerId];
|
|
3029
|
+
if (declaration?.kind !== "worker") return void 0;
|
|
3030
|
+
const keys = /* @__PURE__ */ new Set([
|
|
3031
|
+
...declaration.provides ?? [],
|
|
3032
|
+
...declaration.requires ?? [],
|
|
3033
|
+
"EVENT_PUBLISHER_CONNECTION_STRING"
|
|
3034
|
+
]);
|
|
3035
|
+
for (const other of Object.values(manifest)) {
|
|
3036
|
+
if (other.kind !== "queue" && other.kind !== "topic") continue;
|
|
3037
|
+
for (const key of other.provides ?? []) keys.add(key);
|
|
3038
|
+
}
|
|
3039
|
+
addEdgeKeys(manifest, [...dependenciesOf(declaration).map((edge) => edge.target), ...runnables[workerId] ?? []], keys);
|
|
3040
|
+
return keys;
|
|
3041
|
+
}
|
|
3042
|
+
/** What each construct an edge reaches hands the process that reaches it. */
|
|
3043
|
+
function addEdgeKeys(manifest, edges, keys) {
|
|
2987
3044
|
for (const id of edges) {
|
|
2988
3045
|
const target = manifest[id];
|
|
2989
3046
|
if (!target) continue;
|
|
@@ -3000,8 +3057,6 @@ function appEnvKeys(manifest, appName, runnables = {}) {
|
|
|
3000
3057
|
keys.add("AWS_REGION");
|
|
3001
3058
|
}
|
|
3002
3059
|
}
|
|
3003
|
-
if (declaration.kind === "site" || declaration.kind === "mobile-app") for (const key of Object.keys(publicEnvFor(declaration, manifest))) keys.add(key);
|
|
3004
|
-
return keys;
|
|
3005
3060
|
}
|
|
3006
3061
|
/** One compose service per app the workspace runs, behind the apps profile. */
|
|
3007
3062
|
function appServices(workspace, manifest, containers, runnables = {}, fakes = {}) {
|
|
@@ -4532,6 +4587,707 @@ function localAddress(port) {
|
|
|
4532
4587
|
return `http://localhost:${port}`;
|
|
4533
4588
|
}
|
|
4534
4589
|
//#endregion
|
|
4590
|
+
//#region src/generators/CronGenerator.ts
|
|
4591
|
+
var CronGenerator = class extends ConstructGenerator {
|
|
4592
|
+
async build(context, constructs, outputDir, options) {
|
|
4593
|
+
const target = options?.target ?? "aws";
|
|
4594
|
+
const logger = console;
|
|
4595
|
+
const cronInfos = [];
|
|
4596
|
+
if (target === "server") {
|
|
4597
|
+
await this.generateServerCronsFile(outputDir, constructs);
|
|
4598
|
+
logger.log(`Generated server crons file with ${constructs.length} crons`);
|
|
4599
|
+
return cronInfos;
|
|
4600
|
+
}
|
|
4601
|
+
if (constructs.length === 0) return cronInfos;
|
|
4602
|
+
const cronsDir = join(outputDir, "crons");
|
|
4603
|
+
await mkdir(cronsDir, { recursive: true });
|
|
4604
|
+
for (const { key, construct, path } of constructs) {
|
|
4605
|
+
const handlerFile = await this.generateCronHandler(cronsDir, path.relative, key, context, construct.owner);
|
|
4606
|
+
cronInfos.push({
|
|
4607
|
+
name: key,
|
|
4608
|
+
handler: relative(options?.root ?? process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
4609
|
+
schedule: construct.schedule || "rate(1 hour)",
|
|
4610
|
+
timeout: construct.timeout,
|
|
4611
|
+
memorySize: construct.memorySize,
|
|
4612
|
+
environment: await construct.getEnvironment({ markOptional: context.markOptional }),
|
|
4613
|
+
dependencies: construct.constructs
|
|
4614
|
+
});
|
|
4615
|
+
logger.log(`Generated cron handler: ${key}`);
|
|
4616
|
+
}
|
|
4617
|
+
return cronInfos;
|
|
4618
|
+
}
|
|
4619
|
+
isConstruct(value) {
|
|
4620
|
+
return Cron.isCron(value);
|
|
4621
|
+
}
|
|
4622
|
+
/**
|
|
4623
|
+
* The crons file a server entry imports.
|
|
4624
|
+
*
|
|
4625
|
+
* Shaped after `generateServerSubscribersFile`: one module, every cron, a
|
|
4626
|
+
* `setupCrons` the generated entry calls beside `setupSubscribers`.
|
|
4627
|
+
*
|
|
4628
|
+
* It picks its scheduler from what the process can reach. With a Postgres it
|
|
4629
|
+
* uses pg-boss, whose schedules live in the database — so a deployment
|
|
4630
|
+
* running four replicas fires each job once, which is the thing a timer in
|
|
4631
|
+
* every process gets wrong and does not report. With no database it falls
|
|
4632
|
+
* back to timers and says plainly that it is doing so.
|
|
4633
|
+
*/
|
|
4634
|
+
async generateServerCronsFile(outputDir, crons) {
|
|
4635
|
+
await mkdir(outputDir, { recursive: true });
|
|
4636
|
+
const cronsPath = join(outputDir, "crons.ts");
|
|
4637
|
+
if (crons.length === 0) {
|
|
4638
|
+
await writeFile(cronsPath, `/**
|
|
4639
|
+
* Generated cron setup — this app declares none.
|
|
4640
|
+
*/
|
|
4641
|
+
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
4642
|
+
import type { Logger } from '@geekmidas/logger';
|
|
4643
|
+
|
|
4644
|
+
export async function setupCrons(
|
|
4645
|
+
_envParser: EnvironmentParser<any>,
|
|
4646
|
+
_logger: Logger,
|
|
4647
|
+
): Promise<void> {}
|
|
4648
|
+
|
|
4649
|
+
export function cronsStatus(): { failed: string[]; connections: unknown[] } {
|
|
4650
|
+
return { failed: [], connections: [] };
|
|
4651
|
+
}
|
|
4652
|
+
|
|
4653
|
+
export async function stopCrons(): Promise<void> {}
|
|
4654
|
+
`);
|
|
4655
|
+
return cronsPath;
|
|
4656
|
+
}
|
|
4657
|
+
const importsByFile = /* @__PURE__ */ new Map();
|
|
4658
|
+
for (const { path, key } of crons) {
|
|
4659
|
+
const importPath = relative(dirname(cronsPath), path.relative).replace(/\.ts$/, ".js");
|
|
4660
|
+
if (!importsByFile.has(importPath)) importsByFile.set(importPath, []);
|
|
4661
|
+
importsByFile.get(importPath)?.push(key);
|
|
4662
|
+
}
|
|
4663
|
+
const content = `/**
|
|
4664
|
+
* Generated cron setup.
|
|
4665
|
+
*
|
|
4666
|
+
* Schedules every cron this app declares, in the process that serves it.
|
|
4667
|
+
*
|
|
4668
|
+
* The schedule lives in Postgres, through pg-boss, in the database the worker
|
|
4669
|
+
* named with \`.database(db)\`. That is what makes running several replicas
|
|
4670
|
+
* safe: each job fires once, where a timer in every process fires it once per
|
|
4671
|
+
* replica and reports nothing.
|
|
4672
|
+
*
|
|
4673
|
+
* No connection string appears here. The construct that owns the database is
|
|
4674
|
+
* the only thing that knows its key, and it is resolved through service
|
|
4675
|
+
* discovery like any other dependency.
|
|
4676
|
+
*/
|
|
4677
|
+
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
4678
|
+
import type { Logger } from '@geekmidas/logger';
|
|
4679
|
+
import { runCron, scheduleInProcess, toCronExpression } from '@geekmidas/constructs/crons';
|
|
4680
|
+
import { ServiceDiscovery } from '@geekmidas/services';
|
|
4681
|
+
${Array.from(importsByFile.entries()).map(([importPath, exports]) => `import { ${exports.join(", ")} } from '${importPath}';`).join("\n")}
|
|
4682
|
+
|
|
4683
|
+
const crons = [
|
|
4684
|
+
${crons.map(({ key }) => ` { name: '${key}', cron: ${key} }`).join(",\n")}
|
|
4685
|
+
];
|
|
4686
|
+
|
|
4687
|
+
type Stoppable = { stop: () => Promise<void> | void };
|
|
4688
|
+
const running: Stoppable[] = [];
|
|
4689
|
+
// What a worker's health check reads: the crons that cannot be scheduled, and
|
|
4690
|
+
// the broker connection the schedule is kept through.
|
|
4691
|
+
const failed: string[] = [];
|
|
4692
|
+
const connections: import('@geekmidas/events').EventConnection[] = [];
|
|
4693
|
+
|
|
4694
|
+
export async function setupCrons(
|
|
4695
|
+
envParser: EnvironmentParser<any>,
|
|
4696
|
+
logger: Logger,
|
|
4697
|
+
): Promise<void> {
|
|
4698
|
+
if (crons.length === 0) {
|
|
4699
|
+
return;
|
|
4700
|
+
}
|
|
4701
|
+
|
|
4702
|
+
const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
|
|
4703
|
+
|
|
4704
|
+
// Resolved once per cron rather than per firing: registering services on
|
|
4705
|
+
// every tick would reconnect a database every minute.
|
|
4706
|
+
const prepared = [];
|
|
4707
|
+
for (const { name, cron } of crons) {
|
|
4708
|
+
try {
|
|
4709
|
+
const schedule = toCronExpression(cron.schedule);
|
|
4710
|
+
const services = cron.services.length > 0
|
|
4711
|
+
? await serviceDiscovery.register(cron.services)
|
|
4712
|
+
: {};
|
|
4713
|
+
|
|
4714
|
+
prepared.push({ name, cron, schedule, services });
|
|
4715
|
+
} catch (error) {
|
|
4716
|
+
// An unrepresentable schedule is a build-time mistake, and failing the
|
|
4717
|
+
// process for it would take the HTTP server down with it.
|
|
4718
|
+
logger.error({ err: error, cron: name }, 'Cron has no runnable schedule, skipping');
|
|
4719
|
+
failed.push(name);
|
|
4720
|
+
}
|
|
4721
|
+
}
|
|
4722
|
+
|
|
4723
|
+
const run = async (entry: (typeof prepared)[number]) => {
|
|
4724
|
+
try {
|
|
4725
|
+
// The cron's own function, the way its Lambda would run it — services,
|
|
4726
|
+
// the worker's database as \`db\`, its events. It used to call a
|
|
4727
|
+
// \`handler\` a Cron does not have, so every firing logged a failure.
|
|
4728
|
+
await runCron(entry.cron, serviceDiscovery);
|
|
4729
|
+
} catch (error) {
|
|
4730
|
+
logger.error({ err: error, cron: entry.name }, 'Cron failed');
|
|
4731
|
+
}
|
|
4732
|
+
};
|
|
4733
|
+
|
|
4734
|
+
// The app's one pg-boss: the events broker's, as the broker's role, in the
|
|
4735
|
+
// broker's schema. A server fires its own crons, and a process scheduling in
|
|
4736
|
+
// memory fires each job once per replica — so the schedule lives in
|
|
4737
|
+
// Postgres. It used to be a second pg-boss on the worker's database, as the
|
|
4738
|
+
// runtime role, which can neither create a schema nor use the broker's.
|
|
4739
|
+
const { url, devPid } = envParser
|
|
4740
|
+
.create((get) => ({
|
|
4741
|
+
url: get('EVENT_PUBLISHER_CONNECTION_STRING').string().optional(),
|
|
4742
|
+
// Set by \`gkm dev\` on the server it starts, and by nothing deployed.
|
|
4743
|
+
devPid: get('GKM_DEV_PID').string().optional(),
|
|
4744
|
+
}))
|
|
4745
|
+
.parse();
|
|
4746
|
+
|
|
4747
|
+
if (!url?.startsWith('pgboss://')) {
|
|
4748
|
+
// \`gkm dev\` on a target whose crons are infrastructure once deployed —
|
|
4749
|
+
// EventBridge rules on AWS — has no broker to keep a schedule in. It is one
|
|
4750
|
+
// process, so a timer fires each job once, which is all pg-boss is for
|
|
4751
|
+
// here. Only there: a deployed server firing by timer fires once per
|
|
4752
|
+
// replica, so outside dev this stays the error below.
|
|
4753
|
+
if (devPid) {
|
|
4754
|
+
for (const entry of prepared) {
|
|
4755
|
+
running.push(scheduleInProcess(entry.schedule, () => run(entry)));
|
|
4756
|
+
logger.info({ cron: entry.name, schedule: entry.schedule }, 'Cron scheduled in this process');
|
|
4757
|
+
}
|
|
4758
|
+
return;
|
|
4759
|
+
}
|
|
4760
|
+
|
|
4761
|
+
logger.error(
|
|
4762
|
+
{ crons: prepared.map(({ name }) => name) },
|
|
4763
|
+
'These crons have nowhere to keep their schedule. On a server they are ' +
|
|
4764
|
+
'scheduled through the pg-boss events broker ' +
|
|
4765
|
+
'(EVENT_PUBLISHER_CONNECTION_STRING), and ' +
|
|
4766
|
+
(url ? 'the broker configured is not pg-boss.' : 'none is configured.'),
|
|
4767
|
+
);
|
|
4768
|
+
failed.push(...prepared.map(({ name }) => name));
|
|
4769
|
+
return;
|
|
4770
|
+
}
|
|
4771
|
+
|
|
4772
|
+
// Through the registered pg-boss driver, which the entry registers for a
|
|
4773
|
+
// server whose broker is pg-boss. Naming \`@geekmidas/events/pgboss\` here
|
|
4774
|
+
// made every bundle with a cron resolve pg-boss, whatever its broker.
|
|
4775
|
+
const { EventConnectionFactory } = await import('@geekmidas/events');
|
|
4776
|
+
const connection = (await EventConnectionFactory.fromConnectionString(
|
|
4777
|
+
url,
|
|
4778
|
+
)) as import('@geekmidas/events').EventConnection & { instance?: any };
|
|
4779
|
+
const boss = connection.instance!;
|
|
4780
|
+
|
|
4781
|
+
// What this app declares now. Anything else under the prefix belonged to a
|
|
4782
|
+
// cron since renamed or deleted, and would keep firing at a handler that has
|
|
4783
|
+
// gone.
|
|
4784
|
+
//
|
|
4785
|
+
// The prefix is \`cron.\` rather than \`cron:\`: pg-boss accepts
|
|
4786
|
+
// alphanumerics, underscores, hyphens, periods and slashes in a queue name,
|
|
4787
|
+
// and rejects a colon outright.
|
|
4788
|
+
const declared = new Set(prepared.map(({ name }) => \`cron.\${name}\`));
|
|
4789
|
+
for (const existing of await boss.getSchedules()) {
|
|
4790
|
+
if (existing.name.startsWith('cron.') && !declared.has(existing.name)) {
|
|
4791
|
+
await boss.unschedule(existing.name);
|
|
4792
|
+
logger.info({ cron: existing.name }, 'Removed a schedule nothing declares');
|
|
4793
|
+
}
|
|
4794
|
+
}
|
|
4795
|
+
|
|
4796
|
+
for (const entry of prepared) {
|
|
4797
|
+
const queue = \`cron.\${entry.name}\`;
|
|
4798
|
+
await boss.createQueue(queue);
|
|
4799
|
+
// UTC, so this agrees with the schedule a deploy target would have made.
|
|
4800
|
+
await boss.schedule(queue, entry.schedule, undefined, { tz: 'UTC' });
|
|
4801
|
+
await boss.work(queue, () => run(entry));
|
|
4802
|
+
|
|
4803
|
+
logger.info({ cron: entry.name, schedule: entry.schedule }, 'Cron scheduled');
|
|
4804
|
+
}
|
|
4805
|
+
|
|
4806
|
+
running.push({ stop: () => connection.close() });
|
|
4807
|
+
connections.push(connection);
|
|
4808
|
+
logger.info({ count: prepared.length }, 'Crons scheduled');
|
|
4809
|
+
}
|
|
4810
|
+
|
|
4811
|
+
/** The crons that cannot be scheduled, and the connection that schedules the rest. */
|
|
4812
|
+
export function cronsStatus(): {
|
|
4813
|
+
failed: string[];
|
|
4814
|
+
connections: import('@geekmidas/events').EventConnection[];
|
|
4815
|
+
} {
|
|
4816
|
+
return { failed: [...failed], connections: [...connections] };
|
|
4817
|
+
}
|
|
4818
|
+
|
|
4819
|
+
/**
|
|
4820
|
+
* Stop scheduling, let a firing in flight finish, and close the broker
|
|
4821
|
+
* connection — pg-boss's graceful stop does all three. The caller bounds it.
|
|
4822
|
+
*/
|
|
4823
|
+
export async function stopCrons(): Promise<void> {
|
|
4824
|
+
const open = [...running];
|
|
4825
|
+
running.length = 0;
|
|
4826
|
+
connections.length = 0;
|
|
4827
|
+
await Promise.all(open.map((r) => r.stop()));
|
|
4828
|
+
}
|
|
4829
|
+
`;
|
|
4830
|
+
await writeFile(cronsPath, content);
|
|
4831
|
+
return cronsPath;
|
|
4832
|
+
}
|
|
4833
|
+
async generateCronHandler(outputDir, sourceFile, exportName, context, owner) {
|
|
4834
|
+
const handlerFileName = `${exportName}.ts`;
|
|
4835
|
+
const handlerPath = join(outputDir, handlerFileName);
|
|
4836
|
+
const importPath = relative(dirname(handlerPath), sourceFile).replace(/\.ts$/, ".js");
|
|
4837
|
+
const runtime = runtimeFor(context, dirname(handlerPath), owner);
|
|
4838
|
+
const content = `import { AWSScheduledFunction } from '@geekmidas/constructs/aws';
|
|
4839
|
+
import { ${exportName} } from '${importPath}';
|
|
4840
|
+
${runtime.imports}
|
|
4841
|
+
${context.storageDrivers?.imports ?? ""}
|
|
4842
|
+
${runtime.bindings}
|
|
4843
|
+
${context.storageDrivers?.setup ? `\n// The handler registers the drivers its target needs.\n${context.storageDrivers.setup}\n` : ""}
|
|
4844
|
+
const adapter = new AWSScheduledFunction(envParser, ${exportName});
|
|
4845
|
+
|
|
4846
|
+
export const handler = adapter.handler;
|
|
4847
|
+
`;
|
|
4848
|
+
await writeFile(handlerPath, content);
|
|
4849
|
+
return handlerPath;
|
|
4850
|
+
}
|
|
4851
|
+
};
|
|
4852
|
+
//#endregion
|
|
4853
|
+
//#region src/generators/QueueGenerator.ts
|
|
4854
|
+
/**
|
|
4855
|
+
* Generates the runtime for queue consumers (`worker.queue(…)`).
|
|
4856
|
+
*
|
|
4857
|
+
* - **server** (`gkm dev`): a single `queues.ts` exposing `setupQueues()` that
|
|
4858
|
+
* polls each queue in-process, alongside the Hono server, through the queue's
|
|
4859
|
+
* own connection string — pg-boss by default, SQS on the AWS emulator.
|
|
4860
|
+
* - **aws-lambda**: one handler file per queue wrapping `AWSLambdaQueue`, backed
|
|
4861
|
+
* by an SQS event-source mapping.
|
|
4862
|
+
*/
|
|
4863
|
+
var QueueGenerator = class extends ConstructGenerator {
|
|
4864
|
+
isConstruct(value) {
|
|
4865
|
+
return Queue.isQueue(value);
|
|
4866
|
+
}
|
|
4867
|
+
async build(context, constructs, outputDir, options) {
|
|
4868
|
+
const target = options?.target ?? "aws";
|
|
4869
|
+
const logger = console;
|
|
4870
|
+
const queueInfos = [];
|
|
4871
|
+
if (target === "server") {
|
|
4872
|
+
await this.generateServerQueuesFile(outputDir, constructs);
|
|
4873
|
+
logger.log(`Generated server queues file with ${constructs.length} queues (polling mode)`);
|
|
4874
|
+
return queueInfos;
|
|
4875
|
+
}
|
|
4876
|
+
if (constructs.length === 0) return queueInfos;
|
|
4877
|
+
const queuesDir = join(outputDir, "queues");
|
|
4878
|
+
await mkdir(queuesDir, { recursive: true });
|
|
4879
|
+
for (const { key, construct, path } of constructs) {
|
|
4880
|
+
const handlerFile = await this.generateQueueHandler(queuesDir, path.relative, key, context, construct.owner);
|
|
4881
|
+
queueInfos.push({
|
|
4882
|
+
name: construct.name,
|
|
4883
|
+
handler: relative(options?.root ?? process.cwd(), handlerFile).replace(/\.ts$/, ".handler"),
|
|
4884
|
+
batchSize: construct.batchSize,
|
|
4885
|
+
fifo: construct.fifo,
|
|
4886
|
+
timeout: construct.timeout,
|
|
4887
|
+
environment: await construct.getEnvironment({ markOptional: context.markOptional }),
|
|
4888
|
+
dependencies: construct.constructs
|
|
4889
|
+
});
|
|
4890
|
+
logger.log(`Generated queue handler: ${key}`);
|
|
4891
|
+
}
|
|
4892
|
+
return queueInfos;
|
|
4893
|
+
}
|
|
4894
|
+
async generateQueueHandler(outputDir, sourceFile, exportName, context, owner) {
|
|
4895
|
+
const handlerPath = join(outputDir, `${exportName}.ts`);
|
|
4896
|
+
const importPath = relative(dirname(handlerPath), sourceFile).replace(/\.ts$/, ".js");
|
|
4897
|
+
const runtime = runtimeFor(context, dirname(handlerPath), owner);
|
|
4898
|
+
const content = `import { AWSLambdaQueue } from '@geekmidas/constructs/aws';
|
|
4899
|
+
import { ${exportName} } from '${importPath}';
|
|
4900
|
+
${runtime.imports}
|
|
4901
|
+
${context.storageDrivers?.imports ?? ""}
|
|
4902
|
+
${runtime.bindings}
|
|
4903
|
+
${context.storageDrivers?.setup ? `\n// The handler registers the drivers its target needs.\n${context.storageDrivers.setup}\n` : ""}
|
|
4904
|
+
const adapter = new AWSLambdaQueue(envParser, ${exportName});
|
|
4905
|
+
|
|
4906
|
+
export const handler = adapter.handler;
|
|
4907
|
+
`;
|
|
4908
|
+
await writeFile(handlerPath, content);
|
|
4909
|
+
return handlerPath;
|
|
4910
|
+
}
|
|
4911
|
+
async generateServerQueuesFile(outputDir, queues) {
|
|
4912
|
+
await mkdir(outputDir, { recursive: true });
|
|
4913
|
+
const queuesPath = join(outputDir, "queues.ts");
|
|
4914
|
+
if (queues.length === 0) {
|
|
4915
|
+
await writeFile(queuesPath, `/**
|
|
4916
|
+
* Generated queues setup — this app declares none.
|
|
4917
|
+
*/
|
|
4918
|
+
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
4919
|
+
import type { Logger } from '@geekmidas/logger';
|
|
4920
|
+
|
|
4921
|
+
export async function setupQueues(
|
|
4922
|
+
_envParser: EnvironmentParser<any>,
|
|
4923
|
+
_logger: Logger,
|
|
4924
|
+
): Promise<void> {}
|
|
4925
|
+
|
|
4926
|
+
export function queuesStatus(): { failed: string[]; connections: unknown[] } {
|
|
4927
|
+
return { failed: [], connections: [] };
|
|
4928
|
+
}
|
|
4929
|
+
|
|
4930
|
+
export async function stopQueues(): Promise<void> {}
|
|
4931
|
+
`);
|
|
4932
|
+
return queuesPath;
|
|
4933
|
+
}
|
|
4934
|
+
const importsByFile = /* @__PURE__ */ new Map();
|
|
4935
|
+
for (const { path, key } of queues) {
|
|
4936
|
+
const importPath = relative(dirname(queuesPath), path.relative).replace(/\.ts$/, ".js");
|
|
4937
|
+
if (!importsByFile.has(importPath)) importsByFile.set(importPath, []);
|
|
4938
|
+
importsByFile.get(importPath)?.push(key);
|
|
4939
|
+
}
|
|
4940
|
+
const content = `/**
|
|
4941
|
+
* Generated queue consumers setup.
|
|
4942
|
+
*
|
|
4943
|
+
* Each queue's one consumer polls its own queue, through the queue's own
|
|
4944
|
+
* connection string — the one its producers publish on. SQS cannot push, so
|
|
4945
|
+
* this is a poller on every transport: pg-boss locally by default, the SQS
|
|
4946
|
+
* queue on the AWS emulator.
|
|
4947
|
+
*
|
|
4948
|
+
* Deployed on Lambda, a queue's consumer is an SQS event source instead.
|
|
4949
|
+
*/
|
|
4950
|
+
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
4951
|
+
import type { Logger } from '@geekmidas/logger';
|
|
4952
|
+
import { EventConnectionFactory, Subscriber } from '@geekmidas/events';
|
|
4953
|
+
import type { EventConnection } from '@geekmidas/events';
|
|
4954
|
+
import { ServiceDiscovery } from '@geekmidas/services';
|
|
4955
|
+
${Array.from(importsByFile.entries()).map(([importPath, exports]) => `import { ${exports.join(", ")} } from '${importPath}';`).join("\n")}
|
|
4956
|
+
|
|
4957
|
+
const queues = [
|
|
4958
|
+
${queues.map(({ key, construct }) => ` { queue: ${key}, connectionKey: '${provideKey(construct.id, "publisherConnectionString")}' },`).join("\n")}
|
|
4959
|
+
];
|
|
4960
|
+
|
|
4961
|
+
// What was opened, and what could not be: a worker's health check reads both,
|
|
4962
|
+
// and its shutdown closes what was opened.
|
|
4963
|
+
const connections = new Map<string, EventConnection>();
|
|
4964
|
+
const failed: string[] = [];
|
|
4965
|
+
|
|
4966
|
+
export async function setupQueues(
|
|
4967
|
+
envParser: EnvironmentParser<any>,
|
|
4968
|
+
logger: Logger,
|
|
4969
|
+
): Promise<void> {
|
|
4970
|
+
const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
|
|
4971
|
+
|
|
4972
|
+
for (const { queue, connectionKey } of queues) {
|
|
4973
|
+
const connectionString = process.env[connectionKey];
|
|
4974
|
+
if (!connectionString) {
|
|
4975
|
+
logger.error({ queue: queue.name, connectionKey }, 'No connection string for this queue');
|
|
4976
|
+
failed.push(queue.name);
|
|
4977
|
+
continue;
|
|
4978
|
+
}
|
|
4979
|
+
|
|
4980
|
+
try {
|
|
4981
|
+
let connection = connections.get(connectionString);
|
|
4982
|
+
if (!connection) {
|
|
4983
|
+
connection = await EventConnectionFactory.fromConnectionString(connectionString);
|
|
4984
|
+
connections.set(connectionString, connection);
|
|
4985
|
+
}
|
|
4986
|
+
const eventSubscriber = await Subscriber.fromConnection(connection);
|
|
4987
|
+
const services = queue.services.length > 0
|
|
4988
|
+
? await serviceDiscovery.register(queue.services)
|
|
4989
|
+
: {};
|
|
4990
|
+
// The queue's database — the worker's, or its own — handed over as \`db\`.
|
|
4991
|
+
const db = queue.databaseService
|
|
4992
|
+
? (await serviceDiscovery.register([queue.databaseService]))[
|
|
4993
|
+
queue.databaseService.serviceName
|
|
4994
|
+
]
|
|
4995
|
+
: undefined;
|
|
4996
|
+
|
|
4997
|
+
// A queue's messages carry one type — its own name.
|
|
4998
|
+
await eventSubscriber.subscribe([queue.name], async (message) => {
|
|
4999
|
+
try {
|
|
5000
|
+
const validation = await queue.messageSchema['~standard'].validate(message.payload);
|
|
5001
|
+
if (validation.issues) {
|
|
5002
|
+
logger.error({ issues: validation.issues, queue: queue.name }, 'Queue message failed validation');
|
|
5003
|
+
return;
|
|
5004
|
+
}
|
|
5005
|
+
|
|
5006
|
+
await queue.handler({
|
|
5007
|
+
messages: [validation.value],
|
|
5008
|
+
services: services,
|
|
5009
|
+
logger: queue.logger,
|
|
5010
|
+
db,
|
|
5011
|
+
} as any);
|
|
5012
|
+
} catch (error) {
|
|
5013
|
+
logger.error({ err: error, queue: queue.name }, 'Failed to process queue message');
|
|
5014
|
+
// Rethrown so the transport keeps the message for a retry.
|
|
5015
|
+
throw error;
|
|
5016
|
+
}
|
|
5017
|
+
});
|
|
5018
|
+
logger.info({ queue: queue.name }, 'Queue consumer started polling');
|
|
5019
|
+
} catch (error) {
|
|
5020
|
+
logger.error({ err: error, queue: queue.name }, 'Failed to set up queue consumer');
|
|
5021
|
+
failed.push(queue.name);
|
|
5022
|
+
}
|
|
5023
|
+
}
|
|
5024
|
+
}
|
|
5025
|
+
|
|
5026
|
+
/** The consumers that could not start, and the connections that did. */
|
|
5027
|
+
export function queuesStatus(): { failed: string[]; connections: EventConnection[] } {
|
|
5028
|
+
return { failed: [...failed], connections: [...connections.values()] };
|
|
5029
|
+
}
|
|
5030
|
+
|
|
5031
|
+
/**
|
|
5032
|
+
* Stop polling, let the messages in flight finish, and close each broker
|
|
5033
|
+
* connection — pg-boss's graceful stop does all three. The caller bounds it.
|
|
5034
|
+
*/
|
|
5035
|
+
export async function stopQueues(): Promise<void> {
|
|
5036
|
+
const open = [...connections.values()];
|
|
5037
|
+
connections.clear();
|
|
5038
|
+
await Promise.all(open.map((connection) => connection.close()));
|
|
5039
|
+
}
|
|
5040
|
+
`;
|
|
5041
|
+
await writeFile(queuesPath, content);
|
|
5042
|
+
return queuesPath;
|
|
5043
|
+
}
|
|
5044
|
+
};
|
|
5045
|
+
//#endregion
|
|
5046
|
+
//#region src/generators/WorkerGenerator.ts
|
|
5047
|
+
/**
|
|
5048
|
+
* A worker whose topic subscribers would have to be pushed to over HTTP.
|
|
5049
|
+
*
|
|
5050
|
+
* On SNS a subscriber is an HTTP endpoint SNS delivers to, and a worker's
|
|
5051
|
+
* process serves nothing but its health check. A server target's broker is
|
|
5052
|
+
* pg-boss, where each subscriber polls a queue of its own, so this is only
|
|
5053
|
+
* reached by building server images for a project that deploys to AWS — where
|
|
5054
|
+
* a subscriber is a Lambda instead.
|
|
5055
|
+
*/
|
|
5056
|
+
var WorkerSubscribersNeedPush = class extends Error {
|
|
5057
|
+
worker;
|
|
5058
|
+
subscribers;
|
|
5059
|
+
constructor(worker, subscribers) {
|
|
5060
|
+
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.`);
|
|
5061
|
+
this.worker = worker;
|
|
5062
|
+
this.subscribers = subscribers;
|
|
5063
|
+
this.name = "WorkerSubscribersNeedPush";
|
|
5064
|
+
}
|
|
5065
|
+
};
|
|
5066
|
+
/** Where a worker's generated entry lives, under an app's `.gkm/server`. */
|
|
5067
|
+
function workerEntryDir(serverDir, workerId) {
|
|
5068
|
+
return join(serverDir, "workers", appKey(workerId));
|
|
5069
|
+
}
|
|
5070
|
+
/** The file a worker's bundle is written to, beside the server's. */
|
|
5071
|
+
function workerBundleName(workerId) {
|
|
5072
|
+
return `worker-${appKey(workerId)}.mjs`;
|
|
5073
|
+
}
|
|
5074
|
+
/**
|
|
5075
|
+
* A Worker's production entry: the process that runs its crons, its queue
|
|
5076
|
+
* consumers and its topic subscribers, and answers nothing but a health check.
|
|
5077
|
+
*
|
|
5078
|
+
* It is the server's runtime without the server. The `crons.ts`, `queues.ts`
|
|
5079
|
+
* and `subscribers.ts` it starts are the files `gkm dev` starts — written by
|
|
5080
|
+
* the same generators, given only this worker's constructs — so a consumer
|
|
5081
|
+
* runs the same code in development and in the image. What it adds is the
|
|
5082
|
+
* process around them: drivers, a health check that says when a consumer or
|
|
5083
|
+
* the broker is down, and a shutdown that drains.
|
|
5084
|
+
*/
|
|
5085
|
+
var WorkerGenerator = class {
|
|
5086
|
+
generators;
|
|
5087
|
+
constructor(generators = {
|
|
5088
|
+
cron: new CronGenerator(),
|
|
5089
|
+
queue: new QueueGenerator(),
|
|
5090
|
+
subscriber: new SubscriberGenerator()
|
|
5091
|
+
}) {
|
|
5092
|
+
this.generators = generators;
|
|
5093
|
+
}
|
|
5094
|
+
/** Write the worker's files; returns its entry, `worker.ts`. */
|
|
5095
|
+
async build(input) {
|
|
5096
|
+
const { context, workerId, outputDir } = input;
|
|
5097
|
+
if (input.subscribers.length > 0 && context.eventsBackends?.includes("sns")) throw new WorkerSubscribersNeedPush(workerId, input.subscribers.map(({ key }) => key));
|
|
5098
|
+
await mkdir(outputDir, { recursive: true });
|
|
5099
|
+
const options = { target: "server" };
|
|
5100
|
+
await Promise.all([
|
|
5101
|
+
this.generators.cron.build(context, input.crons, outputDir, options),
|
|
5102
|
+
this.generators.queue.build(context, input.queues, outputDir, options),
|
|
5103
|
+
this.generators.subscriber.build(context, input.subscribers, outputDir, options)
|
|
5104
|
+
]);
|
|
5105
|
+
await writeFile(join(outputDir, "app.ts"), workerApp(context, workerId, outputDir));
|
|
5106
|
+
await writeFile(join(outputDir, "telemetry.ts"), generateTelemetryModule(context.telemetry ? {
|
|
5107
|
+
...context.telemetry,
|
|
5108
|
+
serviceName: workerId
|
|
5109
|
+
} : void 0));
|
|
5110
|
+
const entry = join(outputDir, "worker.ts");
|
|
5111
|
+
await writeFile(entry, workerEntry(context.production?.healthCheck ?? "/health"));
|
|
5112
|
+
return entry;
|
|
5113
|
+
}
|
|
5114
|
+
};
|
|
5115
|
+
/**
|
|
5116
|
+
* `app.ts`: the worker's runtime — its logger and environment parser off the
|
|
5117
|
+
* Worker construct, the drivers its target needs, and a `startWorker` that
|
|
5118
|
+
* starts every cron, consumer and subscriber it owns.
|
|
5119
|
+
*/
|
|
5120
|
+
function workerApp(context, workerId, outputDir) {
|
|
5121
|
+
const runtime = runtimeFor(context, outputDir, workerId);
|
|
5122
|
+
return `/**
|
|
5123
|
+
* Generated worker runtime for ${workerId}.
|
|
5124
|
+
* Generated by 'gkm build --production'.
|
|
5125
|
+
*/
|
|
5126
|
+
import { setupCrons, cronsStatus, stopCrons } from './crons.js';
|
|
5127
|
+
import { setupQueues, queuesStatus, stopQueues } from './queues.js';
|
|
5128
|
+
import { setupSubscribers, subscribersStatus, stopSubscribers } from './subscribers.js';
|
|
5129
|
+
${runtime.imports}
|
|
5130
|
+
${context.storageDrivers?.imports ?? ""}
|
|
5131
|
+
|
|
5132
|
+
${runtime.bindings}
|
|
5133
|
+
|
|
5134
|
+
${context.storageDrivers?.setup ? `// The entry point registers the drivers its target needs.
|
|
5135
|
+
${context.storageDrivers.setup}
|
|
5136
|
+
` : ""}
|
|
5137
|
+
// Who this worker is to Postgres: application_name on every connection.
|
|
5138
|
+
process.env.GKM_APP_NAME ??= ${JSON.stringify(workerId)};
|
|
5139
|
+
|
|
5140
|
+
export { logger };
|
|
5141
|
+
|
|
5142
|
+
/** What the health check answers, and why. */
|
|
5143
|
+
export interface WorkerHealth {
|
|
5144
|
+
healthy: boolean;
|
|
5145
|
+
/** The crons, queues and subscribers that could not start. */
|
|
5146
|
+
failed: string[];
|
|
5147
|
+
/** How many broker connections did not answer. */
|
|
5148
|
+
down: number;
|
|
5149
|
+
}
|
|
5150
|
+
|
|
5151
|
+
/** Whether one broker connection answers: open, and — on pg-boss — a query. */
|
|
5152
|
+
async function answers(connection: unknown): Promise<boolean> {
|
|
5153
|
+
const c = connection as {
|
|
5154
|
+
isConnected?: () => boolean;
|
|
5155
|
+
instance?: { getDb?: () => { executeSql: (sql: string) => Promise<unknown> } };
|
|
5156
|
+
};
|
|
5157
|
+
if (c.isConnected && !c.isConnected()) return false;
|
|
5158
|
+
const db = c.instance?.getDb?.();
|
|
5159
|
+
if (!db) return true;
|
|
5160
|
+
try {
|
|
5161
|
+
await Promise.race([
|
|
5162
|
+
db.executeSql('select 1'),
|
|
5163
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 2000).unref()),
|
|
5164
|
+
]);
|
|
5165
|
+
return true;
|
|
5166
|
+
} catch {
|
|
5167
|
+
return false;
|
|
5168
|
+
}
|
|
5169
|
+
}
|
|
5170
|
+
|
|
5171
|
+
/** Start everything this worker runs. */
|
|
5172
|
+
export async function startWorker(): Promise<{
|
|
5173
|
+
health: () => Promise<WorkerHealth>;
|
|
5174
|
+
stop: () => Promise<void>;
|
|
5175
|
+
}> {
|
|
5176
|
+
// An app object with no routes: on a server target nothing is pushed to a
|
|
5177
|
+
// subscriber, so there is nothing to mount.
|
|
5178
|
+
const noRoutes = { post: () => undefined };
|
|
5179
|
+
|
|
5180
|
+
await setupSubscribers(noRoutes, envParser, logger).catch((error) => {
|
|
5181
|
+
logger.error({ err: error }, 'Failed to start subscribers');
|
|
5182
|
+
});
|
|
5183
|
+
await setupQueues(envParser, logger).catch((error) => {
|
|
5184
|
+
logger.error({ err: error }, 'Failed to start queue consumers');
|
|
5185
|
+
});
|
|
5186
|
+
await setupCrons(envParser, logger).catch((error) => {
|
|
5187
|
+
logger.error({ err: error }, 'Failed to schedule crons');
|
|
5188
|
+
});
|
|
5189
|
+
|
|
5190
|
+
const statuses = () => [subscribersStatus(), queuesStatus(), cronsStatus()];
|
|
5191
|
+
|
|
5192
|
+
return {
|
|
5193
|
+
async health() {
|
|
5194
|
+
const all = statuses();
|
|
5195
|
+
const failed = all.flatMap((s) => s.failed);
|
|
5196
|
+
const alive = await Promise.all(
|
|
5197
|
+
all.flatMap((s) => s.connections as unknown[]).map(answers),
|
|
5198
|
+
);
|
|
5199
|
+
const down = alive.filter((ok) => !ok).length;
|
|
5200
|
+
return { healthy: failed.length === 0 && down === 0, failed, down };
|
|
5201
|
+
},
|
|
5202
|
+
// Each stops pulling, waits for what it has in flight, and closes its
|
|
5203
|
+
// broker connection. Together, so one slow handler does not hold the
|
|
5204
|
+
// others' connections open.
|
|
5205
|
+
async stop() {
|
|
5206
|
+
await Promise.all([stopSubscribers(), stopQueues(), stopCrons()]);
|
|
5207
|
+
},
|
|
5208
|
+
};
|
|
5209
|
+
}
|
|
5210
|
+
`;
|
|
5211
|
+
}
|
|
5212
|
+
/**
|
|
5213
|
+
* `worker.ts`: the process. Health first, so the container answers while the
|
|
5214
|
+
* consumers connect; then the worker; then a shutdown that drains.
|
|
5215
|
+
*/
|
|
5216
|
+
function workerEntry(healthCheck) {
|
|
5217
|
+
return `#!/usr/bin/env node
|
|
5218
|
+
/**
|
|
5219
|
+
* Production worker entry point.
|
|
5220
|
+
* Generated by 'gkm build --production'.
|
|
5221
|
+
*
|
|
5222
|
+
* Runs this worker's crons, queue consumers and topic subscribers, and serves
|
|
5223
|
+
* one route: ${healthCheck}, on PORT — 200 when every consumer started and
|
|
5224
|
+
* every broker connection answers, 503 otherwise.
|
|
5225
|
+
*
|
|
5226
|
+
* On SIGTERM it stops pulling messages and scheduling crons, lets the handlers
|
|
5227
|
+
* in flight finish, closes its broker connections and database pools, and
|
|
5228
|
+
* exits 0 — or 1, at GKM_SHUTDOWN_TIMEOUT_MS (8s by default, under Docker's
|
|
5229
|
+
* 10s stop timeout).
|
|
5230
|
+
*/
|
|
5231
|
+
import { createServer } from 'node:http';
|
|
5232
|
+
import { runShutdownHooks } from '@geekmidas/constructs';
|
|
5233
|
+
import { startTelemetry } from './telemetry.js';
|
|
5234
|
+
|
|
5235
|
+
// Before the worker is imported, so the libraries it loads are instrumented.
|
|
5236
|
+
await startTelemetry({ ignorePaths: [${JSON.stringify(healthCheck)}] });
|
|
5237
|
+
|
|
5238
|
+
const { startWorker, logger } = await import('./app.js');
|
|
5239
|
+
|
|
5240
|
+
const port = Number(process.env.PORT) || 3000;
|
|
5241
|
+
|
|
5242
|
+
let worker: Awaited<ReturnType<typeof startWorker>> | undefined;
|
|
5243
|
+
let stopping = false;
|
|
5244
|
+
|
|
5245
|
+
const server = createServer((req, res) => {
|
|
5246
|
+
const path = (req.url ?? '/').split('?')[0];
|
|
5247
|
+
if (path !== ${JSON.stringify(healthCheck)} || (req.method !== 'GET' && req.method !== 'HEAD')) {
|
|
5248
|
+
res.writeHead(404).end();
|
|
5249
|
+
return;
|
|
5250
|
+
}
|
|
5251
|
+
const answer = (status: number, body: unknown) => {
|
|
5252
|
+
res.writeHead(status, { 'content-type': 'application/json' });
|
|
5253
|
+
res.end(JSON.stringify(body));
|
|
5254
|
+
};
|
|
5255
|
+
if (stopping) return answer(503, { status: 'stopping' });
|
|
5256
|
+
if (!worker) return answer(503, { status: 'starting' });
|
|
5257
|
+
worker.health().then(
|
|
5258
|
+
(health) => answer(health.healthy ? 200 : 503, { status: health.healthy ? 'ok' : 'unhealthy', ...health }),
|
|
5259
|
+
(error) => answer(503, { status: 'unhealthy', error: String(error) }),
|
|
5260
|
+
);
|
|
5261
|
+
});
|
|
5262
|
+
server.listen(port);
|
|
5263
|
+
|
|
5264
|
+
const shutdown = async () => {
|
|
5265
|
+
if (stopping) return;
|
|
5266
|
+
stopping = true;
|
|
5267
|
+
const deadline = Number(process.env.GKM_SHUTDOWN_TIMEOUT_MS) || 8000;
|
|
5268
|
+
logger.info({ deadline }, 'Graceful shutdown initiated');
|
|
5269
|
+
setTimeout(() => {
|
|
5270
|
+
logger.warn({ deadline }, 'Shutdown deadline reached, exiting');
|
|
5271
|
+
process.exit(1);
|
|
5272
|
+
}, deadline).unref();
|
|
5273
|
+
|
|
5274
|
+
// Nothing new is pulled or scheduled; what is in flight finishes.
|
|
5275
|
+
await worker?.stop().catch((error) => logger.error({ err: error }, 'Failed to stop the worker'));
|
|
5276
|
+
// Then what the handlers used: database pools.
|
|
5277
|
+
await runShutdownHooks((error) => logger.error({ err: error }, 'Shutdown hook failed'));
|
|
5278
|
+
server.close();
|
|
5279
|
+
logger.info('Worker stopped');
|
|
5280
|
+
process.exit(0);
|
|
5281
|
+
};
|
|
5282
|
+
|
|
5283
|
+
process.on('SIGTERM', shutdown);
|
|
5284
|
+
process.on('SIGINT', shutdown);
|
|
5285
|
+
|
|
5286
|
+
worker = await startWorker();
|
|
5287
|
+
logger.info({ port }, 'Worker started');
|
|
5288
|
+
`;
|
|
5289
|
+
}
|
|
5290
|
+
//#endregion
|
|
4535
5291
|
//#region src/secrets/encryption.ts
|
|
4536
5292
|
var encryption_exports = /* @__PURE__ */ __exportAll({
|
|
4537
5293
|
encryptSecrets: () => encryptSecrets,
|
|
@@ -4584,6 +5340,74 @@ function keyFingerprint(masterKey) {
|
|
|
4584
5340
|
return createHash("sha256").update(masterKey).digest("hex").slice(0, 8);
|
|
4585
5341
|
}
|
|
4586
5342
|
//#endregion
|
|
5343
|
+
//#region src/build/workers.ts
|
|
5344
|
+
/** A worker with background work and no backend app to build it from. */
|
|
5345
|
+
var WorkerHasNoApp = class extends Error {
|
|
5346
|
+
worker;
|
|
5347
|
+
constructor(worker) {
|
|
5348
|
+
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.`);
|
|
5349
|
+
this.worker = worker;
|
|
5350
|
+
this.name = "WorkerHasNoApp";
|
|
5351
|
+
}
|
|
5352
|
+
};
|
|
5353
|
+
/** A worker whose service name an app already has. */
|
|
5354
|
+
var WorkerNameTaken = class extends Error {
|
|
5355
|
+
worker;
|
|
5356
|
+
service;
|
|
5357
|
+
constructor(worker, service) {
|
|
5358
|
+
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.`);
|
|
5359
|
+
this.worker = worker;
|
|
5360
|
+
this.service = service;
|
|
5361
|
+
this.name = "WorkerNameTaken";
|
|
5362
|
+
}
|
|
5363
|
+
};
|
|
5364
|
+
/**
|
|
5365
|
+
* The workers a workspace deploys — each one that has background work — and
|
|
5366
|
+
* the app each is built from.
|
|
5367
|
+
*
|
|
5368
|
+
* The host is the backend app whose directory holds the most of the worker's
|
|
5369
|
+
* files; one whose work sits outside every app (a shared `jobs/` directory)
|
|
5370
|
+
* is built by the first backend, by name, that generates a server. An app
|
|
5371
|
+
* with its own entry is never one — gkm's generators do not build it — and a
|
|
5372
|
+
* surface that serves itself, an auth server, only when it holds the work.
|
|
5373
|
+
*
|
|
5374
|
+
* @param background Each worker's files, from discovery's `background`.
|
|
5375
|
+
*/
|
|
5376
|
+
function workerUnits(workspace, manifest, background) {
|
|
5377
|
+
const hosts = Object.entries(workspace.apps).filter(([, app]) => app.type === "backend" && !app.entry).map(([name, app]) => {
|
|
5378
|
+
const surface = Object.entries(manifest).find(([id, d]) => d.kind === "rest-api" && appKey(id) === name)?.[1];
|
|
5379
|
+
return {
|
|
5380
|
+
name,
|
|
5381
|
+
root: isAbsolute(app.path) ? app.path : join(workspace.root, app.path),
|
|
5382
|
+
generated: !(surface?.kind === "rest-api" && surface.endpoints.length > 0)
|
|
5383
|
+
};
|
|
5384
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
5385
|
+
const units = [];
|
|
5386
|
+
for (const [id, declaration] of Object.entries(manifest)) {
|
|
5387
|
+
if (declaration.kind !== "worker") continue;
|
|
5388
|
+
const files = background[id] ?? [];
|
|
5389
|
+
if (files.length === 0) continue;
|
|
5390
|
+
const name = appKey(id);
|
|
5391
|
+
if (workspace.apps[name]) throw new WorkerNameTaken(id, name);
|
|
5392
|
+
const ranked = hosts.map((host) => ({
|
|
5393
|
+
host,
|
|
5394
|
+
holds: files.filter((file) => isUnder(file, host.root)).length
|
|
5395
|
+
})).sort((a, b) => b.holds - a.holds || Number(b.host.generated) - Number(a.host.generated));
|
|
5396
|
+
const host = ranked[0]?.host;
|
|
5397
|
+
if (!host || ranked[0].holds === 0 && !host.generated) throw new WorkerHasNoApp(id);
|
|
5398
|
+
units.push({
|
|
5399
|
+
id,
|
|
5400
|
+
name,
|
|
5401
|
+
app: host.name
|
|
5402
|
+
});
|
|
5403
|
+
}
|
|
5404
|
+
return units.sort((a, b) => a.name.localeCompare(b.name));
|
|
5405
|
+
}
|
|
5406
|
+
function isUnder(file, dir) {
|
|
5407
|
+
const rel = relative(dir, file);
|
|
5408
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel) ? !rel.startsWith(`..${sep}`) : false;
|
|
5409
|
+
}
|
|
5410
|
+
//#endregion
|
|
4587
5411
|
//#region src/deploy/devServices.ts
|
|
4588
5412
|
/**
|
|
4589
5413
|
* Mail and object storage on a deployed stage: real ones, or — only when
|
|
@@ -5041,7 +5865,7 @@ async function dockerCommand(options) {
|
|
|
5041
5865
|
const cwd = options.cwd ?? process.cwd();
|
|
5042
5866
|
const loaded = await loadWorkspaceConfig(cwd);
|
|
5043
5867
|
const names = loaded.type === "single" ? Object.fromEntries(Object.keys(loaded.workspace.apps).map((name) => [name, resolveDockerConfig(loaded.raw).imageName])) : {};
|
|
5044
|
-
const result = await workspaceDockerCommand(loaded.workspace, loaded.manifest, names);
|
|
5868
|
+
const result = await workspaceDockerCommand(loaded.workspace, loaded.manifest, names, loaded.background);
|
|
5045
5869
|
if (options.build) for (const app of result.apps) await buildDockerImage(app, result.buildRoot, options);
|
|
5046
5870
|
if (options.push) for (const app of result.apps) await pushDockerImage(app.imageName, options, result.buildRoot);
|
|
5047
5871
|
return result;
|
|
@@ -5093,7 +5917,7 @@ async function pushDockerImage(imageName, options, cwd) {
|
|
|
5093
5917
|
* Generate Dockerfiles for all apps in a workspace.
|
|
5094
5918
|
* @internal Exported for testing
|
|
5095
5919
|
*/
|
|
5096
|
-
async function workspaceDockerCommand(workspace, manifest, imageNames = {}) {
|
|
5920
|
+
async function workspaceDockerCommand(workspace, manifest, imageNames = {}, background = {}) {
|
|
5097
5921
|
const results = [];
|
|
5098
5922
|
const layout = imageLayout(workspace);
|
|
5099
5923
|
logger$1.log(`\n🐳 Generating Dockerfiles for workspace: ${workspace.name}`);
|
|
@@ -5123,6 +5947,24 @@ async function workspaceDockerCommand(workspace, manifest, imageNames = {}) {
|
|
|
5123
5947
|
imageName: imageNames[appName] ?? appName
|
|
5124
5948
|
});
|
|
5125
5949
|
}
|
|
5950
|
+
for (const worker of manifest ? workerUnits(workspace, manifest, background) : []) {
|
|
5951
|
+
logger$1.log(`\n 📄 Generating Dockerfile for ${worker.name} (worker ${worker.id}, built from ${worker.app})`);
|
|
5952
|
+
const dockerfile = workerDockerfile(worker, workspace.apps[worker.app], {
|
|
5953
|
+
layout,
|
|
5954
|
+
workspaceRoot: workspace.root
|
|
5955
|
+
});
|
|
5956
|
+
const path = workerDockerfileOf(worker.name);
|
|
5957
|
+
const dockerfilePath = join(workspace.root, path);
|
|
5958
|
+
await mkdir(dirname(dockerfilePath), { recursive: true });
|
|
5959
|
+
await writeFile(dockerfilePath, dockerfile);
|
|
5960
|
+
logger$1.log(` Generated: ${path}`);
|
|
5961
|
+
results.push({
|
|
5962
|
+
appName: worker.name,
|
|
5963
|
+
type: "backend",
|
|
5964
|
+
dockerfile: dockerfilePath,
|
|
5965
|
+
imageName: imageNames[worker.name] ?? worker.name
|
|
5966
|
+
});
|
|
5967
|
+
}
|
|
5126
5968
|
const dockerignorePath = await ensureDockerignore(layout.buildRoot);
|
|
5127
5969
|
logger$1.log(`\n Ensured: ${dockerignorePath}`);
|
|
5128
5970
|
const composePath = await writeConstructsCompose(workspace);
|
|
@@ -5162,6 +6004,24 @@ function appDockerfile(appName, app, options) {
|
|
|
5162
6004
|
healthCheckPath: "/health"
|
|
5163
6005
|
});
|
|
5164
6006
|
}
|
|
6007
|
+
/** Where `gkm docker` writes a worker's Dockerfile. */
|
|
6008
|
+
function workerDockerfileOf(worker) {
|
|
6009
|
+
return `.gkm/docker/Dockerfile.${worker}`;
|
|
6010
|
+
}
|
|
6011
|
+
/**
|
|
6012
|
+
* A Worker's Dockerfile: its host app's build, and the worker's bundle as the
|
|
6013
|
+
* runner — see `generateWorkerDockerfile`.
|
|
6014
|
+
*/
|
|
6015
|
+
function workerDockerfile(worker, host, options) {
|
|
6016
|
+
return generateWorkerDockerfile({
|
|
6017
|
+
...appImageOptions(options.layout, worker.app, host, options.workspaceRoot),
|
|
6018
|
+
imageName: worker.name,
|
|
6019
|
+
port: WORKER_PORT,
|
|
6020
|
+
worker: worker.id,
|
|
6021
|
+
bundle: workerBundleName(worker.id),
|
|
6022
|
+
healthCheckPath: "/health"
|
|
6023
|
+
});
|
|
6024
|
+
}
|
|
5165
6025
|
/**
|
|
5166
6026
|
* A site's Dockerfile, with an `ARG` for every key its bundler inlines.
|
|
5167
6027
|
*
|
|
@@ -5399,7 +6259,7 @@ async function deployDocker(options) {
|
|
|
5399
6259
|
const cwd = options.cwd ?? process.cwd();
|
|
5400
6260
|
const docker = {
|
|
5401
6261
|
cwd,
|
|
5402
|
-
dockerfile: join(cwd, dockerfileOf(config.appName ?? imageName, options.appPath ?? ".")),
|
|
6262
|
+
dockerfile: join(cwd, options.dockerfile ?? dockerfileOf(config.appName ?? imageName, options.appPath ?? ".")),
|
|
5403
6263
|
...options.signal ? { signal: options.signal } : {},
|
|
5404
6264
|
...options.stdio ? { stdio: options.stdio } : {}
|
|
5405
6265
|
};
|
|
@@ -5725,6 +6585,6 @@ function setPostgresBackupId(state, backupId) {
|
|
|
5725
6585
|
if (state.backups) state.backups.postgresBackupId = backupId;
|
|
5726
6586
|
}
|
|
5727
6587
|
//#endregion
|
|
5728
|
-
export {
|
|
6588
|
+
export { constructGlobs as $, applicationName as $t, DevServicesNeedServerTarget as A, eventsBackendFor as At, requiredStageKeys as B, findAvailablePort as Bt, validateImageRef as C, composeBuildPaths as Ct, isOtlpDestination as D, detectPackageManager as Dt, resolveHost as E, WORKER_PORT as Et, devServicesUsed as F, fakeKey as Ft, keyFingerprint as G, isRemoteStore as Gt, workerUnits as H, loadEnvFiles as Ht, externalServices as I, portKeys as It, workerEntryDir as J, FileSecretsStore as Jt, WorkerGenerator as K, secretsStoreFor as Kt, manifestServiceDeclarations as L, primaryPortKey as Lt, STORAGE_KEY_PAIR as M, storageBackendFor as Mt, UnknownDevService as N, DEFAULT_IMAGES as Nt, otelEnv as O, findBuildRoot as Ot, assertExternalServices as P, describeServices as Pt, backendsOf as Q, readKey as Qt, parseDevServices as R, createCredentialsPreload as Rt, workerDockerfileOf as S, hostFor as St, isMainFrontendApp as T, TURBO_VERSION as Tt, encryptSecrets as U, loadSecretsForApp as Ut, suppliedOnly as V, isPortAvailable as Vt, encryption_exports as W, prepareEntryCredentials as Wt, CronGenerator as X, keystoreProject as Xt, QueueGenerator as Y, getKeyPath as Yt, FAKE_ENV as Z, projectKey as Zt, deployDocker as _, workerEnvKeys as _t, recordRelease as a, ownershipMarker as an, applyBuckets as at, ensureDockerignore as b, planFor as bt, setBackupState as c, maskPassword as cn, bucketPolicies as ct, setGeneratedSecret as d, withCustomSecret as dn, quoteIdentifier as dt, deployIdentity as en, derivedContainers as et, setPostgresBackupId as f, composeFor as ft, credentialsFileContent as g, networkEnv as gt, credentialsBuildArg as h, appEnvKeys as ht, isDnsVerified as i, markerOf as in, forgetState as it, ExternalServicesNotConfigured as j, providerOf as jt, DEV_SERVICES as k, cacheBackendFor as kt, setDnsRecord as l, storage_exports as ln, postgresDatabaseNames as lt, withGeneratedSecrets as m, pgClient as mt, getBackupState as n, imageRef as nn, workspace_exports as nt, recordRollback as o, projectName as on, applyPolicies as ot, DeployJournal as p, bucketClient as pt, workerBundleName as q, store_exports as qt, getGeneratedSecret as r, isOwnedBy as rn, LOCAL_CA_PATH as rt, setApplicationId as s, initStageSecrets as sn, applyPostgres as st, createEmptyState as t, imageName as tn, reconcileWorkspace as tt, setDnsVerification as u, toEmbeddableSecrets as un, postgresStatements as ut, appDockerfile as v, envFor as vt, NoDomainForStage as w, imageLayout as wt, workerDockerfile as x, resourceName as xt, dockerCommand as y, localRolePassword as yt, reportDevServices as z, createEntryWrapper as zt };
|
|
5729
6589
|
|
|
5730
|
-
//# sourceMappingURL=state-
|
|
6590
|
+
//# sourceMappingURL=state-DtS2HEfi.mjs.map
|