@chidchanun/bcp 0.2.9 → 0.2.10

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.
@@ -66,30 +66,38 @@ Related guides: [Authentication](authentication.md), [Auth Session Stores](auth-
66
66
 
67
67
  ## `bcp/jobs`
68
68
 
69
- Server-only Background Jobs and Job Scheduling APIs.
69
+ Server-only Background Jobs, Job Scheduling and Durable Jobs APIs.
70
70
 
71
- Queue APIs:
71
+ Core queue APIs:
72
72
 
73
73
  ```ts
74
74
  import {
75
75
  createJobQueue,
76
76
  createMemoryJobQueueAdapter,
77
77
  type BackgroundJobQueue,
78
+ type CleanupJobsOptions,
79
+ type DeadLetterJobRecord,
78
80
  type EnqueueJobOptions,
81
+ type HeartbeatJobOptions,
79
82
  type JobHandler,
80
83
  type JobHandlerContext,
81
84
  type JobQueueAdapter,
82
85
  type JobQueueOptions,
86
+ type JobQueueStats,
83
87
  type JobRecord,
84
88
  type JobRetryDelay,
85
89
  type JobState,
86
90
  type JobWorker,
87
91
  type MemoryJobQueueAdapter,
92
+ type ProcessNextJobOptions,
93
+ type RecoverStaleJobsOptions,
94
+ type RequeueDeadLetterOptions,
95
+ type ReserveJobOptions,
88
96
  type StartJobWorkerOptions,
89
97
  } from "bcp/jobs";
90
98
  ```
91
99
 
92
- Scheduling APIs added in `0.2.9`:
100
+ Scheduling APIs:
93
101
 
94
102
  ```ts
95
103
  import {
@@ -110,15 +118,68 @@ import {
110
118
  } from "bcp/jobs";
111
119
  ```
112
120
 
113
- `createJobQueue()` provides delayed enqueueing, retry/backoff, cancellation, manual `processNext()` execution and concurrent workers.
121
+ Durable Redis-compatible APIs added in `0.2.10`:
114
122
 
115
- `createJobScheduler()` adds recurring interval and UTC five-field cron schedules. The scheduler leases due records from `JobScheduleStore`, then enqueues normal queue jobs using deterministic occurrence ids.
123
+ ```ts
124
+ import {
125
+ createRedisJobQueueAdapter,
126
+ createRedisJobScheduleStore,
127
+ type RedisCommandClient,
128
+ type RedisJobQueueAdapter,
129
+ type RedisJobsAdapterOptions,
130
+ type RedisJobScheduleStore,
131
+ } from "bcp/jobs";
132
+ ```
133
+
134
+ ### Queue lifecycle
135
+
136
+ `createJobQueue()` supports:
137
+
138
+ ```text
139
+ immediate/delayed enqueue
140
+ retry/backoff
141
+ cancellation
142
+ manual processNext()
143
+ worker concurrency
144
+ visibility timeout
145
+ heartbeat lease renewal
146
+ stale-running recovery
147
+ DLQ inspection/requeue
148
+ terminal retention cleanup
149
+ queue statistics
150
+ ```
151
+
152
+ Durable adapter methods such as `heartbeat()`, `recoverStale()`, `listDeadLetters()`, `requeueDeadLetter()`, `cleanup()` and `stats()` are optional so earlier `JobQueueAdapter` implementations remain compatible.
153
+
154
+ `reserve()` receives an optional second `ReserveJobOptions` argument containing `ownerId` and `visibilityTimeoutMs`. Production durable adapters should use it to create an atomic visibility lease.
155
+
156
+ ### Scheduling
157
+
158
+ `createJobScheduler()` supports recurring interval and UTC five-field cron schedules.
159
+
160
+ The scheduler leases due records from `JobScheduleStore`, then enqueues normal jobs using deterministic occurrence IDs. `nextCronTime()` and `nextScheduleTime()` are available for tooling and tests.
161
+
162
+ ### Redis-compatible durable adapters
163
+
164
+ `createRedisJobQueueAdapter()` implements the queue contract on a shared Redis-compatible command client.
165
+
166
+ `createRedisJobScheduleStore()` implements the schedule-store contract using the same minimal client shape:
167
+
168
+ ```ts
169
+ interface RedisCommandClient {
170
+ sendCommand(
171
+ command: string[]
172
+ ): Promise<unknown>;
173
+ }
174
+ ```
175
+
176
+ BCP does not install a Redis library and does not create the connection. Applications own authentication, TLS, Cluster/Sentinel configuration and connection shutdown.
116
177
 
117
- The built-in queue adapter and schedule store are process-local. Durable multi-instance deployments should implement both `JobQueueAdapter` and `JobScheduleStore` against shared infrastructure. `JobQueueAdapter.reserve()` and `JobScheduleStore.acquireDue()` are the respective atomic concurrency boundaries.
178
+ The reference adapters use atomic Lua state transitions. The default namespace `bcp:{jobs}` uses a Redis Cluster hash tag so all job/schedule keys share one slot.
118
179
 
119
- Cron evaluation in `0.2.9` uses UTC. `nextCronTime()` and `nextScheduleTime()` are available for tooling and tests.
180
+ The processing model is at-least-once. Handlers that perform non-idempotent external side effects should use application-level idempotency protection.
120
181
 
121
- Related guides: [Background Jobs Platform](background-jobs.md), [Job Scheduling Platform](job-scheduling.md), [Observability Platform v2](observability.md).
182
+ Related guides: [Background Jobs Platform](background-jobs.md), [Job Scheduling Platform](job-scheduling.md), [Durable Jobs Platform](durable-jobs.md), [Observability Platform v2](observability.md).
122
183
 
123
184
  ## `bcp/observability`
124
185
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "framework": "bcp",
4
- "versionTarget": "0.2.9",
4
+ "versionTarget": "0.2.10",
5
5
  "releaseState": "unreleased",
6
6
  "sections": [
7
7
  {
@@ -57,7 +57,7 @@
57
57
  {
58
58
  "id": "runtime",
59
59
  "title": "Runtime & Infrastructure",
60
- "description": "Middleware, background jobs, scheduling, observability, logging, caching, security and production hardening.",
60
+ "description": "Middleware, durable background jobs, scheduling, observability, logging, caching, security and production hardening.",
61
61
  "pages": [
62
62
  { "route": "/docs/middleware", "source": "middleware.md", "title": "Middleware" },
63
63
  { "route": "/docs/hydration", "source": "hydration.md", "title": "Hydration" },
@@ -65,6 +65,7 @@
65
65
  { "route": "/docs/observability", "source": "observability.md", "title": "Observability Platform v2" },
66
66
  { "route": "/docs/background-jobs", "source": "background-jobs.md", "title": "Background Jobs Platform" },
67
67
  { "route": "/docs/job-scheduling", "source": "job-scheduling.md", "title": "Job Scheduling Platform" },
68
+ { "route": "/docs/durable-jobs", "source": "durable-jobs.md", "title": "Durable Jobs Platform" },
68
69
  { "route": "/docs/caching", "source": "caching.md", "title": "Caching" },
69
70
  { "route": "/docs/security", "source": "security.md", "title": "Security" },
70
71
  { "route": "/docs/production-hardening", "source": "production-hardening.md", "title": "Production Hardening" }
@@ -110,7 +111,8 @@
110
111
  }
111
112
  ],
112
113
  "releases": [
113
- { "route": "/releases/0.2.9", "source": "releases/0.2.9.md", "version": "0.2.9", "state": "unreleased" },
114
+ { "route": "/releases/0.2.10", "source": "releases/0.2.10.md", "version": "0.2.10", "state": "unreleased" },
115
+ { "route": "/releases/0.2.9", "source": "releases/0.2.9.md", "version": "0.2.9" },
114
116
  { "route": "/releases/0.2.8", "source": "releases/0.2.8.md", "version": "0.2.8" },
115
117
  { "route": "/releases/0.2.7", "source": "releases/0.2.7.md", "version": "0.2.7" },
116
118
  { "route": "/releases/0.2.6", "source": "releases/0.2.6.md", "version": "0.2.6" },
@@ -0,0 +1,359 @@
1
+ # Durable Jobs Platform
2
+
3
+ BCP Framework `0.2.10` extends `bcp/jobs` with production-oriented worker lifecycle primitives, dead-letter handling, retention utilities and Redis-compatible queue/scheduler adapters.
4
+
5
+ The public entrypoint remains:
6
+
7
+ ```ts
8
+ import {
9
+ createJobQueue,
10
+ createJobScheduler,
11
+ } from "bcp/jobs";
12
+ ```
13
+
14
+ `0.2.10` is additive to the queue contract introduced in `0.2.8` and the scheduling contract introduced in `0.2.9`.
15
+
16
+ ## What 0.2.10 adds
17
+
18
+ ```text
19
+ visibility timeout
20
+ worker lease owner
21
+ heartbeat renewal
22
+ stale-running recovery
23
+ dead-letter queue (DLQ)
24
+ dead-letter requeue
25
+ terminal-job cleanup
26
+ queue statistics
27
+ Redis-compatible JobQueueAdapter
28
+ Redis-compatible JobScheduleStore
29
+ ```
30
+
31
+ The built-in memory adapter implements the same lifecycle for development and deterministic tests. Durable multi-process deployments should use a shared adapter such as the Redis-compatible adapter or another implementation of the public contracts.
32
+
33
+ ## Worker visibility timeout
34
+
35
+ Workers now reserve jobs with a visibility lease:
36
+
37
+ ```ts
38
+ const worker =
39
+ jobs.startWorker({
40
+ workerId: "email-worker",
41
+ concurrency: 4,
42
+ visibilityTimeoutMs: 30_000,
43
+ heartbeatIntervalMs: 10_000,
44
+ pollIntervalMs: 250,
45
+ });
46
+ ```
47
+
48
+ A durable adapter can persist:
49
+
50
+ ```text
51
+ leaseOwner
52
+ leaseUntil
53
+ heartbeatAt
54
+ ```
55
+
56
+ While a handler is running, BCP renews the lease when the adapter implements `heartbeat()`.
57
+
58
+ If the process disappears and the lease expires, `recoverStale()` can move the job back to `queued` so another worker can process it.
59
+
60
+ This is an **at-least-once** model. Handlers that perform non-idempotent side effects should use application-level idempotency keys or transactions.
61
+
62
+ ## Manual stale recovery
63
+
64
+ ```ts
65
+ const recovered =
66
+ await jobs.recoverStale({
67
+ limit: 100,
68
+ });
69
+ ```
70
+
71
+ Workers also request stale-job recovery before reserving work when the adapter implements the capability.
72
+
73
+ A recovered job keeps its attempt count. It may therefore reach `maxAttempts` after repeated crashes or visibility-timeout recoveries.
74
+
75
+ ## Dead-letter queue
76
+
77
+ When a handler exhausts `maxAttempts`, the memory and Redis adapters keep the job in terminal `failed` state and also add it to the DLQ index.
78
+
79
+ ```ts
80
+ const failedJobs =
81
+ await jobs.deadLetters();
82
+ ```
83
+
84
+ Each result includes:
85
+
86
+ ```ts
87
+ {
88
+ state: "failed",
89
+ deadLetteredAt: number,
90
+ // normal JobRecord fields
91
+ }
92
+ ```
93
+
94
+ Requeue a DLQ job:
95
+
96
+ ```ts
97
+ await jobs.requeueDeadLetter(
98
+ jobId,
99
+ {
100
+ delayMs: 5_000,
101
+ resetAttempts: true,
102
+ }
103
+ );
104
+ ```
105
+
106
+ `resetAttempts` defaults to `true`.
107
+
108
+ ## Queue statistics
109
+
110
+ ```ts
111
+ const stats =
112
+ await jobs.stats();
113
+ ```
114
+
115
+ Shape:
116
+
117
+ ```ts
118
+ {
119
+ total: number;
120
+ queued: number;
121
+ running: number;
122
+ succeeded: number;
123
+ failed: number;
124
+ cancelled: number;
125
+ deadLetters: number;
126
+ }
127
+ ```
128
+
129
+ Adapters may provide an optimized `stats()` implementation. Otherwise BCP derives statistics from `list()` plus the optional DLQ contract.
130
+
131
+ ## Retention cleanup
132
+
133
+ Terminal jobs can be removed after an application-defined retention window:
134
+
135
+ ```ts
136
+ const sevenDaysAgo =
137
+ Date.now() -
138
+ 7 * 24 * 60 * 60 * 1000;
139
+
140
+ await jobs.cleanup({
141
+ before: sevenDaysAgo,
142
+ states: [
143
+ "succeeded",
144
+ "cancelled",
145
+ ],
146
+ });
147
+ ```
148
+
149
+ If `states` is omitted, cleanup considers:
150
+
151
+ ```text
152
+ succeeded
153
+ failed
154
+ cancelled
155
+ ```
156
+
157
+ For failed jobs, cleanup also removes the DLQ index entry.
158
+
159
+ The Redis reference adapter removes at most 1,000 matching terminal records per cleanup call. Large installations can invoke cleanup repeatedly from a scheduled maintenance job.
160
+
161
+ ## Redis-compatible queue adapter
162
+
163
+ BCP does not install or own a Redis client library. The adapter accepts a minimal command client:
164
+
165
+ ```ts
166
+ interface RedisCommandClient {
167
+ sendCommand(
168
+ command: string[]
169
+ ): Promise<unknown>;
170
+ }
171
+ ```
172
+
173
+ Create the queue adapter:
174
+
175
+ ```ts
176
+ import {
177
+ createJobQueue,
178
+ createRedisJobQueueAdapter,
179
+ } from "bcp/jobs";
180
+
181
+ const adapter =
182
+ createRedisJobQueueAdapter({
183
+ client: redisCommandClient,
184
+ namespace: "my-app:{jobs}",
185
+ });
186
+
187
+ export const jobs =
188
+ createJobQueue({
189
+ adapter,
190
+ });
191
+ ```
192
+
193
+ The default namespace is:
194
+
195
+ ```text
196
+ bcp:{jobs}
197
+ ```
198
+
199
+ The `{jobs}` Redis hash tag keeps all adapter keys in the same Redis Cluster slot so Lua operations can remain atomic.
200
+
201
+ ### Connection lifecycle
202
+
203
+ Applications own the Redis connection. When desired, provide a close hook:
204
+
205
+ ```ts
206
+ const adapter =
207
+ createRedisJobQueueAdapter({
208
+ client: redisCommandClient,
209
+ close: async () => {
210
+ await redisClient.quit();
211
+ },
212
+ });
213
+ ```
214
+
215
+ Then:
216
+
217
+ ```ts
218
+ await jobs.close();
219
+ ```
220
+
221
+ will stop BCP workers and invoke the adapter close hook.
222
+
223
+ If queue and scheduler share one Redis connection, normally provide the close hook to only one owner or coordinate connection shutdown at the application layer.
224
+
225
+ ## Redis environment configuration
226
+
227
+ A typical application can use:
228
+
229
+ ```dotenv
230
+ REDIS_URL=redis://localhost:6379
231
+ ```
232
+
233
+ `bcp/jobs` does **not** read `REDIS_URL` automatically. Connection creation remains application-owned so credentials, TLS, Sentinel/Cluster configuration and client-library choice stay explicit.
234
+
235
+ ## Atomic Redis behavior
236
+
237
+ The reference Redis adapter uses Lua `EVAL` for state-changing operations that require atomicity, including:
238
+
239
+ ```text
240
+ enqueue duplicate protection
241
+ job reservation
242
+ complete/fail/cancel
243
+ heartbeat lease renewal
244
+ stale-running recovery
245
+ DLQ requeue
246
+ retention cleanup
247
+ schedule upsert/remove
248
+ schedule lease acquisition
249
+ schedule completion/release
250
+ ```
251
+
252
+ Queue reservation atomically removes one eligible job from the available index, marks it running and writes the visibility lease before another worker can reserve it.
253
+
254
+ ## Redis-backed scheduler
255
+
256
+ The same Redis command client can back the scheduling contract:
257
+
258
+ ```ts
259
+ import {
260
+ createJobScheduler,
261
+ createRedisJobScheduleStore,
262
+ } from "bcp/jobs";
263
+
264
+ const scheduleStore =
265
+ createRedisJobScheduleStore({
266
+ client: redisCommandClient,
267
+ namespace: "my-app:{jobs}",
268
+ });
269
+
270
+ export const scheduler =
271
+ createJobScheduler({
272
+ queue: jobs,
273
+ store: scheduleStore,
274
+ ownerId: "scheduler-a",
275
+ });
276
+ ```
277
+
278
+ `acquireDue()` is implemented with an atomic Redis lease, allowing multiple scheduler processes to share the same schedule store.
279
+
280
+ ## Recommended production topology
281
+
282
+ ```text
283
+ Web/API instances
284
+ |
285
+ | enqueue / schedule
286
+ v
287
+ Redis-compatible shared job state
288
+ |
289
+ +-------------------+
290
+ | |
291
+ v v
292
+ Worker A Worker B
293
+ visibility lease visibility lease
294
+ heartbeat heartbeat
295
+ |
296
+ +---- failure ----> retry / DLQ
297
+
298
+ Scheduler A -----+
299
+ +---- shared schedule lease
300
+ Scheduler B -----+
301
+ ```
302
+
303
+ Queue workers and schedulers can run in the same Node process for small deployments, but separate worker processes/containers are recommended when job load can affect web latency.
304
+
305
+ ## Adapter compatibility
306
+
307
+ Existing `0.2.8` `JobQueueAdapter` implementations remain valid. Durable methods are optional:
308
+
309
+ ```ts
310
+ heartbeat?()
311
+ recoverStale?()
312
+ listDeadLetters?()
313
+ requeueDeadLetter?()
314
+ cleanup?()
315
+ stats?()
316
+ ```
317
+
318
+ The `reserve()` method now receives an optional second argument with the worker owner ID and visibility timeout. Existing JavaScript adapters that ignore the optional argument continue to work.
319
+
320
+ For production durable adapters, implementations should support the lease contract rather than ignoring it.
321
+
322
+ ## Failure model
323
+
324
+ BCP jobs intentionally use practical at-least-once delivery semantics.
325
+
326
+ Applications should assume that a job can execute more than once when:
327
+
328
+ - a worker completes external side effects but crashes before recording success,
329
+ - a visibility lease expires during a long or blocked handler,
330
+ - a DLQ job is manually requeued,
331
+ - infrastructure retries a command after an ambiguous network result.
332
+
333
+ Use idempotency keys, unique database constraints or transactional outbox/inbox patterns for side effects where duplicates are unsafe.
334
+
335
+ ## Observability
336
+
337
+ `jobs.stats()` can feed application metrics from `bcp/observability`:
338
+
339
+ ```ts
340
+ const stats =
341
+ await jobs.stats();
342
+
343
+ queueDepth.set(
344
+ stats.queued
345
+ );
346
+
347
+ dlqDepth.set(
348
+ stats.deadLetters
349
+ );
350
+ ```
351
+
352
+ For Redis deployments, infrastructure-level Redis monitoring should complement application-level BCP metrics.
353
+
354
+ ## Related guides
355
+
356
+ - [Background Jobs Platform](background-jobs.md)
357
+ - [Job Scheduling Platform](job-scheduling.md)
358
+ - [Observability Platform v2](observability.md)
359
+ - [Production Hardening](production-hardening.md)
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "framework": "bcp",
4
- "version": "0.2.9",
4
+ "version": "0.2.10",
5
5
  "releaseState": "unreleased",
6
- "baseline": "job-scheduling-platform",
6
+ "baseline": "durable-jobs-platform",
7
7
  "runtime": {
8
8
  "node": ">=24.11.0",
9
9
  "react": "19",
@@ -80,6 +80,16 @@
80
80
  "jobScheduleStoreContract": true,
81
81
  "schedulerLeases": true,
82
82
  "scheduledRunDeduplication": true,
83
+ "durableJobsPlatform": true,
84
+ "redisJobQueueAdapter": true,
85
+ "redisJobScheduleStore": true,
86
+ "jobVisibilityTimeout": true,
87
+ "jobHeartbeats": true,
88
+ "staleJobRecovery": true,
89
+ "deadLetterQueue": true,
90
+ "deadLetterRequeue": true,
91
+ "jobRetentionCleanup": true,
92
+ "jobQueueStatistics": true,
83
93
  "databaseMigrations": true,
84
94
  "databaseAdapterContract": true,
85
95
  "databasePostgresql": true,
@@ -119,7 +129,7 @@
119
129
  "s3-compatible"
120
130
  ],
121
131
  "compatibility": {
122
- "previousBaseline": "0.2.8",
132
+ "previousBaseline": "0.2.9",
123
133
  "intentionalBreakingChangesFromPreviousBaseline": false,
124
134
  "migrationGuide": "migration-0.2.md"
125
135
  },
@@ -138,7 +148,8 @@
138
148
  "observability": "observability.md",
139
149
  "backgroundJobs": "background-jobs.md",
140
150
  "jobScheduling": "job-scheduling.md",
151
+ "durableJobs": "durable-jobs.md",
141
152
  "migrationGuide": "migration-0.2.md",
142
- "releaseNotes": "releases/0.2.9.md"
153
+ "releaseNotes": "releases/0.2.10.md"
143
154
  }
144
155
  }
@@ -0,0 +1,148 @@
1
+ # 0.2.10 — Durable Jobs Platform
2
+
3
+ **State:** unreleased development target
4
+
5
+ BCP Framework `0.2.10` extends the Background Jobs and Job Scheduling platforms with durable worker lifecycle contracts, dead-letter handling, retention/statistics APIs and Redis-compatible reference adapters.
6
+
7
+ ## Highlights
8
+
9
+ ### Durable worker leases
10
+
11
+ Workers can reserve jobs with a visibility timeout and stable worker identity.
12
+
13
+ New job record metadata:
14
+
15
+ ```text
16
+ leaseOwner
17
+ leaseUntil
18
+ heartbeatAt
19
+ recoveredAt
20
+ ```
21
+
22
+ When an adapter supports `heartbeat()`, BCP workers renew the lease while a handler is running.
23
+
24
+ When an adapter supports `recoverStale()`, expired running jobs can be returned to the queue for at-least-once recovery.
25
+
26
+ ### Dead-letter queue
27
+
28
+ Retry-exhausted jobs remain terminal `failed` jobs and can also be indexed in the adapter DLQ.
29
+
30
+ New queue APIs:
31
+
32
+ ```ts
33
+ jobs.deadLetters()
34
+ jobs.requeueDeadLetter()
35
+ ```
36
+
37
+ The built-in memory adapter and Redis-compatible adapter implement both APIs.
38
+
39
+ ### Queue maintenance
40
+
41
+ New operational APIs:
42
+
43
+ ```ts
44
+ jobs.stats()
45
+ jobs.cleanup()
46
+ jobs.recoverStale()
47
+ ```
48
+
49
+ Statistics include queue state totals and DLQ depth.
50
+
51
+ Cleanup can remove old succeeded, failed and cancelled records after an application-defined retention period.
52
+
53
+ ### Redis-compatible queue adapter
54
+
55
+ New public API:
56
+
57
+ ```ts
58
+ createRedisJobQueueAdapter()
59
+ ```
60
+
61
+ The adapter consumes a minimal application-owned Redis command client exposing:
62
+
63
+ ```ts
64
+ sendCommand(command: string[]): Promise<unknown>
65
+ ```
66
+
67
+ BCP does not add a Redis client runtime dependency.
68
+
69
+ The Redis adapter uses atomic Lua operations for:
70
+
71
+ - duplicate-safe enqueue,
72
+ - job reservation,
73
+ - complete/fail/cancel transitions,
74
+ - heartbeat lease renewal,
75
+ - stale-job recovery,
76
+ - DLQ requeue,
77
+ - retention cleanup.
78
+
79
+ The default Redis namespace is `bcp:{jobs}` so all adapter keys share one Redis Cluster hash slot.
80
+
81
+ ### Redis-compatible scheduler store
82
+
83
+ New public API:
84
+
85
+ ```ts
86
+ createRedisJobScheduleStore()
87
+ ```
88
+
89
+ The scheduler store implements the `0.2.9` `JobScheduleStore` contract on shared Redis state and uses atomic schedule leasing for multi-instance schedulers.
90
+
91
+ ### Compiled npm runtime
92
+
93
+ The published `bcp/jobs` default runtime continues to use generated `jobs.mjs` rather than requiring consumers to execute framework TypeScript directly.
94
+
95
+ The Durable Jobs package smoke imports that compiled runtime and verifies the new public APIs.
96
+
97
+ ## Public API additions
98
+
99
+ `bcp/jobs` adds:
100
+
101
+ ```text
102
+ createRedisJobQueueAdapter
103
+ createRedisJobScheduleStore
104
+ RedisCommandClient
105
+ RedisJobQueueAdapter
106
+ RedisJobScheduleStore
107
+ RedisJobsAdapterOptions
108
+
109
+ CleanupJobsOptions
110
+ DeadLetterJobRecord
111
+ HeartbeatJobOptions
112
+ JobQueueStats
113
+ ProcessNextJobOptions
114
+ RecoverStaleJobsOptions
115
+ RequeueDeadLetterOptions
116
+ ReserveJobOptions
117
+ ```
118
+
119
+ Existing queue and scheduling APIs remain available.
120
+
121
+ ## Compatibility
122
+
123
+ The release is intended to be backward compatible with `0.2.9`.
124
+
125
+ Existing `JobQueueAdapter` implementations remain valid because durable capabilities are optional and the new reservation metadata is provided as an optional second parameter to `reserve()`.
126
+
127
+ Adapters that need production visibility-timeout semantics should implement the new durable methods.
128
+
129
+ ## Delivery semantics
130
+
131
+ Durable jobs use at-least-once processing semantics.
132
+
133
+ Applications should make side-effecting handlers idempotent when duplicate execution would be unsafe.
134
+
135
+ ## Validation target
136
+
137
+ Before tagging/publishing `0.2.10`, run:
138
+
139
+ ```bash
140
+ npm run typecheck
141
+ npm run test:unit
142
+ npm run test:integration
143
+ npm run test:e2e
144
+ npm run test:package
145
+ npm run rc:check
146
+ ```
147
+
148
+ The final release tag must point to the exact commit that passed the complete RC sequence.