@strav/cli 0.4.17 → 0.4.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strav/cli",
3
- "version": "0.4.17",
3
+ "version": "0.4.25",
4
4
  "type": "module",
5
5
  "description": "CLI framework and code generators for the Strav framework",
6
6
  "license": "MIT",
@@ -34,11 +34,11 @@
34
34
  "strav": "./src/cli/strav.ts"
35
35
  },
36
36
  "peerDependencies": {
37
- "@strav/kernel": "0.4.17",
38
- "@strav/http": "0.4.17",
39
- "@strav/database": "0.4.17",
40
- "@strav/queue": "0.4.17",
41
- "@strav/signal": "0.4.17"
37
+ "@strav/kernel": "0.4.25",
38
+ "@strav/http": "0.4.25",
39
+ "@strav/database": "0.4.25",
40
+ "@strav/queue": "0.4.25",
41
+ "@strav/signal": "0.4.25"
42
42
  },
43
43
  "dependencies": {
44
44
  "chalk": "^5.6.2",
@@ -41,12 +41,23 @@ export async function shutdown(db: Database): Promise<void> {
41
41
  await db.close()
42
42
  }
43
43
 
44
+ export interface WithProvidersOptions {
45
+ /**
46
+ * Install SIGINT/SIGTERM handlers for graceful shutdown. Default: `true`.
47
+ * Set to `false` when the caller owns signal handling and must control
48
+ * shutdown ordering — e.g. `queue:work`, which lets the worker drain its
49
+ * in-flight job before `app.shutdown()` tears down providers.
50
+ */
51
+ signalHandlers?: boolean
52
+ }
53
+
44
54
  /**
45
55
  * Bootstrap an Application with the given service providers.
46
56
  *
47
57
  * Creates a fresh Application, registers all providers, boots them
48
58
  * in dependency order, and returns the running application.
49
- * Signal handlers for graceful shutdown are installed automatically.
59
+ * Signal handlers for graceful shutdown are installed automatically
60
+ * unless `options.signalHandlers` is `false`.
50
61
  *
51
62
  * @example
52
63
  * const app = await withProviders([
@@ -55,9 +66,12 @@ export async function shutdown(db: Database): Promise<void> {
55
66
  * new AuthProvider({ resolver: (id) => User.find(id) }),
56
67
  * ])
57
68
  */
58
- export async function withProviders(providers: ServiceProvider[]): Promise<Application> {
69
+ export async function withProviders(
70
+ providers: ServiceProvider[],
71
+ options?: WithProvidersOptions,
72
+ ): Promise<Application> {
59
73
  const app = new Application()
60
74
  for (const provider of providers) app.use(provider)
61
- await app.start()
75
+ await app.start(options)
62
76
  return app
63
77
  }
package/src/cli/index.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { bootstrap, shutdown, withProviders } from './bootstrap'
2
- export type { BootstrapResult } from './bootstrap'
2
+ export type { BootstrapResult, WithProvidersOptions } from './bootstrap'
3
3
  export { default as CommandLoader } from './command_loader'
@@ -1,9 +1,21 @@
1
1
  import type { Command } from 'commander'
2
2
  import chalk from 'chalk'
3
- import { bootstrap, shutdown } from '../cli/bootstrap.ts'
3
+ import path from 'node:path'
4
+ import { bootstrap, shutdown, withProviders } from '../cli/bootstrap.ts'
5
+ import type Application from '@strav/kernel/core/application'
6
+ import type ServiceProvider from '@strav/kernel/core/service_provider'
7
+ import type Database from '@strav/database/database/database'
4
8
  import Queue from '@strav/queue/queue/queue'
9
+ import QueueProvider from '@strav/queue/providers/queue_provider'
5
10
  import Worker from '@strav/queue/queue/worker'
6
11
 
12
+ /**
13
+ * Conventional file the worker loads on start. Its top-level `Queue.handle(...)`
14
+ * calls register the job handlers, and its `providers` export lists the service
15
+ * providers the worker boots — mirroring `start/providers.ts` for the web entry.
16
+ */
17
+ const JOBS_FILE = 'start/jobs.ts'
18
+
7
19
  export function register(program: Command): void {
8
20
  program
9
21
  .command('queue:work')
@@ -11,19 +23,67 @@ export function register(program: Command): void {
11
23
  .option('--queue <name>', 'Queue to process', 'default')
12
24
  .option('--sleep <ms>', 'Poll interval in milliseconds', '1000')
13
25
  .action(async options => {
14
- let db
26
+ const queue = options.queue
27
+ const sleep = parseInt(options.sleep)
28
+
29
+ let app: Application | undefined
30
+ let db: Database | undefined
31
+
15
32
  try {
16
- const { db: database, config } = await bootstrap()
17
- db = database
33
+ const jobsPath = path.resolve(JOBS_FILE)
34
+
35
+ if (await Bun.file(jobsPath).exists()) {
36
+ // Importing start/jobs.ts runs its top-level Queue.handle(...) calls
37
+ // and exposes the provider list the worker needs. Booting those
38
+ // providers wires the facades (mail, notification, …) that handlers
39
+ // depend on — bootstrap() alone leaves them unregistered.
40
+ const jobs = await import(jobsPath)
41
+ const providers: ServiceProvider[] = Array.isArray(jobs.providers)
42
+ ? [...jobs.providers]
43
+ : []
44
+
45
+ if (providers.length === 0) {
46
+ throw new Error(
47
+ `${JOBS_FILE} must export a \`providers\` array — the service ` +
48
+ `providers the worker boots (ConfigProvider, DatabaseProvider, ` +
49
+ `QueueProvider, plus any facade providers your handlers use).`,
50
+ )
51
+ }
52
+
53
+ // QueueProvider wires the Queue singleton and ensures the tables —
54
+ // add it if the app's provider list left it out.
55
+ if (!providers.some(p => p.name === 'queue')) {
56
+ providers.push(new QueueProvider())
57
+ }
18
58
 
19
- new Queue(db, config)
20
- await Queue.ensureTables()
59
+ // signalHandlers: false — the Worker installs its own SIGINT/SIGTERM
60
+ // handlers that drain the in-flight job before start() returns. We
61
+ // call app.shutdown() only afterwards (in finally), so providers are
62
+ // never torn down while a job is still running.
63
+ app = await withProviders(providers, { signalHandlers: false })
64
+ } else {
65
+ console.warn(
66
+ chalk.yellow(`No ${JOBS_FILE} found — the worker has no registered handlers.`),
67
+ )
68
+ console.warn(
69
+ chalk.dim(
70
+ ` Create ${JOBS_FILE} with your Queue.handle(...) calls and a ` +
71
+ `\`providers\` export, or every dispatched job will fail.`,
72
+ ),
73
+ )
74
+ const { db: database, config } = await bootstrap()
75
+ db = database
76
+ new Queue(db, config)
77
+ await Queue.ensureTables()
78
+ }
21
79
 
22
- const queue = options.queue
23
- const sleep = parseInt(options.sleep)
80
+ const handlers = [...Queue.handlers.keys()]
24
81
 
25
82
  console.log(chalk.cyan(`Worker starting on queue "${queue}"...`))
26
83
  console.log(chalk.dim(` poll interval: ${sleep}ms`))
84
+ console.log(
85
+ chalk.dim(` handlers: ${handlers.length > 0 ? handlers.join(', ') : 'none'}`),
86
+ )
27
87
  console.log(chalk.dim(' Press Ctrl+C to stop.\n'))
28
88
 
29
89
  const worker = new Worker({ queue, sleep })
@@ -34,6 +94,7 @@ export function register(program: Command): void {
34
94
  console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
35
95
  process.exit(1)
36
96
  } finally {
97
+ if (app) await app.shutdown()
37
98
  if (db) await shutdown(db)
38
99
  }
39
100
  })