mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 10:21:24 +00:00
refactor(infra): extract json file + async lock helpers
This commit is contained in:
52
src/infra/json-files.ts
Normal file
52
src/infra/json-files.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
export async function readJsonFile<T>(filePath: string): Promise<T | null> {
|
||||||
|
try {
|
||||||
|
const raw = await fs.readFile(filePath, "utf8");
|
||||||
|
return JSON.parse(raw) as T;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function writeJsonAtomic(
|
||||||
|
filePath: string,
|
||||||
|
value: unknown,
|
||||||
|
options?: { mode?: number },
|
||||||
|
) {
|
||||||
|
const mode = options?.mode ?? 0o600;
|
||||||
|
const dir = path.dirname(filePath);
|
||||||
|
await fs.mkdir(dir, { recursive: true });
|
||||||
|
const tmp = `${filePath}.${randomUUID()}.tmp`;
|
||||||
|
await fs.writeFile(tmp, JSON.stringify(value, null, 2), "utf8");
|
||||||
|
try {
|
||||||
|
await fs.chmod(tmp, mode);
|
||||||
|
} catch {
|
||||||
|
// best-effort; ignore on platforms without chmod
|
||||||
|
}
|
||||||
|
await fs.rename(tmp, filePath);
|
||||||
|
try {
|
||||||
|
await fs.chmod(filePath, mode);
|
||||||
|
} catch {
|
||||||
|
// best-effort; ignore on platforms without chmod
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAsyncLock() {
|
||||||
|
let lock: Promise<void> = Promise.resolve();
|
||||||
|
return async function withLock<T>(fn: () => Promise<T>): Promise<T> {
|
||||||
|
const prev = lock;
|
||||||
|
let release: (() => void) | undefined;
|
||||||
|
lock = new Promise<void>((resolve) => {
|
||||||
|
release = resolve;
|
||||||
|
});
|
||||||
|
await prev;
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} finally {
|
||||||
|
release?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
|
||||||
import fs from "node:fs/promises";
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { resolveStateDir } from "../config/paths.js";
|
import { resolveStateDir } from "../config/paths.js";
|
||||||
|
|
||||||
|
export { createAsyncLock, readJsonFile, writeJsonAtomic } from "./json-files.js";
|
||||||
|
|
||||||
export function resolvePairingPaths(baseDir: string | undefined, subdir: string) {
|
export function resolvePairingPaths(baseDir: string | undefined, subdir: string) {
|
||||||
const root = baseDir ?? resolveStateDir();
|
const root = baseDir ?? resolveStateDir();
|
||||||
const dir = path.join(root, subdir);
|
const dir = path.join(root, subdir);
|
||||||
@@ -13,33 +13,6 @@ export function resolvePairingPaths(baseDir: string | undefined, subdir: string)
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function readJsonFile<T>(filePath: string): Promise<T | null> {
|
|
||||||
try {
|
|
||||||
const raw = await fs.readFile(filePath, "utf8");
|
|
||||||
return JSON.parse(raw) as T;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function writeJsonAtomic(filePath: string, value: unknown) {
|
|
||||||
const dir = path.dirname(filePath);
|
|
||||||
await fs.mkdir(dir, { recursive: true });
|
|
||||||
const tmp = `${filePath}.${randomUUID()}.tmp`;
|
|
||||||
await fs.writeFile(tmp, JSON.stringify(value, null, 2), "utf8");
|
|
||||||
try {
|
|
||||||
await fs.chmod(tmp, 0o600);
|
|
||||||
} catch {
|
|
||||||
// best-effort; ignore on platforms without chmod
|
|
||||||
}
|
|
||||||
await fs.rename(tmp, filePath);
|
|
||||||
try {
|
|
||||||
await fs.chmod(filePath, 0o600);
|
|
||||||
} catch {
|
|
||||||
// best-effort; ignore on platforms without chmod
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function pruneExpiredPending<T extends { ts: number }>(
|
export function pruneExpiredPending<T extends { ts: number }>(
|
||||||
pendingById: Record<string, T>,
|
pendingById: Record<string, T>,
|
||||||
nowMs: number,
|
nowMs: number,
|
||||||
@@ -51,20 +24,3 @@ export function pruneExpiredPending<T extends { ts: number }>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAsyncLock() {
|
|
||||||
let lock: Promise<void> = Promise.resolve();
|
|
||||||
return async function withLock<T>(fn: () => Promise<T>): Promise<T> {
|
|
||||||
const prev = lock;
|
|
||||||
let release: (() => void) | undefined;
|
|
||||||
lock = new Promise<void>((resolve) => {
|
|
||||||
release = resolve;
|
|
||||||
});
|
|
||||||
await prev;
|
|
||||||
try {
|
|
||||||
return await fn();
|
|
||||||
} finally {
|
|
||||||
release?.();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
|
||||||
import fs from "node:fs/promises";
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { resolveStateDir } from "../config/paths.js";
|
import { resolveStateDir } from "../config/paths.js";
|
||||||
|
import { createAsyncLock, readJsonFile, writeJsonAtomic } from "./json-files.js";
|
||||||
|
|
||||||
export type VoiceWakeConfig = {
|
export type VoiceWakeConfig = {
|
||||||
triggers: string[];
|
triggers: string[];
|
||||||
@@ -22,37 +21,7 @@ function sanitizeTriggers(triggers: string[] | undefined | null): string[] {
|
|||||||
return cleaned.length > 0 ? cleaned : DEFAULT_TRIGGERS;
|
return cleaned.length > 0 ? cleaned : DEFAULT_TRIGGERS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readJSON<T>(filePath: string): Promise<T | null> {
|
const withLock = createAsyncLock();
|
||||||
try {
|
|
||||||
const raw = await fs.readFile(filePath, "utf8");
|
|
||||||
return JSON.parse(raw) as T;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeJSONAtomic(filePath: string, value: unknown) {
|
|
||||||
const dir = path.dirname(filePath);
|
|
||||||
await fs.mkdir(dir, { recursive: true });
|
|
||||||
const tmp = `${filePath}.${randomUUID()}.tmp`;
|
|
||||||
await fs.writeFile(tmp, JSON.stringify(value, null, 2), "utf8");
|
|
||||||
await fs.rename(tmp, filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
let lock: Promise<void> = Promise.resolve();
|
|
||||||
async function withLock<T>(fn: () => Promise<T>): Promise<T> {
|
|
||||||
const prev = lock;
|
|
||||||
let release: (() => void) | undefined;
|
|
||||||
lock = new Promise<void>((resolve) => {
|
|
||||||
release = resolve;
|
|
||||||
});
|
|
||||||
await prev;
|
|
||||||
try {
|
|
||||||
return await fn();
|
|
||||||
} finally {
|
|
||||||
release?.();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function defaultVoiceWakeTriggers() {
|
export function defaultVoiceWakeTriggers() {
|
||||||
return [...DEFAULT_TRIGGERS];
|
return [...DEFAULT_TRIGGERS];
|
||||||
@@ -60,7 +29,7 @@ export function defaultVoiceWakeTriggers() {
|
|||||||
|
|
||||||
export async function loadVoiceWakeConfig(baseDir?: string): Promise<VoiceWakeConfig> {
|
export async function loadVoiceWakeConfig(baseDir?: string): Promise<VoiceWakeConfig> {
|
||||||
const filePath = resolvePath(baseDir);
|
const filePath = resolvePath(baseDir);
|
||||||
const existing = await readJSON<VoiceWakeConfig>(filePath);
|
const existing = await readJsonFile<VoiceWakeConfig>(filePath);
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
return { triggers: defaultVoiceWakeTriggers(), updatedAtMs: 0 };
|
return { triggers: defaultVoiceWakeTriggers(), updatedAtMs: 0 };
|
||||||
}
|
}
|
||||||
@@ -84,7 +53,7 @@ export async function setVoiceWakeTriggers(
|
|||||||
triggers: sanitized,
|
triggers: sanitized,
|
||||||
updatedAtMs: Date.now(),
|
updatedAtMs: Date.now(),
|
||||||
};
|
};
|
||||||
await writeJSONAtomic(filePath, next);
|
await writeJsonAtomic(filePath, next);
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user