Skip to content

Commit a6525fa

Browse files
committed
docs: fix README examples and driver imports
- The exponential backoff example wrapped the factory in another function. It failed to typecheck and threw "getNextRetryAt is not a function" on the first retry. - The fixed backoff example passed an options object; fixedBackoff() takes a duration and threw "val.toLowerCase is not a function". - The dedup section suggested ttl: 0 for no expiry, then said it is rejected. It is rejected. - The dedup section promised race-free Knex/Kysely dedup; the partial unique index that makes it race-free only exists on PostgreSQL and SQLite. - The schedule options table now says .id() defaults to the job name, and that scheduling the same job twice replaces the first schedule. - Three JSDoc examples imported redis from the package root, which does not export it. A typed test mirrors the corrected backoff examples so a signature change breaks the build.
1 parent fd254ba commit a6525fa

5 files changed

Lines changed: 47 additions & 24 deletions

File tree

‎README.md‎

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -257,17 +257,17 @@ const { jobId, deduped } = await SaveDraftJob.dispatch({ content: '...' })
257257

258258
- The dedup ID is automatically prefixed with the job name (`SendInvoiceJob::order-123`), so different job types can reuse the same key.
259259
- The user-supplied `id` must be ≤ 400 characters, and the combined `<jobName>::<id>` key must be ≤ 510 characters (constrained by the Knex storage column). Both limits are validated at `.dedup()` time.
260-
- `ttl` accepts a Duration (`'5s'`, `'1m'`) or milliseconds, and must be **positive** when provided. Use `0` or omit `ttl` if you want no expiry — `ttl: 0` is rejected to avoid an ambiguous "expired immediately vs no-expiry" interpretation across engines.
260+
- `ttl` accepts a Duration (`'5s'`, `'1m'`) or milliseconds, and must be **positive** when provided. Omit `ttl` if you want no expiry. `ttl: 0` is rejected to avoid an ambiguous "expired immediately vs no-expiry" interpretation across engines.
261261
- `extend` and `replace` **require** `ttl` — calling them without `ttl` throws.
262262
- `replace` only applies to jobs in `pending` or `delayed` state. Jobs that are active (executing) or retained in history (`completed`/`failed` with retention) are left alone; the dispatch returns `{ deduped: 'skipped' }`.
263263
- `replace` swaps the **payload only** — priority, queue, delay, groupId, and stored dedup options of the existing job are retained. To change those, use a different dedup id or wait for the TTL to expire.
264264
- `extend` resets the TTL clock but never changes the window length. The window length is fixed to the `ttl` from the first dispatch that created the dedup slot. Later dispatches that pass a different `ttl` only reset the clock; their `ttl` value is ignored. To resize the window, let the slot expire and start over with a new dispatch.
265265
- `extend` works in **all states** — even when the existing job is `active` (executing) or retained in history. Unlike `replace` (which is no-op on non-replaceable states), `extend` always refreshes the dedup TTL window. Use this when you want the dedup slot to keep blocking new dispatches for the lifetime of a long-running job.
266266
- `extend` requires the **first** dispatch to have set a `ttl`. If the slot was created without a `ttl`, later `extend` dispatches have no window to refresh and return `{ deduped: 'skipped' }` instead of `'extended'`.
267267
- `retryJob` does not touch the dedup entry — a retried job continues to occupy the dedup slot. TTL runs on wall-clock time, so long-running retries may outlive the TTL window. Use a generous TTL or no TTL if retries must stay deduped.
268-
- Atomic and race-free:
268+
- Atomicity:
269269
- **Redis**: a single Lua script per dispatch performs the dedup-key lookup, state check (pending/delayed ZSCORE), payload swap, and TTL refresh atomically.
270-
- **Knex/Kysely**: transactional `SELECT ... FOR UPDATE` + insert/update inside a transaction. A savepoint catches unique-constraint violations under concurrent inserts and returns `{ deduped: 'skipped' }` pointing at the winner.
270+
- **Knex/Kysely**: transactional `SELECT ... FOR UPDATE` + insert/update inside a transaction. On PostgreSQL and SQLite, a partial unique index makes concurrent first dispatches race-free: a savepoint catches the unique-constraint violation and returns `{ deduped: 'skipped' }` pointing at the winner. MySQL has no partial unique index, see the caveat below.
271271
- **SyncAdapter**: executes inline, no dedup support.
272272

