@geekmidas/cli 10.0.0-alpha.39 → 10.0.0-alpha.40

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.
Files changed (47) hide show
  1. package/dist/{OpenApiTsGenerator-CeEI4ll9.cjs → OpenApiTsGenerator-B4Kj_qNU.cjs} +12 -5
  2. package/dist/{OpenApiTsGenerator-CeEI4ll9.cjs.map → OpenApiTsGenerator-B4Kj_qNU.cjs.map} +1 -1
  3. package/dist/{OpenApiTsGenerator-DQqOuSW8.mjs → OpenApiTsGenerator-CEh3EKN3.mjs} +12 -5
  4. package/dist/{OpenApiTsGenerator-DQqOuSW8.mjs.map → OpenApiTsGenerator-CEh3EKN3.mjs.map} +1 -1
  5. package/dist/config.d.cts +2 -2
  6. package/dist/config.d.mts +2 -2
  7. package/dist/discover-B0f_kuNN.cjs.map +1 -1
  8. package/dist/discover-BLhf7V_J.mjs.map +1 -1
  9. package/dist/{index-CLH1DgsO.d.cts → index-B3PeGOQp.d.cts} +2 -2
  10. package/dist/{index-CLH1DgsO.d.cts.map → index-B3PeGOQp.d.cts.map} +1 -1
  11. package/dist/{index-CST2jOPH.d.mts → index-BOyaGcCm.d.mts} +2 -2
  12. package/dist/{index-CST2jOPH.d.mts.map → index-BOyaGcCm.d.mts.map} +1 -1
  13. package/dist/index.cjs +206 -172
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.mjs +206 -172
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/{migrate-BJR912RE.mjs → migrate-BVI3yqcE.mjs} +2 -2
  18. package/dist/{migrate-BJR912RE.mjs.map → migrate-BVI3yqcE.mjs.map} +1 -1
  19. package/dist/{migrate-Bu8POqsR.cjs → migrate-CFOUApUt.cjs} +2 -2
  20. package/dist/{migrate-Bu8POqsR.cjs.map → migrate-CFOUApUt.cjs.map} +1 -1
  21. package/dist/openapi.cjs +1 -1
  22. package/dist/openapi.d.cts +1 -1
  23. package/dist/openapi.d.mts +1 -1
  24. package/dist/openapi.mjs +1 -1
  25. package/dist/{ready-BXmLS4VB.mjs → ready-D8UXsP6q.mjs} +2 -2
  26. package/dist/{ready-BXmLS4VB.mjs.map → ready-D8UXsP6q.mjs.map} +1 -1
  27. package/dist/{ready-B0WVtkjR.cjs → ready-mcxamf73.cjs} +2 -2
  28. package/dist/{ready-B0WVtkjR.cjs.map → ready-mcxamf73.cjs.map} +1 -1
  29. package/dist/reconcile/public.d.cts +1 -1
  30. package/dist/reconcile/public.d.mts +1 -1
  31. package/dist/{types-B0EVAF6U.d.mts → types-0R124hrY.d.mts} +1 -2
  32. package/dist/types-0R124hrY.d.mts.map +1 -0
  33. package/dist/{types-CHKCUKM0.d.cts → types-DfdYe31u.d.cts} +1 -2
  34. package/dist/types-DfdYe31u.d.cts.map +1 -0
  35. package/dist/vitest.cjs +1 -1
  36. package/dist/vitest.mjs +1 -1
  37. package/dist/workspace/index.d.cts +2 -2
  38. package/dist/workspace/index.d.mts +2 -2
  39. package/dist/{workspace-DZ1G4zow.cjs → workspace-BIqeeqcR.cjs} +167 -33
  40. package/dist/workspace-BIqeeqcR.cjs.map +1 -0
  41. package/dist/{workspace-CrFbItPz.mjs → workspace-DwS1JK5u.mjs} +165 -33
  42. package/dist/workspace-DwS1JK5u.mjs.map +1 -0
  43. package/package.json +16 -14
  44. package/dist/types-B0EVAF6U.d.mts.map +0 -1
  45. package/dist/types-CHKCUKM0.d.cts.map +0 -1
  46. package/dist/workspace-CrFbItPz.mjs.map +0 -1
  47. package/dist/workspace-DZ1G4zow.cjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -6,10 +6,10 @@ const require_config = require("./config-CJ-PAb7M.cjs");
6
6
  const require_workspace = require("./workspace-RYSYtHP7.cjs");
7
7
  const require_storage = require("./storage-CSGoC3bK.cjs");
8
8
  const require_store = require("./store-DQgxT_WR.cjs");
9
- const require_workspace$1 = require("./workspace-DZ1G4zow.cjs");
10
- const require_OpenApiTsGenerator = require("./OpenApiTsGenerator-CeEI4ll9.cjs");
9
+ const require_workspace$1 = require("./workspace-BIqeeqcR.cjs");
10
+ const require_OpenApiTsGenerator = require("./OpenApiTsGenerator-B4Kj_qNU.cjs");
11
11
  const require_openapi = require("./openapi.cjs");
