everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
174 lines (172 loc) • 9.18 kB
JavaScript
import { fetchJsonOrNull, fetchRaw } from "../../utils/http-client.mjs";
import { Context, Layer } from "effect";
//#region src/services/registry/fastkv.ts
function getNetworkIdForAccount(accountId) {
return accountId.endsWith(".testnet") ? "testnet" : "mainnet";
}
const FASTKV_TESTNET_URL = "https://kv.test.fastnear.com";
const FASTKV_MAINNET_URL = "https://kv.main.fastnear.com";
const REGISTRY_NAMESPACE_TESTNET = "dev.allthethings.testnet";
const REGISTRY_NAMESPACE_MAINNET = "dev.everything.near";
function getFastKvBaseUrlForNetwork(network) {
return network === "testnet" ? FASTKV_TESTNET_URL : FASTKV_MAINNET_URL;
}
function getFastKvBaseUrlForAccount(accountId) {
return getNetworkIdForAccount(accountId) === "testnet" ? getFastKvBaseUrlForNetwork("testnet") : getFastKvBaseUrlForNetwork("mainnet");
}
function buildRegistryConfigUrl(accountId, gatewayId, registry) {
const baseUrl = getFastKvBaseUrlForAccount(accountId);
const namespace = getRegistryNamespaceForAccount(accountId, registry);
const key = encodeURIComponent(getRegistryConfigKey(accountId, gatewayId));
return `${baseUrl}/v0/latest/${encodeURIComponent(namespace)}/${encodeURIComponent(accountId)}/${key}`;
}
function buildRegistryConfigUrlForNetwork(network, accountId, gatewayId, registry) {
const baseUrl = getFastKvBaseUrlForNetwork(network);
const namespace = getRegistryNamespaceForNetwork(network, registry);
const key = encodeURIComponent(getRegistryConfigKey(accountId, gatewayId));
return `${baseUrl}/v0/latest/${encodeURIComponent(namespace)}/${encodeURIComponent(accountId)}/${key}`;
}
function getRegistryNamespaceForAccount(accountId, registry) {
return registry ?? getRegistryNamespaceForNetwork(getNetworkIdForAccount(accountId));
}
function getRegistryNamespaceForNetwork(network, registry) {
if (registry) return registry;
return network === "testnet" ? REGISTRY_NAMESPACE_TESTNET : REGISTRY_NAMESPACE_MAINNET;
}
function getRegistryConfigKey(accountId, gatewayId, pathSegments = []) {
return `apps/${accountId}/${gatewayId}${pathSegments.length > 0 ? `/${pathSegments.map((segment) => encodeURIComponent(segment)).join("/")}` : ""}/bos.config.json`;
}
function parseBosUrl(bosUrl) {
const match = bosUrl.split("#")[0].match(/^bos:\/\/([^/]+)\/(.+)$/);
if (!match?.[1] || !match[2]) throw new Error(`Invalid BOS URL: ${bosUrl}`);
const pathSegments = match[2].split("/").filter(Boolean).map((segment) => decodeURIComponent(segment));
if (pathSegments.length === 0) throw new Error(`Invalid BOS URL: ${bosUrl}`);
const [gatewayId, ...pathSegmentsTail] = pathSegments;
if (!gatewayId) throw new Error(`Invalid BOS URL: ${bosUrl}`);
return {
accountId: match[1],
gatewayId,
pathSegments: pathSegmentsTail
};
}
async function fetchBosConfigFromFastKv(bosUrl, registry) {
const { accountId, gatewayId, pathSegments } = parseBosUrl(bosUrl);
const key = encodeURIComponent(getRegistryConfigKey(accountId, gatewayId, pathSegments));
return parseFastKvConfigValue(await fetchJson(`${getFastKvBaseUrlForAccount(accountId)}/v0/latest/${encodeURIComponent(getRegistryNamespaceForAccount(accountId, registry))}/${encodeURIComponent(accountId)}/${key}`), bosUrl);
}
/**
* Read an arbitrary registry key under `apps/<account>/<gateway>/` — the
* well-known-slot seam (ADR 0034's `platform.json` lives beside the config).
* Absent key (404) → null; a network failure throws so callers can distinguish
* "the slot is empty" from "the slot is unreachable" (a swallowed error here
* would silently tighten a compat floor).
*/
async function fetchRegistryKey(accountId, gatewayId, keyPath, registry) {
const key = encodeURIComponent(`apps/${accountId}/${gatewayId}/${keyPath}`);
const value = (await fetchJson(`${getFastKvBaseUrlForAccount(accountId)}/v0/latest/${encodeURIComponent(getRegistryNamespaceForAccount(accountId, registry))}/${encodeURIComponent(accountId)}/${key}`))?.entries?.find(Boolean)?.value;
if (!value) return null;
if (typeof value === "string") return JSON.parse(value);
return value;
}
/** Tolerant variant of {@link fetchRegistryKey} — any failure collapses to null. */
async function fetchRegistryKeyOrNull(accountId, gatewayId, keyPath, registry) {
try {
return await fetchRegistryKey(accountId, gatewayId, keyPath, registry);
} catch {
return null;
}
}
/** Read a published config from an already-built registry URL
* (`buildRegistryConfigUrlForNetwork`). Unlike {@link fetchBosConfigFromFastKv}
* this takes the URL directly, so the read key is exactly the one the publish
* writes, and a network failure is reported as such instead of collapsing
* into "No config found" (fetchJsonOrNull swallows errors into null). */
async function fetchPublishedConfigFromFastKv(url) {
const response = await fetchRaw(url, { headers: { accept: "application/json" } });
if (!response.ok) {
if (response.status === 404) throw new Error(`No config found for ${url}`);
throw new Error(`HTTP ${response.status} fetching ${url}`);
}
return parseFastKvConfigValue(await response.json(), url);
}
function parseFastKvConfigValue(payload, label) {
const value = payload?.entries?.find(Boolean)?.value;
if (!value) throw new Error(`No config found for ${label}`);
if (typeof value === "string") return JSON.parse(value);
if (typeof value !== "object") throw new Error(`Invalid config value for ${label}`);
return value;
}
const FASTKV_HISTORY_MAX_LIMIT = 200;
/**
* Publish history for the registry config key (atomic-deploys 11): the
* exact-key variant of FastData's history endpoint, newest-first. The API
* caps a page at 200 rows; a config key's write history fits well inside
* that, so a single request is the whole story (the limit param is clamped).
*/
async function fetchConfigHistory(opts) {
const key = encodeURIComponent(getRegistryConfigKey(opts.accountId, opts.gatewayId));
const limit = Math.max(1, Math.min(opts.limit ?? 20, FASTKV_HISTORY_MAX_LIMIT));
return ((await fetchJson(`${getFastKvBaseUrlForAccount(opts.accountId)}/v0/history/${encodeURIComponent(getRegistryNamespaceForAccount(opts.accountId, opts.registry))}/${encodeURIComponent(opts.accountId)}/${key}?limit=${limit}`))?.entries ?? []).filter(Boolean).map((entry) => ({
blockHeight: entry.block_height ?? 0,
blockTimestampNs: String(entry.block_timestamp ?? ""),
...entry.tx_hash ? { txHash: entry.tx_hash } : {},
value: typeof entry.value === "string" && entry.value.length > 0 ? JSON.parse(entry.value) : entry.value
})).sort((a, b) => b.blockHeight - a.blockHeight);
}
/**
* The per-deploy audit trail (atomic-deploys 03/12): every publish writes
* `apps/<account>/<gateway>/manifests/<ts>.json` atomically with the pointer
* swap — this lists that key family newest-first via the history-by-prefix
* endpoint.
*/
async function fetchDeployManifests(opts) {
const keyPrefix = encodeURIComponent(`${getRegistryConfigKey(opts.accountId, opts.gatewayId).replace(/bos\.config\.json$/, "")}manifests/`);
return ((await fetchJson(`${getFastKvBaseUrlForAccount(opts.accountId)}/v0/history/${encodeURIComponent(getRegistryNamespaceForAccount(opts.accountId, opts.registry))}/${encodeURIComponent(opts.accountId)}`, {
method: "POST",
body: JSON.stringify({
key_prefix: decodeURIComponent(keyPrefix),
asc: false,
limit: Math.max(1, Math.min(opts.limit ?? 20, FASTKV_HISTORY_MAX_LIMIT))
})
}))?.entries ?? []).filter(Boolean).map((entry) => ({
key: entry.key ?? "",
blockHeight: entry.block_height ?? 0,
blockTimestampNs: String(entry.block_timestamp ?? ""),
value: typeof entry.value === "string" && entry.value.length > 0 ? JSON.parse(entry.value) : entry.value
})).sort((a, b) => b.blockHeight - a.blockHeight);
}
async function fetchRemotePluginManifest(cdnUrl) {
const baseUrl = cdnUrl.replace(/\/$/, "");
return fetchJsonOrNull(`${baseUrl}/plugin.manifest.json`, { retries: 2 });
}
async function fetchJson(url, init) {
const headers = {
accept: "application/json",
"content-type": "application/json",
...init?.headers
};
return fetchJsonOrNull(url, {
method: init?.method,
headers,
body: init?.body ?? void 0,
retries: 3
});
}
/**
* The FastKV registry as a Context.Service (#425): reads (config, published
* config, history, deploy manifests) and the bos:// URL algebra behind one
* tag. The registry WRITE is a signing concern — it rides the signer
* services (#423), not this tag. Callers may keep using the plain
* functions — the module is the implementation; the tag is the seam.
*/
var FastKVRegistryService = class extends Context.Service()("everything-dev/registry/fastkv") {
static Live = Layer.succeed(this, this.of({
fetchBosConfigFromFastKv,
fetchPublishedConfigFromFastKv,
fetchConfigHistory,
fetchDeployManifests
}));
};
//#endregion
export { FastKVRegistryService, buildRegistryConfigUrl, buildRegistryConfigUrlForNetwork, fetchBosConfigFromFastKv, fetchConfigHistory, fetchDeployManifests, fetchPublishedConfigFromFastKv, fetchRegistryKey, fetchRegistryKeyOrNull, fetchRemotePluginManifest, getFastKvBaseUrlForNetwork, getRegistryNamespaceForAccount, getRegistryNamespaceForNetwork, parseBosUrl, parseFastKvConfigValue };
//# sourceMappingURL=fastkv.mjs.map