1+ import { PGlite } from "@electric-sql/pglite"
2+ import { drizzle } from "drizzle-orm/pglite"
3+ import { afterEach , beforeEach , describe , expect , it } from "vitest"
4+
5+ import { DrizzleQueueAdapter } from "~/index"
6+ import * as schema from "~/schema"
7+
8+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/consistent-type-imports
9+ const { pushSchema } = require ( "drizzle-kit/api" ) as typeof import ( "drizzle-kit/api" )
10+
11+ describe ( "Timezone Edge Cases" , ( ) => {
12+ let db : ReturnType < typeof drizzle < typeof schema > >
13+ let adapter : DrizzleQueueAdapter
14+ let client : PGlite
15+
16+ beforeEach ( async ( ) => {
17+ client = new PGlite ( )
18+ db = drizzle ( client , { schema } )
19+
20+ const { apply } = await pushSchema ( schema , db as never )
21+ await apply ( )
22+
23+ adapter = new DrizzleQueueAdapter ( db , "timezone-edge-test" )
24+ await adapter . connect ( )
25+ } )
26+
27+ afterEach ( async ( ) => {
28+ await adapter . disconnect ( )
29+ await client . close ( )
30+ } )
31+
32+ it ( "should handle dates with explicit timezone offsets" , async ( ) => {
33+ // User passes date with timezone offset
34+ const dateWithOffset = new Date ( "2024-01-15T14:00:00+02:00" ) // 2 PM in +2 timezone = 12 PM UTC
35+
36+ const job = await adapter . addJob ( {
37+ name : "offset-test" ,
38+ payload : { test : "data" } ,
39+ status : "pending" ,
40+ priority : 2 ,
41+ attempts : 0 ,
42+ maxAttempts : 3 ,
43+ processAt : dateWithOffset ,
44+ progress : 0 ,
45+ repeatCount : 0 ,
46+ } )
47+
48+ // Should store the UTC equivalent (12 PM UTC, not 2 PM UTC)
49+ expect ( job . processAt . toISOString ( ) ) . toBe ( "2024-01-15T12:00:00.000Z" )
50+ expect ( job . processAt . getTime ( ) ) . toBe ( dateWithOffset . getTime ( ) )
51+ } )
52+
53+ it ( "should handle negative timezone offsets" , async ( ) => {
54+ // User passes date with negative timezone offset
55+ const dateWithNegativeOffset = new Date ( "2024-01-15T14:00:00-05:00" ) // 2 PM EST = 7 PM UTC
56+
57+ const job = await adapter . addJob ( {
58+ name : "negative-offset-test" ,
59+ payload : { test : "data" } ,
60+ status : "pending" ,
61+ priority : 2 ,
62+ attempts : 0 ,
63+ maxAttempts : 3 ,
64+ processAt : dateWithNegativeOffset ,
65+ progress : 0 ,
66+ repeatCount : 0 ,
67+ } )
68+
69+ // Should store the UTC equivalent (7 PM UTC)
70+ expect ( job . processAt . toISOString ( ) ) . toBe ( "2024-01-15T19:00:00.000Z" )
71+ expect ( job . processAt . getTime ( ) ) . toBe ( dateWithNegativeOffset . getTime ( ) )
72+ } )
73+
74+ it ( "should handle server in different timezone" , async ( ) => {
75+ // Simulate server running in different timezone by creating dates in different ways
76+ const utcDate = new Date ( "2024-01-15T14:00:00Z" ) // Explicit UTC
77+ const localDate = new Date ( "2024-01-15T14:00:00" ) // Local time (depends on server timezone)
78+
79+ const utcJob = await adapter . addJob ( {
80+ name : "utc-job" ,
81+ payload : { type : "utc" } ,
82+ status : "pending" ,
83+ priority : 2 ,
84+ attempts : 0 ,
85+ maxAttempts : 3 ,
86+ processAt : utcDate ,
87+ progress : 0 ,
88+ repeatCount : 0 ,
89+ } )
90+
91+ const localJob = await adapter . addJob ( {
92+ name : "local-job" ,
93+ payload : { type : "local" } ,
94+ status : "pending" ,
95+ priority : 2 ,
96+ attempts : 0 ,
97+ maxAttempts : 3 ,
98+ processAt : localDate ,
99+ progress : 0 ,
100+ repeatCount : 0 ,
101+ } )
102+
103+ // UTC date should be stored exactly as provided
104+ expect ( utcJob . processAt . toISOString ( ) ) . toBe ( "2024-01-15T14:00:00.000Z" )
105+
106+ // Local date gets stored as whatever the server interprets it as
107+ // This is the key test - we store whatever Date object represents in UTC
108+ expect ( localJob . processAt ) . toBeInstanceOf ( Date )
109+ expect ( localJob . processAt . getTime ( ) ) . toBe ( localDate . getTime ( ) )
110+ } )
111+
112+ it ( "should handle daylight saving time transitions" , async ( ) => {
113+ // Spring forward: March 10, 2024 in America/New_York
114+ const springForwardDate = new Date ( "2024-03-10T07:00:00Z" ) // 2 AM EST becomes 3 AM EDT
115+
116+ // Fall back: November 3, 2024 in America/New_York
117+ const fallBackDate = new Date ( "2024-11-03T06:00:00Z" ) // 2 AM EDT becomes 1 AM EST
118+
119+ const springJob = await adapter . addJob ( {
120+ name : "spring-job" ,
121+ payload : { dst : "spring" } ,
122+ status : "pending" ,
123+ priority : 2 ,
124+ attempts : 0 ,
125+ maxAttempts : 3 ,
126+ processAt : springForwardDate ,
127+ progress : 0 ,
128+ repeatCount : 0 ,
129+ } )
130+
131+ const fallJob = await adapter . addJob ( {
132+ name : "fall-job" ,
133+ payload : { dst : "fall" } ,
134+ status : "pending" ,
135+ priority : 2 ,
136+ attempts : 0 ,
137+ maxAttempts : 3 ,
138+ processAt : fallBackDate ,
139+ progress : 0 ,
140+ repeatCount : 0 ,
141+ } )
142+
143+ // Both should store exact UTC timestamps regardless of DST
144+ expect ( springJob . processAt . toISOString ( ) ) . toBe ( "2024-03-10T07:00:00.000Z" )
145+ expect ( fallJob . processAt . toISOString ( ) ) . toBe ( "2024-11-03T06:00:00.000Z" )
146+ } )
147+
148+ it ( "should handle various date formats consistently" , async ( ) => {
149+ const testCases = [
150+ {
151+ name : "ISO with Z" ,
152+ date : new Date ( "2024-01-15T14:00:00Z" ) ,
153+ expected : "2024-01-15T14:00:00.000Z"
154+ } ,
155+ {
156+ name : "ISO with +00:00" ,
157+ date : new Date ( "2024-01-15T14:00:00+00:00" ) ,
158+ expected : "2024-01-15T14:00:00.000Z"
159+ } ,
160+ {
161+ name : "ISO with +02:00" ,
162+ date : new Date ( "2024-01-15T14:00:00+02:00" ) ,
163+ expected : "2024-01-15T12:00:00.000Z" // 2 hours earlier in UTC
164+ } ,
165+ {
166+ name : "ISO with -05:00" ,
167+ date : new Date ( "2024-01-15T14:00:00-05:00" ) ,
168+ expected : "2024-01-15T19:00:00.000Z" // 5 hours later in UTC
169+ } ,
170+ {
171+ name : "Timestamp" ,
172+ date : new Date ( 1705327200000 ) , // 2024-01-15T14:00:00Z
173+ expected : "2024-01-15T14:00:00.000Z"
174+ }
175+ ]
176+
177+ for ( const testCase of testCases ) {
178+ const job = await adapter . addJob ( {
179+ name : `format-test-${ testCase . name } ` ,
180+ payload : { format : testCase . name } ,
181+ status : "pending" ,
182+ priority : 2 ,
183+ attempts : 0 ,
184+ maxAttempts : 3 ,
185+ processAt : testCase . date ,
186+ progress : 0 ,
187+ repeatCount : 0 ,
188+ } )
189+
190+ expect ( job . processAt . toISOString ( ) ) . toBe ( testCase . expected )
191+ }
192+ } )
193+
194+ it ( "should maintain timestamp consistency across retrieval" , async ( ) => {
195+ // Test that what goes in comes out exactly the same
196+ const originalTimestamp = 1705327200000 // 2024-01-15T14:00:00Z
197+ const originalDate = new Date ( originalTimestamp )
198+
199+ const job = await adapter . addJob ( {
200+ name : "consistency-test" ,
201+ payload : { test : "data" } ,
202+ status : "pending" ,
203+ priority : 2 ,
204+ attempts : 0 ,
205+ maxAttempts : 3 ,
206+ processAt : originalDate ,
207+ progress : 0 ,
208+ repeatCount : 0 ,
209+ } )
210+
211+ // Retrieve the job
212+ const retrievedJob = await adapter . getNextJob ( )
213+
214+ expect ( retrievedJob ) . toBeTruthy ( )
215+ expect ( retrievedJob ?. processAt . getTime ( ) ) . toBe ( originalTimestamp )
216+ expect ( retrievedJob ?. processAt . toISOString ( ) ) . toBe ( originalDate . toISOString ( ) )
217+ } )
218+ } )
0 commit comments