From 903ef8c1a7bfd477a753afc42c8fbf6b3ffacaea Mon Sep 17 00:00:00 2001 From: hbc Date: Sun, 18 Jan 2026 18:15:24 -0800 Subject: [PATCH 1/4] chore: bump api in typescript-client --- samples/typescript-client/package-lock.json | 5 ++++- samples/typescript-client/src/index.ts | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/samples/typescript-client/package-lock.json b/samples/typescript-client/package-lock.json index c4e19f2..3d5b83c 100644 --- a/samples/typescript-client/package-lock.json +++ b/samples/typescript-client/package-lock.json @@ -23,8 +23,11 @@ }, "../../sdks/typescript": { "name": "@absurd-sqlite/sdk", - "version": "0.2.1-alpha.2", + "version": "0.3.0-alpha.1", "license": "Apache-2.0", + "dependencies": { + "temporal-polyfill": "^0.3.0" + }, "devDependencies": { "@types/better-sqlite3": "^7.6.13", "@types/node": "^22.18.0", diff --git a/samples/typescript-client/src/index.ts b/samples/typescript-client/src/index.ts index 157d450..c604ccf 100644 --- a/samples/typescript-client/src/index.ts +++ b/samples/typescript-client/src/index.ts @@ -1,4 +1,4 @@ -import { Absurd, SQLiteConnection, SQLiteDatabase } from "@absurd-sqlite/sdk"; +import { Absurd, SQLiteConnection, SQLiteDatabase, Temporal } from "@absurd-sqlite/sdk"; import sqlite from "better-sqlite3"; async function main() { @@ -27,7 +27,7 @@ async function main() { return {}; }); - await ctx.sleepFor("back off 15s", 15); + await ctx.sleepFor("back off 15s", Temporal.Duration.from({ seconds: 15 })); await ctx.step("process", async () => { console.log("process step"); From b96148918a74d1a455ced0d5fffd82513642c5af Mon Sep 17 00:00:00 2001 From: hbc Date: Sun, 18 Jan 2026 18:24:48 -0800 Subject: [PATCH 2/4] feat: replace datetime with temporal --- sdks/bun-worker/bun.lock | 6 ++++- sdks/bun-worker/package.json | 3 ++- sdks/bun-worker/src/index.ts | 1 + sdks/bun-worker/src/sqlite.ts | 40 +++++++++++++++++++++-------- sdks/bun-worker/test/basic.test.ts | 18 ++++++++----- sdks/bun-worker/test/events.test.ts | 19 +++++++++----- sdks/bun-worker/test/retry.test.ts | 10 ++++---- sdks/bun-worker/test/setup.ts | 19 +++++++------- sdks/bun-worker/test/sqlite.test.ts | 9 ++++--- sdks/bun-worker/test/step.test.ts | 18 +++++++++---- 10 files changed, 96 insertions(+), 47 deletions(-) diff --git a/sdks/bun-worker/bun.lock b/sdks/bun-worker/bun.lock index 6dc77fc..fcd6126 100644 --- a/sdks/bun-worker/bun.lock +++ b/sdks/bun-worker/bun.lock @@ -15,7 +15,7 @@ }, }, "packages": { - "@absurd-sqlite/sdk": ["@absurd-sqlite/sdk@0.3.0-alpha.0", "", { "peerDependencies": { "better-sqlite3": "^12.5.0" } }, "sha512-6l9FkMg1KvOGxLo5aJYR/BCGn3XgSLVGaVjw0hrtkss44u3hvmYjXZzLZDf0wB1lahzBTWRaAHuhjedqg+uF3Q=="], + "@absurd-sqlite/sdk": ["@absurd-sqlite/sdk@0.3.0-alpha.1", "", { "dependencies": { "temporal-polyfill": "^0.3.0" }, "peerDependencies": { "better-sqlite3": "^12.5.0" } }, "sha512-EivuE5Lk61wjHblVsSpZ+QN2v4qIRbJy8e0jWqz8rL3KqP6CLg1+iinKbl4UiNvPs9v+Xwdv6dmoXEwOTIqOKQ=="], "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], @@ -93,6 +93,10 @@ "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "temporal-polyfill": ["temporal-polyfill@0.3.0", "", { "dependencies": { "temporal-spec": "0.3.0" } }, "sha512-qNsTkX9K8hi+FHDfHmf22e/OGuXmfBm9RqNismxBrnSmZVJKegQ+HYYXT+R7Ha8F/YSm2Y34vmzD4cxMu2u95g=="], + + "temporal-spec": ["temporal-spec@0.3.0", "", {}, "sha512-n+noVpIqz4hYgFSMOSiINNOUOMFtV5cZQNCmmszA6GiVFVRt3G7AqVyhXjhCSmowvQn+NsGn+jMDMKJYHd3bSQ=="], + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], diff --git a/sdks/bun-worker/package.json b/sdks/bun-worker/package.json index b8a68fa..1d41401 100644 --- a/sdks/bun-worker/package.json +++ b/sdks/bun-worker/package.json @@ -39,7 +39,8 @@ "homepage": "https://github.com/b4fun/absurd-sqlite#readme", "dependencies": { "@absurd-sqlite/sdk": "next", - "cac": "^6.7.14" + "cac": "^6.7.14", + "temporal-polyfill": "^0.3.0" }, "devDependencies": { "bun-types": "^1.3.6", diff --git a/sdks/bun-worker/src/index.ts b/sdks/bun-worker/src/index.ts index 85beb94..5f2aa5f 100644 --- a/sdks/bun-worker/src/index.ts +++ b/sdks/bun-worker/src/index.ts @@ -14,6 +14,7 @@ export type { WorkerOptions } from "@absurd-sqlite/sdk"; export { downloadExtension, type DownloadExtensionOptions, + Temporal, } from "@absurd-sqlite/sdk"; /** diff --git a/sdks/bun-worker/src/sqlite.ts b/sdks/bun-worker/src/sqlite.ts index 2218de2..6c867eb 100644 --- a/sdks/bun-worker/src/sqlite.ts +++ b/sdks/bun-worker/src/sqlite.ts @@ -8,7 +8,7 @@ import type { SQLiteStatement, SQLiteValueCodec, } from "@absurd-sqlite/sdk"; -import { SQLiteConnection } from "@absurd-sqlite/sdk"; +import { SQLiteConnection, Temporal } from "@absurd-sqlite/sdk"; export class BunSqliteConnection extends SQLiteConnection { constructor(db: Database, options: SQLiteConnectionOptions = {}) { @@ -115,6 +115,19 @@ function decodeRowValues(args: { }) => unknown; }): R { const decodedRow: any = {}; + if (args.columns && args.decodeColumn) { + for (const column of args.columns) { + const columnName = column.name; + const rawValue = args.row[columnName]; + decodedRow[columnName] = args.decodeColumn({ + value: rawValue, + columnName, + columnType: column.type, + }); + } + return decodedRow as R; + } + for (const [columnName, rawValue] of Object.entries(args.row)) { decodedRow[columnName] = decodeColumnValue({ value: rawValue, @@ -132,21 +145,22 @@ function decodeColumnValue(args: { columnType: string | null; verbose?: (...args: any[]) => void; }): V | null { - const { value, columnName } = args; + const { value, columnName, columnType } = args; if (value === null || value === undefined) { return null; } - if (isTimestampColumn(columnName)) { - if (typeof value === "number") { - return new Date(value) as V; - } + const isDateTime = columnType === "datetime" || isTimestampColumn(columnName); + if (isDateTime) { if (typeof value === "string") { - const parsed = Date.parse(value); - if (!Number.isNaN(parsed)) { - return new Date(parsed) as V; - } + return Temporal.Instant.from(value) as V; + } + if (typeof value === "number") { + return Temporal.Instant.fromEpochMilliseconds(value) as V; } + throw new Error( + `Expected datetime column ${columnName} to be a string or number, got ${typeof value}` + ); } if (typeof value === "string") { @@ -171,6 +185,12 @@ function tryDecodeJson(value: string): V | null { } function encodeColumnValue(value: SQLiteBindValue): SQLiteBindValue { + if (value instanceof Temporal.Instant) { + return value.toString(); + } + if (value instanceof Temporal.Duration) { + return value.toString(); + } if (value instanceof Date) { return value.toISOString(); } diff --git a/sdks/bun-worker/test/basic.test.ts b/sdks/bun-worker/test/basic.test.ts index 40f3492..f663431 100644 --- a/sdks/bun-worker/test/basic.test.ts +++ b/sdks/bun-worker/test/basic.test.ts @@ -7,7 +7,7 @@ import { jest, } from "bun:test"; import assert from "node:assert/strict"; -import type { Absurd } from "@absurd-sqlite/sdk"; +import { Temporal, type Absurd } from "@absurd-sqlite/sdk"; import { createTestAbsurd, randomName, type TestContext } from "./setup"; import { EventEmitter, once } from "events"; import { waitFor } from "./wait-for"; @@ -171,7 +171,7 @@ describe("Basic SDK Operations", () => { const scheduledRun = await ctx.getRun(runID); expect(scheduledRun).toMatchObject({ state: "sleeping", - available_at: wakeAt, + available_at: Temporal.Instant.fromEpochMilliseconds(wakeAt.getTime()), wake_event: null, }); @@ -189,7 +189,7 @@ describe("Basic SDK Operations", () => { const resumedRun = await ctx.getRun(runID); expect(resumedRun).toMatchObject({ state: "running", - started_at: wakeAt, + started_at: Temporal.Instant.fromEpochMilliseconds(wakeAt.getTime()), }); }); @@ -216,7 +216,9 @@ describe("Basic SDK Operations", () => { expect(running).toMatchObject({ state: "running", claimed_by: "worker-a", - claim_expires_at: new Date(baseTime.getTime() + 30 * 1000), + claim_expires_at: Temporal.Instant.fromEpochMilliseconds( + baseTime.getTime() + 30 * 1000, + ), }); await ctx.setFakeNow(new Date(baseTime.getTime() + 5 * 60 * 1000)); @@ -275,7 +277,9 @@ describe("Basic SDK Operations", () => { const runRow = await ctx.getRun(runID); expect(runRow).toMatchObject({ claimed_by: "worker-clean", - claim_expires_at: new Date(base.getTime() + 60 * 1000), + claim_expires_at: Temporal.Instant.fromEpochMilliseconds( + base.getTime() + 60 * 1000, + ), }); const beforeTTL = new Date(finishTime.getTime() + 30 * 60 * 1000); @@ -482,7 +486,9 @@ describe("Basic SDK Operations", () => { const getExpiresAt = async (runID: string) => { const run = await ctx.getRun(runID); - return run?.claim_expires_at ? run.claim_expires_at.getTime() : 0; + return run?.claim_expires_at + ? run.claim_expires_at.epochMilliseconds + : 0; }; absurd.workBatch("test-worker", claimTimeout); diff --git a/sdks/bun-worker/test/events.test.ts b/sdks/bun-worker/test/events.test.ts index 9edd62d..65f3ced 100644 --- a/sdks/bun-worker/test/events.test.ts +++ b/sdks/bun-worker/test/events.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect, beforeAll, afterEach } from "bun:test"; -import type { Absurd } from "@absurd-sqlite/sdk"; +import { Temporal, type Absurd } from "@absurd-sqlite/sdk"; import { createTestAbsurd, randomName, type TestContext } from "./setup"; import { TimeoutError } from "@absurd-sqlite/sdk"; @@ -21,7 +21,9 @@ describe("Event system", () => { const eventName = randomName("test_event"); absurd.registerTask({ name: "waiter" }, async (params, ctx) => { - const payload = await ctx.awaitEvent(eventName, { timeout: 60 }); + const payload = await ctx.awaitEvent(eventName, { + timeout: Temporal.Duration.from({ seconds: 60 }), + }); return { received: payload }; }); @@ -86,7 +88,7 @@ describe("Event system", () => { absurd.registerTask({ name: "timeout-waiter" }, async (_params, ctx) => { try { const payload = await ctx.awaitEvent(eventName, { - timeout: timeoutSeconds, + timeout: Temporal.Duration.from({ seconds: timeoutSeconds }), }); return { timedOut: false, result: payload }; } catch (err) { @@ -109,7 +111,9 @@ describe("Event system", () => { wake_event: eventName, }); const expectedWake = new Date(baseTime.getTime() + timeoutSeconds * 1000); - expect(sleepingRun?.available_at?.getTime()).toBe(expectedWake.getTime()); + expect(sleepingRun?.available_at?.epochMilliseconds).toBe( + expectedWake.getTime(), + ); await ctx.setFakeNow(new Date(expectedWake.getTime() + 1000)); await absurd.workBatch("worker1", 120, 1); @@ -170,13 +174,16 @@ describe("Event system", () => { absurd.registerTask({ name: "timeout-no-loop" }, async (_params, ctx) => { try { - await ctx.awaitEvent(eventName, { stepName: "wait", timeout: 10 }); + await ctx.awaitEvent(eventName, { + stepName: "wait", + timeout: Temporal.Duration.from({ seconds: 10 }), + }); return { stage: "unexpected" }; } catch (err) { if (err instanceof TimeoutError) { const payload = await ctx.awaitEvent(eventName, { stepName: "wait", - timeout: 10, + timeout: Temporal.Duration.from({ seconds: 10 }), }); return { stage: "resumed", payload }; } diff --git a/sdks/bun-worker/test/retry.test.ts b/sdks/bun-worker/test/retry.test.ts index 6702663..330e1ac 100644 --- a/sdks/bun-worker/test/retry.test.ts +++ b/sdks/bun-worker/test/retry.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect, beforeAll, afterEach } from "bun:test"; -import type { Absurd } from "@absurd-sqlite/sdk"; +import { Temporal, type Absurd } from "@absurd-sqlite/sdk"; import { createTestAbsurd, randomName, type TestContext } from "./setup"; describe("Retry and cancellation", () => { @@ -159,7 +159,7 @@ describe("Retry and cancellation", () => { const { taskID } = await absurd.spawn("duration-cancel", undefined, { maxAttempts: 4, retryStrategy: { kind: "fixed", baseSeconds: 30 }, - cancellation: { maxDuration: 90 }, + cancellation: { maxDuration: Temporal.Duration.from({ seconds: 90 }) }, }); await absurd.workBatch("worker1", 60, 1); @@ -185,7 +185,7 @@ describe("Retry and cancellation", () => { }); const { taskID } = await absurd.spawn("delay-cancel", undefined, { - cancellation: { maxDelay: 60 }, + cancellation: { maxDelay: Temporal.Duration.from({ seconds: 60 }) }, }); await ctx.setFakeNow(new Date(baseTime.getTime() + 61 * 1000)); @@ -312,8 +312,8 @@ describe("Retry and cancellation", () => { await absurd.cancelTask(taskID); const second = await ctx.getTask(taskID); - expect(second?.cancelled_at?.getTime()).toBe( - first?.cancelled_at?.getTime(), + expect(second?.cancelled_at?.epochMilliseconds).toBe( + first?.cancelled_at?.epochMilliseconds, ); }); diff --git a/sdks/bun-worker/test/setup.ts b/sdks/bun-worker/test/setup.ts index 84b3bd9..d0df428 100644 --- a/sdks/bun-worker/test/setup.ts +++ b/sdks/bun-worker/test/setup.ts @@ -6,6 +6,7 @@ import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { Absurd, + Temporal, type AbsurdHooks, type JsonValue, } from "@absurd-sqlite/sdk"; @@ -23,8 +24,8 @@ export interface TaskRow { retry_strategy: JsonValue | null; max_attempts: number | null; cancellation: JsonValue | null; - enqueue_at: Date; - first_started_at: Date | null; + enqueue_at: Temporal.Instant; + first_started_at: Temporal.Instant | null; state: | "pending" | "running" @@ -35,7 +36,7 @@ export interface TaskRow { attempts: number; last_attempt_run: string | null; completed_payload: JsonValue | null; - cancelled_at: Date | null; + cancelled_at: Temporal.Instant | null; } export interface RunRow { @@ -50,16 +51,16 @@ export interface RunRow { | "failed" | "cancelled"; claimed_by: string | null; - claim_expires_at: Date | null; - available_at: Date; + claim_expires_at: Temporal.Instant | null; + available_at: Temporal.Instant; wake_event: string | null; event_payload: JsonValue | null; - started_at: Date | null; - completed_at: Date | null; - failed_at: Date | null; + started_at: Temporal.Instant | null; + completed_at: Temporal.Instant | null; + failed_at: Temporal.Instant | null; result: JsonValue | null; failure_reason: JsonValue | null; - created_at: Date; + created_at: Temporal.Instant; } interface SqliteFixture { diff --git a/sdks/bun-worker/test/sqlite.test.ts b/sdks/bun-worker/test/sqlite.test.ts index 09816df..7291405 100644 --- a/sdks/bun-worker/test/sqlite.test.ts +++ b/sdks/bun-worker/test/sqlite.test.ts @@ -5,6 +5,7 @@ import { join } from "node:path"; import { tmpdir } from "node:os"; import { BunSqliteConnection } from "../src/sqlite"; +import { Temporal } from "@absurd-sqlite/sdk"; describe("BunSqliteConnection", () => { it("rewrites postgres-style params and absurd schema names", async () => { @@ -75,7 +76,7 @@ describe("BunSqliteConnection", () => { db.close(); }); - it("decodes datetime columns into Date objects", async () => { + it("decodes datetime columns into Temporal.Instant objects", async () => { const db = new Database(":memory:"); const conn = new BunSqliteConnection(db); const now = Date.now(); @@ -83,12 +84,12 @@ describe("BunSqliteConnection", () => { await conn.exec("CREATE TABLE t_date (created_at DATETIME)"); await conn.exec("INSERT INTO t_date (created_at) VALUES ($1)", [now]); - const { rows } = await conn.query<{ created_at: Date }>( + const { rows } = await conn.query<{ created_at: Temporal.Instant }>( "SELECT created_at FROM t_date" ); - expect(rows[0]?.created_at).toBeInstanceOf(Date); - expect(rows[0]?.created_at.getTime()).toBe(now); + expect(rows[0]?.created_at).toBeInstanceOf(Temporal.Instant); + expect(rows[0]?.created_at.epochMilliseconds).toBe(now); db.close(); }); diff --git a/sdks/bun-worker/test/step.test.ts b/sdks/bun-worker/test/step.test.ts index b8259ea..bd11c34 100644 --- a/sdks/bun-worker/test/step.test.ts +++ b/sdks/bun-worker/test/step.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect, beforeAll, afterEach, jest } from "bun:test"; -import type { Absurd } from "@absurd-sqlite/sdk"; +import { Temporal, type Absurd } from "@absurd-sqlite/sdk"; import { createTestAbsurd, randomName, type TestContext } from "./setup"; describe("Step functionality", () => { @@ -193,7 +193,10 @@ describe("Step functionality", () => { const durationSeconds = 60; absurd.registerTask({ name: "sleep-for" }, async (_params, ctx) => { - await ctx.sleepFor("wait-for", durationSeconds); + await ctx.sleepFor( + "wait-for", + Temporal.Duration.from({ seconds: durationSeconds }), + ); return { resumed: true }; }); @@ -205,7 +208,9 @@ describe("Step functionality", () => { state: "sleeping", }); const wakeTime = new Date(base.getTime() + durationSeconds * 1000); - expect(sleepingRun?.available_at?.getTime()).toBe(wakeTime.getTime()); + expect(sleepingRun?.available_at?.epochMilliseconds).toBe( + wakeTime.getTime(), + ); const resumeTime = new Date(wakeTime.getTime() + 5 * 1000); jest.setSystemTime(resumeTime); @@ -225,11 +230,14 @@ describe("Step functionality", () => { await ctx.setFakeNow(base); const wakeTime = new Date(base.getTime() + 5 * 60 * 1000); + const wakeInstant = Temporal.Instant.fromEpochMilliseconds( + wakeTime.getTime(), + ); let executions = 0; absurd.registerTask({ name: "sleep-until" }, async (_params, ctx) => { executions++; - await ctx.sleepUntil("sleep-step", wakeTime); + await ctx.sleepUntil("sleep-step", wakeInstant); return { executions }; }); @@ -240,7 +248,7 @@ describe("Step functionality", () => { expect(checkpointRow).toMatchObject({ checkpoint_name: "sleep-step", owner_run_id: runID, - state: wakeTime.toISOString(), + state: wakeInstant.toString(), }); const sleepingRun = await ctx.getRun(runID); From 36ec5351c1979ae2e0dad60edb0df2bd2aa6e9e8 Mon Sep 17 00:00:00 2001 From: hbc Date: Sun, 18 Jan 2026 18:26:59 -0800 Subject: [PATCH 3/4] chore: update sample to use Temporal api --- samples/bun-worker/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/bun-worker/src/index.ts b/samples/bun-worker/src/index.ts index 4b159bc..7afab99 100644 --- a/samples/bun-worker/src/index.ts +++ b/samples/bun-worker/src/index.ts @@ -1,4 +1,4 @@ -import run from "@absurd-sqlite/bun-worker"; +import run, { Temporal } from "@absurd-sqlite/bun-worker"; import { Database } from "bun:sqlite"; import { existsSync, readdirSync } from "node:fs"; import { join } from "node:path"; @@ -19,7 +19,7 @@ await run(async (absurd) => { return {}; }); - await ctx.sleepFor("back off 15s", 15); + await ctx.sleepFor("back off 15s", Temporal.Duration.from({ seconds: 15 })); await ctx.step("process", async () => { console.log("process step"); From 8c56948e55b233fed6d10a0f4d76862d40e75c9e Mon Sep 17 00:00:00 2001 From: hbc Date: Sun, 18 Jan 2026 18:28:09 -0800 Subject: [PATCH 4/4] sdks/bun-worker: 0.3.0-alpha.1 --- sdks/bun-worker/bun.lock | 1 + sdks/bun-worker/package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/sdks/bun-worker/bun.lock b/sdks/bun-worker/bun.lock index fcd6126..322d5f8 100644 --- a/sdks/bun-worker/bun.lock +++ b/sdks/bun-worker/bun.lock @@ -7,6 +7,7 @@ "dependencies": { "@absurd-sqlite/sdk": "next", "cac": "^6.7.14", + "temporal-polyfill": "^0.3.0", }, "devDependencies": { "bun-types": "^1.3.6", diff --git a/sdks/bun-worker/package.json b/sdks/bun-worker/package.json index 1d41401..94e97a5 100644 --- a/sdks/bun-worker/package.json +++ b/sdks/bun-worker/package.json @@ -1,6 +1,6 @@ { "name": "@absurd-sqlite/bun-worker", - "version": "0.3.0-alpha.0", + "version": "0.3.0-alpha.1", "description": "Bun worker utilities for Absurd-SQLite", "type": "module", "main": "dist/index.js",