@dunx/create-app 1.2.0 → 1.3.0

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": "@dunx/create-app",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Scaffold a new dunx application - bunx @dunx/create-app my-api",
5
5
  "keywords": [
6
6
  "bun",
@@ -80,9 +80,14 @@ export class DocsDemo {
80
80
  // markup, with both script bodies removed. Inside a <script> everything is
81
81
  // text, and minified React's own string table contains `src=` and `<script`.
82
82
  const shell = html.replace(/(<script[^>]*>)[\s\S]*?(<\/script>)/g, '$1$2');
83
+ // A `<link>` counts only if it would actually fetch. The page carries one
84
+ // for its favicon, as a `data:` URI, which the browser never requests.
85
+ const fetchedLink = [...shell.matchAll(/<link\b[^>]*href="([^"]*)"/g)].some(
86
+ ([, href]) => href !== undefined && !href.startsWith('data:'),
87
+ );
83
88
  const external =
84
89
  /\ssrc=/.test(shell) ||
85
- /<link\b/.test(shell) ||
90
+ fetchedLink ||
86
91
  /url\(\s*["']?(https?:)?\/\//.test(html) ||
87
92
  html.includes('//cdn');
88
93
  logger.info(
@@ -26,7 +26,7 @@ const oneJob = { params: z.object({ id: z.string().min(1) }) } as const;
26
26
  /**
27
27
  * The publish side. Nothing here consumes: `QueueModule.forRoot` binds
28
28
  * `JobPublisher` and no worker, so this process enqueues and returns immediately.
29
- * Run `bun run worker` to consume.
29
+ * Consumed by this same process - see `JobsModule`'s `consume: true`.
30
30
  */
31
31
  @Controller('jobs')
32
32
  export class JobsController {
@@ -10,10 +10,13 @@ import { ThumbnailJobs } from './thumbnail.jobs.js';
10
10
  * process publishes, a separate worker process consumes, and they agree only on
11
11
  * this module.
12
12
  *
13
- * `QueueModule.forRoot` binds the publish side alone, so importing it does not
14
- * open a worker - a web process that publishes never consumes by accident.
13
+ * `consume: true` is what makes this process work them as well as publish, and it
14
+ * is the only line about it anywhere - the container owns starting and stopping the
15
+ * workers, so no entrypoint has to. Leave it out and the module binds the publish
16
+ * side alone, which is what a web tier with a separate worker fleet wants.
17
+ *
15
18
  * `PicturesModule` is here because the handler injects `Thumbnails`, and the
16
- * worker's container has to be able to build it.
19
+ * container that runs it has to be able to build it.
17
20
  */
18
21
  @Module({
19
22
  imports: [
@@ -23,6 +26,13 @@ import { ThumbnailJobs } from './thumbnail.jobs.js';
23
26
  return {
24
27
  ...(url === undefined ? {} : { url }),
25
28
  prefix: 'dunx-full',
29
+ // This process works its own queues. The container starts the workers at
30
+ // onInit and stops them at onShutdown - before the database they use -
31
+ // so `main.ts` says nothing about queues and there is no second command.
32
+ consume: true,
33
+ // The file bullmq forks into for a queue whose handler is marked
34
+ // `background`. Absolute, because the child resolves it, not this module.
35
+ processor: new URL('./jobs.processor.ts', import.meta.url).pathname,
26
36
  };
27
37
  },
28
38
  inject: [AppConfigService] as const,
@@ -0,0 +1,37 @@
1
+ import { JobProcessor } from '@dunx/infra/queue';
2
+ import { ConfigModule, Module } from '@dunx/core';
3
+ import { LoggerModule } from '@dunx/infra/logger';
4
+ import { AppConfigService, validate } from '../config.js';
5
+ import { JobsModule } from './jobs.module.js';
6
+
7
+ /**
8
+ * **The file bullmq forks into.** Its default export is the processor, and nothing
9
+ * else here runs in the parent.
10
+ *
11
+ * The child builds its own container, which is the whole point: a handler gets the
12
+ * database, the image pipeline and the logger it declares, without sharing an event
13
+ * loop with the process serving HTTP. `JobProcessor` builds it once per child and
14
+ * reuses it for every job on that child.
15
+ *
16
+ * Its own module rather than reusing `WorkerModule` from `worker.ts`: that file is
17
+ * an entrypoint with a `run()` at the bottom, and importing it here would boot a
18
+ * second worker inside every child.
19
+ */
20
+ @Module({
21
+ imports: [
22
+ ConfigModule.forRoot({ validate, as: AppConfigService }),
23
+ LoggerModule.forRootAsync({
24
+ useFactory: (config: AppConfigService) => ({
25
+ // Named so a line from a child is attributable to one on sight - which is
26
+ // the traceability a sandbox is for.
27
+ name: `${config.get('appName')}-job`,
28
+ level: config.get('log').level,
29
+ }),
30
+ inject: [AppConfigService] as const,
31
+ }),
32
+ JobsModule,
33
+ ],
34
+ })
35
+ class JobProcessorModule {}
36
+
37
+ export default new JobProcessor(JobProcessorModule).handle;
@@ -32,7 +32,10 @@ export class ThumbnailJobs {
32
32
  private readonly logger: Logger,
33
33
  ) {}
34
34
 
35
- @JobHandler({ queue: THUMBNAIL_QUEUE, name: 'render' })
35
+ // `background: true` puts this queue's jobs in a forked child: a slow or
36
+ // crashing render cannot take the server with it, and its log lines still land
37
+ // in this process's stream.
38
+ @JobHandler({ queue: THUMBNAIL_QUEUE, name: 'render', background: true })
36
39
  async render(job: Job<RenderRequest>): Promise<RenderResult> {
37
40
  const encoded = await this.thumbnails.render({
38
41
  width: job.data.width,
@@ -45,8 +48,7 @@ export class ThumbnailJobs {
45
48
  height: encoded.height,
46
49
  bytes: encoded.bytes.byteLength,
47
50
  };
48
- // Written by the worker process, so seeing this line is how you know the job
49
- // did not run in the web process.
51
+ // Written in the child, and visible here: that is the point of the sandbox.
50
52
  this.logger.info(`rendered job ${job.id ?? '?'}`, result);
51
53
  return result;
52
54
  }