273273
### Caveats
@@ -553,13 +553,12 @@ export default class ReliableJob extends Job<Payload> {
553553
static options: JobOptions = {
554554
maxRetries: 5,
555555
retry: {
556-
backoff: () =>
557-
exponentialBackoff({
558-
baseDelay: '1s',
559-
maxDelay: '1m',
560-
multiplier: 2,
561-
jitter: true,
562-
}),
556+
backoff: exponentialBackoff({
557+
baseDelay: '1s',
558+
maxDelay: '1m',
559+
multiplier: 2,
560+
jitter: true,
561+
}),
563562
},
564563
}
565564
}
@@ -586,7 +585,7 @@ exponentialBackoff({ baseDelay: '1s', maxDelay: '1m', multiplier: 2 })
586585
linearBackoff({ baseDelay: '1s', maxDelay: '30s', multiplier: 1 })
587586

588587
// Fixed: 5s, 5s, 5s...
589-
fixedBackoff({ baseDelay: '5s', jitter: true })
588+
fixedBackoff('5s')
590589
```
591590

592591
</details>
@@ -668,16 +667,19 @@ const redisSchedules = await Schedule.list({}, { adapter: 'redis' })
668667

669668
**Schedule options:**
670669

671-
| Method | Description |
672-
| ------------------- | --------------------------------- |
673-
| `.id(string)` | Unique identifier |
674-
| `.every(duration)` | Fixed interval ('5s', '1m', '1h') |
675-
| `.cron(expression)` | Cron schedule |
676-
| `.timezone(tz)` | Timezone (default: 'UTC') |
677-
| `.from(date)` | Start boundary |
678-
| `.to(date)` | End boundary |
679-
| `.limit(n)` | Maximum runs |
680-
| `.with(adapter)` | Adapter that owns the Schedule |
670+
| Method | Description |
671+
| ------------------- | -------------------------------------------- |
672+
| `.id(string)` | Unique identifier (defaults to the job name) |
673+
| `.every(duration)` | Fixed interval ('5s', '1m', '1h') |
674+
| `.cron(expression)` | Cron schedule |
675+
| `.timezone(tz)` | Timezone (default: 'UTC') |
676+
| `.from(date)` | Start boundary |
677+
| `.to(date)` | End boundary |
678+
| `.limit(n)` | Maximum runs |
679+
| `.with(adapter)` | Adapter that owns the Schedule |
680+
681+
Scheduling the same Job twice without `.id()` replaces the first Schedule, since both use the job
682+
name as their id.
681683

682684
A Schedule and every Job it dispatches stay on the same Adapter. Start a Worker for each Adapter
683685
that owns Schedules.

‎src/contracts/adapter.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export interface AcquiredJob extends JobData {
3636
*
3737
* @example
3838
* ```typescript
39-
* import { redis } from '@boringnode/queue'
39+
* import { redis } from '@boringnode/queue/drivers/redis_adapter'
4040
*
4141
* const config = {
4242
* default: 'redis',

‎src/queue_manager.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ type QueueManagerFakeState = {
3737
*
3838
* @example
3939
* ```typescript
40-
* import { QueueManager, redis } from '@boringnode/queue'
40+
* import { QueueManager, exponentialBackoff } from '@boringnode/queue'
41+
* import { redis } from '@boringnode/queue/drivers/redis_adapter'
4142
*
4243
* await QueueManager.init({
4344
* default: 'redis',

‎src/worker.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ import {
3030
*
3131
* @example
3232
* ```typescript
33-
* import { Worker, redis } from '@boringnode/queue'
33+
* import { Worker } from '@boringnode/queue'
34+
* import { redis } from '@boringnode/queue/drivers/redis_adapter'
3435
*
3536
* const worker = new Worker({
3637
* default: 'redis',

‎tests/backoff_strategy.spec.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
linearBackoff,
88
} from '../src/strategies/backoff_strategy.js'
99
import * as errors from '../src/exceptions.js'
10+
import type { JobOptions } from '../src/types/main.js'
1011

1112
test.group('BackoffStrategy', () => {
1213
test('should validate negative baseDelay', ({ assert }) => {
@@ -239,4 +240,22 @@ test.group('BackoffStrategy | Fixed', () => {
239240
assert.isTrue(nextRetry.getTime() >= now + 1000)
240241
assert.isTrue(nextRetry.getTime() <= after + 1000)
241242
})
243+
244+
test('README retry examples compile and produce retry dates', ({ assert }) => {
245+
// Mirrors the "Retry & Backoff" section of the README.
246+
const options: JobOptions = {
247+
maxRetries: 5,
248+
retry: {
249+
backoff: exponentialBackoff({
250+
baseDelay: '1s',
251+
maxDelay: '1m',
252+
multiplier: 2,
253+
jitter: true,
254+
}),
255+
},
256+
}
257+
258+
assert.instanceOf(options.retry!.backoff!().getNextRetryAt(1), Date)
259+
assert.equal(fixedBackoff('5s')().calculateDelay(3), 5000)
260+
})
242261
})

0 commit comments

Comments
 (0)