@velajs/vela 1.12.0 → 1.14.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +101 -1
  2. package/dist/application.d.ts +17 -0
  3. package/dist/application.js +94 -3
  4. package/dist/container/container.d.ts +21 -1
  5. package/dist/container/container.js +89 -2
  6. package/dist/container/types.d.ts +27 -0
  7. package/dist/discovery/discovery.service.d.ts +8 -0
  8. package/dist/discovery/discovery.service.js +11 -0
  9. package/dist/entrypoint/entrypoint.registry.d.ts +3 -1
  10. package/dist/entrypoint/entrypoint.registry.js +9 -3
  11. package/dist/event-emitter/event-emitter.module.js +1 -0
  12. package/dist/factory/bootstrap.js +4 -0
  13. package/dist/http/route.manager.js +5 -0
  14. package/dist/i18n/i18n.module.js +5 -0
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.js +1 -1
  17. package/dist/module/decorators.js +4 -2
  18. package/dist/module/define-module.d.ts +7 -0
  19. package/dist/module/define-module.js +9 -4
  20. package/dist/module/lazy-modules.d.ts +82 -0
  21. package/dist/module/lazy-modules.js +231 -0
  22. package/dist/module/module-loader.d.ts +21 -0
  23. package/dist/module/module-loader.js +99 -24
  24. package/dist/pipeline/index.d.ts +2 -0
  25. package/dist/pipeline/index.js +1 -0
  26. package/dist/pipeline/scoped-components.d.ts +26 -0
  27. package/dist/pipeline/scoped-components.js +27 -0
  28. package/dist/queue/index.d.ts +10 -0
  29. package/dist/queue/index.js +12 -0
  30. package/dist/queue/inline.driver.d.ts +30 -0
  31. package/dist/queue/inline.driver.js +58 -0
  32. package/dist/queue/queue.binding.d.ts +23 -0
  33. package/dist/queue/queue.binding.js +53 -0
  34. package/dist/queue/queue.client.d.ts +20 -0
  35. package/dist/queue/queue.client.js +33 -0
  36. package/dist/queue/queue.decorators.d.ts +23 -0
  37. package/dist/queue/queue.decorators.js +46 -0
  38. package/dist/queue/queue.dispatch.d.ts +36 -0
  39. package/dist/queue/queue.dispatch.js +102 -0
  40. package/dist/queue/queue.module.d.ts +32 -0
  41. package/dist/queue/queue.module.js +80 -0
  42. package/dist/queue/queue.tokens.d.ts +21 -0
  43. package/dist/queue/queue.tokens.js +34 -0
  44. package/dist/queue/queue.types.d.ts +61 -0
  45. package/dist/queue/queue.types.js +4 -0
  46. package/dist/registry/types.d.ts +10 -0
  47. package/dist/schedule/schedule.module.js +3 -0
  48. package/dist/seeder/seeder.module.js +3 -0
  49. package/package.json +5 -1
