170 lines
5.4 KiB
TypeScript
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);
|
||
|
|
};
|