Skip to content

Commit f86f9de

Browse files
Copilotbcho
andcommitted
Replace Date with Temporal API for datetime types in TypeScript SDK
Co-authored-by: bcho <1975118+bcho@users.noreply.github.com>
1 parent 9606b67 commit f86f9de

13 files changed

Lines changed: 330 additions & 36 deletions

sdks/typescript/package-lock.json

Lines changed: 17 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdks/typescript/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
"better-sqlite3": "^12.5.0"
3737
},
3838
"dependencies": {
39-
"absurd-sdk": "https://github.com/bcho/absurd/releases/download/sdks%2Ftypescript%2Fv0.0.7/typescript-sdk-v0.0.7.tgz"
39+
"absurd-sdk": "https://github.com/bcho/absurd/releases/download/sdks%2Ftypescript%2Fv0.0.7/typescript-sdk-v0.0.7.tgz",
40+
"temporal-polyfill": "^0.3.0"
4041
},
4142
"devDependencies": {
4243
"@types/better-sqlite3": "^7.6.13",
@@ -48,4 +49,4 @@
4849
"engines": {
4950
"node": ">=18.0.0"
5051
}
51-
}
52+
}

sdks/typescript/src/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,21 @@ export type {
3535
SQLiteVerboseLog,
3636
} from "./sqlite-types";
3737

38+
// Export Temporal types and helpers
39+
export {
40+
Temporal,
41+
type Duration,
42+
type Instant,
43+
durationFromSeconds,
44+
durationToSeconds,
45+
instantFromDate,
46+
instantFromEpochMilliseconds,
47+
instantToDate,
48+
instantToEpochMilliseconds,
49+
isDuration,
50+
isInstant,
51+
} from "./temporal-types";
52+
3853
export class Absurd extends AbsurdBase implements AbsurdClient {
3954
private db: SQLiteDatabase;
4055

sdks/typescript/src/sqlite-types.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
1-
export type SQLiteBindValue = number | string | Buffer | bigint | Date | null;
1+
import type { Instant } from "./temporal-types";
2+
3+
/**
4+
* Values that can be bound to SQLite prepared statements.
5+
* Supports both JavaScript Date and Temporal.Instant for datetime values.
6+
*/
7+
export type SQLiteBindValue =
8+
| number
9+
| string
10+
| Buffer
11+
| bigint
12+
| Date
13+
| Instant
14+
| null;
215

316
export type SQLiteBindParams =
417
| SQLiteBindValue[]

sdks/typescript/src/sqlite.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import type {
55
SQLiteStatement,
66
SQLiteVerboseLog,
77
} from "./sqlite-types";
8+
import {
9+
isInstant,
10+
instantFromEpochMilliseconds,
11+
instantToDate,
12+
} from "./temporal-types";
813

914
export class SqliteConnection implements Queryable {
1015
private readonly db: SQLiteDatabase;
@@ -164,14 +169,20 @@ function decodeColumnValue<V = any>(
164169
`Expected datetime column ${columnName} to be a number, got ${typeof value}`
165170
);
166171
}
167-
return new Date(value) as V;
172+
// Return Temporal.Instant for datetime columns
173+
return instantFromEpochMilliseconds(value) as V;
168174
}
169175

170176
// For other types, return as is
171177
return value as V;
172178
}
173179

