config: enforce browser preflight and generated DTOs
This commit is contained in:
Generated
+1
@@ -601,6 +601,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sha2 0.11.0",
|
||||
"thiserror 2.0.18",
|
||||
"ts-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
+4
-1
@@ -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]);
|
||||
}
|
||||
|
||||
Binary file not shown.
+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 }, };
|
||||
+6
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user