@@ -0,0 +1,26 @@
1
+ import type { Container } from '../container/container';
2
+ import type { ComponentType, Constructor } from '../registry/types';
3
+ import type { CanActivate, ExceptionFilter, NestInterceptor, NestMiddleware, PipeTransform } from './types';
4
+ export interface ResolvedComponentMap {
5
+ guard: CanActivate;
6
+ pipe: PipeTransform;
7
+ interceptor: NestInterceptor;
8
+ filter: ExceptionFilter;
9
+ middleware: NestMiddleware;
10
+ }
11
+ /**
12
+ * Public seam for custom dispatchers: the `@UseGuards`/`@UsePipes`/
13
+ * `@UseInterceptors`/`@UseFilters` components scoped to a handler (class-level
14
+ * then method-level, declaration order), resolved to instances through the
15
+ * given container (classes constructed with DI; instances passed through).
16
+ *
17
+ * Order is preserved exactly as declared — conventions stay with the CALLER:
18
+ * filters run closest-first, so dispatchers reverse them
19
+ * (`resolveScopedComponents('filter', ...).reverse()`), and app-wide `APP_*`
20
+ * components are a separate, transport-specific decision.
21
+ *
22
+ * Promoted to the public API by the QueueModule work (the "openness proof"):
23
+ * it was the one capability custom entrypoint dispatchers needed that only
24
+ * the internal `ComponentManager` provided.
25
+ */
26
+ export declare function resolveScopedComponents<T extends ComponentType>(type: T, targetClass: Constructor, methodName: string | symbol, container: Container): ResolvedComponentMap[T][];
@@ -0,0 +1,27 @@
1
+ import { ComponentManager } from "./component.manager.js";
2
+ const RESOLVERS = {
3
+ guard: ComponentManager.resolveGuards.bind(ComponentManager),
4
+ pipe: ComponentManager.resolvePipes.bind(ComponentManager),
5
+ interceptor: ComponentManager.resolveInterceptors.bind(ComponentManager),
6
+ filter: ComponentManager.resolveFilters.bind(ComponentManager),
7
+ middleware: ComponentManager.resolveMiddleware.bind(ComponentManager)
8
+ };
9
+ /**
10
+ * Public seam for custom dispatchers: the `@UseGuards`/`@UsePipes`/
11
+ * `@UseInterceptors`/`@UseFilters` components scoped to a handler (class-level
12
+ * then method-level, declaration order), resolved to instances through the
13
+ * given container (classes constructed with DI; instances passed through).
14
+ *
15
+ * Order is preserved exactly as declared — conventions stay with the CALLER:
16
+ * filters run closest-first, so dispatchers reverse them
17
+ * (`resolveScopedComponents('filter', ...).reverse()`), and app-wide `APP_*`
18
+ * components are a separate, transport-specific decision.
19
+ *
20
+ * Promoted to the public API by the QueueModule work (the "openness proof"):
21
+ * it was the one capability custom entrypoint dispatchers needed that only
22
+ * the internal `ComponentManager` provided.
23
+ */ export function resolveScopedComponents(type, targetClass, methodName, container) {
24
+ const scoped = ComponentManager.getScopedComponents(type, targetClass, methodName);
25
+ const resolve = RESOLVERS[type];
26
+ return resolve(scoped, container);
27
+ }
@@ -0,0 +1,10 @@
1
+ export { QueueModule, QUEUE_MODULE_OPTIONS } from './queue.module';
2
+ export { Processor, Process, getProcessHandlers } from './queue.decorators';
3
+ export { queueToken, QUEUE_DRIVER, PROCESSOR_METADATA, PROCESS_METADATA } from './queue.tokens';
4
+ export { QueueClient } from './queue.client';
5
+ export { QueueDispatchBinding } from './queue.binding';
6
+ export { dispatchQueueJob } from './queue.dispatch';
7
+ export type { QueueDispatchResult, QueueEntry } from './queue.dispatch';
8
+ export { inline } from './inline.driver';
9
+ export type { InlineQueueDriver, InlineQueueOptions } from './inline.driver';
10
+ export type { AddJobOptions, ProcessMetadata, ProcessorMetadata, QueueDispatchFn, QueueDriver, QueueDriverBindHooks, QueueJob, QueueModuleOptions, } from './queue.types';
@@ -0,0 +1,12 @@
1
+ // @velajs/vela/queue — first-party queue subsystem, authored entirely on the
2
+ // public API (every vela import in src/queue/* comes from '../index'; the
3
+ // openness audit test enforces it). Deliberately NOT re-exported from the
4
+ // main barrel: @velajs/cloudflare already exports an (unrelated) QueueModule
5
+ // for the CF Queues binding, and subpath-only avoids the collision.
6
+ export { QueueModule, QUEUE_MODULE_OPTIONS } from "./queue.module.js";
7
+ export { Processor, Process, getProcessHandlers } from "./queue.decorators.js";
8
+ export { queueToken, QUEUE_DRIVER, PROCESSOR_METADATA, PROCESS_METADATA } from "./queue.tokens.js";
9
+ export { QueueClient } from "./queue.client.js";
10
+ export { QueueDispatchBinding } from "./queue.binding.js";
11
+ export { dispatchQueueJob } from "./queue.dispatch.js";
12
+ export { inline } from "./inline.driver.js";
@@ -0,0 +1,30 @@
1
+ import type { QueueDriver } from './queue.types';
2
+ export interface InlineQueueOptions {
3
+ /**
4
+ * `immediate` (default): deliver on a microtask after `enqueue` resolves —
5
+ * `add()` resolving does NOT mean the job was handled. Handler errors are
6
+ * routed to the binding's error hook (container diagnostics), never thrown
7
+ * into the detached microtask.
8
+ *
9
+ * `manual`: buffer until `flush()` — deterministic delivery for tests.
10
+ */
11
+ mode?: 'immediate' | 'manual';
12
+ }
13
+ export interface InlineQueueDriver extends QueueDriver {
14
+ /**
15
+ * Deliver everything buffered (manual mode; also drains jobs enqueued
16
+ * before the driver was bound). Resolves with the delivered count; rejects
17
+ * with an `AggregateError` after attempting ALL buffered jobs if any
18
+ * handler error went unclaimed — the awaiter exists here, so the
19
+ * platform-retry rethrow contract is meaningful.
20
+ */
21
+ flush(): Promise<number>;
22
+ /** Jobs currently buffered (unbound immediate + all manual). */
23
+ readonly size: number;
24
+ }
25
+ /**
26
+ * In-process driver for dev, tests, and single-isolate apps. Edge-pure: no
27
+ * timers — `immediate` mode uses `queueMicrotask`. `delayMs` is not
28
+ * supported (warns once through the bound error hook's diagnostics side).
29
+ */
30
+ export declare function inline(options?: InlineQueueOptions): InlineQueueDriver;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * In-process driver for dev, tests, and single-isolate apps. Edge-pure: no
3
+ * timers — `immediate` mode uses `queueMicrotask`. `delayMs` is not
4
+ * supported (warns once through the bound error hook's diagnostics side).
5
+ */ export function inline(options = {}) {
6
+ const mode = options.mode ?? 'immediate';
7
+ const buffer = [];
8
+ let dispatch;
9
+ let onError;
10
+ let warnedDelay = false;
11
+ const deliverDetached = (job)=>{
12
+ queueMicrotask(()=>{
13
+ void dispatch(job).catch((error)=>onError?.(error, job));
14
+ });
15
+ };
16
+ return {
17
+ kind: 'inline',
18
+ get size () {
19
+ return buffer.length;
20
+ },
21
+ async enqueue (job, addOptions) {
22
+ if (addOptions?.delayMs !== undefined && !warnedDelay) {
23
+ warnedDelay = true;
24
+ console.warn(`[vela] inline() queue driver does not support delayMs — job '${job.name}' delivers without delay.`);
25
+ }
26
+ if (mode === 'manual' || !dispatch) {
27
+ buffer.push(job);
28
+ return;
29
+ }
30
+ deliverDetached(job);
31
+ },
32
+ bind (fn, hooks) {
33
+ dispatch = fn;
34
+ onError = hooks?.onError;
35
+ if (mode === 'immediate' && buffer.length > 0) {
36
+ for (const job of buffer.splice(0))deliverDetached(job);
37
+ }
38
+ },
39
+ async flush () {
40
+ if (!dispatch) return 0;
41
+ const jobs = buffer.splice(0);
42
+ const errors = [];
43
+ let delivered = 0;
44
+ for (const job of jobs){
45
+ try {
46
+ await dispatch(job);
47
+ delivered++;
48
+ } catch (error) {
49
+ errors.push(error);
50
+ }
51
+ }
52
+ if (errors.length > 0) {
53
+ throw new AggregateError(errors, `queue flush: ${errors.length} of ${jobs.length} jobs failed (${delivered} delivered)`);
54
+ }
55
+ return delivered;
56
+ }
57
+ };
58
+ }
@@ -0,0 +1,23 @@
1
+ import type { Container, DiscoveryService } from '../index';
2
+ import type { QueueDriver } from './queue.types';
3
+ /**
4
+ * Wires a `QueueModule` instance's driver to the app: binds in-process
5
+ * delivery to the dispatch core and validates that no other module instance
6
+ * provides the same queue names.
7
+ *
8
+ * Delivery resolves processors from the per-app `EntrypointRegistry` once it
9
+ * exists (registered into the container at the end of
10
+ * `callOnApplicationBootstrap`); deliveries that arrive earlier (a producer's
11
+ * `onModuleInit` calling `add()`) fall back to `DiscoveryService` with
12
+ * `deferLazy` — identical entries, no buffering, no lost jobs.
13
+ *
14
+ * Every `QueueClient` injects this binding, so the driver is always bound
15
+ * before the first `add()` — including when the module materializes lazily.
16
+ */
17
+ export declare class QueueDispatchBinding {
18
+ private readonly container;
19
+ private readonly discovery;
20
+ constructor(container: Container, discovery: DiscoveryService, driver: QueueDriver, queues: string[]);
21
+ private deliver;
22
+ private routeError;
23
+ }
@@ -0,0 +1,53 @@
1
+ import { EntrypointRegistry } from "../index.js";
2
+ import { dispatchJobToEntries } from "./queue.dispatch.js";
3
+ import { PROCESSOR_METADATA, queueToken } from "./queue.tokens.js";
4
+ /**
5
+ * Wires a `QueueModule` instance's driver to the app: binds in-process
6
+ * delivery to the dispatch core and validates that no other module instance
7
+ * provides the same queue names.
8
+ *
9
+ * Delivery resolves processors from the per-app `EntrypointRegistry` once it
10
+ * exists (registered into the container at the end of
11
+ * `callOnApplicationBootstrap`); deliveries that arrive earlier (a producer's
12
+ * `onModuleInit` calling `add()`) fall back to `DiscoveryService` with
13
+ * `deferLazy` — identical entries, no buffering, no lost jobs.
14
+ *
15
+ * Every `QueueClient` injects this binding, so the driver is always bound
16
+ * before the first `add()` — including when the module materializes lazily.
17
+ */ export class QueueDispatchBinding {
18
+ container;
19
+ discovery;
20
+ constructor(container, discovery, driver, queues){
21
+ this.container = container;
22
+ this.discovery = discovery;
23
+ for (const queue of queues){
24
+ const owners = container.getOwnerModuleIds(queueToken(queue));
25
+ if (owners.length > 1) {
26
+ throw new Error(`Queue '${queue}' is provided by multiple QueueModule instances (${owners.join(', ')}). ` + `Queue names must be unique per app — either dedup the forRoot options or rename the queue.`);
27
+ }
28
+ }
29
+ driver.bind?.((job)=>this.deliver(job), {
30
+ onError: (error, job)=>this.routeError(error, job)
31
+ });
32
+ }
33
+ async deliver(job) {
34
+ const entries = this.container.has(EntrypointRegistry) ? this.container.resolve(EntrypointRegistry).ofKind('queue').map((ep)=>({
35
+ token: ep.token,
36
+ meta: ep.meta
37
+ })) : this.discovery.providersWithMeta(PROCESSOR_METADATA, {
38
+ deferLazy: true
39
+ }).map((found)=>({
40
+ token: found.token,
41
+ meta: found.meta
42
+ }));
43
+ await dispatchJobToEntries(this.container, entries, job);
44
+ }
45
+ routeError(error, job) {
46
+ const mode = this.container.getDiagnostics();
47
+ if (mode === 'silent') return;
48
+ if (mode === 'throw') {
49
+ throw error instanceof Error ? error : new Error(String(error));
50
+ }
51
+ console.error(`[vela] unhandled error processing queue job '${job.name}' on '${job.queue}':`, error);
52
+ }
53
+ }
@@ -0,0 +1,20 @@
1
+ import type { AddJobOptions, QueueDriver, QueueJob } from './queue.types';
2
+ /**
3
+ * Producer handle for one named queue — inject via `queueToken(name)`:
4
+ *
5
+ * ```ts
6
+ * constructor(@Inject(queueToken('email')) private readonly email: QueueClient) {}
7
+ * await this.email.add('welcome', { userId });
8
+ * ```
9
+ *
10
+ * `add()` resolves when the DRIVER accepted the job, not when a processor
11
+ * handled it (the inline driver's `immediate` mode delivers on a following
12
+ * microtask; platform drivers deliver in another isolate entirely).
13
+ */
14
+ export declare class QueueClient {
15
+ private readonly queue;
16
+ private readonly driver;
17
+ constructor(queue: string, driver: QueueDriver);
18
+ get name(): string;
19
+ add<T>(jobName: string, data: T, options?: AddJobOptions): Promise<QueueJob<T>>;
20
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Producer handle for one named queue — inject via `queueToken(name)`:
3
+ *
4
+ * ```ts
5
+ * constructor(@Inject(queueToken('email')) private readonly email: QueueClient) {}
6
+ * await this.email.add('welcome', { userId });
7
+ * ```
8
+ *
9
+ * `add()` resolves when the DRIVER accepted the job, not when a processor
10
+ * handled it (the inline driver's `immediate` mode delivers on a following
11
+ * microtask; platform drivers deliver in another isolate entirely).
12
+ */ export class QueueClient {
13
+ queue;
14
+ driver;
15
+ constructor(queue, driver){
16
+ this.queue = queue;
17
+ this.driver = driver;
18
+ }
19
+ get name() {
20
+ return this.queue;
21
+ }
22
+ async add(jobName, data, options = {}) {
23
+ const job = {
24
+ id: crypto.randomUUID(),
25
+ queue: this.queue,
26
+ name: jobName,
27
+ data,
28
+ attempt: 1
29
+ };
30
+ await this.driver.enqueue(job, options);
31
+ return job;
32
+ }
33
+ }
@@ -0,0 +1,23 @@
1
+ import type { ProcessMetadata } from './queue.types';
2
+ /**
3
+ * Marks a provider class as a processor for one queue:
4
+ *
5
+ * ```ts
6
+ * @Processor('email')
7
+ * @Injectable()
8
+ * class EmailProcessor {
9
+ * @Process('welcome')
10
+ * async sendWelcome(job: QueueJob<{ userId: string }>) { ... }
11
+ * }
12
+ * ```
13
+ */
14
+ export declare function Processor(queueName: string): ClassDecorator;
15
+ /**
16
+ * Marks a processor method as the handler for a job name. Omit the name for
17
+ * the wildcard handler (receives every job on the queue that has no named
18
+ * handler). Named handlers win over the wildcard; a duplicate registration
19
+ * for the same name is first-wins with a diagnostics warning at dispatch.
20
+ */
21
+ export declare function Process(jobName?: string): MethodDecorator;
22
+ /** `@Process` entries declared on a processor class (declaration order). */
23
+ export declare function getProcessHandlers(processorClass: object): ProcessMetadata[];
@@ -0,0 +1,46 @@
1
+ import { createDiscoverableDecorator, defineMetadata, getMetadata, registerEntrypointKind } from "../index.js";
2
+ import { PROCESS_METADATA, PROCESSOR_METADATA } from "./queue.tokens.js";
3
+ const ProcessorMeta = createDiscoverableDecorator(PROCESSOR_METADATA);
4
+ // The open entrypoint kind: adapters enumerate processors via
5
+ // `app.entrypoints.ofKind<ProcessorMetadata>('queue')`. Declared at import
6
+ // time next to the decorator — zero kernel involvement.
7
+ registerEntrypointKind({
8
+ kind: 'queue',
9
+ metaKey: PROCESSOR_METADATA,
10
+ level: 'class'
11
+ });
12
+ /**
13
+ * Marks a provider class as a processor for one queue:
14
+ *
15
+ * ```ts
16
+ * @Processor('email')
17
+ * @Injectable()
18
+ * class EmailProcessor {
19
+ * @Process('welcome')
20
+ * async sendWelcome(job: QueueJob<{ userId: string }>) { ... }
21
+ * }
22
+ * ```
23
+ */ export function Processor(queueName) {
24
+ return ProcessorMeta({
25
+ queueName
26
+ });
27
+ }
28
+ /**
29
+ * Marks a processor method as the handler for a job name. Omit the name for
30
+ * the wildcard handler (receives every job on the queue that has no named
31
+ * handler). Named handlers win over the wildcard; a duplicate registration
32
+ * for the same name is first-wins with a diagnostics warning at dispatch.
33
+ */ export function Process(jobName) {
34
+ return (target, propertyKey)=>{
35
+ const ctor = target.constructor;
36
+ const existing = getMetadata(PROCESS_METADATA, ctor) ?? [];
37
+ existing.push({
38
+ jobName,
39
+ methodName: propertyKey
40
+ });
41
+ defineMetadata(PROCESS_METADATA, existing, ctor);
42
+ };
43
+ }
44
+ /** `@Process` entries declared on a processor class (declaration order). */ export function getProcessHandlers(processorClass) {
45
+ return getMetadata(PROCESS_METADATA, processorClass) ?? [];
46
+ }
@@ -0,0 +1,36 @@
1
+ import type { Container, EntrypointRegistry, Token } from '../index';
2
+ import type { ProcessorMetadata, QueueJob } from './queue.types';
3
+ export interface QueueDispatchResult {
4
+ /** Processors that ran a handler for this job. */
5
+ handled: number;
6
+ }
7
+ /** One dispatchable processor: its class token + `@Processor` meta. */
8
+ export interface QueueEntry {
9
+ token: Token;
10
+ meta: ProcessorMetadata;
11
+ }
12
+ /**
13
+ * Deliver one job to every `@Processor` of its queue — the primitive both the
14
+ * in-core `inline()` driver and platform adapters call.
15
+ *
16
+ * Each matching processor runs inside `runInEntrypointScope` (request-scoped
17
+ * dependencies rebuild per job) and is re-resolved BY TOKEN through the async
18
+ * seam, so processors living in `lazy: true` modules materialize cleanly on
19
+ * first dispatch — async providers and lifecycle hooks included. Scoped
20
+ * guards/interceptors/filters run through `PipelineRunner`
21
+ * (`getType() === 'queue'`, `getPayload()` is the job); app-wide `APP_*`
22
+ * components deliberately do NOT apply (cloudflare queue/scheduled parity —
23
+ * documented divergence from the WebSocket dispatcher).
24
+ *
25
+ * Errors no scoped filter claims RETHROW so awaiting platforms keep their
26
+ * retry semantics; fire-and-forget callers (inline `immediate` mode) must
27
+ * catch — `QueueDispatchBinding` routes those to diagnostics.
28
+ */
29
+ export declare function dispatchQueueJob(container: Container, entrypoints: EntrypointRegistry, job: QueueJob): Promise<QueueDispatchResult>;
30
+ /**
31
+ * Entry-list core shared by `dispatchQueueJob` (registry) and the in-process
32
+ * driver binding (discovery fallback before the registry exists). Entries are
33
+ * tokens + meta ONLY — every processor is re-resolved by token in its own
34
+ * scope, so request-scoped and lazy-module processors work on both paths.
35
+ */
36
+ export declare function dispatchJobToEntries(container: Container, entries: QueueEntry[], job: QueueJob): Promise<QueueDispatchResult>;
@@ -0,0 +1,102 @@
1
+ import { buildEntrypointExecutionContext, PipelineRunner, resolveScopedComponents, runInEntrypointScope, shouldFilterCatch } from "../index.js";
2
+ import { getProcessHandlers } from "./queue.decorators.js";
3
+ const warnedDuplicates = new WeakSet();
4
+ function selectHandler(container, processorClass, handlers, jobName) {
5
+ const named = handlers.filter((h)=>h.jobName === jobName);
6
+ const pool = named.length > 0 ? named : handlers.filter((h)=>h.jobName === undefined);
7
+ if (pool.length > 1 && !warnedDuplicates.has(processorClass)) {
8
+ warnedDuplicates.add(processorClass);
9
+ const label = named.length > 0 ? `@Process('${jobName}')` : '@Process() (wildcard)';
10
+ const message = `[vela] duplicate ${label} handlers on ${processorClass.name}; ` + `keeping the first ('${String(pool[0].methodName)}').`;
11
+ if (container.getDiagnostics() === 'throw') throw new Error(message);
12
+ if (container.getDiagnostics() === 'log') console.warn(message);
13
+ }
14
+ return pool[0];
15
+ }
16
+ /**
17
+ * Deliver one job to every `@Processor` of its queue — the primitive both the
18
+ * in-core `inline()` driver and platform adapters call.
19
+ *
20
+ * Each matching processor runs inside `runInEntrypointScope` (request-scoped
21
+ * dependencies rebuild per job) and is re-resolved BY TOKEN through the async
22
+ * seam, so processors living in `lazy: true` modules materialize cleanly on
23
+ * first dispatch — async providers and lifecycle hooks included. Scoped
24
+ * guards/interceptors/filters run through `PipelineRunner`
25
+ * (`getType() === 'queue'`, `getPayload()` is the job); app-wide `APP_*`
26
+ * components deliberately do NOT apply (cloudflare queue/scheduled parity —
27
+ * documented divergence from the WebSocket dispatcher).
28
+ *
29
+ * Errors no scoped filter claims RETHROW so awaiting platforms keep their
30
+ * retry semantics; fire-and-forget callers (inline `immediate` mode) must
31
+ * catch — `QueueDispatchBinding` routes those to diagnostics.
32
+ */ export async function dispatchQueueJob(container, entrypoints, job) {
33
+ const entries = entrypoints.ofKind('queue').map((ep)=>({
34
+ token: ep.token,
35
+ meta: ep.meta
36
+ }));
37
+ return dispatchJobToEntries(container, entries, job);
38
+ }
39
+ /**
40
+ * Entry-list core shared by `dispatchQueueJob` (registry) and the in-process
41
+ * driver binding (discovery fallback before the registry exists). Entries are
42
+ * tokens + meta ONLY — every processor is re-resolved by token in its own
43
+ * scope, so request-scoped and lazy-module processors work on both paths.
44
+ */ export async function dispatchJobToEntries(container, entries, job) {
45
+ const processors = entries.filter((entry)=>entry.meta.queueName === job.queue);
46
+ if (processors.length === 0) {
47
+ if (container.getDiagnostics() === 'log') {
48
+ console.warn(`[vela] queue job '${job.name}' on '${job.queue}' has no @Processor('${job.queue}') — dropped.`);
49
+ }
50
+ return {
51
+ handled: 0
52
+ };
53
+ }
54
+ // All matching processors receive the job; the first unclaimed error
55
+ // rejects the whole dispatch (platform retries the delivery — CF parity).
56
+ const outcomes = await Promise.all(processors.map((entry)=>dispatchToProcessor(container, entry.token, job)));
57
+ return {
58
+ handled: outcomes.filter(Boolean).length
59
+ };
60
+ }
61
+ async function dispatchToProcessor(container, processorClass, job) {
62
+ const handler = selectHandler(container, processorClass, getProcessHandlers(processorClass), job.name);
63
+ if (!handler) {
64
+ if (container.getDiagnostics() === 'log') {
65
+ console.warn(`[vela] no @Process('${job.name}') (or wildcard) handler on ` + `${processorClass.name} for queue '${job.queue}' — skipped.`);
66
+ }
67
+ return false;
68
+ }
69
+ return runInEntrypointScope(container, async (scope)=>{
70
+ // Async seam: materializes lazy processor modules (drainAsync awaits
71
+ // their async providers/hooks) and rebuilds request-scoped processors.
72
+ const instance = await scope.resolveAsync(processorClass);
73
+ const context = buildEntrypointExecutionContext('queue', processorClass, handler.methodName, job);
74
+ const guards = resolveScopedComponents('guard', processorClass, handler.methodName, scope);
75
+ const interceptors = resolveScopedComponents('interceptor', processorClass, handler.methodName, scope);
76
+ // Closest-first: handler/class filters reversed by the caller (WS/CF convention).
77
+ const filters = resolveScopedComponents('filter', processorClass, handler.methodName, scope).reverse();
78
+ try {
79
+ await PipelineRunner.run({
80
+ context,
81
+ guards,
82
+ interceptors,
83
+ resolveArgs: async ()=>[
84
+ job
85
+ ],
86
+ invoke: async (args)=>{
87
+ const method = instance[handler.methodName];
88
+ return method.apply(instance, args);
89
+ }
90
+ });
91
+ return true;
92
+ } catch (error) {
93
+ for (const filter of filters){
94
+ if (shouldFilterCatch(filter, error)) {
95
+ await filter.catch(error, context);
96
+ return true;
97
+ }
98
+ }
99
+ throw error;
100
+ }
101
+ });
102
+ }
@@ -0,0 +1,32 @@
1
+ import type { QueueModuleOptions } from './queue.types';
2
+ /**
3
+ * First-party queue module — authored 100% on vela's public API (the
4
+ * roadmap's "openness proof"). Producers inject a per-queue `QueueClient`
5
+ * via `queueToken(name)`; consumers are `@Processor`/`@Process` classes in
6
+ * ordinary user modules; platforms deliver through `dispatchQueueJob` or the
7
+ * in-core `inline()` driver.
8
+ *
9
+ * ```ts
10
+ * imports: [QueueModule.forRoot({ queues: ['email'] })]
11
+ * ```
12
+ *
13
+ * `lazy: true` (dogfoods 1.13): the module materializes when an eager
14
+ * producer injects a client (bootstrap — same structural reality that keeps
15
+ * WebSocketModule eager) or, in consumer-only workers, at the first
16
+ * delivered job.
17
+ *
18
+ * `queues` is STRUCTURAL: clients are options-derived providers, so
19
+ * `forRootAsync` callers pass it alongside the factory —
20
+ * `forRootAsync({ queues: ['email'], useFactory: () => ({ driver }) })`.
21
+ * Note that async options inherit the 1.13 lazy-module contract: the module
22
+ * must first materialize through an async seam (an eager producer's injection
23
+ * during the bootstrap sweep — the common case — or
24
+ * `app.materializeLazyModules()`); a synchronous first touch throws the
25
+ * descriptive sync-seam error.
26
+ */
27
+ declare const ConfigurableModuleClass: import("../module").ConfigurableModuleClassType<QueueModuleOptions, "forRoot", "create", {
28
+ isGlobal?: boolean;
29
+ }>, MODULE_OPTIONS_TOKEN: import("../container").InjectionToken<QueueModuleOptions>;
30
+ export declare class QueueModule extends ConfigurableModuleClass {
31
+ }
32
+ export { MODULE_OPTIONS_TOKEN as QUEUE_MODULE_OPTIONS };
@@ -0,0 +1,80 @@
1
+ import { Container, defineModule, DiscoveryService, stableHash } from "../index.js";
2
+ import { inline } from "./inline.driver.js";
3
+ import { QueueClient } from "./queue.client.js";
4
+ import { QueueDispatchBinding } from "./queue.binding.js";
5
+ import { QUEUE_DRIVER, queueToken } from "./queue.tokens.js";
6
+ /**
7
+ * First-party queue module — authored 100% on vela's public API (the
8
+ * roadmap's "openness proof"). Producers inject a per-queue `QueueClient`
9
+ * via `queueToken(name)`; consumers are `@Processor`/`@Process` classes in
10
+ * ordinary user modules; platforms deliver through `dispatchQueueJob` or the
11
+ * in-core `inline()` driver.
12
+ *
13
+ * ```ts
14
+ * imports: [QueueModule.forRoot({ queues: ['email'] })]
15
+ * ```
16
+ *
17
+ * `lazy: true` (dogfoods 1.13): the module materializes when an eager
18
+ * producer injects a client (bootstrap — same structural reality that keeps
19
+ * WebSocketModule eager) or, in consumer-only workers, at the first
20
+ * delivered job.
21
+ *
22
+ * `queues` is STRUCTURAL: clients are options-derived providers, so
23
+ * `forRootAsync` callers pass it alongside the factory —
24
+ * `forRootAsync({ queues: ['email'], useFactory: () => ({ driver }) })`.
25
+ * Note that async options inherit the 1.13 lazy-module contract: the module
26
+ * must first materialize through an async seam (an eager producer's injection
27
+ * during the bootstrap sweep — the common case — or
28
+ * `app.materializeLazyModules()`); a synchronous first touch throws the
29
+ * descriptive sync-seam error.
30
+ */ const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = defineModule({
31
+ name: 'Queue',
32
+ lazy: true,
33
+ key: (o)=>stableHash({
34
+ queues: o.queues ?? [],
35
+ driver: o.driver?.kind ?? 'inline'
36
+ }),
37
+ setup: ({ OPTIONS, options })=>{
38
+ const queues = options.queues;
39
+ if (!queues || queues.length === 0) {
40
+ throw new Error("QueueModule requires 'queues' as a structural option: " + "QueueModule.forRoot({ queues: ['email'] }) — for forRootAsync, pass it alongside " + 'the factory: forRootAsync({ queues: [...], useFactory }).');
41
+ }
42
+ const clientProviders = queues.map((name)=>({
43
+ provide: queueToken(name),
44
+ useFactory: (driver, _binding)=>new QueueClient(name, driver),
45
+ inject: [
46
+ QUEUE_DRIVER,
47
+ QueueDispatchBinding
48
+ ]
49
+ }));
50
+ return {
51
+ providers: [
52
+ {
53
+ provide: QUEUE_DRIVER,
54
+ useFactory: (o)=>o.driver ?? inline(),
55
+ inject: [
56
+ OPTIONS
57
+ ]
58
+ },
59
+ {
60
+ provide: QueueDispatchBinding,
61
+ useFactory: (container, discovery, driver)=>new QueueDispatchBinding(container, discovery, driver, queues),
62
+ inject: [
63
+ Container,
64
+ DiscoveryService,
65
+ QUEUE_DRIVER
66
+ ]
67
+ },
68
+ ...clientProviders
69
+ ],
70
+ exports: [
71
+ QUEUE_DRIVER,
72
+ QueueDispatchBinding,
73
+ ...queues.map((name)=>queueToken(name))
74
+ ]
75
+ };
76
+ }
77
+ });
78
+ export class QueueModule extends ConfigurableModuleClass {
79
+ }
80
+ export { MODULE_OPTIONS_TOKEN as QUEUE_MODULE_OPTIONS };
@@ -0,0 +1,21 @@
1
+ import { InjectionToken } from '../index';
2
+ import type { QueueClient } from './queue.client';
3
+ import type { QueueDriver } from './queue.types';
4
+ export declare const PROCESSOR_METADATA = "vela:queue:processor";
5
+ export declare const PROCESS_METADATA = "vela:queue:process";
6
+ /** The driver in effect for a `QueueModule` instance. */
7
+ export declare const QUEUE_DRIVER: InjectionToken<QueueDriver>;
8
+ /**
9
+ * The injection token for a named queue's `QueueClient`:
10
+ *
11
+ * ```ts
12
+ * constructor(@Inject(queueToken('email')) private readonly email: QueueClient) {}
13
+ * ```
14
+ *
15
+ * Memoized per name (HMR-stable). Deliberately NO `InjectionToken` default
16
+ * factory: the container resolves default-factory tokens from the root bucket
17
+ * BEFORE walking module imports, which would break the exported-provider
18
+ * path — the token description (`vela:queue:client:<name>`) keeps the
19
+ * no-provider error readable instead.
20
+ */
21
+ export declare function queueToken(name: string): InjectionToken<QueueClient>;