trellis
Version:
Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications
1,055 lines (1,042 loc) • 29 kB
JavaScript
import {
init_relay_persistence
} from "./chunk-KXAISKZT.js";
import {
Signal
} from "./chunk-UBGUDMUV.js";
// src/realtime/room.ts
var RealtimeRoom = class _RealtimeRoom {
id;
transport;
myState;
peers = /* @__PURE__ */ new Map();
/** Sender timestamps from presence messages (not local lastSeen). */
presenceTsByPeer = /* @__PURE__ */ new Map();
channelHandlers = /* @__PURE__ */ new Map();
_presence = new Signal([]);
now;
heartbeatMs;
timeoutMs;
heartbeatTimer;
unsubscribe;
closed = false;
pendingReplay = null;
/** Ids of broadcasts already delivered — keeps replay idempotent (G-Set). */
seenMsgIds = /* @__PURE__ */ new Set();
constructor(opts) {
this.transport = opts.transport;
this.id = opts.transport.id;
this.myState = opts.initialPresence ?? {};
this.now = opts.now ?? (() => Date.now());
this.heartbeatMs = opts.heartbeatMs ?? 2e3;
this.timeoutMs = opts.timeoutMs ?? 6e3;
this.unsubscribe = this.transport.onMessage((m) => this.handle(m));
this.recomputePresence();
}
/** Join a room and announce presence. */
static join(opts) {
const room = new _RealtimeRoom(opts);
room.announceHello();
room.announcePresence();
room.startHeartbeat();
return room;
}
// -------------------------------------------------------------------------
// Presence
// -------------------------------------------------------------------------
/** The local peer id. */
get selfId() {
return this.id;
}
/** Current local presence state. */
getSelfState() {
return this.myState;
}
/** Merge a partial update into local presence and broadcast it. */
setPresence(partial) {
this.myState = { ...this.myState, ...partial };
this.recomputePresence();
this.announcePresence();
}
/** Replace local presence wholesale and broadcast it. */
replacePresence(state) {
this.myState = state;
this.recomputePresence();
this.announcePresence();
}
/** All peers including self (self first). */
getPresence() {
return this._presence.value;
}
/** Peers excluding self. */
getOthers() {
return this._presence.value.filter((p) => !p.self);
}
/** Subscribe to presence changes. Called immediately with current peers. */
onPresence(cb) {
const unsub = this._presence.subscribe(cb);
this.flushPendingReplay();
return unsub;
}
/** Reactive presence signal (for framework adapters). */
get presenceSignal() {
return this._presence;
}
// -------------------------------------------------------------------------
// Broadcast pub/sub
// -------------------------------------------------------------------------
/**
* Fire-and-forget broadcast to all other peers on a channel. Returns the
* stable message id assigned to this broadcast — persist it alongside an
* optimistic local render so an echoed copy (relay replay, reconnect) is
* deduplicated by id rather than re-rendered.
*/
broadcast(channel, event, payload) {
const id = this.newMsgId();
if (this.closed) return id;
this.seenMsgIds.add(id);
this.transport.send({
v: 1,
t: "msg",
from: this.id,
channel,
event,
payload,
ts: this.now(),
id
});
return id;
}
newMsgId() {
const rand = typeof globalThis.crypto?.randomUUID === "function" ? globalThis.crypto.randomUUID() : Math.random().toString(36).slice(2);
return `${this.id}:${rand}`;
}
/** Subscribe to broadcasts on a channel. Returns an unsubscribe fn. */
on(channel, handler) {
let set = this.channelHandlers.get(channel);
if (!set) {
set = /* @__PURE__ */ new Set();
this.channelHandlers.set(channel, set);
}
set.add(handler);
this.flushPendingReplay();
return () => {
set.delete(handler);
};
}
// -------------------------------------------------------------------------
// Lifecycle
// -------------------------------------------------------------------------
/**
* Apply a relay replay batch (chat history, text snapshot, presence).
* Used when reconnecting to a hub with {@link RelayPersistence}.
*/
replay(messages) {
for (const message of messages) {
this.integrateRemote(message);
}
}
/** Announce departure and tear down. */
leave() {
if (this.closed) return;
this.closed = true;
try {
this.transport.send({ v: 1, t: "bye", from: this.id, ts: this.now() });
} catch {
}
if (this.heartbeatTimer !== void 0) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = void 0;
}
this.unsubscribe();
this.transport.close();
}
// -------------------------------------------------------------------------
// Internal
// -------------------------------------------------------------------------
handle(message) {
if (this.closed) return;
if (message.t === "replay") {
this.pendingReplay = message.messages;
this.flushPendingReplay();
return;
}
if (message.from === this.id) return;
this.integrateRemote(message);
}
flushPendingReplay() {
if (!this.pendingReplay || this.pendingReplay.length === 0) return;
if (!this.hasActiveSubscribers()) return;
const batch = this.pendingReplay;
this.pendingReplay = null;
this.replay(batch);
}
hasActiveSubscribers() {
if (this._presence.hasSubscribers()) return true;
for (const handlers of this.channelHandlers.values()) {
if (handlers.size > 0) return true;
}
return false;
}
integrateRemote(message) {
if (message.from === this.id) return;
switch (message.t) {
case "hello":
this.announcePresence();
break;
case "presence":
this.upsertPeer(message.from, message.state, message.ts);
break;
case "bye":
if (this.peers.delete(message.from)) {
this.presenceTsByPeer.delete(message.from);
this.recomputePresence();
}
break;
case "msg": {
if (message.id !== void 0 && this.seenMsgIds.has(message.id)) return;
const handlers = this.channelHandlers.get(message.channel);
if (!handlers || handlers.size === 0) return;
if (message.id !== void 0) this.seenMsgIds.add(message.id);
const event = {
from: message.from,
channel: message.channel,
event: message.event,
payload: message.payload,
ts: message.ts,
id: message.id
};
for (const handler of handlers) {
try {
handler(event);
} catch {
}
}
break;
}
case "replay":
break;
}
}
upsertPeer(id, state, ts) {
const prevTs = this.presenceTsByPeer.get(id) ?? 0;
if (ts < prevTs) {
const existing = this.peers.get(id);
if (existing) existing.lastSeen = this.now();
return;
}
this.presenceTsByPeer.set(id, ts);
this.peers.set(id, { id, state, lastSeen: this.now(), self: false });
this.recomputePresence();
}
announceHello() {
this.transport.send({ v: 1, t: "hello", from: this.id });
}
announcePresence() {
this.transport.send({
v: 1,
t: "presence",
from: this.id,
state: this.myState,
ts: this.now()
});
}
startHeartbeat() {
if (this.heartbeatMs <= 0 || typeof setInterval !== "function") {
return;
}
this.heartbeatTimer = setInterval(() => {
this.announcePresence();
this.pruneExpired();
}, this.heartbeatMs);
}
pruneExpired() {
const cutoff = this.now() - this.timeoutMs;
let changed = false;
for (const [id, peer] of this.peers) {
if (peer.lastSeen < cutoff) {
this.peers.delete(id);
this.presenceTsByPeer.delete(id);
changed = true;
}
}
if (changed) this.recomputePresence();
}
recomputePresence() {
this.peers.delete(this.id);
const self = {
id: this.id,
state: this.myState,
lastSeen: this.now(),
self: true
};
const others = [...this.peers.values()].sort(
(a, b) => a.id.localeCompare(b.id)
);
this._presence.value = [self, ...others];
}
};
// src/realtime/memory-hub.ts
var MemoryHub = class {
transports = /* @__PURE__ */ new Set();
/** Create a transport bound to this hub for the given peer id. */
connect(id) {
const transport = new MemoryRealtimeTransport(this, id);
this.transports.add(transport);
return transport;
}
/** Number of currently connected transports. */
size() {
return this.transports.size;
}
/** @internal */
_broadcast(from, message) {
for (const transport of this.transports) {
if (transport === from) continue;
transport._deliver(message);
}
}
/** @internal */
_remove(transport) {
this.transports.delete(transport);
}
};
var MemoryRealtimeTransport = class {
id;
hub;
handlers = /* @__PURE__ */ new Set();
closed = false;
constructor(hub, id) {
this.hub = hub;
this.id = id;
}
send(message) {
if (this.closed) return;
this.hub._broadcast(this, message);
}
onMessage(handler) {
this.handlers.add(handler);
return () => {
this.handlers.delete(handler);
};
}
close() {
if (this.closed) return;
this.closed = true;
this.hub._remove(this);
this.handlers.clear();
}
/** @internal */
_deliver(message) {
if (this.closed) return;
for (const handler of this.handlers) {
try {
handler(message);
} catch {
}
}
}
};
// src/realtime/broadcast-channel-transport.ts
var BroadcastChannelTransport = class {
id;
bc;
handlers = /* @__PURE__ */ new Set();
closed = false;
constructor(opts) {
this.id = opts.id;
const Impl = opts.BroadcastChannelImpl ?? globalThis.BroadcastChannel;
if (!Impl) {
throw new Error(
"BroadcastChannelTransport requires BroadcastChannel or opts.BroadcastChannelImpl."
);
}
this.bc = new Impl(opts.channel);
const onIncoming = (event) => {
const message = event.data;
if (!message || typeof message !== "object" || message.v !== 1) return;
for (const handler of this.handlers) {
try {
handler(message);
} catch {
}
}
};
if (this.bc.addEventListener) {
this.bc.addEventListener("message", onIncoming);
} else {
this.bc.onmessage = onIncoming;
}
}
send(message) {
if (this.closed) return;
this.bc.postMessage(message);
}
onMessage(handler) {
this.handlers.add(handler);
return () => {
this.handlers.delete(handler);
};
}
close() {
if (this.closed) return;
this.closed = true;
this.handlers.clear();
try {
this.bc.close();
} catch {
}
}
};
// src/realtime/websocket-relay-transport.ts
var WebSocketRelayTransport = class {
id;
ws;
handlers = /* @__PURE__ */ new Set();
closed = false;
pending = [];
constructor(opts) {
this.id = opts.id;
const WS = opts.WebSocketImpl ?? globalThis.WebSocket;
if (!WS) {
throw new Error(
"WebSocketRelayTransport requires WebSocket or opts.WebSocketImpl."
);
}
this.ws = new WS(opts.url);
this.ws.addEventListener("open", () => this.flushPending());
this.ws.addEventListener("message", (event) => {
let message;
try {
message = JSON.parse(String(event.data));
} catch {
return;
}
if (!message || typeof message !== "object" || message.v !== 1) return;
for (const handler of this.handlers) {
try {
handler(message);
} catch {
}
}
});
this.ws.addEventListener("close", () => {
if (!this.closed) this.pending = [];
});
}
send(message) {
if (this.closed) return;
if (this.ws.readyState === 1) {
this.ws.send(JSON.stringify(message));
return;
}
this.pending.push(message);
}
onMessage(handler) {
this.handlers.add(handler);
return () => {
this.handlers.delete(handler);
};
}
close() {
if (this.closed) return;
this.closed = true;
this.pending = [];
this.handlers.clear();
try {
this.ws.close();
} catch {
}
}
flushPending() {
if (this.ws.readyState !== 1) return;
const batch = this.pending.splice(0);
for (const message of batch) {
this.ws.send(JSON.stringify(message));
}
}
};
// src/realtime/durable-object-relay-transport.ts
var DEFAULT_RECONNECT = {
maxAttempts: 0,
baseDelayMs: 500,
maxDelayMs: 1e4
};
function defaultBuildUrl(opts) {
let url = opts.url;
if (opts.room) {
url = `${url.replace(/\/$/, "")}/${encodeURIComponent(opts.room)}`;
}
if (opts.auth) {
url += `${url.includes("?") ? "&" : "?"}token=${encodeURIComponent(opts.auth)}`;
}
return url;
}
var DurableObjectRelayTransport = class {
id;
url;
WS;
reconnect;
maxPending;
ws = null;
handlers = /* @__PURE__ */ new Set();
pending = [];
closed = false;
attempts = 0;
reconnectTimer = null;
constructor(opts) {
this.id = opts.id;
const WS = opts.WebSocketImpl ?? globalThis.WebSocket;
if (!WS) {
throw new Error(
"DurableObjectRelayTransport requires WebSocket or opts.WebSocketImpl."
);
}
this.WS = WS;
this.url = (opts.buildUrl ?? defaultBuildUrl)({
url: opts.url,
room: opts.room,
auth: opts.auth
});
this.reconnect = opts.reconnect === false ? null : { ...DEFAULT_RECONNECT, ...opts.reconnect ?? {} };
this.maxPending = opts.maxPending ?? 128;
this.open();
}
send(message) {
if (this.closed) return;
if (this.ws && this.ws.readyState === 1) {
this.ws.send(JSON.stringify(message));
return;
}
this.pending.push(message);
if (this.pending.length > this.maxPending) {
this.pending.splice(0, this.pending.length - this.maxPending);
}
}
onMessage(handler) {
this.handlers.add(handler);
return () => {
this.handlers.delete(handler);
};
}
close() {
if (this.closed) return;
this.closed = true;
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
this.pending = [];
this.handlers.clear();
try {
this.ws?.close();
} catch {
}
this.ws = null;
}
open() {
if (this.closed) return;
const ws = new this.WS(this.url);
this.ws = ws;
ws.addEventListener("open", () => {
if (this.closed) return;
this.attempts = 0;
try {
ws.send(JSON.stringify({ v: 1, t: "hello", from: this.id }));
} catch {
}
this.flushPending();
});
ws.addEventListener("message", (event) => {
const data = event.data;
let message;
try {
message = JSON.parse(String(data));
} catch {
return;
}
if (!message || typeof message !== "object" || message.v !== 1) return;
for (const handler of this.handlers) {
try {
handler(message);
} catch {
}
}
});
ws.addEventListener("close", () => {
if (this.ws === ws) this.ws = null;
this.scheduleReconnect();
});
ws.addEventListener("error", () => {
});
}
scheduleReconnect() {
if (this.closed || !this.reconnect || this.reconnectTimer) return;
if (this.reconnect.maxAttempts > 0 && this.attempts >= this.reconnect.maxAttempts) {
return;
}
const delay = Math.min(
this.reconnect.baseDelayMs * 2 ** this.attempts,
this.reconnect.maxDelayMs
);
this.attempts += 1;
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this.open();
}, delay);
this.reconnectTimer?.unref?.();
}
flushPending() {
if (!this.ws || this.ws.readyState !== 1) return;
const batch = this.pending.splice(0);
for (const message of batch) {
this.ws.send(JSON.stringify(message));
}
}
};
// src/realtime/presence.ts
function createPresenceTransport(opts) {
if (opts.transport) return opts.transport;
if (opts.relayUrl) {
return new DurableObjectRelayTransport({
id: opts.peerId,
url: opts.relayUrl,
room: opts.room,
auth: opts.auth,
WebSocketImpl: opts.WebSocketImpl
});
}
return new BroadcastChannelTransport({
id: opts.peerId,
channel: `presence:${opts.room}`,
BroadcastChannelImpl: opts.BroadcastChannelImpl
});
}
function joinPresence(opts) {
return RealtimeRoom.join({
transport: createPresenceTransport(opts),
initialPresence: opts.initialPresence,
heartbeatMs: opts.heartbeatMs,
timeoutMs: opts.timeoutMs,
now: opts.now
});
}
// src/realtime/index.ts
init_relay_persistence();
// src/realtime/persistent-channel.ts
var DEFAULT_MAX_RECORDS = 200;
var PersistentChannel = class _PersistentChannel {
/** Reactive, deduped, `(ts, id)`-sorted message list. */
messages = new Signal([]);
room;
channel;
event;
store;
max;
resolveMeta;
now;
records = [];
byId = /* @__PURE__ */ new Map();
unsubscribe;
disposed = false;
constructor(room, channel, opts) {
this.room = room;
this.channel = channel;
this.event = opts.event ?? "message";
this.store = opts.store;
this.max = opts.max ?? DEFAULT_MAX_RECORDS;
this.resolveMeta = opts.resolveMeta;
this.now = opts.now ?? (() => Date.now());
this.unsubscribe = this.room.on(this.channel, this.handleEvent);
void this.hydrate();
}
/** Create a persistent view over `room`'s `channel`. */
static create(room, channel, opts = {}) {
return new _PersistentChannel(room, channel, opts);
}
/**
* Optimistically record locally, broadcast to peers, and persist. Returns the
* stable message id. The room dedups its own id, so the broadcast never
* echoes back into this channel.
*/
send(payload, meta) {
const id = this.room.broadcast(this.channel, this.event, payload);
this.commitOne({ id, from: this.room.selfId, ts: this.now(), payload, meta });
return id;
}
/** Current records (deduped + sorted). Read without subscribing. */
snapshot() {
return this.messages.peek();
}
/** Stop listening. Does not clear the durable store. */
dispose() {
if (this.disposed) return;
this.disposed = true;
this.unsubscribe();
}
// --- internal --------------------------------------------------------------
handleEvent = (event) => {
if (event.event !== this.event) return;
this.commitOne({
id: event.id ?? `${event.from}:${event.ts}`,
from: event.from,
ts: event.ts,
payload: event.payload,
meta: this.resolveMeta?.(event)
});
};
async hydrate() {
if (!this.store) return;
let loaded;
try {
loaded = await this.store.load();
} catch {
return;
}
if (this.disposed) return;
let changed = false;
for (const rec of loaded) {
if (this.add(rec)) changed = true;
}
if (changed) this.commit();
}
/** Insert one record and publish if it was new. */
commitOne(rec) {
if (this.add(rec)) this.commit();
}
/** Pure insert with dedup + cap. Returns true when a new record was added. */
add(rec) {
if (!rec || !rec.id || this.byId.has(rec.id)) return false;
this.byId.set(rec.id, rec);
this.records.push(rec);
this.records.sort((a, b) => a.ts - b.ts || a.id.localeCompare(b.id));
if (this.records.length > this.max) {
const evicted = this.records.splice(0, this.records.length - this.max);
for (const m of evicted) this.byId.delete(m.id);
}
return true;
}
commit() {
this.messages.value = [...this.records];
void this.store?.save(this.records);
}
};
function localStorageChannelStore(key, storage = globalThis.localStorage) {
return {
load() {
try {
const raw = storage?.getItem(key);
const parsed = raw ? JSON.parse(raw) : [];
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
},
save(records) {
try {
storage?.setItem(key, JSON.stringify(records));
} catch {
}
}
};
}
// src/realtime/text.ts
function parseId(id) {
const at = id.indexOf("@");
return {
counter: Number(id.slice(0, at)),
peer: id.slice(at + 1)
};
}
function compareSiblings(a, b) {
const pa = parseId(a);
const pb = parseId(b);
if (pa.counter !== pb.counter) return pb.counter - pa.counter;
return pb.peer.localeCompare(pa.peer);
}
var RealtimeText = class {
nodes = /* @__PURE__ */ new Map();
counter = 0;
peerId;
listeners = /* @__PURE__ */ new Set();
room;
channel;
unsubscribe;
constructor(opts) {
this.peerId = opts.peerId;
this.room = opts.room;
this.channel = opts.channel ?? "text";
if (this.room) {
this.unsubscribe = this.room.on(this.channel, (e) => {
if (e.from === this.peerId) return;
this.onRemote(e.event, e.payload);
});
this.room.broadcast(this.channel, "state-req", {});
}
}
/** Materialized visible text. */
toString() {
return this.visibleNodes().map((n) => n.ch).join("");
}
/** Current length of visible text. */
get length() {
return this.visibleNodes().length;
}
/** Insert a string at a visible index. Broadcasts ops when room-bound. */
insert(index, str) {
if (str.length === 0) return [];
const visible = this.visibleNodes();
let after = index <= 0 ? null : visible[index - 1]?.id ?? null;
const ops = [];
for (const ch of str) {
const id = this.nextId();
const node = { id, ch, after, deleted: false };
this.nodes.set(id, node);
ops.push({ op: "ins", id, ch, after });
after = id;
}
this.emit();
this.publish(ops);
return ops;
}
/** Delete `count` visible characters starting at `index`. */
delete(index, count = 1) {
if (count <= 0) return [];
const visible = this.visibleNodes();
const ops = [];
for (let k = 0; k < count; k++) {
const target = visible[index + k];
if (!target) break;
const node = this.nodes.get(target.id);
if (node && !node.deleted) {
node.deleted = true;
ops.push({ op: "del", id: node.id });
}
}
if (ops.length > 0) {
this.emit();
this.publish(ops);
}
return ops;
}
/** Apply a single remote op. Returns true if the document changed. */
applyOp(op) {
if (op.op === "ins") {
if (this.nodes.has(op.id)) return false;
this.bumpCounter(op.id);
this.nodes.set(op.id, {
id: op.id,
ch: op.ch,
after: op.after,
deleted: false
});
return true;
}
const node = this.nodes.get(op.id);
if (!node || node.deleted) return false;
node.deleted = true;
return true;
}
/** Apply a batch of remote ops, emitting once. */
applyOps(ops) {
let changed = false;
for (const op of ops) {
if (this.applyOp(op)) changed = true;
}
if (changed) this.emit();
}
/** Snapshot of all nodes (including tombstones) for state sync. */
getNodes() {
return [...this.nodes.values()];
}
/** Merge a remote snapshot of nodes. */
mergeNodes(nodes) {
let changed = false;
for (const incoming of nodes) {
this.bumpCounter(incoming.id);
const existing = this.nodes.get(incoming.id);
if (!existing) {
this.nodes.set(incoming.id, { ...incoming });
changed = true;
} else if (incoming.deleted && !existing.deleted) {
existing.deleted = true;
changed = true;
}
}
if (changed) this.emit();
}
/** Subscribe to text changes. Returns an unsubscribe fn. */
onChange(cb) {
this.listeners.add(cb);
return () => {
this.listeners.delete(cb);
};
}
/** Detach room bindings. */
dispose() {
this.unsubscribe?.();
this.listeners.clear();
}
// -------------------------------------------------------------------------
// Internal
// -------------------------------------------------------------------------
onRemote(event, payload) {
switch (event) {
case "op":
this.applyOps(payload);
break;
case "state-req":
this.room?.broadcast(this.channel, "state", this.getNodes());
break;
case "state":
this.mergeNodes(payload);
break;
}
}
publish(ops) {
this.room?.broadcast(this.channel, "op", ops);
}
visibleNodes() {
const children = /* @__PURE__ */ new Map();
for (const node of this.nodes.values()) {
const list = children.get(node.after);
if (list) list.push(node);
else children.set(node.after, [node]);
}
for (const list of children.values()) {
list.sort((a, b) => compareSiblings(a.id, b.id));
}
const result = [];
const stack = [...children.get(null) ?? []].reverse();
while (stack.length > 0) {
const node = stack.pop();
if (!node.deleted) result.push(node);
const kids = children.get(node.id);
if (kids) {
for (let i = kids.length - 1; i >= 0; i--) stack.push(kids[i]);
}
}
return result;
}
nextId() {
this.counter += 1;
return `${this.counter}@${this.peerId}`;
}
bumpCounter(id) {
const { counter } = parseId(id);
if (Number.isFinite(counter) && counter > this.counter) {
this.counter = counter;
}
}
emit() {
const text = this.toString();
for (const cb of this.listeners) {
try {
cb(text);
} catch {
}
}
}
};
// src/realtime/types.ts
var REALTIME_PROTOCOL = 1;
// src/realtime/blob-client.ts
var HASH_RE = /^[a-f0-9]{64}$/;
function createBlobClient(opts) {
const baseUrl = opts.baseUrl.replace(/\/$/, "");
const doFetch = opts.fetch ?? fetch;
const verify = opts.verify ?? false;
return {
async get(hash) {
assertHash(hash);
const res = await doFetch(`${baseUrl}/blob/${hash}`);
if (res.status === 404) return null;
if (!res.ok) {
throw new Error(`Blob fetch failed: ${res.status} ${res.statusText}`);
}
const buf = await res.arrayBuffer();
if (verify) {
const actual = await sha256Hex(buf);
if (actual !== hash) {
throw new Error(
`Blob integrity check failed: expected ${hash}, got ${actual}`
);
}
}
return buf;
},
async has(hash) {
assertHash(hash);
const res = await doFetch(`${baseUrl}/blob/${hash}`, { method: "HEAD" });
if (res.status === 404) return false;
if (!res.ok) {
throw new Error(`Blob HEAD failed: ${res.status} ${res.statusText}`);
}
return true;
},
async put(bytes, meta) {
const body = bytes instanceof Uint8Array ? bytes : bytes instanceof ArrayBuffer ? new Uint8Array(bytes) : bytes;
const headers = {
"Content-Type": meta?.contentType?.trim() || "application/octet-stream"
};
if (meta?.name?.trim()) {
headers["X-Trellis-Filename"] = meta.name.trim().slice(0, 255);
}
const res = await doFetch(`${baseUrl}/blob`, {
method: "PUT",
headers,
body
});
if (!res.ok) {
throw new Error(`Blob upload failed: ${res.status} ${res.statusText}`);
}
const json = await res.json();
if (!json.hash || !HASH_RE.test(json.hash)) {
throw new Error("Blob upload response missing valid hash");
}
return json.hash;
},
async list() {
const res = await doFetch(`${baseUrl}/blob`);
if (!res.ok) {
throw new Error(`Blob list failed: ${res.status} ${res.statusText}`);
}
const json = await res.json();
if (!Array.isArray(json.blobs)) {
throw new Error("Blob list response missing blobs[]");
}
return json.blobs.filter(
(b) => typeof b.hash === "string" && HASH_RE.test(b.hash) && typeof b.size === "number"
).map((b) => ({
hash: b.hash,
size: b.size,
...typeof b.name === "string" ? { name: b.name } : {},
...typeof b.contentType === "string" ? { contentType: b.contentType } : {},
...typeof b.uploadedAt === "number" ? { uploadedAt: b.uploadedAt } : {}
}));
}
};
}
function assertHash(hash) {
if (!HASH_RE.test(hash)) {
throw new Error(`Invalid blob hash: ${hash}`);
}
}
async function sha256Hex(buf) {
const digest = await crypto.subtle.digest("SHA-256", buf);
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
}
export {
RealtimeRoom,
MemoryHub,
MemoryRealtimeTransport,
BroadcastChannelTransport,
WebSocketRelayTransport,
DurableObjectRelayTransport,
createPresenceTransport,
joinPresence,
DEFAULT_MAX_RECORDS,
PersistentChannel,
localStorageChannelStore,
RealtimeText,
REALTIME_PROTOCOL,
createBlobClient
};