trellis
Version:
Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications
212 lines (209 loc) • 6.27 kB
JavaScript
import {
bindingEntityId,
bindingToEntity,
entitiesQuery,
entityQuery,
isSparseBinding,
resolveRelations
} from "./chunk-JUEMDWLU.js";
import {
Signal
} from "./chunk-UBGUDMUV.js";
// src/client/live.ts
function liveQuery(client, eql) {
const signal = new Signal({
data: [],
loading: true,
error: null
});
let sub = null;
return {
signal,
start() {
if (sub) return () => stop();
try {
sub = client.subscribe(eql, (rows) => {
signal.value = { data: rows, loading: false, error: null };
});
} catch (err) {
signal.value = {
data: [],
loading: false,
error: err instanceof Error ? err : new Error(String(err))
};
}
return () => stop();
}
};
function stop() {
sub?.unsubscribe();
sub = null;
}
}
function normalizeLiveEntitiesOpts(whereOrOpts) {
if (!whereOrOpts) return {};
if ("resolve" in whereOrOpts || "where" in whereOrOpts) {
return whereOrOpts;
}
return { where: whereOrOpts };
}
function liveEntities(client, typeOrSchema, whereOrOpts) {
const schema = typeof typeOrSchema === "string" ? void 0 : typeOrSchema;
const type = typeof typeOrSchema === "string" ? typeOrSchema : typeOrSchema.type;
const { where, resolve } = normalizeLiveEntitiesOpts(whereOrOpts);
const signal = new Signal({
data: [],
loading: true,
error: null
});
let sub = null;
let token = 0;
return {
signal,
start() {
if (sub) return () => stop();
try {
const serverResolve = schema && resolve && Object.keys(resolve).length > 0;
sub = client.subscribe(
entitiesQuery(type, where),
(rows, _diff, meta) => {
const turn = ++token;
const hydrate = async () => hydrateSubscriptionRows(client, rows, meta);
hydrate().then(async (entities) => {
if (turn !== token) return;
let data = entities;
if (schema && resolve && Object.keys(resolve).length > 0 && !meta?.resolved) {
data = await resolveRelations(
client,
schema,
data,
resolve
);
}
if (turn !== token) return;
signal.value = { data, loading: false, error: null };
}).catch((err) => {
if (turn !== token) return;
signal.value = {
data: signal.peek().data,
loading: false,
error: err instanceof Error ? err : new Error(String(err))
};
});
},
serverResolve ? { entityType: type, resolve } : void 0
);
} catch (err) {
signal.value = {
data: [],
loading: false,
error: err instanceof Error ? err : new Error(String(err))
};
}
return () => stop();
}
};
function stop() {
token++;
sub?.unsubscribe();
sub = null;
}
}
async function hydrateSubscriptionRows(client, rows, meta) {
if (rows.length === 0) return [];
if (meta?.resolved) return rows;
if (rows.some((r) => isSparseBinding(r))) {
const ids = rows.map((r) => bindingEntityId(r)).filter((id) => Boolean(id));
const loaded = await Promise.all(ids.map((id) => client.read(id)));
return loaded.filter((e) => e != null);
}
return rows.map((r) => bindingToEntity(r));
}
async function pickEntityFromRows(client, rows, entityId, meta) {
const hydrated = await hydrateSubscriptionRows(client, rows, meta);
const hit = hydrated.find((r) => r.id === entityId);
if (hit) return hit;
if (hydrated.length === 0) return client.read(entityId);
return null;
}
function liveEntity(client, typeOrSchema, id, opts) {
const schema = typeof typeOrSchema === "string" ? void 0 : typeOrSchema;
const type = typeof typeOrSchema === "string" ? typeOrSchema : typeOrSchema.type;
const { resolve } = opts ?? {};
const signal = new Signal({
data: null,
loading: Boolean(id),
error: null
});
let sub = null;
let token = 0;
return {
signal,
start() {
if (!id) {
signal.value = { data: null, loading: false, error: null };
return () => {
};
}
if (sub) return () => stop();
const entityId = id;
const serverResolve = Boolean(schema) && Boolean(resolve && Object.keys(resolve).length > 0);
const applyRow = async (row, turn) => {
if (turn !== token) return;
if (!row) {
signal.value = { data: null, loading: false, error: null };
return;
}
let data = row;
if (schema && resolve && Object.keys(resolve).length > 0 && !serverResolve) {
const [resolved] = await resolveRelations(client, schema, [data], resolve);
data = resolved ?? data;
}
if (turn !== token) return;
signal.value = { data, loading: false, error: null };
};
const readGen = token;
client.read(entityId).then((row) => applyRow(row, readGen)).catch((err) => {
signal.value = {
data: signal.peek().data,
loading: false,
error: err instanceof Error ? err : new Error(String(err))
};
});
try {
sub = client.subscribe(
entityQuery(type, entityId),
(rows, _diff, meta) => {
const turn = ++token;
pickEntityFromRows(client, rows, entityId, meta).then((row) => applyRow(row, turn)).catch((err) => {
if (turn !== token) return;
signal.value = {
data: signal.peek().data,
loading: false,
error: err instanceof Error ? err : new Error(String(err))
};
});
},
serverResolve ? { entityType: type, resolve } : void 0
);
} catch (err) {
signal.value = {
data: null,
loading: false,
error: err instanceof Error ? err : new Error(String(err))
};
}
return () => stop();
}
};
function stop() {
token++;
sub?.unsubscribe();
sub = null;
}
}
export {
liveQuery,
liveEntities,
liveEntity
};