174180
function encodeColumnValue(value: any): any {
181+
// Handle Temporal.Instant - convert to ISO string
182+
if (isInstant(value)) {
183+
return instantToDate(value).toISOString();
184+
}
185+
// Handle legacy Date objects
175186
if (value instanceof Date) {
176187
return value.toISOString();
177188
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/**
2+
* Temporal API types for representing dates/times and durations.
3+
*
4+
* This module re-exports the Temporal types from temporal-polyfill and provides
5+
* helper functions for working with them in the context of SQLite.
6+
*
7+
* @see https://tc39.es/proposal-temporal/docs/
8+
* @see https://github.com/fullcalendar/temporal-polyfill
9+
*/
10+
11+
import { Temporal } from "temporal-polyfill";
12+
13+
// Re-export the Temporal namespace for use with this SDK
14+
export { Temporal };
15+
16+
/**
17+
* Represents an exact point in time, independent of timezone.
18+
* This is a re-export of Temporal.Instant from temporal-polyfill.
19+
*/
20+
export type Instant = Temporal.Instant;
21+
22+
/**
23+
* Represents a duration of time.
24+
* This is a re-export of Temporal.Duration from temporal-polyfill.
25+
*/
26+
export type Duration = Temporal.Duration;
27+
28+
/**
29+
* Creates a Duration from seconds.
30+
* @param seconds Number of seconds
31+
* @returns A Temporal.Duration representing the given seconds
32+
*/
33+
export function durationFromSeconds(seconds: number): Duration {
34+
return Temporal.Duration.from({ seconds });
35+
}
36+
37+
/**
38+
* Converts a Duration to total seconds.
39+
* @param duration The duration to convert
40+
* @returns Total seconds (may be fractional for sub-second durations)
41+
*/
42+
export function durationToSeconds(duration: Duration): number {
43+
return duration.total({ unit: "second" });
44+
}
45+
46+
/**
47+
* Creates an Instant from a Date object.
48+
* @param date JavaScript Date object
49+
* @returns A Temporal.Instant
50+
*/
51+
export function instantFromDate(date: Date): Instant {
52+
return Temporal.Instant.fromEpochMilliseconds(date.getTime());
53+
}
54+
55+
/**
56+
* Creates an Instant from epoch milliseconds.
57+
* @param epochMs Unix timestamp in milliseconds
58+
* @returns A Temporal.Instant
59+
*/
60+
export function instantFromEpochMilliseconds(epochMs: number): Instant {
61+
return Temporal.Instant.fromEpochMilliseconds(epochMs);
62+
}
63+
64+
/**
65+
* Converts an Instant to a Date object.
66+
* @param instant The instant to convert
67+
* @returns A JavaScript Date object
68+
*/
69+
export function instantToDate(instant: Instant): Date {
70+
return new Date(instant.epochMilliseconds);
71+
}
72+
73+
/**
74+
* Converts an Instant to epoch milliseconds.
75+
* @param instant The instant to convert
76+
* @returns Unix timestamp in milliseconds
77+
*/
78+
export function instantToEpochMilliseconds(instant: Instant): number {
79+
return instant.epochMilliseconds;
80+
}
81+
82+
/**
83+
* Type guard to check if a value is a Temporal.Instant.
84+
* @param value The value to check
85+
* @returns True if the value is a Temporal.Instant
86+
*/
87+
export function isInstant(value: unknown): value is Instant {
88+
return (
89+
value !== null &&
90+
typeof value === "object" &&
91+
value instanceof Temporal.Instant
92+
);
93+
}
94+
95+
/**
96+
* Type guard to check if a value is a Temporal.Duration.
97+
* @param value The value to check
98+
* @returns True if the value is a Temporal.Duration
99+
*/
100+
export function isDuration(value: unknown): value is Duration {
101+
return (
102+
value !== null &&
103+
typeof value === "object" &&
104+
value instanceof Temporal.Duration
105+
);
106+
}

sdks/typescript/test/basic.test.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from "vitest";
1010
import { createTestAbsurd, randomName, type TestContext } from "./setup.js";
1111
import type { Absurd } from "../src/index.js";
12+
import { instantFromDate } from "../src/temporal-types.js";
1213
import { EventEmitter, once } from "events";
1314

1415
describe("Basic SDK Operations", () => {
@@ -170,7 +171,7 @@ describe("Basic SDK Operations", () => {
170171
const scheduledRun = await ctx.getRun(runID);
171172
expect(scheduledRun).toMatchObject({
172173
state: "sleeping",
173-
available_at: wakeAt,
174+
available_at: instantFromDate(wakeAt),
174175
wake_event: null,
175176
});
176177

@@ -188,7 +189,7 @@ describe("Basic SDK Operations", () => {
188189
const resumedRun = await ctx.getRun(runID);
189190
expect(resumedRun).toMatchObject({
190191
state: "running",
191-
started_at: wakeAt,
192+
started_at: instantFromDate(wakeAt),
192193
});
193194
});
194195

@@ -215,7 +216,7 @@ describe("Basic SDK Operations", () => {
215216
expect(running).toMatchObject({
216217
state: "running",
217218
claimed_by: "worker-a",
218-
claim_expires_at: new Date(baseTime.getTime() + 30 * 1000),
219+
claim_expires_at: instantFromDate(new Date(baseTime.getTime() + 30 * 1000)),
219220
});
220221

221222
await ctx.setFakeNow(new Date(baseTime.getTime() + 5 * 60 * 1000));
@@ -274,7 +275,7 @@ describe("Basic SDK Operations", () => {
274275
const runRow = await ctx.getRun(runID);
275276
expect(runRow).toMatchObject({
276277
claimed_by: "worker-clean",
277-
claim_expires_at: new Date(base.getTime() + 60 * 1000),
278+
claim_expires_at: instantFromDate(new Date(base.getTime() + 60 * 1000)),
278279
});
279280

280281
const beforeTTL = new Date(finishTime.getTime() + 30 * 60 * 1000);
@@ -481,7 +482,7 @@ describe("Basic SDK Operations", () => {
481482

482483
const getExpiresAt = async (runID: string) => {
483484
const run = await ctx.getRun(runID);
484-
return run?.claim_expires_at ? run.claim_expires_at.getTime() : 0;
485+
return run?.claim_expires_at ? run.claim_expires_at.epochMilliseconds : 0;
485486
};
486487

487488
absurd.workBatch("test-worker", claimTimeout);

sdks/typescript/test/events.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ describe("Event system", () => {
109109
wake_event: eventName,
110110
});
111111
const expectedWake = new Date(baseTime.getTime() + timeoutSeconds * 1000);
112-
expect(sleepingRun?.available_at?.getTime()).toBe(expectedWake.getTime());
112+
expect(sleepingRun?.available_at?.epochMilliseconds).toBe(expectedWake.getTime());
113113

114114
await ctx.setFakeNow(new Date(expectedWake.getTime() + 1000));
115115
await absurd.workBatch("worker1", 120, 1);

sdks/typescript/test/retry.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,8 @@ describe("Retry and cancellation", () => {
312312

313313
await absurd.cancelTask(taskID);
314314
const second = await ctx.getTask(taskID);
315-
expect(second?.cancelled_at?.getTime()).toBe(
316-
first?.cancelled_at?.getTime(),
315+
expect(second?.cancelled_at?.epochMilliseconds).toBe(
316+
first?.cancelled_at?.epochMilliseconds,
317317
);
318318
});
319319

0 commit comments

Comments
 (0)