feat : notify new update income

This commit is contained in:
2026-09-26 23:34:42 +07:00 Unverified
parent e99a5b76b1
commit 4ef1968b04
14 changed files with 499 additions and 33 deletions
+63
View File
@@ -0,0 +1,63 @@
import "server-only";
import type {
DeploymentStatus,
DeploymentStatusEvent,
} from "./status";
import { parseDeploymentStatusEvent } from "./status";
import {
getRedisClient,
redisEventChannel,
redisEventPrefix,
} from "@/lib/redis/client";
export const DEPLOYMENT_EVENT_TOPIC = "deployments";
const statusKey = () => `${redisEventPrefix()}:deployments:latest`;
const STATUS_TTL_SECONDS: Record<DeploymentStatus, number> = {
deploying: 60 * 60,
ready: 10 * 60,
failed: 10 * 60,
};
const publishScript = `
if ARGV[2] ~= "deploying" then
local current = redis.call("GET", KEYS[1])
if not current then return 0 end
local ok, decoded = pcall(cjson.decode, current)
if not ok or decoded.deploymentId ~= ARGV[3] then return 0 end
end
redis.call("SET", KEYS[1], ARGV[1], "EX", ARGV[4])
redis.call("PUBLISH", KEYS[2], ARGV[1])
return 1
`;
export async function getLatestDeploymentStatus(): Promise<DeploymentStatusEvent | null> {
const redis = await getRedisClient();
const value = await redis.get(statusKey());
return value ? parseDeploymentStatusEvent(value) : null;
}
export async function publishDeploymentStatus(
deploymentId: string,
status: DeploymentStatus,
): Promise<{ accepted: boolean; event: DeploymentStatusEvent }> {
const event: DeploymentStatusEvent = {
deploymentId,
status,
updatedAt: new Date().toISOString(),
};
const payload = JSON.stringify(event);
const redis = await getRedisClient();
const accepted = await redis.eval(
publishScript,
2,
statusKey(),
redisEventChannel(DEPLOYMENT_EVENT_TOPIC),
payload,
status,
deploymentId,
STATUS_TTL_SECONDS[status],
);
return { accepted: accepted === 1, event };
}
+46
View File
@@ -0,0 +1,46 @@
import { describe, expect, it } from "vitest";
import {
isDeploymentId,
isDeploymentStatus,
parseDeploymentStatusEvent,
} from "./status";
const deploymentId = "a".repeat(40);
describe("deployment lifecycle status", () => {
it("accepts supported statuses and full commit identifiers", () => {
expect(isDeploymentId(deploymentId)).toBe(true);
expect(isDeploymentId("abc123")).toBe(false);
expect(isDeploymentStatus("deploying")).toBe(true);
expect(isDeploymentStatus("ready")).toBe(true);
expect(isDeploymentStatus("failed")).toBe(true);
expect(isDeploymentStatus("unknown")).toBe(false);
});
it("parses a valid lifecycle event", () => {
expect(parseDeploymentStatusEvent(JSON.stringify({
deploymentId,
status: "deploying",
updatedAt: "2026-09-26T12:00:00.000Z",
}))).toEqual({
deploymentId,
status: "deploying",
updatedAt: "2026-09-26T12:00:00.000Z",
});
});
it("rejects malformed or incomplete events", () => {
expect(parseDeploymentStatusEvent("not-json")).toBeNull();
expect(parseDeploymentStatusEvent(JSON.stringify({
deploymentId,
status: "unknown",
updatedAt: "2026-09-26T12:00:00.000Z",
}))).toBeNull();
expect(parseDeploymentStatusEvent(JSON.stringify({
deploymentId,
status: "ready",
updatedAt: "not-a-date",
}))).toBeNull();
});
});
+42
View File
@@ -0,0 +1,42 @@
export const DEPLOYMENT_STATUSES = ["deploying", "ready", "failed"] as const;
export type DeploymentStatus = (typeof DEPLOYMENT_STATUSES)[number];
export interface DeploymentStatusEvent {
deploymentId: string;
status: DeploymentStatus;
updatedAt: string;
}
const DEPLOYMENT_ID_PATTERN = /^[a-f0-9]{40}$/u;
export function isDeploymentId(value: unknown): value is string {
return typeof value === "string" && DEPLOYMENT_ID_PATTERN.test(value);
}
export function isDeploymentStatus(value: unknown): value is DeploymentStatus {
return DEPLOYMENT_STATUSES.some((status) => status === value);
}
export function parseDeploymentStatusEvent(
value: string,
): DeploymentStatusEvent | null {
try {
const parsed = JSON.parse(value) as Record<string, unknown>;
if (
!isDeploymentId(parsed.deploymentId) ||
!isDeploymentStatus(parsed.status) ||
typeof parsed.updatedAt !== "string" ||
!Number.isFinite(Date.parse(parsed.updatedAt))
) {
return null;
}
return {
deploymentId: parsed.deploymentId,
status: parsed.status,
updatedAt: parsed.updatedAt,
};
} catch {
return null;
}
}