Skip to content

Commit ec56385

Browse files
committed
simplify timezone handling
1 parent 3f8a54c commit ec56385

File tree

8 files changed

+160
-378
lines changed

8 files changed

+160
-378
lines changed

README.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,26 +108,35 @@ await queue.add("health-check", payload, {
108108

109109
## Timezone Support
110110

111+
Vorsteh Queue uses a **UTC-first approach** for reliable timezone handling:
112+
111113
```typescript
112-
// Schedule job for 9 AM New York time
114+
// Schedule job for 9 AM New York time - converted to UTC immediately
113115
await queue.add("daily-report", payload, {
114116
cron: "0 9 * * *",
115-
timezone: "America/New_York"
117+
timezone: "America/New_York" // Timezone used for conversion only
116118
})
117119

118-
// Schedule job for specific time in Tokyo
120+
// Schedule job for specific time in Tokyo - stored as UTC
119121
await queue.add("notification", payload, {
120122
runAt: new Date("2024-01-15T10:00:00"),
121-
timezone: "Asia/Tokyo"
123+
timezone: "Asia/Tokyo" // Interprets runAt in Tokyo time
122124
})
123125

124-
// Complex cron with timezone (every 15 min during business hours)
126+
// Complex cron with timezone - result always UTC
125127
await queue.add("business-task", payload, {
126128
cron: "*/15 9-17 * * 1-5",
127-
timezone: "Europe/London"
129+
timezone: "Europe/London" // Business hours in London time
128130
})
129131
```
130132

133+
### How It Works
134+
135+
1. **Timezone conversion happens at job creation** - not at execution
136+
2. **All timestamps stored in database are UTC** - no timezone ambiguity
137+
3. **Recurring jobs recalculate using original timezone** - maintains accuracy
138+
4. **Simple and predictable** - no runtime timezone complexity
139+
131140
## Progress Tracking
132141

133142
```typescript

packages/adapter-drizzle/src/drizzle-adapter.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ export class DrizzleQueueAdapter extends BaseQueueAdapter {
6767
repeatEvery: job.repeatEvery,
6868
repeatLimit: job.repeatLimit,
6969
repeatCount: job.repeatCount,
70-
timezone: job.timezone ?? "UTC",
7170
})
7271
.returning()
7372

@@ -250,7 +249,6 @@ export class DrizzleQueueAdapter extends BaseQueueAdapter {
250249
repeatEvery: job.repeatEvery ?? undefined,
251250
repeatLimit: job.repeatLimit ?? undefined,
252251
repeatCount: job.repeatCount ?? 0,
253-
timezone: job.timezone,
254252
}
255253
}
256254
}

packages/adapter-drizzle/src/schema.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ export const queueJobs = pgTable(
1919
processedAt: timestamp("processed_at", { withTimezone: true }),
2020
completedAt: timestamp("completed_at", { withTimezone: true }),
2121
failedAt: timestamp("failed_at", { withTimezone: true }),
22-
timezone: varchar("timezone", { length: 50 }).default("UTC").notNull(),
2322
error: jsonb("error"),
2423
progress: integer("progress").default(0),
2524
cron: varchar("cron", { length: 255 }),

packages/adapter-drizzle/tests/timezone.test.ts

Lines changed: 0 additions & 166 deletions
This file was deleted.

packages/core/src/core/queue.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {
1111
QueueStats,
1212
} from "../types"
1313
import { calculateDelay, waitFor } from "../utils/helpers"
14-
import { calculateNextRun, nowUtc, toUtcDate } from "../utils/scheduler"
14+
import { calculateNextRun, nowUtc, parseCron, toUtcDate } from "../utils/scheduler"
1515
import { createJobWrapper } from "./job-wrapper"
1616

1717
type EventListener<TEventData> = (data: TEventData) => void | Promise<void>
@@ -126,11 +126,16 @@ export class Queue {
126126
let status: JobStatus = "pending"
127127

128128
if (jobOptions.runAt) {
129+
// Convert user-provided time to UTC using timezone context
129130
processAt = toUtcDate(jobOptions.runAt, timezone)
130131
status = processAt > now ? "delayed" : "pending"
131132
} else if (jobOptions.delay) {
132133
processAt = new Date(now.getTime() + jobOptions.delay)
133134
status = "delayed"
135+
} else if (jobOptions.cron) {
136+
// Parse cron in timezone, get UTC result
137+
processAt = parseCron(jobOptions.cron, timezone, now)
138+
status = "delayed"
134139
} else {
135140
processAt = now
136141
}
@@ -147,7 +152,6 @@ export class Queue {
147152
repeatEvery: jobOptions.repeat?.every,
148153
repeatLimit: jobOptions.repeat?.limit,
149154
repeatCount: 0,
150-
timezone,
151155
})
152156

153157
this.emit("job:added", job)
@@ -431,7 +435,7 @@ export class Queue {
431435
}
432436
}
433437

434-
private async handleRecurringJob(job: BaseJob): Promise<void> {
438+
private async handleRecurringJob(job: BaseJob, originalTimezone?: string): Promise<void> {
435439
if (!job.cron && !job.repeatEvery) return
436440

437441
const nextCount = (job.repeatCount ?? 0) + 1
@@ -443,7 +447,7 @@ export class Queue {
443447
const nextRun = calculateNextRun({
444448
cron: job.cron,
445449
repeatEvery: job.repeatEvery,
446-
timezone: job.timezone,
450+
timezone: originalTimezone ?? "UTC",
447451
lastRun: new Date(),
448452
})
449453

packages/core/src/types/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,6 @@ export interface BaseJob<TJobPayload = unknown> {
4848
readonly repeatLimit?: number
4949
/** Current repetition count for recurring jobs */
5050
readonly repeatCount?: number
51-
/** Timezone for job scheduling (default: UTC) */
52-
readonly timezone?: string
5351
}
5452

5553
/**

0 commit comments

Comments
 (0)