12
- const require_ready = require("./ready-B0WVtkjR.cjs");
12
+ const require_ready = require("./ready-mcxamf73.cjs");
13
13
  const require_emit = require("./emit-BdoPFjZf.cjs");
14
14
  let commander = require("commander");
15
15
  let node_process = require("node:process");
@@ -47,7 +47,7 @@ let _geekmidas_constructs_subscribers = require("@geekmidas/constructs/subscribe
47
47
  let _geekmidas_constructs_topic = require("@geekmidas/constructs/topic");
48
48
  let node_dns_promises = require("node:dns/promises");
49
49
  //#region package.json
50
- var version = "10.0.0-alpha.39";
50
+ var version = "10.0.0-alpha.40";
51
51
  //#endregion
52
52
  //#region src/auth/credentials.ts
53
53
  /**
@@ -1214,7 +1214,10 @@ function getProductionConfigFromGkm(config) {
1214
1214
  const serverConfig = config.providers?.server;
1215
1215
  if (typeof serverConfig === "object") return serverConfig.production;
1216
1216
  }
1217
+ /** Read by the generated server: no subscriber is subscribed or polled. */
1218
+ const SUBSCRIBERS_ENV = "GKM_SUBSCRIBERS";
1217
1219
  async function devCommand(options) {
1220
+ if (options.subscribers === false) process.env[SUBSCRIBERS_ENV] = "off";
1218
1221
  if (options.entry) return entryDevCommand(options);
1219
1222
  const defaultEnv = require_workspace$1.loadEnvFiles(".env");
1220
1223
  if (defaultEnv.loaded.length > 0) logger$10.log(`📦 Loaded env: ${defaultEnv.loaded.join(", ")}`);
@@ -1569,9 +1572,9 @@ async function workspaceDevCommand(workspace, options) {
1569
1572
  const fakes = options.fake ? await serveFakes(reconciled.fakes, reconciled.ports) : [];
1570
1573
  for (const { id, port } of fakes) logger$10.log(`🎭 ${id} fake: http://localhost:${port}`);
1571
1574
  if (options.migrate || options.seed) {
1572
- const { prepareDevDatabases } = await Promise.resolve().then(() => require("./migrate-Bu8POqsR.cjs"));
1575
+ const { prepareDevDatabases } = await Promise.resolve().then(() => require("./migrate-CFOUApUt.cjs"));
1573
1576
  await prepareDevDatabases(workspace, reconciled.env, { seed: !!options.seed });
1574
- } else await Promise.resolve().then(() => require("./migrate-Bu8POqsR.cjs")).then(({ reportPendingMigrations }) => reportPendingMigrations(workspace, reconciled.env)).catch((error) => {
1577
+ } else await Promise.resolve().then(() => require("./migrate-CFOUApUt.cjs")).then(({ reportPendingMigrations }) => reportPendingMigrations(workspace, reconciled.env)).catch((error) => {
1575
1578
  logger$10.log(`⚠️ Could not check for pending migrations: ${error instanceof Error ? error.message : String(error)}`);
1576
1579
  });
1577
1580
  const appUrls = {};
@@ -2216,7 +2219,7 @@ export async function setupCrons(
2216
2219
  // runtime role, which can neither create a schema nor use the broker's.
2217
2220
  const { url } = envParser
2218
2221
  .create((get) => ({
2219
- url: get('EVENT_SUBSCRIBER_CONNECTION_STRING').string().optional(),
2222
+ url: get('EVENT_PUBLISHER_CONNECTION_STRING').string().optional(),
2220
2223
  }))
2221
2224
  .parse();
2222
2225
 
@@ -2225,7 +2228,7 @@ export async function setupCrons(
2225
2228
  { crons: prepared.map(({ name }) => name) },
2226
2229
  'These crons have nowhere to keep their schedule. On a server they are ' +
2227
2230
  'scheduled through the pg-boss events broker ' +
2228
- '(EVENT_SUBSCRIBER_CONNECTION_STRING), and ' +
2231
+ '(EVENT_PUBLISHER_CONNECTION_STRING), and ' +
2229
2232
  (url ? 'the broker configured is not pg-boss.' : 'none is configured.'),
2230
2233
  );
2231
2234
  return;
@@ -2338,11 +2341,11 @@ export const handler = adapter.handler;
2338
2341
  //#endregion
2339
2342
  //#region src/generators/QueueGenerator.ts
2340
2343
  /**
2341
- * Generates the runtime for `q` queue workers.
2344
+ * Generates the runtime for queue consumers (`worker.queue(…)`).
2342
2345
  *
2343
2346
  * - **server** (`gkm dev`): a single `queues.ts` exposing `setupQueues()` that
2344
- * runs an in-process pg-boss poller alongside the Hono server — no SQS/Lambda.
2345
- * Each queue subscribes by its **name** on the shared event connection.
2347
+ * polls each queue in-process, alongside the Hono server, through the queue's
2348
+ * own connection string — pg-boss by default, SQS on the AWS emulator.
2346
2349
  * - **aws-lambda**: one handler file per queue wrapping `AWSLambdaQueue`, backed
2347
2350
  * by an SQS event-source mapping.
2348
2351
  */
@@ -2417,64 +2420,52 @@ export async function setupQueues(
2417
2420
  importsByFile.get(importPath)?.push(key);
2418
2421
  }
2419
2422
  const content = `/**
2420
- * Generated queue workers setup
2423
+ * Generated queue consumers setup.
2421
2424
  *
2422
- * ⚠️ WARNING: This is for LOCAL DEVELOPMENT ONLY
2423
- * This runs an in-process pg-boss poller alongside the HTTP server. Each queue
2424
- * subscribes by its name on the shared event connection
2425
- * (EVENT_SUBSCRIBER_CONNECTION_STRING), so producers using
2426
- * <NAME>_PUBLISHER_CONNECTION_STRING (pgboss:// locally) reach it.
2425
+ * Each queue's one consumer polls its own queue, through the queue's own
2426
+ * connection string — the one its producers publish on. SQS cannot push, so
2427
+ * this is a poller on every transport: pg-boss locally by default, the SQS
2428
+ * queue on the AWS emulator.
2427
2429
  *
2428
- * For production, queues run as AWS Lambda with SQS event-source mappings.
2430
+ * Deployed on Lambda, a queue's consumer is an SQS event source instead.
2429
2431
  */
2430
2432
  import type { EnvironmentParser } from '@geekmidas/envkit';
2431
2433
  import type { Logger } from '@geekmidas/logger';
2432
2434
  import { EventConnectionFactory, Subscriber } from '@geekmidas/events';
2433
- import type { EventConnection, EventSubscriber } from '@geekmidas/events';
2435
+ import type { EventConnection } from '@geekmidas/events';
2434
2436
  import { ServiceDiscovery } from '@geekmidas/services';
2435
2437
  ${Array.from(importsByFile.entries()).map(([importPath, exports]) => `import { ${exports.join(", ")} } from '${importPath}';`).join("\n")}
2436
2438
 
2437
2439
  const queues = [
2438
- ${queues.map(({ key }) => key).join(",\n ")}
2440
+ ${queues.map(({ key, construct }) => ` { queue: ${key}, connectionKey: '${(0, _geekmidas_manifest.provideKey)(construct.id, "publisherConnectionString")}' },`).join("\n")}
2439
2441
  ];
2440
2442
 
2441
- const activeSubscribers: EventSubscriber<any>[] = [];
2442
-
2443
2443
  export async function setupQueues(
2444
2444
  envParser: EnvironmentParser<any>,
2445
2445
  logger: Logger,
2446
2446
  ): Promise<void> {
2447
- if (queues.length === 0) {
2448
- return;
2449
- }
2450
-
2451
- logger.info('Setting up queue workers in polling mode (local development)');
2452
-
2453
- const config = envParser.create((get) => ({
2454
- connectionString: get('EVENT_SUBSCRIBER_CONNECTION_STRING').string(),
2455
- })).parse();
2456
-
2457
2447
  const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
2448
+ const connections = new Map<string, EventConnection>();
2458
2449
 
2459
- let connection: EventConnection;
2460
- try {
2461
- connection = await EventConnectionFactory.fromConnectionString(config.connectionString);
2462
- const connectionType = new URL(config.connectionString).protocol.replace(':', '');
2463
- logger.info({ connectionType }, 'Created shared event connection for queues');
2464
- } catch (error) {
2465
- logger.error({ error }, 'Failed to create event connection for queues');
2466
- return;
2467
- }
2450
+ for (const { queue, connectionKey } of queues) {
2451
+ const connectionString = process.env[connectionKey];
2452
+ if (!connectionString) {
2453
+ logger.error({ queue: queue.name, connectionKey }, 'No connection string for this queue');
2454
+ continue;
2455
+ }
2468
2456
 
2469
- for (const queue of queues) {
2470
2457
  try {
2458
+ let connection = connections.get(connectionString);
2459
+ if (!connection) {
2460
+ connection = await EventConnectionFactory.fromConnectionString(connectionString);
2461
+ connections.set(connectionString, connection);
2462
+ }
2471
2463
  const eventSubscriber = await Subscriber.fromConnection(connection);
2472
-
2473
2464
  const services = queue.services.length > 0
2474
2465
  ? await serviceDiscovery.register(queue.services)
2475
2466
  : {};
2476
2467
 
2477
- // A queue subscribes to a single "type" — its own name.
2468
+ // A queue's messages carry one type — its own name.
2478
2469
  await eventSubscriber.subscribe([queue.name], async (message) => {
2479
2470
  try {
2480
2471
  const validation = await queue.messageSchema['~standard'].validate(message.payload);
@@ -2488,30 +2479,23 @@ export async function setupQueues(
2488
2479
  services: services,
2489
2480
  logger: queue.logger,
2490
2481
  });
2491
-
2492
- logger.debug({ queue: queue.name }, 'Successfully processed queue message');
2493
2482
  } catch (error) {
2494
2483
  logger.error({ error, queue: queue.name }, 'Failed to process queue message');
2495
- // Message will become visible again for retry.
2484
+ // Rethrown so the transport keeps the message for a retry.
2485
+ throw error;
2496
2486
  }
2497
2487
  });
2498
-
2499
- activeSubscribers.push(eventSubscriber);
2500
- logger.info({ queue: queue.name }, 'Queue worker started polling');
2488
+ logger.info({ queue: queue.name }, 'Queue consumer started polling');
2501
2489
  } catch (error) {
2502
- logger.error({ error, queue: queue.name }, 'Failed to setup queue worker');
2490
+ logger.error({ error, queue: queue.name }, 'Failed to set up queue consumer');
2503
2491
  }
2504
2492
  }
2505
2493
 
2506
2494
  const shutdown = () => {
2507
- logger.info('Stopping all queue workers');
2508
- for (const _ of activeSubscribers) {
2509
- connection.stop();
2510
- }
2495
+ for (const connection of connections.values()) void connection.close();
2511
2496
  };
2512
-
2513
- process.on('SIGTERM', shutdown);
2514
- process.on('SIGINT', shutdown);
2497
+ process.once('SIGTERM', shutdown);
2498
+ process.once('SIGINT', shutdown);
2515
2499
  }
2516
2500
  `;
2517
2501
  await (0, node_fs_promises.writeFile)(queuesPath, content);
@@ -2584,141 +2568,194 @@ import type { EnvironmentParser } from '@geekmidas/envkit';
2584
2568
  import type { Logger } from '@geekmidas/logger';
2585
2569
 
2586
2570
  export async function setupSubscribers(
2571
+ _app: unknown,
2587
2572
  _envParser: EnvironmentParser<any>,
2588
2573
  _logger: Logger,
2589
- ): Promise<void> {}
2574
+ ): Promise<(port: number) => Promise<void>> {
2575
+ return async () => {};
2576
+ }
2590
2577
  `);
2591
2578
  return subscribersPath;
2592
2579
  }
2593
2580
  const importsByFile = /* @__PURE__ */ new Map();
2594
2581
  for (const { path, key } of subscribers) {
2595
2582
  const importPath = (0, node_path.relative)((0, node_path.dirname)(subscribersPath), path.relative).replace(/\.ts$/, ".js");
2596
- if (!importsByFile.has(importPath)) importsByFile.set(importPath, []);
2597
- importsByFile.get(importPath)?.push(key);
2583
+ importsByFile.set(importPath, [...importsByFile.get(importPath) ?? [], key]);
2598
2584
  }
2599
2585
  const content = `/**
2600
- * Generated subscribers setup
2586
+ * Generated subscribers setup.
2601
2587
  *
2602
- * ⚠️ WARNING: This is for LOCAL DEVELOPMENT ONLY
2603
- * This uses event polling which is not suitable for production.
2588
+ * Each subscriber is run against the topic it is bound to, through that
2589
+ * topic's own connection string:
2604
2590
  *
2605
- * For production, use AWS Lambda with SQS/SNS event source mappings.
2606
- * Lambda automatically:
2607
- * - Scales based on queue depth
2608
- * - Handles batch processing and retries
2609
- * - Manages dead letter queues
2610
- * - Provides better cost optimization
2591
+ * - **sns://** — pushed, not polled. The subscriber gets an HTTP route on this
2592
+ * server, and the route is subscribed to the topic with a filter policy of
2593
+ * the events it named, so SNS fans out to each subscriber. The route runs the
2594
+ * subscriber through the same adaptor a Lambda subscription does.
2595
+ * - **anything else** (pgboss://, rabbitmq://, sqs://) — polled. On pg-boss
2596
+ * each subscriber drains a queue of its own, \`<topic>/<subscriber>\`, so
2597
+ * every subscriber sees every event; replicas of one subscriber share it.
2611
2598
  *
2612
- * This polling implementation is useful for:
2613
- * - Local development and testing
2614
- * - Understanding event flow without Lambda deployment
2615
- *
2616
- * Supported connection strings:
2617
- * - sqs://region/account-id/queue-name (SQS queue)
2618
- * - sns://region/account-id/topic-name (SNS topic)
2619
- * - rabbitmq://host:port/queue-name (RabbitMQ)
2620
- * - pgboss://user:pass@host:port/database (pg-boss / PostgreSQL)
2621
- * - basic://in-memory (In-memory for testing)
2599
+ * GKM_SUBSCRIBERS=off (\`gkm dev --no-subscribers\`) runs none of them.
2600
+ * GKM_SUBSCRIBER_PUSH_URL is the address SNS pushes to; against the local
2601
+ * emulator it defaults to this server through host.docker.internal.
2622
2602
  */
2603
+ import { connect } from 'node:net';
2623
2604
  import type { EnvironmentParser } from '@geekmidas/envkit';
2624
2605
  import type { Logger } from '@geekmidas/logger';
2625
2606
  import { EventConnectionFactory, Subscriber } from '@geekmidas/events';
2626
- import type { EventConnection, EventSubscriber } from '@geekmidas/events';
2607
+ import type { EventConnection } from '@geekmidas/events';
2627
2608
  import { ServiceDiscovery } from '@geekmidas/services';
2628
2609
  ${Array.from(importsByFile.entries()).map(([importPath, exports]) => `import { ${exports.join(", ")} } from '${importPath}';`).join("\n")}
2629
2610
 
2630
2611
  const subscribers = [
2631
- ${subscribers.map(({ key }) => key).join(",\n ")}
2612
+ ${subscribers.map(({ key, construct }) => {
2613
+ const connectionKey = construct.topicName ? (0, _geekmidas_manifest.provideKey)((0, _geekmidas_manifest.canonicalId)(construct.topicName), "publisherConnectionString") : void 0;
2614
+ return ` { id: '${key}', subscriber: ${key}, topic: ${construct.topicName ? `'${construct.topicName}'` : "undefined"}, connectionKey: ${connectionKey ? `'${connectionKey}'` : "undefined"} },`;
2615
+ }).join("\n")}
2632
2616
  ];
2633
2617
 
2634
- const activeSubscribers: EventSubscriber<any>[] = [];
2618
+ /** The route a subscriber's pushes arrive on. */
2619
+ export const SUBSCRIBER_ROUTE = (id: string) => \`/__gkm/subscribers/\${id}\`;
2635
2620
 
2621
+ /** Resolves once something is accepting connections on the port. */
2622
+ async function listening(port: number): Promise<void> {
2623
+ for (let attempt = 0; attempt < 100; attempt++) {
2624
+ const open = await new Promise<boolean>((resolve) => {
2625
+ const socket = connect(port, '127.0.0.1');
2626
+ socket.once('connect', () => { socket.end(); resolve(true); });
2627
+ socket.once('error', () => resolve(false));
2628
+ });
2629
+ if (open) return;
2630
+ await new Promise((resolve) => setTimeout(resolve, 50));
2631
+ }
2632
+ }
2633
+
2634
+ /**
2635
+ * Mount each pushed subscriber's route and start each polled one. Returns what
2636
+ * to run once the server is listening: subscribing a route makes SNS send its
2637
+ * confirmation at once, so the route has to be reachable first.
2638
+ */
2636
2639
  export async function setupSubscribers(
2640
+ app: { post: (path: string, handler: (c: any) => unknown) => unknown },
2637
2641
  envParser: EnvironmentParser<any>,
2638
2642
  logger: Logger,
2639
- ): Promise<void> {
2640
- if (subscribers.length === 0) {
2641
- return;
2643
+ ): Promise<(port: number) => Promise<void>> {
2644
+ if (process.env.GKM_SUBSCRIBERS === 'off') {
2645
+ logger.info('Subscribers are off (--no-subscribers)');
2646
+ return async () => {};
2642
2647
  }
2643
2648
 
2644
- logger.info('Setting up subscribers in polling mode (local development)');
2645
-
2646
- const config = envParser.create((get) => ({
2647
- connectionString: get('EVENT_SUBSCRIBER_CONNECTION_STRING').string(),
2648
- })).parse();
2649
-
2650
2649
  const serviceDiscovery = ServiceDiscovery.getInstance(envParser);
2650
+ const connections = new Map<string, EventConnection>();
2651
+ const afterListening: ((port: number) => Promise<void>)[] = [];
2652
+
2653
+ for (const { id, subscriber, topic, connectionKey } of subscribers) {
2654
+ const events = subscriber.subscribedEvents || [];
2655
+ if (events.length === 0) {
2656
+ logger.warn({ subscriber: id }, 'Subscriber has no subscribed events, skipping');
2657
+ continue;
2658
+ }
2659
+ const connectionString = connectionKey ? process.env[connectionKey] : undefined;
2660
+ if (!connectionString) {
2661
+ logger.error(
2662
+ { subscriber: id, connectionKey },
2663
+ 'No connection string for the topic this subscriber is bound to',
2664
+ );
2665
+ continue;
2666
+ }
2651
2667
 
2652
- // Create connection once, outside the loop (more efficient)
2653
- // EventConnectionFactory automatically determines the right connection type
2654
- let connection: EventConnection;
2655
- try {
2656
- connection = await EventConnectionFactory.fromConnectionString(config.connectionString);
2657
-
2658
- const connectionType = new URL(config.connectionString).protocol.replace(':', '');
2659
- logger.info({ connectionType }, 'Created shared event connection');
2660
- } catch (error) {
2661
- logger.error({ error }, 'Failed to create event connection');
2662
- return;
2663
- }
2664
-
2665
- for (const subscriber of subscribers) {
2666
2668
  try {
2667
- // Create subscriber from shared connection
2668
- const eventSubscriber = await Subscriber.fromConnection(connection);
2669
+ if (connectionString.startsWith('sns:')) {
2670
+ const { SNSConnection, snsUrl, subscribeHttpEndpoint } = await import('@geekmidas/events/sns');
2671
+ const { SnsPushSubscriberAdaptor } = await import('@geekmidas/constructs/aws');
2672
+ const address = snsUrl.parse(connectionString);
2673
+ const route = SUBSCRIBER_ROUTE(id);
2674
+ const adaptor = new SnsPushSubscriberAdaptor(envParser, subscriber, {
2675
+ topicArn: address.topicArn,
2676
+ // An emulator signs nothing. Decided by the address this server was
2677
+ // configured with, never by anything a request says about itself.
2678
+ verify: !address.endpoint,
2679
+ ...(address.endpoint ? { emulatorEndpoint: address.endpoint } : {}),
2680
+ });
2681
+
2682
+ app.post(route, async (c: any) => {
2683
+ const body = JSON.parse(await c.req.text());
2684
+ const response = await adaptor.handle(body);
2685
+ return c.json(response.body, response.status);
2686
+ });
2687
+
2688
+ afterListening.push(async (port) => {
2689
+ const base =
2690
+ process.env.GKM_SUBSCRIBER_PUSH_URL ??
2691
+ (address.endpoint ? \`http://host.docker.internal:\${port}\` : undefined);
2692
+ if (!base) {
2693
+ logger.error(
2694
+ { subscriber: id },
2695
+ 'Set GKM_SUBSCRIBER_PUSH_URL to the public URL of this worker, which SNS pushes to',
2696
+ );
2697
+ return;
2698
+ }
2699
+ const connection = await SNSConnection.fromConnectionString(connectionString);
2700
+ await subscribeHttpEndpoint(connection, {
2701
+ endpoint: new URL(route, base).toString(),
2702
+ events,
2703
+ });
2704
+ logger.info({ subscriber: id, events }, 'Subscriber subscribed for push');
2705
+ });
2706
+ continue;
2707
+ }
2669
2708
 
2670
- // Register services
2709
+ let connection = connections.get(connectionString);
2710
+ if (!connection) {
2711
+ connection = await EventConnectionFactory.fromConnectionString(connectionString);
2712
+ connections.set(connectionString, connection);
2713
+ }
2714
+ // Named, so a broker with one address for everything (pg-boss) gives
2715
+ // this subscriber a queue of its own: fan-out, not competition.
2716
+ const eventSubscriber = await Subscriber.fromConnection(connection, {
2717
+ topic,
2718
+ subscription: id,
2719
+ });
2671
2720
  const services = subscriber.services.length > 0
2672
2721
  ? await serviceDiscovery.register(subscriber.services)
2673
2722
  : {};
2674
2723
 
2675
- // Subscribe to events
2676
- const subscribedEvents = subscriber.subscribedEvents || [];
2677
-
2678
- if (subscribedEvents.length === 0) {
2679
- logger.warn({ subscriber: subscriber.constructor.name }, 'Subscriber has no subscribed events, skipping');
2680
- continue;
2681
- }
2682
-
2683
- await eventSubscriber.subscribe(subscribedEvents, async (event) => {
2724
+ await eventSubscriber.subscribe(events, async (event) => {
2684
2725
  try {
2685
- // Process single event (batch of 1)
2686
2726
  await subscriber.handler({
2687
2727
  events: [event],
2688
2728
  services: services as any,
2689
2729
  logger: subscriber.logger,
2690
2730
  });
2691
-
2692
- logger.debug({ eventType: event.type }, 'Successfully processed event');
2693
2731
  } catch (error) {
2694
- logger.error({ error, event }, 'Failed to process event');
2695
- // Event will become visible again for retry
2732
+ logger.error({ error, event, subscriber: id }, 'Failed to process event');
2733
+ // Rethrown so the transport keeps the event for a retry — as a queue
2734
+ // consumer's failure does, and as a pushed subscriber's 500 does.
2735
+ throw error;
2696
2736
  }
2697
2737
  });
2698
-
2699
- activeSubscribers.push(eventSubscriber);
2700
-
2701
- logger.info(
2702
- {
2703
- events: subscribedEvents,
2704
- },
2705
- 'Subscriber started polling'
2706
- );
2738
+ logger.info({ subscriber: id, events }, 'Subscriber started polling');
2707
2739
  } catch (error) {
2708
- logger.error({ error, subscriber: subscriber.constructor.name }, 'Failed to setup subscriber');
2740
+ logger.error({ error, subscriber: id }, 'Failed to set up subscriber');
2709
2741
  }
2710
2742
  }
2711
2743
 
2712
- // Setup graceful shutdown
2713
2744
  const shutdown = () => {
2714
- logger.info('Stopping all subscribers');
2715
- for (const eventSubscriber of activeSubscribers) {
2716
- connection.stop();
2745
+ for (const connection of connections.values()) void connection.close();
2746
+ };
2747
+ process.once('SIGTERM', shutdown);
2748
+ process.once('SIGINT', shutdown);
2749
+
2750
+ return async (port) => {
2751
+ if (afterListening.length === 0) return;
2752
+ await listening(port);
2753
+ for (const subscribe of afterListening) {
2754
+ await subscribe(port).catch((error) => {
2755
+ logger.error({ error }, 'Failed to subscribe for push');
2756
+ });
2717
2757
  }
2718
2758
  };
2719
-
2720
- process.on('SIGTERM', shutdown);
2721
- process.on('SIGINT', shutdown);
2722
2759
  }
2723
2760
  `;
2724
2761
  await (0, node_fs_promises.writeFile)(subscribersPath, content);
@@ -8654,11 +8691,7 @@ function generateConnectionUrls(services) {
8654
8691
  if (services.postgres) urls.DATABASE_URL = generatePostgresUrl(services.postgres);
8655
8692
  if (services.redis) urls.REDIS_URL = generateRedisUrl(services.redis);
8656
8693
  if (services.minio) urls.STORAGE_ENDPOINT = generateMinioEndpoint(services.minio);
8657
- if (services.pgboss) {
8658
- const pgbossUrl = generatePgBossUrl(services.pgboss);
8659
- urls.EVENT_PUBLISHER_CONNECTION_STRING = pgbossUrl;
8660
- urls.EVENT_SUBSCRIBER_CONNECTION_STRING = pgbossUrl;
8661
- }
8694
+ if (services.pgboss) urls.EVENT_PUBLISHER_CONNECTION_STRING = generatePgBossUrl(services.pgboss);
8662
8695
  if (services.mailpit) {
8663
8696
  urls.SMTP_HOST = services.mailpit.host;
8664
8697
  urls.SMTP_PORT = String(services.mailpit.port);
@@ -9555,26 +9588,26 @@ function loadPackageJson() {
9555
9588
  * Run: pnpm --filter @geekmidas/cli sync-versions
9556
9589
  */
9557
9590
  const GEEKMIDAS_VERSIONS = {
9558
- "@geekmidas/audit": "~10.0.0-alpha.39",
9559
- "@geekmidas/auth": "~10.0.0-alpha.39",
9560
- "@geekmidas/cache": "~10.0.0-alpha.39",
9561
- "@geekmidas/client": "~10.0.0-alpha.39",
9562
- "@geekmidas/cloud": "~10.0.0-alpha.39",
9563
- "@geekmidas/constructs": "~10.0.0-alpha.39",
9564
- "@geekmidas/db": "~10.0.0-alpha.39",
9565
- "@geekmidas/emailkit": "~10.0.0-alpha.39",
9566
- "@geekmidas/envkit": "~10.0.0-alpha.39",
9567
- "@geekmidas/errors": "~10.0.0-alpha.39",
9568
- "@geekmidas/events": "~10.0.0-alpha.39",
9569
- "@geekmidas/logger": "~10.0.0-alpha.39",
9570
- "@geekmidas/manifest": "~10.0.0-alpha.39",
9571
- "@geekmidas/rate-limit": "~10.0.0-alpha.39",
9572
- "@geekmidas/schema": "~10.0.0-alpha.39",
9573
- "@geekmidas/services": "~10.0.0-alpha.39",
9574
- "@geekmidas/storage": "~10.0.0-alpha.39",
9575
- "@geekmidas/studio": "~10.0.0-alpha.39",
9576
- "@geekmidas/telescope": "~10.0.0-alpha.39",
9577
- "@geekmidas/testkit": "~10.0.0-alpha.39",
9591
+ "@geekmidas/audit": "~10.0.0-alpha.40",
9592
+ "@geekmidas/auth": "~10.0.0-alpha.40",
9593
+ "@geekmidas/cache": "~10.0.0-alpha.40",
9594
+ "@geekmidas/client": "~10.0.0-alpha.40",
9595
+ "@geekmidas/cloud": "~10.0.0-alpha.40",
9596
+ "@geekmidas/constructs": "~10.0.0-alpha.40",
9597
+ "@geekmidas/db": "~10.0.0-alpha.40",
9598
+ "@geekmidas/emailkit": "~10.0.0-alpha.40",
9599
+ "@geekmidas/envkit": "~10.0.0-alpha.40",
9600
+ "@geekmidas/errors": "~10.0.0-alpha.40",
9601
+ "@geekmidas/events": "~10.0.0-alpha.40",
9602
+ "@geekmidas/logger": "~10.0.0-alpha.40",
9603
+ "@geekmidas/manifest": "~10.0.0-alpha.40",
9604
+ "@geekmidas/rate-limit": "~10.0.0-alpha.40",
9605
+ "@geekmidas/schema": "~10.0.0-alpha.40",
9606
+ "@geekmidas/services": "~10.0.0-alpha.40",
9607
+ "@geekmidas/storage": "~10.0.0-alpha.40",
9608
+ "@geekmidas/studio": "~10.0.0-alpha.40",
9609
+ "@geekmidas/telescope": "~10.0.0-alpha.40",
9610
+ "@geekmidas/testkit": "~10.0.0-alpha.40",
9578
9611
  "@geekmidas/cli": `~${loadPackageJson().version}`
9579
9612
  };
9580
9613
  //#endregion
@@ -16323,7 +16356,7 @@ function testDatabases(reconciled) {
16323
16356
  * starts without the reference data the app needs.
16324
16357
  */
16325
16358
  async function migrateTestStage(workspace, env) {
16326
- const { migrateAndSeed } = await Promise.resolve().then(() => require("./migrate-Bu8POqsR.cjs"));
16359
+ const { migrateAndSeed } = await Promise.resolve().then(() => require("./migrate-CFOUApUt.cjs"));
16327
16360
  const { discover } = await Promise.resolve().then(() => require("./discover-B0f_kuNN.cjs")).then((n) => n.discover_exports);
16328
16361
  const sources = {};
16329
16362
  const manifest = await discover({
@@ -16723,7 +16756,7 @@ program.command("build").description("Build handlers and the manifest from decla
16723
16756
  process.exit(1);
16724
16757
  }
16725
16758
  });
16726
- program.command("dev").description("Reconcile constructs and start the dev server with automatic reload").option("-p, --port <port>", "Port to run the development server on").option("--entry <file>", "Entry file to run (bypasses gkm config)").option("--watch", "Watch for file changes (default: true with --entry)").option("--no-watch", "Disable file watching").option("--enable-openapi", "Enable OpenAPI documentation for development server", true).option("--migrate", "Apply pending migrations before the apps start").option("--seed", "Migrate, then run the seeds, before the apps start").option("--fake", "Call each external API’s fake (test/fakes/<id>.ts) instead of the provider").action(async (options) => {
16759
+ program.command("dev").description("Reconcile constructs and start the dev server with automatic reload").option("-p, --port <port>", "Port to run the development server on").option("--entry <file>", "Entry file to run (bypasses gkm config)").option("--watch", "Watch for file changes (default: true with --entry)").option("--no-watch", "Disable file watching").option("--enable-openapi", "Enable OpenAPI documentation for development server", true).option("--migrate", "Apply pending migrations before the apps start").option("--seed", "Migrate, then run the seeds, before the apps start").option("--fake", "Call each external API’s fake (test/fakes/<id>.ts) instead of the provider").option("--no-subscribers", "Run no topic subscribers: nothing is subscribed, polled or pushed to").action(async (options) => {
16727
16760
  try {
16728
16761
  const globalOptions = program.opts();
16729
16762
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
@@ -16735,7 +16768,8 @@ program.command("dev").description("Reconcile constructs and start the dev serve
16735
16768
  watch: options.watch,
16736
16769
  migrate: options.migrate,
16737
16770
  seed: options.seed,
16738
- fake: options.fake
16771
+ fake: options.fake,
16772
+ subscribers: options.subscribers
16739
16773
  });
16740
16774
  } catch (error) {
16741
16775
  console.error(formatError(error));
@@ -16769,7 +16803,7 @@ program.command("migrate").description("Apply each database construct's migratio
16769
16803
  try {
16770
16804
  const globalOptions = program.opts();
16771
16805
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16772
- const { migrateCommand } = await Promise.resolve().then(() => require("./migrate-Bu8POqsR.cjs"));
16806
+ const { migrateCommand } = await Promise.resolve().then(() => require("./migrate-CFOUApUt.cjs"));
16773
16807
  await migrateCommand({
16774
16808
  ...options,
16775
16809
  ...construct ? { construct } : {}
@@ -16783,7 +16817,7 @@ program.command("seed").description("Migrate, then run each database construct's
16783
16817
  try {
16784
16818
  const globalOptions = program.opts();
16785
16819
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16786
- const { seedCommand } = await Promise.resolve().then(() => require("./migrate-Bu8POqsR.cjs"));
16820
+ const { seedCommand } = await Promise.resolve().then(() => require("./migrate-CFOUApUt.cjs"));
16787
16821
  await seedCommand({
16788
16822
  ...options,
16789
16823
  ...construct ? { construct } : {}
@@ -16797,7 +16831,7 @@ program.command("migration").description("Write a construct's next migration: an
16797
16831
  try {
16798
16832
  const globalOptions = program.opts();
16799
16833
  if (globalOptions.cwd) process.chdir(globalOptions.cwd);
16800
- const { migrationCommand } = await Promise.resolve().then(() => require("./migrate-Bu8POqsR.cjs"));
16834
+ const { migrationCommand } = await Promise.resolve().then(() => require("./migrate-CFOUApUt.cjs"));
16801
16835
  await migrationCommand({
16802
16836
  construct,
16803
16837
  ...name ? { name } : {}
@@ -16936,7 +16970,7 @@ program.command("secrets:reconcile").description("Backfill missing custom secret
16936
16970
  console.error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
16937
16971
  process.exit(1);
16938
16972
  }
16939
- const { derivedContainers } = await Promise.resolve().then(() => require("./workspace-DZ1G4zow.cjs")).then((n) => n.workspace_exports);
16973
+ const { derivedContainers } = await Promise.resolve().then(() => require("./workspace-BIqeeqcR.cjs")).then((n) => n.workspace_exports);
16940
16974
  const result = reconcileMissingSecrets(secrets, workspace, await derivedContainers(workspace, stage));
16941
16975
  if (!result) {
16942
16976
  console.log(`\n✓ Secrets for stage "${stage}" are up-to-date`);