@iskra-bun/worker-kit 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,73 @@
1
1
  # @iskra-bun/worker-kit
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3579944: **Security:** runtime hardening from the second audit round.
8
+
9
+ - `process-kit` (**breaking**): a child no longer inherits the app's whole environment. It gets the variables programs need and that carry no secrets (`PATH`, `HOME`, `USER`, `SHELL`, `TERM`, the locale, `TZ`, the temp dir, `NODE_ENV`, and the Windows essentials), plus `env`: `DATABASE_URL`, `AUTH_SECRET`, cloud keys and whatever was loaded from `.env` reached every child, third-party code included. The new `inheritEnv` option (validated by the core config schema, as is `maxPendingStdinBytes`) takes more names to pass, or `true` for all of them as before.
10
+ - `process-kit`: `send()` refuses a message, with one warning until the child catches up, when the bytes still waiting for a child that is not reading its stdin would go over `maxPendingStdinBytes` (8 MiB by default). They piled up in the app's memory without a bound: 256 MiB sent to such a child grew RSS by 263 MiB. `send()` now resolves to whether the message was sent (`false` for every refusal).
11
+ - `core`: OpenTelemetry. The options of each entry of `otel.instrumentations` reach the instrumentation (the config schema kept only `enabled`, so hooks and `redactedQueryParams` were dropped). HTTP spans export URLs with the values of secret-looking query parameters (`SECRET_QUERY_PARAMS`: `token`, `access_token`, `api_key`, `key`, `code`, `state`, `sig`, `X-Amz-Signature`…) replaced by `REDACTED`, through instrumentation-http's `redactedQueryParams` / `redactedQueryParamsServer` (which the app can set) and a `requestHook` for releases without them; the app's own `requestHook` still runs. The startup log shows only the endpoint's origin (its path, query or password can be an API key), and a plain `http://` endpoint on a remote host logs a warning. New exports: `SECRET_QUERY_PARAMS`, `redactUrl()`, `autoInstrumentationOptions()`, `describeOtelEndpoint()`.
12
+ - `web-kit`: `OtelTracingFeature` exported `url.full` as requested (@hono/otel sets it to `c.req.url`): the `?token=` of an email verification link, the token of better-auth's `/reset-password/<token>` and `?api_key=` reached the collector. The values of `SECRET_QUERY_PARAMS` (or the new `redactedQueryParams`) and that path token are now `REDACTED`. New `ignoreIncomingTraceContext` option to start a new trace per request instead of continuing the client's `traceparent` (default unchanged). `@opentelemetry/api` is now a direct dependency (it already came with `@hono/otel`).
13
+ - `web-kit`: `OpenAPIFeature`'s `/docs` page loaded `@scalar/api-reference@latest` on the app's origin. It now loads a pinned release (1.68.0) with its SRI hash and `crossorigin`, sends a Content-Security-Policy (scripts from that host only; requests only to the app and the spec's `servers`), turns off Scalar's web fonts and AI agent (which sends the spec to Scalar's servers), and HTML-escapes the title. New options: `docs: false` serves neither `/openapi.json` nor `/docs`; `authorize(c)` gates both (they are registered before middleware added after `initialize()`, so a `basicAuth()` there did not cover them); `scalar: { src, integrity }` or `false`.
14
+ - `web-kit` (**breaking**): `HealthCheckFeature`'s `/health/ready` lists the check names (`checks`, `failed`) and `/health/live` the `uptime` only with `includeDetails: true`, like `/health`; the names of failed readiness checks are logged instead.
15
+ - `worker-kit` (**breaking**): finished jobs are no longer kept in Redis forever with their payloads (BullMQ's default when `removeOnComplete`/`removeOnFail` are unset, which worker-kit never set). The queue keeps the last 1000 completed jobs and the failed ones of the last 7 days (at most 5000); `defaultJobOptions` or a job's options override it (`false` keeps them all), and both options now take BullMQ's `{ age, count }` form. `result()` of a job removed since rejects. The dead-letter example in the docs logged the whole payload with `console.error`, outside the logger's redaction; it logs ids now.
16
+
17
+ - fd36d6d: Job routing and connection fixes.
18
+
19
+ - A job with no registered handler used to be marked **completed** (the worker returned early), so it silently disappeared. It now fails with BullMQ's `UnrecoverableError` (no pointless retries), stays in the failed set, and is dead-lettered when `deadLetter` is on.
20
+ - New `consume: false` option for producer-only processes (e.g. an API node): `start()` creates no `Worker`, and `enqueue` accepts jobs whose handler lives in another process. Previously every producer had to register the handler and therefore also consume jobs.
21
+ - Redis URLs keep the ACL username, percent-decode credentials, and `rediss://` enables TLS.
22
+ - BullMQ queue/worker connection errors go to the app logger instead of the console.
23
+
24
+ ### Patch Changes
25
+
26
+ - 840439a: Packages declare the runtime they are tested on: `engines.bun` `>=1.3.0` (the monorepo now builds and tests on Bun 1.3). `create-iskra`, a CLI that also runs under `npm create iskra`, declares `engines.node` `>=18`.
27
+
28
+ Every package is published with an npm provenance attestation (`publishConfig.provenance`), linking each version to the commit and CI run that built it.
29
+
30
+ - cb3ec43: Register what each kit puts on the app with core's new registries: `app.context.get('db' | 'kv' | 'oracle')` returns the kit's driver, and the `process:*`, `socket:connected` / `socket:disconnected` and `worker:dead-letter` events have typed payloads. `ProcessManager.send()` takes `unknown` data.
31
+ - 87f6de2: `KVManager` throws when its constructor gets `adapter`, `driver` or `connection`: the store is chosen by the App config (`kv: { driver, connection }`), and the README's `new KVManager({ adapter: 'redis' })` was silently ignored, leaving the app on per-process memory. Without a `kv` driver it now logs a warning in production instead of an info line. The READMEs of kv-kit, worker-kit (`connection` and `queueName`, not `queue`), db-kit (the App's `db` config) and process-kit (the App's `processes` config) show working examples.
32
+ - 58d4a8f: `concurrency: 0` now means producer-only, like `consume: false`. It used to fall back to a concurrency of 1, so a service that set it to only enqueue (forms-app's forms-api did) also consumed jobs from the queue it had no handler for, and those jobs were lost.
33
+ - ee559ec: Per-job options no longer erase `defaultJobOptions`: every unset option was passed to BullMQ as `undefined`, which overrides the queue default, so a job enqueued with just `{ priority }` or `{ delay }`, and every job from `schedule()`, lost its `attempts`, `backoff` and `removeOnComplete`/`removeOnFail` (no retries, and completed repeat jobs kept in Redis forever). IPv6 Redis URLs (`redis://[::1]:6379`) now connect: the host kept its brackets.
34
+ - fba319a: A job whose handler throws is logged once at error level ("Job failed", now with `attemptsMade`) instead of twice: the processor also logged a `JobError` at error level before BullMQ's `failed` event logged it again. That processor log is now debug.
35
+ - Updated dependencies [620da18]
36
+ - Updated dependencies [b635a2c]
37
+ - Updated dependencies [5b2b0fd]
38
+ - Updated dependencies [58d4a8f]
39
+ - Updated dependencies [5c70c5b]
40
+ - Updated dependencies [ec198d4]
41
+ - Updated dependencies [cb3ec43]
42
+ - Updated dependencies [ef2009b]
43
+ - Updated dependencies [840439a]
44
+ - Updated dependencies [dbf8817]
45
+ - Updated dependencies [3dc5581]
46
+ - Updated dependencies [9872d30]
47
+ - Updated dependencies [f2346f5]
48
+ - Updated dependencies [3579944]
49
+ - @iskra-bun/core@0.2.0
50
+
51
+ ## 0.2.0
52
+
53
+ ### Minor Changes
54
+
55
+ - f9654df: New worker features:
56
+
57
+ - Scheduled/repeat jobs: a `repeat` option on `JobOptions` plus a `schedule(name, data, repeat, opts?)` convenience for cron/interval jobs.
58
+ - Dead-letter handling: opt-in `deadLetter` emits a `worker:dead-letter` event with the job and `failedReason` once retries are exhausted.
59
+ - Job results: handlers may return a value (`JobHandler<T, R>`); the enqueue descriptor exposes a `result()` helper backed by BullMQ `QueueEvents`.
60
+
61
+ ### Patch Changes
62
+
63
+ - f9654df: `register`, `enqueue`, and `JobHandler` are now generic over the job payload type, so a handler's `job.data` and the enqueued payload are typed instead of `any`. Defaults to `unknown`, so existing call sites compile unchanged.
64
+ - f9654df: `stop()` now drains in-flight jobs by fully closing the worker before closing the queue, instead of closing both concurrently (which could leave a running job stuck in the `active` state).
65
+ - Fix a connection leak after `stop()`. `stop()` now sets the stopped flag first, and `getQueueEvents()` throws `WorkerManager is stopped; cannot open QueueEvents` rather than lazily opening a new orphaned connection. Job descriptors created after stop reject instead of silently holding an open connection.
66
+ - Updated dependencies [f9654df]
67
+ - Updated dependencies
68
+ - Updated dependencies [f9654df]
69
+ - @iskra-bun/core@0.1.1
70
+
3
71
  ## 0.1.0
4
72
 
5
73
  ### Minor Changes
package/README.md CHANGED
@@ -17,14 +17,20 @@ import { App } from '@iskra-bun/core'
17
17
  import { WorkerManager } from '@iskra-bun/worker-kit'
18
18
 
19
19
  const app = new App({ name: 'mi-worker' })
20
- app.register(new WorkerManager({ queue: 'mis-jobs', concurrency: 2 }))
20
+ app.register(
21
+ new WorkerManager({
22
+ connection: process.env.REDIS_URL ?? 'redis://localhost:6379',
23
+ queueName: 'mis-jobs',
24
+ concurrency: 2,
25
+ }),
26
+ )
21
27
 
22
28
  await app.start()
23
29
  ```
24
30
 
25
31
  ## Documentacion
26
32
 
27
- Guia completa: [docs/worker-kit.md](../../docs/worker-kit.md)
33
+ Guia completa: [@iskra-bun/worker-kit](https://iskra-docs.fly.dev/es/packages/worker-kit/)
28
34
 
29
35
  ## Licencia
30
36
 
package/dist/index.d.ts CHANGED
@@ -1,20 +1,49 @@
1
1
  import { IskraError, Driver, App } from '@iskra-bun/core';
2
+ import { KeepJobs } from 'bullmq';
2
3
 
3
4
  interface WorkerManagerOptions {
4
- /** URL de conexión a Redis (ej: 'redis://localhost:6379') */
5
+ /** URL de conexión a Redis (ej: 'redis://user:pass@localhost:6379/0'; `rediss://` activa TLS) */
5
6
  connection: string | {
6
7
  host: string;
7
8
  port: number;
9
+ username?: string;
8
10
  password?: string;
9
11
  db?: number;
12
+ tls?: object;
10
13
  };
11
- /** Cantidad de jobs que se procesan en paralelo (default: 1) */
14
+ /**
15
+ * `false` = solo productor: `start()` no crea un Worker y `enqueue` acepta
16
+ * jobs sin handler local (los procesa otro proceso). Default: true.
17
+ */
18
+ consume?: boolean;
19
+ /** Cantidad de jobs que se procesan en paralelo (default: 1). `0` = solo productor, como `consume: false`. */
12
20
  concurrency?: number;
13
21
  /** Nombre de la queue en Redis (default: 'iskra-jobs') */
14
22
  queueName?: string;
15
23
  /** Opciones por defecto para cada job */
16
24
  defaultJobOptions?: JobOptions;
25
+ /**
26
+ * Activa el ruteo a dead-letter: cuando un job agota todos sus reintentos
27
+ * se emite el evento `worker:dead-letter` en el bus de eventos de la App.
28
+ * Opt-in para no cambiar el comportamiento existente (default: false).
29
+ */
30
+ deadLetter?: boolean;
17
31
  }
32
+ /**
33
+ * Especificación de repetición para jobs programados.
34
+ *
35
+ * - Un string se interpreta como un patrón cron (ej: '0 0 * * *').
36
+ * - `{ every: ms }` repite cada `ms` milisegundos.
37
+ * - `{ pattern: cron }` repite según el patrón cron, con opciones extra.
38
+ */
39
+ type RepeatSpec = string | {
40
+ every: number;
41
+ limit?: number;
42
+ } | {
43
+ pattern: string;
44
+ limit?: number;
45
+ tz?: string;
46
+ };
18
47
  interface JobOptions {
19
48
  /** Reintentos en caso de fallo */
20
49
  attempts?: number;
@@ -27,17 +56,59 @@ interface JobOptions {
27
56
  type: 'fixed' | 'exponential';
28
57
  delay: number;
29
58
  };
30
- /** Eliminar el job de Redis al completarse */
31
- removeOnComplete?: boolean | number;
32
- /** Eliminar el job de Redis al fallar */
33
- removeOnFail?: boolean | number;
59
+ /**
60
+ * Jobs completados que quedan en Redis, con sus datos: `true` los borra,
61
+ * un número conserva los últimos N, `{ age, count }` por edad (s) y
62
+ * cantidad. Default `{ count: 1000 }`; `false` los conserva todos.
63
+ */
64
+ removeOnComplete?: boolean | number | KeepJobs;
65
+ /**
66
+ * Jobs fallidos (sin reintentos pendientes) que quedan en Redis, igual que
67
+ * removeOnComplete. Default `{ age: 7 días, count: 5000 }`; `false` los conserva todos.
68
+ */
69
+ removeOnFail?: boolean | number | KeepJobs;
70
+ /**
71
+ * Programa el job como repetible (cron o intervalo).
72
+ * Se reenvía a la opción `repeat` de BullMQ.
73
+ */
74
+ repeat?: RepeatSpec;
34
75
  }
35
- type JobHandler = (job: {
76
+ /**
77
+ * Handler de un job. Puede devolver un valor `R` que queda disponible como
78
+ * resultado del job (recuperable vía `job.waitUntilFinished`). Devolver `void`
79
+ * sigue siendo válido (R por defecto es `void`).
80
+ */
81
+ type JobHandler<T = unknown, R = void> = (job: {
36
82
  id: string;
37
83
  name: string;
38
- data: any;
84
+ data: T;
85
+ attemptsMade: number;
86
+ }) => Promise<R>;
87
+ /**
88
+ * Payload del evento `worker:dead-letter`, emitido cuando un job agota todos
89
+ * sus reintentos y `deadLetter` está activado.
90
+ */
91
+ interface DeadLetterPayload {
92
+ jobId: string | undefined;
93
+ name: string | undefined;
94
+ data: unknown;
95
+ failedReason: string | undefined;
39
96
  attemptsMade: number;
40
- }) => Promise<void>;
97
+ }
98
+ /**
99
+ * Descriptor devuelto por `enqueue`/`schedule`. Además de los datos del job,
100
+ * expone `result()` para esperar el valor de retorno del handler.
101
+ */
102
+ interface JobDescriptor<T = unknown, R = unknown> {
103
+ id: string;
104
+ name: string;
105
+ data: T;
106
+ /**
107
+ * Espera a que el job termine y resuelve con el valor que devolvió el
108
+ * handler. Lanza si el job falló. Requiere una conexión a Redis viva.
109
+ */
110
+ result(ttlMs?: number): Promise<R>;
111
+ }
41
112
 
42
113
  declare class QueueError extends IskraError {
43
114
  constructor(message: string, options?: {
@@ -58,25 +129,89 @@ declare class WorkerManager implements Driver {
58
129
  private handlers;
59
130
  private queue;
60
131
  private worker;
132
+ private queueEvents;
133
+ private stopped;
61
134
  private options;
135
+ /** Tope de tamaño (bytes) del payload serializado de un job. */
136
+ private static readonly MAX_PAYLOAD_BYTES;
62
137
  constructor(options: WorkerManagerOptions);
63
138
  init(app: App): Promise<void>;
64
139
  /**
65
- * Registra un handler para un tipo de job.
140
+ * Registra un handler para un tipo de job. El handler puede devolver un
141
+ * valor `R` que queda disponible como resultado del job.
142
+ */
143
+ register<T = unknown, R = void>(jobName: string, handler: JobHandler<T, R>): this;
144
+ /**
145
+ * Encola un job para ser procesado. Devuelve un descriptor que, además de
146
+ * los datos del job, expone `result()` para esperar el valor de retorno del
147
+ * handler.
148
+ */
149
+ enqueue<T = unknown, R = unknown>(name: string, data: T, opts?: JobOptions): Promise<JobDescriptor<T, R>>;
150
+ /**
151
+ * Programa un job repetible (cron o intervalo). Conveniencia sobre
152
+ * `enqueue` con la opción `repeat` ya configurada.
153
+ *
154
+ * @param repeat patrón cron (string) o `{ every: ms }`/`{ pattern: cron }`.
66
155
  */
67
- register(jobName: string, handler: JobHandler): this;
156
+ schedule<T = unknown, R = unknown>(name: string, data: T, repeat: RepeatSpec, opts?: JobOptions): Promise<JobDescriptor<T, R>>;
68
157
  /**
69
- * Encola un job para ser procesado.
158
+ * `consume: false`, or `concurrency: 0` (which used to fall back to 1, so
159
+ * a service meant to only enqueue consumed jobs it had no handler for).
70
160
  */
71
- enqueue(name: string, data: any, opts?: JobOptions): Promise<{
72
- id: string;
73
- name: string;
74
- data: any;
75
- }>;
161
+ private get producerOnly();
76
162
  start(): Promise<void>;
77
163
  stop(): Promise<void>;
164
+ private logConnectionError;
165
+ /**
166
+ * Turns a redis:// or rediss:// URL into ioredis options, keeping the ACL
167
+ * username, the percent-decoded password, the db index and TLS (rediss).
168
+ */
78
169
  private parseConnection;
170
+ /**
171
+ * Only the options that were given: BullMQ merges `{ ...defaultJobOptions,
172
+ * ...opts }`, so an explicit `undefined` erased the queue default (a job
173
+ * enqueued with just `{ priority }`, and every scheduled job, lost its
174
+ * attempts, backoff and removeOn* settings).
175
+ */
79
176
  private mapJobOptions;
177
+ /**
178
+ * Normaliza una RepeatSpec a la forma `repeat` de BullMQ:
179
+ * - string → `{ pattern: cron }`
180
+ * - `{ every }` / `{ pattern }` → se reenvían tal cual.
181
+ */
182
+ private mapRepeat;
183
+ /**
184
+ * Valida la entrada de `enqueue` ANTES de tocar Redis, para evitar que
185
+ * entrada no confiable inunde la queue, almacene payloads gigantes o
186
+ * programe repeticiones malformadas. Lanza `QueueError` ante cualquier
187
+ * problema; no muta nada.
188
+ */
189
+ private validateEnqueue;
190
+ /** Rechaza payloads cuya serialización JSON excede el tope configurado. */
191
+ private validatePayloadSize;
192
+ /** Rechaza specs de repetición vacías, intervalos no positivos o crons en blanco. */
193
+ private validateRepeat;
194
+ /**
195
+ * Maneja el evento `failed` del worker. Loggea el fallo y, si el job agotó
196
+ * todos sus reintentos y `deadLetter` está activado, emite
197
+ * `worker:dead-letter` en el bus de eventos de la App.
198
+ */
199
+ private onFailed;
200
+ /**
201
+ * Construye el descriptor de un job, incluyendo el helper `result()` que
202
+ * espera el valor de retorno del handler vía `job.waitUntilFinished`.
203
+ */
204
+ private buildDescriptor;
205
+ /**
206
+ * Devuelve (creando perezosamente) una instancia compartida de QueueEvents
207
+ * usada para esperar resultados de jobs.
208
+ */
209
+ private getQueueEvents;
210
+ }
211
+ declare module '@iskra-bun/core' {
212
+ interface AppEvents {
213
+ 'worker:dead-letter': DeadLetterPayload;
214
+ }
80
215
  }
81
216
 
82
- export { JobError, type JobHandler, type JobOptions, QueueError, WorkerManager, type WorkerManagerOptions };
217
+ export { type DeadLetterPayload, type JobDescriptor, JobError, type JobHandler, type JobOptions, QueueError, type RepeatSpec, WorkerManager, type WorkerManagerOptions };