config: enforce browser preflight and generated DTOs

This commit is contained in:
2026-08-14 01:27:29 +09:00
parent fa9bd8207f
commit 544abbbf56
29 changed files with 259 additions and 128 deletions
Generated
+1
View File
@@ -601,6 +601,7 @@ dependencies = [
"serde_json",
"sha2 0.11.0",
"thiserror 2.0.18",
"ts-rs",
]
[[package]]
+27 -1
View File
@@ -32,6 +32,13 @@ pub fn apply_changes(changes: JsValue) -> Result<JsValue, JsValue> {
})
}
#[wasm_bindgen]
pub fn changes_between(base: JsValue, candidate: JsValue) -> Result<JsValue, JsValue> {
let base: ConfigTreeSnapshot = decode(base)?;
let candidate: ConfigTreeSnapshot = decode(candidate)?;
encode(base.changes_to(&candidate))
}
#[wasm_bindgen]
pub fn evaluate_current(contract: JsValue) -> Result<JsValue, JsValue> {
let contract: ToolchainContract = decode(contract)?;
@@ -68,7 +75,7 @@ struct WasmCompletionItem {
pub fn complete_current(
entrypoint: String,
source: String,
utf8_byte_offset: usize,
utf16_offset: usize,
explicit: bool,
) -> Result<JsValue, JsValue> {
let entrypoint = VirtualPath::parse(entrypoint).map_err(js_error)?;
@@ -77,6 +84,7 @@ pub fn complete_current(
let snapshot = session
.as_ref()
.ok_or_else(|| JsValue::from_str("config source snapshot is not initialized"))?;
let utf8_byte_offset = utf16_to_utf8_offset(&source, utf16_offset)?;
let result = SnapshotEnvironment::new(snapshot.clone())
.complete(&entrypoint, &source, utf8_byte_offset, explicit)
.map_err(|error| JsValue::from_str(&format!("{error:?}")))?
@@ -128,6 +136,24 @@ pub fn format_source(source: String) -> Result<String, JsValue> {
.map_err(|error| JsValue::from_str(&error))
}
fn utf16_to_utf8_offset(source: &str, utf16_offset: usize) -> Result<usize, JsValue> {
let mut units = 0usize;
for (byte_offset, character) in source.char_indices() {
if units == utf16_offset {
return Ok(byte_offset);
}
units += character.len_utf16();
if units > utf16_offset {
return Err(JsValue::from_str("UTF-16 offset splits a surrogate pair"));
}
}
if units == utf16_offset {
Ok(source.len())
} else {
Err(JsValue::from_str("UTF-16 offset is outside the source"))
}
}
fn decode<T: serde::de::DeserializeOwned>(value: JsValue) -> Result<T, JsValue> {
from_value(value).map_err(|error| JsValue::from_str(&error.to_string()))
}
+1
View File
@@ -13,6 +13,7 @@ serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
sha2.workspace = true
thiserror.workspace = true
ts-rs = "12.0.1"
[dev-dependencies]
pretty_assertions = "1"
+71 -11
View File
@@ -19,7 +19,7 @@ pub const MAX_ENTRY_BYTES: usize = 256 * 1024;
pub const MAX_TOTAL_BYTES: usize = 4 * 1024 * 1024;
pub const MAX_PATH_BYTES: usize = 512;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, ts_rs::TS)]
#[serde(transparent)]
pub struct VirtualPath(String);
@@ -71,7 +71,7 @@ impl fmt::Display for VirtualPath {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
#[serde(rename_all = "snake_case")]
pub enum ConfigContentType {
Decodal,
@@ -87,7 +87,7 @@ impl ConfigContentType {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
pub struct ConfigEntry {
pub path: VirtualPath,
pub content_type: ConfigContentType,
@@ -114,8 +114,9 @@ impl ConfigEntry {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
pub struct ConfigTreeSnapshot {
#[ts(type = "number")]
pub revision: u64,
pub digest: String,
pub entries: BTreeMap<VirtualPath, ConfigEntry>,
@@ -174,6 +175,38 @@ impl ConfigTreeSnapshot {
self.entries.get(path)
}
pub fn changes_to(&self, candidate: &Self) -> Vec<ConfigTreeChange> {
let mut changes = Vec::new();
for (path, base_entry) in &self.entries {
match candidate.entries.get(path) {
None => changes.push(ConfigTreeChange::Delete {
path: path.clone(),
expected_digest: base_entry.content_digest.clone(),
}),
Some(candidate_entry)
if candidate_entry.content_digest != base_entry.content_digest =>
{
changes.push(ConfigTreeChange::Update {
path: path.clone(),
expected_digest: base_entry.content_digest.clone(),
content: candidate_entry.content.clone(),
});
}
Some(_) => {}
}
}
for (path, entry) in &candidate.entries {
if !self.entries.contains_key(path) {
changes.push(ConfigTreeChange::Create {
path: path.clone(),
content_type: entry.content_type,
content: entry.content.clone(),
});
}
}
changes
}
pub fn apply(&self, changes: &[ConfigTreeChange]) -> Result<Self, ConfigTreeError> {
if changes.len() > MAX_CHANGE_COUNT {
return Err(ConfigTreeError::LimitExceeded("change count"));
@@ -253,7 +286,7 @@ impl ConfigTreeSnapshot {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ConfigTreeChange {
Create {
@@ -288,7 +321,7 @@ impl ConfigTreeChange {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
pub struct ToolchainContract {
pub contract_version: u32,
pub decodal_version: String,
@@ -329,21 +362,22 @@ impl ToolchainContract {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
pub struct ConfigSpan {
pub start_byte: u32,
pub end_byte: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
pub struct ConfigDiagnosticLabel {
pub span: ConfigSpan,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
pub struct ConfigDiagnostic {
pub path: VirtualPath,
#[ts(type = "number")]
pub revision: u64,
pub tree_digest: String,
pub kind: String,
@@ -353,14 +387,16 @@ pub struct ConfigDiagnostic {
pub notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)]
#[ts(export)]
pub struct EvaluatedProjection {
pub entrypoint: VirtualPath,
#[ts(type = "unknown")]
pub data_json: serde_json::Value,
pub projection_digest: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)]
pub struct EvaluationResult {
pub projections: Vec<EvaluatedProjection>,
pub projection_digest: String,
@@ -779,6 +815,30 @@ pub enum ConfigTreeError {
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use ts_rs::TS;
#[test]
fn exports_typescript_contract() {
let output = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../web/workspace/src/lib/workspace/config-source/generated/types");
std::fs::create_dir_all(&output).unwrap();
macro_rules! export {
($type:ty) => {
<$type>::export_all(&ts_rs::Config::default().with_out_dir(&output)).unwrap();
};
}
export!(VirtualPath);
export!(ConfigContentType);
export!(ConfigEntry);
export!(ConfigTreeSnapshot);
export!(ConfigTreeChange);
export!(ToolchainContract);
export!(ConfigSpan);
export!(ConfigDiagnosticLabel);
export!(ConfigDiagnostic);
export!(EvaluatedProjection);
export!(EvaluationResult);
}
fn path(value: &str) -> VirtualPath {
VirtualPath::parse(value).unwrap()
+22 -4
View File
@@ -11,15 +11,18 @@ use crate::{Error, Result, SqliteWorkspaceStore};
pub const DEFAULT_CONFIG_ENTRYPOINT: &str = "workspace.dcdl";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)]
#[ts(export)]
pub struct WorkspaceConfigState {
pub snapshot: ConfigTreeSnapshot,
pub contract: ToolchainContract,
pub projection_digest: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)]
#[ts(export)]
pub struct EvaluatedConfigCandidate {
#[ts(type = "number")]
pub base_revision: u64,
pub base_digest: String,
pub snapshot: ConfigTreeSnapshot,
@@ -27,8 +30,10 @@ pub struct EvaluatedConfigCandidate {
pub evaluation: EvaluationResult,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)]
#[ts(export)]
pub struct ConfigCommitRequest {
#[ts(type = "number")]
pub base_revision: u64,
pub base_digest: String,
pub changes: Vec<ConfigTreeChange>,
@@ -36,7 +41,8 @@ pub struct ConfigCommitRequest {
pub toolchain_fingerprint: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)]
#[ts(export)]
pub struct ConfigPreviewRequest {
pub changes: Vec<ConfigTreeChange>,
pub entrypoints: Vec<VirtualPath>,
@@ -494,6 +500,18 @@ mod tests {
assert!(store.load_workspace_config("w-config").unwrap().is_none());
}
#[test]
fn exports_typescript_transport_contract() {
use ts_rs::TS;
let output = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../web/workspace/src/lib/workspace/config-source/generated/types");
let config = ts_rs::Config::default().with_out_dir(&output);
WorkspaceConfigState::export_all(&config).unwrap();
EvaluatedConfigCandidate::export_all(&config).unwrap();
ConfigCommitRequest::export_all(&config).unwrap();
ConfigPreviewRequest::export_all(&config).unwrap();
}
#[test]
fn migration_creates_config_authority_without_changing_applied_migrations() {
let store = SqliteWorkspaceStore::in_memory().unwrap();
@@ -25,6 +25,8 @@
let baseRevision = $state(0);
let baseDigest = $state("");
let renamePath = $state("");
let baseSnapshot = $state<WorkspaceConfigTreeResponse["snapshot"] | null>(null);
let preflightDigest = $state("");
let toolchain: ConfigSourceToolchain | null = null;
const paths = $derived(
@@ -34,6 +36,7 @@
treeState && selectedPath ? treeState.snapshot.entries[selectedPath] : undefined,
);
const dirty = $derived(draftChanges.length > 0 || (selected ? source !== selected.content : source.length > 0));
const commitReady = $derived(dirty && preflightDigest === treeState?.snapshot.digest);
onMount(() => {
toolchain = new ConfigSourceToolchain();
@@ -48,6 +51,7 @@
selectedPath = Object.keys(treeState.snapshot.entries).toSorted()[0] ?? "";
}
source = selectedPath ? treeState.snapshot.entries[selectedPath].content : "";
baseSnapshot = structuredClone(treeState.snapshot);
baseRevision = treeState.snapshot.revision;
baseDigest = treeState.snapshot.digest;
await toolchain?.setSnapshot(treeState.snapshot);
@@ -65,13 +69,10 @@
async function stageCurrent() {
const change = currentChange();
if (!change || !toolchain) return;
draftChanges = [...draftChanges.filter((item) => !changeTouches(item, selectedPath)), change];
const candidate = await toolchain.applyChanges([change]);
if (treeState) treeState = { ...treeState, snapshot: candidate };
}
function changeTouches(change: ConfigTreeChange, path: string): boolean {
return change.kind === "rename" ? change.from === path || change.to === path : change.path === path;
if (baseSnapshot) draftChanges = await toolchain.changesBetween(baseSnapshot, candidate);
preflightDigest = "";
}
async function select(path: string) {
@@ -144,6 +145,7 @@
toolchain_fingerprint: treeState.contract.fingerprint,
});
diagnostics = [];
preflightDigest = candidate.snapshot.digest;
status = `Preview valid · projection ${candidate.evaluation.projection_digest.slice(0, 20)}…`;
} catch (error) {
status = String(error);
@@ -159,6 +161,10 @@
status = "No draft changes to commit.";
return;
}
if (preflightDigest !== treeState.snapshot.digest) {
status = "Preview the complete candidate successfully before Commit.";
return;
}
busy = true;
try {
treeState = await commitConfigTree(workspaceId, {
@@ -169,6 +175,8 @@
toolchain_fingerprint: treeState.contract.fingerprint,
});
draftChanges = [];
preflightDigest = "";
baseSnapshot = structuredClone(treeState.snapshot);
baseRevision = treeState.snapshot.revision;
baseDigest = treeState.snapshot.digest;
await toolchain?.setSnapshot(treeState.snapshot);
@@ -198,9 +206,10 @@
path: selectedPath,
expected_digest: selected.content_digest,
};
draftChanges = [...draftChanges.filter((item) => !changeTouches(item, selectedPath)), change];
const candidate = await toolchain.applyChanges([change]);
treeState = { ...treeState, snapshot: candidate };
if (baseSnapshot) draftChanges = await toolchain.changesBetween(baseSnapshot, candidate);
preflightDigest = "";
selectedPath = Object.keys(candidate.entries).toSorted()[0] ?? "";
source = selectedPath ? candidate.entries[selectedPath].content : "";
renamePath = selectedPath;
@@ -218,9 +227,10 @@
to,
expected_digest: selected.content_digest,
};
draftChanges = [...draftChanges.filter((item) => !changeTouches(item, selectedPath)), change];
const candidate = await toolchain.applyChanges([change]);
treeState = { ...treeState, snapshot: candidate };
if (baseSnapshot) draftChanges = await toolchain.changesBetween(baseSnapshot, candidate);
preflightDigest = "";
selectedPath = to;
source = candidate.entries[to]?.content ?? "";
status = `Rename to ${to} staged. Preview and Commit to persist.`;
@@ -261,7 +271,7 @@
<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="primary" type="button" onclick={commit} disabled={!commitReady || busy}>Commit</button>
<button class="danger" type="button" onclick={deleteEntry} disabled={!selected || busy}>Delete</button>
</div>
</header>
@@ -5,7 +5,9 @@ export function analyze_snapshot(snapshot: any, entrypoint: string, source_overr
export function apply_changes(changes: any): any;
export function complete_current(entrypoint: string, source: string, utf8_byte_offset: number, explicit: boolean): any;
export function changes_between(base: any, candidate: any): any;
export function complete_current(entrypoint: string, source: string, utf16_offset: number, explicit: boolean): any;
export function evaluate_current(contract: any): any;
@@ -23,6 +25,7 @@ export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly analyze_snapshot: (a: any, b: number, c: number, d: number, e: number) => [number, number, number];
readonly apply_changes: (a: any) => [number, number, number];
readonly changes_between: (a: any, b: any) => [number, number, number];
readonly complete_current: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number];
readonly evaluate_current: (a: any) => [number, number, number];
readonly evaluate_snapshot: (a: any, b: any) => [number, number, number];
@@ -30,19 +30,32 @@ export function apply_changes(changes) {
return takeFromExternrefTable0(ret[0]);
}
/**
* @param {any} base
* @param {any} candidate
* @returns {any}
*/
export function changes_between(base, candidate) {
const ret = wasm.changes_between(base, candidate);
if (ret[2]) {
throw takeFromExternrefTable0(ret[1]);
}
return takeFromExternrefTable0(ret[0]);
}
/**
* @param {string} entrypoint
* @param {string} source
* @param {number} utf8_byte_offset
* @param {number} utf16_offset
* @param {boolean} explicit
* @returns {any}
*/
export function complete_current(entrypoint, source, utf8_byte_offset, explicit) {
export function complete_current(entrypoint, source, utf16_offset, explicit) {
const ptr0 = passStringToWasm0(entrypoint, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(source, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.complete_current(ptr0, len0, ptr1, len1, utf8_byte_offset, explicit);
const ret = wasm.complete_current(ptr0, len0, ptr1, len1, utf16_offset, explicit);
if (ret[2]) {
throw takeFromExternrefTable0(ret[1]);
}
@@ -3,6 +3,7 @@
export const memory: WebAssembly.Memory;
export const analyze_snapshot: (a: any, b: number, c: number, d: number, e: number) => [number, number, number];
export const apply_changes: (a: any) => [number, number, number];
export const changes_between: (a: any, b: any) => [number, number, number];
export const complete_current: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number];
export const evaluate_current: (a: any) => [number, number, number];
export const evaluate_snapshot: (a: any, b: any) => [number, number, number];
@@ -0,0 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ConfigTreeChange } from "./ConfigTreeChange";
import type { VirtualPath } from "./VirtualPath";
export type ConfigCommitRequest = { base_revision: number, base_digest: string, changes: Array<ConfigTreeChange>, entrypoints: Array<VirtualPath>, toolchain_fingerprint: string, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ConfigContentType = "decodal" | "text";
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ConfigDiagnosticLabel } from "./ConfigDiagnosticLabel";
import type { ConfigSpan } from "./ConfigSpan";
import type { VirtualPath } from "./VirtualPath";
export type ConfigDiagnostic = { path: VirtualPath, revision: number, tree_digest: string, kind: string, span: ConfigSpan, message: string, labels: Array<ConfigDiagnosticLabel>, notes: Array<string>, };
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ConfigSpan } from "./ConfigSpan";
export type ConfigDiagnosticLabel = { span: ConfigSpan, message: string, };
@@ -0,0 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ConfigContentType } from "./ConfigContentType";
import type { VirtualPath } from "./VirtualPath";
export type ConfigEntry = { path: VirtualPath, content_type: ConfigContentType, content: string, content_digest: string, };
@@ -0,0 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ConfigTreeChange } from "./ConfigTreeChange";
import type { VirtualPath } from "./VirtualPath";
export type ConfigPreviewRequest = { changes: Array<ConfigTreeChange>, entrypoints: Array<VirtualPath>, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ConfigSpan = { start_byte: number, end_byte: number, };
@@ -0,0 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ConfigContentType } from "./ConfigContentType";
import type { VirtualPath } from "./VirtualPath";
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, };
@@ -0,0 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ConfigEntry } from "./ConfigEntry";
import type { VirtualPath } from "./VirtualPath";
export type ConfigTreeSnapshot = { revision: number, digest: string, entries: { [key in VirtualPath]: ConfigEntry }, };
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ConfigTreeSnapshot } from "./ConfigTreeSnapshot";
import type { EvaluationResult } from "./EvaluationResult";
import type { ToolchainContract } from "./ToolchainContract";
export type EvaluatedConfigCandidate = { base_revision: number, base_digest: string, snapshot: ConfigTreeSnapshot, contract: ToolchainContract, evaluation: EvaluationResult, };
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { VirtualPath } from "./VirtualPath";
export type EvaluatedProjection = { entrypoint: VirtualPath, data_json: unknown, projection_digest: string, };
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { EvaluatedProjection } from "./EvaluatedProjection";
export type EvaluationResult = { projections: Array<EvaluatedProjection>, projection_digest: string, };
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { VirtualPath } from "./VirtualPath";
export type ToolchainContract = { contract_version: number, decodal_version: string, schema_version: number, entrypoints: Array<VirtualPath>, import_policy_version: number, fingerprint: string, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type VirtualPath = string;
@@ -0,0 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ConfigTreeSnapshot } from "./ConfigTreeSnapshot";
import type { ToolchainContract } from "./ToolchainContract";
export type WorkspaceConfigState = { snapshot: ConfigTreeSnapshot, contract: ToolchainContract, projection_digest: string, };
@@ -4,6 +4,7 @@ import type { ConfigSourceWorkerRequest, ConfigSourceWorkerResponse } from "./to
type Command =
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "set_snapshot" }>, "id">
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "apply_changes" }>, "id">
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "changes_between" }>, "id">
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "analyze" }>, "id">
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "evaluate" }>, "id">
| Omit<Extract<ConfigSourceWorkerRequest, { kind: "complete" }>, "id">
@@ -31,14 +32,17 @@ export class ConfigSourceToolchain {
applyChanges(changes: ConfigTreeChange[]): Promise<ConfigTreeSnapshot> {
return this.#request({ kind: "apply_changes", changes });
}
changesBetween(base: ConfigTreeSnapshot, candidate: ConfigTreeSnapshot): Promise<ConfigTreeChange[]> {
return this.#request({ kind: "changes_between", base, candidate });
}
analyze(path: string, source?: string): Promise<ConfigDiagnostic[]> {
return this.#request({ kind: "analyze", path, source });
}
evaluate(contract: ToolchainContract) {
return this.#request({ kind: "evaluate", contract });
}
complete(path: string, source: string, utf8ByteOffset: number, explicit = false): Promise<import("@codemirror/autocomplete").CompletionResult | null> {
return this.#request({ kind: "complete", path, source, utf8ByteOffset, explicit });
complete(path: string, source: string, utf16Offset: number, explicit = false): Promise<import("@codemirror/autocomplete").CompletionResult | null> {
return this.#request({ kind: "complete", path, source, utf16Offset, explicit });
}
format(source: string): Promise<string> {
return this.#request({ kind: "format", source });
@@ -1,6 +1,7 @@
import init, {
analyze_snapshot,
apply_changes,
changes_between,
complete_current,
evaluate_current,
format_source,
@@ -11,9 +12,10 @@ import type { ConfigTreeChange } from "./types.ts";
export type ConfigSourceWorkerRequest =
| { id: number; kind: "set_snapshot"; snapshot: unknown }
| { id: number; kind: "apply_changes"; changes: ConfigTreeChange[] }
| { id: number; kind: "changes_between"; base: unknown; candidate: unknown }
| { id: number; kind: "analyze"; path: string; source?: string }
| { id: number; kind: "evaluate"; contract: unknown }
| { id: number; kind: "complete"; path: string; source: string; utf8ByteOffset: number; explicit: boolean }
| { id: number; kind: "complete"; path: string; source: string; utf16Offset: number; explicit: boolean }
| { id: number; kind: "format"; source: string };
export type ConfigSourceWorkerResponse =
@@ -38,6 +40,9 @@ self.onmessage = async (event: MessageEvent<ConfigSourceWorkerRequest>): Promise
snapshot = apply_changes(request.changes);
result = snapshot;
break;
case "changes_between":
result = changes_between(request.base, request.candidate);
break;
case "analyze":
if (!snapshot) throw new Error("config source snapshot is not initialized");
result = analyze_snapshot(snapshot, request.path, request.source);
@@ -46,7 +51,7 @@ self.onmessage = async (event: MessageEvent<ConfigSourceWorkerRequest>): Promise
result = evaluate_current(request.contract);
break;
case "complete":
result = complete_current(request.path, request.source, request.utf8ByteOffset, request.explicit);
result = complete_current(request.path, request.source, request.utf16Offset, request.explicit);
break;
case "format":
result = format_source(request.source);
@@ -1,93 +1,16 @@
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[];
toolchain_fingerprint: string;
}
// Core and HTTP DTOs are generated from Rust with ts-rs.
export type { ConfigContentType } from "./generated/types/ConfigContentType.ts";
export type { ConfigDiagnostic } from "./generated/types/ConfigDiagnostic.ts";
export type { ConfigDiagnosticLabel } from "./generated/types/ConfigDiagnosticLabel.ts";
export type { ConfigEntry } from "./generated/types/ConfigEntry.ts";
export type { ConfigSpan } from "./generated/types/ConfigSpan.ts";
export type { ConfigTreeChange } from "./generated/types/ConfigTreeChange.ts";
export type { ConfigTreeSnapshot } from "./generated/types/ConfigTreeSnapshot.ts";
export type { EvaluatedProjection } from "./generated/types/EvaluatedProjection.ts";
export type { EvaluationResult } from "./generated/types/EvaluationResult.ts";
export type { ToolchainContract } from "./generated/types/ToolchainContract.ts";
export type { VirtualPath } from "./generated/types/VirtualPath.ts";
export type { ConfigCommitRequest } from "./generated/types/ConfigCommitRequest.ts";
export type { ConfigPreviewRequest } from "./generated/types/ConfigPreviewRequest.ts";
export type { EvaluatedConfigCandidate } from "./generated/types/EvaluatedConfigCandidate.ts";
export type { WorkspaceConfigState as WorkspaceConfigTreeResponse } from "./generated/types/WorkspaceConfigState.ts";
@@ -16,7 +16,7 @@
readonly?: boolean;
ariaLabel?: string;
onChange?: (value: string) => void;
onComplete?: (source: string, utf8ByteOffset: number, explicit: boolean) => Promise<CompletionResult | null>;
onComplete?: (source: string, utf16Offset: number, explicit: boolean) => Promise<CompletionResult | null>;
} = $props();
let host = $state<HTMLDivElement | null>(null);
@@ -54,8 +54,7 @@
decodal(),
...(handleComplete ? [autocompletion({ override: [async (context: CompletionContext) => {
const doc = context.state.doc.toString();
const utf8ByteOffset = new TextEncoder().encode(doc.slice(0, context.pos)).byteLength;
return await handleComplete(doc, utf8ByteOffset, context.explicit);
return await handleComplete(doc, context.pos, context.explicit);
}] })] : []),
keymap.of([]),
EditorState.readOnly.of(initialReadonly),