bunqueue-client 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/canonical-manifest.json +57 -53
  3. package/dist/embedded.js +1353 -1261
  4. package/dist/index.js +52 -4
  5. package/dist/legacy.js +1 -1
  6. package/dist/types/sdk/typescript/src/index.d.ts +1 -1
  7. package/dist/types/sdk/typescript/src/legacy.d.ts +1 -1
  8. package/dist/types/src/application/clientOwnership.d.ts +65 -0
  9. package/dist/types/src/application/clientTracking.d.ts +7 -11
  10. package/dist/types/src/application/operations/jobClaim.d.ts +2 -2
  11. package/dist/types/src/application/operations/jobManagement.d.ts +2 -0
  12. package/dist/types/src/application/orphanRecovery.d.ts +25 -0
  13. package/dist/types/src/application/queue-manager/state.d.ts +2 -0
  14. package/dist/types/src/application/stallDetection.d.ts +16 -0
  15. package/dist/types/src/application/types/contextFactory.d.ts +2 -0
  16. package/dist/types/src/application/types/contexts.d.ts +3 -0
  17. package/dist/types/src/application/types/queueManager.d.ts +2 -0
  18. package/dist/types/src/client/forwarder.d.ts +4 -3
  19. package/dist/types/src/client/types/options.d.ts +2 -0
  20. package/dist/types/src/domain/job/constants.d.ts +2 -0
  21. package/dist/types/src/domain/job/create.d.ts +7 -0
  22. package/dist/types/src/domain/job/locks.d.ts +13 -1
  23. package/dist/types/src/domain/job/state.d.ts +15 -0
  24. package/dist/types/src/domain/types/commands/core.d.ts +2 -5
  25. package/dist/types/src/domain/types/cron.d.ts +2 -4
  26. package/dist/types/src/domain/types/jobs/model.d.ts +1 -4
  27. package/package.json +1 -1
  28. package/src/index.ts +1 -1
  29. package/src/legacy.ts +1 -1
package/dist/index.js CHANGED
@@ -187,6 +187,18 @@ class BatchExecution {
187
187
  };
188
188
  }
189
189
  }
190
+ // ../../src/domain/job/constants.ts
191
+ var DEFAULT_MAX_BACKOFF = 3600000;
192
+ var JOB_DEFAULTS = {
193
+ priority: 0,
194
+ maxAttempts: 3,
195
+ backoff: 1000,
196
+ lifo: false,
197
+ removeOnComplete: false,
198
+ removeOnFail: false,
199
+ stackTraceLimit: 10
200
+ };
201
+ var MAX_BACKOFF_DELAY = 86400000;
190
202
  // ../../src/domain/job/payload.ts
191
203
  var DEFAULT_JOB_NAME = "default";
192
204
  var MAX_JOB_NAME_LENGTH = 256;
@@ -225,6 +237,13 @@ function normalizeLegacyJobPayload(source, fallback = DEFAULT_JOB_NAME) {
225
237
  return { name: checkedName(legacy.name), data: legacy.data, legacy: true };
226
238
  return { name: checkedName(fallback), data: source.data, legacy: false };
227
239
  }
240
+
241
+ // ../../src/domain/job/create.ts
242
+ function parseMaxDelay(value) {
243
+ if (typeof value !== "number" || !Number.isFinite(value))
244
+ return;
245
+ return value >= 0 && value <= MAX_BACKOFF_DELAY ? value : undefined;
246
+ }
228
247
  // src/canonical-transport/runtime.ts
229
248
  import { access, unlink } from "node:fs/promises";
230
249
  import { createHash, randomBytes } from "node:crypto";
