mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
322 lines
8.9 KiB
JavaScript
322 lines
8.9 KiB
JavaScript
const MODE_ALIASES = Object.freeze({
|
|
1: "strict",
|
|
strict: "strict",
|
|
safe: "strict",
|
|
serial: "strict",
|
|
2: "balanced",
|
|
balance: "balanced",
|
|
balanced: "balanced",
|
|
normal: "balanced",
|
|
3: "fast",
|
|
fast: "fast",
|
|
async: "fast",
|
|
});
|
|
|
|
export const MAINTENANCE_EXECUTION_MODES = Object.freeze([
|
|
"strict",
|
|
"balanced",
|
|
"fast",
|
|
]);
|
|
|
|
function clampInt(value, fallback, min = 1, max = 64) {
|
|
const parsed = Math.floor(Number(value));
|
|
if (!Number.isFinite(parsed)) return fallback;
|
|
return Math.max(min, Math.min(max, parsed));
|
|
}
|
|
|
|
export function normalizeMaintenanceExecutionMode(value = "strict") {
|
|
const normalized = String(value ?? "")
|
|
.trim()
|
|
.toLowerCase();
|
|
return MODE_ALIASES[normalized] || "strict";
|
|
}
|
|
|
|
export function getMaintenanceExecutionModeLevel(value = "strict") {
|
|
switch (normalizeMaintenanceExecutionMode(value)) {
|
|
case "balanced":
|
|
return 2;
|
|
case "fast":
|
|
return 3;
|
|
case "strict":
|
|
default:
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
export function isStrictMaintenanceMode(value = "strict") {
|
|
return normalizeMaintenanceExecutionMode(value) === "strict";
|
|
}
|
|
|
|
export function isFastMaintenanceMode(value = "strict") {
|
|
return normalizeMaintenanceExecutionMode(value) === "fast";
|
|
}
|
|
|
|
export function resolveConcurrencyConfig(settings = {}, modeOverride = null) {
|
|
const mode = normalizeMaintenanceExecutionMode(
|
|
modeOverride ?? settings?.maintenanceExecutionMode,
|
|
);
|
|
const strict = mode === "strict";
|
|
return {
|
|
mode,
|
|
level: getMaintenanceExecutionModeLevel(mode),
|
|
vectorQueryConcurrency: strict
|
|
? 1
|
|
: clampInt(settings?.parallelVectorQueryConcurrency, 3, 1, 12),
|
|
neighborQueryConcurrency: strict
|
|
? 1
|
|
: clampInt(settings?.parallelNeighborQueryConcurrency, 3, 1, 12),
|
|
llmConcurrency: strict
|
|
? 1
|
|
: clampInt(settings?.parallelLlmConcurrency, 2, 1, 4),
|
|
backgroundMaintenanceMaxRetries: clampInt(
|
|
settings?.backgroundMaintenanceMaxRetries,
|
|
2,
|
|
0,
|
|
10,
|
|
),
|
|
backgroundMaintenanceRetryBaseMs: clampInt(
|
|
settings?.backgroundMaintenanceRetryBaseMs,
|
|
800,
|
|
50,
|
|
60000,
|
|
),
|
|
backgroundMaintenanceMaxQueueItems: clampInt(
|
|
settings?.backgroundMaintenanceMaxQueueItems,
|
|
24,
|
|
1,
|
|
256,
|
|
),
|
|
};
|
|
}
|
|
|
|
export function throwIfSignalAborted(signal) {
|
|
if (!signal?.aborted) return;
|
|
const reason = signal.reason;
|
|
if (reason instanceof Error) throw reason;
|
|
throw new DOMException(String(reason || "Operation aborted"), "AbortError");
|
|
}
|
|
|
|
export async function runLimited(
|
|
items = [],
|
|
worker,
|
|
{ concurrency = 1, signal = undefined, preserveOrder = true, failFast = true } = {},
|
|
) {
|
|
const list = Array.isArray(items) ? items : [];
|
|
const limit = clampInt(concurrency, 1, 1, Math.max(1, list.length || 1));
|
|
const results = new Array(list.length);
|
|
let cursor = 0;
|
|
|
|
if (typeof worker !== "function" || list.length === 0) {
|
|
return preserveOrder ? results : [];
|
|
}
|
|
|
|
async function runWorker() {
|
|
while (cursor < list.length) {
|
|
throwIfSignalAborted(signal);
|
|
const index = cursor;
|
|
cursor += 1;
|
|
try {
|
|
results[index] = await worker(list[index], index);
|
|
} catch (error) {
|
|
if (error?.name === "AbortError") throw error;
|
|
if (failFast) throw error;
|
|
results[index] = { error };
|
|
}
|
|
}
|
|
}
|
|
|
|
const workers = Array.from(
|
|
{ length: Math.min(limit, list.length) },
|
|
() => runWorker(),
|
|
);
|
|
await Promise.all(workers);
|
|
return preserveOrder ? results : results.filter((item) => item !== undefined);
|
|
}
|
|
|
|
function delay(ms) {
|
|
const safeMs = Math.max(0, Math.floor(Number(ms) || 0));
|
|
if (safeMs <= 0) return Promise.resolve();
|
|
return new Promise((resolve) => setTimeout(resolve, safeMs));
|
|
}
|
|
|
|
function buildQueueSnapshot(queueState) {
|
|
return {
|
|
state: queueState.active
|
|
? "running"
|
|
: queueState.queue.length > 0
|
|
? "queued"
|
|
: "idle",
|
|
queued: queueState.queue.length,
|
|
activeId: queueState.active?.id || "",
|
|
activeName: queueState.active?.name || "",
|
|
completed: queueState.completed,
|
|
failed: queueState.failed,
|
|
dropped: queueState.dropped,
|
|
lastTask: queueState.lastTask ? { ...queueState.lastTask } : null,
|
|
updatedAt: queueState.updatedAt,
|
|
};
|
|
}
|
|
|
|
export function createBackgroundMaintenanceQueue(options = {}) {
|
|
const queueState = {
|
|
queue: [],
|
|
active: null,
|
|
completed: 0,
|
|
failed: 0,
|
|
dropped: 0,
|
|
lastTask: null,
|
|
updatedAt: Date.now(),
|
|
nextId: 1,
|
|
draining: false,
|
|
maxItems: clampInt(options.maxItems, 24, 1, 256),
|
|
maxRetries: clampInt(options.maxRetries, 2, 0, 10),
|
|
retryBaseMs: clampInt(options.retryBaseMs, 800, 50, 60000),
|
|
onStatus: typeof options.onStatus === "function" ? options.onStatus : null,
|
|
};
|
|
|
|
function emitStatus() {
|
|
queueState.updatedAt = Date.now();
|
|
const snapshot = buildQueueSnapshot(queueState);
|
|
try {
|
|
queueState.onStatus?.(snapshot);
|
|
} catch {
|
|
}
|
|
return snapshot;
|
|
}
|
|
|
|
async function runTask(item) {
|
|
queueState.active = item;
|
|
item.status = "running";
|
|
item.startedAt = Date.now();
|
|
emitStatus();
|
|
while (item.attempts <= item.maxRetries) {
|
|
try {
|
|
item.attempts += 1;
|
|
const result = await item.run({
|
|
attempt: item.attempts,
|
|
maxRetries: item.maxRetries,
|
|
id: item.id,
|
|
name: item.name,
|
|
});
|
|
item.status = "success";
|
|
item.finishedAt = Date.now();
|
|
item.result = result;
|
|
queueState.completed += 1;
|
|
queueState.lastTask = {
|
|
id: item.id,
|
|
name: item.name,
|
|
status: item.status,
|
|
attempts: item.attempts,
|
|
error: "",
|
|
finishedAt: item.finishedAt,
|
|
};
|
|
return result;
|
|
} catch (error) {
|
|
if (error?.name === "AbortError") {
|
|
item.status = "aborted";
|
|
item.error = error?.message || String(error);
|
|
break;
|
|
}
|
|
item.error = error?.message || String(error) || "background-task-failed";
|
|
if (item.attempts > item.maxRetries) {
|
|
item.status = "failed";
|
|
break;
|
|
}
|
|
item.status = "retrying";
|
|
emitStatus();
|
|
await delay(item.retryBaseMs * Math.max(1, item.attempts));
|
|
item.status = "running";
|
|
emitStatus();
|
|
}
|
|
}
|
|
item.finishedAt = Date.now();
|
|
queueState.failed += 1;
|
|
queueState.lastTask = {
|
|
id: item.id,
|
|
name: item.name,
|
|
status: item.status,
|
|
attempts: item.attempts,
|
|
error: item.error || "",
|
|
finishedAt: item.finishedAt,
|
|
};
|
|
return null;
|
|
}
|
|
|
|
async function drain() {
|
|
if (queueState.draining) return;
|
|
queueState.draining = true;
|
|
try {
|
|
while (queueState.queue.length > 0) {
|
|
const item = queueState.queue.shift();
|
|
await runTask(item);
|
|
queueState.active = null;
|
|
emitStatus();
|
|
}
|
|
} finally {
|
|
queueState.active = null;
|
|
queueState.draining = false;
|
|
emitStatus();
|
|
}
|
|
}
|
|
|
|
return {
|
|
configure(nextOptions = {}) {
|
|
queueState.maxItems = clampInt(nextOptions.maxItems, queueState.maxItems, 1, 256);
|
|
queueState.maxRetries = clampInt(nextOptions.maxRetries, queueState.maxRetries, 0, 10);
|
|
queueState.retryBaseMs = clampInt(nextOptions.retryBaseMs, queueState.retryBaseMs, 50, 60000);
|
|
if (typeof nextOptions.onStatus === "function") {
|
|
queueState.onStatus = nextOptions.onStatus;
|
|
}
|
|
return emitStatus();
|
|
},
|
|
enqueue(name, run, taskOptions = {}) {
|
|
if (typeof run !== "function") {
|
|
throw new TypeError("background maintenance task must be a function");
|
|
}
|
|
const occupiedSlots = queueState.queue.length + (queueState.active ? 1 : 0);
|
|
if (occupiedSlots >= queueState.maxItems) {
|
|
queueState.dropped += 1;
|
|
const dropped = {
|
|
id: "",
|
|
name: String(name || "background-task"),
|
|
status: "dropped",
|
|
attempts: 0,
|
|
error: "background-maintenance-queue-full",
|
|
finishedAt: Date.now(),
|
|
};
|
|
queueState.lastTask = dropped;
|
|
emitStatus();
|
|
return {
|
|
queued: false,
|
|
reason: "background-maintenance-queue-full",
|
|
snapshot: buildQueueSnapshot(queueState),
|
|
};
|
|
}
|
|
const item = {
|
|
id: String(taskOptions.id || `bg-${Date.now()}-${queueState.nextId++}`),
|
|
name: String(name || taskOptions.name || "background-task"),
|
|
run,
|
|
attempts: 0,
|
|
maxRetries: clampInt(taskOptions.maxRetries, queueState.maxRetries, 0, 10),
|
|
retryBaseMs: clampInt(taskOptions.retryBaseMs, queueState.retryBaseMs, 50, 60000),
|
|
status: "queued",
|
|
createdAt: Date.now(),
|
|
};
|
|
queueState.queue.push(item);
|
|
const snapshot = emitStatus();
|
|
void drain();
|
|
return {
|
|
queued: true,
|
|
id: item.id,
|
|
snapshot,
|
|
};
|
|
},
|
|
getSnapshot() {
|
|
return buildQueueSnapshot(queueState);
|
|
},
|
|
get size() {
|
|
return queueState.queue.length + (queueState.active ? 1 : 0);
|
|
},
|
|
};
|
|
}
|