UNPKG

trellis

Version:

Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications

599 lines (595 loc) 18.7 kB
import { RelayPersistence, init_relay_persistence } from "./chunk-KXAISKZT.js"; import { __esm } from "./chunk-2ESYSVXG.js"; // src/realtime/blob-handler.ts function createBlobRequestHandler(store, opts = {}) { const maxBytes = opts.maxBlobBytes ?? DEFAULT_MAX_BLOB_BYTES; const authorize = opts.authorizeBlobWrite ?? (() => true); return (req, res) => { const reqPath = (req.url ?? "/").split("?")[0]; const method = req.method ?? "GET"; if (reqPath === "/blob" || reqPath.startsWith("/blob/")) { if (method === "OPTIONS") { res.writeHead(204, BLOB_CORS); res.end(); return true; } } if (reqPath.startsWith("/blob/")) { const rest = reqPath.slice("/blob/".length); const metaMatch = /^([a-f0-9]{64})\/meta$/.exec(rest); if (metaMatch) { const hash2 = metaMatch[1]; if (method === "PUT") { void handlePutMeta(req, res, store, hash2, authorize); return true; } if (method === "GET") { if (!store.has(hash2)) { res.writeHead(404, { "content-type": "application/json", ...BLOB_CORS }); res.end(JSON.stringify({ error: "not_found", hash: hash2 })); return true; } const meta = store.getMeta(hash2) ?? {}; res.writeHead(200, { "content-type": "application/json", ...BLOB_CORS }); res.end(JSON.stringify({ hash: hash2, ...meta })); return true; } res.writeHead(405, { allow: "GET, PUT, OPTIONS", ...BLOB_CORS }); res.end(); return true; } const hash = rest; if (!HASH_RE.test(hash)) { res.writeHead(400, { "content-type": "application/json", ...BLOB_CORS }); res.end(JSON.stringify({ error: "invalid_hash", hash })); return true; } if (method === "HEAD") { const size = store.size(hash); if (size == null) { res.writeHead(404, BLOB_CORS); res.end(); return true; } const meta = store.getMeta(hash); res.writeHead(200, { "content-type": meta?.contentType || "application/octet-stream", "content-length": size, etag: `"${hash}"`, "accept-ranges": "bytes", "cache-control": "public, max-age=31536000, immutable", ...BLOB_CORS }); res.end(); return true; } if (method === "GET") { const inm = req.headers["if-none-match"]; if (inm && etagMatches(inm, hash)) { res.writeHead(304, { etag: `"${hash}"`, "cache-control": "public, max-age=31536000, immutable", ...BLOB_CORS }); res.end(); return true; } const size = store.size(hash); if (size == null) { res.writeHead(404, { "content-type": "application/json", ...BLOB_CORS }); res.end(JSON.stringify({ error: "not_found", hash })); return true; } const meta = store.getMeta(hash); const baseHeaders = { "content-type": meta?.contentType || "application/octet-stream", etag: `"${hash}"`, "accept-ranges": "bytes", "cache-control": "public, max-age=31536000, immutable", ...BLOB_CORS }; const rangeHeader = req.headers["range"]; if (typeof rangeHeader === "string" && rangeHeader.length > 0) { const range = parseByteRange(rangeHeader, size); if (!range) { res.writeHead(416, { ...baseHeaders, "content-range": `bytes */${size}` }); res.end(); return true; } const stream2 = store.createReadStream(hash, range); if (!stream2) { res.writeHead(404, { "content-type": "application/json", ...BLOB_CORS }); res.end(JSON.stringify({ error: "not_found", hash })); return true; } res.writeHead(206, { ...baseHeaders, "content-range": `bytes ${range.start}-${range.end}/${size}`, "content-length": range.end - range.start + 1 }); pipeBlob(stream2, res); return true; } const stream = store.createReadStream(hash); if (!stream) { res.writeHead(404, { "content-type": "application/json", ...BLOB_CORS }); res.end(JSON.stringify({ error: "not_found", hash })); return true; } res.writeHead(200, { ...baseHeaders, "content-length": size }); pipeBlob(stream, res); return true; } res.writeHead(405, { allow: "GET, HEAD, OPTIONS", ...BLOB_CORS }); res.end(); return true; } if (reqPath === "/blob" && method === "GET") { const blobs = store.listHashes().map((hash) => { const meta = store.getMeta(hash); return { hash, size: store.size(hash) ?? 0, ...meta?.name ? { name: meta.name } : {}, ...meta?.contentType ? { contentType: meta.contentType } : {}, ...meta?.uploadedAt ? { uploadedAt: meta.uploadedAt } : {} }; }); res.writeHead(200, { "content-type": "application/json", ...BLOB_CORS }); res.end(JSON.stringify({ blobs })); return true; } if (reqPath === "/blob" && method === "PUT") { void handlePut(req, res, store, maxBytes, authorize); return true; } if (reqPath === "/blob") { res.writeHead(405, { allow: "GET, PUT, OPTIONS", ...BLOB_CORS }); res.end(); return true; } return false; }; } function parseByteRange(header, size) { const m = /^bytes=(\d*)-(\d*)$/.exec(header.trim()); if (!m) return null; const [, startStr, endStr] = m; if (startStr === "" && endStr === "") return null; let start; let end; if (startStr === "") { const suffix = Number(endStr); if (suffix <= 0) return null; start = Math.max(0, size - suffix); end = size - 1; } else { start = Number(startStr); end = endStr === "" ? size - 1 : Math.min(Number(endStr), size - 1); } if (start < 0 || start > end || start >= size) return null; return { start, end }; } function pipeBlob(stream, res) { stream.on("error", () => { if (!res.headersSent) { res.writeHead(500, { "content-type": "application/json", ...BLOB_CORS }); res.end(JSON.stringify({ error: "read_failed" })); } else { res.destroy(); } }); stream.pipe(res); } function etagMatches(ifNoneMatch, hash) { const expected = `"${hash}"`; return ifNoneMatch.split(",").map((s) => s.trim()).some((tag) => tag === "*" || tag === expected || tag === `W/${expected}` || tag === hash); } async function handlePut(req, res, store, maxBytes, authorize) { try { const allowed = await authorize(req); if (!allowed) { res.writeHead(401, { "content-type": "application/json", ...BLOB_CORS }); res.end(JSON.stringify({ error: "unauthorized" })); return; } const declared = Number(req.headers["content-length"]); if (Number.isFinite(declared) && declared > maxBytes) { res.writeHead(413, { "content-type": "application/json", ...BLOB_CORS }); res.end( JSON.stringify({ error: "payload_too_large", maxBlobBytes: maxBytes }) ); return; } const body = await readBodyLimited(req, maxBytes); if (body === "too_large") { res.writeHead(413, { "content-type": "application/json", ...BLOB_CORS }); res.end(JSON.stringify({ error: "payload_too_large", maxBlobBytes: maxBytes })); return; } const hash = await store.put(body); const rawName = req.headers["x-trellis-filename"]; const filename = typeof rawName === "string" ? rawName.trim().slice(0, 255) : ""; const declaredType = req.headers["content-type"]; const contentType = typeof declaredType === "string" && declaredType.length > 0 && !/^application\/octet-stream$/i.test(declaredType) ? declaredType.split(";")[0].trim().slice(0, 128) : void 0; if (filename || contentType) { store.setMeta(hash, { name: filename || void 0, contentType, uploadedAt: Date.now() }); } res.writeHead(201, { "content-type": "application/json", etag: `"${hash}"`, ...BLOB_CORS }); res.end(JSON.stringify({ hash })); } catch { if (!res.headersSent) { res.writeHead(500, { "content-type": "application/json", ...BLOB_CORS }); res.end(JSON.stringify({ error: "upload_failed" })); } } } async function handlePutMeta(req, res, store, hash, authorize) { try { const allowed = await authorize(req); if (!allowed) { res.writeHead(401, { "content-type": "application/json", ...BLOB_CORS }); res.end(JSON.stringify({ error: "unauthorized" })); return; } if (!store.has(hash)) { res.writeHead(404, { "content-type": "application/json", ...BLOB_CORS }); res.end(JSON.stringify({ error: "not_found", hash })); return; } const body = await readBodyLimited(req, 64 * 1024); if (body === "too_large") { res.writeHead(413, { "content-type": "application/json", ...BLOB_CORS }); res.end(JSON.stringify({ error: "payload_too_large" })); return; } let parsed; try { parsed = JSON.parse(body.toString("utf8")); } catch { res.writeHead(400, { "content-type": "application/json", ...BLOB_CORS }); res.end(JSON.stringify({ error: "invalid_json" })); return; } if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { res.writeHead(400, { "content-type": "application/json", ...BLOB_CORS }); res.end(JSON.stringify({ error: "invalid_meta" })); return; } const obj = parsed; const name = typeof obj.name === "string" ? obj.name.trim().slice(0, 255) : void 0; const contentType = typeof obj.contentType === "string" ? obj.contentType.trim().slice(0, 128) : void 0; store.setMeta(hash, { name, contentType, uploadedAt: typeof obj.uploadedAt === "number" ? obj.uploadedAt : Date.now() }); res.writeHead(200, { "content-type": "application/json", ...BLOB_CORS }); res.end(JSON.stringify({ hash, ...store.getMeta(hash) ?? {} })); } catch { if (!res.headersSent) { res.writeHead(500, { "content-type": "application/json", ...BLOB_CORS }); res.end(JSON.stringify({ error: "meta_failed" })); } } } function readBodyLimited(req, maxBytes) { return new Promise((resolve, reject) => { const chunks = []; let total = 0; let settled = false; let tooLarge = false; const finish = (value) => { if (settled) return; settled = true; resolve(value); }; req.on("data", (chunk) => { if (tooLarge) return; total += chunk.length; if (total > maxBytes) { tooLarge = true; chunks.length = 0; req.resume(); finish("too_large"); return; } chunks.push(chunk); }); req.on("end", () => { if (!settled) finish(Buffer.concat(chunks)); }); req.on("error", (err) => { if (!settled) { settled = true; reject(err); } }); }); } var HASH_RE, DEFAULT_MAX_BLOB_BYTES, BLOB_CORS; var init_blob_handler = __esm({ "src/realtime/blob-handler.ts"() { "use strict"; HASH_RE = /^[a-f0-9]{64}$/; DEFAULT_MAX_BLOB_BYTES = 64 * 1024 * 1024; BLOB_CORS = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, HEAD, PUT, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, If-None-Match, X-Trellis-Filename" }; } }); // src/realtime/relay-server.ts function isBlobRequestClaimed(req) { return Boolean(req[TRELLIS_BLOB_CLAIMED]); } function resolveBlobStore(blobStore) { if (blobStore === false || blobStore == null) return null; return blobStore(); } function blobHandlerOpts(opts) { return { maxBlobBytes: opts.maxBlobBytes, authorizeBlobWrite: opts.authorizeBlobWrite }; } function roomFromPath(reqPath, basePath) { const normalized = reqPath.replace(/\/+$/, "") || "/"; const base = basePath.replace(/\/+$/, "") || "/"; if (normalized === base) return DEFAULT_ROOM; const prefix = base === "/" ? "/" : `${base}/`; if (!reqPath.startsWith(prefix)) return null; const rest = reqPath.slice(prefix.length).split("?")[0].replace(/\/+$/, ""); return rest ? decodeURIComponent(rest) : DEFAULT_ROOM; } async function attachRealtimeRelay(server, opts = {}) { const path = opts.path ?? "/rt"; const graceMs = opts.replayGraceMs ?? REPLAY_GRACE_MS; const makePersistence = opts.persistence === false ? null : opts.persistence ?? (() => new RelayPersistence()); const store = resolveBlobStore(opts.blobStore); const handleBlob = store ? createBlobRequestHandler(store, blobHandlerOpts(opts)) : null; const onRequest = handleBlob ? (req, res) => { if (handleBlob(req, res)) { req[TRELLIS_BLOB_CLAIMED] = true; } } : null; if (onRequest) server.prependListener("request", onRequest); const Wss = opts.WebSocketServerImpl ?? (await import("ws")).WebSocketServer; const wss = new Wss({ noServer: true }); const rooms = /* @__PURE__ */ new Map(); const pendingRoom = /* @__PURE__ */ new WeakMap(); const roomState = (room) => { let st = rooms.get(room); if (!st) { st = { clients: /* @__PURE__ */ new Set(), persistence: makePersistence?.() ?? null }; rooms.set(room, st); } return st; }; const sendReplay = (ws, st) => { if (!st.persistence) return; const messages = st.persistence.buildReplay(); if (messages.length === 0) return; const frame = { v: 1, t: "replay", from: "relay", messages }; ws.send(JSON.stringify(frame)); }; wss.on("connection", (ws) => { const room = pendingRoom.get(ws) ?? DEFAULT_ROOM; pendingRoom.delete(ws); const st = roomState(room); st.clients.add(ws); const replaySent = { value: false }; const deliverReplay = () => { if (replaySent.value) return; replaySent.value = true; sendReplay(ws, st); }; const graceTimer = graceMs > 0 ? setTimeout(deliverReplay, graceMs) : null; graceTimer?.unref?.(); ws.on("message", (data, isBinary) => { const raw = isBinary ? data : String(data); let message; try { message = JSON.parse(String(raw)); } catch { return; } if (message?.v === 1) { if (message.t === "hello") deliverReplay(); st.persistence?.record(message); } for (const peer of st.clients) { if (peer === ws || peer.readyState !== peer.OPEN) continue; peer.send(String(raw)); } }); ws.on("close", () => { if (graceTimer) clearTimeout(graceTimer); st.clients.delete(ws); if (st.clients.size === 0) rooms.delete(room); }); }); const onUpgrade = (req, socket, head) => { const reqPath = (req.url ?? "").split("?")[0]; const room = roomFromPath(reqPath, path); if (room === null) return; wss.handleUpgrade(req, socket, head, (ws) => { pendingRoom.set(ws, room); wss.emit("connection", ws, req); }); }; server.on("upgrade", onUpgrade); return { clientCount: (room) => { if (room !== void 0) return rooms.get(room)?.clients.size ?? 0; let total = 0; for (const st of rooms.values()) total += st.clients.size; return total; }, rooms: () => [...rooms.keys()], persistenceFor: (room) => rooms.get(room)?.persistence ?? null, close: () => new Promise((resolve) => { server.off("upgrade", onUpgrade); if (onRequest) server.off("request", onRequest); for (const st of rooms.values()) { for (const ws of st.clients) { try { ws.close(); } catch { } } st.clients.clear(); } rooms.clear(); wss.close(() => resolve()); }) }; } async function createRealtimeRelay(opts = {}) { const { createServer } = await import("node:http"); const path = opts.path ?? "/rt"; const port = opts.port ?? 8231; const hostname = opts.hostname ?? "0.0.0.0"; const store = resolveBlobStore(opts.blobStore); const handleBlob = store ? createBlobRequestHandler(store, blobHandlerOpts(opts)) : null; const server = createServer((req, res) => { if (handleBlob?.(req, res)) return; const reqPath = (req.url ?? "/").split("?")[0]; if (reqPath === "/" || reqPath === "/health") { if (req.method === "OPTIONS") { res.writeHead(204, RELAY_HEALTH_CORS); res.end(); return; } if (req.method === "GET") { res.writeHead(200, { "content-type": "application/json", ...RELAY_HEALTH_CORS }); res.end(JSON.stringify({ ok: true, relay: path })); return; } } res.writeHead(404).end("not found"); }); const { blobStore: _blob, ...relayOpts } = opts; const relay = await attachRealtimeRelay(server, { ...relayOpts, blobStore: false }); await new Promise((resolve) => server.listen(port, hostname, resolve)); const addr = server.address(); const boundPort = typeof addr === "object" && addr ? addr.port : port; return { ...relay, port: boundPort, server, close: async () => { await relay.close(); await new Promise((resolve) => server.close(() => resolve())); } }; } var TRELLIS_BLOB_CLAIMED, REPLAY_GRACE_MS, DEFAULT_ROOM, RELAY_HEALTH_CORS; var init_relay_server = __esm({ "src/realtime/relay-server.ts"() { init_relay_persistence(); init_blob_handler(); init_blob_handler(); TRELLIS_BLOB_CLAIMED = Symbol.for("trellis.blobClaimed"); REPLAY_GRACE_MS = 250; DEFAULT_ROOM = "default"; RELAY_HEALTH_CORS = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS" }; } }); export { BLOB_CORS, createBlobRequestHandler, TRELLIS_BLOB_CLAIMED, isBlobRequestClaimed, attachRealtimeRelay, createRealtimeRelay, init_relay_server };