mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-01 09:29:38 +02:00
Enforce strict Effect tsgo migrations
This commit is contained in:
parent
64fb23e7d0
commit
f6878d4dd7
49 changed files with 5547 additions and 3250 deletions
|
|
@ -2,7 +2,7 @@ import { Clipboard as BrowserClipboard } from "@effect/platform-browser";
|
|||
import * as BrowserHttpClient from "@effect/platform-browser/BrowserHttpClient";
|
||||
import * as BrowserKeyValueStore from "@effect/platform-browser/BrowserKeyValueStore";
|
||||
import { BaseApi, type ConnectionState, type DocumentMetadata, type ExplainEvent, type StreamingMetadata, type Term, type Triple } from "@trustgraph/client";
|
||||
import { Cause, Context, Data, Effect, Layer, Metric, Option, Schema as S } from "effect";
|
||||
import { Cause, Clock, Context, Effect, Layer, Metric, Option, Random, Schema as S } from "effect";
|
||||
import * as Otlp from "effect/unstable/observability/Otlp";
|
||||
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
|
||||
import * as Atom from "effect/unstable/reactivity/Atom";
|
||||
|
|
@ -29,23 +29,31 @@ const browserObservabilityLayer = Otlp.layerJson({
|
|||
shutdownTimeout: "1 second",
|
||||
});
|
||||
|
||||
class WorkbenchPromiseError extends Data.TaggedError("WorkbenchPromiseError")<{
|
||||
readonly cause: unknown;
|
||||
readonly message: string;
|
||||
}> {}
|
||||
class WorkbenchPromiseError extends S.TaggedErrorClass<WorkbenchPromiseError>()(
|
||||
"WorkbenchPromiseError",
|
||||
{
|
||||
cause: S.Unknown,
|
||||
message: S.String,
|
||||
},
|
||||
) {}
|
||||
|
||||
type WorkbenchError = WorkbenchPromiseError;
|
||||
|
||||
const isWorkbenchPromiseError = S.is(WorkbenchPromiseError);
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
if (error instanceof WorkbenchPromiseError) return error.message;
|
||||
if (error instanceof Error) return error.message;
|
||||
if (isWorkbenchPromiseError(error)) return error.message;
|
||||
if (typeof error === "object" && error !== null && "message" in error) {
|
||||
const message = (error as { message?: unknown }).message;
|
||||
if (typeof message === "string") return message;
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
|
||||
function promiseBoundary<A>(evaluate: () => Promise<A>): Effect.Effect<A, WorkbenchPromiseError> {
|
||||
return Effect.tryPromise({
|
||||
try: evaluate,
|
||||
catch: (cause) => new WorkbenchPromiseError({ cause, message: errorMessage(cause) }),
|
||||
catch: (cause) => WorkbenchPromiseError.make({ cause, message: errorMessage(cause) }),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -73,7 +81,7 @@ export class WorkbenchFiles extends Context.Service<
|
|||
Effect.flatMap((buffer) =>
|
||||
Effect.try({
|
||||
try: () => base64FromArrayBuffer(buffer),
|
||||
catch: (cause) => new WorkbenchPromiseError({ cause, message: errorMessage(cause) }),
|
||||
catch: (cause) => WorkbenchPromiseError.make({ cause, message: errorMessage(cause) }),
|
||||
})
|
||||
),
|
||||
);
|
||||
|
|
@ -480,12 +488,12 @@ export function resultError<A, E>(result: AsyncResult.AsyncResult<A, E>): string
|
|||
return resultErrorMessage(result);
|
||||
}
|
||||
|
||||
function nextMessageId(): string {
|
||||
return `msg-${crypto.randomUUID()}`;
|
||||
}
|
||||
|
||||
function nextNotificationId(): string {
|
||||
return `notif-${crypto.randomUUID()}`;
|
||||
function randomId(prefix: string): Effect.Effect<string> {
|
||||
return Effect.gen(function*() {
|
||||
const left = yield* Random.nextIntBetween(0, 36 ** 6, { halfOpen: true });
|
||||
const right = yield* Random.nextIntBetween(0, 36 ** 6, { halfOpen: true });
|
||||
return `${prefix}-${left.toString(36).padStart(6, "0")}${right.toString(36).padStart(6, "0")}`;
|
||||
});
|
||||
}
|
||||
|
||||
function metadataFrom(metadata: StreamingMetadata | undefined): ChatMessage["metadata"] | undefined {
|
||||
|
|
@ -520,7 +528,7 @@ function parseConfigEntries<T>(raw: unknown, label: string): T[] {
|
|||
for (const item of mapConfigEntries(raw)) {
|
||||
const config = parseJsonUnknown(item.value);
|
||||
if (config === undefined) {
|
||||
console.warn(`[workbench-atoms] Failed to parse ${label}: ${item.key}`);
|
||||
Effect.runSync(Effect.logWarning(`[workbench-atoms] Failed to parse ${label}: ${item.key}`));
|
||||
} else {
|
||||
entries.push({ key: item.key, config } as T);
|
||||
}
|
||||
|
|
@ -743,7 +751,7 @@ export const clearMessagesAtom = Atom.writable(
|
|||
export const pushNotificationAtom = localCommandAtom<Omit<Notification, "id">, void>(
|
||||
"pushNotification",
|
||||
Effect.fn("trustgraph.workbench.pushNotification")(function*(input, get) {
|
||||
const id = nextNotificationId();
|
||||
const id = yield* randomId("notif");
|
||||
const notification: Notification = { id, ...input };
|
||||
get.set(notificationsAtom, [...get(notificationsAtom), notification]);
|
||||
yield* Effect.sleep("5 seconds");
|
||||
|
|
@ -1307,9 +1315,11 @@ const uploadDocumentChunkedEffect = Effect.fn("trustgraph.workbench.uploadDocume
|
|||
bytesUploaded: 0,
|
||||
} });
|
||||
const lib = api.librarian();
|
||||
const documentId = yield* randomId("upload");
|
||||
const timestamp = yield* Clock.currentTimeMillis;
|
||||
const beginResp = yield* promiseBoundary(() => lib.beginUpload({
|
||||
id: crypto.randomUUID(),
|
||||
time: Math.floor(Date.now() / 1000),
|
||||
id: documentId,
|
||||
time: Math.floor(timestamp / 1000),
|
||||
kind: input.mimeType,
|
||||
title: input.title,
|
||||
comments: input.comments,
|
||||
|
|
@ -1478,7 +1488,7 @@ export const deleteCollectionAtom = commandAtom<string, void>("deleteCollection"
|
|||
|
||||
export const submitMessageAtom = commandAtom<{ input: string }, void>(
|
||||
"submitMessage",
|
||||
Effect.fn("trustgraph.workbench.submitMessage")(({ input }, get, api) => Effect.sync(() => {
|
||||
Effect.fn("trustgraph.workbench.submitMessage")(function*({ input }, get, api) {
|
||||
const trimmed = input.trim();
|
||||
if (trimmed.length === 0) return;
|
||||
|
||||
|
|
@ -1488,7 +1498,8 @@ export const submitMessageAtom = commandAtom<{ input: string }, void>(
|
|||
const chatMode = get(conversationAtom).chatMode;
|
||||
const flow = api.flow(flowId);
|
||||
const explainEvents: ExplainEvent[] = [];
|
||||
const requestId = crypto.randomUUID();
|
||||
const requestId = yield* randomId("chat");
|
||||
const timestamp = yield* Clock.currentTimeMillis;
|
||||
const isActiveRequest = () => get(activeChatRequestAtom) === requestId;
|
||||
const finishRequest = () => {
|
||||
if (isActiveRequest()) {
|
||||
|
|
@ -1498,16 +1509,16 @@ export const submitMessageAtom = commandAtom<{ input: string }, void>(
|
|||
};
|
||||
|
||||
const userMsg: ChatMessage = {
|
||||
id: nextMessageId(),
|
||||
id: yield* randomId("msg"),
|
||||
role: "user",
|
||||
content: input,
|
||||
timestamp: Date.now(),
|
||||
timestamp,
|
||||
};
|
||||
const assistantMsg: ChatMessage = {
|
||||
id: nextMessageId(),
|
||||
id: yield* randomId("msg"),
|
||||
role: "assistant",
|
||||
content: "",
|
||||
timestamp: Date.now(),
|
||||
timestamp,
|
||||
isStreaming: true,
|
||||
...(chatMode === "agent"
|
||||
? {
|
||||
|
|
@ -1625,7 +1636,7 @@ export const submitMessageAtom = commandAtom<{ input: string }, void>(
|
|||
);
|
||||
break;
|
||||
}
|
||||
})),
|
||||
}),
|
||||
);
|
||||
|
||||
export const cancelChatAtom = Atom.writable(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
type FallbackProps,
|
||||
} from "react-error-boundary";
|
||||
import { AlertTriangle, RefreshCw } from "lucide-react";
|
||||
import { Effect } from "effect";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
|
|
@ -12,7 +13,9 @@ interface Props {
|
|||
}
|
||||
|
||||
const errorMessage = (error: unknown): string =>
|
||||
error instanceof Error ? error.message : "An unexpected error occurred.";
|
||||
typeof error === "object" && error !== null && "message" in error && typeof error.message === "string"
|
||||
? error.message
|
||||
: "An unexpected error occurred.";
|
||||
|
||||
function DefaultFallback({ error, resetErrorBoundary }: FallbackProps) {
|
||||
return (
|
||||
|
|
@ -42,7 +45,7 @@ export function ErrorBoundary({ children, fallback }: Props) {
|
|||
<ReactErrorBoundary
|
||||
fallbackRender={(props) => fallback ?? <DefaultFallback {...props} />}
|
||||
onError={(error, info) => {
|
||||
console.error("[ErrorBoundary]", error, info.componentStack);
|
||||
Effect.runSync(Effect.logError("[ErrorBoundary]", { error, componentStack: info.componentStack }));
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import {
|
|||
import { Dialog } from "@/components/ui/dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import type { DocumentMetadata } from "@trustgraph/client";
|
||||
import { DateTime } from "effect";
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return "0 B";
|
||||
|
|
@ -53,6 +54,10 @@ function guessKind(doc: DocumentMetadata): string {
|
|||
return kind.length > 0 ? kind : "--";
|
||||
}
|
||||
|
||||
function formatUnixTimestamp(seconds: number): string {
|
||||
return DateTime.formatLocal(DateTime.makeUnsafe(seconds * 1000));
|
||||
}
|
||||
|
||||
function resetUploadForm(form: UploadForm): UploadForm {
|
||||
return { ...form, file: null, title: "", tags: "", comments: "", uploading: false, dragOver: false, progress: null };
|
||||
}
|
||||
|
|
@ -242,7 +247,7 @@ function DocumentDetailDialog() {
|
|||
<h3 className="mb-1 flex items-center gap-1.5 text-xs font-medium uppercase tracking-wider text-fg-subtle">
|
||||
<Clock className="h-3 w-3" /> Created
|
||||
</h3>
|
||||
<p className="text-sm text-fg-muted">{new Date(doc.time * 1000).toLocaleString()}</p>
|
||||
<p className="text-sm text-fg-muted">{formatUnixTimestamp(doc.time)}</p>
|
||||
</div>
|
||||
)}
|
||||
{doc.metadata !== undefined && doc.metadata.length > 0 && (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { makeBaseApiWithRpc, type BaseApi, type DocumentMetadata, type ProcessingMetadata, type StreamingMetadata, type Triple } from "@trustgraph/client";
|
||||
import { Option, Schema as S } from "effect";
|
||||
import { Clock, Effect, Option, Schema as S } from "effect";
|
||||
|
||||
type ConfigValues = Record<string, Record<string, unknown>>;
|
||||
|
||||
|
|
@ -268,12 +268,13 @@ function configValues(state: MockState, type: string) {
|
|||
|
||||
function addDocument(state: MockState, metadata: DocumentMetadata): DocumentMetadata {
|
||||
const id = metadata.id ?? `qa-doc-${state.library.documents.length + 1}`;
|
||||
const currentTimeSeconds = Math.floor(Effect.runSync(Clock.currentTimeMillis) / 1000);
|
||||
const document = {
|
||||
...metadata,
|
||||
id,
|
||||
title: metadata.title ?? id,
|
||||
kind: metadata.kind ?? metadata["document-type"] ?? "text/plain",
|
||||
time: metadata.time ?? Math.floor(Date.now() / 1000),
|
||||
time: metadata.time ?? currentTimeSeconds,
|
||||
user: metadata.user ?? state.settings.user,
|
||||
tags: metadata.tags ?? [],
|
||||
};
|
||||
|
|
@ -526,16 +527,14 @@ export function makeMockBaseApi(fixture: MockWorkbenchFixture = {}): BaseApi {
|
|||
input.flow,
|
||||
),
|
||||
),
|
||||
dispatchStream: async (input, receiver) => {
|
||||
await dispatchStream(state, input.service, (message) => {
|
||||
dispatchStream: (input, receiver) =>
|
||||
dispatchStream(state, input.service, (message) => {
|
||||
const chunk = message as { response?: unknown; complete?: boolean };
|
||||
return receiver({
|
||||
response: chunk.response,
|
||||
complete: chunk.complete === true,
|
||||
});
|
||||
});
|
||||
return undefined;
|
||||
},
|
||||
}).then(() => undefined),
|
||||
subscribe: (listener) => {
|
||||
listener({ status: token === undefined ? "connected" : "connected" });
|
||||
return () => {};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue