feat(tasks): implement long-term delayed tasks with KV and Cron

This commit is contained in:
2025-11-29 09:03:21 -05:00
parent 40fa0080b5
commit 24d507a48f
8 changed files with 769 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
import { DateTime } from "luxon";
import { beforeEach, describe, expect, it, mock } from "bun:test";
import type { DelayedTaskMetadata } from "./delayedTask";
import {
deserializeDelayedTask,
generateTaskKey,
serializeDelayedTask,
} from "./delayedTask";
describe("delayedTask", () => {
describe("generateTaskKey", () => {
it("generates key with correct format", () => {
const scheduledTime = 1732896000;
const taskId = "abc-123-def";
const key = generateTaskKey(scheduledTime, taskId);
expect(key).toBe("delayed-task:1732896000:abc-123-def");
});
it("generates unique keys for different timestamps", () => {
const taskId = "same-id";
const key1 = generateTaskKey(1000, taskId);
const key2 = generateTaskKey(2000, taskId);
expect(key1).not.toBe(key2);
});
it("generates unique keys for different task IDs", () => {
const timestamp = 1000;
const key1 = generateTaskKey(timestamp, "id-1");
const key2 = generateTaskKey(timestamp, "id-2");
expect(key1).not.toBe(key2);
});
});
describe("serializeDelayedTask & deserializeDelayedTask", () => {
let testMetadata: DelayedTaskMetadata;
beforeEach(() => {
testMetadata = {
queueName: "NEW_EPISODE",
body: { aniListId: 12345, episodeNumber: 1 },
headers: {
"Content-Type": "application/json",
"X-Anilist-Token": "test-token",
},
scheduledEpochTime: 1732896000,
taskId: "test-task-id",
createdAt: 1732800000,
retryCount: 0,
};
});
it("serializes metadata to JSON string", () => {
const serialized = serializeDelayedTask(testMetadata);
expect(serialized).toBeTypeOf("string");
expect(() => JSON.parse(serialized)).not.toThrow();
});
it("deserializes JSON string back to metadata", () => {
const serialized = serializeDelayedTask(testMetadata);
const deserialized = deserializeDelayedTask(serialized);
expect(deserialized).toEqual(testMetadata);
});
it("preserves all metadata fields", () => {
const serialized = serializeDelayedTask(testMetadata);
const deserialized = deserializeDelayedTask(serialized);
expect(deserialized.queueName).toBe("NEW_EPISODE");
expect(deserialized.body).toEqual({ aniListId: 12345, episodeNumber: 1 });
expect(deserialized.headers).toEqual({
"Content-Type": "application/json",
"X-Anilist-Token": "test-token",
});
expect(deserialized.scheduledEpochTime).toBe(1732896000);
expect(deserialized.taskId).toBe("test-task-id");
expect(deserialized.createdAt).toBe(1732800000);
expect(deserialized.retryCount).toBe(0);
});
it("handles metadata without retryCount", () => {
delete testMetadata.retryCount;
const serialized = serializeDelayedTask(testMetadata);
const deserialized = deserializeDelayedTask(serialized);
expect(deserialized.retryCount).toBeUndefined();
});
it("handles ANILIST_UPDATES queue body", () => {
const anilistMetadata: DelayedTaskMetadata = {
queueName: "ANILIST_UPDATES",
body: {
deviceId: "device-123",
watchStatus: null,
titleId: 456,
updateType: 0,
},
headers: { "Content-Type": "application/json" },
scheduledEpochTime: 1732896000,
taskId: "test-id",
createdAt: 1732800000,
};
const serialized = serializeDelayedTask(anilistMetadata);
const deserialized = deserializeDelayedTask(serialized);
expect(deserialized.queueName).toBe("ANILIST_UPDATES");
expect(deserialized.body).toEqual({
deviceId: "device-123",
watchStatus: null,
titleId: 456,
updateType: 0,
});
});
});
});