chore(ts): consolidate effect native closeout

This commit is contained in:
elpresidank 2026-06-11 08:34:59 -05:00
parent cd6c9107d7
commit fab718dce8
21 changed files with 199 additions and 3533 deletions

View file

@ -75,12 +75,12 @@ export const Field = S.Struct({
});
export type Field = typeof Field.Type;
export const RowSchema = S.Struct({
export const Row = S.Struct({
name: S.String,
description: S.optionalKey(S.String),
fields: S.Array(Field).pipe(S.mutable),
});
export type RowSchema = typeof RowSchema.Type;
export type Row = typeof Row.Type;
export const LlmResult = S.Struct({
text: S.String,

View file

@ -6,8 +6,13 @@ export * from "./models/Triple.js";
export * from "./models/messages.js";
export * from "./models/namespaces.js";
// Export socket client
export * from "./socket/trustgraph-socket.js";
// Export retained compatibility types from the legacy socket shim.
export type {
ConnectionState,
ExplainEvent,
GraphRagOptions,
StreamingMetadata,
} from "./socket/trustgraph-socket.js";
export * from "./socket/effect-rpc-client.js";
export * from "./rpc/contract.js";

View file

@ -7,7 +7,7 @@ import type {
RpcConnectionState,
} from "./effect-rpc-client.js";
import { getDefaultSocketUrl, getRandomValues } from "./websocket-adapter.js";
import { Match, Option, Schema as S } from "effect";
import { Array as A, Match, Option, Order, Schema as S } from "effect";
import * as Predicate from "effect/Predicate";
// Import all message types for different services
@ -1240,9 +1240,7 @@ export function makeFlowsApi(api: BaseApi) {
return this.getConfigAll().then((r) => {
const config = r as { config?: { prompt?: Record<string, unknown> } };
const promptNs = config.config?.prompt ?? {};
return Object.keys(promptNs)
.filter((k) => k !== "system")
.sort()
return A.sort(Object.keys(promptNs).filter((k) => k !== "system"), Order.String)
.map((id) => ({ id, name: id }));
});
},
@ -2204,9 +2202,7 @@ export function makeConfigApi(api: BaseApi) {
return this.getConfigAll().then((r) => {
const config = r as { config?: { prompt?: Record<string, unknown> } };
const promptNs = config.config?.prompt ?? {};
return Object.keys(promptNs)
.filter((k) => k !== "system")
.sort()
return A.sort(Object.keys(promptNs).filter((k) => k !== "system"), Order.String)
.map((id) => ({ id, name: id }));
});
},

View file

@ -5,7 +5,7 @@
*/
import {NodeRuntime} from "@effect/platform-node";
import {Duration, Effect, HashMap, Match, Option, SynchronizedRef} from "effect";
import {Array as A, Duration, Effect, HashMap, Match, Option, Order, SynchronizedRef} from "effect";
import * as Predicate from "effect/Predicate";
import * as S from "effect/Schema";
import type {
@ -122,26 +122,27 @@ const initialState = (): ConfigServiceState => ({
const getHashMapValue = <K, V>(store: HashMap.HashMap<K, V>, key: K): V | undefined =>
Option.getOrUndefined(HashMap.get(store, key));
const compareText = (left: string, right: string): number =>
left.localeCompare(right);
const compareWorkspace = (left: string, right: string): number =>
const workspaceOrder = Order.make<string>((left, right) =>
left === right
? 0
: left === DEFAULT_WORKSPACE
? -1
: right === DEFAULT_WORKSPACE
? 1
: compareText(left, right);
: Order.String(left, right)
);
const orderByKey = <A>(order: Order.Order<string>): Order.Order<readonly [string, A]> =>
Order.mapInput(order, ([key]) => key);
const workspaceEntries = (store: ConfigStore): ReadonlyArray<readonly [string, WorkspaceStore]> =>
HashMap.toEntries(store).sort(([left], [right]) => compareWorkspace(left, right));
A.sort(HashMap.toEntries(store), orderByKey<WorkspaceStore>(workspaceOrder));
const namespaceEntries = (store: WorkspaceStore): ReadonlyArray<readonly [string, NamespaceStore]> =>
HashMap.toEntries(store).sort(([left], [right]) => compareText(left, right));
A.sort(HashMap.toEntries(store), orderByKey<NamespaceStore>(Order.String));
const valueEntries = (store: NamespaceStore): ReadonlyArray<readonly [string, unknown]> =>
HashMap.toEntries(store).sort(([left], [right]) => compareText(left, right));
A.sort(HashMap.toEntries(store), orderByKey<unknown>(Order.String));
const toPersistedWorkspaces = (
store: ConfigStore,

View file

@ -29,7 +29,7 @@ import {
processorLifecycleError,
topics,
} from "@trustgraph/base";
import {Duration, Effect, HashMap, Match, SynchronizedRef} from "effect";
import {Array as A, Duration, Effect, HashMap, Match, Order, SynchronizedRef} from "effect";
import * as O from "effect/Option";
import * as S from "effect/Schema";
import {ensureDirectoryEffect, joinPath, readTextFileEffect, writeTextFileEffect} from "../runtime/effect-files.js";
@ -137,8 +137,11 @@ const cloneKnowledgeCore = (core: KnowledgeCore): KnowledgeCore => ({
})),
});
const sortedEntries = <A>(store: HashMap.HashMap<string, A>): ReadonlyArray<readonly [string, A]> =>
HashMap.toEntries(store).sort(([left], [right]) => left.localeCompare(right));
const sortedEntries = <Value>(store: HashMap.HashMap<string, Value>): ReadonlyArray<readonly [string, Value]> =>
A.sort(
HashMap.toEntries(store),
Order.make<readonly [string, Value]>(([left], [right]) => Order.String(left, right)),
);
const toPersistedSnapshot = (state: KnowledgeCoreServiceState): PersistedKnowledgeSnapshot => {
const kg: Record<string, KnowledgeCore> = {};

View file

@ -37,7 +37,7 @@ import {
import { makeProcessorProgram } from "@trustgraph/base";
import type { Message } from "@trustgraph/base";
import { NodeRuntime } from "@effect/platform-node";
import { Duration, Effect, HashMap, Match, Option, SynchronizedRef } from "effect";
import { Array as A, Duration, Effect, HashMap, Match, Option, Order, SynchronizedRef } from "effect";
import * as S from "effect/Schema";
// ---------- Internal state types ----------
@ -236,8 +236,11 @@ const isStringRecord = (value: unknown): value is Record<string, string> =>
const getHashMapValue = <K, V>(store: HashMap.HashMap<K, V>, key: K): V | undefined =>
Option.getOrUndefined(HashMap.get(store, key));
const sortedEntries = <A>(store: HashMap.HashMap<string, A>): ReadonlyArray<readonly [string, A]> =>
HashMap.toEntries(store).sort(([left], [right]) => left.localeCompare(right));
const sortedEntries = <Value>(store: HashMap.HashMap<string, Value>): ReadonlyArray<readonly [string, Value]> =>
A.sort(
HashMap.toEntries(store),
Order.make<readonly [string, Value]>(([left], [right]) => Order.String(left, right)),
);
const sortedKeys = <A>(store: HashMap.HashMap<string, A>): Array<string> =>
sortedEntries(store).map(([key]) => key);

View file

@ -35,7 +35,7 @@ import {
} from "@trustgraph/base";
import type { Message } from "@trustgraph/base";
import { NodeRuntime } from "@effect/platform-node";
import { Clock, Config, DateTime, Duration, Effect, Match, Option, Random, SynchronizedRef } from "effect";
import { Clock, Config, DateTime, Duration, Effect, Match, Option, Order, Random, SynchronizedRef } from "effect";
import * as A from "effect/Array";
import * as MutableHashMap from "effect/MutableHashMap";
import * as S from "effect/Schema";
@ -479,7 +479,7 @@ export function makeLibrarianService(config: LibrarianServiceConfig): LibrarianS
if (session === undefined) {
return yield* librarianServiceError("get-upload-status", `Upload not found: ${uploadId}`);
}
const receivedChunks = Array.from(MutableHashMap.keys(session.chunks)).sort((a, b) => a - b);
const receivedChunks = A.sort(Array.from(MutableHashMap.keys(session.chunks)), Order.Number);
const receivedSet = new Set(receivedChunks);
const missingChunks = Array.from({ length: session.totalChunks }, (_, i) => i).filter((i) => !receivedSet.has(i));
return {

View file

@ -20,7 +20,7 @@ import type {
TriplesQueryResponse,
} from "@trustgraph/base";
import { Triple, errorMessage } from "@trustgraph/base";
import { Context, Effect, Layer, Match } from "effect";
import { Array as A, Context, Effect, Layer, Match, Order } from "effect";
import * as O from "effect/Option";
import * as S from "effect/Schema";
@ -348,8 +348,10 @@ const scoreEdges = Effect.fn("GraphRagEngine.scoreEdges")(function* (
yield* Effect.log(`[GraphRag] Edge scoring LLM response (first 500 chars): ${llmResp.response.slice(0, 500)}`);
const scored = parseScoredEdges(llmResp.response);
scored.sort((a, b) => b.score - a.score);
const scored = A.sort(
parseScoredEdges(llmResp.response),
Order.make<typeof ScoredEdge.Type>((left, right) => Order.Number(right.score, left.score)),
);
const topN = scored.slice(0, config.edgeLimit);
const result: Triple[] = [];

View file

@ -9,7 +9,7 @@ import type {
import {
makeTrustGraphGatewayClientScoped,
} from "@trustgraph/client";
import {Clock, Config, Context, Effect, Layer} from "effect";
import {Array as A, Clock, Config, Context, Effect, Layer, Order} from "effect";
import * as O from "effect/Option";
import * as Predicate from "effect/Predicate";
import {McpServer, Tool, Toolkit} from "effect/unstable/ai";
@ -1775,9 +1775,7 @@ export const TrustGraphMcpToolkitLive = TrustGraphMcpToolkit.toLayer(
).pipe(
Effect.map((response) => {
const promptNs = asRecord(asRecord(response.config).prompt)
const prompts = Object.keys(promptNs)
.filter((key) => key !== "system")
.sort()
const prompts = A.sort(Object.keys(promptNs).filter((key) => key !== "system"), Order.String)
.map((id) => ({id, name: id}))
return GetPromptsSuccess.make({prompts})
}),

View file

@ -19,8 +19,9 @@ import {
GatewayWorkbenchHttpApi,
TrustGraphRpcs,
} from "@trustgraph/client";
import type { WorkbenchQaApi } from "@/qa/mock-api";
import type { Scope, } from "effect";
import { Cause, Clock, Context, Effect, Layer, Match, Metric, Option, Random, Schema as S, Stream } from "effect";
import { Cause, Clock, Context, Effect, Layer, Match, Metric, Option, Order, Random, Schema as S, Stream } from "effect";
import * as A from "effect/Array";
import * as MutableHashMap from "effect/MutableHashMap";
import * as Predicate from "effect/Predicate";
@ -802,9 +803,9 @@ function ensureNoGatewayResponseError<A>(operation: string, value: A): Effect.Ef
: Effect.fail(WorkbenchPromiseError.make({ cause: value, message: `${operation}: ${message}` }));
}
function qaBaseApi(): import("@trustgraph/client").BaseApi | undefined {
function qaBaseApi(): WorkbenchQaApi | undefined {
if (typeof window === "undefined") return undefined;
return (window as Window & { __TRUSTGRAPH_WORKBENCH_QA_API__?: import("@trustgraph/client").BaseApi }).__TRUSTGRAPH_WORKBENCH_QA_API__;
return (window as Window & { __TRUSTGRAPH_WORKBENCH_QA_API__?: WorkbenchQaApi }).__TRUSTGRAPH_WORKBENCH_QA_API__;
}
function makeWorkbenchGatewayApi(settings: Settings) {
@ -897,9 +898,7 @@ function makeWorkbenchGatewayApi(settings: Settings) {
Effect.map((response) => {
const config = asJsonRecord(response.config);
const promptNs = asJsonRecord(config.prompt);
return Object.keys(promptNs)
.filter((key) => key !== "system")
.sort()
return A.sort(Object.keys(promptNs).filter((key) => key !== "system"), Order.String)
.map((id) => ({ id, name: id }));
}),
),
@ -1826,9 +1825,9 @@ function explainTriplesKey(input: ExplainTriplesInput): string {
const graphs = input.events
.map((event) => event.explainGraph ?? event.explainId ?? "")
.filter((id) => id.length > 0)
.sort()
const sortedGraphs = A.sort(graphs, Order.String)
.join(explainGraphSeparator);
return [input.flowId, input.collection, graphs].join(atomFamilyKeySeparator);
return [input.flowId, input.collection, sortedGraphs].join(atomFamilyKeySeparator);
}
const graphTriplesAtomByKey = Atom.family((key: string) => {

View file

@ -1,5 +1,6 @@
import { lazy, Suspense } from "react";
import { useAtom, useAtomValue } from "@effect/atom-react";
import { Array as A, Order } from "effect";
import { Network, ChevronRight, ChevronDown, Loader2 } from "lucide-react";
import * as Atom from "effect/unstable/reactivity/Atom";
import {
@ -74,7 +75,7 @@ export function ExplainGraph({ explainEvents, collection }: ExplainGraphProps) {
const loading = expanded && resultLoading(result, triples);
const error = resultError(result);
const { data: graphData, typeMap } = triplesToGraph(triples);
const uniqueTypes = Array.from(new Set(Array.from(typeMap.values()).map(localName))).sort();
const uniqueTypes = A.sort(Array.from(new Set(Array.from(typeMap.values()).map(localName))), Order.String);
return (
<div className="mt-2 rounded-md border border-border/50">

View file

@ -1,5 +1,6 @@
import { lazy, Suspense } from "react";
import { useAtom, useAtomRefresh, useAtomValue } from "@effect/atom-react";
import { Array as A, Order } from "effect";
import {
Rotate3d,
Search,
@ -180,7 +181,7 @@ export default function GraphPage() {
const selectedNode = view.selectedNodeId !== null
? data.nodes.find((node) => node.id === view.selectedNodeId)
: undefined;
const uniqueTypes = Array.from(new Set(Array.from(typeMap.values()).map(localName))).sort();
const uniqueTypes = A.sort(Array.from(new Set(Array.from(typeMap.values()).map(localName))), Order.String);
return (
<div className="flex h-full flex-col">

View file

@ -8,7 +8,7 @@ import {
flowIdAtom,
settingsAtom,
} from "@/atoms/workbench";
import type { BaseApi } from "@trustgraph/client";
import type { WorkbenchQaApi } from "@/qa/mock-api";
import { MockWorkbenchFixture, makeMockBaseApi, qaSettingsFromFixture, } from "@/qa/mock-api";
import { Schema as S } from "effect";
@ -21,7 +21,7 @@ export class WorkbenchQaWindowConfig extends S.Class<WorkbenchQaWindowConfig>("W
declare global {
interface Window {
__TRUSTGRAPH_WORKBENCH_QA__?: WorkbenchQaWindowConfig;
__TRUSTGRAPH_WORKBENCH_QA_API__?: BaseApi;
__TRUSTGRAPH_WORKBENCH_QA_API__?: WorkbenchQaApi;
}
}

View file

@ -1,9 +1,26 @@
import type { BaseApi, DocumentMetadata, ProcessingMetadata, StreamingMetadata, Triple } from "@trustgraph/client";
import { makeBaseApiWithRpc, } from "@trustgraph/client";
import { Match, Option, Schema as S } from "effect";
import type { DocumentMetadata, ProcessingMetadata, StreamingMetadata, Triple } from "@trustgraph/client";
import { Array as A, Match, Option, Order, Schema as S } from "effect";
type ConfigValues = Record<string, Record<string, unknown>>;
export interface WorkbenchQaApi {
readonly makeRequest: <RequestType extends object, ResponseType>(
service: string,
request: RequestType,
timeout?: number,
retries?: number,
flow?: string,
) => Promise<ResponseType>;
readonly makeRequestMulti: <RequestType extends object, ResponseType>(
service: string,
request: RequestType,
receiver: (resp: unknown) => boolean,
timeout?: number,
retries?: number,
flow?: string,
) => Promise<ResponseType>;
}
const UnknownRecord = S.Record(S.String, S.Unknown);
const ConfigValuesRecord = S.Record(S.String, UnknownRecord);
@ -358,7 +375,9 @@ function dispatchFlow(state: MockState, request: Record<string, unknown>): unkno
const id = stringValue(request["flow-id"], "default");
return { flow: encodeJson(state.flows.definitions[id] ?? { id, description: "Mock flow" }) };
}
if (operation === "list-blueprints") return { "blueprint-names": Object.keys(state.flows.blueprints).sort() };
if (operation === "list-blueprints") {
return { "blueprint-names": A.sort(Object.keys(state.flows.blueprints), Order.String) };
}
if (operation === "get-blueprint") {
const name = stringValue(request["blueprint-name"], "qa-blueprint");
return { "blueprint-definition": encodeJson(state.flows.blueprints[name] ?? {}) };
@ -563,33 +582,37 @@ function dispatchStream<ResponseType>(
return Promise.resolve({} as ResponseType);
}
export function makeMockBaseApi(fixture: MockWorkbenchFixture = {}): BaseApi {
export function makeMockBaseApi(fixture: MockWorkbenchFixture = {}): WorkbenchQaApi {
const state = createState(fixture);
const token = state.settings.apiKey.length > 0 ? state.settings.apiKey : undefined;
return makeBaseApiWithRpc(state.settings.user, token, state.settings.gatewayUrl, {
dispatch: (input) =>
return {
makeRequest: <RequestType extends object, ResponseType>(
service: string,
request: RequestType,
_timeout?: number,
_retries?: number,
flow?: string,
): Promise<ResponseType> =>
Promise.resolve(
dispatchRequest(
state,
input.service,
input.request,
input.flow,
),
service,
request as Record<string, unknown>,
flow,
) as ResponseType,
),
dispatchStream: (input, receiver) =>
dispatchStream(state, input.service, (message) => {
makeRequestMulti: <RequestType extends object, ResponseType>(
service: string,
_request: RequestType,
receiver: (resp: unknown) => boolean,
): Promise<ResponseType> =>
dispatchStream(state, service, (message) => {
const chunk = message as { response?: unknown; complete?: boolean };
return receiver({
response: chunk.response,
complete: chunk.complete === true,
});
}).then(() => undefined),
subscribe: (listener) => {
listener({ status: token === undefined ? "connected" : "connected" });
return () => {};
},
close: () => Promise.resolve(),
});
}).then(() => ({}) as ResponseType),
};
}
export function qaSettingsFromFixture(fixture: MockWorkbenchFixture = {}) {