web: edit virtual config sources with WASM core
This commit is contained in:
Generated
+13
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"version": "5",
|
||||
"specifiers": {
|
||||
"jsr:@std/assert@*": "1.0.19",
|
||||
"jsr:@std/internal@^1.0.12": "1.0.14",
|
||||
"npm:@codemirror/state@6.5.2": "6.5.2",
|
||||
"npm:@codemirror/view@6.38.8": "6.38.8",
|
||||
"npm:@fontsource/ibm-plex-mono@5.3.0": "5.3.0",
|
||||
@@ -20,6 +22,17 @@
|
||||
"npm:typescript@5.9.3": "5.9.3",
|
||||
"npm:vite@7.2.7": "7.2.7"
|
||||
},
|
||||
"jsr": {
|
||||
"@std/assert@1.0.19": {
|
||||
"integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e",
|
||||
"dependencies": [
|
||||
"jsr:@std/internal"
|
||||
]
|
||||
},
|
||||
"@std/internal@1.0.14": {
|
||||
"integrity": "291516b3d4c35024d6ffbc0a9df5bf4c64116e05b50012cf846710152d2ffdf7"
|
||||
}
|
||||
},
|
||||
"npm": {
|
||||
"@codemirror/language@6.12.4": {
|
||||
"integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import DecodalSourceEditor from "$lib/workspace/settings/DecodalSourceEditor.svelte";
|
||||
import {
|
||||
commitConfigTree,
|
||||
fetchConfigTree,
|
||||
previewConfigTree,
|
||||
} from "./api.ts";
|
||||
import { ConfigSourceToolchain } from "./toolchain.ts";
|
||||
import type {
|
||||
ConfigDiagnostic,
|
||||
ConfigTreeChange,
|
||||
WorkspaceConfigTreeResponse,
|
||||
} from "./types.ts";
|
||||
|
||||
let { workspaceId }: { workspaceId: string } = $props();
|
||||
let treeState = $state<WorkspaceConfigTreeResponse | null>(null);
|
||||
let selectedPath = $state("");
|
||||
let source = $state("");
|
||||
let newPath = $state("workspace.dcdl");
|
||||
let diagnostics = $state<ConfigDiagnostic[]>([]);
|
||||
let status = $state("Loading source tree…");
|
||||
let busy = $state(false);
|
||||
let toolchain: ConfigSourceToolchain | null = null;
|
||||
|
||||
const paths = $derived(
|
||||
treeState ? Object.keys(treeState.snapshot.entries).toSorted() : [],
|
||||
);
|
||||
const selected = $derived(
|
||||
treeState && selectedPath ? treeState.snapshot.entries[selectedPath] : undefined,
|
||||
);
|
||||
const dirty = $derived(selected ? source !== selected.content : source.length > 0);
|
||||
|
||||
onMount(() => {
|
||||
toolchain = new ConfigSourceToolchain();
|
||||
void reload();
|
||||
return () => toolchain?.close();
|
||||
});
|
||||
|
||||
async function reload() {
|
||||
try {
|
||||
treeState = await fetchConfigTree(workspaceId);
|
||||
if (!selectedPath || !treeState.snapshot.entries[selectedPath]) {
|
||||
selectedPath = Object.keys(treeState.snapshot.entries).toSorted()[0] ?? "";
|
||||
}
|
||||
source = selectedPath ? treeState.snapshot.entries[selectedPath].content : "";
|
||||
diagnostics = [];
|
||||
status = treeState.snapshot.revision === 0
|
||||
? "No committed sources yet. Create workspace.dcdl to begin."
|
||||
: `Revision ${treeState.snapshot.revision} · ${treeState.snapshot.digest.slice(0, 20)}…`;
|
||||
} catch (error) {
|
||||
status = String(error);
|
||||
}
|
||||
}
|
||||
|
||||
function select(path: string) {
|
||||
selectedPath = path;
|
||||
source = treeState?.snapshot.entries[path]?.content ?? "";
|
||||
diagnostics = [];
|
||||
}
|
||||
|
||||
function currentChange(): ConfigTreeChange | null {
|
||||
if (!treeState || !selectedPath) return null;
|
||||
const entry = treeState.snapshot.entries[selectedPath];
|
||||
if (!entry) {
|
||||
return {
|
||||
kind: "create",
|
||||
path: selectedPath,
|
||||
content_type: "decodal",
|
||||
content: source,
|
||||
};
|
||||
}
|
||||
if (source === entry.content) return null;
|
||||
return {
|
||||
kind: "update",
|
||||
path: selectedPath,
|
||||
expected_digest: entry.content_digest,
|
||||
content: source,
|
||||
};
|
||||
}
|
||||
|
||||
function entrypoints(): string[] {
|
||||
if (!treeState) return [];
|
||||
if (treeState.contract.entrypoints.length > 0) return treeState.contract.entrypoints;
|
||||
if (treeState.snapshot.entries["workspace.dcdl"] || selectedPath === "workspace.dcdl") {
|
||||
return ["workspace.dcdl"];
|
||||
}
|
||||
return selectedPath ? [selectedPath] : [];
|
||||
}
|
||||
|
||||
async function analyze() {
|
||||
if (!toolchain || !treeState || !selectedPath) return;
|
||||
diagnostics = await toolchain.analyze(treeState.snapshot, selectedPath, source);
|
||||
status = diagnostics.length === 0 ? "No diagnostics." : `${diagnostics.length} diagnostic(s).`;
|
||||
}
|
||||
|
||||
async function format() {
|
||||
if (!toolchain) return;
|
||||
try {
|
||||
source = await toolchain.format(source);
|
||||
await analyze();
|
||||
} catch (error) {
|
||||
status = String(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function preview() {
|
||||
if (!treeState) return;
|
||||
const change = currentChange();
|
||||
if (!change) {
|
||||
status = "No draft changes to preview.";
|
||||
return;
|
||||
}
|
||||
busy = true;
|
||||
try {
|
||||
const candidate = await previewConfigTree(workspaceId, {
|
||||
changes: [change],
|
||||
entrypoints: entrypoints(),
|
||||
});
|
||||
diagnostics = [];
|
||||
status = `Preview valid · projection ${candidate.evaluation.projection_digest.slice(0, 20)}…`;
|
||||
} catch (error) {
|
||||
status = String(error);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function commit() {
|
||||
if (!treeState) return;
|
||||
const change = currentChange();
|
||||
if (!change) {
|
||||
status = "No draft changes to commit.";
|
||||
return;
|
||||
}
|
||||
busy = true;
|
||||
try {
|
||||
treeState = await commitConfigTree(workspaceId, {
|
||||
base_revision: treeState.snapshot.revision,
|
||||
base_digest: treeState.snapshot.digest,
|
||||
changes: [change],
|
||||
entrypoints: entrypoints(),
|
||||
});
|
||||
source = treeState.snapshot.entries[selectedPath]?.content ?? "";
|
||||
diagnostics = [];
|
||||
status = `Committed revision ${treeState.snapshot.revision}.`;
|
||||
} catch (error) {
|
||||
status = String(error);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
function createEntry() {
|
||||
const path = newPath.trim();
|
||||
if (!path || treeState?.snapshot.entries[path]) return;
|
||||
selectedPath = path;
|
||||
source = "{}\n";
|
||||
diagnostics = [];
|
||||
status = `Drafting new source ${path}. It is not persisted until Commit succeeds.`;
|
||||
}
|
||||
|
||||
async function deleteEntry() {
|
||||
if (!treeState || !selected) return;
|
||||
busy = true;
|
||||
try {
|
||||
const remainingEntrypoints = treeState.contract.entrypoints.filter((path) => path !== selectedPath);
|
||||
treeState = await commitConfigTree(workspaceId, {
|
||||
base_revision: treeState.snapshot.revision,
|
||||
base_digest: treeState.snapshot.digest,
|
||||
changes: [{
|
||||
kind: "delete",
|
||||
path: selectedPath,
|
||||
expected_digest: selected.content_digest,
|
||||
}],
|
||||
entrypoints: remainingEntrypoints,
|
||||
});
|
||||
selectedPath = Object.keys(treeState.snapshot.entries).toSorted()[0] ?? "";
|
||||
source = selectedPath ? treeState.snapshot.entries[selectedPath].content : "";
|
||||
diagnostics = [];
|
||||
status = `Committed revision ${treeState.snapshot.revision}.`;
|
||||
} catch (error) {
|
||||
status = String(error);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="config-source-shell" aria-label="Workspace configuration source tree">
|
||||
<aside class="config-source-tree">
|
||||
<div class="config-source-tree__header">
|
||||
<strong>Source tree</strong>
|
||||
<span>{paths.length}</span>
|
||||
</div>
|
||||
<nav aria-label="Virtual configuration paths">
|
||||
{#each paths as path}
|
||||
<button
|
||||
type="button"
|
||||
class:active={path === selectedPath}
|
||||
onclick={() => select(path)}
|
||||
>{path}</button>
|
||||
{/each}
|
||||
</nav>
|
||||
<form class="config-source-create" onsubmit={(event) => { event.preventDefault(); createEntry(); }}>
|
||||
<label for="new-config-path">New path</label>
|
||||
<input id="new-config-path" bind:value={newPath} placeholder="workspace.dcdl" />
|
||||
<button type="submit">Create draft</button>
|
||||
</form>
|
||||
</aside>
|
||||
|
||||
<div class="config-source-workbench">
|
||||
<header class="config-source-workbench__header">
|
||||
<div>
|
||||
<span>Virtual path</span>
|
||||
<strong>{selectedPath || "Select or create a source"}</strong>
|
||||
</div>
|
||||
<div class="config-source-actions">
|
||||
<button type="button" onclick={format} disabled={!selectedPath || busy}>Format</button>
|
||||
<button type="button" onclick={analyze} disabled={!selectedPath || busy}>Analyze</button>
|
||||
<button type="button" onclick={preview} disabled={!dirty || busy}>Preview</button>
|
||||
<button class="primary" type="button" onclick={commit} disabled={!dirty || busy}>Commit</button>
|
||||
<button class="danger" type="button" onclick={deleteEntry} disabled={!selected || busy}>Delete</button>
|
||||
</div>
|
||||
</header>
|
||||
<DecodalSourceEditor value={source} readonly={!selectedPath || busy} onChange={(value) => source = value} />
|
||||
<p class="config-source-status" aria-live="polite">{status}</p>
|
||||
{#if diagnostics.length > 0}
|
||||
<ol class="config-source-diagnostics">
|
||||
{#each diagnostics as diagnostic}
|
||||
<li>
|
||||
<strong>{diagnostic.kind}</strong>
|
||||
<span>{diagnostic.message}</span>
|
||||
<small>bytes {diagnostic.span.start_byte}–{diagnostic.span.end_byte}</small>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,60 @@
|
||||
/// <reference lib="deno.ns" />
|
||||
|
||||
import { assert, assertEquals } from "jsr:@std/assert";
|
||||
import {
|
||||
commitConfigTree,
|
||||
fetchConfigEntry,
|
||||
fetchConfigTree,
|
||||
previewConfigTree,
|
||||
} from "./api.ts";
|
||||
|
||||
function response(body: unknown, status = 200): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
Deno.test("config source API stays workspace-scoped and separates preview from commit", async () => {
|
||||
const calls: Array<{ url: string; init?: RequestInit }> = [];
|
||||
const fetcher = ((input: string | URL | Request, init?: RequestInit) => {
|
||||
calls.push({ url: String(input), init });
|
||||
return Promise.resolve(response({ ok: true }));
|
||||
}) as typeof fetch;
|
||||
|
||||
await fetchConfigTree("w/one", fetcher);
|
||||
await fetchConfigEntry("w/one", "profiles/main.dcdl", fetcher);
|
||||
await previewConfigTree("w/one", { changes: [], entrypoints: [] }, fetcher);
|
||||
await commitConfigTree("w/one", {
|
||||
base_revision: 4,
|
||||
base_digest: "sha256:base",
|
||||
changes: [],
|
||||
entrypoints: [],
|
||||
}, fetcher);
|
||||
|
||||
assertEquals(calls.map((call) => call.url), [
|
||||
"/api/w/w%2Fone/config/source-tree",
|
||||
"/api/w/w%2Fone/config/source-tree/entries/profiles%2Fmain.dcdl",
|
||||
"/api/w/w%2Fone/config/source-tree/preview",
|
||||
"/api/w/w%2Fone/config/source-tree/commit",
|
||||
]);
|
||||
assertEquals(calls[2].init?.method, "POST");
|
||||
assertEquals(calls[3].init?.method, "POST");
|
||||
assert(
|
||||
String(calls[3].init?.body).includes('"base_digest":"sha256:base"'),
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("config source API surfaces failed evaluation instead of treating it as a draft write", async () => {
|
||||
const fetcher = (() =>
|
||||
Promise.resolve(
|
||||
new Response("structured diagnostics", { status: 422 }),
|
||||
)) as typeof fetch;
|
||||
let message = "";
|
||||
try {
|
||||
await previewConfigTree("w", { changes: [], entrypoints: [] }, fetcher);
|
||||
} catch (error) {
|
||||
message = String(error);
|
||||
}
|
||||
assert(message.includes("structured diagnostics"));
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import type {
|
||||
ConfigCommitRequest,
|
||||
ConfigEntry,
|
||||
EvaluatedConfigCandidate,
|
||||
WorkspaceConfigTreeResponse,
|
||||
} from "./types.ts";
|
||||
|
||||
function sourceTreeUrl(workspaceId: string): string {
|
||||
return `/api/w/${encodeURIComponent(workspaceId)}/config/source-tree`;
|
||||
}
|
||||
|
||||
async function readJson<T>(response: Response): Promise<T> {
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
throw new Error(body || `${response.status} ${response.statusText}`);
|
||||
}
|
||||
return await response.json() as T;
|
||||
}
|
||||
|
||||
export async function fetchConfigTree(
|
||||
workspaceId: string,
|
||||
fetcher: typeof fetch = fetch,
|
||||
): Promise<WorkspaceConfigTreeResponse> {
|
||||
return await readJson(
|
||||
await fetcher(sourceTreeUrl(workspaceId), {
|
||||
headers: { accept: "application/json" },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchConfigEntry(
|
||||
workspaceId: string,
|
||||
path: string,
|
||||
fetcher: typeof fetch = fetch,
|
||||
): Promise<ConfigEntry> {
|
||||
return await readJson(
|
||||
await fetcher(
|
||||
`${sourceTreeUrl(workspaceId)}/entries/${encodeURIComponent(path)}`,
|
||||
{ headers: { accept: "application/json" } },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export async function previewConfigTree(
|
||||
workspaceId: string,
|
||||
request: Omit<ConfigCommitRequest, "base_revision" | "base_digest">,
|
||||
fetcher: typeof fetch = fetch,
|
||||
): Promise<EvaluatedConfigCandidate> {
|
||||
return await readJson(
|
||||
await fetcher(`${sourceTreeUrl(workspaceId)}/preview`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(request),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function commitConfigTree(
|
||||
workspaceId: string,
|
||||
request: ConfigCommitRequest,
|
||||
fetcher: typeof fetch = fetch,
|
||||
): Promise<WorkspaceConfigTreeResponse> {
|
||||
return await readJson(
|
||||
await fetcher(`${sourceTreeUrl(workspaceId)}/commit`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(request),
|
||||
}),
|
||||
);
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
export function analyze_snapshot(snapshot: any, entrypoint: string, source_override?: string | null): any;
|
||||
|
||||
export function evaluate_snapshot(snapshot: any, contract: any): any;
|
||||
|
||||
export function formatSource(source: string): string;
|
||||
|
||||
export function format_source(source: string): string;
|
||||
|
||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
export interface InitOutput {
|
||||
readonly memory: WebAssembly.Memory;
|
||||
readonly analyze_snapshot: (a: any, b: number, c: number, d: number, e: number) => [number, number, number];
|
||||
readonly evaluate_snapshot: (a: any, b: any) => [number, number, number];
|
||||
readonly format_source: (a: number, b: number) => [number, number, number, number];
|
||||
readonly formatSource: (a: number, b: number) => [number, number];
|
||||
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
readonly __wbindgen_exn_store: (a: number) => void;
|
||||
readonly __externref_table_alloc: () => number;
|
||||
readonly __wbindgen_externrefs: WebAssembly.Table;
|
||||
readonly __externref_table_dealloc: (a: number) => void;
|
||||
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||
readonly __wbindgen_start: () => void;
|
||||
}
|
||||
|
||||
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
||||
|
||||
/**
|
||||
* Instantiates the given `module`, which can either be bytes or
|
||||
* a precompiled `WebAssembly.Module`.
|
||||
*
|
||||
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {InitOutput}
|
||||
*/
|
||||
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
||||
|
||||
/**
|
||||
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
||||
* for everything else, calls `WebAssembly.instantiate` directly.
|
||||
*
|
||||
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {Promise<InitOutput>}
|
||||
*/
|
||||
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|
||||
@@ -0,0 +1,582 @@
|
||||
/* @ts-self-types="./config_source_wasm.d.ts" */
|
||||
|
||||
/**
|
||||
* @param {any} snapshot
|
||||
* @param {string} entrypoint
|
||||
* @param {string | null} [source_override]
|
||||
* @returns {any}
|
||||
*/
|
||||
export function analyze_snapshot(snapshot, entrypoint, source_override) {
|
||||
const ptr0 = passStringToWasm0(entrypoint, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
var ptr1 = isLikeNone(source_override) ? 0 : passStringToWasm0(source_override, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len1 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.analyze_snapshot(snapshot, ptr0, len0, ptr1, len1);
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
return takeFromExternrefTable0(ret[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} snapshot
|
||||
* @param {any} contract
|
||||
* @returns {any}
|
||||
*/
|
||||
export function evaluate_snapshot(snapshot, contract) {
|
||||
const ret = wasm.evaluate_snapshot(snapshot, contract);
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
return takeFromExternrefTable0(ret[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} source
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatSource(source) {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(source, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.formatSource(ptr0, len0);
|
||||
deferred2_0 = ret[0];
|
||||
deferred2_1 = ret[1];
|
||||
return getStringFromWasm0(ret[0], ret[1]);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} source
|
||||
* @returns {string}
|
||||
*/
|
||||
export function format_source(source) {
|
||||
let deferred3_0;
|
||||
let deferred3_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(source, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.format_source(ptr0, len0);
|
||||
var ptr2 = ret[0];
|
||||
var len2 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr2 = 0; len2 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred3_0 = ptr2;
|
||||
deferred3_1 = len2;
|
||||
return getStringFromWasm0(ptr2, len2);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function __wbg_get_imports() {
|
||||
const import0 = {
|
||||
__proto__: null,
|
||||
__wbg_Error_2e59b1b37a9a34c3: function(arg0, arg1) {
|
||||
const ret = Error(getStringFromWasm0(arg0, arg1));
|
||||
return ret;
|
||||
},
|
||||
__wbg_Number_e6ffdb596c888833: function(arg0) {
|
||||
const ret = Number(arg0);
|
||||
return ret;
|
||||
},
|
||||
__wbg_String_8564e559799eccda: function(arg0, arg1) {
|
||||
const ret = String(arg1);
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||
},
|
||||
__wbg___wbindgen_bigint_get_as_i64_2c5082002e4826e2: function(arg0, arg1) {
|
||||
const v = arg1;
|
||||
const ret = typeof(v) === 'bigint' ? v : undefined;
|
||||
getDataViewMemory0().setBigInt64(arg0 + 8 * 1, isLikeNone(ret) ? BigInt(0) : ret, true);
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
|
||||
},
|
||||
__wbg___wbindgen_boolean_get_a86c216575a75c30: function(arg0) {
|
||||
const v = arg0;
|
||||
const ret = typeof(v) === 'boolean' ? v : undefined;
|
||||
return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0;
|
||||
},
|
||||
__wbg___wbindgen_debug_string_dd5d2d07ce9e6c57: function(arg0, arg1) {
|
||||
const ret = debugString(arg1);
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||
},
|
||||
__wbg___wbindgen_in_4bd7a57e54337366: function(arg0, arg1) {
|
||||
const ret = arg0 in arg1;
|
||||
return ret;
|
||||
},
|
||||
__wbg___wbindgen_is_bigint_6c98f7e945dacdde: function(arg0) {
|
||||
const ret = typeof(arg0) === 'bigint';
|
||||
return ret;
|
||||
},
|
||||
__wbg___wbindgen_is_function_49868bde5eb1e745: function(arg0) {
|
||||
const ret = typeof(arg0) === 'function';
|
||||
return ret;
|
||||
},
|
||||
__wbg___wbindgen_is_object_40c5a80572e8f9d3: function(arg0) {
|
||||
const val = arg0;
|
||||
const ret = typeof(val) === 'object' && val !== null;
|
||||
return ret;
|
||||
},
|
||||
__wbg___wbindgen_is_string_b29b5c5a8065ba1a: function(arg0) {
|
||||
const ret = typeof(arg0) === 'string';
|
||||
return ret;
|
||||
},
|
||||
__wbg___wbindgen_is_undefined_c0cca72b82b86f4d: function(arg0) {
|
||||
const ret = arg0 === undefined;
|
||||
return ret;
|
||||
},
|
||||
__wbg___wbindgen_jsval_eq_7d430e744a913d26: function(arg0, arg1) {
|
||||
const ret = arg0 === arg1;
|
||||
return ret;
|
||||
},
|
||||
__wbg___wbindgen_jsval_loose_eq_3a72ae764d46d944: function(arg0, arg1) {
|
||||
const ret = arg0 == arg1;
|
||||
return ret;
|
||||
},
|
||||
__wbg___wbindgen_number_get_7579aab02a8a620c: function(arg0, arg1) {
|
||||
const obj = arg1;
|
||||
const ret = typeof(obj) === 'number' ? obj : undefined;
|
||||
getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true);
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
|
||||
},
|
||||
__wbg___wbindgen_string_get_914df97fcfa788f2: function(arg0, arg1) {
|
||||
const obj = arg1;
|
||||
const ret = typeof(obj) === 'string' ? obj : undefined;
|
||||
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len1 = WASM_VECTOR_LEN;
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||
},
|
||||
__wbg___wbindgen_throw_81fc77679af83bc6: function(arg0, arg1) {
|
||||
throw new Error(getStringFromWasm0(arg0, arg1));
|
||||
},
|
||||
__wbg_call_7f2987183bb62793: function() { return handleError(function (arg0, arg1) {
|
||||
const ret = arg0.call(arg1);
|
||||
return ret;
|
||||
}, arguments); },
|
||||
__wbg_done_547d467e97529006: function(arg0) {
|
||||
const ret = arg0.done;
|
||||
return ret;
|
||||
},
|
||||
__wbg_entries_616b1a459b85be0b: function(arg0) {
|
||||
const ret = Object.entries(arg0);
|
||||
return ret;
|
||||
},
|
||||
__wbg_get_4848e350b40afc16: function(arg0, arg1) {
|
||||
const ret = arg0[arg1 >>> 0];
|
||||
return ret;
|
||||
},
|
||||
__wbg_get_ed0642c4b9d31ddf: function() { return handleError(function (arg0, arg1) {
|
||||
const ret = Reflect.get(arg0, arg1);
|
||||
return ret;
|
||||
}, arguments); },
|
||||
__wbg_get_unchecked_7d7babe32e9e6a54: function(arg0, arg1) {
|
||||
const ret = arg0[arg1 >>> 0];
|
||||
return ret;
|
||||
},
|
||||
__wbg_get_with_ref_key_6412cf3094599694: function(arg0, arg1) {
|
||||
const ret = arg0[arg1];
|
||||
return ret;
|
||||
},
|
||||
__wbg_instanceof_ArrayBuffer_ff7c1337a5e3b33a: function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
result = arg0 instanceof ArrayBuffer;
|
||||
} catch (_) {
|
||||
result = false;
|
||||
}
|
||||
const ret = result;
|
||||
return ret;
|
||||
},
|
||||
__wbg_instanceof_Uint8Array_4b8da683deb25d72: function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
result = arg0 instanceof Uint8Array;
|
||||
} catch (_) {
|
||||
result = false;
|
||||
}
|
||||
const ret = result;
|
||||
return ret;
|
||||
},
|
||||
__wbg_isArray_db61795ad004c139: function(arg0) {
|
||||
const ret = Array.isArray(arg0);
|
||||
return ret;
|
||||
},
|
||||
__wbg_isSafeInteger_ea83862ba994770c: function(arg0) {
|
||||
const ret = Number.isSafeInteger(arg0);
|
||||
return ret;
|
||||
},
|
||||
__wbg_iterator_de403ef31815a3e6: function() {
|
||||
const ret = Symbol.iterator;
|
||||
return ret;
|
||||
},
|
||||
__wbg_length_0c32cb8543c8e4c8: function(arg0) {
|
||||
const ret = arg0.length;
|
||||
return ret;
|
||||
},
|
||||
__wbg_length_6e821edde497a532: function(arg0) {
|
||||
const ret = arg0.length;
|
||||
return ret;
|
||||
},
|
||||
__wbg_new_4f9fafbb3909af72: function() {
|
||||
const ret = new Object();
|
||||
return ret;
|
||||
},
|
||||
__wbg_new_99cabae501c0a8a0: function() {
|
||||
const ret = new Map();
|
||||
return ret;
|
||||
},
|
||||
__wbg_new_a560378ea1240b14: function(arg0) {
|
||||
const ret = new Uint8Array(arg0);
|
||||
return ret;
|
||||
},
|
||||
__wbg_new_f3c9df4f38f3f798: function() {
|
||||
const ret = new Array();
|
||||
return ret;
|
||||
},
|
||||
__wbg_next_01132ed6134b8ef5: function(arg0) {
|
||||
const ret = arg0.next;
|
||||
return ret;
|
||||
},
|
||||
__wbg_next_b3713ec761a9dbfd: function() { return handleError(function (arg0) {
|
||||
const ret = arg0.next();
|
||||
return ret;
|
||||
}, arguments); },
|
||||
__wbg_prototypesetcall_3e05eb9545565046: function(arg0, arg1, arg2) {
|
||||
Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2);
|
||||
},
|
||||
__wbg_set_08463b1df38a7e29: function(arg0, arg1, arg2) {
|
||||
const ret = arg0.set(arg1, arg2);
|
||||
return ret;
|
||||
},
|
||||
__wbg_set_6be42768c690e380: function(arg0, arg1, arg2) {
|
||||
arg0[arg1] = arg2;
|
||||
},
|
||||
__wbg_set_6c60b2e8ad0e9383: function(arg0, arg1, arg2) {
|
||||
arg0[arg1 >>> 0] = arg2;
|
||||
},
|
||||
__wbg_value_7f6052747ccf940f: function(arg0) {
|
||||
const ret = arg0.value;
|
||||
return ret;
|
||||
},
|
||||
__wbindgen_cast_0000000000000001: function(arg0) {
|
||||
// Cast intrinsic for `F64 -> Externref`.
|
||||
const ret = arg0;
|
||||
return ret;
|
||||
},
|
||||
__wbindgen_cast_0000000000000002: function(arg0) {
|
||||
// Cast intrinsic for `I64 -> Externref`.
|
||||
const ret = arg0;
|
||||
return ret;
|
||||
},
|
||||
__wbindgen_cast_0000000000000003: function(arg0, arg1) {
|
||||
// Cast intrinsic for `Ref(String) -> Externref`.
|
||||
const ret = getStringFromWasm0(arg0, arg1);
|
||||
return ret;
|
||||
},
|
||||
__wbindgen_cast_0000000000000004: function(arg0) {
|
||||
// Cast intrinsic for `U64 -> Externref`.
|
||||
const ret = BigInt.asUintN(64, arg0);
|
||||
return ret;
|
||||
},
|
||||
__wbindgen_init_externref_table: function() {
|
||||
const table = wasm.__wbindgen_externrefs;
|
||||
const offset = table.grow(4);
|
||||
table.set(0, undefined);
|
||||
table.set(offset + 0, undefined);
|
||||
table.set(offset + 1, null);
|
||||
table.set(offset + 2, true);
|
||||
table.set(offset + 3, false);
|
||||
},
|
||||
};
|
||||
return {
|
||||
__proto__: null,
|
||||
"./config_source_wasm_bg.js": import0,
|
||||
};
|
||||
}
|
||||
|
||||
function addToExternrefTable0(obj) {
|
||||
const idx = wasm.__externref_table_alloc();
|
||||
wasm.__wbindgen_externrefs.set(idx, obj);
|
||||
return idx;
|
||||
}
|
||||
|
||||
function debugString(val) {
|
||||
// primitive types
|
||||
const type = typeof val;
|
||||
if (type == 'number' || type == 'boolean' || val == null) {
|
||||
return `${val}`;
|
||||
}
|
||||
if (type == 'string') {
|
||||
return `"${val}"`;
|
||||
}
|
||||
if (type == 'symbol') {
|
||||
const description = val.description;
|
||||
if (description == null) {
|
||||
return 'Symbol';
|
||||
} else {
|
||||
return `Symbol(${description})`;
|
||||
}
|
||||
}
|
||||
if (type == 'function') {
|
||||
const name = val.name;
|
||||
if (typeof name == 'string' && name.length > 0) {
|
||||
return `Function(${name})`;
|
||||
} else {
|
||||
return 'Function';
|
||||
}
|
||||
}
|
||||
// objects
|
||||
if (Array.isArray(val)) {
|
||||
const length = val.length;
|
||||
let debug = '[';
|
||||
if (length > 0) {
|
||||
debug += debugString(val[0]);
|
||||
}
|
||||
for(let i = 1; i < length; i++) {
|
||||
debug += ', ' + debugString(val[i]);
|
||||
}
|
||||
debug += ']';
|
||||
return debug;
|
||||
}
|
||||
// Test for built-in
|
||||
const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
|
||||
let className;
|
||||
if (builtInMatches && builtInMatches.length > 1) {
|
||||
className = builtInMatches[1];
|
||||
} else {
|
||||
// Failed to match the standard '[object ClassName]'
|
||||
return toString.call(val);
|
||||
}
|
||||
if (className == 'Object') {
|
||||
// we're a user defined class or Object
|
||||
// JSON.stringify avoids problems with cycles, and is generally much
|
||||
// easier than looping through ownProperties of `val`.
|
||||
try {
|
||||
return 'Object(' + JSON.stringify(val) + ')';
|
||||
} catch (_) {
|
||||
return 'Object';
|
||||
}
|
||||
}
|
||||
// errors
|
||||
if (val instanceof Error) {
|
||||
return `${val.name}: ${val.message}\n${val.stack}`;
|
||||
}
|
||||
// TODO we could test for more things here, like `Set`s and `Map`s.
|
||||
return className;
|
||||
}
|
||||
|
||||
function getArrayU8FromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
|
||||
}
|
||||
|
||||
let cachedDataViewMemory0 = null;
|
||||
function getDataViewMemory0() {
|
||||
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
|
||||
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
|
||||
}
|
||||
return cachedDataViewMemory0;
|
||||
}
|
||||
|
||||
function getStringFromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
return decodeText(ptr, len);
|
||||
}
|
||||
|
||||
let cachedUint8ArrayMemory0 = null;
|
||||
function getUint8ArrayMemory0() {
|
||||
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
||||
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
||||
}
|
||||
return cachedUint8ArrayMemory0;
|
||||
}
|
||||
|
||||
function handleError(f, args) {
|
||||
try {
|
||||
return f.apply(this, args);
|
||||
} catch (e) {
|
||||
const idx = addToExternrefTable0(e);
|
||||
wasm.__wbindgen_exn_store(idx);
|
||||
}
|
||||
}
|
||||
|
||||
function isLikeNone(x) {
|
||||
return x === undefined || x === null;
|
||||
}
|
||||
|
||||
function passStringToWasm0(arg, malloc, realloc) {
|
||||
if (realloc === undefined) {
|
||||
const buf = cachedTextEncoder.encode(arg);
|
||||
const ptr = malloc(buf.length, 1) >>> 0;
|
||||
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
||||
WASM_VECTOR_LEN = buf.length;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
let len = arg.length;
|
||||
let ptr = malloc(len, 1) >>> 0;
|
||||
|
||||
const mem = getUint8ArrayMemory0();
|
||||
|
||||
let offset = 0;
|
||||
|
||||
for (; offset < len; offset++) {
|
||||
const code = arg.charCodeAt(offset);
|
||||
if (code > 0x7F) break;
|
||||
mem[ptr + offset] = code;
|
||||
}
|
||||
if (offset !== len) {
|
||||
if (offset !== 0) {
|
||||
arg = arg.slice(offset);
|
||||
}
|
||||
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
||||
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
||||
const ret = cachedTextEncoder.encodeInto(arg, view);
|
||||
|
||||
offset += ret.written;
|
||||
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
||||
}
|
||||
|
||||
WASM_VECTOR_LEN = offset;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
function takeFromExternrefTable0(idx) {
|
||||
const value = wasm.__wbindgen_externrefs.get(idx);
|
||||
wasm.__externref_table_dealloc(idx);
|
||||
return value;
|
||||
}
|
||||
|
||||
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
||||
cachedTextDecoder.decode();
|
||||
const MAX_SAFARI_DECODE_BYTES = 2146435072;
|
||||
let numBytesDecoded = 0;
|
||||
function decodeText(ptr, len) {
|
||||
numBytesDecoded += len;
|
||||
if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
|
||||
cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
||||
cachedTextDecoder.decode();
|
||||
numBytesDecoded = len;
|
||||
}
|
||||
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
||||
}
|
||||
|
||||
const cachedTextEncoder = new TextEncoder();
|
||||
|
||||
if (!('encodeInto' in cachedTextEncoder)) {
|
||||
cachedTextEncoder.encodeInto = function (arg, view) {
|
||||
const buf = cachedTextEncoder.encode(arg);
|
||||
view.set(buf);
|
||||
return {
|
||||
read: arg.length,
|
||||
written: buf.length
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
let WASM_VECTOR_LEN = 0;
|
||||
|
||||
let wasmModule, wasm;
|
||||
function __wbg_finalize_init(instance, module) {
|
||||
wasm = instance.exports;
|
||||
wasmModule = module;
|
||||
cachedDataViewMemory0 = null;
|
||||
cachedUint8ArrayMemory0 = null;
|
||||
wasm.__wbindgen_start();
|
||||
return wasm;
|
||||
}
|
||||
|
||||
async function __wbg_load(module, imports) {
|
||||
if (typeof Response === 'function' && module instanceof Response) {
|
||||
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||
try {
|
||||
return await WebAssembly.instantiateStreaming(module, imports);
|
||||
} catch (e) {
|
||||
const validResponse = module.ok && expectedResponseType(module.type);
|
||||
|
||||
if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') {
|
||||
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
|
||||
|
||||
} else { throw e; }
|
||||
}
|
||||
}
|
||||
|
||||
const bytes = await module.arrayBuffer();
|
||||
return await WebAssembly.instantiate(bytes, imports);
|
||||
} else {
|
||||
const instance = await WebAssembly.instantiate(module, imports);
|
||||
|
||||
if (instance instanceof WebAssembly.Instance) {
|
||||
return { instance, module };
|
||||
} else {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
function expectedResponseType(type) {
|
||||
switch (type) {
|
||||
case 'basic': case 'cors': case 'default': return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function initSync(module) {
|
||||
if (wasm !== undefined) return wasm;
|
||||
|
||||
|
||||
if (module !== undefined) {
|
||||
if (Object.getPrototypeOf(module) === Object.prototype) {
|
||||
({module} = module)
|
||||
} else {
|
||||
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
|
||||
}
|
||||
}
|
||||
|
||||
const imports = __wbg_get_imports();
|
||||
if (!(module instanceof WebAssembly.Module)) {
|
||||
module = new WebAssembly.Module(module);
|
||||
}
|
||||
const instance = new WebAssembly.Instance(module, imports);
|
||||
return __wbg_finalize_init(instance, module);
|
||||
}
|
||||
|
||||
async function __wbg_init(module_or_path) {
|
||||
if (wasm !== undefined) return wasm;
|
||||
|
||||
|
||||
if (module_or_path !== undefined) {
|
||||
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
|
||||
({module_or_path} = module_or_path)
|
||||
} else {
|
||||
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
|
||||
}
|
||||
}
|
||||
|
||||
if (module_or_path === undefined) {
|
||||
module_or_path = new URL('config_source_wasm_bg.wasm', import.meta.url);
|
||||
}
|
||||
const imports = __wbg_get_imports();
|
||||
|
||||
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
|
||||
module_or_path = fetch(module_or_path);
|
||||
}
|
||||
|
||||
const { instance, module } = await __wbg_load(await module_or_path, imports);
|
||||
|
||||
return __wbg_finalize_init(instance, module);
|
||||
}
|
||||
|
||||
export { initSync, __wbg_init as default };
|
||||
Binary file not shown.
+15
@@ -0,0 +1,15 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export const memory: WebAssembly.Memory;
|
||||
export const analyze_snapshot: (a: any, b: number, c: number, d: number, e: number) => [number, number, number];
|
||||
export const evaluate_snapshot: (a: any, b: any) => [number, number, number];
|
||||
export const format_source: (a: number, b: number) => [number, number, number, number];
|
||||
export const formatSource: (a: number, b: number) => [number, number];
|
||||
export const __wbindgen_malloc: (a: number, b: number) => number;
|
||||
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
export const __wbindgen_exn_store: (a: number) => void;
|
||||
export const __externref_table_alloc: () => number;
|
||||
export const __wbindgen_externrefs: WebAssembly.Table;
|
||||
export const __externref_table_dealloc: (a: number) => void;
|
||||
export const __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||
export const __wbindgen_start: () => void;
|
||||
@@ -0,0 +1,76 @@
|
||||
import type {
|
||||
ConfigDiagnostic,
|
||||
ConfigTreeSnapshot,
|
||||
ToolchainContract,
|
||||
} from "./types.ts";
|
||||
import type {
|
||||
ConfigSourceWorkerRequest,
|
||||
ConfigSourceWorkerResponse,
|
||||
} from "./toolchain.worker.ts";
|
||||
|
||||
type ConfigSourceWorkerCommand =
|
||||
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "analyze" }>, "id">
|
||||
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "evaluate" }>, "id">
|
||||
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "format" }>, "id">;
|
||||
|
||||
export class ConfigSourceToolchain {
|
||||
#worker: Worker;
|
||||
#nextId = 1;
|
||||
#pending = new Map<
|
||||
number,
|
||||
{ resolve: (value: unknown) => void; reject: (reason: unknown) => void }
|
||||
>();
|
||||
|
||||
constructor(
|
||||
worker = new Worker(new URL("./toolchain.worker.ts", import.meta.url), {
|
||||
type: "module",
|
||||
}),
|
||||
) {
|
||||
this.#worker = worker;
|
||||
worker.addEventListener(
|
||||
"message",
|
||||
(event: MessageEvent<ConfigSourceWorkerResponse>) => {
|
||||
const pending = this.#pending.get(event.data.id);
|
||||
if (!pending) return;
|
||||
this.#pending.delete(event.data.id);
|
||||
if (event.data.ok) pending.resolve(event.data.result);
|
||||
else pending.reject(event.data.error);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
analyze(
|
||||
snapshot: ConfigTreeSnapshot,
|
||||
path: string,
|
||||
source?: string,
|
||||
): Promise<ConfigDiagnostic[]> {
|
||||
return this.#request({ kind: "analyze", snapshot, path, source });
|
||||
}
|
||||
|
||||
evaluate(snapshot: ConfigTreeSnapshot, contract: ToolchainContract) {
|
||||
return this.#request({ kind: "evaluate", snapshot, contract });
|
||||
}
|
||||
|
||||
format(source: string): Promise<string> {
|
||||
return this.#request({ kind: "format", source });
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.#worker.terminate();
|
||||
for (const pending of this.#pending.values()) {
|
||||
pending.reject(new Error("config source toolchain was closed"));
|
||||
}
|
||||
this.#pending.clear();
|
||||
}
|
||||
|
||||
#request<T>(request: ConfigSourceWorkerCommand): Promise<T> {
|
||||
const id = this.#nextId++;
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
this.#pending.set(id, {
|
||||
resolve: (value) => resolve(value as T),
|
||||
reject,
|
||||
});
|
||||
this.#worker.postMessage({ ...request, id });
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import init, {
|
||||
analyze_snapshot,
|
||||
evaluate_snapshot,
|
||||
format_source,
|
||||
} from "./generated/config_source_wasm.js";
|
||||
import type {
|
||||
ConfigDiagnostic,
|
||||
ConfigTreeSnapshot,
|
||||
ToolchainContract,
|
||||
} from "./types.ts";
|
||||
|
||||
export type ConfigSourceWorkerRequest =
|
||||
| {
|
||||
id: number;
|
||||
kind: "analyze";
|
||||
snapshot: ConfigTreeSnapshot;
|
||||
path: string;
|
||||
source?: string;
|
||||
}
|
||||
| {
|
||||
id: number;
|
||||
kind: "evaluate";
|
||||
snapshot: ConfigTreeSnapshot;
|
||||
contract: ToolchainContract;
|
||||
}
|
||||
| { id: number; kind: "format"; source: string };
|
||||
|
||||
export type ConfigSourceWorkerResponse =
|
||||
| { id: number; ok: true; result: unknown }
|
||||
| { id: number; ok: false; error: unknown };
|
||||
|
||||
const ready = init();
|
||||
|
||||
self.onmessage = async (
|
||||
event: MessageEvent<ConfigSourceWorkerRequest>,
|
||||
): Promise<void> => {
|
||||
const request = event.data;
|
||||
try {
|
||||
await ready;
|
||||
let result: unknown;
|
||||
switch (request.kind) {
|
||||
case "analyze":
|
||||
result = analyze_snapshot(
|
||||
request.snapshot,
|
||||
request.path,
|
||||
request.source,
|
||||
) as ConfigDiagnostic[];
|
||||
break;
|
||||
case "evaluate":
|
||||
result = evaluate_snapshot(request.snapshot, request.contract);
|
||||
break;
|
||||
case "format":
|
||||
result = format_source(request.source);
|
||||
break;
|
||||
}
|
||||
self.postMessage({ id: request.id, ok: true, result });
|
||||
} catch (error) {
|
||||
self.postMessage({
|
||||
id: request.id,
|
||||
ok: false,
|
||||
error: normalizeError(error),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function normalizeError(error: unknown): unknown {
|
||||
if (error instanceof Error) return error.message;
|
||||
return error;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
export type VirtualPath = string;
|
||||
|
||||
export type ConfigContentType = "decodal" | "text";
|
||||
|
||||
export interface ConfigEntry {
|
||||
path: VirtualPath;
|
||||
content_type: ConfigContentType;
|
||||
content: string;
|
||||
content_digest: string;
|
||||
}
|
||||
|
||||
export interface ConfigTreeSnapshot {
|
||||
revision: number;
|
||||
digest: string;
|
||||
entries: Record<VirtualPath, ConfigEntry>;
|
||||
}
|
||||
|
||||
export type ConfigTreeChange =
|
||||
| {
|
||||
kind: "create";
|
||||
path: VirtualPath;
|
||||
content_type: ConfigContentType;
|
||||
content: string;
|
||||
}
|
||||
| {
|
||||
kind: "update";
|
||||
path: VirtualPath;
|
||||
expected_digest: string;
|
||||
content: string;
|
||||
}
|
||||
| {
|
||||
kind: "rename";
|
||||
from: VirtualPath;
|
||||
to: VirtualPath;
|
||||
expected_digest: string;
|
||||
}
|
||||
| {
|
||||
kind: "delete";
|
||||
path: VirtualPath;
|
||||
expected_digest: string;
|
||||
};
|
||||
|
||||
export interface ToolchainContract {
|
||||
contract_version: number;
|
||||
decodal_version: string;
|
||||
schema_version: number;
|
||||
entrypoints: VirtualPath[];
|
||||
import_policy_version: number;
|
||||
fingerprint: string;
|
||||
}
|
||||
|
||||
export interface ConfigDiagnostic {
|
||||
path: VirtualPath;
|
||||
revision: number;
|
||||
tree_digest: string;
|
||||
kind: string;
|
||||
span: { start_byte: number; end_byte: number };
|
||||
message: string;
|
||||
labels: Array<{
|
||||
span: { start_byte: number; end_byte: number };
|
||||
message: string;
|
||||
}>;
|
||||
notes: string[];
|
||||
}
|
||||
|
||||
export interface WorkspaceConfigTreeResponse {
|
||||
snapshot: ConfigTreeSnapshot;
|
||||
contract: ToolchainContract;
|
||||
projection_digest: string;
|
||||
}
|
||||
|
||||
export interface EvaluatedConfigCandidate {
|
||||
base_revision: number;
|
||||
base_digest: string;
|
||||
snapshot: ConfigTreeSnapshot;
|
||||
contract: ToolchainContract;
|
||||
evaluation: {
|
||||
projections: Array<{
|
||||
entrypoint: VirtualPath;
|
||||
data_json: unknown;
|
||||
projection_digest: string;
|
||||
}>;
|
||||
projection_digest: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ConfigCommitRequest {
|
||||
base_revision: number;
|
||||
base_digest: string;
|
||||
changes: ConfigTreeChange[];
|
||||
entrypoints: VirtualPath[];
|
||||
}
|
||||
@@ -7,6 +7,7 @@ export type Diagnostic = {
|
||||
export type SettingsSectionId =
|
||||
| "runtime-connections"
|
||||
| "runtime-inventory"
|
||||
| "configuration-sources"
|
||||
| "profile-sources"
|
||||
| "backend-config"
|
||||
| "workspace-identity";
|
||||
@@ -98,6 +99,18 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
|
||||
"Console routes may still target a Runtime handle directly, but Runtime discovery belongs under Settings.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "configuration-sources",
|
||||
label: "Configuration Sources",
|
||||
status: "editable",
|
||||
summary:
|
||||
"Edit the Server-owned virtual Decodal source tree through one native/WASM toolchain contract.",
|
||||
bullets: [
|
||||
"Virtual paths and imports resolve inside the committed Workspace tree, never from browser or Server host paths.",
|
||||
"Browser analysis is advisory; Server evaluation is required before an atomic revision commit.",
|
||||
"Profile, Skill, Prompt, and Plugin consumers remain on their existing authorities until their follow-up cutovers.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "profile-sources",
|
||||
label: "Profile Sources",
|
||||
@@ -160,6 +173,8 @@ export function settingsSectionHref(id: SettingsSectionId): string {
|
||||
return `${SETTINGS_ROUTE}/runtime-connections`;
|
||||
case "runtime-inventory":
|
||||
return `${SETTINGS_ROUTE}/runtimes`;
|
||||
case "configuration-sources":
|
||||
return `${SETTINGS_ROUTE}/configuration`;
|
||||
case "profile-sources":
|
||||
return `${SETTINGS_ROUTE}/profiles`;
|
||||
case "workspace-identity":
|
||||
|
||||
@@ -515,6 +515,157 @@
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.config-source-shell {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(12rem, 18rem) minmax(0, 1fr);
|
||||
min-height: 40rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-panel);
|
||||
background: var(--bg-raised);
|
||||
}
|
||||
.config-source-tree {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
border-right: 1px solid var(--line);
|
||||
background: var(--bg-subtle);
|
||||
}
|
||||
.config-source-tree__header,
|
||||
.config-source-workbench__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.config-source-tree nav {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
overflow: auto;
|
||||
padding: var(--space-2);
|
||||
}
|
||||
.config-source-tree nav button {
|
||||
border: 0;
|
||||
border-radius: 0.45rem;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
padding: 0.45rem 0.55rem;
|
||||
font: inherit;
|
||||
font-family: var(--font-mono);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.config-source-tree nav button.active,
|
||||
.config-source-tree nav button:hover {
|
||||
background: var(--interactive-hover);
|
||||
color: var(--text-strong);
|
||||
}
|
||||
.config-source-create {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-3);
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.config-source-create label,
|
||||
.config-source-workbench__header span {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.config-source-create input {
|
||||
min-width: 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 0.45rem;
|
||||
background: var(--bg-raised);
|
||||
color: var(--text-strong);
|
||||
padding: 0.45rem;
|
||||
font: inherit;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.config-source-create button,
|
||||
.config-source-actions button {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 0.45rem;
|
||||
background: var(--bg-subtle);
|
||||
color: var(--text-strong);
|
||||
padding: 0.4rem 0.6rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.config-source-actions button.primary {
|
||||
border-color: transparent;
|
||||
background: var(--accent);
|
||||
color: var(--bg);
|
||||
}
|
||||
.config-source-actions button.danger {
|
||||
color: var(--danger);
|
||||
}
|
||||
.config-source-actions button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
}
|
||||
.config-source-workbench {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(20rem, 1fr) auto auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.config-source-workbench__header > div:first-child {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
}
|
||||
.config-source-workbench__header strong {
|
||||
overflow: hidden;
|
||||
font-family: var(--font-mono);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.config-source-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.config-source-status {
|
||||
margin: 0;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border-top: 1px solid var(--line);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.config-source-diagnostics {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
max-height: 12rem;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
padding: var(--space-3);
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.config-source-diagnostics li {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
gap: var(--space-2);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.config-source-diagnostics strong {
|
||||
color: var(--danger);
|
||||
}
|
||||
@media (max-width: 48rem) {
|
||||
.config-source-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.config-source-tree {
|
||||
grid-template-rows: auto auto auto;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.config-source-tree nav {
|
||||
max-height: 12rem;
|
||||
}
|
||||
.config-source-workbench__header {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.status-message.error {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import ConfigSourceEditor from "$lib/workspace/config-source/ConfigSourceEditor.svelte";
|
||||
import type { PageProps } from "./$types";
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
let workspaceId = $derived(data.workspace?.workspace_id ?? "");
|
||||
let workspaceName = $derived(data.workspace?.display_name ?? "Workspace");
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Configuration | {workspaceName}</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="settings-page">
|
||||
<header class="page-header">
|
||||
<div>
|
||||
<p class="eyebrow">Workspace settings</p>
|
||||
<h2>Configuration</h2>
|
||||
<p>
|
||||
Edit the Server-owned virtual Decodal source tree. Drafts stay in this browser;
|
||||
the Server persists only a fully evaluated candidate.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<ConfigSourceEditor {workspaceId} />
|
||||
</section>
|
||||
Reference in New Issue
Block a user