chore(db): remove obsolete JSON FCM token repository

Delete the unused JSON storage implementation now that SQLite is the sole persistence backend.
This commit is contained in:
Jose Olarte III
2026-07-24 17:57:44 +08:00
parent e79d55fac6
commit ddb969c26d
2 changed files with 0 additions and 292 deletions

View File

@@ -1,290 +0,0 @@
import { randomUUID } from "node:crypto";
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
import path from "node:path";
const dataDir =
process.env.FCM_TOKEN_DATA_DIR ?? path.join(process.cwd(), "data");
const dataFile = path.join(dataDir, "fcm-tokens.json");
export type StoredRow = {
id: string;
userId: string;
deviceId: string;
fcmToken: string;
platform: string;
testMode?: boolean;
createdAt: string;
updatedAt: string;
lastNotifiedAt?: number;
};
type ParsedRow = {
id?: string;
userId?: string;
deviceId?: string;
fcmToken: string;
platform: string;
testMode?: boolean;
createdAt: string;
updatedAt: string;
lastNotifiedAt?: number | string;
};
export function storageKey(userId: string, deviceId: string): string {
return `${userId}::${deviceId}`;
}
function mergeDeviceRows(
key: string,
a: StoredRow,
b: StoredRow
): StoredRow {
const primary =
new Date(a.updatedAt) >= new Date(b.updatedAt) ? a : b;
const lastMs = Math.max(a.lastNotifiedAt ?? 0, b.lastNotifiedAt ?? 0);
const created =
new Date(a.createdAt) <= new Date(b.createdAt)
? a.createdAt
: b.createdAt;
return {
...primary,
id: primary.id,
userId: primary.userId,
deviceId: primary.deviceId,
fcmToken: primary.fcmToken,
lastNotifiedAt: lastMs > 0 ? lastMs : undefined,
createdAt: created,
};
}
function normalizeParsedRow(
mapKey: string,
r: ParsedRow,
onMutate: () => void
): StoredRow {
let id = r.id;
if (id === undefined || id === "") {
id = randomUUID();
onMutate();
}
let lastNotifiedAt: number | undefined;
if (typeof r.lastNotifiedAt === "string") {
const ms = Date.parse(r.lastNotifiedAt);
lastNotifiedAt = Number.isNaN(ms) ? undefined : ms;
onMutate();
} else if (typeof r.lastNotifiedAt === "number") {
lastNotifiedAt = Number.isNaN(r.lastNotifiedAt)
? undefined
: r.lastNotifiedAt;
}
const deviceId = (r.deviceId ?? r.fcmToken ?? mapKey).trim();
if (r.deviceId === undefined || r.deviceId === "") {
onMutate();
}
let userId = r.userId?.trim();
if (userId === undefined || userId === "") {
const fromKey = mapKey.includes("::")
? mapKey.slice(0, mapKey.indexOf("::"))
: "";
userId = fromKey || "__legacy__";
onMutate();
}
return {
id,
userId,
deviceId,
fcmToken: r.fcmToken,
platform: r.platform,
testMode: r.testMode,
createdAt: r.createdAt,
updatedAt: r.updatedAt,
lastNotifiedAt,
};
}
function rowKey(row: StoredRow): string {
if (row.userId === "__legacy__") {
return row.deviceId;
}
return storageKey(row.userId, row.deviceId);
}
async function load(): Promise<Record<string, StoredRow>> {
try {
const raw = await readFile(dataFile, "utf8");
const parsed = JSON.parse(raw) as Record<string, ParsedRow>;
let dirty = false;
const markDirty = (): void => {
dirty = true;
};
const buckets = new Map<string, StoredRow[]>();
for (const [mapKey, rawRow] of Object.entries(parsed)) {
const row = normalizeParsedRow(mapKey, rawRow, markDirty);
const key = rowKey(row);
if (mapKey !== key) markDirty();
const list = buckets.get(key) ?? [];
list.push(row);
buckets.set(key, list);
}
const out: Record<string, StoredRow> = {};
for (const [key, rows] of buckets) {
if (rows.length === 1) {
out[key] = rows[0];
} else {
out[key] = rows
.slice(1)
.reduce((acc, cur) => mergeDeviceRows(key, acc, cur), rows[0]);
markDirty();
}
}
if (dirty) await save(out);
return out;
} catch (e: unknown) {
const code = (e as NodeJS.ErrnoException).code;
if (code === "ENOENT") return {};
throw e;
}
}
async function save(records: Record<string, StoredRow>): Promise<void> {
await mkdir(dataDir, { recursive: true });
const tmp = path.join(dataDir, `.fcm-tokens.${process.pid}.tmp`);
const payload = JSON.stringify(records, null, 2);
await writeFile(tmp, payload, "utf8");
await rename(tmp, dataFile);
}
export const db = {
async upsert(row: {
userId: string;
deviceId: string;
fcmToken: string;
platform: string;
testMode?: boolean;
updatedAt: Date;
}): Promise<void> {
const all = await load();
const key = storageKey(row.userId, row.deviceId);
const prev = all[key];
const now = row.updatedAt.toISOString();
all[key] = {
id: prev?.id ?? randomUUID(),
userId: row.userId,
deviceId: row.deviceId,
fcmToken: row.fcmToken,
platform: row.platform,
testMode: row.testMode,
updatedAt: now,
createdAt: prev?.createdAt ?? now,
lastNotifiedAt: prev?.lastNotifiedAt,
};
for (const k of [...Object.keys(all)]) {
const other = all[k];
if (
k !== key &&
other.userId === row.userId &&
other.fcmToken === row.fcmToken
) {
delete all[k];
}
}
await save(all);
},
async getAll(): Promise<StoredRow[]> {
const all = await load();
return Object.values(all);
},
/** Scheduler iteration; excludes legacy rows pending migration cleanup. */
async getAllForScheduler(): Promise<StoredRow[]> {
const all = await load();
// TODO: migrate or remove __legacy__ rows after auth rollout
return Object.values(all).filter((r) => r.userId !== "__legacy__");
},
/**
* Resolve a device owned by userId via deviceId and/or fcmToken.
* When both are given, they must refer to the same row.
*/
async resolveOwnedDevice(
userId: string,
query: { deviceId?: string; fcmToken?: string }
): Promise<StoredRow | undefined> {
const deviceId = query.deviceId?.trim();
const fcmToken = query.fcmToken;
if (deviceId !== undefined && deviceId.length > 0) {
const byDevice = await this.getByDeviceId(userId, deviceId);
if (byDevice === undefined) return undefined;
if (
fcmToken !== undefined &&
fcmToken.length > 0 &&
byDevice.fcmToken !== fcmToken
) {
return undefined;
}
return byDevice;
}
if (fcmToken !== undefined && fcmToken.length > 0) {
return this.getByFcmTokenForUser(userId, fcmToken);
}
return undefined;
},
async getByUserId(userId: string): Promise<StoredRow[]> {
const all = await load();
return Object.values(all).filter((r) => r.userId === userId);
},
async getByDeviceId(
userId: string,
deviceId: string
): Promise<StoredRow | undefined> {
const all = await load();
return all[storageKey(userId, deviceId)];
},
async getByFcmToken(fcmToken: string): Promise<StoredRow | undefined> {
const all = await load();
const matches = Object.values(all).filter((r) => r.fcmToken === fcmToken);
if (matches.length === 0) return undefined;
const owned = matches.filter((r) => r.userId !== "__legacy__");
const pool = owned.length > 0 ? owned : matches;
return pool.sort(
(a, b) =>
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
)[0];
},
async getByFcmTokenForUser(
userId: string,
fcmToken: string
): Promise<StoredRow | undefined> {
const all = await load();
return Object.values(all).find(
(r) => r.userId === userId && r.fcmToken === fcmToken
);
},
async update(id: string, patch: { lastNotifiedAt: number }): Promise<void> {
const all = await load();
const found = Object.entries(all).find(([, r]) => r.id === id);
if (found === undefined) return;
const [key, row] = found;
all[key] = { ...row, ...patch };
await save(all);
},
};

View File

@@ -4,8 +4,6 @@ import { getDatabase } from "./sqlite.js";
/**
* SQLite-backed FCM registration repository.
* This is the service's storage backend.
* The JSON repository is unused at runtime and will be removed in a later cleanup.
* There is no production-data migration from JSON.
*/
export type StoredRow = {
id: string;