mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
feat(authority): add server-primary capability probe
This commit is contained in:
187
index.js
187
index.js
@@ -249,6 +249,16 @@ import {
|
||||
getPersistedSettingsSnapshot,
|
||||
mergePersistedSettings,
|
||||
} from "./runtime/settings-defaults.js";
|
||||
import {
|
||||
createDefaultAuthorityCapabilityState,
|
||||
normalizeAuthorityCapabilityState,
|
||||
probeAuthorityCapabilities,
|
||||
} from "./runtime/authority-capabilities.js";
|
||||
import {
|
||||
createAuthorityBrowserState,
|
||||
getAuthorityBrowserStateSnapshot,
|
||||
normalizeAuthorityBrowserState,
|
||||
} from "./sync/authority-browser-state.js";
|
||||
import { retrieve } from "./retrieval/retriever.js";
|
||||
import {
|
||||
applyProcessedHistorySnapshotToGraph,
|
||||
@@ -1234,6 +1244,9 @@ let lastExtractionStatus = createUiStatus("待命", "尚未执行提取", "idle"
|
||||
let lastVectorStatus = createUiStatus("待命", "尚未执行向量任务", "idle");
|
||||
let lastRecallStatus = createUiStatus("待命", "尚未执行召回", "idle");
|
||||
let graphPersistenceState = createGraphPersistenceState();
|
||||
let authorityCapabilityState = createDefaultAuthorityCapabilityState();
|
||||
let authorityBrowserState = createAuthorityBrowserState();
|
||||
let authorityProbePromise = null;
|
||||
const lastStatusToastAt = {};
|
||||
let pendingRecallSendIntent = createRecallInputRecord();
|
||||
let lastRecallSentUserMessage = createRecallInputRecord();
|
||||
@@ -1437,6 +1450,115 @@ function isLukerPrimaryPersistenceHost(context = getContext()) {
|
||||
return resolvePersistenceHostProfile(context) === "luker";
|
||||
}
|
||||
|
||||
function getAuthorityRuntimeSnapshot(settings = getSettings()) {
|
||||
authorityCapabilityState = normalizeAuthorityCapabilityState(
|
||||
authorityCapabilityState,
|
||||
settings,
|
||||
);
|
||||
authorityBrowserState = normalizeAuthorityBrowserState(
|
||||
authorityBrowserState,
|
||||
settings,
|
||||
);
|
||||
return {
|
||||
capability: authorityCapabilityState,
|
||||
browserState: getAuthorityBrowserStateSnapshot(authorityBrowserState, settings),
|
||||
};
|
||||
}
|
||||
|
||||
function buildAuthorityPersistenceStatePatch(settings = getSettings()) {
|
||||
const { capability, browserState } = getAuthorityRuntimeSnapshot(settings);
|
||||
return {
|
||||
authority: cloneRuntimeDebugValue(capability, null),
|
||||
authorityBrowserState: cloneRuntimeDebugValue(browserState, null),
|
||||
authorityInstalled: Boolean(capability.installed),
|
||||
authorityHealthy: Boolean(capability.healthy),
|
||||
authorityServerPrimaryReady: Boolean(capability.serverPrimaryReady),
|
||||
authorityStoragePrimaryReady: Boolean(capability.storagePrimaryReady),
|
||||
authorityTriviumPrimaryReady: Boolean(capability.triviumPrimaryReady),
|
||||
authorityBrowserCacheMode: String(browserState.mode || "minimal"),
|
||||
authorityOfflineQueueBytes: Number(browserState.offlineQueueBytes || 0),
|
||||
authorityOfflineQueueItems: Number(browserState.offlineQueueItems || 0),
|
||||
authorityDegradedReason: capability.serverPrimaryReady
|
||||
? ""
|
||||
: String(capability.reason || capability.lastError || ""),
|
||||
};
|
||||
}
|
||||
|
||||
async function refreshAuthorityRuntimeState({
|
||||
force = false,
|
||||
source = "authority-refresh",
|
||||
} = {}) {
|
||||
if (authorityProbePromise && !force) {
|
||||
return await authorityProbePromise;
|
||||
}
|
||||
const settings = getSettings();
|
||||
authorityBrowserState = normalizeAuthorityBrowserState(
|
||||
authorityBrowserState,
|
||||
settings,
|
||||
);
|
||||
updateGraphPersistenceState({
|
||||
...buildAuthorityPersistenceStatePatch(settings),
|
||||
authorityLastRefreshSource: String(source || "authority-refresh"),
|
||||
});
|
||||
|
||||
const allowRelativeUrl =
|
||||
typeof window === "object" &&
|
||||
Boolean(window?.location) &&
|
||||
typeof window.location.href === "string";
|
||||
authorityProbePromise = probeAuthorityCapabilities({
|
||||
settings,
|
||||
fetchImpl:
|
||||
typeof globalThis.fetch === "function"
|
||||
? globalThis.fetch.bind(globalThis)
|
||||
: null,
|
||||
headerProvider:
|
||||
typeof getRequestHeaders === "function" ? getRequestHeaders : null,
|
||||
allowRelativeUrl,
|
||||
nowMs: Date.now(),
|
||||
})
|
||||
.then((snapshot) => {
|
||||
authorityCapabilityState = normalizeAuthorityCapabilityState(
|
||||
snapshot,
|
||||
settings,
|
||||
);
|
||||
authorityBrowserState = normalizeAuthorityBrowserState(
|
||||
{
|
||||
...authorityBrowserState,
|
||||
lastProbeAt: authorityCapabilityState.lastProbeAt,
|
||||
lastError: authorityCapabilityState.lastError,
|
||||
},
|
||||
settings,
|
||||
);
|
||||
updateGraphPersistenceState({
|
||||
...buildAuthorityPersistenceStatePatch(settings),
|
||||
authorityLastRefreshSource: String(source || "authority-refresh"),
|
||||
});
|
||||
return authorityCapabilityState;
|
||||
})
|
||||
.catch((error) => {
|
||||
authorityCapabilityState = normalizeAuthorityCapabilityState(
|
||||
{
|
||||
installed: false,
|
||||
healthy: false,
|
||||
reason: "probe-failed",
|
||||
lastError: error?.message || String(error),
|
||||
lastProbeAt: Date.now(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
settings,
|
||||
);
|
||||
updateGraphPersistenceState({
|
||||
...buildAuthorityPersistenceStatePatch(settings),
|
||||
authorityLastRefreshSource: String(source || "authority-refresh"),
|
||||
});
|
||||
return authorityCapabilityState;
|
||||
})
|
||||
.finally(() => {
|
||||
authorityProbePromise = null;
|
||||
});
|
||||
return await authorityProbePromise;
|
||||
}
|
||||
|
||||
function getGraphPersistenceLiveState() {
|
||||
const liveCommitMarker =
|
||||
cloneRuntimeDebugValue(graphPersistenceState.commitMarker, null) ||
|
||||
@@ -1452,6 +1574,7 @@ function getGraphPersistenceLiveState() {
|
||||
adapterRuntime.adapter.hostProfile ||
|
||||
persistenceEnvironment.hostProfile,
|
||||
);
|
||||
const authorityRuntime = getAuthorityRuntimeSnapshot();
|
||||
const primaryStorageTier = normalizePersistenceStorageTier(
|
||||
graphPersistenceState.primaryStorageTier ||
|
||||
persistenceEnvironment.primaryStorageTier,
|
||||
@@ -1515,6 +1638,38 @@ function getGraphPersistenceLiveState() {
|
||||
updatedAt: graphPersistenceState.updatedAt,
|
||||
storagePrimary: graphPersistenceState.storagePrimary || "indexeddb",
|
||||
storageMode: graphPersistenceState.storageMode || "indexeddb",
|
||||
authority: cloneRuntimeDebugValue(authorityRuntime.capability, null),
|
||||
authorityBrowserState: cloneRuntimeDebugValue(
|
||||
authorityRuntime.browserState,
|
||||
null,
|
||||
),
|
||||
authorityInstalled: Boolean(authorityRuntime.capability.installed),
|
||||
authorityHealthy: Boolean(authorityRuntime.capability.healthy),
|
||||
authorityServerPrimaryReady: Boolean(
|
||||
authorityRuntime.capability.serverPrimaryReady,
|
||||
),
|
||||
authorityStoragePrimaryReady: Boolean(
|
||||
authorityRuntime.capability.storagePrimaryReady,
|
||||
),
|
||||
authorityTriviumPrimaryReady: Boolean(
|
||||
authorityRuntime.capability.triviumPrimaryReady,
|
||||
),
|
||||
authorityBrowserCacheMode: String(
|
||||
authorityRuntime.browserState.mode || "minimal",
|
||||
),
|
||||
authorityOfflineQueueBytes: Number(
|
||||
authorityRuntime.browserState.offlineQueueBytes || 0,
|
||||
),
|
||||
authorityOfflineQueueItems: Number(
|
||||
authorityRuntime.browserState.offlineQueueItems || 0,
|
||||
),
|
||||
authorityDegradedReason: authorityRuntime.capability.serverPrimaryReady
|
||||
? ""
|
||||
: String(
|
||||
authorityRuntime.capability.reason ||
|
||||
authorityRuntime.capability.lastError ||
|
||||
"",
|
||||
),
|
||||
resolvedLocalStore: String(
|
||||
graphPersistenceState.resolvedLocalStore ||
|
||||
buildGraphLocalStoreSelectorKey(getPreferredGraphLocalStorePresentationSync()),
|
||||
@@ -13299,6 +13454,27 @@ function updateModuleSettings(patch = {}) {
|
||||
]);
|
||||
const recallUiKeys = new Set(["recallCardUserInputDisplayMode"]);
|
||||
const noticeUiKeys = new Set(["noticeDisplayMode"]);
|
||||
const authorityKeys = new Set([
|
||||
"authorityEnabled",
|
||||
"authorityBaseUrl",
|
||||
"authorityPrimaryWhenAvailable",
|
||||
"authorityStorageMode",
|
||||
"authorityVectorMode",
|
||||
"authoritySqlPrimary",
|
||||
"authorityTriviumPrimary",
|
||||
"authorityGraphQueryEnabled",
|
||||
"authorityJobsEnabled",
|
||||
"authorityBlobCheckpointEnabled",
|
||||
"authorityBrowserCacheMode",
|
||||
"authorityOfflineWritePolicy",
|
||||
"authorityOfflineQueueMaxBytes",
|
||||
"authorityOfflineQueueMaxItems",
|
||||
"authorityOfflineQueueMaxAgeMs",
|
||||
"authorityVectorSyncChunkSize",
|
||||
"authorityVectorFailOpen",
|
||||
"authorityDiagnosticsEnabled",
|
||||
"authorityProbeIntervalMs",
|
||||
]);
|
||||
const settings = getSettings();
|
||||
const previousCloudStorageMode = String(
|
||||
settings.cloudStorageMode || "automatic",
|
||||
@@ -13380,6 +13556,13 @@ function updateModuleSettings(patch = {}) {
|
||||
refreshVisibleStageNotices();
|
||||
}
|
||||
|
||||
if (Object.keys(patch).some((key) => authorityKeys.has(key))) {
|
||||
void refreshAuthorityRuntimeState({
|
||||
force: true,
|
||||
source: "settings-updated",
|
||||
});
|
||||
}
|
||||
|
||||
const currentGraphLocalStorageMode = getRequestedGraphLocalStorageMode(
|
||||
settings,
|
||||
);
|
||||
@@ -20007,6 +20190,10 @@ async function onCompactLukerSidecar() {
|
||||
|
||||
(async function init() {
|
||||
await loadServerSettings();
|
||||
void refreshAuthorityRuntimeState({
|
||||
force: true,
|
||||
source: "init",
|
||||
});
|
||||
const { target, lightweightHostMode, adapter } = syncBmeHostRuntimeFlags(getContext());
|
||||
updateGraphPersistenceState({
|
||||
hostProfile: adapter.hostProfile,
|
||||
|
||||
367
runtime/authority-capabilities.js
Normal file
367
runtime/authority-capabilities.js
Normal file
@@ -0,0 +1,367 @@
|
||||
const DEFAULT_AUTHORITY_BASE_URL = "/api/plugins/authority";
|
||||
const DEFAULT_AUTHORITY_PROBE_INTERVAL_MS = 60000;
|
||||
|
||||
const SQL_FEATURES = ["sql", "sql.query", "sql.page", "sql.pageall", "querysql"];
|
||||
const SQL_MUTATION_FEATURES = ["sql", "sql.mutation", "sql.execute", "sql.write", "sql.transaction"];
|
||||
const TRIVIUM_FEATURES = ["trivium", "trivium.search", "trivium.query", "trivium.filterwhere", "trivium.bulkupsert"];
|
||||
const JOB_FEATURES = ["jobs", "jobs.list", "jobs.wait", "events", "sse"];
|
||||
const BLOB_FEATURES = ["blob", "blob.write", "privatefiles", "private.files", "files.private"];
|
||||
|
||||
function toBoolean(value, fallback = false) {
|
||||
if (typeof value === "boolean") return value;
|
||||
if (typeof value === "number") return value !== 0;
|
||||
if (typeof value === "string") {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (["true", "1", "yes", "on"].includes(normalized)) return true;
|
||||
if (["false", "0", "no", "off"].includes(normalized)) return false;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function clampInteger(value, fallback, min, max) {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) return fallback;
|
||||
return Math.min(max, Math.max(min, Math.trunc(numeric)));
|
||||
}
|
||||
|
||||
function normalizeMode(value, fallback, allowed) {
|
||||
const normalized = String(value ?? fallback).trim().toLowerCase();
|
||||
return allowed.includes(normalized) ? normalized : fallback;
|
||||
}
|
||||
|
||||
function normalizeFeatureName(value) {
|
||||
return String(value ?? "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
function addFeature(features, value) {
|
||||
const normalized = normalizeFeatureName(value);
|
||||
if (normalized) features.add(normalized);
|
||||
}
|
||||
|
||||
function addFeatureObject(features, value, prefix = "") {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
||||
for (const [key, enabled] of Object.entries(value)) {
|
||||
if (!enabled) continue;
|
||||
addFeature(features, key);
|
||||
if (prefix) addFeature(features, `${prefix}.${key}`);
|
||||
if (enabled && typeof enabled === "object" && !Array.isArray(enabled)) {
|
||||
addFeatureObject(features, enabled, prefix ? `${prefix}.${key}` : key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasAnyFeature(features, aliases) {
|
||||
return aliases.some((alias) => features.has(normalizeFeatureName(alias)));
|
||||
}
|
||||
|
||||
function createFeatureReadiness(features) {
|
||||
return {
|
||||
sql: hasAnyFeature(features, SQL_FEATURES),
|
||||
sqlMutation: hasAnyFeature(features, SQL_MUTATION_FEATURES),
|
||||
trivium: hasAnyFeature(features, TRIVIUM_FEATURES),
|
||||
jobs: hasAnyFeature(features, JOB_FEATURES),
|
||||
blob: hasAnyFeature(features, BLOB_FEATURES),
|
||||
};
|
||||
}
|
||||
|
||||
function collectMissingFeatures(readiness) {
|
||||
const missing = [];
|
||||
if (!readiness.sql) missing.push("sql.query");
|
||||
if (!readiness.sqlMutation) missing.push("sql.mutation");
|
||||
if (!readiness.trivium) missing.push("trivium.search");
|
||||
if (!readiness.jobs) missing.push("jobs");
|
||||
if (!readiness.blob) missing.push("blob-or-private-files");
|
||||
return missing;
|
||||
}
|
||||
|
||||
function isRelativeAuthorityUrl(baseUrl) {
|
||||
return /^\//.test(String(baseUrl || ""));
|
||||
}
|
||||
|
||||
function normalizeLatencyMs(startedAt, finishedAt) {
|
||||
return Math.max(0, Math.round((Number(finishedAt) - Number(startedAt)) * 10) / 10);
|
||||
}
|
||||
|
||||
function readNowMs() {
|
||||
if (typeof performance === "object" && typeof performance.now === "function") {
|
||||
return performance.now();
|
||||
}
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
export function normalizeAuthorityBaseUrl(baseUrl = DEFAULT_AUTHORITY_BASE_URL) {
|
||||
const normalized = String(baseUrl || DEFAULT_AUTHORITY_BASE_URL).trim() || DEFAULT_AUTHORITY_BASE_URL;
|
||||
return normalized.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
export function normalizeAuthoritySettings(settings = {}) {
|
||||
const source = settings && typeof settings === "object" && !Array.isArray(settings) ? settings : {};
|
||||
const enabledMode = normalizeMode(source.authorityEnabled ?? source.enabledMode, "auto", ["auto", "on", "off", "true", "false"]);
|
||||
return {
|
||||
enabledMode: enabledMode === "true" ? "on" : enabledMode === "false" ? "off" : enabledMode,
|
||||
enabled: enabledMode !== "off" && enabledMode !== "false",
|
||||
baseUrl: normalizeAuthorityBaseUrl(source.authorityBaseUrl ?? source.baseUrl),
|
||||
primaryWhenAvailable: toBoolean(source.authorityPrimaryWhenAvailable ?? source.primaryWhenAvailable, true),
|
||||
storageMode: normalizeMode(source.authorityStorageMode ?? source.storageMode, "auto-server-primary", ["auto-server-primary", "server-primary", "local-primary", "off"]),
|
||||
vectorMode: normalizeMode(source.authorityVectorMode ?? source.vectorMode, "auto-primary", ["auto-primary", "primary", "local-fallback", "off"]),
|
||||
sqlPrimary: toBoolean(source.authoritySqlPrimary ?? source.sqlPrimary, true),
|
||||
triviumPrimary: toBoolean(source.authorityTriviumPrimary ?? source.triviumPrimary, true),
|
||||
jobsEnabled: toBoolean(source.authorityJobsEnabled ?? source.jobsEnabled, true),
|
||||
blobCheckpointEnabled: toBoolean(source.authorityBlobCheckpointEnabled ?? source.blobCheckpointEnabled, true),
|
||||
diagnosticsEnabled: toBoolean(source.authorityDiagnosticsEnabled ?? source.diagnosticsEnabled, true),
|
||||
failOpen: toBoolean(source.authorityFailOpen ?? source.failOpen, true),
|
||||
probeIntervalMs: clampInteger(source.authorityProbeIntervalMs ?? source.probeIntervalMs, DEFAULT_AUTHORITY_PROBE_INTERVAL_MS, 1000, 3600000),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildAuthorityProbeUrls(baseUrl = DEFAULT_AUTHORITY_BASE_URL) {
|
||||
const normalizedBaseUrl = normalizeAuthorityBaseUrl(baseUrl);
|
||||
return [
|
||||
`${normalizedBaseUrl}/v1/diagnostics/probe`,
|
||||
`${normalizedBaseUrl}/v1/probe`,
|
||||
`${normalizedBaseUrl}/probe`,
|
||||
normalizedBaseUrl,
|
||||
];
|
||||
}
|
||||
|
||||
export function collectAuthorityFeatures(payload = {}) {
|
||||
const features = new Set();
|
||||
const source = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
||||
for (const value of Array.isArray(source.features) ? source.features : []) {
|
||||
addFeature(features, value);
|
||||
}
|
||||
for (const value of Array.isArray(source.capabilities) ? source.capabilities : []) {
|
||||
addFeature(features, value);
|
||||
}
|
||||
addFeatureObject(features, source.features);
|
||||
addFeatureObject(features, source.capabilities);
|
||||
addFeatureObject(features, source.services);
|
||||
addFeatureObject(features, source.featureFlags);
|
||||
addFeatureObject(features, source.flags);
|
||||
return features;
|
||||
}
|
||||
|
||||
export function createDefaultAuthorityCapabilityState(overrides = {}) {
|
||||
return {
|
||||
enabledMode: "auto",
|
||||
baseUrl: DEFAULT_AUTHORITY_BASE_URL,
|
||||
installed: false,
|
||||
healthy: false,
|
||||
sessionReady: false,
|
||||
permissionReady: false,
|
||||
minimumFeatureSetReady: false,
|
||||
serverPrimaryReady: false,
|
||||
storagePrimaryReady: false,
|
||||
triviumPrimaryReady: false,
|
||||
jobsReady: false,
|
||||
blobReady: false,
|
||||
features: [],
|
||||
missingFeatures: ["sql.query", "sql.mutation", "trivium.search", "jobs", "blob-or-private-files"],
|
||||
reason: "not-probed",
|
||||
lastError: "",
|
||||
endpoint: "",
|
||||
status: 0,
|
||||
latencyMs: 0,
|
||||
lastProbeAt: 0,
|
||||
updatedAt: "",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeAuthorityCapabilityState(input = {}, settings = {}) {
|
||||
const normalizedSettings = normalizeAuthoritySettings(settings);
|
||||
const source = input && typeof input === "object" && !Array.isArray(input) ? input : {};
|
||||
const features = new Set((Array.isArray(source.features) ? source.features : []).map(normalizeFeatureName).filter(Boolean));
|
||||
const readiness = createFeatureReadiness(features);
|
||||
const missingFeatures = Array.isArray(source.missingFeatures) && source.missingFeatures.length
|
||||
? source.missingFeatures.map(String)
|
||||
: collectMissingFeatures(readiness);
|
||||
const healthy = Boolean(source.healthy);
|
||||
const sessionReady = source.sessionReady == null ? healthy : Boolean(source.sessionReady);
|
||||
const permissionReady = source.permissionReady == null ? sessionReady : Boolean(source.permissionReady);
|
||||
const storagePrimaryReady = healthy && sessionReady && permissionReady && readiness.sql && readiness.sqlMutation;
|
||||
const triviumPrimaryReady = healthy && sessionReady && permissionReady && readiness.trivium;
|
||||
const jobsReady = healthy && readiness.jobs;
|
||||
const blobReady = healthy && readiness.blob;
|
||||
const minimumFeatureSetReady = storagePrimaryReady && triviumPrimaryReady && jobsReady && blobReady;
|
||||
const serverPrimaryRequested =
|
||||
normalizedSettings.enabled &&
|
||||
normalizedSettings.primaryWhenAvailable &&
|
||||
normalizedSettings.storageMode !== "local-primary" &&
|
||||
normalizedSettings.storageMode !== "off";
|
||||
return createDefaultAuthorityCapabilityState({
|
||||
...source,
|
||||
enabledMode: normalizedSettings.enabledMode,
|
||||
baseUrl: normalizedSettings.baseUrl,
|
||||
installed: Boolean(source.installed),
|
||||
healthy,
|
||||
sessionReady,
|
||||
permissionReady,
|
||||
minimumFeatureSetReady,
|
||||
serverPrimaryReady: serverPrimaryRequested && minimumFeatureSetReady,
|
||||
storagePrimaryReady,
|
||||
triviumPrimaryReady,
|
||||
jobsReady,
|
||||
blobReady,
|
||||
features: Array.from(features).sort(),
|
||||
missingFeatures,
|
||||
reason: String(source.reason || (healthy ? "ok" : "not-ready")),
|
||||
lastError: String(source.lastError || ""),
|
||||
endpoint: String(source.endpoint || ""),
|
||||
status: clampInteger(source.status, 0, 0, 999),
|
||||
latencyMs: Math.max(0, Number(source.latencyMs) || 0),
|
||||
lastProbeAt: Math.max(0, Number(source.lastProbeAt) || 0),
|
||||
updatedAt: String(source.updatedAt || ""),
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeAuthorityProbeResponse(payload = {}, context = {}) {
|
||||
const settings = normalizeAuthoritySettings(context.settings || {});
|
||||
const features = collectAuthorityFeatures(payload);
|
||||
const readiness = createFeatureReadiness(features);
|
||||
const missingFeatures = collectMissingFeatures(readiness);
|
||||
const sessionReady = payload?.sessionReady ?? payload?.session?.ready ?? payload?.session?.active ?? true;
|
||||
const permissionReady = payload?.permissionReady ?? payload?.permissions?.ready ?? payload?.authorized ?? sessionReady;
|
||||
const healthy = payload?.healthy ?? payload?.ok ?? true;
|
||||
return normalizeAuthorityCapabilityState(
|
||||
{
|
||||
installed: true,
|
||||
healthy: Boolean(healthy),
|
||||
sessionReady: Boolean(sessionReady),
|
||||
permissionReady: Boolean(permissionReady),
|
||||
features: Array.from(features),
|
||||
missingFeatures,
|
||||
reason: missingFeatures.length ? "missing-required-features" : "ok",
|
||||
endpoint: context.endpoint || "",
|
||||
status: context.status || 200,
|
||||
latencyMs: context.latencyMs || 0,
|
||||
lastProbeAt: context.nowMs || Date.now(),
|
||||
updatedAt: new Date(context.nowMs || Date.now()).toISOString(),
|
||||
},
|
||||
settings,
|
||||
);
|
||||
}
|
||||
|
||||
export async function probeAuthorityCapabilities(options = {}) {
|
||||
const settings = normalizeAuthoritySettings(options.settings || {});
|
||||
const nowMs = Number(options.nowMs) || Date.now();
|
||||
if (!settings.enabled || settings.storageMode === "off") {
|
||||
return normalizeAuthorityCapabilityState(
|
||||
{
|
||||
reason: "disabled",
|
||||
lastProbeAt: nowMs,
|
||||
updatedAt: new Date(nowMs).toISOString(),
|
||||
},
|
||||
settings,
|
||||
);
|
||||
}
|
||||
|
||||
const fetchImpl = options.fetchImpl || (typeof fetch === "function" ? fetch : null);
|
||||
if (typeof fetchImpl !== "function") {
|
||||
return normalizeAuthorityCapabilityState(
|
||||
{
|
||||
reason: "fetch-unavailable",
|
||||
lastError: "fetch unavailable",
|
||||
lastProbeAt: nowMs,
|
||||
updatedAt: new Date(nowMs).toISOString(),
|
||||
},
|
||||
settings,
|
||||
);
|
||||
}
|
||||
|
||||
if (options.allowRelativeUrl === false && isRelativeAuthorityUrl(settings.baseUrl)) {
|
||||
return normalizeAuthorityCapabilityState(
|
||||
{
|
||||
reason: "relative-url-unavailable",
|
||||
lastError: "relative Authority URL cannot be probed in this runtime",
|
||||
lastProbeAt: nowMs,
|
||||
updatedAt: new Date(nowMs).toISOString(),
|
||||
},
|
||||
settings,
|
||||
);
|
||||
}
|
||||
|
||||
let headers = { Accept: "application/json" };
|
||||
if (typeof options.headerProvider === "function") {
|
||||
try {
|
||||
headers = { ...headers, ...(options.headerProvider() || {}) };
|
||||
} catch {
|
||||
headers = { ...headers };
|
||||
}
|
||||
}
|
||||
|
||||
let lastError = "";
|
||||
let lastStatus = 0;
|
||||
for (const endpoint of buildAuthorityProbeUrls(settings.baseUrl)) {
|
||||
const startedAt = readNowMs();
|
||||
try {
|
||||
const response = await fetchImpl(endpoint, { method: "GET", headers });
|
||||
const finishedAt = readNowMs();
|
||||
const status = Number(response?.status || 0);
|
||||
lastStatus = status;
|
||||
if (status === 404) continue;
|
||||
if (status === 401 || status === 403) {
|
||||
return normalizeAuthorityCapabilityState(
|
||||
{
|
||||
installed: true,
|
||||
healthy: false,
|
||||
sessionReady: false,
|
||||
permissionReady: false,
|
||||
reason: "permission-denied",
|
||||
lastError: `HTTP ${status}`,
|
||||
endpoint,
|
||||
status,
|
||||
latencyMs: normalizeLatencyMs(startedAt, finishedAt),
|
||||
lastProbeAt: nowMs,
|
||||
updatedAt: new Date(nowMs).toISOString(),
|
||||
},
|
||||
settings,
|
||||
);
|
||||
}
|
||||
if (!response?.ok) {
|
||||
return normalizeAuthorityCapabilityState(
|
||||
{
|
||||
installed: status > 0,
|
||||
healthy: false,
|
||||
reason: "http-error",
|
||||
lastError: `HTTP ${status || "unknown"}`,
|
||||
endpoint,
|
||||
status,
|
||||
latencyMs: normalizeLatencyMs(startedAt, finishedAt),
|
||||
lastProbeAt: nowMs,
|
||||
updatedAt: new Date(nowMs).toISOString(),
|
||||
},
|
||||
settings,
|
||||
);
|
||||
}
|
||||
let payload = {};
|
||||
try {
|
||||
payload = typeof response.json === "function" ? await response.json() : {};
|
||||
} catch {
|
||||
payload = {};
|
||||
}
|
||||
return normalizeAuthorityProbeResponse(payload, {
|
||||
settings,
|
||||
endpoint,
|
||||
status,
|
||||
latencyMs: normalizeLatencyMs(startedAt, finishedAt),
|
||||
nowMs,
|
||||
});
|
||||
} catch (error) {
|
||||
lastError = error?.message || String(error);
|
||||
}
|
||||
}
|
||||
|
||||
return normalizeAuthorityCapabilityState(
|
||||
{
|
||||
installed: false,
|
||||
healthy: false,
|
||||
reason: lastStatus === 404 ? "not-installed" : "probe-failed",
|
||||
lastError,
|
||||
status: lastStatus,
|
||||
lastProbeAt: nowMs,
|
||||
updatedAt: new Date(nowMs).toISOString(),
|
||||
},
|
||||
settings,
|
||||
);
|
||||
}
|
||||
@@ -117,6 +117,26 @@ export const defaultSettings = {
|
||||
embeddingBackendApiUrl: "",
|
||||
embeddingAutoSuffix: true,
|
||||
|
||||
authorityEnabled: "auto",
|
||||
authorityBaseUrl: "/api/plugins/authority",
|
||||
authorityPrimaryWhenAvailable: true,
|
||||
authorityStorageMode: "auto-server-primary",
|
||||
authorityVectorMode: "auto-primary",
|
||||
authoritySqlPrimary: true,
|
||||
authorityTriviumPrimary: true,
|
||||
authorityGraphQueryEnabled: true,
|
||||
authorityJobsEnabled: true,
|
||||
authorityBlobCheckpointEnabled: true,
|
||||
authorityBrowserCacheMode: "minimal",
|
||||
authorityOfflineWritePolicy: "queue-local-dirty",
|
||||
authorityOfflineQueueMaxBytes: 1048576,
|
||||
authorityOfflineQueueMaxItems: 128,
|
||||
authorityOfflineQueueMaxAgeMs: 3600000,
|
||||
authorityVectorSyncChunkSize: 1000,
|
||||
authorityVectorFailOpen: true,
|
||||
authorityDiagnosticsEnabled: true,
|
||||
authorityProbeIntervalMs: 60000,
|
||||
|
||||
// Native 性能加速(灰度)
|
||||
graphUseNativeLayout: true,
|
||||
graphNativeLayoutThresholdNodes: 280,
|
||||
|
||||
205
sync/authority-browser-state.js
Normal file
205
sync/authority-browser-state.js
Normal file
@@ -0,0 +1,205 @@
|
||||
const DEFAULT_MAX_BYTES = 1024 * 1024;
|
||||
const DEFAULT_MAX_ITEMS = 128;
|
||||
const DEFAULT_MAX_AGE_MS = 60 * 60 * 1000;
|
||||
|
||||
function clampInteger(value, fallback, min, max) {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) return fallback;
|
||||
return Math.min(max, Math.max(min, Math.trunc(numeric)));
|
||||
}
|
||||
|
||||
function normalizeMode(value = "minimal") {
|
||||
const normalized = String(value || "minimal").trim().toLowerCase();
|
||||
return ["minimal", "off", "emergency-snapshot"].includes(normalized)
|
||||
? normalized
|
||||
: "minimal";
|
||||
}
|
||||
|
||||
function estimateJsonBytes(value) {
|
||||
let serialized = "";
|
||||
try {
|
||||
serialized = JSON.stringify(value ?? null) || "null";
|
||||
} catch {
|
||||
serialized = String(value ?? "");
|
||||
}
|
||||
if (typeof TextEncoder === "function") {
|
||||
return new TextEncoder().encode(serialized).byteLength;
|
||||
}
|
||||
return serialized.length * 2;
|
||||
}
|
||||
|
||||
function normalizeQueueItem(item = {}, nowMs = Date.now()) {
|
||||
const source = item && typeof item === "object" && !Array.isArray(item) ? item : {};
|
||||
const payload = source.payload && typeof source.payload === "object" ? source.payload : source;
|
||||
const rawCreatedAt = Number(source.createdAt ?? nowMs);
|
||||
const createdAt = Number.isFinite(rawCreatedAt)
|
||||
? Math.max(0, rawCreatedAt)
|
||||
: nowMs;
|
||||
const rawBytes = Number(source.bytes ?? estimateJsonBytes(payload));
|
||||
const bytes = Number.isFinite(rawBytes) ? Math.max(0, rawBytes) : 0;
|
||||
return {
|
||||
id: String(source.id || `offline-${createdAt}-${Math.random().toString(36).slice(2, 8)}`),
|
||||
kind: String(source.kind || "mutation"),
|
||||
reason: String(source.reason || "authority-offline"),
|
||||
createdAt,
|
||||
bytes,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
function pruneQueue(queue = [], policy = {}, nowMs = Date.now()) {
|
||||
const maxAgeMs = Math.max(0, Number(policy.maxAgeMs || DEFAULT_MAX_AGE_MS) || 0);
|
||||
if (!Array.isArray(queue)) return [];
|
||||
return queue
|
||||
.map((item) => normalizeQueueItem(item, nowMs))
|
||||
.filter((item) => maxAgeMs <= 0 || nowMs - item.createdAt <= maxAgeMs)
|
||||
.sort((left, right) => left.createdAt - right.createdAt);
|
||||
}
|
||||
|
||||
function summarizeQueue(queue = []) {
|
||||
const items = Array.isArray(queue) ? queue : [];
|
||||
const bytes = items.reduce((sum, item) => sum + Math.max(0, Number(item?.bytes || 0) || 0), 0);
|
||||
return {
|
||||
items: items.length,
|
||||
bytes,
|
||||
};
|
||||
}
|
||||
|
||||
export function getAuthorityBrowserStoragePolicy(settings = {}) {
|
||||
const source = settings && typeof settings === "object" && !Array.isArray(settings) ? settings : {};
|
||||
return {
|
||||
mode: normalizeMode(source.authorityBrowserCacheMode),
|
||||
offlineWritePolicy: String(source.authorityOfflineWritePolicy || "queue-local-dirty"),
|
||||
maxBytes: clampInteger(source.authorityOfflineQueueMaxBytes, DEFAULT_MAX_BYTES, 0, 64 * 1024 * 1024),
|
||||
maxItems: clampInteger(source.authorityOfflineQueueMaxItems, DEFAULT_MAX_ITEMS, 0, 100000),
|
||||
maxAgeMs: clampInteger(source.authorityOfflineQueueMaxAgeMs, DEFAULT_MAX_AGE_MS, 0, 30 * 24 * 60 * 60 * 1000),
|
||||
};
|
||||
}
|
||||
|
||||
export function createAuthorityBrowserState(overrides = {}) {
|
||||
return {
|
||||
mode: "minimal",
|
||||
serverRevision: 0,
|
||||
serverIntegrity: "",
|
||||
lastProbeAt: 0,
|
||||
lastCommitAt: 0,
|
||||
lastError: "",
|
||||
offlineQueue: [],
|
||||
offlineQueueBytes: 0,
|
||||
offlineQueueItems: 0,
|
||||
offlineQueueOverflow: false,
|
||||
offlineQueueOverflowReason: "",
|
||||
updatedAt: "",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeAuthorityBrowserState(input = {}, settings = {}, nowMs = Date.now()) {
|
||||
const policy = getAuthorityBrowserStoragePolicy(settings);
|
||||
const source = input && typeof input === "object" && !Array.isArray(input) ? input : {};
|
||||
const queue = pruneQueue(source.offlineQueue, policy, nowMs);
|
||||
const summary = summarizeQueue(queue);
|
||||
return createAuthorityBrowserState({
|
||||
mode: policy.mode,
|
||||
serverRevision: Math.max(0, Number(source.serverRevision || 0) || 0),
|
||||
serverIntegrity: String(source.serverIntegrity || ""),
|
||||
lastProbeAt: Math.max(0, Number(source.lastProbeAt || 0) || 0),
|
||||
lastCommitAt: Math.max(0, Number(source.lastCommitAt || 0) || 0),
|
||||
lastError: String(source.lastError || ""),
|
||||
offlineQueue: queue,
|
||||
offlineQueueBytes: summary.bytes,
|
||||
offlineQueueItems: summary.items,
|
||||
offlineQueueOverflow: Boolean(source.offlineQueueOverflow),
|
||||
offlineQueueOverflowReason: String(source.offlineQueueOverflowReason || ""),
|
||||
updatedAt: String(source.updatedAt || ""),
|
||||
});
|
||||
}
|
||||
|
||||
export function recordAuthorityAcceptedRevision(state = {}, accepted = {}, settings = {}, nowMs = Date.now()) {
|
||||
const current = normalizeAuthorityBrowserState(state, settings, nowMs);
|
||||
return createAuthorityBrowserState({
|
||||
...current,
|
||||
serverRevision: Math.max(current.serverRevision, Number(accepted.revision || 0) || 0),
|
||||
serverIntegrity: String(accepted.integrity || current.serverIntegrity || ""),
|
||||
lastCommitAt: Math.max(0, Number(accepted.committedAt || nowMs) || nowMs),
|
||||
lastError: "",
|
||||
updatedAt: new Date(nowMs).toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
export function enqueueAuthorityOfflineMutation(state = {}, mutation = {}, settings = {}, nowMs = Date.now()) {
|
||||
const policy = getAuthorityBrowserStoragePolicy(settings);
|
||||
const current = normalizeAuthorityBrowserState(state, settings, nowMs);
|
||||
if (policy.mode === "off" || policy.offlineWritePolicy === "off") {
|
||||
const nextState = createAuthorityBrowserState({
|
||||
...current,
|
||||
offlineQueueOverflow: true,
|
||||
offlineQueueOverflowReason: "offline-queue-disabled",
|
||||
updatedAt: new Date(nowMs).toISOString(),
|
||||
});
|
||||
return { accepted: false, reason: "offline-queue-disabled", state: nextState };
|
||||
}
|
||||
|
||||
const item = normalizeQueueItem(mutation, nowMs);
|
||||
const nextItems = [...current.offlineQueue, item];
|
||||
const nextSummary = summarizeQueue(nextItems);
|
||||
if (policy.maxItems > 0 && nextSummary.items > policy.maxItems) {
|
||||
const nextState = createAuthorityBrowserState({
|
||||
...current,
|
||||
offlineQueueOverflow: true,
|
||||
offlineQueueOverflowReason: "max-items-exceeded",
|
||||
updatedAt: new Date(nowMs).toISOString(),
|
||||
});
|
||||
return { accepted: false, reason: "max-items-exceeded", state: nextState };
|
||||
}
|
||||
if (policy.maxBytes > 0 && nextSummary.bytes > policy.maxBytes) {
|
||||
const nextState = createAuthorityBrowserState({
|
||||
...current,
|
||||
offlineQueueOverflow: true,
|
||||
offlineQueueOverflowReason: "max-bytes-exceeded",
|
||||
updatedAt: new Date(nowMs).toISOString(),
|
||||
});
|
||||
return { accepted: false, reason: "max-bytes-exceeded", state: nextState };
|
||||
}
|
||||
|
||||
const nextState = createAuthorityBrowserState({
|
||||
...current,
|
||||
offlineQueue: nextItems,
|
||||
offlineQueueBytes: nextSummary.bytes,
|
||||
offlineQueueItems: nextSummary.items,
|
||||
offlineQueueOverflow: false,
|
||||
offlineQueueOverflowReason: "",
|
||||
updatedAt: new Date(nowMs).toISOString(),
|
||||
});
|
||||
return { accepted: true, reason: "queued", item, state: nextState };
|
||||
}
|
||||
|
||||
export function clearAuthorityOfflineQueue(state = {}, settings = {}, nowMs = Date.now()) {
|
||||
const current = normalizeAuthorityBrowserState(state, settings, nowMs);
|
||||
return createAuthorityBrowserState({
|
||||
...current,
|
||||
offlineQueue: [],
|
||||
offlineQueueBytes: 0,
|
||||
offlineQueueItems: 0,
|
||||
offlineQueueOverflow: false,
|
||||
offlineQueueOverflowReason: "",
|
||||
updatedAt: new Date(nowMs).toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
export function getAuthorityBrowserStateSnapshot(state = {}, settings = {}, nowMs = Date.now()) {
|
||||
const normalized = normalizeAuthorityBrowserState(state, settings, nowMs);
|
||||
return {
|
||||
mode: normalized.mode,
|
||||
serverRevision: normalized.serverRevision,
|
||||
serverIntegrity: normalized.serverIntegrity,
|
||||
lastProbeAt: normalized.lastProbeAt,
|
||||
lastCommitAt: normalized.lastCommitAt,
|
||||
lastError: normalized.lastError,
|
||||
offlineQueueBytes: normalized.offlineQueueBytes,
|
||||
offlineQueueItems: normalized.offlineQueueItems,
|
||||
offlineQueueOverflow: normalized.offlineQueueOverflow,
|
||||
offlineQueueOverflowReason: normalized.offlineQueueOverflowReason,
|
||||
updatedAt: normalized.updatedAt,
|
||||
};
|
||||
}
|
||||
142
tests/authority-browser-state.mjs
Normal file
142
tests/authority-browser-state.mjs
Normal file
@@ -0,0 +1,142 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
clearAuthorityOfflineQueue,
|
||||
enqueueAuthorityOfflineMutation,
|
||||
getAuthorityBrowserStateSnapshot,
|
||||
getAuthorityBrowserStoragePolicy,
|
||||
normalizeAuthorityBrowserState,
|
||||
recordAuthorityAcceptedRevision,
|
||||
} from "../sync/authority-browser-state.js";
|
||||
import { defaultSettings } from "../runtime/settings-defaults.js";
|
||||
|
||||
const policy = getAuthorityBrowserStoragePolicy(defaultSettings);
|
||||
assert.equal(policy.mode, "minimal");
|
||||
assert.equal(policy.offlineWritePolicy, "queue-local-dirty");
|
||||
assert.equal(policy.maxBytes, 1048576);
|
||||
assert.equal(policy.maxItems, 128);
|
||||
assert.equal(policy.maxAgeMs, 3600000);
|
||||
|
||||
const normalized = normalizeAuthorityBrowserState(
|
||||
{
|
||||
serverRevision: 7,
|
||||
serverIntegrity: "abc",
|
||||
offlineQueue: [
|
||||
{
|
||||
id: "old",
|
||||
createdAt: 0,
|
||||
bytes: 10,
|
||||
payload: { a: 1 },
|
||||
},
|
||||
{
|
||||
id: "fresh",
|
||||
createdAt: 9000,
|
||||
bytes: 20,
|
||||
payload: { b: 2 },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...defaultSettings,
|
||||
authorityOfflineQueueMaxAgeMs: 1000,
|
||||
},
|
||||
10000,
|
||||
);
|
||||
assert.equal(normalized.serverRevision, 7);
|
||||
assert.equal(normalized.serverIntegrity, "abc");
|
||||
assert.equal(normalized.offlineQueueItems, 1);
|
||||
assert.equal(normalized.offlineQueueBytes, 20);
|
||||
assert.equal(normalized.offlineQueue[0].id, "fresh");
|
||||
|
||||
const acceptedRevision = recordAuthorityAcceptedRevision(
|
||||
normalized,
|
||||
{
|
||||
revision: 11,
|
||||
integrity: "server-integrity",
|
||||
},
|
||||
defaultSettings,
|
||||
12000,
|
||||
);
|
||||
assert.equal(acceptedRevision.serverRevision, 11);
|
||||
assert.equal(acceptedRevision.serverIntegrity, "server-integrity");
|
||||
assert.equal(acceptedRevision.lastCommitAt, 12000);
|
||||
assert.equal(acceptedRevision.offlineQueueItems, 1);
|
||||
|
||||
const enqueueResult = enqueueAuthorityOfflineMutation(
|
||||
acceptedRevision,
|
||||
{
|
||||
id: "mutation-1",
|
||||
kind: "commitDelta",
|
||||
payload: { upsertNodes: [{ id: "n1" }] },
|
||||
},
|
||||
{
|
||||
...defaultSettings,
|
||||
authorityOfflineQueueMaxItems: 3,
|
||||
},
|
||||
13000,
|
||||
);
|
||||
assert.equal(enqueueResult.accepted, true);
|
||||
assert.equal(enqueueResult.state.offlineQueueItems, 2);
|
||||
assert.equal(enqueueResult.state.offlineQueueOverflow, false);
|
||||
|
||||
const itemOverflow = enqueueAuthorityOfflineMutation(
|
||||
enqueueResult.state,
|
||||
{
|
||||
id: "mutation-overflow",
|
||||
payload: { upsertNodes: [{ id: "n2" }] },
|
||||
},
|
||||
{
|
||||
...defaultSettings,
|
||||
authorityOfflineQueueMaxItems: 1,
|
||||
},
|
||||
14000,
|
||||
);
|
||||
assert.equal(itemOverflow.accepted, false);
|
||||
assert.equal(itemOverflow.reason, "max-items-exceeded");
|
||||
assert.equal(itemOverflow.state.offlineQueueItems, 2);
|
||||
assert.equal(itemOverflow.state.offlineQueueOverflow, true);
|
||||
|
||||
const byteOverflow = enqueueAuthorityOfflineMutation(
|
||||
{},
|
||||
{
|
||||
id: "large-mutation",
|
||||
payload: { text: "x".repeat(64) },
|
||||
},
|
||||
{
|
||||
...defaultSettings,
|
||||
authorityOfflineQueueMaxBytes: 8,
|
||||
},
|
||||
15000,
|
||||
);
|
||||
assert.equal(byteOverflow.accepted, false);
|
||||
assert.equal(byteOverflow.reason, "max-bytes-exceeded");
|
||||
assert.equal(byteOverflow.state.offlineQueueItems, 0);
|
||||
assert.equal(byteOverflow.state.offlineQueueOverflow, true);
|
||||
|
||||
const disabled = enqueueAuthorityOfflineMutation(
|
||||
{},
|
||||
{
|
||||
id: "disabled-mutation",
|
||||
payload: { a: 1 },
|
||||
},
|
||||
{
|
||||
...defaultSettings,
|
||||
authorityBrowserCacheMode: "off",
|
||||
},
|
||||
16000,
|
||||
);
|
||||
assert.equal(disabled.accepted, false);
|
||||
assert.equal(disabled.reason, "offline-queue-disabled");
|
||||
|
||||
const cleared = clearAuthorityOfflineQueue(enqueueResult.state, defaultSettings, 17000);
|
||||
assert.equal(cleared.offlineQueueItems, 0);
|
||||
assert.equal(cleared.offlineQueueBytes, 0);
|
||||
assert.equal(cleared.offlineQueueOverflow, false);
|
||||
|
||||
const snapshot = getAuthorityBrowserStateSnapshot(acceptedRevision, defaultSettings, 18000);
|
||||
assert.equal(snapshot.serverRevision, 11);
|
||||
assert.equal(snapshot.serverIntegrity, "server-integrity");
|
||||
assert.equal(snapshot.offlineQueueItems, 1);
|
||||
assert.equal("offlineQueue" in snapshot, false);
|
||||
|
||||
console.log("authority-browser-state tests passed");
|
||||
116
tests/authority-capabilities.mjs
Normal file
116
tests/authority-capabilities.mjs
Normal file
@@ -0,0 +1,116 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
buildAuthorityProbeUrls,
|
||||
collectAuthorityFeatures,
|
||||
normalizeAuthorityCapabilityState,
|
||||
normalizeAuthoritySettings,
|
||||
probeAuthorityCapabilities,
|
||||
} from "../runtime/authority-capabilities.js";
|
||||
import { defaultSettings } from "../runtime/settings-defaults.js";
|
||||
|
||||
const normalizedSettings = normalizeAuthoritySettings(defaultSettings);
|
||||
assert.equal(normalizedSettings.enabled, true);
|
||||
assert.equal(normalizedSettings.enabledMode, "auto");
|
||||
assert.equal(normalizedSettings.baseUrl, "/api/plugins/authority");
|
||||
assert.equal(normalizedSettings.storageMode, "auto-server-primary");
|
||||
assert.equal(normalizedSettings.vectorMode, "auto-primary");
|
||||
assert.equal(normalizedSettings.primaryWhenAvailable, true);
|
||||
|
||||
assert.deepEqual(buildAuthorityProbeUrls("/api/plugins/authority/"), [
|
||||
"/api/plugins/authority/v1/diagnostics/probe",
|
||||
"/api/plugins/authority/v1/probe",
|
||||
"/api/plugins/authority/probe",
|
||||
"/api/plugins/authority",
|
||||
]);
|
||||
|
||||
const collected = collectAuthorityFeatures({
|
||||
features: ["sql.query", "trivium.search"],
|
||||
services: {
|
||||
sql: true,
|
||||
jobs: true,
|
||||
blob: true,
|
||||
},
|
||||
});
|
||||
assert.equal(collected.has("sql.query"), true);
|
||||
assert.equal(collected.has("trivium.search"), true);
|
||||
assert.equal(collected.has("sql"), true);
|
||||
assert.equal(collected.has("jobs"), true);
|
||||
assert.equal(collected.has("blob"), true);
|
||||
|
||||
const readyState = normalizeAuthorityCapabilityState(
|
||||
{
|
||||
installed: true,
|
||||
healthy: true,
|
||||
features: ["sql", "trivium", "jobs", "blob"],
|
||||
},
|
||||
defaultSettings,
|
||||
);
|
||||
assert.equal(readyState.serverPrimaryReady, true);
|
||||
assert.equal(readyState.storagePrimaryReady, true);
|
||||
assert.equal(readyState.triviumPrimaryReady, true);
|
||||
assert.equal(readyState.minimumFeatureSetReady, true);
|
||||
|
||||
const missingState = normalizeAuthorityCapabilityState(
|
||||
{
|
||||
installed: true,
|
||||
healthy: true,
|
||||
features: ["sql"],
|
||||
},
|
||||
defaultSettings,
|
||||
);
|
||||
assert.equal(missingState.serverPrimaryReady, false);
|
||||
assert.equal(missingState.triviumPrimaryReady, false);
|
||||
assert.ok(missingState.missingFeatures.includes("trivium.search"));
|
||||
|
||||
const disabledState = await probeAuthorityCapabilities({
|
||||
settings: {
|
||||
...defaultSettings,
|
||||
authorityEnabled: "off",
|
||||
},
|
||||
fetchImpl: async () => {
|
||||
throw new Error("should-not-fetch");
|
||||
},
|
||||
nowMs: 1000,
|
||||
});
|
||||
assert.equal(disabledState.reason, "disabled");
|
||||
assert.equal(disabledState.serverPrimaryReady, false);
|
||||
assert.equal(disabledState.lastProbeAt, 1000);
|
||||
|
||||
let requestedUrl = "";
|
||||
const probedState = await probeAuthorityCapabilities({
|
||||
settings: defaultSettings,
|
||||
allowRelativeUrl: true,
|
||||
nowMs: 2000,
|
||||
fetchImpl: async (url) => {
|
||||
requestedUrl = url;
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
async json() {
|
||||
return {
|
||||
healthy: true,
|
||||
sessionReady: true,
|
||||
permissionReady: true,
|
||||
features: ["sql", "trivium", "jobs", "blob"],
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
assert.equal(requestedUrl, "/api/plugins/authority/v1/diagnostics/probe");
|
||||
assert.equal(probedState.installed, true);
|
||||
assert.equal(probedState.healthy, true);
|
||||
assert.equal(probedState.serverPrimaryReady, true);
|
||||
assert.equal(probedState.lastProbeAt, 2000);
|
||||
|
||||
const relativeUnavailable = await probeAuthorityCapabilities({
|
||||
settings: defaultSettings,
|
||||
allowRelativeUrl: false,
|
||||
fetchImpl: async () => ({ ok: true, status: 200, json: async () => ({}) }),
|
||||
nowMs: 3000,
|
||||
});
|
||||
assert.equal(relativeUnavailable.reason, "relative-url-unavailable");
|
||||
assert.equal(relativeUnavailable.serverPrimaryReady, false);
|
||||
|
||||
console.log("authority-capabilities tests passed");
|
||||
@@ -68,6 +68,25 @@ assert.equal(defaultSettings.worldInfoFilterMode, "default");
|
||||
assert.equal(defaultSettings.worldInfoFilterCustomKeywords, "");
|
||||
assert.equal("maintenanceAutoMinNewNodes" in defaultSettings, false);
|
||||
assert.equal(defaultSettings.embeddingTransportMode, "direct");
|
||||
assert.equal(defaultSettings.authorityEnabled, "auto");
|
||||
assert.equal(defaultSettings.authorityBaseUrl, "/api/plugins/authority");
|
||||
assert.equal(defaultSettings.authorityPrimaryWhenAvailable, true);
|
||||
assert.equal(defaultSettings.authorityStorageMode, "auto-server-primary");
|
||||
assert.equal(defaultSettings.authorityVectorMode, "auto-primary");
|
||||
assert.equal(defaultSettings.authoritySqlPrimary, true);
|
||||
assert.equal(defaultSettings.authorityTriviumPrimary, true);
|
||||
assert.equal(defaultSettings.authorityGraphQueryEnabled, true);
|
||||
assert.equal(defaultSettings.authorityJobsEnabled, true);
|
||||
assert.equal(defaultSettings.authorityBlobCheckpointEnabled, true);
|
||||
assert.equal(defaultSettings.authorityBrowserCacheMode, "minimal");
|
||||
assert.equal(defaultSettings.authorityOfflineWritePolicy, "queue-local-dirty");
|
||||
assert.equal(defaultSettings.authorityOfflineQueueMaxBytes, 1048576);
|
||||
assert.equal(defaultSettings.authorityOfflineQueueMaxItems, 128);
|
||||
assert.equal(defaultSettings.authorityOfflineQueueMaxAgeMs, 3600000);
|
||||
assert.equal(defaultSettings.authorityVectorSyncChunkSize, 1000);
|
||||
assert.equal(defaultSettings.authorityVectorFailOpen, true);
|
||||
assert.equal(defaultSettings.authorityDiagnosticsEnabled, true);
|
||||
assert.equal(defaultSettings.authorityProbeIntervalMs, 60000);
|
||||
assert.equal(defaultSettings.graphUseNativeLayout, true);
|
||||
assert.equal(defaultSettings.graphNativeLayoutThresholdNodes, 280);
|
||||
assert.equal(defaultSettings.graphNativeLayoutThresholdEdges, 1600);
|
||||
|
||||
@@ -96,6 +96,51 @@ export function createGraphPersistenceState() {
|
||||
storagePrimary: "indexeddb",
|
||||
storageMode: "indexeddb",
|
||||
resolvedLocalStore: "indexeddb:indexeddb",
|
||||
authority: {
|
||||
enabledMode: "auto",
|
||||
baseUrl: "/api/plugins/authority",
|
||||
installed: false,
|
||||
healthy: false,
|
||||
sessionReady: false,
|
||||
permissionReady: false,
|
||||
minimumFeatureSetReady: false,
|
||||
serverPrimaryReady: false,
|
||||
storagePrimaryReady: false,
|
||||
triviumPrimaryReady: false,
|
||||
jobsReady: false,
|
||||
blobReady: false,
|
||||
features: [],
|
||||
missingFeatures: [],
|
||||
reason: "not-probed",
|
||||
lastError: "",
|
||||
endpoint: "",
|
||||
status: 0,
|
||||
latencyMs: 0,
|
||||
lastProbeAt: 0,
|
||||
updatedAt: "",
|
||||
},
|
||||
authorityBrowserState: {
|
||||
mode: "minimal",
|
||||
serverRevision: 0,
|
||||
serverIntegrity: "",
|
||||
lastProbeAt: 0,
|
||||
lastCommitAt: 0,
|
||||
lastError: "",
|
||||
offlineQueueBytes: 0,
|
||||
offlineQueueItems: 0,
|
||||
offlineQueueOverflow: false,
|
||||
offlineQueueOverflowReason: "",
|
||||
updatedAt: "",
|
||||
},
|
||||
authorityInstalled: false,
|
||||
authorityHealthy: false,
|
||||
authorityServerPrimaryReady: false,
|
||||
authorityStoragePrimaryReady: false,
|
||||
authorityTriviumPrimaryReady: false,
|
||||
authorityBrowserCacheMode: "minimal",
|
||||
authorityOfflineQueueBytes: 0,
|
||||
authorityOfflineQueueItems: 0,
|
||||
authorityDegradedReason: "",
|
||||
localStoreFormatVersion: 1,
|
||||
localStoreMigrationState: "idle",
|
||||
opfsWriteLockState: {
|
||||
|
||||
Reference in New Issue
Block a user