@dotpress/events 0.0.0-stage → 2.0.0-alpha.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/LICENSE +21 -0
- package/README.md +707 -2
- package/dist/aws-sqs.d.ts +127 -0
- package/dist/aws-sqs.js +440 -0
- package/dist/aws-sqs.js.map +1 -0
- package/dist/azure-service-bus.d.ts +99 -0
- package/dist/azure-service-bus.js +320 -0
- package/dist/azure-service-bus.js.map +1 -0
- package/dist/bullmq.d.ts +92 -0
- package/dist/bullmq.js +169 -0
- package/dist/bullmq.js.map +1 -0
- package/dist/bus-Bei_kN-e.d.ts +137 -0
- package/dist/chunk-E4NMMZMY.js +24 -0
- package/dist/chunk-E4NMMZMY.js.map +1 -0
- package/dist/chunk-NCQ4ZTNB.js +50 -0
- package/dist/chunk-NCQ4ZTNB.js.map +1 -0
- package/dist/cli.js +202 -0
- package/dist/cli.js.map +1 -0
- package/dist/cloudflare-queues.d.ts +120 -0
- package/dist/cloudflare-queues.js +333 -0
- package/dist/cloudflare-queues.js.map +1 -0
- package/dist/google-pubsub.d.ts +111 -0
- package/dist/google-pubsub.js +420 -0
- package/dist/google-pubsub.js.map +1 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +701 -0
- package/dist/index.js.map +1 -0
- package/dist/pg-boss.d.ts +79 -0
- package/dist/pg-boss.js +189 -0
- package/dist/pg-boss.js.map +1 -0
- package/dist/plugin.d.ts +40 -0
- package/dist/plugin.js +27 -0
- package/dist/plugin.js.map +1 -0
- package/dist/transport-BtNR1ucF.d.ts +273 -0
- package/package.json +139 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/define.ts","../src/envelope.ts","../src/transport.ts","../src/worker.ts","../src/bus.ts","../src/fake.ts","../src/start-worker.ts","../src/transports/memory.ts","../src/transports/sync.ts"],"sourcesContent":["import type { z } from 'zod'\nimport type { EventsLogger } from './logger.js'\n\n/**\n * Names of the transports, besides `default`, declared by module\n * augmentation so that typos are type errors:\n *\n * ```ts\n * declare module '@dotpress/events' {\n * interface EventTransports {\n * edge: true\n * }\n * }\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface EventTransports {}\n\n/** Name of a transport: `default`, or a name of `EventTransports`. */\nexport type TransportName = 'default' | Extract<keyof EventTransports, string>\n\n/** Event created by `defineEvent()`. */\nexport interface EventDefinition<S extends z.ZodType = z.ZodType> {\n readonly name: string\n readonly schema: S\n /** Transport of its listeners; undefined for `default`. */\n readonly transport: TransportName | undefined\n}\n\n/** Payload accepted by `emit()` for an event. */\nexport type EventInput<E> =\n E extends EventDefinition<infer S> ? z.input<S> : never\n\n/** Payload received by the listeners of an event. */\nexport type EventPayload<E> =\n E extends EventDefinition<infer S> ? z.output<S> : never\n\n/** Context of a listener call. */\nexport interface ListenerContext<TPayload = unknown> {\n /** Payload, validated by the schema of the event. */\n readonly payload: TPayload\n /** Name of the event. */\n readonly event: string\n /** Name of the listener. */\n readonly listener: string\n /**\n * Identifier of the message, the same for every attempt and when the\n * emission is repeated: use it to ignore duplicates.\n */\n readonly id: string\n /** Identifier of the emission, shared by the messages of its listeners. */\n readonly emissionId: string\n /** Attempt number, from 1. */\n readonly attempt: number\n readonly emittedAt: Date\n /** Metadata of the emission (request ID…). */\n readonly metadata: Readonly<Record<string, string>>\n readonly logger: EventsLogger\n}\n\n/** Function of a listener. Throw to have the message retried. */\nexport type ListenerHandler<TPayload = unknown> = (\n context: ListenerContext<TPayload>\n) => unknown\n\n/** Module loaded by `load()`: the handler is its default export. */\nexport interface ListenerModule<TPayload = unknown> {\n default: ListenerHandler<TPayload>\n}\n\n/** Options of `defineListener()`. */\nexport type ListenerOptions<TPayload> = {\n /**\n * Unique name: letters, digits, `.`, `_`, `:` and `-`. It identifies the\n * listener in messages: renaming it orphans messages still in queues.\n */\n name: string\n /** Transport of this listener; defaults to the one of the event. */\n transport?: TransportName | undefined\n} & (\n | { handle: ListenerHandler<TPayload>; load?: never }\n | {\n /**\n * Loads the module of the handler on first use, so that the code\n * emitting events does not load the code of the listeners:\n * `load: () => import('./listeners/send-welcome-mail.js')`.\n */\n load: () => Promise<ListenerModule<TPayload>>\n handle?: never\n }\n)\n\n/** Listener created by `defineListener()`. */\nexport interface ListenerDefinition<S extends z.ZodType = z.ZodType> {\n readonly name: string\n readonly event: EventDefinition<S>\n /** Transport of this listener, if it differs from the one of its event. */\n readonly transport: TransportName | undefined\n}\n\nconst NAME = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nconst checkName = (prefix: string, kind: string, name: unknown) => {\n if (typeof name !== 'string' || !NAME.test(name)) {\n throw new TypeError(\n `${prefix}: ${kind} must have 1 to 128 letters, digits, \".\", \"_\", \":\" or \"-\", starting with a letter or a digit.`\n )\n }\n}\n\nconst checkTransport = (prefix: string, transport: unknown) => {\n if (\n transport !== undefined &&\n (typeof transport !== 'string' || transport === '')\n ) {\n throw new TypeError(`${prefix}: transport must be a non-empty string.`)\n }\n}\n\nconst checkKeys = (\n prefix: string,\n options: unknown,\n allowed: ReadonlySet<string>\n): Record<string, unknown> => {\n if (!isRecord(options)) throw new TypeError(`${prefix}: expected an object.`)\n for (const key of Object.keys(options)) {\n if (!allowed.has(key)) {\n throw new TypeError(`${prefix}: unknown option \"${key}\".`)\n }\n }\n return options\n}\n\nconst events = new WeakSet<object>()\nconst loaders = new WeakMap<object, () => Promise<ListenerHandler>>()\n\n/** Whether a value was created by `defineEvent()`. */\nexport const isEvent = (value: unknown): value is EventDefinition =>\n // WeakSet.has() returns false for other values.\n events.has(value as object)\n\n/** Handler loader of a listener, or undefined for another value. */\nexport const loaderOf = (listener: unknown) => loaders.get(listener as object)\n\n/**\n * Defines an event: a fact that listeners react to, with the schema of its\n * payload.\n *\n * ```ts\n * export const userCreated = defineEvent({\n * name: 'user.created',\n * schema: z.object({ userId: z.string() }),\n * })\n * ```\n */\nexport const defineEvent = <S extends z.ZodType>(options: {\n /** Unique name: letters, digits, `.`, `_`, `:` and `-`. */\n name: string\n /**\n * Schema of the payload. Payloads travel as JSON: describe JSON values\n * (dates as strings, or coerced on the way in).\n */\n schema: S\n /** Transport of the listeners. Defaults to `default`. */\n transport?: TransportName | undefined\n}): EventDefinition<S> => {\n const name = (options as { name?: unknown } | null)?.name\n const prefix =\n typeof name === 'string' ? `Invalid event \"${name}\"` : 'Invalid event'\n const values = checkKeys(\n prefix,\n options,\n new Set(['name', 'schema', 'transport'])\n )\n checkName(prefix, 'name', name)\n if (\n typeof (values.schema as { safeParseAsync?: unknown } | null)\n ?.safeParseAsync !== 'function'\n ) {\n throw new TypeError(`${prefix}: schema must be a Zod schema.`)\n }\n checkTransport(prefix, values.transport)\n\n const event: EventDefinition<S> = Object.freeze({\n name: name as string,\n schema: values.schema as S,\n transport: values.transport as TransportName | undefined,\n })\n events.add(event)\n return event\n}\n\n/**\n * Defines a listener of an event. Its handler runs in a worker, once per\n * emission, and is retried when it throws.\n *\n * ```ts\n * export const sendWelcomeMail = defineListener(userCreated, {\n * name: 'send-welcome-mail',\n * load: () => import('./listeners/send-welcome-mail.js'),\n * })\n * ```\n */\nexport const defineListener = <S extends z.ZodType>(\n event: EventDefinition<S>,\n options: ListenerOptions<z.output<S>>\n): ListenerDefinition<S> => {\n const name = (options as { name?: unknown } | null)?.name\n const prefix =\n typeof name === 'string' ? `Invalid listener \"${name}\"` : 'Invalid listener'\n if (!isEvent(event)) {\n throw new TypeError(\n `${prefix}: expected an event created by defineEvent().`\n )\n }\n const values = checkKeys(\n prefix,\n options,\n new Set(['name', 'transport', 'handle', 'load'])\n )\n checkName(prefix, 'name', name)\n checkTransport(prefix, values.transport)\n\n const { handle, load } = values\n if ((handle === undefined) === (load === undefined)) {\n throw new TypeError(`${prefix}: set exactly one of handle and load.`)\n }\n if (typeof (handle ?? load) !== 'function') {\n throw new TypeError(\n `${prefix}: ${handle === undefined ? 'load' : 'handle'} must be a function.`\n )\n }\n\n const listener: ListenerDefinition<S> = Object.freeze({\n name: name as string,\n event,\n transport: values.transport as TransportName | undefined,\n })\n\n let loading: Promise<ListenerHandler> | undefined\n loaders.set(\n listener,\n handle === undefined\n ? () =>\n (loading ??= (load as () => Promise<unknown>)().then((module) => {\n const loaded = (module as { default?: unknown } | null)?.default\n if (typeof loaded !== 'function') {\n throw new TypeError(\n `Listener \"${name as string}\": load() must resolve to a module whose default export is the handler.`\n )\n }\n return loaded as ListenerHandler\n })).catch((error: unknown) => {\n // A failed import is tried again on the next message.\n loading = undefined\n throw error\n })\n : () => Promise.resolve(handle as ListenerHandler)\n )\n return listener\n}\n","/**\n * Message sent to a transport: one per listener of an emission. It travels\n * as JSON.\n */\nexport interface Envelope {\n /** Version of the format. */\n readonly v: 1\n /** Identifier of the message, stable across retries and emissions. */\n readonly id: string\n /** Identifier of the emission, shared by its messages. */\n readonly emissionId: string\n readonly event: string\n readonly listener: string\n /** Payload, as JSON. */\n readonly payload: unknown\n /** ISO date of the emission. */\n readonly emittedAt: string\n readonly metadata: Readonly<Record<string, string>>\n}\n\nconst hex = (bytes: Uint8Array) =>\n Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')\n\n/**\n * Identifier of the message of a listener for an emission: a UUID (version\n * 8, RFC 9562) derived from both, so that emitting again gives the same\n * identifiers.\n */\nexport const messageId = async (emissionId: string, listener: string) => {\n const digest = await crypto.subtle.digest(\n 'SHA-256',\n new TextEncoder().encode(`${emissionId}\\n${listener}`)\n )\n const bytes = new Uint8Array(digest.slice(0, 16))\n bytes[6] = ((bytes[6] as number) & 0x0f) | 0x80\n bytes[8] = ((bytes[8] as number) & 0x3f) | 0x80\n const value = hex(bytes)\n return `${value.slice(0, 8)}-${value.slice(8, 12)}-${value.slice(12, 16)}-${value.slice(16, 20)}-${value.slice(20)}`\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nconst STRING_FIELDS = [\n 'id',\n 'emissionId',\n 'event',\n 'listener',\n 'emittedAt',\n] as const\n\n/**\n * Envelope of a received message (an object, or its JSON), or a reason\n * why it is not one.\n */\nexport const parseEnvelope = (\n message: unknown\n): { envelope: Envelope } | { problem: string } => {\n let value = message\n if (typeof value === 'string') {\n try {\n value = JSON.parse(value)\n } catch {\n return { problem: 'the message is not JSON' }\n }\n }\n if (!isRecord(value)) return { problem: 'the message is not an object' }\n if (value.v !== 1) {\n return {\n problem: `unsupported envelope version ${JSON.stringify(value.v)}`,\n }\n }\n for (const field of STRING_FIELDS) {\n if (typeof value[field] !== 'string' || value[field] === '') {\n return { problem: `the envelope has no ${field}` }\n }\n }\n if (!('payload' in value)) return { problem: 'the envelope has no payload' }\n const { metadata } = value\n if (\n !isRecord(metadata) ||\n !Object.values(metadata).every((item) => typeof item === 'string')\n ) {\n return { problem: 'the metadata of the envelope is not a string map' }\n }\n return { envelope: value as unknown as Envelope }\n}\n","import type { Envelope } from './envelope.js'\nimport type { EventsLogger } from './logger.js'\nimport type { Worker } from './worker.js'\n\n/** Options of `transport.send()`. */\nexport interface SendOptions {\n /** Delay before delivery, in seconds. */\n readonly delay: number | undefined\n}\n\n/**\n * Sends messages to a queue. Implemented by each transport (SQS, BullMQ,\n * Cloudflare Queues…); throw a `PartialSendError` when only some messages\n * failed.\n */\nexport interface Transport {\n /** Kind of transport, for logs: `sqs`, `memory`… */\n readonly kind: string\n send(envelopes: readonly Envelope[], options: SendOptions): Promise<void>\n /**\n * In-process transports only (`sync`, `memory`): receives the worker\n * that runs the listeners routed to this transport.\n */\n bind?(worker: Worker): void\n /** Attempts of an in-process transport, for `FailureInfo.final`. */\n readonly maxAttempts?: number\n}\n\n/** Options of `source.consume()`. */\nexport interface ConsumeOptions {\n /** Aborted to stop: finish the messages in progress, then resolve. */\n readonly signal: AbortSignal\n readonly logger: EventsLogger\n}\n\n/**\n * Reads messages from a queue and hands them to a worker, until stopped:\n * the long-running side of a transport (SQS polling, BullMQ worker…).\n */\nexport interface Source {\n /** Kind of source, for logs. */\n readonly kind: string\n consume(worker: Worker, options: ConsumeOptions): Promise<void>\n}\n\n/** Whether a value has the shape of a transport. */\nexport const isTransport = (value: unknown): value is Transport =>\n typeof (value as Transport | null)?.send === 'function' &&\n typeof (value as Transport).kind === 'string'\n","import {\n loaderOf,\n type ListenerDefinition,\n type ListenerHandler,\n} from './define.js'\nimport { parseEnvelope, type Envelope } from './envelope.js'\nimport { checkLogger, silentLogger, type EventsLogger } from './logger.js'\n\n/** Why a message can never be handled: retrying would not help. */\nexport type DeadReason =\n /** Not an envelope, or an envelope of another version. */\n | 'invalid-envelope'\n /** No listener of this name in this worker (not deployed here?). */\n | 'unknown-listener'\n /** The payload does not match the schema of the event. */\n | 'invalid-payload'\n\n/** Result of `worker.handle()`, translated by each transport. */\nexport type HandleResult =\n | { readonly status: 'success' }\n /** The listener failed: retry the message. */\n | { readonly status: 'retry'; readonly error: unknown }\n /** The message can never be handled: do not retry it. */\n | {\n readonly status: 'dead'\n readonly reason: DeadReason\n readonly error: unknown\n }\n\n/** Details of a failure, for `onFailure`. */\nexport interface FailureInfo {\n /** Envelope of the message; undefined when it is not an envelope. */\n readonly envelope: Envelope | undefined\n readonly reason: 'listener-error' | DeadReason\n readonly attempt: number\n /**\n * Whether the message will not be retried: a dead message, or the last\n * attempt (with `maxAttempts`).\n */\n readonly final: boolean\n}\n\n/** Called for every failure: logs, alerts… */\nexport type FailureHook = (\n error: unknown,\n info: FailureInfo\n) => void | Promise<void>\n\n/** Options of `createWorker()`. */\nexport interface WorkerOptions {\n /** Listeners run by this worker. */\n listeners: readonly ListenerDefinition[]\n logger?: EventsLogger | undefined\n /** Called for every failure. Its own errors are logged and ignored. */\n onFailure?: FailureHook | undefined\n /**\n * Attempts before the transport gives up (its retry limit, like the\n * `maxReceiveCount` of an SQS queue), to report the last one as `final`.\n */\n maxAttempts?: number | undefined\n}\n\n/** Runs the listeners of received messages. */\nexport interface Worker {\n /** Names of its listeners. */\n readonly listeners: readonly string[]\n /**\n * Handles a received message (an envelope, or its JSON). Never throws:\n * the result tells the transport what to do with the message.\n */\n handle(\n message: unknown,\n options?: { attempt?: number | undefined }\n ): Promise<HandleResult>\n}\n\nconst OPTIONS = new Set(['listeners', 'logger', 'onFailure', 'maxAttempts'])\n\n/** Checks a list of listeners; returns them by name. */\nexport const listenersByName = (label: string, listeners: unknown) => {\n if (!Array.isArray(listeners)) {\n throw new TypeError(`${label}: listeners must be a list.`)\n }\n const byName = new Map<string, ListenerDefinition>()\n for (const listener of listeners as unknown[]) {\n if (!loaderOf(listener)) {\n throw new TypeError(\n `${label}: listeners must be created by defineListener().`\n )\n }\n const { name } = listener as ListenerDefinition\n if (byName.has(name)) {\n throw new TypeError(`${label}: two listeners are named \"${name}\".`)\n }\n byName.set(name, listener as ListenerDefinition)\n }\n return byName\n}\n\nconst describe = (error: unknown) =>\n error instanceof Error ? error.message : String(error)\n\n/**\n * Creates a worker: it runs the listeners of the messages it receives from\n * a transport (Lambda handler, queue consumer, `startWorker()`…).\n *\n * ```ts\n * const worker = createWorker({ listeners: [sendWelcomeMail, geocodeUser] })\n * export const handler = createSqsHandler(worker)\n * ```\n */\nexport const createWorker = (options: WorkerOptions): Worker => {\n const label = 'createWorker()'\n if (typeof options !== 'object' || options === null) {\n throw new TypeError(`${label}: expected an object of options.`)\n }\n for (const key of Object.keys(options)) {\n if (!OPTIONS.has(key)) {\n throw new TypeError(`${label}: unknown option \"${key}\".`)\n }\n }\n const listeners = listenersByName(label, options.listeners)\n const logger =\n options.logger === undefined\n ? silentLogger\n : checkLogger(label, options.logger)\n const { onFailure, maxAttempts } = options\n if (onFailure !== undefined && typeof onFailure !== 'function') {\n throw new TypeError(`${label}: onFailure must be a function.`)\n }\n if (\n maxAttempts !== undefined &&\n (!Number.isInteger(maxAttempts) || maxAttempts < 1)\n ) {\n throw new TypeError(`${label}: maxAttempts must be a positive integer.`)\n }\n\n const fail = async (\n error: unknown,\n info: FailureInfo\n ): Promise<HandleResult> => {\n const fields = {\n event: info.envelope?.event,\n listener: info.envelope?.listener,\n messageId: info.envelope?.id,\n attempt: info.attempt,\n reason: info.reason,\n error: describe(error),\n }\n if (info.final) {\n logger.error(fields, 'Event listener failed, the message is not retried')\n } else {\n logger.warn(fields, 'Event listener failed, the message is retried')\n }\n if (onFailure) {\n try {\n await onFailure(error, info)\n } catch (hookError) {\n logger.error(\n { ...fields, hookError: describe(hookError) },\n 'onFailure hook failed'\n )\n }\n }\n return info.reason === 'listener-error'\n ? { status: 'retry', error }\n : { status: 'dead', reason: info.reason, error }\n }\n\n const handle: Worker['handle'] = async (message, handleOptions = {}) => {\n const attempt = handleOptions.attempt ?? 1\n const parsed = parseEnvelope(message)\n if ('problem' in parsed) {\n return fail(new Error(`Invalid message: ${parsed.problem}.`), {\n envelope: undefined,\n reason: 'invalid-envelope',\n attempt,\n final: true,\n })\n }\n const { envelope } = parsed\n const dead = (reason: DeadReason, text: string) =>\n fail(new Error(text), { envelope, reason, attempt, final: true })\n\n const listener = listeners.get(envelope.listener)\n if (!listener) {\n return dead(\n 'unknown-listener',\n `Unknown listener \"${envelope.listener}\": this worker runs ${[...listeners.keys()].join(', ') || 'no listener'}.`\n )\n }\n if (listener.event.name !== envelope.event) {\n return dead(\n 'invalid-envelope',\n `Listener \"${listener.name}\" listens to \"${listener.event.name}\", not \"${envelope.event}\".`\n )\n }\n\n let result: Awaited<ReturnType<typeof listener.event.schema.safeParseAsync>>\n try {\n result = await listener.event.schema.safeParseAsync(envelope.payload)\n } catch (error) {\n // A refinement that throws (database lookup…): may succeed later.\n return fail(error, {\n envelope,\n reason: 'listener-error',\n attempt,\n final: maxAttempts !== undefined && attempt >= maxAttempts,\n })\n }\n if (!result.success) {\n const issues = result.error.issues\n .map(({ path, message: text }) =>\n path.length > 0 ? `${path.map(String).join('.')}: ${text}` : text\n )\n .join('; ')\n return dead(\n 'invalid-payload',\n `Invalid payload for event \"${envelope.event}\": ${issues}`\n )\n }\n\n const context = {\n payload: result.data,\n event: envelope.event,\n listener: envelope.listener,\n id: envelope.id,\n emissionId: envelope.emissionId,\n attempt,\n emittedAt: new Date(envelope.emittedAt),\n metadata: envelope.metadata,\n logger,\n }\n try {\n const handler: ListenerHandler = await (\n loaderOf(listener) as () => Promise<ListenerHandler>\n )()\n await handler(context)\n } catch (error) {\n return fail(error, {\n envelope,\n reason: 'listener-error',\n attempt,\n final: maxAttempts !== undefined && attempt >= maxAttempts,\n })\n }\n logger.debug?.(\n {\n event: envelope.event,\n listener: envelope.listener,\n messageId: envelope.id,\n attempt,\n },\n 'Event listener succeeded'\n )\n return { status: 'success' }\n }\n\n return Object.freeze({\n listeners: Object.freeze([...listeners.keys()]),\n handle,\n })\n}\n","import {\n isEvent,\n type EventDefinition,\n type EventInput,\n type ListenerDefinition,\n type TransportName,\n} from './define.js'\nimport { messageId, type Envelope } from './envelope.js'\nimport {\n EventEmitError,\n EventPayloadError,\n PartialSendError,\n type FailedMessage,\n type MessageRef,\n} from './errors.js'\nimport { checkLogger, silentLogger, type EventsLogger } from './logger.js'\nimport { isTransport, type Transport } from './transport.js'\nimport { createWorker, listenersByName, type FailureHook } from './worker.js'\n\n/** Options of `createEventBus()`. */\nexport type EventBusOptions = {\n /**\n * Every listener of the app, with their routes: the bus sends one\n * message per listener of an emitted event. Workers only need the\n * listeners they run.\n */\n listeners: readonly ListenerDefinition[]\n logger?: EventsLogger | undefined\n /**\n * Failures of the listeners run by in-process transports (`sync`,\n * `memory`). Workers have their own.\n */\n onFailure?: FailureHook | undefined\n} & (\n | {\n /** The transport of every event. */\n transport: Transport\n transports?: never\n }\n | {\n /** Transports by name; events without a `transport` use `default`. */\n transports: { readonly [K in TransportName]?: Transport }\n transport?: never\n }\n)\n\n/** Options of `emit()`. */\nexport interface EmitOptions {\n /** Delay before delivery, in seconds, within the limit of the transport. */\n delay?: number | undefined\n /** Metadata of the emission, passed to the listeners. */\n metadata?: Readonly<Record<string, string>> | undefined\n}\n\n/** Result of `emit()`. */\nexport interface EmitResult {\n readonly emissionId: string\n /** One message per listener of the event. */\n readonly messages: readonly MessageRef[]\n}\n\n/** Context of a bus: the logger and metadata of a request, for instance. */\nexport interface EventBusContext {\n logger?: EventsLogger | undefined\n /** Added to the metadata of every emission. */\n metadata?: Readonly<Record<string, string>> | undefined\n}\n\nexport interface EventBus {\n /**\n * Emits an event: validates its payload, then sends one message per\n * listener to the transport of the listener.\n */\n emit<E extends EventDefinition>(\n event: E,\n payload: EventInput<E>,\n options?: EmitOptions\n ): Promise<EmitResult>\n /** The same bus, with another logger and additional metadata. */\n withContext(context: EventBusContext): EventBus\n}\n\nconst OPTIONS = new Set([\n 'listeners',\n 'logger',\n 'onFailure',\n 'transport',\n 'transports',\n])\nconst METADATA_KEY = /^[A-Za-z0-9._-]{1,128}$/\nconst LINE_BREAK = /[\\r\\n]/\n\nexport const checkMetadata = (label: string, metadata: unknown) => {\n if (metadata === undefined) return {}\n if (\n typeof metadata !== 'object' ||\n metadata === null ||\n Array.isArray(metadata)\n ) {\n throw new TypeError(`${label}: metadata must be an object.`)\n }\n for (const [key, value] of Object.entries(metadata)) {\n if (!METADATA_KEY.test(key)) {\n throw new TypeError(\n `${label}: metadata keys must have 1 to 128 letters, digits, \".\", \"_\" or \"-\": ${JSON.stringify(key)}.`\n )\n }\n if (typeof value !== 'string' || LINE_BREAK.test(value)) {\n throw new TypeError(\n `${label}: metadata.${key} must be a string on one line.`\n )\n }\n }\n return metadata as Record<string, string>\n}\n\n/** Transport name of a listener: its own, the one of its event, or default. */\nexport const routeOf = (listener: ListenerDefinition): string =>\n listener.transport ?? listener.event.transport ?? 'default'\n\n/**\n * Checks an emission like `emit()`: the event, the delay, the metadata,\n * and the payload, which travels as JSON. Returns the payload received by\n * the listeners and the metadata of the emission.\n */\nexport const checkEmission = async (\n event: unknown,\n payload: unknown,\n emitOptions: EmitOptions | undefined,\n contextMetadata: Readonly<Record<string, string>>\n) => {\n if (!isEvent(event)) {\n throw new TypeError('emit(): expected an event created by defineEvent().')\n }\n const emitLabel = `emit(\"${event.name}\")`\n const { delay, metadata } = emitOptions ?? {}\n if (delay !== undefined && (!Number.isInteger(delay) || delay < 0)) {\n throw new TypeError(\n `${emitLabel}: delay must be a non-negative integer (seconds).`\n )\n }\n const checked = {\n ...contextMetadata,\n ...checkMetadata(emitLabel, metadata),\n }\n\n // The payload travels as JSON: listeners receive this value.\n let json: unknown\n try {\n json = JSON.parse(JSON.stringify(payload) ?? 'null')\n } catch (error) {\n throw new EventPayloadError(event.name, [\n {\n path: [],\n message: `the payload cannot be serialized as JSON (${error instanceof Error ? error.message : String(error)})`,\n },\n ])\n }\n const result = await event.schema.safeParseAsync(json)\n if (!result.success) {\n throw new EventPayloadError(\n event.name,\n result.error.issues.map(({ path, message }) => ({\n path: path.map((key) => (typeof key === 'symbol' ? String(key) : key)),\n message,\n }))\n )\n }\n return { event, json, delay, metadata: checked }\n}\n\n/**\n * Creates an event bus: it validates the emitted events and sends one\n * message per listener to its transport.\n *\n * ```ts\n * const events = createEventBus({\n * transport: createMemoryTransport(),\n * listeners: [sendWelcomeMail, geocodeUser],\n * })\n * await events.emit(userCreated, { userId: '42' })\n * ```\n */\nexport const createEventBus = (options: EventBusOptions): EventBus => {\n const label = 'createEventBus()'\n if (typeof options !== 'object' || options === null) {\n throw new TypeError(`${label}: expected an object of options.`)\n }\n for (const key of Object.keys(options)) {\n if (!OPTIONS.has(key)) {\n throw new TypeError(`${label}: unknown option \"${key}\".`)\n }\n }\n const listeners = listenersByName(label, options.listeners)\n const logger =\n options.logger === undefined\n ? silentLogger\n : checkLogger(label, options.logger)\n const { onFailure } = options\n if (onFailure !== undefined && typeof onFailure !== 'function') {\n throw new TypeError(`${label}: onFailure must be a function.`)\n }\n\n // Transports by name.\n if (\n (options.transport === undefined) ===\n (options.transports === undefined)\n ) {\n throw new TypeError(\n `${label}: set exactly one of transport and transports.`\n )\n }\n const transports = new Map<string, Transport>(\n options.transport === undefined\n ? Object.entries(options.transports ?? {}).filter(\n ([, transport]) => transport !== undefined\n )\n : [['default', options.transport]]\n )\n if (\n options.transports !== undefined &&\n (typeof options.transports !== 'object' || options.transports === null)\n ) {\n throw new TypeError(`${label}: transports must be an object.`)\n }\n for (const [name, transport] of transports) {\n if (!isTransport(transport)) {\n throw new TypeError(\n `${label}: the \"${name}\" transport must have a kind and a send() method.`\n )\n }\n }\n\n // Every listener needs its transport.\n const routed = new Map<string, ListenerDefinition[]>()\n for (const listener of listeners.values()) {\n const route = routeOf(listener)\n if (!transports.has(route)) {\n throw new TypeError(\n `${label}: listener \"${listener.name}\" (event \"${listener.event.name}\") uses the \"${route}\" transport, which is not configured.`\n )\n }\n routed.set(route, [...(routed.get(route) ?? []), listener])\n }\n\n // In-process transports run their listeners themselves.\n for (const [name, transport] of transports) {\n transport.bind?.(\n createWorker({\n listeners: routed.get(name) ?? [],\n logger,\n onFailure,\n maxAttempts: transport.maxAttempts,\n })\n )\n }\n\n const listenersOf = (event: string) =>\n [...listeners.values()].filter((listener) => listener.event.name === event)\n\n const create = (context: {\n logger: EventsLogger\n metadata: Readonly<Record<string, string>>\n }): EventBus => {\n const emit: EventBus['emit'] = async (event, payload, emitOptions = {}) => {\n const { json, delay, metadata } = await checkEmission(\n event,\n payload,\n emitOptions,\n context.metadata\n )\n\n const emissionId = crypto.randomUUID()\n const targets = listenersOf(event.name)\n if (targets.length === 0) {\n context.logger.debug?.(\n { event: event.name, emissionId },\n 'Event emitted without listeners'\n )\n return { emissionId, messages: [] }\n }\n\n const base = {\n v: 1 as const,\n emissionId,\n event: event.name,\n payload: json,\n emittedAt: new Date().toISOString(),\n metadata,\n }\n const messages: { ref: MessageRef; envelope: Envelope }[] = []\n for (const listener of targets) {\n const id = await messageId(emissionId, listener.name)\n messages.push({\n ref: { id, listener: listener.name, transport: routeOf(listener) },\n envelope: { ...base, id, listener: listener.name },\n })\n }\n\n // One send() per transport, in parallel.\n const groups = new Map<string, typeof messages>()\n for (const message of messages) {\n const { transport } = message.ref\n groups.set(transport, [...(groups.get(transport) ?? []), message])\n }\n const sent: MessageRef[] = []\n const failed: FailedMessage[] = []\n await Promise.all(\n [...groups].map(async ([name, group]) => {\n try {\n await (transports.get(name) as Transport).send(\n group.map(({ envelope }) => envelope),\n { delay }\n )\n for (const { ref } of group) sent.push(ref)\n } catch (error) {\n const partial =\n error instanceof PartialSendError\n ? new Map(error.failed.map((item) => [item.id, item.error]))\n : undefined\n for (const { ref } of group) {\n if (partial && !partial.has(ref.id)) sent.push(ref)\n else failed.push({ ...ref, error: partial?.get(ref.id) ?? error })\n }\n }\n })\n )\n\n const order = <T extends MessageRef>(refs: T[]) =>\n refs.sort(\n (a, b) =>\n targets.findIndex(({ name }) => name === a.listener) -\n targets.findIndex(({ name }) => name === b.listener)\n )\n if (failed.length > 0) {\n throw new EventEmitError(event.name, order(sent), order(failed))\n }\n context.logger.info(\n { event: event.name, emissionId, messages: sent.length },\n 'Event emitted'\n )\n return {\n emissionId,\n messages: order(messages.map(({ ref }) => ref)),\n }\n }\n\n return Object.freeze({\n emit,\n withContext: ({ logger: other, metadata }: EventBusContext = {}) =>\n create({\n logger:\n other === undefined\n ? context.logger\n : checkLogger('withContext()', other),\n metadata: {\n ...context.metadata,\n ...checkMetadata('withContext()', metadata),\n },\n }),\n })\n }\n\n return create({ logger, metadata: {} })\n}\n","import {\n checkEmission,\n checkMetadata,\n type EventBus,\n type EventBusContext,\n} from './bus.js'\nimport { checkLogger } from './logger.js'\n\n/** An emission recorded by a fake event bus. */\nexport interface RecordedEmission {\n readonly emissionId: string\n /** Name of the event. */\n readonly event: string\n /** Payload, as the listeners would receive it (after JSON). */\n readonly payload: unknown\n readonly delay: number | undefined\n /** Metadata of the emission, with the metadata of the context. */\n readonly metadata: Readonly<Record<string, string>>\n readonly emittedAt: Date\n}\n\n/** Event bus that records emissions instead of sending them, for tests. */\nexport interface FakeEventBus extends EventBus {\n /** Recorded emissions, oldest first, from this bus and its contexts. */\n readonly emitted: readonly RecordedEmission[]\n /** Forgets the recorded emissions. */\n clear(): void\n}\n\n/**\n * Creates an event bus that checks emissions like `createEventBus()` (event,\n * payload, delay, metadata) and records them, without running listeners:\n * to test the code that emits, with `expectEventEmitted()` of\n * `@dotpress/testing`.\n *\n * ```ts\n * const events = createFakeEventBus()\n * await signUp(events, 'ada@example.com')\n * expect(events.emitted).toMatchObject([{ event: 'user.created' }])\n * ```\n */\nexport const createFakeEventBus = (): FakeEventBus => {\n const emitted: RecordedEmission[] = []\n\n const create = (metadata: Readonly<Record<string, string>>): EventBus => {\n const emit: EventBus['emit'] = async (event, payload, options) => {\n const checked = await checkEmission(event, payload, options, metadata)\n const emissionId = crypto.randomUUID()\n emitted.push(\n Object.freeze({\n emissionId,\n event: checked.event.name,\n payload: checked.json,\n delay: checked.delay,\n metadata: Object.freeze(checked.metadata),\n emittedAt: new Date(),\n })\n )\n return { emissionId, messages: [] }\n }\n return Object.freeze({\n emit,\n withContext: ({ logger, metadata: added }: EventBusContext = {}) => {\n if (logger !== undefined) checkLogger('withContext()', logger)\n return create({\n ...metadata,\n ...checkMetadata('withContext()', added),\n })\n },\n })\n }\n\n const root = create({})\n return Object.freeze({\n ...root,\n emitted,\n clear: () => {\n emitted.length = 0\n },\n })\n}\n","import { checkLogger, silentLogger, type EventsLogger } from './logger.js'\nimport type { Source } from './transport.js'\nimport type { Worker } from './worker.js'\n\n/** A started worker. */\nexport interface RunningWorker {\n /** Stops reading messages, waits for those in progress, then resolves. */\n stop(): Promise<void>\n /** Resolves when the worker has stopped; rejects if the source failed. */\n readonly done: Promise<void>\n}\n\n/** Options of `startWorker()`. */\nexport interface StartWorkerOptions {\n logger?: EventsLogger | undefined\n /** Stops the worker when aborted. */\n signal?: AbortSignal | undefined\n}\n\n/**\n * Starts a long-running worker: the source reads messages from its queue\n * (SQS polling, BullMQ, pg-boss…) and the worker runs their listeners,\n * until `stop()`. Used by containers and by `dotpress-events work`.\n */\nexport const startWorker = (\n worker: Worker,\n source: Source,\n options: StartWorkerOptions = {}\n): RunningWorker => {\n const label = 'startWorker()'\n if (typeof worker?.handle !== 'function') {\n throw new TypeError(\n `${label}: expected a worker created by createWorker().`\n )\n }\n if (typeof source?.consume !== 'function') {\n throw new TypeError(`${label}: expected a source with a consume() method.`)\n }\n const logger =\n options.logger === undefined\n ? silentLogger\n : checkLogger(label, options.logger)\n\n const controller = new AbortController()\n const stop = () => controller.abort()\n options.signal?.addEventListener('abort', stop, { once: true })\n if (options.signal?.aborted) stop()\n\n logger.info(\n { source: source.kind, listeners: worker.listeners },\n 'Event worker started'\n )\n const done = source\n .consume(worker, { signal: controller.signal, logger })\n .finally(() => {\n options.signal?.removeEventListener('abort', stop)\n logger.info({ source: source.kind }, 'Event worker stopped')\n })\n\n return {\n stop: () => {\n stop()\n return done\n },\n done,\n }\n}\n","import type { Envelope } from '../envelope.js'\nimport type { Transport } from '../transport.js'\nimport type { Worker } from '../worker.js'\n\n/** Options of `createMemoryTransport()`. */\nexport interface MemoryTransportOptions {\n /** Attempts per message, the first included. Defaults to 3. */\n maxAttempts?: number | undefined\n /**\n * Delay before an attempt, in milliseconds, given the number of the\n * failed attempt. Defaults to 100 ms, doubled at each attempt.\n */\n retryDelay?: ((attempt: number) => number) | undefined\n}\n\n/** In-process queue, for development and tests. */\nexport interface MemoryTransport extends Transport {\n /** Every message sent, oldest first. */\n readonly sent: readonly Envelope[]\n /** Resolves when no message is waiting or running. */\n drain(): Promise<void>\n /** Forgets the sent messages (not the pending ones). */\n clear(): void\n}\n\n/**\n * Creates a transport that queues messages in memory and runs their\n * listeners in the same process, with retries. Messages are lost when the\n * process stops: use a real queue in production.\n */\nexport const createMemoryTransport = (\n options: MemoryTransportOptions = {}\n): MemoryTransport => {\n const label = 'createMemoryTransport()'\n const {\n maxAttempts = 3,\n retryDelay = (attempt: number) => 100 * 2 ** (attempt - 1),\n } = options ?? {}\n if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {\n throw new TypeError(`${label}: maxAttempts must be a positive integer.`)\n }\n if (typeof retryDelay !== 'function') {\n throw new TypeError(`${label}: retryDelay must be a function.`)\n }\n\n const sent: Envelope[] = []\n let worker: Worker | undefined\n let pending = 0\n const idle: (() => void)[] = []\n\n const settle = () => {\n pending -= 1\n if (pending === 0) {\n for (const resolve of idle.splice(0)) resolve()\n }\n }\n\n const schedule = (json: string, attempt: number, delayMs: number) => {\n pending += 1\n setTimeout(() => {\n void (async () => {\n const result = await (worker as Worker).handle(json, { attempt })\n if (result.status === 'retry' && attempt < maxAttempts) {\n schedule(json, attempt + 1, retryDelay(attempt))\n }\n settle()\n })()\n }, delayMs)\n }\n\n return {\n kind: 'memory',\n maxAttempts,\n sent,\n bind: (bound) => {\n worker = bound\n },\n send: async (envelopes, { delay }) => {\n if (!worker) {\n throw new Error('The memory transport is not used by an event bus.')\n }\n for (const envelope of envelopes) {\n // Through JSON, like the other transports.\n const json = JSON.stringify(envelope)\n sent.push(JSON.parse(json) as Envelope)\n schedule(json, 1, (delay ?? 0) * 1000)\n }\n },\n drain: () =>\n pending === 0\n ? Promise.resolve()\n : new Promise<void>((resolve) => {\n idle.push(resolve)\n }),\n clear: () => {\n sent.length = 0\n },\n }\n}\n","import { PartialSendError } from '../errors.js'\nimport type { Transport } from '../transport.js'\nimport type { Worker } from '../worker.js'\n\n/**\n * Transport that runs the listeners inside `emit()`, which waits for them\n * and throws an `EventEmitError` when one fails. For development, tests,\n * and apps without a queue. Delays are ignored: listeners run at once.\n */\nexport const createSyncTransport = (): Transport => {\n let worker: Worker | undefined\n\n return {\n kind: 'sync',\n maxAttempts: 1,\n bind: (bound) => {\n worker = bound\n },\n send: async (envelopes) => {\n if (!worker) {\n throw new Error('The sync transport is not used by an event bus.')\n }\n const failed: { id: string; error: unknown }[] = []\n for (const envelope of envelopes) {\n // The bus already serialized the payload as JSON.\n const result = await worker.handle(envelope, { attempt: 1 })\n if (result.status !== 'success') {\n failed.push({ id: envelope.id, error: result.error })\n }\n }\n if (failed.length > 0) throw new PartialSendError(failed)\n },\n }\n}\n"],"mappings":"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","names":["isRecord","OPTIONS"]}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { QueueOptions, PgBoss, ConstructorOptions, Job, JobResult } from 'pg-boss';
|
|
2
|
+
import { Transport, Source, Worker, Envelope } from './index.js';
|
|
3
|
+
import 'zod';
|
|
4
|
+
|
|
5
|
+
/** Options of the queue, used when the transport or the source creates it. */
|
|
6
|
+
interface PgBossQueueOptions extends QueueOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Queue of the failed jobs (created too): jobs go there after their last
|
|
9
|
+
* attempt, and messages that can never be handled at once.
|
|
10
|
+
*/
|
|
11
|
+
deadLetter?: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
/** Options shared by the transport and the source. */
|
|
14
|
+
interface PgBossOptions {
|
|
15
|
+
/**
|
|
16
|
+
* A pg-boss instance (started by you), or the options to create one: a
|
|
17
|
+
* connection string, or the options of `new PgBoss()`.
|
|
18
|
+
*/
|
|
19
|
+
boss: PgBoss | string | ConstructorOptions;
|
|
20
|
+
/** Name of the queue. */
|
|
21
|
+
queue: string;
|
|
22
|
+
/**
|
|
23
|
+
* Options of the queue when it does not exist yet, merged with the
|
|
24
|
+
* defaults: 3 attempts (`retryLimit: 2`), exponential backoff from 1
|
|
25
|
+
* second. Existing queues are not changed.
|
|
26
|
+
*/
|
|
27
|
+
queueOptions?: PgBossQueueOptions | undefined;
|
|
28
|
+
}
|
|
29
|
+
/** Options of `createPgBossSource()`. */
|
|
30
|
+
interface PgBossSourceOptions extends PgBossOptions {
|
|
31
|
+
/** Jobs fetched at once, handled in parallel. Defaults to 1. */
|
|
32
|
+
batchSize?: number | undefined;
|
|
33
|
+
/** Wait between two fetches of an empty queue, in seconds. */
|
|
34
|
+
pollingIntervalSeconds?: number | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* On stop, how long to wait for the jobs in progress, in milliseconds,
|
|
37
|
+
* before stopping anyway. Defaults to 30 seconds.
|
|
38
|
+
*/
|
|
39
|
+
closeTimeout?: number | undefined;
|
|
40
|
+
}
|
|
41
|
+
/** pg-boss transport: close it to stop the instance it created. */
|
|
42
|
+
interface PgBossTransport extends Transport {
|
|
43
|
+
/** Stops the pg-boss instance, if the transport created it. */
|
|
44
|
+
close(): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
declare const DEFAULT_QUEUE_OPTIONS: QueueOptions;
|
|
47
|
+
/**
|
|
48
|
+
* Creates a transport that inserts messages as jobs in a pg-boss queue
|
|
49
|
+
* (`pg-boss`, peer dependency): Postgres as the queue, no other
|
|
50
|
+
* infrastructure. The job id is the message id.
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* const transport = createPgBossTransport({
|
|
54
|
+
* boss: process.env.DATABASE_URL!,
|
|
55
|
+
* queue: 'events',
|
|
56
|
+
* })
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
declare const createPgBossTransport: (options: PgBossOptions) => PgBossTransport;
|
|
60
|
+
/**
|
|
61
|
+
* Handler of a batch of jobs, with a result per job: completed, failed
|
|
62
|
+
* (retried within the retry limit), or sent to the dead-letter queue.
|
|
63
|
+
*/
|
|
64
|
+
declare const handlerOf: (worker: Worker, deadLetter: () => string | undefined) => (jobs: Job<Envelope>[]) => Promise<JobResult[]>;
|
|
65
|
+
/**
|
|
66
|
+
* Creates a source that works on the pg-boss queue, for `startWorker()` and
|
|
67
|
+
* `dotpress-events work` (Node.js and Bun).
|
|
68
|
+
*
|
|
69
|
+
* ```ts
|
|
70
|
+
* export default {
|
|
71
|
+
* worker: createWorker({ listeners, maxAttempts: 3 }),
|
|
72
|
+
* source: createPgBossSource({ boss: process.env.DATABASE_URL!, queue: 'events' }),
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
declare const createPgBossSource: (options: PgBossSourceOptions) => Source;
|
|
77
|
+
|
|
78
|
+
export { DEFAULT_QUEUE_OPTIONS, createPgBossSource, createPgBossTransport, handlerOf };
|
|
79
|
+
export type { PgBossOptions, PgBossQueueOptions, PgBossSourceOptions, PgBossTransport };
|
package/dist/pg-boss.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// src/transports/pg-boss.ts
|
|
2
|
+
import {
|
|
3
|
+
PgBoss
|
|
4
|
+
} from "pg-boss";
|
|
5
|
+
var DEFAULT_QUEUE_OPTIONS = {
|
|
6
|
+
retryLimit: 2,
|
|
7
|
+
retryDelay: 1,
|
|
8
|
+
retryBackoff: true
|
|
9
|
+
};
|
|
10
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11
|
+
var checkOptions = (label, options, allowed) => {
|
|
12
|
+
if (!isRecord(options)) {
|
|
13
|
+
throw new TypeError(`${label}: expected an object of options.`);
|
|
14
|
+
}
|
|
15
|
+
for (const key of Object.keys(options)) {
|
|
16
|
+
if (!allowed.includes(key)) {
|
|
17
|
+
throw new TypeError(`${label}: unknown option "${key}".`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return options;
|
|
21
|
+
};
|
|
22
|
+
var isBoss = (value) => typeof value?.insert === "function";
|
|
23
|
+
var bossOf = (label, values) => {
|
|
24
|
+
const { boss, queue, queueOptions } = values;
|
|
25
|
+
if (typeof queue !== "string" || queue === "") {
|
|
26
|
+
throw new TypeError(`${label}: queue must be a non-empty string.`);
|
|
27
|
+
}
|
|
28
|
+
if (queueOptions !== void 0 && !isRecord(queueOptions)) {
|
|
29
|
+
throw new TypeError(`${label}: queueOptions must be an object.`);
|
|
30
|
+
}
|
|
31
|
+
const owned = !isBoss(boss);
|
|
32
|
+
if (owned && typeof boss !== "string" && !isRecord(boss)) {
|
|
33
|
+
throw new TypeError(
|
|
34
|
+
`${label}: boss must be a pg-boss instance, a connection string, or pg-boss options.`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const instance = !owned ? boss : typeof boss === "string" ? new PgBoss(boss) : new PgBoss(boss);
|
|
38
|
+
const { deadLetter, ...options } = {
|
|
39
|
+
...DEFAULT_QUEUE_OPTIONS,
|
|
40
|
+
...queueOptions ?? {}
|
|
41
|
+
};
|
|
42
|
+
let ready;
|
|
43
|
+
const prepare = () => ready ??= (async () => {
|
|
44
|
+
if (owned) await instance.start();
|
|
45
|
+
if (deadLetter && !await instance.getQueue(deadLetter)) {
|
|
46
|
+
await instance.createQueue(deadLetter);
|
|
47
|
+
}
|
|
48
|
+
const existing = await instance.getQueue(queue);
|
|
49
|
+
if (!existing) {
|
|
50
|
+
await instance.createQueue(queue, {
|
|
51
|
+
...options,
|
|
52
|
+
...deadLetter ? { deadLetter } : {}
|
|
53
|
+
});
|
|
54
|
+
return { deadLetter };
|
|
55
|
+
}
|
|
56
|
+
return { deadLetter: existing.deadLetter ?? void 0 };
|
|
57
|
+
})().catch((error) => {
|
|
58
|
+
ready = void 0;
|
|
59
|
+
throw error;
|
|
60
|
+
});
|
|
61
|
+
return { instance, owned, queue, prepare };
|
|
62
|
+
};
|
|
63
|
+
var createPgBossTransport = (options) => {
|
|
64
|
+
const label = "createPgBossTransport()";
|
|
65
|
+
const values = checkOptions(label, options, ["boss", "queue", "queueOptions"]);
|
|
66
|
+
const { instance, owned, queue, prepare } = bossOf(label, values);
|
|
67
|
+
return {
|
|
68
|
+
kind: "pg-boss",
|
|
69
|
+
send: async (envelopes, { delay }) => {
|
|
70
|
+
await prepare();
|
|
71
|
+
await instance.insert(
|
|
72
|
+
queue,
|
|
73
|
+
envelopes.map((envelope) => ({
|
|
74
|
+
id: envelope.id,
|
|
75
|
+
data: envelope,
|
|
76
|
+
...delay ? { startAfter: delay } : {}
|
|
77
|
+
}))
|
|
78
|
+
);
|
|
79
|
+
},
|
|
80
|
+
close: async () => {
|
|
81
|
+
if (owned) await instance.stop({ graceful: true });
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
var describe = (error) => error instanceof Error ? error.message : String(error);
|
|
86
|
+
var handlerOf = (worker, deadLetter) => (jobs) => Promise.all(
|
|
87
|
+
jobs.map(async ({ id, data, retryCount }) => {
|
|
88
|
+
const result = await worker.handle(data, { attempt: retryCount + 1 });
|
|
89
|
+
if (result.status === "success") return { id, status: "completed" };
|
|
90
|
+
return {
|
|
91
|
+
id,
|
|
92
|
+
// Without dead-letter queue, dead messages fail until the limit.
|
|
93
|
+
status: result.status === "dead" && deadLetter() ? "deadletter" : "failed",
|
|
94
|
+
output: {
|
|
95
|
+
reason: result.status === "dead" ? result.reason : "listener-error",
|
|
96
|
+
error: describe(result.error)
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
var createPgBossSource = (options) => {
|
|
102
|
+
const label = "createPgBossSource()";
|
|
103
|
+
const values = checkOptions(label, options, [
|
|
104
|
+
"boss",
|
|
105
|
+
"queue",
|
|
106
|
+
"queueOptions",
|
|
107
|
+
"batchSize",
|
|
108
|
+
"pollingIntervalSeconds",
|
|
109
|
+
"closeTimeout"
|
|
110
|
+
]);
|
|
111
|
+
const { instance, owned, queue, prepare } = bossOf(label, values);
|
|
112
|
+
const {
|
|
113
|
+
batchSize = 1,
|
|
114
|
+
pollingIntervalSeconds,
|
|
115
|
+
closeTimeout = 3e4
|
|
116
|
+
} = values;
|
|
117
|
+
if (!Number.isInteger(batchSize) || batchSize < 1) {
|
|
118
|
+
throw new TypeError(`${label}: batchSize must be a positive integer.`);
|
|
119
|
+
}
|
|
120
|
+
if (pollingIntervalSeconds !== void 0 && (typeof pollingIntervalSeconds !== "number" || !(pollingIntervalSeconds >= 0.5))) {
|
|
121
|
+
throw new TypeError(
|
|
122
|
+
`${label}: pollingIntervalSeconds must be a number of seconds, at least 0.5.`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
if (!Number.isInteger(closeTimeout) || closeTimeout < 0) {
|
|
126
|
+
throw new TypeError(
|
|
127
|
+
`${label}: closeTimeout must be a non-negative integer (milliseconds).`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
kind: "pg-boss",
|
|
132
|
+
consume: async (worker, { signal, logger }) => {
|
|
133
|
+
const onError = (error) => {
|
|
134
|
+
logger.error({ queue, error: error.message }, "pg-boss error");
|
|
135
|
+
};
|
|
136
|
+
instance.on("error", onError);
|
|
137
|
+
try {
|
|
138
|
+
const { deadLetter } = await prepare();
|
|
139
|
+
await instance.work(
|
|
140
|
+
queue,
|
|
141
|
+
{
|
|
142
|
+
batchSize,
|
|
143
|
+
perJobResults: true,
|
|
144
|
+
...pollingIntervalSeconds === void 0 ? {} : { pollingIntervalSeconds }
|
|
145
|
+
},
|
|
146
|
+
handlerOf(worker, () => deadLetter)
|
|
147
|
+
);
|
|
148
|
+
await new Promise((resolve) => {
|
|
149
|
+
if (signal.aborted) resolve();
|
|
150
|
+
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
151
|
+
});
|
|
152
|
+
await stopWithin(
|
|
153
|
+
async () => {
|
|
154
|
+
await instance.offWork(queue, { wait: true });
|
|
155
|
+
if (owned) await instance.stop({ graceful: true });
|
|
156
|
+
},
|
|
157
|
+
closeTimeout,
|
|
158
|
+
logger,
|
|
159
|
+
queue
|
|
160
|
+
);
|
|
161
|
+
} finally {
|
|
162
|
+
instance.off("error", onError);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
var stopWithin = async (stop, timeout, logger, queue) => {
|
|
168
|
+
let timer;
|
|
169
|
+
const stopped = await Promise.race([
|
|
170
|
+
stop().then(() => true),
|
|
171
|
+
new Promise((resolve) => {
|
|
172
|
+
timer = setTimeout(() => resolve(false), timeout);
|
|
173
|
+
})
|
|
174
|
+
]);
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
if (!stopped) {
|
|
177
|
+
logger.warn(
|
|
178
|
+
{ queue, closeTimeout: timeout },
|
|
179
|
+
"pg-boss worker not stopped in time: stopped without waiting"
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
export {
|
|
184
|
+
DEFAULT_QUEUE_OPTIONS,
|
|
185
|
+
createPgBossSource,
|
|
186
|
+
createPgBossTransport,
|
|
187
|
+
handlerOf
|
|
188
|
+
};
|
|
189
|
+
//# sourceMappingURL=pg-boss.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/transports/pg-boss.ts"],"sourcesContent":["// pg-boss runs on Node.js and Bun: PgBoss extends the EventEmitter of Node.js.\n/// <reference types=\"node\" />\nimport {\n PgBoss,\n type ConstructorOptions,\n type Job,\n type JobInsert,\n type JobResult,\n type QueueOptions,\n} from 'pg-boss'\nimport type { Envelope } from '../envelope.js'\nimport type { EventsLogger } from '../logger.js'\nimport type { Source, Transport } from '../transport.js'\nimport type { Worker } from '../worker.js'\n\n/** Options of the queue, used when the transport or the source creates it. */\nexport interface PgBossQueueOptions extends QueueOptions {\n /**\n * Queue of the failed jobs (created too): jobs go there after their last\n * attempt, and messages that can never be handled at once.\n */\n deadLetter?: string | undefined\n}\n\n/** Options shared by the transport and the source. */\nexport interface PgBossOptions {\n /**\n * A pg-boss instance (started by you), or the options to create one: a\n * connection string, or the options of `new PgBoss()`.\n */\n boss: PgBoss | string | ConstructorOptions\n /** Name of the queue. */\n queue: string\n /**\n * Options of the queue when it does not exist yet, merged with the\n * defaults: 3 attempts (`retryLimit: 2`), exponential backoff from 1\n * second. Existing queues are not changed.\n */\n queueOptions?: PgBossQueueOptions | undefined\n}\n\n/** Options of `createPgBossSource()`. */\nexport interface PgBossSourceOptions extends PgBossOptions {\n /** Jobs fetched at once, handled in parallel. Defaults to 1. */\n batchSize?: number | undefined\n /** Wait between two fetches of an empty queue, in seconds. */\n pollingIntervalSeconds?: number | undefined\n /**\n * On stop, how long to wait for the jobs in progress, in milliseconds,\n * before stopping anyway. Defaults to 30 seconds.\n */\n closeTimeout?: number | undefined\n}\n\n/** pg-boss transport: close it to stop the instance it created. */\nexport interface PgBossTransport extends Transport {\n /** Stops the pg-boss instance, if the transport created it. */\n close(): Promise<void>\n}\n\nexport const DEFAULT_QUEUE_OPTIONS: QueueOptions = {\n retryLimit: 2,\n retryDelay: 1,\n retryBackoff: true,\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nconst checkOptions = (\n label: string,\n options: unknown,\n allowed: readonly string[]\n): Record<string, unknown> => {\n if (!isRecord(options)) {\n throw new TypeError(`${label}: expected an object of options.`)\n }\n for (const key of Object.keys(options)) {\n if (!allowed.includes(key)) {\n throw new TypeError(`${label}: unknown option \"${key}\".`)\n }\n }\n return options\n}\n\nconst isBoss = (value: unknown): value is PgBoss =>\n typeof (value as PgBoss | null)?.insert === 'function'\n\n/** The pg-boss instance of the options, started and with its queue. */\nconst bossOf = (label: string, values: Record<string, unknown>) => {\n const { boss, queue, queueOptions } = values\n if (typeof queue !== 'string' || queue === '') {\n throw new TypeError(`${label}: queue must be a non-empty string.`)\n }\n if (queueOptions !== undefined && !isRecord(queueOptions)) {\n throw new TypeError(`${label}: queueOptions must be an object.`)\n }\n const owned = !isBoss(boss)\n if (owned && typeof boss !== 'string' && !isRecord(boss)) {\n throw new TypeError(\n `${label}: boss must be a pg-boss instance, a connection string, or pg-boss options.`\n )\n }\n const instance = !owned\n ? (boss as PgBoss)\n : typeof boss === 'string'\n ? new PgBoss(boss)\n : new PgBoss(boss as ConstructorOptions)\n const { deadLetter, ...options } = {\n ...DEFAULT_QUEUE_OPTIONS,\n ...((queueOptions as PgBossQueueOptions | undefined) ?? {}),\n }\n\n let ready: Promise<{ deadLetter: string | undefined }> | undefined\n /** Starts the instance (if owned) and creates the queues, once. */\n const prepare = () =>\n (ready ??= (async () => {\n if (owned) await instance.start()\n if (deadLetter && !(await instance.getQueue(deadLetter))) {\n await instance.createQueue(deadLetter)\n }\n const existing = await instance.getQueue(queue)\n if (!existing) {\n await instance.createQueue(queue, {\n ...options,\n ...(deadLetter ? { deadLetter } : {}),\n })\n return { deadLetter }\n }\n return { deadLetter: existing.deadLetter ?? undefined }\n })().catch((error: unknown) => {\n // Tried again on the next use.\n ready = undefined\n throw error\n }))\n\n return { instance, owned, queue, prepare }\n}\n\n/**\n * Creates a transport that inserts messages as jobs in a pg-boss queue\n * (`pg-boss`, peer dependency): Postgres as the queue, no other\n * infrastructure. The job id is the message id.\n *\n * ```ts\n * const transport = createPgBossTransport({\n * boss: process.env.DATABASE_URL!,\n * queue: 'events',\n * })\n * ```\n */\nexport const createPgBossTransport = (\n options: PgBossOptions\n): PgBossTransport => {\n const label = 'createPgBossTransport()'\n const values = checkOptions(label, options, ['boss', 'queue', 'queueOptions'])\n const { instance, owned, queue, prepare } = bossOf(label, values)\n\n return {\n kind: 'pg-boss',\n send: async (envelopes, { delay }) => {\n await prepare()\n await instance.insert(\n queue,\n envelopes.map((envelope): JobInsert<Envelope> => ({\n id: envelope.id,\n data: envelope,\n ...(delay ? { startAfter: delay } : {}),\n }))\n )\n },\n close: async () => {\n if (owned) await instance.stop({ graceful: true })\n },\n }\n}\n\nconst describe = (error: unknown) =>\n error instanceof Error ? error.message : String(error)\n\n/**\n * Handler of a batch of jobs, with a result per job: completed, failed\n * (retried within the retry limit), or sent to the dead-letter queue.\n */\nexport const handlerOf =\n (worker: Worker, deadLetter: () => string | undefined) =>\n (jobs: Job<Envelope>[]): Promise<JobResult[]> =>\n Promise.all(\n jobs.map(async ({ id, data, retryCount }) => {\n const result = await worker.handle(data, { attempt: retryCount + 1 })\n if (result.status === 'success') return { id, status: 'completed' }\n return {\n id,\n // Without dead-letter queue, dead messages fail until the limit.\n status:\n result.status === 'dead' && deadLetter() ? 'deadletter' : 'failed',\n output: {\n reason: result.status === 'dead' ? result.reason : 'listener-error',\n error: describe(result.error),\n },\n }\n })\n )\n\n/**\n * Creates a source that works on the pg-boss queue, for `startWorker()` and\n * `dotpress-events work` (Node.js and Bun).\n *\n * ```ts\n * export default {\n * worker: createWorker({ listeners, maxAttempts: 3 }),\n * source: createPgBossSource({ boss: process.env.DATABASE_URL!, queue: 'events' }),\n * }\n * ```\n */\nexport const createPgBossSource = (options: PgBossSourceOptions): Source => {\n const label = 'createPgBossSource()'\n const values = checkOptions(label, options, [\n 'boss',\n 'queue',\n 'queueOptions',\n 'batchSize',\n 'pollingIntervalSeconds',\n 'closeTimeout',\n ])\n const { instance, owned, queue, prepare } = bossOf(label, values)\n const {\n batchSize = 1,\n pollingIntervalSeconds,\n closeTimeout = 30_000,\n } = values\n if (!Number.isInteger(batchSize) || (batchSize as number) < 1) {\n throw new TypeError(`${label}: batchSize must be a positive integer.`)\n }\n if (\n pollingIntervalSeconds !== undefined &&\n (typeof pollingIntervalSeconds !== 'number' ||\n !((pollingIntervalSeconds as number) >= 0.5))\n ) {\n throw new TypeError(\n `${label}: pollingIntervalSeconds must be a number of seconds, at least 0.5.`\n )\n }\n if (!Number.isInteger(closeTimeout) || (closeTimeout as number) < 0) {\n throw new TypeError(\n `${label}: closeTimeout must be a non-negative integer (milliseconds).`\n )\n }\n\n return {\n kind: 'pg-boss',\n consume: async (worker, { signal, logger }) => {\n const onError = (error: Error) => {\n logger.error({ queue, error: error.message }, 'pg-boss error')\n }\n // Without a listener, an error event would crash the process.\n instance.on('error', onError)\n try {\n const { deadLetter } = await prepare()\n await instance.work<Envelope>(\n queue,\n {\n batchSize: batchSize as number,\n perJobResults: true,\n ...(pollingIntervalSeconds === undefined\n ? {}\n : { pollingIntervalSeconds: pollingIntervalSeconds as number }),\n },\n handlerOf(worker, () => deadLetter)\n )\n await new Promise<void>((resolve) => {\n if (signal.aborted) resolve()\n signal.addEventListener('abort', () => resolve(), { once: true })\n })\n await stopWithin(\n async () => {\n await instance.offWork(queue, { wait: true })\n if (owned) await instance.stop({ graceful: true })\n },\n closeTimeout as number,\n logger,\n queue\n )\n } finally {\n instance.off('error', onError)\n }\n },\n }\n}\n\n/** Runs `stop`, giving up after `timeout` milliseconds. */\nconst stopWithin = async (\n stop: () => Promise<void>,\n timeout: number,\n logger: EventsLogger,\n queue: string\n) => {\n let timer: ReturnType<typeof setTimeout> | undefined\n const stopped = await Promise.race([\n stop().then(() => true),\n new Promise<false>((resolve) => {\n timer = setTimeout(() => resolve(false), timeout)\n }),\n ])\n clearTimeout(timer)\n if (!stopped) {\n logger.warn(\n { queue, closeTimeout: timeout },\n 'pg-boss worker not stopped in time: stopped without waiting'\n )\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as _dotpress_core from '@dotpress/core';
|
|
2
|
+
import { EventBus } from './index.js';
|
|
3
|
+
import './transport-BtNR1ucF.js';
|
|
4
|
+
import 'zod';
|
|
5
|
+
|
|
6
|
+
declare module '@dotpress/core' {
|
|
7
|
+
interface RequestContext {
|
|
8
|
+
/**
|
|
9
|
+
* Event bus (`eventsPlugin`): logs with the request logger, and adds
|
|
10
|
+
* the request ID to the metadata of emitted events.
|
|
11
|
+
*/
|
|
12
|
+
events: EventBus;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Provides `ctx.events` to routes and middlewares. Emitted events carry the
|
|
17
|
+
* request ID in their metadata (`requestId`), and are logged with the
|
|
18
|
+
* request logger.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* await createApp({ adapter, plugins: [eventsPlugin(events)] })
|
|
22
|
+
*
|
|
23
|
+
* defineRoute({
|
|
24
|
+
* method: 'post',
|
|
25
|
+
* path: '/users',
|
|
26
|
+
* handler: async ({ events, body }) => {
|
|
27
|
+
* const user = await createUser(body)
|
|
28
|
+
* await events.emit(userCreated, { userId: user.id })
|
|
29
|
+
* return user
|
|
30
|
+
* },
|
|
31
|
+
* })
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
declare const eventsPlugin: (events: EventBus) => {
|
|
35
|
+
readonly name: "events";
|
|
36
|
+
readonly events: EventBus;
|
|
37
|
+
readonly setup: (api: _dotpress_core.PluginAPI) => void;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export { eventsPlugin };
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/plugin.ts
|
|
2
|
+
import { declareContextKey, definePlugin } from "@dotpress/core";
|
|
3
|
+
declareContextKey("events", "eventsPlugin");
|
|
4
|
+
var eventsPlugin = (events) => {
|
|
5
|
+
if (typeof events?.withContext !== "function") {
|
|
6
|
+
throw new TypeError(
|
|
7
|
+
"eventsPlugin(): expected an event bus created by createEventBus()."
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
return definePlugin({
|
|
11
|
+
name: "events",
|
|
12
|
+
events,
|
|
13
|
+
setup(api) {
|
|
14
|
+
api.decorateContext(
|
|
15
|
+
"events",
|
|
16
|
+
(ctx) => events.withContext({
|
|
17
|
+
logger: ctx.logger,
|
|
18
|
+
metadata: { requestId: ctx.requestId }
|
|
19
|
+
})
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
export {
|
|
25
|
+
eventsPlugin
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import { declareContextKey, definePlugin } from '@dotpress/core'\nimport type { EventBus } from './bus.js'\n\ndeclare module '@dotpress/core' {\n interface RequestContext {\n /**\n * Event bus (`eventsPlugin`): logs with the request logger, and adds\n * the request ID to the metadata of emitted events.\n */\n events: EventBus\n }\n}\n\n// Accessing ctx.events without the plugin throws an explicit error.\ndeclareContextKey('events', 'eventsPlugin')\n\n/**\n * Provides `ctx.events` to routes and middlewares. Emitted events carry the\n * request ID in their metadata (`requestId`), and are logged with the\n * request logger.\n *\n * ```ts\n * await createApp({ adapter, plugins: [eventsPlugin(events)] })\n *\n * defineRoute({\n * method: 'post',\n * path: '/users',\n * handler: async ({ events, body }) => {\n * const user = await createUser(body)\n * await events.emit(userCreated, { userId: user.id })\n * return user\n * },\n * })\n * ```\n */\nexport const eventsPlugin = (events: EventBus) => {\n if (typeof events?.withContext !== 'function') {\n throw new TypeError(\n 'eventsPlugin(): expected an event bus created by createEventBus().'\n )\n }\n return definePlugin({\n name: 'events',\n events,\n setup(api) {\n api.decorateContext('events', (ctx) =>\n events.withContext({\n logger: ctx.logger,\n metadata: { requestId: ctx.requestId },\n })\n )\n },\n })\n}\n"],"mappings":";AAAA,SAAS,mBAAmB,oBAAoB;AAchD,kBAAkB,UAAU,cAAc;AAqBnC,IAAM,eAAe,CAAC,WAAqB;AAChD,MAAI,OAAO,QAAQ,gBAAgB,YAAY;AAC7C,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO,aAAa;AAAA,IAClB,MAAM;AAAA,IACN;AAAA,IACA,MAAM,KAAK;AACT,UAAI;AAAA,QAAgB;AAAA,QAAU,CAAC,QAC7B,OAAO,YAAY;AAAA,UACjB,QAAQ,IAAI;AAAA,UACZ,UAAU,EAAE,WAAW,IAAI,UAAU;AAAA,QACvC,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF,CAAC;AACH;","names":[]}
|