@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
110 lines (109 loc) • 4.06 kB
JavaScript
/**
* Process-level cache for knowledge index snapshots.
*
* Knowledge sources are constructor-level static content. Multiple NeuroLink
* instances in the same server process can therefore share the same immutable
* lexical index instead of rebuilding it for every conversation/session.
*/
import { createHash } from "crypto";
import { buildIndexSnapshot } from "./knowledgeIndex.js";
import { normalizeAndValidate, resolveEntry } from "./resolve.js";
const CACHE_SCHEMA_VERSION = "knowledge-index-cache:v1";
export const KNOWLEDGE_INDEX_CACHE_LIMIT = 32;
const indexBuildCache = new Map();
const rememberCacheEntry = (cacheKey, buildPromise) => {
if (indexBuildCache.has(cacheKey)) {
indexBuildCache.delete(cacheKey);
}
indexBuildCache.set(cacheKey, buildPromise);
while (indexBuildCache.size > KNOWLEDGE_INDEX_CACHE_LIMIT) {
const oldestKey = indexBuildCache.keys().next().value;
if (oldestKey === undefined) {
return;
}
indexBuildCache.delete(oldestKey);
}
};
const escapePart = (value) => encodeURIComponent(value);
const stableStringify = (value) => {
if (value === null || typeof value !== "object") {
return JSON.stringify(value);
}
if (Array.isArray(value)) {
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
}
const record = value;
const entries = Object.keys(record)
.sort()
.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`);
return `{${entries.join(",")}}`;
};
const compareEntries = (left, right) => left.id.localeCompare(right.id) ||
stableStringify(left).localeCompare(stableStringify(right));
const hashSourceContent = (source, fallbackVersion) => {
const version = source.version ?? fallbackVersion;
const effectiveContent = {
id: source.id,
version,
entries: source.entries
.map((entry) => resolveEntry(entry, version))
.sort(compareEntries),
};
return createHash("sha256")
.update(stableStringify(effectiveContent))
.digest("hex");
};
const formatFieldWeights = (weights) => [
`title=${weights.title}`,
`aliases=${weights.aliases}`,
`keywords=${weights.keywords}`,
`summary=${weights.summary}`,
`body=${weights.body}`,
].join(",");
const formatSource = (source, fallbackVersion) => [
escapePart(source.id),
escapePart(source.version ?? fallbackVersion),
source.entries.length,
source.entries
.map((entry) => escapePart(entry.id))
.sort()
.join(","),
hashSourceContent(source, fallbackVersion),
].join(":");
/**
* Cache key intentionally uses source/version metadata, not conversation scope.
* The source content hash prevents stale reuse even when a host forgets to bump
* a source version after changing entries.
*/
export const buildKnowledgeIndexCacheKey = (sources, fallbackVersion, weights) => [
CACHE_SCHEMA_VERSION,
`weights=${formatFieldWeights(weights)}`,
...sources.map((source) => formatSource(source, fallbackVersion)),
].join("|");
export const getOrBuildKnowledgeIndexSnapshot = async (sources, fallbackVersion, weights) => {
const cacheKey = buildKnowledgeIndexCacheKey(sources, fallbackVersion, weights);
const cached = indexBuildCache.get(cacheKey);
if (cached) {
rememberCacheEntry(cacheKey, cached);
return cached;
}
const buildPromise = (async () => {
const { entries, validation } = await normalizeAndValidate(sources, {
manifestVersion: fallbackVersion,
});
if (!validation.ok) {
return { snapshot: null, validation };
}
return {
snapshot: buildIndexSnapshot(entries, weights),
validation,
};
})();
rememberCacheEntry(cacheKey, buildPromise);
void buildPromise.catch(() => {
if (indexBuildCache.get(cacheKey) === buildPromise) {
indexBuildCache.delete(cacheKey);
}
});
return buildPromise;
};