Files
ST-Bionic-Memory-Ecology/maintenance/authority-blob-adapter.js

365 lines
13 KiB
JavaScript

import { normalizeAuthorityBaseUrl } from "../runtime/authority-capabilities.js";
import { AuthorityHttpClient } from "../runtime/authority-http-client.js";
export const AUTHORITY_BLOB_ENDPOINT = "/fs/private";
function toPlainData(value, fallbackValue = null) {
if (value == null) return fallbackValue;
if (typeof globalThis.structuredClone === "function") {
try {
return globalThis.structuredClone(value);
} catch {
}
}
try {
return JSON.parse(JSON.stringify(value));
} catch {
return fallbackValue;
}
}
function normalizeRecordId(value) {
return String(value ?? "").trim();
}
function normalizeInteger(value, fallback = 0, min = 0, max = Number.MAX_SAFE_INTEGER) {
const parsed = Number(value);
if (!Number.isFinite(parsed)) return fallback;
return Math.min(max, Math.max(min, Math.trunc(parsed)));
}
function decodeBase64Utf8(base64Text = "") {
const normalizedBase64 = String(base64Text ?? "");
if (!normalizedBase64) return "";
if (typeof globalThis.atob === "function" && typeof globalThis.TextDecoder === "function") {
const binary = globalThis.atob(normalizedBase64);
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
if (typeof Buffer !== "undefined") {
return Buffer.from(normalizedBase64, "base64").toString("utf8");
}
return normalizedBase64;
}
function tryParseJsonText(text, fallbackValue = null) {
if (typeof text !== "string") return fallbackValue;
try {
return JSON.parse(text);
} catch {
return fallbackValue;
}
}
export function normalizeAuthorityBlobPath(path = "") {
const normalized = String(path ?? "")
.trim()
.replace(/\\/g, "/")
.replace(/^authority:\/\/private\//i, "")
.replace(/^\/+/, "")
.replace(/\/+/g, "/");
return normalized.replace(/\/+$/g, "");
}
function normalizeBlobPayload(result = null) {
if (!result || typeof result !== "object" || Array.isArray(result)) return result;
const source = result.file || result.blob || result.result || result;
if (source.payload !== undefined) return source.payload;
if (source.json !== undefined) return source.json;
if (source.value !== undefined) return source.value;
if (source.data !== undefined) {
if (source.encoding === "base64" || source.base64 === true) {
return tryParseJsonText(decodeBase64Utf8(source.data), source.data);
}
if (typeof source.data === "string") {
return tryParseJsonText(source.data, source.data);
}
return source.data;
}
if (source.content !== undefined) {
if (typeof source.content === "string") {
return tryParseJsonText(source.content, source.content);
}
return source.content;
}
if (source.body !== undefined) {
if (typeof source.body === "string") {
return tryParseJsonText(source.body, source.body);
}
return source.body;
}
return null;
}
function normalizeBlobRecordSource(input = null) {
if (!input || typeof input !== "object" || Array.isArray(input)) return {};
return input.file || input.blob || input.result || input;
}
export function normalizeAuthorityBlobReadResult(input = null, fallbackPath = "") {
const source = normalizeBlobRecordSource(input);
const entry = source.entry && typeof source.entry === "object" ? source.entry : null;
const path = normalizeAuthorityBlobPath(entry?.path || source.path || source.name || fallbackPath);
const missing =
source.exists === false ||
source.found === false ||
source.missing === true ||
Number(source.status || source.statusCode || 0) === 404;
if (missing) {
return {
exists: false,
path,
payload: null,
contentType: String(source.contentType || source.type || ""),
raw: toPlainData(input, input),
};
}
return {
exists: input != null && source.ok !== false,
path,
payload: normalizeBlobPayload(input),
contentType: String(source.contentType || source.type || "application/json"),
etag: String(source.etag || source.hash || ""),
updatedAt: source.updatedAt || source.updated_at || source.lastModified || entry?.updatedAt || "",
raw: toPlainData(input, input),
};
}
export function normalizeAuthorityBlobWriteResult(input = null, fallbackPath = "") {
const source = normalizeBlobRecordSource(input);
const entry = source.entry && typeof source.entry === "object" ? source.entry : null;
const path = normalizeAuthorityBlobPath(entry?.path || source.path || source.name || fallbackPath);
return {
ok: input == null ? true : source.ok !== false && source.error == null,
path,
url: String(source.url || source.href || ""),
size: normalizeInteger(source.size || source.bytes || entry?.sizeBytes, 0, 0),
etag: String(source.etag || source.hash || ""),
updatedAt: source.updatedAt || source.updated_at || source.lastModified || entry?.updatedAt || "",
raw: toPlainData(input, input),
};
}
export function normalizeAuthorityBlobDeleteResult(input = null, fallbackPath = "") {
const source = normalizeBlobRecordSource(input);
const path = normalizeAuthorityBlobPath(source.path || source.name || fallbackPath);
const missing =
source.exists === false ||
source.found === false ||
source.missing === true ||
Number(source.status || source.statusCode || 0) === 404;
return {
ok: input == null ? true : source.ok !== false,
deleted: missing ? false : source.deleted !== false && source.ok !== false,
missing,
path,
raw: toPlainData(input, input),
};
}
export function normalizeAuthorityBlobConfig(settings = {}, overrides = {}) {
const source = settings && typeof settings === "object" && !Array.isArray(settings) ? settings : {};
return {
baseUrl: normalizeAuthorityBaseUrl(source.authorityBaseUrl ?? source.baseUrl),
enabled:
source.authorityBlobCheckpointEnabled !== false &&
source.blobCheckpointEnabled !== false &&
source.authorityBlobEnabled !== false,
failOpen: source.authorityFailOpen !== false && source.failOpen !== false,
namespace: normalizeRecordId(source.authorityBlobNamespace || source.blobNamespace || "st-bme"),
...overrides,
};
}
export class AuthorityBlobHttpClient {
constructor(options = {}) {
this.http = new AuthorityHttpClient({
...options,
baseUrl: normalizeAuthorityBaseUrl(options.baseUrl),
});
}
async request(path, payload = {}, options = {}) {
return await this.http.requestJson(path, {
method: options.method || "POST",
body: payload,
session: true,
signal: options.signal,
});
}
async writeJson(payload = {}) {
return await this.writeText({
...payload,
contentType: payload.contentType || "application/json",
text: JSON.stringify(toPlainData(payload.payload ?? payload.data, payload.payload ?? payload.data)),
});
}
async writeText(payload = {}) {
return await this.request(`${AUTHORITY_BLOB_ENDPOINT}/write-file`, {
path: normalizeAuthorityBlobPath(payload.path || payload.name),
content: String(payload.text ?? payload.data ?? payload.content ?? ""),
encoding: "utf8",
createParents: true,
}, { signal: payload.signal });
}
async readJson(payload = {}) {
return await this.request(`${AUTHORITY_BLOB_ENDPOINT}/read-file`, {
path: normalizeAuthorityBlobPath(payload.path || payload.name),
encoding: "utf8",
}, { signal: payload.signal });
}
async delete(payload = {}) {
return await this.request(`${AUTHORITY_BLOB_ENDPOINT}/delete`, {
path: normalizeAuthorityBlobPath(payload.path || payload.name),
recursive: false,
}, { signal: payload.signal });
}
async stat(payload = {}) {
return await this.request(`${AUTHORITY_BLOB_ENDPOINT}/stat`, {
path: normalizeAuthorityBlobPath(payload.path || payload.name),
}, { signal: payload.signal });
}
}
export function createAuthorityBlobClient(config = {}, options = {}) {
const injected = options.blobClient || config.blobClient || globalThis.__stBmeAuthorityBlobClient;
if (injected) return injected;
return new AuthorityBlobHttpClient({
baseUrl: config.baseUrl,
fetchImpl: options.fetchImpl || config.fetchImpl,
headerProvider: options.headerProvider || config.headerProvider,
});
}
async function callClient(client, methodNames = [], action = "request", payload = {}) {
for (const methodName of methodNames) {
if (typeof client?.[methodName] === "function") {
return await client[methodName](payload);
}
}
if (typeof client?.request === "function") {
return await client.request(action, payload);
}
if (typeof client === "function") {
return await client({ action, ...payload });
}
throw new Error(`Authority Blob ${action} unavailable`);
}
function throwIfAborted(signal) {
if (signal?.aborted) {
throw signal.reason instanceof Error
? signal.reason
: Object.assign(new Error("操作已终止"), { name: "AbortError" });
}
}
function isMissingAuthorityBlobError(error) {
return Number(error?.status || 0) === 404;
}
export class AuthorityBlobAdapter {
constructor(config = {}, options = {}) {
this.config = normalizeAuthorityBlobConfig(config, options.configOverrides || {});
this.client = createAuthorityBlobClient(this.config, options);
}
async writeJson(path, payload = null, options = {}) {
throwIfAborted(options.signal);
const normalizedPath = normalizeAuthorityBlobPath(path);
if (!normalizedPath) throw new Error("Authority Blob path is required");
const result = await callClient(this.client, ["writeJson", "putJson", "writeFile", "put"], "writeJson", {
namespace: options.namespace || this.config.namespace,
path: normalizedPath,
name: normalizedPath,
contentType: options.contentType || "application/json",
payload: toPlainData(payload, payload),
data: toPlainData(payload, payload),
metadata: toPlainData(options.metadata, {}),
});
return normalizeAuthorityBlobWriteResult(result, normalizedPath);
}
async writeText(path, text = "", options = {}) {
throwIfAborted(options.signal);
const normalizedPath = normalizeAuthorityBlobPath(path);
if (!normalizedPath) throw new Error("Authority Blob path is required");
const result = await callClient(this.client, ["writeText", "writeFile", "putText", "put"], "writeText", {
namespace: options.namespace || this.config.namespace,
path: normalizedPath,
name: normalizedPath,
contentType: options.contentType || "text/plain; charset=utf-8",
text: String(text ?? ""),
data: String(text ?? ""),
metadata: toPlainData(options.metadata, {}),
});
return normalizeAuthorityBlobWriteResult(result, normalizedPath);
}
async readJson(path, options = {}) {
throwIfAborted(options.signal);
const normalizedPath = normalizeAuthorityBlobPath(path);
if (!normalizedPath) return normalizeAuthorityBlobReadResult({ exists: false }, "");
try {
const result = await callClient(this.client, ["readJson", "getJson", "readFile", "get"], "readJson", {
namespace: options.namespace || this.config.namespace,
path: normalizedPath,
name: normalizedPath,
});
return normalizeAuthorityBlobReadResult(result, normalizedPath);
} catch (error) {
if (isMissingAuthorityBlobError(error)) {
return normalizeAuthorityBlobReadResult({ exists: false, status: 404 }, normalizedPath);
}
throw error;
}
}
async delete(path, options = {}) {
throwIfAborted(options.signal);
const normalizedPath = normalizeAuthorityBlobPath(path);
if (!normalizedPath) return normalizeAuthorityBlobDeleteResult({ exists: false }, "");
try {
const result = await callClient(this.client, ["delete", "deleteFile", "remove", "unlink"], "delete", {
namespace: options.namespace || this.config.namespace,
path: normalizedPath,
name: normalizedPath,
});
return normalizeAuthorityBlobDeleteResult(result, normalizedPath);
} catch (error) {
if (isMissingAuthorityBlobError(error)) {
return normalizeAuthorityBlobDeleteResult({ exists: false, status: 404 }, normalizedPath);
}
throw error;
}
}
async stat(path, options = {}) {
throwIfAborted(options.signal);
const normalizedPath = normalizeAuthorityBlobPath(path);
if (!normalizedPath) return normalizeAuthorityBlobReadResult({ exists: false }, "");
try {
const result = await callClient(this.client, ["stat", "head", "metadata"], "stat", {
namespace: options.namespace || this.config.namespace,
path: normalizedPath,
name: normalizedPath,
});
return normalizeAuthorityBlobReadResult(result, normalizedPath);
} catch (error) {
if (isMissingAuthorityBlobError(error)) {
return normalizeAuthorityBlobReadResult({ exists: false, status: 404 }, normalizedPath);
}
throw error;
}
}
}
export function createAuthorityBlobAdapter(config = {}, options = {}) {
return new AuthorityBlobAdapter(config, options);
}