@fusedashlabs/ui9000-workspace
Version:
UI9000 workspace MCP. One tool, show_workspace, draws a chart from your table. Set TYPESAFE_API_KEY to let Jev choose the chart.
167 lines (166 loc) • 5.32 kB
JavaScript
/**
* Filesystem TTL store — copied from mcp-ui `src/utils/dataLinkStore.ts`.
*
* Same role as mcp-ui's `.data/`: one JSON file per link id, each wrapping the
* payload in a `{ value, exp }` envelope so an expired file is dropped on the
* next touch rather than lingering. Reads and writes are sync on purpose — the
* store is small and the signing path must not interleave.
*
* Deviation from mcp-ui: the stored record carries an opaque `payload` instead
* of mcp-ui's `{ config, data }` split, because this package's contract
* (`SignDataLink = (payload: unknown) => SignedLink`) signs arbitrary payloads.
* The envelope, file layout, and expiry mechanics are unchanged.
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
/** `packages/core` — the root a relative STORAGE_DIR resolves against. */
function packageRoot() {
return fileURLToPath(new URL('../../../', import.meta.url));
}
/**
* Resolve the on-disk TTL store directory.
* Cursor/Claude stdio often have cwd=$HOME, so `path.resolve(cwd, '.data')`
* would write to ~/.data while the server reads the package `.data` — the
* links then 404. Default: `<packageRoot>/.data` unless STORAGE_DIR is absolute.
*/
export function resolveDataLinkStorageDir(env = process.env, root = packageRoot()) {
const raw = (env.STORAGE_DIR || '.data').trim() || '.data';
if (path.isAbsolute(raw))
return raw;
return path.resolve(root, raw);
}
export const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
export class TtlStore {
defaultTtlMs;
dataDir;
constructor(defaultTtlMs = DEFAULT_TTL_MS, dataDir) {
this.defaultTtlMs = defaultTtlMs;
this.dataDir = dataDir ?? resolveDataLinkStorageDir();
this.ensureDir();
}
ensureDir() {
if (!fs.existsSync(this.dataDir)) {
fs.mkdirSync(this.dataDir, { recursive: true });
}
}
filePathFor(id) {
return path.join(this.dataDir, `${id}.json`);
}
set(id, payload, customTtlMs) {
const ttlMs = customTtlMs || this.defaultTtlMs;
const now = Date.now();
const createdAt = new Date(now).toISOString();
const value = {
id,
createdAt,
ttlSeconds: Math.floor(ttlMs / 1000),
payload,
};
const envelope = { value, exp: now + ttlMs };
// Fail closed: a signed URL with no file on disk is a 404. mcp-ui swallowed
// this write; we do not — signDataLink must not return a dead handle.
this.ensureDir();
fs.writeFileSync(this.filePathFor(id), JSON.stringify(envelope), 'utf8');
this.cleanup();
}
get(id) {
this.cleanup();
const file = this.filePathFor(id);
if (!fs.existsSync(file))
return null;
try {
const raw = fs.readFileSync(file, 'utf8');
const parsed = JSON.parse(raw);
if (Date.now() > parsed.exp) {
this.unlinkQuietly(file);
return null;
}
return parsed.value;
}
catch (err) {
console.error('Failed to read data link:', err);
return null;
}
}
has(id) {
return this.get(id) !== null;
}
delete(id) {
const file = this.filePathFor(id);
if (!fs.existsSync(file))
return false;
try {
fs.unlinkSync(file);
return true;
}
catch {
return false;
}
}
size() {
this.cleanup();
return this.linkFiles().length;
}
/** Drop every link. Used by tests and by store teardown. */
destroy() {
for (const file of this.linkFiles()) {
this.unlinkQuietly(file);
}
}
linkFiles() {
try {
if (!fs.existsSync(this.dataDir))
return [];
return fs
.readdirSync(this.dataDir)
.filter((f) => f.endsWith('.json'))
.map((f) => path.join(this.dataDir, f));
}
catch {
return [];
}
}
unlinkQuietly(file) {
try {
fs.unlinkSync(file);
}
catch {
// A concurrent reader already removed it — expiry is idempotent.
}
}
/** Sweep expired (and unreadable) files so the directory cannot grow forever. */
cleanup() {
const now = Date.now();
for (const file of this.linkFiles()) {
try {
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
if (now > parsed.exp)
this.unlinkQuietly(file);
}
catch {
// Corrupt or truncated file: it can never be served, so drop it.
this.unlinkQuietly(file);
}
}
}
}
let store = null;
export function getDataLinkStore() {
if (!store) {
store = new TtlStore();
}
return store;
}
/** Test-only: drop the singleton so STORAGE_DIR can be re-read. */
export function resetDataLinkStoreForTests() {
if (store) {
try {
store.destroy();
}
catch {
// Teardown is best-effort.
}
}
store = null;
}