86 lines
2.8 KiB
TypeScript
86 lines
2.8 KiB
TypeScript
import { config as loadDotenv } from "dotenv";
|
|
import { existsSync } from "node:fs";
|
|
import path from "node:path";
|
|
|
|
// Ladda .env från paketet ELLER monorepo-roten (pnpm --filter sätter cwd till paketet).
|
|
for (const candidate of [".env", "../.env", "../../.env"]) {
|
|
const p = path.resolve(process.cwd(), candidate);
|
|
if (existsSync(p)) {
|
|
loadDotenv({ path: p });
|
|
break;
|
|
}
|
|
}
|
|
import { createHmac } from "node:crypto";
|
|
import { createDatabase, type Database } from "@app/database";
|
|
import { createAamosClient, type AamosClient, type BudgetStore } from "@app/ai-contracts";
|
|
import type { Redis } from "ioredis";
|
|
|
|
export interface WorkerContext {
|
|
db: Database;
|
|
aamos: AamosClient;
|
|
redis?: Redis;
|
|
/** Delad dagsbudget-store så att memory-jobbet kan spegla skanningens budgetkoll. */
|
|
budgetStore?: BudgetStore;
|
|
/** Bygger läs-URL för lagrade bilder (samma signaturlogik som API:ts mock-S3). */
|
|
readUrl: (key: string) => string;
|
|
apiBaseUrl: string;
|
|
close: () => Promise<void>;
|
|
}
|
|
|
|
class RedisBudgetStore implements BudgetStore {
|
|
constructor(
|
|
private readonly redis: Redis,
|
|
private readonly key: string,
|
|
) {}
|
|
|
|
async getDailySpendUsd(): Promise<number> {
|
|
const val = await this.redis.get(this.key);
|
|
return val ? Number(val) : 0;
|
|
}
|
|
|
|
async incrementDailySpendUsd(amountUsd: number): Promise<number> {
|
|
const newVal = await this.redis.incrbyfloat(this.key, amountUsd);
|
|
// Expire at next midnight UTC to keep daily window.
|
|
const ttl = this.secondsUntilMidnightUtc();
|
|
await this.redis.expire(this.key, ttl);
|
|
return Number(newVal);
|
|
}
|
|
|
|
private secondsUntilMidnightUtc(): number {
|
|
const now = new Date();
|
|
const midnight = new Date(
|
|
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1),
|
|
);
|
|
return Math.max(1, Math.floor((midnight.getTime() - now.getTime()) / 1000));
|
|
}
|
|
}
|
|
|
|
export function createContext(redis?: Redis): WorkerContext {
|
|
const databaseUrl = process.env.DATABASE_URL;
|
|
if (!databaseUrl) {
|
|
throw new Error("Missing DATABASE_URL. Worker must connect explicitly to the app database.");
|
|
}
|
|
const { db, pool } = createDatabase(databaseUrl);
|
|
const budgetStore = redis
|
|
? new RedisBudgetStore(redis, `gemini:daily:budget:${new Date().toISOString().slice(0, 10)}`)
|
|
: undefined;
|
|
const aamos = createAamosClient(undefined, { budgetStore });
|
|
const apiBaseUrl = process.env.API_BASE_URL ?? "http://localhost:4000";
|
|
const secret = process.env.ENTITLEMENT_SIGNING_SECRET ?? "dev-only-change-me-three";
|
|
|
|
return {
|
|
db,
|
|
aamos,
|
|
redis,
|
|
budgetStore,
|
|
apiBaseUrl,
|
|
readUrl: (key: string) => {
|
|
const sig = createHmac("sha256", secret).update(key).digest("hex").slice(0, 32);
|
|
return `${apiBaseUrl}/v1/mock-s3/${encodeURIComponent(key)}?sig=${sig}`;
|
|
},
|
|
close: async () => {
|
|
await pool.end();
|
|
},
|
|
};
|
|
}
|