Files
mnote/wolai-frontend/src/lib/mindmap/leptos-mindmap-adapter.ts
T

170 lines
5.4 KiB
TypeScript

import {
createSimpleMindMapBridge,
type MindmapAdapterProjection,
type SimpleMindMapBridge,
type SimpleMindMapBridgeEvent,
type SimpleMindMapPluginName,
} from "./simple-mind-map-bridge";
import type { MindmapCompatPayloadPatch, MindmapKernelCommand } from "./mindmap-command-diff";
export type {
MindmapAdapterProjection,
SimpleMindMapBridge,
SimpleMindMapBridgeEvent,
} from "./simple-mind-map-bridge";
export type LeptosMindmapAdapterOptions = {
el: HTMLElement;
projection: MindmapAdapterProjection;
mode?: "edit" | "readonly";
pluginNames?: SimpleMindMapPluginName[];
runtimeOptions?: Record<string, unknown>;
onEvent?: (event: SimpleMindMapBridgeEvent) => void;
};
export type RequestMindmapAdapterProjectionInput = {
documentId: string;
mindmapId: string;
endpoint?: string;
fetcher?: typeof fetch;
};
export type MindmapCommandApplyInput = {
documentId: string;
mindmapId: string;
commands: Array<MindmapKernelCommand | MindmapCompatPayloadPatch>;
projectionRevision?: number | null;
endpoint?: string;
fetcher?: typeof fetch;
};
export type MindmapCommandApplyResult = {
ok: true;
kernelRevision: number | null;
raw: unknown;
};
export class MindmapCommandBridgeError extends Error {
readonly code: "command_failed";
readonly status: number | null;
constructor(message: string, status: number | null = null) {
super(message);
this.name = "MindmapCommandBridgeError";
this.code = "command_failed";
this.status = status;
}
}
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
export const isMindmapAdapterProjection = (value: unknown): value is MindmapAdapterProjection => {
if (!isRecord(value)) return false;
return (
value.schema === "mnote.mindmap.simple_mind_map_scene.v1" &&
value.runtime === "simple-mind-map" &&
"root" in value &&
typeof value.kernelRevision === "number"
);
};
export const createMindmapAdapterProjectionEndpoint = (documentId: string, mindmapId: string) => {
const doc = encodeURIComponent(documentId);
const map = encodeURIComponent(mindmapId);
return `/api/mindmap/${doc}/${map}?view=simple_mind_map_scene&queryName=mindmap.simple_mind_map_scene.get`;
};
export const createMindmapCommandApplyEndpoint = (documentId: string, mindmapId: string) => {
const doc = encodeURIComponent(documentId);
const map = encodeURIComponent(mindmapId);
return `/api/mindmap/${doc}/${map}`;
};
export const requestMindmapAdapterProjection = async (
input: RequestMindmapAdapterProjectionInput,
): Promise<MindmapAdapterProjection> => {
const fetcher = input.fetcher ?? fetch;
const endpoint = input.endpoint ?? createMindmapAdapterProjectionEndpoint(input.documentId, input.mindmapId);
const response = await fetcher(endpoint, {
headers: { Accept: "application/json" },
});
if (!response.ok) {
throw new Error(`projection_load_failed:${response.status}`);
}
const payload = (await response.json()) as unknown;
const projection = isRecord(payload) && "result" in payload ? payload.result : payload;
if (!isMindmapAdapterProjection(projection)) {
throw new Error("projection_load_failed:invalid_adapter_projection");
}
return projection;
};
const readKernelRevision = (value: unknown): number | null => {
if (!isRecord(value)) return null;
const candidates = [
value.kernelRevision,
value.projectionRevision,
isRecord(value.result) ? value.result.kernelRevision : null,
isRecord(value.result) ? value.result.projectionRevision : null,
];
for (const candidate of candidates) {
if (typeof candidate === "number" && Number.isFinite(candidate)) return candidate;
}
return null;
};
export const executeMindmapCommandApply = async (
input: MindmapCommandApplyInput,
): Promise<MindmapCommandApplyResult> => {
if (input.commands.length === 0) {
throw new MindmapCommandBridgeError("command_failed:empty_commands");
}
const fetcher = input.fetcher ?? fetch;
const endpoint = input.endpoint ?? createMindmapCommandApplyEndpoint(input.documentId, input.mindmapId);
const response = await fetcher(endpoint, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
commandName: "mindmap.command.apply",
documentId: input.documentId,
mindmapId: input.mindmapId,
commands: input.commands,
projectionRevision: input.projectionRevision ?? null,
}),
});
const raw = (await response.json().catch(() => null)) as unknown;
if (!response.ok) {
throw new MindmapCommandBridgeError(`command_failed:${response.status}`, response.status);
}
return {
ok: true,
kernelRevision: readKernelRevision(raw),
raw,
};
};
export const executeMindmapCommandApplyAndRefreshProjection = async (
input: MindmapCommandApplyInput & { projectionEndpoint?: string },
): Promise<MindmapAdapterProjection> => {
await executeMindmapCommandApply(input);
return requestMindmapAdapterProjection({
documentId: input.documentId,
mindmapId: input.mindmapId,
endpoint: input.projectionEndpoint,
fetcher: input.fetcher,
});
};
export const createLeptosMindmapAdapter = async (
input: LeptosMindmapAdapterOptions,
): Promise<SimpleMindMapBridge> => {
if (!isMindmapAdapterProjection(input.projection)) {
throw new Error("adapter_init_failed:invalid_projection");
}
return createSimpleMindMapBridge(input);
};