@@ -340,6 +359,17 @@ function jobId(id) {
340
359
  function generateJobId() {
341
360
  return uuid();
342
361
  }
362
+ // ../../src/domain/job/state.ts
363
+ function calculateDelayedErrorDelay(job) {
364
+ const base = job.backoffConfig ? job.backoffConfig.delay : job.backoff;
365
+ const maxDelay = job.backoffConfig?.maxDelay;
366
+ const cap = isPositiveFinite(maxDelay) ? maxDelay : DEFAULT_MAX_BACKOFF;
367
+ const wait = typeof base === "number" && base > 0 ? base : JOB_DEFAULTS.backoff;
368
+ return Math.min(wait, cap);
369
+ }
370
+ function isPositiveFinite(value) {
371
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
372
+ }
343
373
  // ../../src/client/jobHelpers.ts
344
374
  function resolvePublicJobPayload(job) {
345
375
  const payload = normalizeLegacyJobPayload(job);
@@ -387,7 +417,12 @@ function buildParentOpts(job) {
387
417
  return;
388
418
  }
389
419
  function buildJobOpts(job) {
390
- const backoff = job.backoffConfig ? { type: job.backoffConfig.type, delay: job.backoffConfig.delay } : job.backoff;
420
+ const config = job.backoffConfig;
421
+ const backoff = config ? {
422
+ type: config.type,
423
+ delay: config.delay,
424
+ ...typeof config.maxDelay === "number" ? { maxDelay: config.maxDelay } : {}
425
+ } : job.backoff;
391
426
  return {
392
427
  priority: job.priority,
393
428
  delay: job.runAt > job.createdAt ? job.runAt - job.createdAt : 0,
@@ -1322,13 +1357,14 @@ async function handleDelayedError(internalJob, config, context) {
1322
1357
  if (config.shouldAbandonOutcome?.())
1323
1358
  return;
1324
1359
  try {
1360
+ const delay = calculateDelayedErrorDelay(internalJob);
1325
1361
  if (embedded) {
1326
- await getSharedManager().moveToDelayed(internalJob.id, internalJob.backoff || 1000, context.token ?? undefined);
1362
+ await getSharedManager().moveToDelayed(internalJob.id, delay, context.token ?? undefined);
1327
1363
  } else if (tcp) {
1328
1364
  await tcp.send({
1329
1365
  cmd: "MoveToDelayed",
1330
1366
  id: internalJob.id,
1331
- delay: internalJob.backoff || 1000,
1367
+ delay,
1332
1368
  ...context.token ? { token: context.token } : {}
1333
1369
  });
1334
1370
  }
@@ -1536,6 +1572,17 @@ var WORKER_CONSTANTS = {
1536
1572
  };
1537
1573
 
1538
1574
  // ../../src/client/worker/jobParser.ts
1575
+ function parseBackoffConfig(value) {
1576
+ if (typeof value !== "object" || value === null)
1577
+ return null;
1578
+ const { type, delay, maxDelay } = value;
1579
+ if (type !== "fixed" && type !== "exponential")
1580
+ return null;
1581
+ if (typeof delay !== "number" || !Number.isFinite(delay))
1582
+ return null;
1583
+ const cap = parseMaxDelay(maxDelay);
1584
+ return cap === undefined ? { type, delay } : { type, delay, maxDelay: cap };
1585
+ }
1539
1586
  function parseJobFromResponse(jobData, queueName) {
1540
1587
  const payload = normalizeLegacyJobPayload({ name: jobData.name, data: jobData.data });
1541
1588
  return {
@@ -1551,6 +1598,7 @@ function parseJobFromResponse(jobData, queueName) {
1551
1598
  attempts: jobData.attempts ?? 0,
1552
1599
  maxAttempts: jobData.maxAttempts ?? 3,
1553
1600
  backoff: jobData.backoff ?? 1000,
1601
+ backoffConfig: parseBackoffConfig(jobData.backoffConfig),
1554
1602
  ttl: jobData.ttl ?? null,
1555
1603
  timeout: jobData.timeout ?? null,
1556
1604
  uniqueKey: jobData.uniqueKey ?? null,
@@ -10867,7 +10915,7 @@ class FlowProducer extends EventEmitter7 {
10867
10915
  }
10868
10916
  }
10869
10917
  // src/index.ts
10870
- var __version__ = "0.2.0";
10918
+ var __version__ = "0.2.1";
10871
10919
  export {
10872
10920
  AuthError2 as AuthError,
10873
10921
  Bunqueue,
package/dist/legacy.js CHANGED
@@ -2057,7 +2057,7 @@ class FlowProducer {
2057
2057
  }
2058
2058
 
2059
2059
  // src/legacy.ts
2060
- var __version__ = "0.2.0";
2060
+ var __version__ = "0.2.1";
2061
2061
  export {
2062
2062
  AuthError2 as AuthError,
2063
2063
  Bunqueue,
@@ -7,4 +7,4 @@ export { AuthError, BunqueueError, CommandError, CommandTimeoutError, Connection
7
7
  export { consoleLogger, noopLogger } from "./observability.js";
8
8
  export type { Logger, LogLevel, Observability, TelemetryEvent, TelemetryHandler, } from "./observability.js";
9
9
  export { MAX_FRAME_SIZE, PROTOCOL_VERSION } from "./frame.js";
10
- export declare const __version__ = "0.2.0";
10
+ export declare const __version__ = "0.2.1";
@@ -24,4 +24,4 @@ export type { BatchResponse, CountResponse, DataResponse, JobCountsResponse, Job
24
24
  export type { BackoffOptions, DeduplicationOptions, JobCounts, JobOptions, JobStateName, RepeatOptions, } from "./types.js";
25
25
  export { Worker } from "./worker.js";
26
26
  export type { AckBatchOptions, Processor, WorkerEventMap, WorkerOptions } from "./worker-types.js";
27
- export declare const __version__ = "0.2.0";
27
+ export declare const __version__ = "0.2.1";
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Client Ownership - which connection owns a job's current delivery.
3
+ *
4
+ * `clientJobs` maps a connection to the jobs it pulled. `clientJobOwners` is
5
+ * its reverse index: one record per owned job naming the owning connection and
6
+ * the delivery it owns. A delivery is identified by the processing
7
+ * `JobLocation` object the pull installed in `jobIndex`. Only a pull installs a
8
+ * processing location, and every pull installs a fresh object, so the record
9
+ * of an earlier delivery never matches a later delivery of the same job id.
10
+ *
11
+ * Every mutation of the two maps goes through this module so they stay in
12
+ * step: a job is in `clientJobs.get(c)` exactly when its owner record names
13
+ * `c`. Every operation is O(1) per job.
14
+ */
15
+ import type { JobId } from "../domain/types/job.js";
16
+ import type { JobLocation } from "../domain/types/queue.js";
17
+ /** The connection that registered a job's delivery, and that delivery. */
18
+ export interface ClientJobOwner {
19
+ readonly clientId: string;
20
+ /** The `jobIndex` entry current when the connection registered the job. */
21
+ readonly delivery: JobLocation | undefined;
22
+ }
23
+ /** The two ownership maps plus the index that identifies deliveries. */
24
+ export interface ClientOwnershipContext {
25
+ jobIndex: Map<JobId, JobLocation>;
26
+ clientJobs: Map<string, Set<JobId>>;
27
+ clientJobOwners: Map<JobId, ClientJobOwner>;
28
+ }
29
+ /** The maps a detach touches; it never needs the job index. */
30
+ export type ClientOwnershipMaps = Pick<ClientOwnershipContext, 'clientJobs' | 'clientJobOwners'>;
31
+ /**
32
+ * Register `clientId` as the owner of the job's current delivery (called after
33
+ * PULL). A delivery has one owner, so any previous owner is detached first.
34
+ */
35
+ export declare function registerClientJob(clientId: string, jobId: JobId, ctx: ClientOwnershipContext): void;
36
+ /**
37
+ * Remove one connection's claim on a job (ACK/FAIL handled on that
38
+ * connection). Another connection's record for the same job is left alone: it
39
+ * may already own a newer delivery.
40
+ */
41
+ export declare function unregisterClientJob(clientId: string | undefined, jobId: JobId, ctx: ClientOwnershipMaps): void;
42
+ /**
43
+ * End ownership of a job's delivery, whoever holds it. Every transition that
44
+ * ends a delivery without the owner's ACK/FAIL calls this (stall recovery,
45
+ * orphan recovery, lock expiry, timeout, management claims, disconnect
46
+ * release), so a silent connection never keeps a job it no longer runs.
47
+ */
48
+ export declare function detachClientJob(jobId: JobId, ctx: ClientOwnershipMaps): void;
49
+ /**
50
+ * True only when `clientId` registered the job's current delivery: the job is
51
+ * processing and its processing entry is the one the connection registered.
52
+ * Disconnect release acts on nothing else, so a stale registration can never
53
+ * release or expire a later delivery owned by another worker.
54
+ */
55
+ export declare function ownsCurrentDelivery(clientId: string, jobId: JobId, ctx: ClientOwnershipContext): boolean;
56
+ /** Forget a disconnected client: its set and only its own owner records. */
57
+ export declare function dropClient(clientId: string, ctx: ClientOwnershipMaps): void;
58
+ /**
59
+ * Detach every record whose delivery has ended. An outcome sent on another
60
+ * connection than the pull (pooled clients) unregisters the sender, not the
61
+ * owner, so such records would otherwise live as long as the pulling
62
+ * connection. Runs synchronously from periodic cleanup.
63
+ * @returns the number of records removed.
64
+ */
65
+ export declare function pruneEndedClientDeliveries(ctx: ClientOwnershipContext): number;
@@ -2,28 +2,24 @@
2
2
  * Client Tracking - Client-job relationship management
3
3
  * Handles job ownership and release on client disconnect
4
4
  */
5
- import type { JobId } from "../domain/types/job.js";
6
5
  import type { LockContext } from "./types/index.js";
7
- /**
8
- * Register a job as owned by a client (called on PULL).
9
- */
10
- export declare function registerClientJob(clientId: string, jobId: JobId, ctx: LockContext): void;
11
- /**
12
- * Unregister a job from a client (called on ACK/FAIL).
13
- */
14
- export declare function unregisterClientJob(clientId: string | undefined, jobId: JobId, ctx: LockContext): void;
6
+ export { registerClientJob, unregisterClientJob } from "./clientOwnership.js";
15
7
  /**
16
8
  * Release all jobs owned by a client back to queue (called on TCP disconnect).
17
9
  * Returns the number of jobs released.
18
10
  *
19
- * Uses proper locking to prevent race conditions.
11
+ * Only deliveries the client still owns are touched (`ownsCurrentDelivery`): a
12
+ * job recovered from this client and delivered again to another worker is not
13
+ * this client's to release. The rule is checked when collecting and again
14
+ * under the shard and processing write locks.
20
15
  */
21
16
  export declare function releaseClientJobs(clientId: string, ctx: LockContext): Promise<number>;
22
17
  /**
23
18
  * Force-release client tracking without acquiring queue locks.
24
19
  * Used as a last-resort fallback when releaseClientJobs has exhausted its
25
20
  * retry budget (e.g. persistent lock contention on TCP disconnect). For each
26
- * orphaned job:
21
+ * job whose current delivery the client still owns (`ownsCurrentDelivery`;
22
+ * a stale registration must not expire another worker's delivery):
27
23
  * - clears `jobLocks` so a stale lock token can't survive the disconnect
28
24
  * until its TTL expires;
29
25
  * - expires both `lastHeartbeat` (so stall detection's heartbeat check
@@ -3,9 +3,9 @@
3
3
  * job before its worker can ACK or FAIL it.
4
4
  */
5
5
  import type { JobId, JobLock } from "../../domain/types/job.js";
6
- export interface JobClaimContext {
6
+ import { type ClientOwnershipMaps } from "../clientOwnership.js";
7
+ export interface JobClaimContext extends ClientOwnershipMaps {
7
8
  jobLocks: Map<JobId, JobLock>;
8
- clientJobs: Map<string, Set<JobId>>;
9
9
  }
10
10
  /**
11
11
  * Release the live lease and detach the job from its TCP client owner.
@@ -10,6 +10,7 @@ import type { WebhookManager } from "../webhookManager.js";
10
10
  import type { EventsManager } from "../eventsManager.js";
11
11
  import { type RWLock } from "../../shared/lock.js";
12
12
  import type { DependencyResultTracker } from "../dependencyResultTracker.js";
13
+ import type { ClientJobOwner } from "../clientOwnership.js";
13
14
  import { type DependencyCompletionTracker } from "../dependencyCompletions.js";
14
15
  export { discardJob, moveJobToDelayed } from "./jobMoveOperations.js";
15
16
  /** Context for job management operations */
@@ -22,6 +23,7 @@ export interface JobManagementContext {
22
23
  jobIndex: Map<JobId, JobLocation>;
23
24
  jobLocks: Map<JobId, JobLock>;
24
25
  clientJobs: Map<string, Set<JobId>>;
26
+ clientJobOwners: Map<JobId, ClientJobOwner>;
25
27
  webhookManager: WebhookManager;
26
28
  eventsManager: EventsManager;
27
29
  repeatChain?: Map<JobId, JobId>;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Orphan Recovery - cleanup's backstop for the stall checker.
3
+ *
4
+ * An orphan is an active job that has shown no liveness for longer than both
5
+ * a 30-minute floor and its own stall window, on a queue whose stall detection
6
+ * is enabled. It is recovered through the stall path, so anything recovered
7
+ * here the stall checker would also consider stalled. One difference: like lock
8
+ * expiration and startup recovery, it treats `maxStalls: 0` as unlimited,
9
+ * while the stall checker sends a job to the DLQ on its first stall.
10
+ */
11
+ import type { BackgroundContext } from "./types/index.js";
12
+ /** The shortest silence after which an active job can be an orphan. */
13
+ export declare const ORPHAN_WINDOW_FLOOR_MS: number;
14
+ /**
15
+ * An orphan is a stalled job the stall checker did not reclaim (for example
16
+ * its recovery attempt failed on a lock timeout), so it takes the stall
17
+ * recovery path: concurrency, group and unique-key resources are released,
18
+ * the attempt is counted, client ownership is detached, and the job is retried
19
+ * with backoff or moved to the DLQ, persisted and announced like any stalled
20
+ * job. `handleStalledJob` holds `shardLocks` then `processingLocks`, confirms
21
+ * the same job object is still processing, and only then re-runs this whole
22
+ * predicate (configuration included), because a heartbeat, progress update,
23
+ * lock renewal or stall-config change can land while the sweep waits.
24
+ */
25
+ export declare function recoverOrphanedProcessingEntries(ctx: BackgroundContext, now: number): Promise<void>;
@@ -19,6 +19,7 @@ import { DependencyCompletionTracker } from "../dependencyCompletions.js";
19
19
  import { JobTimeoutScheduler } from "../background/timeouts.js";
20
20
  import type { RetiredTimeoutGeneration } from "../types/background.js";
21
21
  import { QueueTelemetryJournal } from "../queueTelemetryJournal.js";
22
+ import type { ClientJobOwner } from "../clientOwnership.js";
22
23
  export type { QueueManagerConfig };
23
24
  export declare abstract class QueueManagerState {
24
25
  protected readonly config: typeof DEFAULT_CONFIG & {
@@ -52,6 +53,7 @@ export declare abstract class QueueManagerState {
52
53
  protected depFlushRunning: boolean;
53
54
  protected readonly jobLocks: Map<JobId, JobLock>;
54
55
  protected readonly clientJobs: Map<string, Set<JobId>>;
56
+ protected readonly clientJobOwners: Map<JobId, ClientJobOwner>;
55
57
  protected readonly repeatChain: Map<JobId, JobId>;
56
58
  protected readonly failedChildrenValues: Map<JobId, Record<string, string>>;
57
59
  protected readonly ignoredChildrenFailures: Map<JobId, Record<string, string>>;
@@ -2,9 +2,25 @@
2
2
  * Stall Detection - Job stall detection and recovery
3
3
  * Uses two-phase detection (like BullMQ) to prevent false positives
4
4
  */
5
+ import type { Job } from "../domain/types/job.js";
6
+ import { StallAction } from "../domain/types/stall.js";
5
7
  import type { BackgroundContext } from "./types/index.js";
6
8
  /**
7
9
  * Check for stalled jobs and handle them
8
10
  * Uses two-phase detection to prevent false positives
9
11
  */
10
12
  export declare function checkStalledJobs(ctx: BackgroundContext): void;
13
+ /**
14
+ * Caller-specific condition evaluated with both write locks held, right after
15
+ * the stall path confirms the same job object is still in processing. Returning
16
+ * false leaves the job untouched, so a caller can re-verify its own trigger
17
+ * (for example cleanup's orphan liveness rule) atomically with the transition.
18
+ */
19
+ export type StallRecheck = (job: Job, now: number) => boolean;
20
+ /**
21
+ * Recover a stalled job: retry it with the attempt counted and backoff applied,
22
+ * or move it to the DLQ once attempts are exhausted or `action` says so.
23
+ * Takes `shardLocks[idx]` then `processingLocks[procIdx]` (lock hierarchy).
24
+ * @returns true when this call performed the transition.
25
+ */
26
+ export declare function handleStalledJob(job: Job, action: StallAction, ctx: BackgroundContext, recheck?: StallRecheck): Promise<boolean>;
@@ -1,4 +1,5 @@
1
1
  import type { Shard } from "../../domain/queue/shard.js";
2
+ import type { ClientJobOwner } from "../clientOwnership.js";
2
3
  import type { FailureReason } from "../../domain/types/dlq.js";
3
4
  import type { Job, JobId, JobLock } from "../../domain/types/job.js";
4
5
  import type { JobLocation } from "../../domain/types/queue.js";
@@ -41,6 +42,7 @@ export interface ContextDependencies {
41
42
  jobLogQueues: Map<JobId, string>;
42
43
  jobLocks: Map<JobId, JobLock>;
43
44
  clientJobs: Map<string, Set<JobId>>;
45
+ clientJobOwners: Map<JobId, ClientJobOwner>;
44
46
  stalledCandidates: Set<JobId>;
45
47
  pendingDepChecks: Set<JobId>;
46
48
  pendingQueueAdmissions: Map<string, number>;
@@ -1,4 +1,5 @@
1
1
  import type { Shard } from "../../domain/queue/shard.js";
2
+ import type { ClientJobOwner } from "../clientOwnership.js";
2
3
  import type { FailureReason } from "../../domain/types/dlq.js";
3
4
  import type { Job, JobId, JobLock } from "../../domain/types/job.js";
4
5
  import type { JobLocation } from "../../domain/types/queue.js";
@@ -38,6 +39,7 @@ export interface QueueManagerState {
38
39
  readonly retiredCronLeaseTokens: MapLike<JobId, string>;
39
40
  readonly timeoutScheduler: JobTimeoutScheduler;
40
41
  readonly clientJobs: Map<string, Set<JobId>>;
42
+ readonly clientJobOwners: Map<JobId, ClientJobOwner>;
41
43
  readonly stalledCandidates: Set<JobId>;
42
44
  readonly pendingDepChecks: Set<JobId>;
43
45
  readonly pendingQueueAdmissions: Map<string, number>;
@@ -70,6 +72,7 @@ export interface LockContext {
70
72
  jobLocks: Map<JobId, JobLock>;
71
73
  retiredCronLeaseTokens: MapLike<JobId, string>;
72
74
  clientJobs: Map<string, Set<JobId>>;
75
+ clientJobOwners: Map<JobId, ClientJobOwner>;
73
76
  processingShards: Map<JobId, Job>[];
74
77
  processingLocks: RWLock[];
75
78
  shards: Shard[];
@@ -1,4 +1,5 @@
1
1
  import type { Shard } from "../../domain/queue/shard.js";
2
+ import type { ClientJobOwner } from "../clientOwnership.js";
2
3
  import type { DlqEntry } from "../../domain/types/dlq.js";
3
4
  import type { FailureReason } from "../../domain/types/dlq.js";
4
5
  import type { Job, JobId, JobInput, JobLock } from "../../domain/types/job.js";
@@ -48,6 +49,7 @@ export interface QueueManagerStateView {
48
49
  stalledCandidates: Set<JobId>;
49
50
  jobLocks: Map<JobId, JobLock>;
50
51
  clientJobs: Map<string, Set<JobId>>;
52
+ clientJobOwners: Map<JobId, ClientJobOwner>;
51
53
  repeatChain: Map<JobId, JobId>;
52
54
  queueNamesCache: Set<string>;
53
55
  eventsManager: EventsManager;
@@ -9,9 +9,10 @@
9
9
  * outages and process restarts; memory-only sources provide no such guarantee.
10
10
  *
11
11
  * Idempotency: every forwarded job carries the deterministic remote jobId
12
- * `fwd:<localQueueKey>:<localJobId>`. The server dedupes custom jobIds while
13
- * their ownership is retained; after bounded retention or removal, downstream
14
- * effects must still tolerate at-least-once delivery.
12
+ * `fwd:<localQueueKey>:<localJobId>`. The server dedupes a custom jobId only
13
+ * while that job is live (waiting, delayed or active); completion or the DLQ
14
+ * releases the id, so a re-forward after that creates a new remote job.
15
+ * Downstream effects must tolerate at-least-once delivery.
15
16
  */
16
17
  import { EventEmitter } from 'events';
17
18
  import type { ConnectionOptions, QueueOptions } from "./types.js";
@@ -5,6 +5,8 @@ export interface ParentOpts {
5
5
  export interface BackoffOptions {
6
6
  type: 'fixed' | 'exponential';
7
7
  delay: number;
8
+ /** Upper bound for one retry delay in ms (0 to 86,400,000). Defaults to 1 hour. */
9
+ maxDelay?: number;
8
10
  }
9
11
  export interface KeepJobs {
10
12
  age?: number;
@@ -10,3 +10,5 @@ export declare const JOB_DEFAULTS: {
10
10
  readonly removeOnFail: false;
11
11
  readonly stackTraceLimit: 10;
12
12
  };
13
+ /** Upper bound accepted for `backoff`, `backoff.delay` and `backoff.maxDelay` (24 hours). */
14
+ export declare const MAX_BACKOFF_DELAY = 86400000;
@@ -1,2 +1,9 @@
1
1
  import type { Job, JobId, JobInput } from "../types/jobs/model.js";
2
+ /**
3
+ * Keep a caller-supplied retry-delay cap only when it is usable. Embedded and
4
+ * cron admission do not pass through the server validator, so a non-numeric,
5
+ * non-finite, negative or over-limit value is dropped and the default cap applies
6
+ * instead of turning the retry delay into NaN or an unbounded wait.
7
+ */
8
+ export declare function parseMaxDelay(value: unknown): number | undefined;
2
9
  export declare function createJob(id: JobId, queue: string, input: JobInput, now?: number): Job;
@@ -1,4 +1,16 @@
1
- import type { JobId, JobLock } from "../types/jobs/model.js";
1
+ import type { Job, JobId, JobLock } from "../types/jobs/model.js";
2
2
  export declare function createJobLock(jobId: JobId, owner: string, ttl?: number, now?: number): JobLock;
3
3
  export declare function isLockExpired(lock: JobLock, now?: number): boolean;
4
4
  export declare function renewLock(lock: JobLock, newTtl?: number, now?: number): void;
5
+ /**
6
+ * True when `lock` was created for an earlier processing generation of `job`,
7
+ * that is, the job was pulled again after the lease was granted. Stall retry
8
+ * deliberately keeps the previous lease as a stale-outcome guard, so a lease in
9
+ * `jobLocks` is not necessarily the current one.
10
+ *
11
+ * Pull stamps `startedAt` from a clock read taken before the lease is created
12
+ * in the same delivery, so a lease from the current pull always has
13
+ * `createdAt >= startedAt`. The comparison is strict: a lease created in the
14
+ * same millisecond as the pull belongs to the current generation.
15
+ */
16
+ export declare function isLeaseFromEarlierGeneration(job: Pick<Job, 'startedAt'>, lock: Pick<JobLock, 'createdAt'>): boolean;
@@ -5,4 +5,19 @@ export declare function isReady(job: Job, now?: number): boolean;
5
5
  export declare function isExpired(job: Job, now?: number): boolean;
6
6
  export declare function isTimedOut(job: Job, now?: number): boolean;
7
7
  export declare function calculateBackoff(job: Job): number;
8
+ /**
9
+ * Wait applied when a processor throws `DelayedError`. It is always a positive
10
+ * finite number of milliseconds: DelayedError never counts an attempt, so a zero
11
+ * wait would let a processor that keeps throwing it re-pull the job in a tight
12
+ * loop with nothing to stop it.
13
+ *
14
+ * The base is the configured delay with no attempt growth and no jitter:
15
+ * `backoff.delay` for the object form, otherwise the numeric `backoff`. A base
16
+ * that is not a positive number (0, missing, negative or NaN) falls back to the
17
+ * 1000 ms default. The base is then capped at `backoff.maxDelay` when that is a
18
+ * positive finite number, otherwise at DEFAULT_MAX_BACKOFF. A `maxDelay` of 0
19
+ * means "retry failures immediately" and does not apply here, because
20
+ * DelayedError is not a failure: such a job waits its capped base delay.
21
+ */
22
+ export declare function calculateDelayedErrorDelay(job: Pick<Job, 'backoff' | 'backoffConfig'>): number;
8
23
  export declare function canRetry(job: Job): boolean;
@@ -1,5 +1,5 @@
1
1
  import type { AtomicFlowJobInput } from "../flow.js";
2
- import type { JobInput } from "../job.js";
2
+ import type { BackoffConfig, JobInput } from "../job.js";
3
3
  import type { GroupPullOptions } from "../group.js";
4
4
  import type { BaseCommand } from "./base.js";
5
5
  export interface PushCommand extends BaseCommand {
@@ -10,10 +10,7 @@ export interface PushCommand extends BaseCommand {
10
10
  readonly priority?: number;
11
11
  readonly delay?: number;
12
12
  readonly maxAttempts?: number;
13
- readonly backoff?: number | {
14
- type: 'fixed' | 'exponential';
15
- delay: number;
16
- };
13
+ readonly backoff?: number | BackoffConfig;
17
14
  readonly ttl?: number;
18
15
  readonly timeout?: number;
19
16
  readonly uniqueKey?: string;
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Cron job domain types
3
3
  */
4
+ import type { BackoffConfig } from "./jobs/model.js";
4
5
  /** Deduplication config for cron-spawned jobs */
5
6
  export interface CronDedup {
6
7
  readonly ttl?: number;
@@ -14,10 +15,7 @@ export interface CronDedup {
14
15
  */
15
16
  export interface CronJobOptions {
16
17
  readonly maxAttempts?: number;
17
- readonly backoff?: number | {
18
- type: 'fixed' | 'exponential';
19
- delay: number;
20
- };
18
+ readonly backoff?: number | BackoffConfig;
21
19
  readonly timeout?: number;
22
20
  readonly delay?: number;
23
21
  readonly stallTimeout?: number;
@@ -93,10 +93,7 @@ export interface JobInput {
93
93
  priority?: number;
94
94
  delay?: number;
95
95
  maxAttempts?: number;
96
- backoff?: number | {
97
- type: 'fixed' | 'exponential';
98
- delay: number;
99
- };
96
+ backoff?: number | BackoffConfig;
100
97
  ttl?: number;
101
98
  timeout?: number;
102
99
  uniqueKey?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunqueue-client",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Cross-runtime TypeScript client for the bunqueue job queue server — Node.js, Bun, Deno and Cloudflare Workers",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/index.ts CHANGED
@@ -22,4 +22,4 @@ export type {
22
22
  TelemetryHandler,
23
23
  } from './observability.js';
24
24
  export { MAX_FRAME_SIZE, PROTOCOL_VERSION } from './frame.js';
25
- export const __version__ = '0.2.0';
25
+ export const __version__ = '0.2.1';
package/src/legacy.ts CHANGED
@@ -88,4 +88,4 @@ export type {
88
88
  export { Worker } from './worker.js';
89
89
  export type { AckBatchOptions, Processor, WorkerEventMap, WorkerOptions } from './worker-types.js';
90
90
 
91
- export const __version__ = '0.2.0';
91
+ export const __version__ = '0.2.1';