From 3bc58c1d56221af113797b71855919488be65569 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:12:59 +0000 Subject: [PATCH 01/18] Initial plan From 8b4fb07d61c5256a47134d6c05eba56a7f87b194 Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Thu, 27 Aug 2026 11:37:47 +0100 Subject: [PATCH 02/18] feat(coln-store): update to newer version of hexnae Co-authored-by: acurrieclark <1306728+acurrieclark@users.noreply.github.com> --- Cargo.lock | 4 ++-- packages/coln-store/Cargo.toml | 2 +- packages/coln-store/src/commit/wire/prim.rs | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 47e4537c..3855a109 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2408,9 +2408,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hexane" -version = "1.0.0-alpha.2" +version = "1.0.0-alpha.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6fa73ae43aeadf80e9032268bcb216fb1bd1930a08d7f73e44bb0b740c3d702" +checksum = "81df830f11babb0eee9a082e09135ef0a7d8791f3fe9b279bb9485bf7eaf23e0" dependencies = [ "leb128", "thiserror 2.0.18", diff --git a/packages/coln-store/Cargo.toml b/packages/coln-store/Cargo.toml index efd8f93a..bbf4d804 100644 --- a/packages/coln-store/Cargo.toml +++ b/packages/coln-store/Cargo.toml @@ -26,7 +26,7 @@ csv = "1.4.0" coln-query = { path = "../coln-query", optional = true } ena = "0.14.4" hex = "0.4.3" -hexane = "1.0.0-alpha.2" +hexane = "1.0.0-alpha.5" js-sys = { version = "0.3.83", optional = true } leb128 = "0.2.6" petgraph = "0.8.3" diff --git a/packages/coln-store/src/commit/wire/prim.rs b/packages/coln-store/src/commit/wire/prim.rs index 027d0bfd..02bf174f 100644 --- a/packages/coln-store/src/commit/wire/prim.rs +++ b/packages/coln-store/src/commit/wire/prim.rs @@ -5,7 +5,7 @@ use std::io::Write; use coln_flir_rs::ir::{self, BuiltinTy}; -use hexane::{PackError, lebsize}; +use hexane::{Codec, PackError, lebsize}; use crate::commit::leb128 as commit_leb128; use crate::{commit::error::CodecError, txn::TxnCellValue}; @@ -116,17 +116,17 @@ impl ValueMeta { } impl hexane::ColumnValue for ValueMeta { - type Encoding = hexane::RleEncoding; + type Encoding = hexane::RleEncoding; } impl hexane::RleValue for ValueMeta { - fn try_unpack(data: &[u8]) -> Result<(usize, ValueMeta), PackError> { + fn try_unpack(data: &[u8]) -> Result<(usize, ValueMeta), PackError> { let mut buf = data; let start = buf.len(); let v = leb128::read::unsigned(&mut buf)?; Ok((start - buf.len(), ValueMeta(v))) } - fn pack(value: ValueMeta, out: &mut Vec) -> bool { + fn pack(value: ValueMeta, out: &mut Vec) -> bool { leb128::write::unsigned(out, value.0).unwrap(); true } From a5be9b32a25937e048835d599989400f6f7fa36a Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Thu, 27 Aug 2026 11:51:34 +0100 Subject: [PATCH 03/18] feat(coln-store): Use `DeltaColumn:scope_to_value` As new versions of hexane supports this. The numbers seem to be roughly consistent, have not seen big performance improvements or drops (ranging from 400ms to 900ms on the pk_insert_benchmark run manually). Probably expected as both are doing binary-ish search. Co-authored-by: acurrieclark <1306728+acurrieclark@users.noreply.github.com> --- packages/coln-store/src/table/col.rs | 45 ++++---------------------- packages/coln-store/src/table/tests.rs | 26 ++++++++++----- 2 files changed, 24 insertions(+), 47 deletions(-) diff --git a/packages/coln-store/src/table/col.rs b/packages/coln-store/src/table/col.rs index 489090d9..35d9af9f 100644 --- a/packages/coln-store/src/table/col.rs +++ b/packages/coln-store/src/table/col.rs @@ -52,7 +52,6 @@ impl IdColumn { self.counters.remove(row); } - // TODO use scope_to_value / seek when supported /// Sorted position of `id`: `Ok(row)` when present, `Err(row)` with the /// insertion point otherwise. Requires ids sorted by /// `(commit_idx, counter)`. @@ -72,53 +71,21 @@ impl IdColumn { Ordering::Greater => {} } - // Binary search the rest of the run. Each `get` probe jumps to the - // containing slab through the column index instead of decoding the - // run from the start. - let mut lo = run.start; - let mut hi = run.end - 1; - while lo < hi { - let mid = lo + (hi - lo) / 2; - let counter = self.counters.get(mid).expect("run is in bounds"); - match counter.cmp(&id.counter) { - Ordering::Less => lo = mid + 1, - Ordering::Equal => return Ok(mid), - Ordering::Greater => hi = mid, - } + let found = self.counters.scope_to_value(id.counter, run); + if found.is_empty() { + Err(found.start) + } else { + Ok(found.start) } - Err(lo) } pub(super) fn len(&self) -> usize { self.counters.len() } - // TODO switch to scope_to_value when delta column supports it. pub(super) fn scope_to_value(&self, value: PackedRowId, range: Range) -> Range { let range = self.commit_idxs.scope_to_value(value.commit_idx, range); - - let mut start = range.start; - let mut end = range.end; - while start < end { - let mid = start + (end - start) / 2; - if self.counters.get(mid).expect("range is in bounds") < value.counter { - start = mid + 1; - } else { - end = mid; - } - } - let lower = start; - - end = range.end; - while start < end { - let mid = start + (end - start) / 2; - if self.counters.get(mid).expect("range is in bounds") <= value.counter { - start = mid + 1; - } else { - end = mid; - } - } - lower..start + self.counters.scope_to_value(value.counter, range) } } diff --git a/packages/coln-store/src/table/tests.rs b/packages/coln-store/src/table/tests.rs index 584f6e03..e58b3d95 100644 --- a/packages/coln-store/src/table/tests.rs +++ b/packages/coln-store/src/table/tests.rs @@ -251,7 +251,7 @@ fn staged_delete_removes_row_and_undo_restores_it() { /// scanning the table. Inserts and deletes are the only ways in and out of /// storage, so both have to keep it in step. #[test] -#[ignore = "indexing disabled until we need it"] +#[ignore = "rebuild index disabled until we need it"] fn rebuild_index_tracks_rows_referring_to_an_id() { let mut tbl = TestTable::new(Path::from("edge"), id_schema(&["left", "right"])); let a = row_id_from(1, 0); @@ -647,6 +647,7 @@ fn invalid_primary_key_name_panics_at_construction() { } /// Manual benchmark for the primary key duplicate check on insert. +/// Inserting `n` rows of one integer (the primary key) and one row id. /// Run with: /// cargo test -p coln-store --release pk_insert_benchmark -- --ignored --nocapture #[test] @@ -654,21 +655,30 @@ fn invalid_primary_key_name_panics_at_construction() { fn pk_insert_benchmark() { let schema = ir::Schema { entity_variant: ir::EntityVariant::Table, - columns: vec![ir::ColumnEntry { - path: Path::from("c0"), - col_type: ColType::BuiltinTy { - builtin_ty: BuiltinTy::BuiltinInt, + columns: vec![ + ir::ColumnEntry { + path: Path::from("rid"), + col_type: ColType::RowId { + path: Path::from("T"), + }, }, - }], + ir::ColumnEntry { + path: Path::from("c0"), + col_type: ColType::BuiltinTy { + builtin_ty: BuiltinTy::BuiltinInt, + }, + }, + ], primary_key: Some(vec![Path::from("c0")]), }; let mut tbl = TestTable::new(Path::from("bench"), schema); let n = 50_000; let start = std::time::Instant::now(); for i in 0..n { - let values = vec![CellValue::Int(i)]; + let row_id = test_row_id(i as u32); + let values = vec![CellValue::Id(row_id), CellValue::Int(i)]; tbl.validate_insert(&values).expect("keys are unique"); - tbl.insert_row(values, test_row_id(i as u32)); + tbl.insert_row(values, row_id); } println!("inserted {n} rows with pk check in {:?}", start.elapsed()); } From aa02d6833400ab8fd189061ab4358c48a0e83710 Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Wed, 26 Aug 2026 13:39:37 +0100 Subject: [PATCH 04/18] feat(coln-store): Add txn abort Co-authored-by: acurrieclark <1306728+acurrieclark@users.noreply.github.com> --- packages/coln-store/src/txn/inner.rs | 18 +++++------- packages/coln-store/src/txn/mod.rs | 42 ++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/packages/coln-store/src/txn/inner.rs b/packages/coln-store/src/txn/inner.rs index ac02ac33..7c71bb65 100644 --- a/packages/coln-store/src/txn/inner.rs +++ b/packages/coln-store/src/txn/inner.rs @@ -31,7 +31,7 @@ pub(crate) struct TxnInner { } impl TxnInner { - pub(crate) fn new(deps: Vec) -> Self { + pub(super) fn new(deps: Vec) -> Self { Self { deps, author: Author::foo(), @@ -66,7 +66,7 @@ impl TxnInner { Ok(temp_id) } - pub(crate) fn add( + pub(super) fn add( &mut self, store: &Store, table: &ir::Path, @@ -108,7 +108,7 @@ impl TxnInner { }); } - pub(crate) fn commit(self, store: &mut Store) -> Result { + pub(super) fn commit(self, store: &mut Store) -> Result { info!(op_count = self.pending.len(), "commit txn"); let TxnInner { deps, @@ -142,13 +142,9 @@ impl TxnInner { Err(err) } } - // 1. validate full batch (PK conflicts including intra-batch) - // 2. compute hash: blake3(deps || timestamp || message || canonical(ops)) - // 3. resolve: TxnRowId(k) -> RowId { commit: hash, counter: k } - // CellValue::TxnId(k) -> CellValue::Id(RowId { commit: hash, counter: k }) - // 4. apply resolved Ops to tables via table.insert_row - // 5. check_rules - // 6. push CommitMeta into store.commit_graph, advance heads - // 7. return hash + } + + pub(super) fn abort(self) { + Self::invalidate_handles(self.pending_handles, "txn abort"); } } diff --git a/packages/coln-store/src/txn/mod.rs b/packages/coln-store/src/txn/mod.rs index bc17adf6..30061f30 100644 --- a/packages/coln-store/src/txn/mod.rs +++ b/packages/coln-store/src/txn/mod.rs @@ -55,6 +55,10 @@ impl<'a> Transaction<'a> { self.inner.commit(self.store) } // pub fn commit_with(mut self, opts: CommitOptions) -> Result { ... } + + pub fn abort(self) { + self.inner.abort() + } } pub struct OwnedTransaction { @@ -79,6 +83,11 @@ impl OwnedTransaction { self.inner.add(&self.store, table, values) } + pub fn abort(self) -> Store { + self.inner.abort(); + self.store + } + // We need to return Store to the user for roll back purposes, so the Err variant must be large #[allow(clippy::result_large_err)] pub fn commit(mut self) -> Result<(CommitHash, Store), (StoreError, Store)> { @@ -225,6 +234,39 @@ mod tests { assert_eq!(edge.cell_at(0, 0), Some(CellValue::Id(node_id))); } + #[test] + fn abort_invalidates_returned_row_handles() { + let nodes = Path::from("Nodes"); + let edges = Path::from("Edges"); + let mut store = Store::new(); + store + .create_table(nodes.clone(), table_schema(vec![], None)) + .expect("create nodes table"); + store + .create_table( + edges.clone(), + table_schema(vec![row_id_col("node", nodes.clone())], None), + ) + .expect("create edges table"); + let mut tx = store.transaction(); + let node = tx.add(&nodes, vec![]).expect("add node"); + tx.abort(); + let err = node.row_id().expect_err("abort invalidates handle"); + assert!(matches!( + err, + StoreError::Validation(ValidationError::InvalidRowHandle { .. }) + )); + let mut tx = store.transaction(); + let err = tx + .add(&edges, vec![node.into()]) + .expect_err("aborted handle cannot be reused"); + assert!(matches!( + err, + StoreError::Validation(ValidationError::InvalidRowHandle { .. }) + )); + assert_eq!(store.table_at(&nodes).expect("Nodes").row_count(), 0); + } + #[test] fn failed_transaction_invalidates_returned_row_handles() { let nodes = Path::from("Nodes"); From 647e65d149327f267b6f422e50b9cb6ac695ee1f Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Wed, 26 Aug 2026 15:17:59 +0100 Subject: [PATCH 05/18] feat: Add new `Store::try_from_commit_bytes` Allow automerge-repo to construct a store from scratch with a chunk of bytes, assuming they contain the root commit. Co-authored-by: acurrieclark <1306728+acurrieclark@users.noreply.github.com> --- packages/coln-store/src/commit/chunk.rs | 6 + packages/coln-store/src/commit/pst.rs | 41 +------ packages/coln-store/src/store/mod.rs | 141 +++++++++++++++++------- packages/coln-store/src/store/tests.rs | 33 ++++++ 4 files changed, 140 insertions(+), 81 deletions(-) diff --git a/packages/coln-store/src/commit/chunk.rs b/packages/coln-store/src/commit/chunk.rs index 39789cb4..3b8f368f 100644 --- a/packages/coln-store/src/commit/chunk.rs +++ b/packages/coln-store/src/commit/chunk.rs @@ -27,6 +27,8 @@ impl From for u8 { } } +/// A chunk what the external world sees as a "chunk" of data, right now it is +/// just a commit, but in the future it might be a sedimemtree fragment. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Chunk { Commit { header: Header, payload: Vec }, @@ -53,6 +55,10 @@ impl Chunk { Ok(chunk) } + pub fn is_root(&self) -> bool { + self.chunk_type() == ChunkType::Root + } + pub(crate) fn chunk_type(&self) -> ChunkType { match self { Chunk::Commit { header, .. } | Chunk::Root { header, .. } => header.chunk_type, diff --git a/packages/coln-store/src/commit/pst.rs b/packages/coln-store/src/commit/pst.rs index 592ad05d..27a1766c 100644 --- a/packages/coln-store/src/commit/pst.rs +++ b/packages/coln-store/src/commit/pst.rs @@ -5,7 +5,7 @@ use std::io::Write; use crate::commit::Commit; -use crate::commit::chunk::{Chunk, ChunkType}; +use crate::commit::chunk::Chunk; use crate::commit::error::CodecError; use crate::commit::leb128 as commit_leb128; use crate::store::Store; @@ -38,7 +38,7 @@ pub fn encode_store(store: &Store) -> Result, CodecError> { /// Decode a store from bytes produced by [`encode_store`]. pub fn decode_store(data: &[u8]) -> Result { let encoded = read_store_envelope(data)?; - decode_store_chunks(encoded.chunks) + Store::try_from_chunks(encoded.chunks) } struct EncodedStore { @@ -83,43 +83,6 @@ fn write_commit_chunk(buf: &mut Vec, commit: &Commit<'_>) { Chunk::from(commit).write(buf) } -fn decode_store_chunks(chunks: Vec) -> Result { - let roots = chunks - .iter() - .filter(|chunk| chunk.chunk_type() == ChunkType::Root) - .collect::>(); - if roots.is_empty() { - return Err(CodecError::DataFormatError("commit graph has no root commit".into()).into()); - } - if roots.len() > 1 { - return Err( - CodecError::DataFormatError("commit graph has multiple root commits".into()).into(), - ); - } - - let root_commit = Commit::from_chunk((*roots[0]).clone(), |_| None)?; - let root_payload = root_commit.root_payload()?; - let mut store = Store::try_from_ir(root_payload)?; - - let mut commits = Vec::new(); - for chunk in chunks { - if chunk.chunk_type() == ChunkType::Root { - continue; - } - - let commit = Commit::from_chunk(chunk, |path| { - store - .resolve_table(path) - .and_then(|oid| store.table_meta(oid)) - })?; - commits.push(commit); - } - - store.apply_commits(commits)?; - - Ok(store) -} - #[cfg(test)] mod tests { use coln_flir_rs::ir::{BuiltinTy, ColType, ColumnEntry, EntityVariant}; diff --git a/packages/coln-store/src/store/mod.rs b/packages/coln-store/src/store/mod.rs index 8da3ec6a..a2e19387 100644 --- a/packages/coln-store/src/store/mod.rs +++ b/packages/coln-store/src/store/mod.rs @@ -203,47 +203,6 @@ impl Store { Ok(graph) } - #[cfg(feature = "native")] - // used in SQL mode only - pub(crate) fn create_table( - &mut self, - path: ir::Path, - schema: ir::Schema, - ) -> Result { - let oid = self.tables.len(); - self.path_to_oid.insert(path.clone(), oid); - self.tables.insert(oid, Table::new(path, oid, schema)); - - let mut tables: Vec<_> = self - .tables - .values() - .map(|table| { - ( - table.oid(), - ir::TableEntry { - path: table.path().clone(), - table: table.schema().clone(), - }, - ) - }) - .collect(); - tables.sort_by_key(|(oid, _)| *oid); - let ir = FlatRealm { - tables: tables.into_iter().map(|(_, entry)| entry).collect(), - rules: self.rule_entries.clone(), - }; - self.commits = Self::graph_with_root_commit(&ir)?; - Ok(oid) - } - - pub fn transaction(&mut self) -> Transaction<'_> { - Transaction::new(self) - } - - pub fn into_transaction(self) -> OwnedTransaction { - OwnedTransaction::new(self) - } - /// Builds an empty column store per `theory.tables` and keeps only `theory.rules` /// (schemas are stored on each [`Table`]). pub fn try_from_ir(ir: FlatRealm) -> Result { @@ -279,6 +238,18 @@ impl Store { } } +impl Store { + // transactions + + pub fn transaction(&mut self) -> Transaction<'_> { + Transaction::new(self) + } + + pub fn into_transaction(self) -> OwnedTransaction { + OwnedTransaction::new(self) + } +} + impl Store { // Dealing with rules fn compile_rules(rules: &[RuleEntry]) -> Result, CompileError> { @@ -663,10 +634,96 @@ impl Store { self.apply_commits(commits) } + + /// Build a store from commit chunks from scratch, assuming that the input + /// `chunk_bytes` contains a valid root commit. + pub fn try_from_commit_bytes( + chunk_bytes: impl IntoIterator>, + ) -> Result { + let chunks = chunk_bytes + .into_iter() + .map(|bytes| Chunk::decode(bytes.as_ref())) + .collect::, _>>()?; + Self::try_from_chunks(chunks) + } + + /// Create a store from the commit chunks, assuming the chunk contains a valid root + pub(crate) fn try_from_chunks(chunks: Vec) -> Result { + let roots = chunks + .iter() + .filter(|chunk| chunk.is_root()) + .collect::>(); + if roots.is_empty() { + return Err( + CodecError::DataFormatError("commit graph has no root commit".into()).into(), + ); + } + if roots.len() > 1 { + return Err(CodecError::DataFormatError( + "commit graph has multiple root commits".into(), + ) + .into()); + } + + let root_commit = Commit::from_chunk((*roots[0]).clone(), |_| None)?; + let root_payload = root_commit.root_payload()?; + let mut store = Store::try_from_ir(root_payload)?; + + let mut commits = Vec::new(); + for chunk in chunks { + if chunk.is_root() { + continue; + } + + let commit = Commit::from_chunk(chunk, |path| { + store + .resolve_table(path) + .and_then(|oid| store.table_meta(oid)) + })?; + commits.push(commit); + } + + store.apply_commits(commits)?; + + Ok(store) + } } impl Store { - // for debugging and testing + // for debugging and testing and experiments + + #[cfg(feature = "native")] + // used in SQL mode only + pub(crate) fn create_table( + &mut self, + path: ir::Path, + schema: ir::Schema, + ) -> Result { + let oid = self.tables.len(); + self.path_to_oid.insert(path.clone(), oid); + self.tables.insert(oid, Table::new(path, oid, schema)); + + let mut tables: Vec<_> = self + .tables + .values() + .map(|table| { + ( + table.oid(), + ir::TableEntry { + path: table.path().clone(), + table: table.schema().clone(), + }, + ) + }) + .collect(); + tables.sort_by_key(|(oid, _)| *oid); + let ir = FlatRealm { + tables: tables.into_iter().map(|(_, entry)| entry).collect(), + rules: self.rule_entries.clone(), + }; + self.commits = Self::graph_with_root_commit(&ir)?; + Ok(oid) + } /// Dump every table in the store for debugging, in ascending [`TableOid`] order, /// separated by a blank line. diff --git a/packages/coln-store/src/store/tests.rs b/packages/coln-store/src/store/tests.rs index 47efaf04..1ed06ea4 100644 --- a/packages/coln-store/src/store/tests.rs +++ b/packages/coln-store/src/store/tests.rs @@ -826,6 +826,39 @@ mod commits { assert_eq!(hashes, vec![commit]); } + #[test] + fn commit_chunks_create_empty_store() { + let source = Store::new(); + let chunks = source + .commit_chunks_after(&[]) + .into_iter() + .map(|chunk| chunk.bytes) + .collect::>(); + + let restored = Store::try_from_commit_bytes(chunks).expect("store from chunks"); + + assert_eq!(restored.table_count(), 0); + assert_eq!(restored.heads(), source.heads()); + } + + #[test] + fn commit_chunks_create_store_from_out_of_order_data() { + let mut source = single_int_store(); + let commit = commit_int(&mut source, 99); + let mut chunks = source + .commit_chunks_after(&[]) + .into_iter() + .map(|chunk| chunk.bytes) + .collect::>(); + chunks.reverse(); + + let restored = Store::try_from_commit_bytes(chunks).expect("store from chunks"); + + let table = restored.table_at(&Path::from("T")).expect("table"); + assert_eq!(table.cell_at(0, 0), Some(CellValue::Int(99))); + assert_eq!(restored.heads(), vec![commit]); + } + #[test] fn apply_commits_applies_rows_and_updates_heads() { let mut source = single_int_store(); From 46dc66b2554c1f545887fdbd577672708126899c Mon Sep 17 00:00:00 2001 From: Shuntian Liu Date: Wed, 26 Aug 2026 19:42:48 +0100 Subject: [PATCH 06/18] feat: Return pending commits after application Previously when calling `Store::apply_commits`, we assume that all commits can be applied, and return an error is that is not the case. As automerge-repo cannot track which commits can be applied, we put this causal delivery logic in coln-js-runtime temporarily, by keeping what is returned from apply_chunk_bytes() and keep retrying until success. coln-js-runtime is not supposed to do this though, and in the long run we wish to move this logic to somewhere else, perhaps subduction. Co-authored-by: acurrieclark <1306728+acurrieclark@users.noreply.github.com> --- packages/coln-js-runtime/src/rust/handles.rs | 349 ++++++++++++++++-- .../coln-js-runtime/src/ts/RealmBindings.ts | 16 + packages/coln-js-runtime/src/ts/index.ts | 2 + .../coln-js-runtime/tests/basic-ir/helpers.ts | 13 +- packages/coln-store/src/commit/error.rs | 2 + packages/coln-store/src/commit/pst.rs | 15 +- packages/coln-store/src/store/error.rs | 16 +- packages/coln-store/src/store/mod.rs | 64 ++-- packages/coln-store/src/store/tests.rs | 86 ++++- packages/coln-store/src/txn/inner.rs | 6 +- 10 files changed, 486 insertions(+), 83 deletions(-) create mode 100644 packages/coln-js-runtime/src/ts/RealmBindings.ts diff --git a/packages/coln-js-runtime/src/rust/handles.rs b/packages/coln-js-runtime/src/rust/handles.rs index fa475898..6446a8f6 100644 --- a/packages/coln-js-runtime/src/rust/handles.rs +++ b/packages/coln-js-runtime/src/rust/handles.rs @@ -4,7 +4,7 @@ use coln_flir_rs::ir; use coln_store::{ - commit::hash::CommitHash as StoreCommitHash, + commit::{chunk::Chunk, hash::CommitHash as StoreCommitHash}, store::Store, table::RowId as StoreRowId, txn::{OwnedTransaction, RowHandle as StoreRowHandle}, @@ -19,13 +19,26 @@ use wasm_bindgen::prelude::wasm_bindgen; #[wasm_bindgen] pub struct StoreHandle { - store: Option, + state: StoreHandleState, +} + +enum StoreHandleState { + Uninitialized { + chunks: Vec>, + has_root: bool, + }, + Ready { + store: Box, + pending_chunks: Vec>, + }, + Moved, } #[wasm_bindgen] pub struct TransactionHandle { tx: Option, recovered_store: Option, + pending_chunks: Vec>, pending_handles: Vec<(StoreRowHandle, JsValue)>, } @@ -94,7 +107,10 @@ impl TransactionHandle { Ok(CommitResult { commit: commit.to_string(), - store: Some(StoreHandle { store: Some(store) }), + store: Some(StoreHandle::ready_with_pending( + store, + std::mem::take(&mut self.pending_chunks), + )), }) } Err((err, store)) => { @@ -110,12 +126,21 @@ impl TransactionHandle { // after committing #[wasm_bindgen(js_name = takeStore)] pub fn take_store(&mut self) -> Result { + if let Some(tx) = self.tx.take() { + return Ok(StoreHandle::ready_with_pending( + tx.abort(), + std::mem::take(&mut self.pending_chunks), + )); + } let store = self .recovered_store .take() .ok_or_else(|| js_error("transaction does not have a recovered store"))?; - Ok(StoreHandle { store: Some(store) }) + Ok(StoreHandle::ready_with_pending( + store, + std::mem::take(&mut self.pending_chunks), + )) } } @@ -127,13 +152,22 @@ pub struct CommitResult { #[wasm_bindgen] impl StoreHandle { + pub fn empty() -> StoreHandle { + Self { + state: StoreHandleState::Uninitialized { + chunks: Vec::new(), + has_root: false, + }, + } + } + #[wasm_bindgen(js_name = fromTheory)] pub fn from_theory(flat_theory_json: String) -> Result { let theory = serde_json::from_str::(&flat_theory_json) .map_err(|err| js_error(format!("invalid flat theory JSON: {err}")))?; let store = Store::try_from_ir(theory).map_err(js_error)?; - Ok(Self { store: Some(store) }) + Ok(Self::ready(store)) } #[wasm_bindgen(js_name = jsonIR)] @@ -163,14 +197,27 @@ impl StoreHandle { #[wasm_bindgen(js_name = beginTransaction)] pub fn begin_transaction(&mut self) -> Result { - let store = self - .store - .take() - .ok_or_else(|| js_error("store handle has already been moved into a transaction"))?; + let state = std::mem::replace(&mut self.state, StoreHandleState::Moved); + let (store, pending_chunks) = match state { + StoreHandleState::Ready { + store, + pending_chunks, + } => (*store, pending_chunks), + state @ StoreHandleState::Uninitialized { .. } => { + self.state = state; + return Err(js_error("store handle has not been initialized")); + } + StoreHandleState::Moved => { + return Err(js_error( + "store handle has already been moved into a transaction", + )); + } + }; Ok(TransactionHandle { tx: Some(store.into_transaction()), recovered_store: None, + pending_chunks, pending_handles: Vec::new(), }) @@ -182,12 +229,19 @@ impl StoreHandle { // For automerge-repo interfacing pub fn heads(&self) -> Result, JsValue> { - let heads = self - .store()? - .heads() - .into_iter() - .map(CommitHash::from) - .collect::>(); + let heads = match &self.state { + StoreHandleState::Uninitialized { .. } => return Ok(Vec::new()), + StoreHandleState::Ready { store, .. } => store, + StoreHandleState::Moved => { + return Err(js_error( + "store handle has already been moved into a transaction", + )); + } + } + .heads() + .into_iter() + .map(CommitHash::from) + .collect::>(); Ok(heads) } @@ -197,6 +251,9 @@ impl StoreHandle { &self, have_heads: Vec, ) -> Result, JsValue> { + if matches!(self.state, StoreHandleState::Uninitialized { .. }) { + return Ok(Vec::new()); + } let have_heads = have_heads .into_iter() .map(StoreCommitHash::try_from) @@ -217,10 +274,7 @@ impl StoreHandle { pub fn apply_chunk_bytes(&mut self, chunk_bytes: JsValue) -> Result<(), JsValue> { let chunk_bytes = serde_wasm_bindgen::from_value::>>(chunk_bytes).map_err(js_error)?; - - self.store_mut()? - .apply_chunk_bytes(chunk_bytes) - .map_err(js_error) + self.apply_chunks(chunk_bytes).map_err(js_error) } } @@ -240,16 +294,76 @@ impl CommitResult { } impl StoreHandle { - fn store(&self) -> Result<&Store, JsValue> { - self.store - .as_ref() - .ok_or_else(|| js_error("store handle has been moved into a transaction")) + fn ready(store: Store) -> Self { + Self::ready_with_pending(store, Vec::new()) } - fn store_mut(&mut self) -> Result<&mut Store, JsValue> { - self.store - .as_mut() - .ok_or_else(|| js_error("store handle has been moved into a transaction")) + fn ready_with_pending(store: Store, pending_chunks: Vec>) -> Self { + Self { + state: StoreHandleState::Ready { + store: Box::new(store), + pending_chunks, + }, + } + } + + // TODO this function is doing causal order delivery. This logic should NOT + // be here, and should be moved to somewhere else in the future. + fn apply_chunks(&mut self, chunk_bytes: Vec>) -> Result<(), String> { + match &mut self.state { + StoreHandleState::Uninitialized { chunks, has_root } => { + let decoded = chunk_bytes + .iter() + .map(|bytes| Chunk::decode(bytes)) + .collect::, _>>() + .map_err(|error| error.to_string())?; + let previous_len = chunks.len(); + let previously_had_root = *has_root; + *has_root |= decoded.iter().any(Chunk::is_root); + chunks.extend(chunk_bytes); + if *has_root { + match Store::try_from_commit_bytes(chunks.iter()) { + Ok((store, pending)) => { + self.state = StoreHandle::ready_with_pending(store, pending).state + } + Err(error) => { + chunks.truncate(previous_len); + *has_root = previously_had_root; + return Err(error.to_string()); + } + } + } + Ok(()) + } + StoreHandleState::Ready { + store, + pending_chunks, + } => { + pending_chunks.extend(chunk_bytes); + match store.apply_chunk_bytes(pending_chunks.iter().cloned()) { + Ok(pending) => { + *pending_chunks = pending; + Ok(()) + } + Err(error) => Err(error.to_string()), + } + } + StoreHandleState::Moved => { + Err("store handle has already been moved into a transaction".into()) + } + } + } + + fn store(&self) -> Result<&Store, JsValue> { + match &self.state { + StoreHandleState::Uninitialized { .. } => { + Err(js_error("store handle has not been initialized")) + } + StoreHandleState::Ready { store, .. } => Ok(store), + StoreHandleState::Moved => Err(js_error( + "store handle has already been moved into a transaction", + )), + } } } @@ -260,3 +374,184 @@ impl TransactionHandle { .ok_or_else(|| js_error("transaction has already been committed")) } } + +#[cfg(test)] +mod tests { + use coln_flir_rs::ir::{ + BuiltinTy, ColType, ColumnEntry, EntityVariant, FlatRealm, Path, Schema, TableEntry, + }; + + use super::*; + + fn source_store() -> Store { + let theory = FlatRealm { + tables: vec![TableEntry { + path: Path::from("T"), + table: Schema { + entity_variant: EntityVariant::Table, + columns: vec![ColumnEntry { + path: Path::from("value"), + col_type: ColType::BuiltinTy { + builtin_ty: BuiltinTy::BuiltinInt, + }, + }], + primary_key: None, + }, + }], + rules: vec![], + }; + let mut store = Store::try_from_ir(theory).expect("store"); + let mut transaction = store.transaction(); + transaction + .add(&Path::from("T"), vec![42_i64.into()]) + .expect("add row"); + transaction.commit().expect("commit"); + store + } + + #[test] + fn empty_handle_buffers_data_until_root_arrives() { + let source = source_store(); + let (root, data): (Vec<_>, Vec<_>) = source + .commit_chunks_after(&[]) + .into_iter() + .map(|chunk| chunk.bytes) + .partition(|bytes| Chunk::decode(bytes).expect("chunk").is_root()); + let mut handle = StoreHandle::empty(); + + handle.apply_chunks(data).expect("buffer data"); + assert!(matches!( + handle.state, + StoreHandleState::Uninitialized { .. } + )); + assert!(handle.heads().expect("heads").is_empty()); + + handle.apply_chunks(root).expect("apply root"); + let store = handle.store().expect("initialized store"); + let table = store.table_at(&Path::from("T")).expect("table"); + assert_eq!(table.row_count(), 1); + } + + #[test] + fn empty_handle_retries_bootstrap_when_missing_parent_arrives() { + let mut source = source_store(); + let mut transaction = source.transaction(); + transaction + .add(&Path::from("T"), vec![84_i64.into()]) + .expect("add second row"); + transaction.commit().expect("second commit"); + + let mut root = None; + let mut data = Vec::new(); + for chunk in source.commit_chunks_after(&[]) { + if Chunk::decode(&chunk.bytes).expect("chunk").is_root() { + root = Some(chunk.bytes); + } else { + data.push(chunk.bytes); + } + } + assert_eq!(data.len(), 2); + + let mut handle = StoreHandle::empty(); + let child = data.pop().expect("child commit"); + handle + .apply_chunks(vec![root.expect("root"), child]) + .expect("buffer child"); + + handle.apply_chunks(data).expect("retry with parent"); + let table = handle + .store() + .expect("initialized store") + .table_at(&Path::from("T")) + .expect("table"); + assert_eq!(table.row_count(), 2); + } + + #[test] + fn ready_handle_retries_commit_when_missing_parent_arrives() { + let mut source = source_store(); + let mut transaction = source.transaction(); + transaction + .add(&Path::from("T"), vec![84_i64.into()]) + .expect("add second row"); + transaction.commit().expect("second commit"); + + let mut root = None; + let mut data = Vec::new(); + for chunk in source.commit_chunks_after(&[]) { + if Chunk::decode(&chunk.bytes).expect("chunk").is_root() { + root = Some(chunk.bytes); + } else { + data.push(chunk.bytes); + } + } + assert_eq!(data.len(), 2); + + let mut handle = StoreHandle::empty(); + handle + .apply_chunks(vec![root.expect("root")]) + .expect("apply root"); + assert!(matches!(handle.state, StoreHandleState::Ready { .. })); + + let child = data.pop().expect("child commit"); + handle.apply_chunks(vec![child]).expect("buffer child"); + let mut transaction = handle.begin_transaction().expect("begin transaction"); + handle = transaction.take_store().expect("abort transaction"); + handle.apply_chunks(data).expect("retry with parent"); + + let table = handle + .store() + .expect("initialized store") + .table_at(&Path::from("T")) + .expect("table"); + assert_eq!(table.row_count(), 2); + } + + #[test] + fn active_transaction_can_return_its_store_without_committing() { + let mut handle = StoreHandle::ready(source_store()); + let mut transaction = handle.begin_transaction().expect("transaction"); + transaction + .tx() + .expect("owned transaction") + .add(&Path::from("T"), vec![84_i64.into()]) + .expect("stage row"); + + let recovered = transaction.take_store().expect("recover store"); + let table = recovered + .store() + .expect("store") + .table_at(&Path::from("T")) + .expect("table"); + assert_eq!(table.row_count(), 1); + } + + #[test] + fn malformed_batch_does_not_poison_later_bootstrap() { + let source = source_store(); + let chunks = source + .commit_chunks_after(&[]) + .into_iter() + .map(|chunk| chunk.bytes) + .collect::>(); + let root = chunks + .iter() + .find(|bytes| Chunk::decode(bytes).expect("chunk").is_root()) + .expect("root") + .clone(); + let mut handle = StoreHandle::empty(); + + assert!(handle.apply_chunks(vec![root, vec![0xff]]).is_err()); + handle.apply_chunks(chunks).expect("valid retry"); + + assert_eq!( + handle + .store() + .expect("initialized store") + .table_at(&Path::from("T")) + .expect("table") + .row_count(), + 1 + ); + } +} diff --git a/packages/coln-js-runtime/src/ts/RealmBindings.ts b/packages/coln-js-runtime/src/ts/RealmBindings.ts new file mode 100644 index 00000000..88516c95 --- /dev/null +++ b/packages/coln-js-runtime/src/ts/RealmBindings.ts @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { StoreHandle, TransactionHandle } from "#wasm-bodge/bindings" + +export interface ColnSchema { + entities: readonly unknown[] + rules: readonly unknown[] +} + +export interface RealmBindings { + schema: ColnSchema + View: new (store: StoreHandle) => { root: ViewRoot } + Transaction: new (store: StoreHandle, transaction: TransactionHandle) => { root: TransactionRoot } +} diff --git a/packages/coln-js-runtime/src/ts/index.ts b/packages/coln-js-runtime/src/ts/index.ts index 9d936a44..19e74047 100644 --- a/packages/coln-js-runtime/src/ts/index.ts +++ b/packages/coln-js-runtime/src/ts/index.ts @@ -5,6 +5,8 @@ export type { CommitChunk, RowRef, RowView, Value } from "#wasm-bodge/bindings"; export { CommitResult, StoreHandle, TransactionHandle, valueEqual } from "#wasm-bodge/bindings" +export type { RealmBindings, ColnSchema } from "./RealmBindings" + export * as ColnSet from "./ColnSet"; export * as ColnRef from "./ColnRef"; diff --git a/packages/coln-js-runtime/tests/basic-ir/helpers.ts b/packages/coln-js-runtime/tests/basic-ir/helpers.ts index fb0aeb10..be9e2327 100644 --- a/packages/coln-js-runtime/tests/basic-ir/helpers.ts +++ b/packages/coln-js-runtime/tests/basic-ir/helpers.ts @@ -2,19 +2,10 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT -import { StoreHandle, type TransactionHandle } from "@coln-project/runtime"; - -interface RealmModule { - schema: unknown; - View: new (store: StoreHandle) => { root: ViewRoot }; - Transaction: new ( - store: StoreHandle, - transaction: TransactionHandle, - ) => { root: TransactionRoot }; -} +import { StoreHandle, type RealmBindings } from "@coln-project/runtime"; export function beginRealm( - realm: RealmModule, + realm: RealmBindings, ) { let store = StoreHandle.fromTheory(JSON.stringify(realm.schema)); const transaction = store.beginTransaction(); diff --git a/packages/coln-store/src/commit/error.rs b/packages/coln-store/src/commit/error.rs index a70a34bc..6a77d69f 100644 --- a/packages/coln-store/src/commit/error.rs +++ b/packages/coln-store/src/commit/error.rs @@ -13,6 +13,7 @@ pub enum CodecError { IOError(io::Error), SchemaError(String), DataFormatError(String), + DataContentError(String), ChecksumMismatch, DecodeError(hexane::PackError), ChunkMismatch { expected: ChunkType, got: ChunkType }, @@ -26,6 +27,7 @@ impl fmt::Display for CodecError { CodecError::IOError(err) => write!(f, "io error: {err}"), CodecError::SchemaError(msg) => write!(f, "schema error: {msg}"), CodecError::DataFormatError(msg) => write!(f, "data format error: {msg}"), + CodecError::DataContentError(msg) => write!(f, "data content error: {msg}"), CodecError::ChecksumMismatch => write!(f, "chunk checksum mismatch"), CodecError::DecodeError(err) => write!(f, "decode error: {err:?}"), CodecError::ChunkMismatch { expected, got } => write!( diff --git a/packages/coln-store/src/commit/pst.rs b/packages/coln-store/src/commit/pst.rs index 27a1766c..3c2e923e 100644 --- a/packages/coln-store/src/commit/pst.rs +++ b/packages/coln-store/src/commit/pst.rs @@ -38,7 +38,14 @@ pub fn encode_store(store: &Store) -> Result, CodecError> { /// Decode a store from bytes produced by [`encode_store`]. pub fn decode_store(data: &[u8]) -> Result { let encoded = read_store_envelope(data)?; - Store::try_from_chunks(encoded.chunks) + let (store, pending) = Store::try_from_chunks(encoded.chunks)?; + if !pending.is_empty() { + return Err(CodecError::DataContentError( + "store snapshot contains commits that could not be applied".into(), + ) + .into()); + } + Ok(store) } struct EncodedStore { @@ -172,13 +179,13 @@ mod tests { .expect("encoded store contains a chunk magic") } - fn is_missing_dep_error(result: Result) -> bool { + fn is_data_content_error(result: Result) -> bool { matches!( result, Err(err) if matches!( &err, - StoreError::Commit(crate::store::error::CommitApplyError::MissingDep) + StoreError::Encode(CodecError::DataContentError(_)) ) ) } @@ -315,7 +322,7 @@ mod tests { frame_chunk(ChunkType::Commit, commit.payload()), ]); - assert!(is_missing_dep_error(decode_store(&bytes))); + assert!(is_data_content_error(decode_store(&bytes))); } #[test] diff --git a/packages/coln-store/src/store/error.rs b/packages/coln-store/src/store/error.rs index bfb53b9d..e2ecd889 100644 --- a/packages/coln-store/src/store/error.rs +++ b/packages/coln-store/src/store/error.rs @@ -4,6 +4,7 @@ use crate::commit::error::CodecError; use crate::commit::graph::CommitGraphError; +use crate::commit::hash::CommitHash; use crate::solver::compile::CompileError; use crate::solver::validate::RuleViolation; use crate::table::ValidationError; @@ -27,15 +28,10 @@ pub enum StoreError { #[derive(Debug, thiserror::Error)] pub enum CommitApplyError { - #[error("missing commit dependency")] - MissingDep, - #[error("disconnected commit")] - DisconnectedCommit, - // A commit that should definitely exist but is missing - #[error("missing commit")] - MissingCommit, + #[error("A commit {0} with no dependency")] + DanglingCommit(CommitHash), #[error("An existing commit has conflict payload")] - ConflictPayload, - #[error("Root commit cannot be applied")] - RootCommit, + ConflictPayload(CommitHash), + #[error("Root commit {0} cannot be applied")] + RootCommit(CommitHash), } diff --git a/packages/coln-store/src/store/mod.rs b/packages/coln-store/src/store/mod.rs index a2e19387..26df187b 100644 --- a/packages/coln-store/src/store/mod.rs +++ b/packages/coln-store/src/store/mod.rs @@ -341,21 +341,31 @@ impl Store { .collect() } + /// This will try to merge the `other` store as much as possible into this store + // TODO need to rethink `merge` more carefully pub fn merge(&mut self, other: &Self) -> Result, StoreError> { let commits = self.commits_added(other); self.apply_commits(commits)?; Ok(self.heads()) } - pub fn apply_commit(&mut self, commit: Commit<'static>) -> Result<(), StoreError> { + /// Apply a single commit, respect its dependency. + /// Return the commit if it cannot be applied due to missing deps + pub fn apply_commit( + &mut self, + commit: Commit<'static>, + ) -> Result>, StoreError> { // This needs to call apply_commits because it needs to do dependency check - self.apply_commits([commit]) + self.apply_commits([commit]).map(|mut h| h.pop()) } + /// Apply as many commits as possible respecting their dependencies. Return the + /// commit hashes that are NOT applied, so the caller knows which ones they + /// need to retry. pub fn apply_commits( &mut self, commits: impl IntoIterator>, - ) -> Result<(), StoreError> { + ) -> Result>, StoreError> { let mut pending = HashMap::new(); for commit in commits { @@ -364,17 +374,21 @@ impl Store { continue; } + // We assume that the root commit has been used to construct the store + // and therefore must have been applied if commit.is_root() { - return Err(CommitApplyError::RootCommit.into()); + return Err(CommitApplyError::RootCommit(commit.hash()).into()); } + + // We assume that all commits will have deps if commit.deps.is_empty() { - return Err(CommitApplyError::MissingDep.into()); + return Err(CommitApplyError::DanglingCommit(commit.hash()).into()); } if let Some(existing) = pending.get(&hash) { let existing: &Commit<'static> = existing; if *existing != commit { - return Err(CommitApplyError::ConflictPayload.into()); + return Err(CommitApplyError::ConflictPayload(commit.hash()).into()); } continue; } @@ -401,8 +415,12 @@ impl Store { count += 1; waiting_on.entry(*dep).or_default().push(*hash); } else { - // deps is not in pending or applied commits - return Err(CommitApplyError::MissingDep.into()); + tracing::info!( + commit_hash = %commit.hash(), + missing_dep = %dep, + "skipping commit with dependency that is neither applied nor pending" + ); + count += 1; } } @@ -416,14 +434,14 @@ impl Store { while let Some(hash) = ready.pop_first() { let commit = pending .remove(&hash) - .ok_or(CommitApplyError::MissingCommit)?; + .expect("hash in ready should also exist in pending"); self.apply_commit_atomic(commit)?; if let Some(waitings) = waiting_on.remove(&hash) { for wh in waitings { let count = unsatisfied .get_mut(&wh) - .ok_or(CommitApplyError::MissingCommit)?; + .expect("A commit that is waiting must be in unsatisfied"); *count -= 1; if *count == 0 { unsatisfied.remove(&wh).unwrap(); @@ -433,12 +451,7 @@ impl Store { } } - // pending is not empty, but there is no commit to apply - if !pending.is_empty() { - return Err(CommitApplyError::DisconnectedCommit.into()); - } - - Ok(()) + Ok(pending.into_values().collect()) } fn apply_commit_atomic(&mut self, commit: Commit<'static>) -> Result<(), StoreError> { @@ -615,10 +628,11 @@ impl Store { } /// Apply the bytes received by interpreting them as chunks, for syncing purposes + /// Return chunk bytes that cannot be applied yet. pub fn apply_chunk_bytes( &mut self, chunk_bytes: impl IntoIterator>, - ) -> Result<(), StoreError> { + ) -> Result>, StoreError> { let commits = chunk_bytes .into_iter() .map(|bytes| Chunk::decode(&bytes)) @@ -632,14 +646,14 @@ impl Store { }) .collect::, _>>()?; - self.apply_commits(commits) + Ok(Self::commits_to_chunk_bytes(self.apply_commits(commits)?)) } /// Build a store from commit chunks from scratch, assuming that the input /// `chunk_bytes` contains a valid root commit. pub fn try_from_commit_bytes( chunk_bytes: impl IntoIterator>, - ) -> Result { + ) -> Result<(Self, Vec>), StoreError> { let chunks = chunk_bytes .into_iter() .map(|bytes| Chunk::decode(bytes.as_ref())) @@ -648,7 +662,7 @@ impl Store { } /// Create a store from the commit chunks, assuming the chunk contains a valid root - pub(crate) fn try_from_chunks(chunks: Vec) -> Result { + pub(crate) fn try_from_chunks(chunks: Vec) -> Result<(Self, Vec>), StoreError> { let roots = chunks .iter() .filter(|chunk| chunk.is_root()) @@ -683,9 +697,15 @@ impl Store { commits.push(commit); } - store.apply_commits(commits)?; + let pending_bytes = Self::commits_to_chunk_bytes(store.apply_commits(commits)?); + Ok((store, pending_bytes)) + } - Ok(store) + fn commits_to_chunk_bytes(commits: Vec>) -> Vec> { + commits + .into_iter() + .map(|commit| Chunk::from(commit).encoded()) + .collect() } } diff --git a/packages/coln-store/src/store/tests.rs b/packages/coln-store/src/store/tests.rs index 1ed06ea4..4a706547 100644 --- a/packages/coln-store/src/store/tests.rs +++ b/packages/coln-store/src/store/tests.rs @@ -835,8 +835,9 @@ mod commits { .map(|chunk| chunk.bytes) .collect::>(); - let restored = Store::try_from_commit_bytes(chunks).expect("store from chunks"); + let (restored, pending) = Store::try_from_commit_bytes(chunks).expect("store from chunks"); + assert!(pending.is_empty()); assert_eq!(restored.table_count(), 0); assert_eq!(restored.heads(), source.heads()); } @@ -852,8 +853,9 @@ mod commits { .collect::>(); chunks.reverse(); - let restored = Store::try_from_commit_bytes(chunks).expect("store from chunks"); + let (restored, pending) = Store::try_from_commit_bytes(chunks).expect("store from chunks"); + assert!(pending.is_empty()); let table = restored.table_at(&Path::from("T")).expect("table"); assert_eq!(table.cell_at(0, 0), Some(CellValue::Int(99))); assert_eq!(restored.heads(), vec![commit]); @@ -915,7 +917,7 @@ mod commits { } #[test] - fn apply_commits_rejects_missing_dependency_without_changing_store() { + fn apply_commits_skips_missing_dependency_without_changing_store() { let mut source = single_int_store(); let mut target = single_int_store(); commit_int(&mut source, 1); @@ -925,12 +927,65 @@ mod commits { .expect("second commit") .clone(); - let err = target.apply_commits([second_commit]).unwrap_err(); + let leftover = target + .apply_commits([second_commit]) + .expect("skip missing dependency"); + let leftover_hashes: Vec<_> = leftover.iter().map(Commit::hash).collect(); - assert!(matches!( - err, - StoreError::Commit(CommitApplyError::MissingDep) - )); + assert_eq!(leftover_hashes, vec![second]); + assert_eq!( + target + .table_at(&Path::from("T")) + .expect("table") + .row_count(), + 0 + ); + } + + #[test] + fn apply_commits_applies_ready_commits_and_returns_blocked_ones() { + let mut source = single_int_store(); + let mut target = single_int_store(); + let first = commit_int(&mut source, 1); + commit_int(&mut source, 2); + let third = commit_int(&mut source, 3); + let first_commit = source.commit_by_hash(&first).expect("first commit").clone(); + let third_commit = source.commit_by_hash(&third).expect("third commit").clone(); + + let leftover = target + .apply_commits([first_commit, third_commit]) + .expect("apply ready commits"); + let leftover_hashes: Vec<_> = leftover.iter().map(Commit::hash).collect(); + + assert_eq!(leftover_hashes, vec![third]); + assert_eq!( + target + .table_at(&Path::from("T")) + .expect("table") + .row_count(), + 1 + ); + assert_eq!(target.heads(), vec![first]); + } + + #[test] + fn apply_chunk_bytes_retries_leftover_when_missing_parent_arrives() { + let mut source = single_int_store(); + let mut target = single_int_store(); + commit_int(&mut source, 1); + let second = commit_int(&mut source, 2); + + let chunks: Vec> = source + .commit_chunks_after(&target.heads()) + .into_iter() + .map(|chunk| chunk.bytes) + .collect(); + assert_eq!(chunks.len(), 2); + + let leftover = target + .apply_chunk_bytes([chunks[1].clone()]) + .expect("skip child without parent"); + assert_eq!(leftover.len(), 1); assert_eq!( target .table_at(&Path::from("T")) @@ -938,6 +993,21 @@ mod commits { .row_count(), 0 ); + + let leftover = target + .apply_chunk_bytes( + leftover + .into_iter() + .chain(std::iter::once(chunks[0].clone())), + ) + .expect("retry leftover with parent"); + assert!(leftover.is_empty()); + + let table = target.table_at(&Path::from("T")).expect("table"); + assert_eq!(table.row_count(), 2); + assert_eq!(table.cell_at(0, 0), Some(CellValue::Int(1))); + assert_eq!(table.cell_at(1, 0), Some(CellValue::Int(2))); + assert_eq!(target.heads(), vec![second]); } } diff --git a/packages/coln-store/src/txn/inner.rs b/packages/coln-store/src/txn/inner.rs index 7c71bb65..f75b6920 100644 --- a/packages/coln-store/src/txn/inner.rs +++ b/packages/coln-store/src/txn/inner.rs @@ -133,10 +133,14 @@ impl TxnInner { let h = cmt.hash(); match store.apply_commit(cmt) { - Ok(()) => { + Ok(None) => { + // Everything applied successfully Self::finalize_handles(pending_handles, h, store); Ok(h) } + Ok(Some(_)) => { + unreachable!("commit a local transaction should always succeed"); + } Err(err) => { Self::invalidate_handles(pending_handles, "txn commit failed"); Err(err) From 58b933917ad4142716b886f85bcf089aad015997 Mon Sep 17 00:00:00 2001 From: Alex Currie-Clark Date: Tue, 1 Sep 2026 15:39:55 +0100 Subject: [PATCH 07/18] coln-repo Co-authored-by: acurrieclark <1306728+acurrieclark@users.noreply.github.com> --- packages/coln-repo/.gitignore | 4 + packages/coln-repo/README.md | 77 ++ packages/coln-repo/package.json | 40 + packages/coln-repo/playwright.config.ts | 42 + packages/coln-repo/pnpm-lock.yaml | 1028 +++++++++++++++++ packages/coln-repo/pnpm-workspace.yaml | 7 + packages/coln-repo/src/colnDocType.ts | 129 +++ packages/coln-repo/src/colnHandle.ts | 82 ++ packages/coln-repo/src/index.ts | 19 + packages/coln-repo/tests/browser/index.html | 11 + packages/coln-repo/tests/browser/main.ts | 107 ++ .../coln-repo/tests/browser/subduction.d.ts | 12 + packages/coln-repo/tests/fixtures/itemFfi.ts | 36 + packages/coln-repo/tests/fixtures/schema.ts | 24 + packages/coln-repo/tests/generated/Graph.ts | 15 + .../coln-repo/tests/generated/GraphRealm.json | 1 + .../tests/generated/GraphRealm.pretty | 13 + .../coln-repo/tests/generated/GraphRealm.ts | 39 + packages/coln-repo/tests/handle.test.ts | 165 +++ .../tests/helpers/TestNetworkAdapter.ts | 50 + .../coln-repo/tests/helpers/TestRepoPair.ts | 83 ++ packages/coln-repo/tests/package/consumer.ts | 73 ++ packages/coln-repo/tests/server.mjs | 114 ++ packages/coln-repo/tests/sync.spec.ts | 190 +++ packages/coln-repo/tsconfig.build.json | 12 + packages/coln-repo/tsconfig.json | 13 + packages/coln-repo/tsconfig.package.json | 8 + packages/coln-repo/vite.config.ts | 12 + 28 files changed, 2406 insertions(+) create mode 100644 packages/coln-repo/.gitignore create mode 100644 packages/coln-repo/README.md create mode 100644 packages/coln-repo/package.json create mode 100644 packages/coln-repo/playwright.config.ts create mode 100644 packages/coln-repo/pnpm-lock.yaml create mode 100644 packages/coln-repo/pnpm-workspace.yaml create mode 100644 packages/coln-repo/src/colnDocType.ts create mode 100644 packages/coln-repo/src/colnHandle.ts create mode 100644 packages/coln-repo/src/index.ts create mode 100644 packages/coln-repo/tests/browser/index.html create mode 100644 packages/coln-repo/tests/browser/main.ts create mode 100644 packages/coln-repo/tests/browser/subduction.d.ts create mode 100644 packages/coln-repo/tests/fixtures/itemFfi.ts create mode 100644 packages/coln-repo/tests/fixtures/schema.ts create mode 100644 packages/coln-repo/tests/generated/Graph.ts create mode 100644 packages/coln-repo/tests/generated/GraphRealm.json create mode 100644 packages/coln-repo/tests/generated/GraphRealm.pretty create mode 100644 packages/coln-repo/tests/generated/GraphRealm.ts create mode 100644 packages/coln-repo/tests/handle.test.ts create mode 100644 packages/coln-repo/tests/helpers/TestNetworkAdapter.ts create mode 100644 packages/coln-repo/tests/helpers/TestRepoPair.ts create mode 100644 packages/coln-repo/tests/package/consumer.ts create mode 100644 packages/coln-repo/tests/server.mjs create mode 100644 packages/coln-repo/tests/sync.spec.ts create mode 100644 packages/coln-repo/tsconfig.build.json create mode 100644 packages/coln-repo/tsconfig.json create mode 100644 packages/coln-repo/tsconfig.package.json create mode 100644 packages/coln-repo/vite.config.ts diff --git a/packages/coln-repo/.gitignore b/packages/coln-repo/.gitignore new file mode 100644 index 00000000..fe30a0cf --- /dev/null +++ b/packages/coln-repo/.gitignore @@ -0,0 +1,4 @@ +dist/ +playwright-report/ +test-results/ +tests/browser/dist/ diff --git a/packages/coln-repo/README.md b/packages/coln-repo/README.md new file mode 100644 index 00000000..f680cc4a --- /dev/null +++ b/packages/coln-repo/README.md @@ -0,0 +1,77 @@ +# `@coln-project/repo` + +Coln Repo exposes Coln stores as [Automerge Repo](https://github.com/automerge/automerge-repo) document handles. Use them to create or find stores, read the current typed view, make atomic changes, and subscribe to changes to update application state or UI. Automerge Repo provides persistence and synchronization. + +Coln Repo currently requires the experimental [doctypes](https://github.com/automerge/automerge-repo/tree/doctypes) branch. + +## Create + +Create a Coln store using generated bindings. + +```ts +import * as Bindings from "./ExampleRealm.js"; +import { create } from "@coln-project/repo"; + +const handle = create(repo, Bindings); +``` + +## Find + +Find a Coln store by Automerge URL. Bindings are optional and must match the document schema when supplied. + +```ts +import * as Bindings from "./ExampleRealm.js"; +import { find } from "@coln-project/repo"; + +const handle = await find(repo, url, Bindings); +``` + +Apply bindings to a raw handle later if needed: + +```ts +import * as Bindings from "./ExampleRealm.js"; +import { applyBindings, find } from "@coln-project/repo"; + +const rawHandle = await find(repo, url); +const handle = applyBindings(rawHandle, Bindings); +``` + +> [!IMPORTANT] +> Once bindings are applied, later raw lookups in the same Repo may return that bound handle. It retains the raw Coln operations and adds `root` to documents and change transactions. + +## Read + +Read the current document synchronously or subscribe to changes. + +```ts +const doc = handle.doc(); +doc.scanTable("Example.Items"); +doc.root.Items.values(); + +handle.on("change", ({ doc }) => { + doc?.scanTable("Example.Items"); + doc?.root.Items.values(); +}); +``` + +Documents expose `heads()`, `jsonIR()`, `rowById()`, and `scanTable()`. + +## Write + +Changes are synchronous and atomic. A thrown error aborts the transaction. + +```ts +handle.change((tx) => { + tx.root.Items.add(); +}); +``` + +Transactions expose `add(path, values)`; bound transactions also expose `root`. + +The underlying `StoreHandle` is available through `handle.fullDoc().store` for advanced use. Its lifecycle operations bypass Coln Repo changes, events, and synchronization; prefer `doc()` and `change()`. + +## Current Limitations + +Values returned by `handle.doc()` are current documents, not durable snapshots. + +Generated read methods such as `has()` and `values()` are not supported inside `handle.change()`. Typed transactions currently support writes only. diff --git a/packages/coln-repo/package.json b/packages/coln-repo/package.json new file mode 100644 index 00000000..ad226573 --- /dev/null +++ b/packages/coln-repo/package.json @@ -0,0 +1,40 @@ +{ + "name": "@coln-project/repo", + "version": "0.0.1-alpha.0", + "private": true, + "license": "(Apache-2.0 OR MIT)", + "files": [ + "dist" + ], + "type": "module", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc -p tsconfig.build.json", + "typecheck": "tsc --noEmit", + "test": "pnpm run typecheck && pnpm run test:unit && pnpm run test:e2e", + "test:unit": "tsx --test \"tests/**/*.test.ts\"", + "test:package": "pnpm run build && tsc -p tsconfig.package.json && node --input-type=module --eval \"const pkg = await import('@coln-project/repo'); if (typeof pkg.find !== 'function') throw new Error('missing find export')\"", + "test:e2e": "pnpm run test:package && playwright test" + }, + "dependencies": { + "@automerge/automerge-repo": "github:automerge/automerge-repo#doctypes&path:packages/automerge-repo", + "@coln-project/runtime": "link:../coln-js-runtime" + }, + "devDependencies": { + "@automerge/automerge-subduction": "0.16.1", + "@playwright/test": "^1.62.1", + "@types/node": "^25.9.5", + "@types/ws": "^8.18.1", + "tsx": "^4.23.13", + "typescript": "^6.0.3", + "vite": "^8.2.2", + "vite-plugin-wasm": "^3.6.0", + "ws": "^8.21.3" + } +} diff --git a/packages/coln-repo/playwright.config.ts b/packages/coln-repo/playwright.config.ts new file mode 100644 index 00000000..9a741d42 --- /dev/null +++ b/packages/coln-repo/playwright.config.ts @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { defineConfig, devices } from "@playwright/test" + +const webPort = Number(process.env.COLN_REPO_TEST_WEB_PORT ?? 5174) + +export default defineConfig({ + testDir: "./tests", + testMatch: "**/*.spec.ts", + timeout: 30_000, + expect: { timeout: 10_000 }, + fullyParallel: false, + retries: 0, + use: { + baseURL: `http://127.0.0.1:${webPort}`, + trace: "retain-on-failure", + }, + webServer: [ + { + command: "node tests/server.mjs", + port: 3031, + reuseExistingServer: false, + stdout: "pipe", + stderr: "pipe", + }, + { + command: `./node_modules/.bin/vite --host 127.0.0.1 --port ${webPort}`, + url: `http://127.0.0.1:${webPort}`, + reuseExistingServer: false, + stdout: "pipe", + stderr: "pipe", + }, + ], + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], +}) diff --git a/packages/coln-repo/pnpm-lock.yaml b/packages/coln-repo/pnpm-lock.yaml new file mode 100644 index 00000000..41883aa0 --- /dev/null +++ b/packages/coln-repo/pnpm-lock.yaml @@ -0,0 +1,1028 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-subduction': 0.16.1 + +importers: + + .: + dependencies: + '@automerge/automerge-repo': + specifier: github:automerge/automerge-repo#doctypes&path:packages/automerge-repo + version: https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo + '@coln-project/runtime': + specifier: link:../coln-js-runtime + version: link:../coln-js-runtime + devDependencies: + '@automerge/automerge-subduction': + specifier: 0.16.1 + version: 0.16.1 + '@playwright/test': + specifier: ^1.62.1 + version: 1.62.1 + '@types/node': + specifier: ^25.9.5 + version: 25.9.5 + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 + tsx: + specifier: ^4.23.13 + version: 4.23.13 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vite: + specifier: ^8.2.2 + version: 8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(tsx@4.23.13) + vite-plugin-wasm: + specifier: ^3.6.0 + version: 3.6.0(vite@8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(tsx@4.23.13)) + ws: + specifier: ^8.21.3 + version: 8.21.3 + +packages: + + '@automerge/automerge-repo@https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo': + resolution: {gitHosted: true, integrity: sha512-9LCmV33S9KLTv/4mlMaRxJanF7Trh/ltok1o499dJanBJUt4AVpj1c+ZJlP4nm73/JeOsSUhW/rDr7mH393giA==, path: packages/automerge-repo, tarball: https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5} + version: 2.6.0-subduction.47 + engines: {node: '>=22.13'} + + '@automerge/automerge-subduction@0.16.1': + resolution: {integrity: sha512-alH7U4eYn0O7sT6hNLv7CJhNODJi+QD6qfRQTNfZCxL3Q5JZdJ8lFpdvXUkBf+I3bu0GLvVlTaYPWGrBihjpIw==} + + '@automerge/automerge@3.3.2': + resolution: {integrity: sha512-9vCdCL7pdQwUra66SBxPVHr+/t9epXKni9KDeak2rNBFMzABVh2u6gSpcwxi3jkR5qr047jVqZv9DlrOfVxLFw==} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} + cpu: [arm64] + os: [darwin] + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + resolution: {integrity: sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==} + cpu: [x64] + os: [darwin] + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + resolution: {integrity: sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==} + cpu: [arm64] + os: [linux] + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + resolution: {integrity: sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==} + cpu: [arm] + os: [linux] + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + resolution: {integrity: sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==} + cpu: [x64] + os: [linux] + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + resolution: {integrity: sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==} + cpu: [x64] + os: [win32] + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@oxc-project/types@0.147.0': + resolution: {integrity: sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==} + + '@playwright/test@1.62.1': + resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} + engines: {node: '>=20'} + hasBin: true + + '@rolldown/binding-android-arm-eabi@1.2.6': + resolution: {integrity: sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.6': + resolution: {integrity: sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.6': + resolution: {integrity: sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.6': + resolution: {integrity: sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.6': + resolution: {integrity: sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.6': + resolution: {integrity: sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.6': + resolution: {integrity: sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.6': + resolution: {integrity: sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.6': + resolution: {integrity: sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.6': + resolution: {integrity: sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.6': + resolution: {integrity: sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.6': + resolution: {integrity: sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.6': + resolution: {integrity: sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.6': + resolution: {integrity: sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.6': + resolution: {integrity: sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@types/node@25.9.5': + resolution: {integrity: sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + + bs58check@4.0.0: + resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} + + cbor-extract@2.2.2: + resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} + hasBin: true + + cbor-x@1.6.6: + resolution: {integrity: sha512-8QiD9PGOxyQHo7s2pzwTBH6lTjqekxPdl9Aq6fXvZgCuCJHOht1puDEA/fTr6mciB76c+M+Gi0qT2i1a4pm4Wg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + playwright-core@1.62.1: + resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.62.1: + resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} + engines: {node: '>=20'} + hasBin: true + + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + + rolldown@1.2.6: + resolution: {integrity: sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tsx@4.23.13: + resolution: {integrity: sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + uuid@14.0.2: + resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} + hasBin: true + + vite-plugin-wasm@3.6.0: + resolution: {integrity: sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==} + peerDependencies: + vite: ^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + + vite@8.2.2: + resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 || ^0.5.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xstate@5.32.6: + resolution: {integrity: sha512-WfA8WNrh6r9osuGwVm+aIPM4jmMW2LKHV1Yv9thYpOtL4HVF/kobh95K/O8v2jDCpDmP2EoY+6uvuqHXCZ6ZLw==} + +snapshots: + + '@automerge/automerge-repo@https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo': + dependencies: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-subduction': 0.16.1 + bs58check: 4.0.0 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + isomorphic-ws: 5.0.0(ws@8.21.3) + uuid: 14.0.2 + ws: 8.21.3 + xstate: 5.32.6 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-subduction@0.16.1': {} + + '@automerge/automerge@3.3.2': {} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@noble/hashes@1.8.0': {} + + '@oxc-project/types@0.147.0': {} + + '@playwright/test@1.62.1': + dependencies: + playwright: 1.62.1 + + '@rolldown/binding-android-arm-eabi@1.2.6': + optional: true + + '@rolldown/binding-android-arm64@1.2.6': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.6': + optional: true + + '@rolldown/binding-darwin-x64@1.2.6': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.6': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.6': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.6': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.6': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.6': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.6': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.6': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.6': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.6': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.6': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.6': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@types/node@25.9.5': + dependencies: + undici-types: 7.24.6 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 25.9.5 + + base-x@5.0.1: {} + + bs58@6.0.0: + dependencies: + base-x: 5.0.1 + + bs58check@4.0.0: + dependencies: + '@noble/hashes': 1.8.0 + bs58: 6.0.0 + + cbor-extract@2.2.2: + dependencies: + node-gyp-build-optional-packages: 5.1.1 + optionalDependencies: + '@cbor-extract/cbor-extract-darwin-arm64': 2.2.2 + '@cbor-extract/cbor-extract-darwin-x64': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm64': 2.2.2 + '@cbor-extract/cbor-extract-linux-x64': 2.2.2 + '@cbor-extract/cbor-extract-win32-x64': 2.2.2 + optional: true + + cbor-x@1.6.6: + optionalDependencies: + cbor-extract: 2.2.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + detect-libc@2.1.2: {} + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + eventemitter3@5.0.4: {} + + fast-sha256@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + isomorphic-ws@5.0.0(ws@8.21.3): + dependencies: + ws: 8.21.3 + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + ms@2.1.3: {} + + nanoid@3.3.18: {} + + node-gyp-build-optional-packages@5.1.1: + dependencies: + detect-libc: 2.1.2 + optional: true + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + playwright-core@1.62.1: {} + + playwright@1.62.1: + dependencies: + playwright-core: 1.62.1 + optionalDependencies: + fsevents: 2.3.2 + + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + rolldown@1.2.6: + dependencies: + '@oxc-project/types': 0.147.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.6 + '@rolldown/binding-android-arm64': 1.2.6 + '@rolldown/binding-darwin-arm64': 1.2.6 + '@rolldown/binding-darwin-x64': 1.2.6 + '@rolldown/binding-freebsd-x64': 1.2.6 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.6 + '@rolldown/binding-linux-arm64-gnu': 1.2.6 + '@rolldown/binding-linux-arm64-musl': 1.2.6 + '@rolldown/binding-linux-ppc64-gnu': 1.2.6 + '@rolldown/binding-linux-s390x-gnu': 1.2.6 + '@rolldown/binding-linux-x64-gnu': 1.2.6 + '@rolldown/binding-linux-x64-musl': 1.2.6 + '@rolldown/binding-openharmony-arm64': 1.2.6 + '@rolldown/binding-win32-arm64-msvc': 1.2.6 + '@rolldown/binding-win32-x64-msvc': 1.2.6 + + source-map-js@1.2.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tsx@4.23.13: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + + typescript@6.0.3: {} + + undici-types@7.24.6: {} + + uuid@14.0.2: {} + + vite-plugin-wasm@3.6.0(vite@8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(tsx@4.23.13)): + dependencies: + vite: 8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(tsx@4.23.13) + + vite@8.2.2(@types/node@25.9.5)(esbuild@0.28.2)(tsx@4.23.13): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.26 + rolldown: 1.2.6 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 25.9.5 + esbuild: 0.28.2 + fsevents: 2.3.3 + tsx: 4.23.13 + + ws@8.21.3: {} + + xstate@5.32.6: {} diff --git a/packages/coln-repo/pnpm-workspace.yaml b/packages/coln-repo/pnpm-workspace.yaml new file mode 100644 index 00000000..087ce062 --- /dev/null +++ b/packages/coln-repo/pnpm-workspace.yaml @@ -0,0 +1,7 @@ +allowBuilds: + cbor-extract: false + '@automerge/automerge-repo@git+https://github.com/automerge/automerge-repo.git': true + esbuild: true +overrides: + "@automerge/automerge": 3.3.2 + "@automerge/automerge-subduction": 0.16.1 diff --git a/packages/coln-repo/src/colnDocType.ts b/packages/coln-repo/src/colnDocType.ts new file mode 100644 index 00000000..45ca8d73 --- /dev/null +++ b/packages/coln-repo/src/colnDocType.ts @@ -0,0 +1,129 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { + StoreHandle, + type CommitChunk, + type ColnSchema, + type RealmBindings, + type TransactionHandle, +} from "@coln-project/runtime" +import { defineDocumentType, type SedimentreeMeta } from "@automerge/automerge-repo/slim" + +export type { ColnSchema, RealmBindings } from "@coln-project/runtime" + +export type ColnState = { + store: StoreHandle + bindings?: RealmBindings +} + +type ColnDocumentBase = Pick +type ColnTransactionBase = Pick + +export type ColnDocument = + Bindings extends RealmBindings + ? ColnDocumentBase & Pick, "root"> + : ColnDocumentBase + +export type ColnTransaction = + Bindings extends RealmBindings + ? ColnTransactionBase & Pick, "root"> + : ColnTransactionBase + +export type ColnChange = ( + transaction: ColnTransaction, +) => void + +export const colnDocType = defineDocumentType< + ColnState, + ColnDocumentBase, + ColnChange, + ColnSchema +>({ + name: "coln", + empty: () => ({ store: StoreHandle.empty() }), + init: schema => ({ store: StoreHandle.fromTheory(serializeSchema(schema)) }), + view: createDocument, + change: (state, change) => runTransaction(state, change), + heads: state => state.store.heads(), + hasData: state => state.store.heads().length > 0, + sedimentree: { + metadata: state => commitChunks(state).map(commitMetadata), + materialize: (state, metadata) => { + const wantedHeads = new Set(metadata.map(entry => entry.head)) + // TODO: Add a runtime commitChunksByHash API if full-history scans become costly. + return commitChunks(state) + .filter(chunk => wantedHeads.has(chunk.hash)) + .map(chunk => ({ ...commitMetadata(chunk), bytes: new Uint8Array(chunk.bytes) })) + }, + apply: (state, blobs) => { + if (blobs.length > 0) { + // TODO: "Add independent StoreHandle snapshots for Repo document states": + // apply blobs to a snapshot and return a new document. + state.store.applyChunkBytes(blobs) + } + return state + }, + liveHashes: state => commitChunks(state).map(chunk => chunk.hash), + }, +}) + +function createDocument(state: ColnState): ColnDocumentBase { + const { store, bindings } = state + const document: ColnDocumentBase & { root?: unknown } = { + heads: () => store.heads(), + jsonIR: () => store.jsonIR(), + rowById: (path, rowId) => store.rowById(path, rowId), + scanTable: path => store.scanTable(path), + } + if (bindings) document.root = new bindings.View(store).root + return Object.freeze(document) +} + +function createTransaction( + state: ColnState, + transaction: TransactionHandle, +): ColnTransactionBase { + const { store, bindings } = state + const exposed: ColnTransactionBase & { root?: unknown } = { + add: (path, values) => transaction.add(path, values), + } + if (bindings) exposed.root = new bindings.Transaction(store, transaction).root + return Object.freeze(exposed) +} + +function runTransaction(document: ColnState, change: ColnChange): ColnState { + // TODO: "Add independent StoreHandle snapshots for Repo document states": + // start the transaction from a snapshot and leave this document unchanged. + const { store, bindings } = document + const transaction = store.beginTransaction() + try { + change(createTransaction(document, transaction)) + const commit = transaction.commit() + return { store: commit.takeStore(), bindings } + } catch (error) { + // beginTransaction() moved ownership of the store into the transaction, and on + // error we cannot return a fresh document. Recover the (aborted) store back onto + // the existing state object, otherwise it would hold a dead StoreHandle. + document.store = transaction.takeStore() + throw error + } +} + +function serializeSchema(schema: ColnSchema): string { + const serialized = JSON.stringify(schema) + if (serialized === undefined) throw new Error("creating a Coln store requires a schema") + return serialized +} + +// TODO: Every call pays to serialize chunk bytes as number[] across the wasm +// boundary, even when only hashes/parents are needed (metadata, liveHashes). +// Switch to Uint8Array output once CommitChunk uses serde_bytes in the runtime. +function commitChunks(document: ColnState): CommitChunk[] { + return document.store.commitChunksAfter([]) +} + +function commitMetadata(chunk: CommitChunk): SedimentreeMeta { + return { kind: "commit", head: chunk.hash, parents: chunk.parents } +} diff --git a/packages/coln-repo/src/colnHandle.ts b/packages/coln-repo/src/colnHandle.ts new file mode 100644 index 00000000..3a892b9a --- /dev/null +++ b/packages/coln-repo/src/colnHandle.ts @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { + AutomergeUrl, + CrdtDocHandle, + DocumentType, + Repo, +} from "@automerge/automerge-repo"; +import type { RealmBindings } from "@coln-project/runtime"; +import { + colnDocType, + type ColnChange, + type ColnDocument, + type ColnSchema, + type ColnState, +} from "./colnDocType.js"; + +type FoundColnHandle = + Bindings extends RealmBindings ? ColnHandle : ColnHandle; + +export type ColnHandle = + CrdtDocHandle< + DocumentType, ColnChange, ColnSchema> + >; + +export function applyBindings( + handle: ColnHandle | ColnHandle, + bindings: Bindings, +): ColnHandle { + const state = handle.fullDoc(); + const existingBindings = state.bindings; + + if (existingBindings) { + if (existingBindings !== bindings) { + throw new TypeError("Coln handle already uses different realm bindings"); + } + return handle as unknown as ColnHandle; + } + + const actualSchema = canonicalJson(JSON.parse(handle.doc().jsonIR())); + const bindingsSchema = canonicalJson(bindings.schema); + if (actualSchema !== bindingsSchema) { + throw new TypeError("Realm bindings schema does not match Coln document schema"); + } + + state.bindings = bindings; + + return handle as unknown as ColnHandle; +} + +export function create( + repo: Repo, + bindings: Bindings, +): ColnHandle { + const handle = repo.create(bindings.schema, colnDocType); + + return applyBindings(handle, bindings); +} + +export async function find( + repo: Repo, + url: AutomergeUrl, + // a rest tuple (rather than an optional parameter) keeps `undefined` in the + // inferred Bindings, so maybe-undefined arguments produce a union return type + ...rest: [bindings: Bindings] | [] +): Promise> { + const [bindings] = rest; + const handle = await repo.find(url, colnDocType); + + return (bindings === undefined ? handle : applyBindings(handle, bindings)) as FoundColnHandle; +} + +function canonicalJson(value: unknown): string | undefined { + return JSON.stringify(value, (_key, current: unknown) => { + if (current === null || Array.isArray(current) || typeof current !== "object") return current; + return Object.fromEntries( + Object.entries(current).sort(([left], [right]) => left.localeCompare(right)), + ); + }); +} diff --git a/packages/coln-repo/src/index.ts b/packages/coln-repo/src/index.ts new file mode 100644 index 00000000..8afb07f1 --- /dev/null +++ b/packages/coln-repo/src/index.ts @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +export { + colnDocType, + type ColnChange, + type ColnDocument, + type RealmBindings, + type ColnSchema, + type ColnTransaction, +} from "./colnDocType.js" + +export { + applyBindings, + create, + find, + type ColnHandle, +} from "./colnHandle.js" diff --git a/packages/coln-repo/tests/browser/index.html b/packages/coln-repo/tests/browser/index.html new file mode 100644 index 00000000..37bb5c8a --- /dev/null +++ b/packages/coln-repo/tests/browser/index.html @@ -0,0 +1,11 @@ + + + + + + Coln Repo Test + + + + + diff --git a/packages/coln-repo/tests/browser/main.ts b/packages/coln-repo/tests/browser/main.ts new file mode 100644 index 00000000..6c1619dc --- /dev/null +++ b/packages/coln-repo/tests/browser/main.ts @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { + Repo, + type AutomergeUrl, + type CrdtDocHandle, +} from "@automerge/automerge-repo" +import { initSync } from "@automerge/automerge-subduction/slim" +import { wasmBase64 } from "@automerge/automerge-subduction/wasm-base64" +import type { RowView, Value } from "@coln-project/runtime" +import { + colnDocType, + applyBindings, + type ColnHandle, + type ColnSchema, +} from "../../src/index.js" +import * as itemFfi from "../fixtures/itemFfi" + +initSync({ module: Uint8Array.from(atob(wasmBase64), char => char.charCodeAt(0)) }) + +type RawHandle = CrdtDocHandle +type ItemHandle = ColnHandle + +const repo = new Repo({ + subductionWebsocketEndpoints: ["ws://127.0.0.1:3031"], +}) +let handle: RawHandle | undefined +let typedHandle: ItemHandle | undefined + +const api = { + create(schema: ColnSchema): AutomergeUrl { + handle = repo.create(schema, colnDocType) + return handle.url + }, + + async find(url: AutomergeUrl): Promise { + handle = await repo.find(url, colnDocType) + }, + + createTyped(): AutomergeUrl { + const rawHandle = repo.create(itemFfi.schema, colnDocType) + handle = rawHandle + typedHandle = applyBindings(rawHandle, itemFfi) + return typedHandle.url + }, + + applyTypedBindings(): void { + typedHandle = applyBindings(currentHandle(), itemFfi) + }, + + add(path: string, values: Value[]): void { + currentHandle().change(transaction => transaction.add(path, values)) + }, + + rows(path: string): RowView[] { + return currentHandle().doc().scanTable(path) + }, + + addTyped(value: string): void { + currentTypedHandle().change(transaction => { + transaction.root.Items(stringValue(value)).add() + }) + }, + + typedCount(value: string): number { + const rows = currentTypedHandle().doc().root.Items(stringValue(value)).values() + let count = 0 + for (let next = rows.next(); !next.done; next = rows.next()) count += 1 + return count + }, + + heads(): string[] { + return currentHandle().doc().heads() + }, + + async flush(): Promise { + await repo.flush() + }, + + async shutdown(): Promise { + await repo.shutdown() + }, +} + +function currentHandle(): RawHandle { + if (!handle) throw new Error("no Coln store is open") + return handle +} + +function currentTypedHandle(): ItemHandle { + if (!typedHandle) throw new Error("no typed Coln store is open") + return typedHandle +} + +function stringValue(value: string): Value { + return { tag: "string", value } +} + +declare global { + interface Window { + colnTest: typeof api + } +} + +window.colnTest = api diff --git a/packages/coln-repo/tests/browser/subduction.d.ts b/packages/coln-repo/tests/browser/subduction.d.ts new file mode 100644 index 00000000..ee91318e --- /dev/null +++ b/packages/coln-repo/tests/browser/subduction.d.ts @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +declare module "@automerge/automerge-subduction/slim" { + export * from "@automerge/automerge-subduction" + export function initSync(input: { module: Uint8Array }): void +} + +declare module "@automerge/automerge-subduction/wasm-base64" { + export const wasmBase64: string +} diff --git a/packages/coln-repo/tests/fixtures/itemFfi.ts b/packages/coln-repo/tests/fixtures/itemFfi.ts new file mode 100644 index 00000000..f181ec17 --- /dev/null +++ b/packages/coln-repo/tests/fixtures/itemFfi.ts @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { + RowIdSet, + type StoreHandle, + type TransactionHandle, + type Value, +} from "@coln-project/runtime" +export { itemSchema as schema } from "./schema" + +interface Root { + Items: (value: Value) => T +} + +export class View { + root: Root + + constructor(store: StoreHandle) { + this.root = { + Items: value => new RowIdSet.View(store, "Test.Items", [value]), + } + } +} + +export class Transaction extends View { + declare root: Root + + constructor(store: StoreHandle, transaction: TransactionHandle) { + super(store) + this.root = { + Items: value => new RowIdSet.Transaction(store, "Test.Items", [value], transaction), + } + } +} diff --git a/packages/coln-repo/tests/fixtures/schema.ts b/packages/coln-repo/tests/fixtures/schema.ts new file mode 100644 index 00000000..d8a45f9d --- /dev/null +++ b/packages/coln-repo/tests/fixtures/schema.ts @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { ColnSchema } from "../../src/index.js" + +export const itemSchema = { + entities: [ + { + path: [["Test"], ["Items"]], + value: { + entityVariant: { tag: "table" }, + columns: [ + { + path: [["value"]], + type: { tag: "builtin", type: "builtinString" }, + }, + ], + primaryKey: null, + }, + }, + ], + rules: [], +} satisfies ColnSchema diff --git a/packages/coln-repo/tests/generated/Graph.ts b/packages/coln-repo/tests/generated/Graph.ts new file mode 100644 index 00000000..db000cd9 --- /dev/null +++ b/packages/coln-repo/tests/generated/Graph.ts @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import * as runtime from "@coln-project/runtime"; + +export interface View { + V: runtime.ColnSet.View; + E: (x: runtime.Value) => (x: runtime.Value) => runtime.ColnSet.View; +} + +export interface Transaction extends View { + V: runtime.ColnSet.Transaction; + E: (x: runtime.Value) => (x: runtime.Value) => runtime.ColnSet.Transaction; +} \ No newline at end of file diff --git a/packages/coln-repo/tests/generated/GraphRealm.json b/packages/coln-repo/tests/generated/GraphRealm.json new file mode 100644 index 00000000..b83d89f3 --- /dev/null +++ b/packages/coln-repo/tests/generated/GraphRealm.json @@ -0,0 +1 @@ +{"entities":[{"path":[["GraphRealm"],["V"]],"value":{"entityVariant":{"tag":"table"},"columns":[],"primaryKey":null}},{"path":[["GraphRealm"],["E"]],"value":{"entityVariant":{"tag":"table"},"columns":[{"path":[["a"]],"type":{"tag":"rowId","path":[["GraphRealm"],["V"]]}},{"path":[["b"]],"type":{"tag":"rowId","path":[["GraphRealm"],["V"]]}}],"primaryKey":null}}],"rules":[{"path":[["GraphRealm"],["V"],["foreignKey"]],"value":{"ruleVariant":"enforced","varNames":[],"varTypes":[],"antecedents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":null,"values":[]}}],"consequents":[]}},{"path":[["GraphRealm"],["E"],["foreignKey"]],"value":{"ruleVariant":"enforced","varNames":[[["a"]],[["b"]]],"varTypes":[{"tag":"rowId","path":[["GraphRealm"],["V"]]},{"tag":"rowId","path":[["GraphRealm"],["V"]]}],"antecedents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["E"]],"rowId":null,"values":[{"column":0,"term":{"tag":"var","index":0}},{"column":1,"term":{"tag":"var","index":1}}]}}],"consequents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":{"tag":"var","index":0},"values":[]}},{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":{"tag":"var","index":1},"values":[]}}]}}]} \ No newline at end of file diff --git a/packages/coln-repo/tests/generated/GraphRealm.pretty b/packages/coln-repo/tests/generated/GraphRealm.pretty new file mode 100644 index 00000000..b4a7d502 --- /dev/null +++ b/packages/coln-repo/tests/generated/GraphRealm.pretty @@ -0,0 +1,13 @@ +flatrealm + entities + table ℜ.V := [] + table ℜ.E := [.a : ℜ.V, .b : ℜ.V] + end + rules + enforced ℜ.V.foreignKey := ℜ.V [] ⊢ ⊤ + enforced ℜ.E.foreignKey a b := ℜ.E [ + .a ↦ a, + .b ↦ b + ] ⊢ a ∈ ℜ.V [] ∧ b ∈ ℜ.V [] + end +end \ No newline at end of file diff --git a/packages/coln-repo/tests/generated/GraphRealm.ts b/packages/coln-repo/tests/generated/GraphRealm.ts new file mode 100644 index 00000000..b7ca28db --- /dev/null +++ b/packages/coln-repo/tests/generated/GraphRealm.ts @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import schema from "./GraphRealm.json" with { type: "json" } +export { schema } +import * as runtime from "@coln-project/runtime" +import * as Graph from "./Graph" + +export class View { + root: Graph.View + + constructor(store: runtime.StoreHandle) { + this.root = { + V: new runtime.RowIdSet.View(store, "GraphRealm.V", []), + E: (a: runtime.Value) => { + return (b: runtime.Value) => { + return new runtime.RowIdSet.View(store, "GraphRealm.E", [a, b]) + } + }, + } + } +} + +export class Transaction extends View { + root: Graph.Transaction + + constructor(store: runtime.StoreHandle, transaction: runtime.TransactionHandle) { + super(store) + this.root = { + V: new runtime.RowIdSet.Transaction(store, "GraphRealm.V", [], transaction), + E: (a: runtime.Value) => { + return (b: runtime.Value) => { + return new runtime.RowIdSet.Transaction(store, "GraphRealm.E", [a, b], transaction) + } + }, + } + } +} diff --git a/packages/coln-repo/tests/handle.test.ts b/packages/coln-repo/tests/handle.test.ts new file mode 100644 index 00000000..a8096050 --- /dev/null +++ b/packages/coln-repo/tests/handle.test.ts @@ -0,0 +1,165 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import assert from "node:assert/strict"; +import { afterEach, beforeEach, test } from "node:test"; +import type { Value } from "@coln-project/runtime"; +import { applyBindings, colnDocType, create, find, type ColnDocument } from "../src/index"; +import * as itemFfi from "./fixtures/itemFfi"; +import schema from "./generated/GraphRealm.json" with { type: "json" }; +import * as GraphRealm from "./generated/GraphRealm"; +import { createTestRepoPair, waitForChange, type TestRepoPair } from "./helpers/TestRepoPair"; + +let repos: TestRepoPair; + +beforeEach(async () => { + repos = await createTestRepoPair(); +}); + +afterEach(async () => { + await repos.shutdown(); +}); + +test("create returns a bound, initialized handle", () => { + const handle = create(repos.source, GraphRealm); + const document = handle.doc(); + + assert.deepEqual(Object.keys(document).sort(), ["heads", "jsonIR", "root", "rowById", "scanTable"]); + assert.deepEqual(JSON.parse(document.jsonIR()), schema); + assert.deepEqual(document.heads(), handle.rawHeads()); + assert.deepEqual(document.scanTable("GraphRealm.V"), []); +}); + +test("document can be updated and changes are reflected", () => { + const handle = create(repos.source, GraphRealm); + + let v1!: Value, v2!: Value, e1!: Value; + + handle.change((tx) => { + v1 = tx.root.V.add(); + v2 = tx.root.V.add(); + e1 = tx.root.E(v1)(v2).add(); + }); + + const doc = handle.doc(); + + assert(doc.root.V.has(v1)); + assert(doc.root.V.has(v2)); + assert(doc.root.E(v1)(v2).has(e1)); + assert.equal(v1.tag, "row_id"); + assert.deepEqual(doc.rowById("GraphRealm.V", v1.value)?.rowId, v1); +}); + +test("changes expose only safe Coln operations", () => { + const handle = create(repos.source, GraphRealm); + + handle.change(transaction => { + assert.deepEqual(Object.keys(transaction).sort(), ["add", "root"]); + transaction.add("GraphRealm.V", []); + }); + + assert.equal(handle.doc().scanTable("GraphRealm.V").length, 1); +}); + +test("failed changes are aborted and leave the handle usable", () => { + const handle = create(repos.source, GraphRealm); + let committed!: Value; + + assert.throws( + () => + handle.change((transaction) => { + transaction.root.V.add(); + throw new Error("abort"); + }), + new Error("abort"), + ); + handle.change((transaction) => { + committed = transaction.root.V.add(); + }); + + assert.deepEqual( + handle + .doc() + .scanTable("GraphRealm.V") + .map((row) => row.rowId), + [committed], + ); +}); + +test("typed handles sync changes", async () => { + const handle = create(repos.source, GraphRealm); + const replica = await find(repos.replica, handle.url, GraphRealm); + let vertex!: Value; + + const changed = new Promise>(resolve => { + replica.once("change", payload => { + if (payload.doc) resolve(payload.doc); + }); + }); + handle.change((tx) => { + vertex = tx.root.V.add(); + }); + const changedDocument = await changed; + + assert(changedDocument.root.V.has(vertex)); + assert.deepEqual(changedDocument.scanTable("GraphRealm.V").map(row => row.rowId), [vertex]); + assert(replica.doc().root.V.has(vertex)); +}); + +test("raw changes sync into a typed handle", async () => { + const handle = create(repos.source, GraphRealm); + const replica = await repos.replica.find(handle.url, colnDocType); + let vertex!: Value; + + const changed = waitForChange(handle); + replica.change((transaction) => { + vertex = transaction.add("GraphRealm.V", []); + }); + await changed; + + assert(handle.doc().root.V.has(vertex)); +}); + +test("find returns a raw handle when bindings are omitted", async () => { + const handle = create(repos.source, GraphRealm); + const replica = await find(repos.replica, handle.url); + + assert.deepEqual(Object.keys(replica.doc()).sort(), ["heads", "jsonIR", "rowById", "scanTable"]); +}); + +test("bindings can be applied to a raw found handle later", async () => { + const handle = create(repos.source, GraphRealm); + const rawReplica = await find(repos.replica, handle.url); + const replica = applyBindings(rawReplica, GraphRealm); + + assert.strictEqual(replica, rawReplica); + assert.strictEqual(applyBindings(replica, GraphRealm), replica); + + let vertex!: Value; + replica.change(transaction => { + vertex = transaction.root.V.add(); + }); + + assert(replica.doc().root.V.has(vertex)); + assert.deepEqual(replica.doc().scanTable("GraphRealm.V").map(row => row.rowId), [vertex]); +}); + +test("applying different bindings to a bound handle is rejected", () => { + const handle = create(repos.source, GraphRealm); + const otherBindings = { ...GraphRealm }; + + assert.throws( + () => applyBindings(handle, otherBindings), + new TypeError("Coln handle already uses different realm bindings"), + ); +}); + +test("applying bindings for a different schema is rejected", () => { + const handle = repos.source.create(schema, colnDocType); + + assert.throws( + () => applyBindings(handle, itemFfi), + new TypeError("Realm bindings schema does not match Coln document schema"), + ); +}); diff --git a/packages/coln-repo/tests/helpers/TestNetworkAdapter.ts b/packages/coln-repo/tests/helpers/TestNetworkAdapter.ts new file mode 100644 index 00000000..e25621d3 --- /dev/null +++ b/packages/coln-repo/tests/helpers/TestNetworkAdapter.ts @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { + NetworkAdapter, + type Message, + type PeerId, +} from "@automerge/automerge-repo" + +export class TestNetworkAdapter extends NetworkAdapter { + #connected = false + #peer?: TestNetworkAdapter + + static createConnectedPair(): [TestNetworkAdapter, TestNetworkAdapter] { + const first = new TestNetworkAdapter() + const second = new TestNetworkAdapter() + first.#peer = second + second.#peer = first + return [first, second] + } + + isReady(): boolean { + return true + } + + async whenReady(): Promise {} + + connect(peerId: PeerId): void { + this.peerId = peerId + this.#connected = true + } + + disconnect(): void { + this.#connected = false + } + + announce(peerId: PeerId): void { + this.emit("peer-candidate", { peerId, peerMetadata: {} }) + } + + send(message: Message): void { + if (!this.#connected) return + queueMicrotask(() => this.#peer?.receive(message)) + } + + private receive(message: Message): void { + if (this.#connected) this.emit("message", message) + } +} diff --git a/packages/coln-repo/tests/helpers/TestRepoPair.ts b/packages/coln-repo/tests/helpers/TestRepoPair.ts new file mode 100644 index 00000000..707f79d2 --- /dev/null +++ b/packages/coln-repo/tests/helpers/TestRepoPair.ts @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { + initSubduction, + Repo, +} from "@automerge/automerge-repo" +import { setTimeout as delay } from "node:timers/promises" +import { TestNetworkAdapter } from "./TestNetworkAdapter" + +export interface TestRepoPair { + source: Repo + replica: Repo + shutdown(): Promise +} + +export async function createTestRepoPair(): Promise { + await initSubduction() + const [sourceAdapter, replicaAdapter] = TestNetworkAdapter.createConnectedPair() + const source = new Repo({ + subductionAdapters: [{ adapter: sourceAdapter, serviceName: "test", role: "connect" }], + subductionTimeouts: { healMaxAttempts: 0, syncMs: 1_000 }, + }) + const replica = new Repo({ + subductionAdapters: [ + { adapter: replicaAdapter, serviceName: "test", role: "accept" }, + ], + subductionTimeouts: { healMaxAttempts: 0, syncMs: 1_000 }, + }) + + const sourceConnected = waitForConnection(source) + const replicaConnected = waitForConnection(replica) + sourceAdapter.announce(replica.peerId) + replicaAdapter.announce(source.peerId) + await Promise.all([sourceConnected, replicaConnected]) + + return { + source, + replica, + async shutdown() { + await Promise.all([source.flush(), replica.flush()]) + await Promise.all([ + source.subduction.then(subduction => subduction.disconnectAll()), + replica.subduction.then(subduction => subduction.disconnectAll()), + ]) + await Promise.all([source.shutdown(), replica.shutdown()]) + }, + } +} + +export function waitForChange(handle: { + once(event: "change", listener: () => void): unknown +}): Promise { + return withTimeout( + new Promise(resolve => handle.once("change", () => resolve())), + "handle change", + ) +} + +function waitForConnection(repo: Repo): Promise { + if (repo.isSubductionConnected()) return Promise.resolve() + return withTimeout( + new Promise(resolve => { + const onConnection = ({ connected }: { connected: boolean }) => { + if (!connected) return + repo.off("subduction-connection", onConnection) + resolve() + } + repo.on("subduction-connection", onConnection) + }), + "Subduction connection", + ) +} + +function withTimeout(promise: Promise, operation: string): Promise { + return Promise.race([ + promise, + delay(5_000, undefined, { ref: false }).then(() => { + throw new Error(`${operation} timed out`) + }), + ]) +} diff --git a/packages/coln-repo/tests/package/consumer.ts b/packages/coln-repo/tests/package/consumer.ts new file mode 100644 index 00000000..e7d36bb0 --- /dev/null +++ b/packages/coln-repo/tests/package/consumer.ts @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { AutomergeUrl, Repo } from "@automerge/automerge-repo" +import type { Value } from "@coln-project/runtime" +import { + colnDocType, + find, + type ColnChange, + type ColnDocument, + type ColnHandle, + type ColnTransaction, + type RealmBindings, +} from "@coln-project/repo" + +type Equal = + (() => Type extends Left ? 1 : 2) extends () => Type extends Right ? 1 : 2 + ? true + : false +type Expect = Type + +declare const repo: Repo +declare const url: AutomergeUrl +declare const bindings: RealmBindings +declare const optionalBindings: RealmBindings | undefined + +const raw = find(repo, url) +const explicitUndefined = find(repo, url, undefined) +const typed = find(repo, url, bindings) +const optional = find(repo, url, optionalBindings) + +void raw.then(handle => { + handle.doc().scanTable("Example.Items") + handle.fullDoc().store.heads() +}) + +void typed.then(handle => { + handle.doc().root + handle.on("change", payload => { + payload.doc?.root + payload.handle.doc().root + }) + handle.addListener("change", payload => payload.doc?.root) + handle.listeners("change").forEach(listener => listener) + handle.on("heads-changed", payload => { + payload.doc.store + payload.handle.doc().root + }) + handle.change(transaction => { + const value: Value = transaction.add("Example.Items", []) + value + transaction.root + }) +}) + +type DocumentOperation = "heads" | "jsonIR" | "rowById" | "scanTable" + +export type TypeChecks = [ + Expect>>, + Expect>>, + Expect>>>, + Expect>>>, + Expect["doc"]>, ColnDocument>>, + Expect< + Equal["change"]>[0], ColnChange> + >, + Expect>[0], ColnTransaction>>, + Expect>, + Expect, DocumentOperation | "root">>, + Expect>, + Expect, "add" | "root">>, +] diff --git a/packages/coln-repo/tests/server.mjs b/packages/coln-repo/tests/server.mjs new file mode 100644 index 00000000..d91e2fc5 --- /dev/null +++ b/packages/coln-repo/tests/server.mjs @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { WebSocketServer } from "ws" +import { + MemorySigner, + MemoryStorage, + Subduction, +} from "@automerge/automerge-subduction" + +class WebSocketTransport { + #socket + #queue = [] + #waiters = [] + #errorWaiters = [] + #closed = false + #closedPromise + #resolveClosed + #disconnectCallback = null + + constructor(socket) { + this.#socket = socket + this.#closedPromise = new Promise(resolve => { + this.#resolveClosed = resolve + }) + + socket.on("message", data => { + const bytes = toUint8Array(data) + const waiter = this.#waiters.shift() + if (waiter) { + this.#errorWaiters.shift() + waiter(bytes) + } else { + this.#queue.push(bytes) + } + }) + socket.on("close", () => this.#markClosed(new Error("WebSocket closed"))) + socket.on("error", error => this.#markClosed(error)) + } + + onDisconnect(callback) { + this.#disconnectCallback = callback + } + + async sendBytes(bytes) { + if (this.#closed) throw new Error("WebSocket closed") + await new Promise((resolve, reject) => { + this.#socket.send(Buffer.from(bytes), error => (error ? reject(error) : resolve())) + }) + } + + recvBytes() { + const queued = this.#queue.shift() + if (queued) return Promise.resolve(queued) + if (this.#closed) return Promise.reject(new Error("WebSocket closed")) + return new Promise((resolve, reject) => { + this.#waiters.push(resolve) + this.#errorWaiters.push(reject) + }) + } + + async disconnect() { + this.#markClosed(new Error("WebSocket closed"), false) + this.#socket.close() + } + + closed() { + return this.#closedPromise + } + + #markClosed(error, notify = true) { + if (this.#closed) return + this.#closed = true + this.#resolveClosed() + for (const reject of this.#errorWaiters) reject(error) + this.#waiters = [] + this.#errorWaiters = [] + if (notify) this.#disconnectCallback?.() + } +} + +function toUint8Array(data) { + if (data instanceof Uint8Array) { + return new Uint8Array(data.buffer, data.byteOffset, data.byteLength) + } + if (data instanceof ArrayBuffer) return new Uint8Array(data) + if (Array.isArray(data)) return toUint8Array(Buffer.concat(data)) + return new Uint8Array(Buffer.from(data)) +} + +const port = 3031 +const serviceName = `127.0.0.1:${port}` +const subduction = new Subduction({ + signer: new MemorySigner(), + storage: new MemoryStorage(), +}) +const server = new WebSocketServer({ port }) + +server.on("connection", socket => { + subduction.acceptTransport(new WebSocketTransport(socket), serviceName).catch(error => { + if (!String(error?.message ?? error).includes("closed")) console.error(error) + }) +}) + +async function shutdown() { + await subduction.disconnectAll().catch(() => {}) + server.close(() => process.exit(0)) +} + +process.on("SIGINT", shutdown) +process.on("SIGTERM", shutdown) + +console.log(`Subduction test relay listening on ws://127.0.0.1:${port}`) diff --git a/packages/coln-repo/tests/sync.spec.ts b/packages/coln-repo/tests/sync.spec.ts new file mode 100644 index 00000000..c4a154e5 --- /dev/null +++ b/packages/coln-repo/tests/sync.spec.ts @@ -0,0 +1,190 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { expect, test, type Page } from "@playwright/test" +import { + initSubduction, + Repo, + type AutomergeUrl, + type CrdtDocHandle, +} from "@automerge/automerge-repo" +import { colnDocType } from "../src/index.js" +import { itemSchema } from "./fixtures/schema" + +const table = "Test.Items" +type RawHandle = CrdtDocHandle + +test("browser and Node repos create, find, and update each other's stores", async ({ browser }) => { + await initSubduction() + const context = await browser.newContext() + const pages: Page[] = [] + let nodeRepo: Repo | undefined + + try { + nodeRepo = new Repo({ + subductionWebsocketEndpoints: ["ws://127.0.0.1:3031"], + }) + const browserCreator = await openPage(context.newPage()) + pages.push(browserCreator) + const browserUrl = await browserCreator.evaluate( + schema => window.colnTest.create(schema), + itemSchema, + ) + await add(browserCreator, "browser-one") + await browserCreator.evaluate(() => window.colnTest.flush()) + + const nodeFinder = await nodeRepo.find(browserUrl, colnDocType) + await expectNodeRows(nodeFinder, ["browser-one"]) + + addInNode(nodeFinder, "node-two") + await nodeRepo.flush() + await expectRows(browserCreator, ["browser-one", "node-two"]) + await expectSameHeads(browserCreator, nodeFinder) + + const nodeCreator = nodeRepo.create(itemSchema, colnDocType) + addInNode(nodeCreator, "node-one") + await nodeRepo.flush() + + const browserFinder = await openPage(context.newPage()) + pages.push(browserFinder) + await browserFinder.evaluate( + url => window.colnTest.find(url), + nodeCreator.url as AutomergeUrl, + ) + await expectRows(browserFinder, ["node-one"]) + + await add(browserFinder, "browser-two") + await browserFinder.evaluate(() => window.colnTest.flush()) + await expectNodeRows(nodeCreator, ["browser-two", "node-one"]) + await expectSameHeads(browserFinder, nodeCreator) + } finally { + try { + await shutdownPages(pages) + } finally { + try { + await nodeRepo?.shutdown() + } finally { + await context.close() + } + } + } +}) + +test("reloads without schema and applies typed bindings", async ({ browser }) => { + const context = await browser.newContext() + const pages: Page[] = [] + + try { + const creator = await openPage(context.newPage()) + pages.push(creator) + const url = await creator.evaluate(() => window.colnTest.createTyped()) + + await creator.evaluate(() => window.colnTest.addTyped("one")) + await creator.evaluate(() => window.colnTest.flush()) + + const finder = await openPage(context.newPage()) + pages.push(finder) + await finder.evaluate(url => window.colnTest.find(url), url) + await expectRows(finder, ["one"]) + await finder.evaluate(() => window.colnTest.applyTypedBindings()) + await expectTypedCount(finder, "one", 1) + + await finder.evaluate(() => window.colnTest.addTyped("two")) + await finder.evaluate(() => window.colnTest.flush()) + await expectTypedCount(creator, "two", 1) + + await shutdownPages([creator, finder]) + await creator.close() + await finder.close() + + const reloaded = await openPage(context.newPage()) + pages.push(reloaded) + await reloaded.evaluate(url => window.colnTest.find(url), url) + await expectRows(reloaded, ["one", "two"]) + await reloaded.evaluate(() => window.colnTest.applyTypedBindings()) + await expectTypedCount(reloaded, "one", 1) + await expectTypedCount(reloaded, "two", 1) + } finally { + try { + await shutdownPages(pages) + } finally { + await context.close() + } + } +}) + +async function openPage(pagePromise: Promise): Promise { + const page = await pagePromise + await page.goto("/") + await page.waitForFunction(() => window.colnTest !== undefined) + return page +} + +async function shutdownPages(pages: Page[]): Promise { + await Promise.all( + pages + .filter(page => !page.isClosed()) + .map(page => page.evaluate(() => window.colnTest.shutdown()).catch(() => undefined)), + ) +} + +async function add(page: Page, value: string) { + await page.evaluate( + ({ table, value }) => window.colnTest.add(table, [{ tag: "string", value }]), + { table, value }, + ) +} + +async function expectRows(page: Page, values: string[]) { + await expect + .poll(() => + page.evaluate( + table => + window.colnTest + .rows(table) + .map(row => row.values[0]) + .filter(value => value?.tag === "string") + .map(value => value.value) + .sort(), + table, + ), + ) + .toEqual([...values].sort()) +} + +function addInNode(handle: RawHandle, value: string) { + handle.change(transaction => transaction.add(table, [{ tag: "string", value }])) +} + +async function expectNodeRows(handle: RawHandle, values: string[]) { + await expect + .poll(() => + handle + .doc() + .scanTable(table) + .map(row => row.values[0]) + .filter(value => value?.tag === "string") + .map(value => value.value) + .sort(), + ) + .toEqual([...values].sort()) +} + +async function expectSameHeads(page: Page, handle: RawHandle) { + await expect + .poll(async () => { + const browserHeads = (await page.evaluate(() => window.colnTest.heads())).sort() + const nodeHeads = handle.doc().heads().sort() + return browserHeads.join(",") === nodeHeads.join(",") + ? "equal" + : `browser: ${browserHeads.join(",")}; Node: ${nodeHeads.join(",")}` + }) + .toBe("equal") +} + +async function expectTypedCount(page: Page, value: string, count: number) { + await expect + .poll(() => page.evaluate(value => window.colnTest.typedCount(value), value)) + .toBe(count) +} diff --git a/packages/coln-repo/tsconfig.build.json b/packages/coln-repo/tsconfig.build.json new file mode 100644 index 00000000..274a9026 --- /dev/null +++ b/packages/coln-repo/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["tests"] +} diff --git a/packages/coln-repo/tsconfig.json b/packages/coln-repo/tsconfig.json new file mode 100644 index 00000000..6d028484 --- /dev/null +++ b/packages/coln-repo/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "ES2022"], + "strict": true, + "noEmit": true, + "types": ["node", "vite/client"] + }, + "include": ["src/**/*.ts", "tests/**/*.ts", "*.config.ts"], + "exclude": ["tests/package"] +} diff --git a/packages/coln-repo/tsconfig.package.json b/packages/coln-repo/tsconfig.package.json new file mode 100644 index 00000000..d8dc1cd9 --- /dev/null +++ b/packages/coln-repo/tsconfig.package.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": [] + }, + "include": ["tests/package/**/*.ts"], + "exclude": [] +} diff --git a/packages/coln-repo/vite.config.ts b/packages/coln-repo/vite.config.ts new file mode 100644 index 00000000..06488873 --- /dev/null +++ b/packages/coln-repo/vite.config.ts @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { defineConfig } from "vite" +import wasm from "vite-plugin-wasm" + +export default defineConfig({ + root: "tests/browser", + plugins: [wasm()], + build: { target: "esnext" }, +}) From 9075449f7ddbab730199a6471cd239b2c3f61ff3 Mon Sep 17 00:00:00 2001 From: Alex Currie-Clark Date: Thu, 3 Sep 2026 07:49:36 +0100 Subject: [PATCH 08/18] Document transaction validation gap Co-authored-by: acurrieclark <1306728+acurrieclark@users.noreply.github.com> --- packages/coln-repo/src/colnDocType.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/coln-repo/src/colnDocType.ts b/packages/coln-repo/src/colnDocType.ts index 45ca8d73..9f8e690f 100644 --- a/packages/coln-repo/src/colnDocType.ts +++ b/packages/coln-repo/src/colnDocType.ts @@ -87,6 +87,8 @@ function createTransaction( ): ColnTransactionBase { const { store, bindings } = state const exposed: ColnTransactionBase & { root?: unknown } = { + // TODO: Validate values before WASM; invalid argument conversion can prevent + // transaction recovery. Avoid duplicating the runtime schema without a clear design. add: (path, values) => transaction.add(path, values), } if (bindings) exposed.root = new bindings.Transaction(store, transaction).root From b8c6ae0043f83c2a0cba5105dd538014b8762303 Mon Sep 17 00:00:00 2001 From: Alex Currie-Clark Date: Wed, 2 Sep 2026 11:18:45 +0100 Subject: [PATCH 09/18] Add sync demos back Co-authored-by: acurrieclark <1306728+acurrieclark@users.noreply.github.com> --- examples/cli-demo/.gitignore | 4 + examples/cli-demo/README.md | 35 + examples/cli-demo/package.json | 30 + examples/cli-demo/playwright.config.ts | 21 + examples/cli-demo/pnpm-lock.yaml | 614 +++++ examples/cli-demo/pnpm-workspace.yaml | 7 + examples/cli-demo/src/cli.ts | 23 + examples/cli-demo/src/cli/arguments.ts | 54 + examples/cli-demo/src/cli/commands/exec.ts | 10 + examples/cli-demo/src/cli/commands/ir.ts | 9 + examples/cli-demo/src/cli/commands/query.ts | 13 + examples/cli-demo/src/cli/evaluation.ts | 35 + examples/cli-demo/src/cli/help.ts | 72 + examples/cli-demo/src/cli/main.ts | 53 + examples/cli-demo/src/cli/repository.ts | 92 + examples/cli-demo/tests/cli/arguments.test.ts | 50 + .../cli-demo/tests/cli/evaluation.test.ts | 62 + examples/cli-demo/tests/server.mjs | 112 + examples/cli-demo/tests/sync.spec.ts | 72 + examples/cli-demo/tsconfig.build.json | 9 + examples/cli-demo/tsconfig.json | 11 + examples/sync/.gitignore | 4 + examples/sync/README.md | 96 + examples/sync/compile.sh | 16 + examples/sync/graph.coln | 7 + examples/sync/index.html | 15 + examples/sync/package.json | 42 + examples/sync/playwright.config.ts | 39 + examples/sync/pnpm-lock.yaml | 2290 +++++++++++++++++ examples/sync/pnpm-workspace.yaml | 7 + examples/sync/server/server.mjs | 161 ++ examples/sync/server/with-server.mjs | 108 + examples/sync/src/browser/App.svelte | 165 ++ .../sync/src/browser/DocumentLoadError.svelte | 53 + examples/sync/src/browser/GraphCanvas.svelte | 205 ++ .../sync/src/browser/GraphControls.svelte | 95 + examples/sync/src/browser/main.ts | 58 + examples/sync/src/browser/style.css | 5 + examples/sync/src/cli/App.tsx | 219 ++ examples/sync/src/cli/main.tsx | 113 + examples/sync/src/generated/Graph.ts | 11 + examples/sync/src/generated/GraphRealm.json | 1 + examples/sync/src/generated/GraphRealm.pretty | 13 + examples/sync/src/generated/GraphRealm.ts | 48 + examples/sync/src/lib/coln-handle.svelte.ts | 34 + examples/sync/src/lib/graph.ts | 114 + .../sync/src/lib/subduction-sync.svelte.ts | 106 + examples/sync/src/query/execute-query.ts | 77 + examples/sync/tests/coln-handle.unit.ts | 89 + examples/sync/tests/observe.svelte.ts | 9 + examples/sync/tests/subduction-sync.unit.ts | 259 ++ examples/sync/tests/sync.spec.ts | 154 ++ examples/sync/tsconfig.json | 21 + examples/sync/vite.config.ts | 15 + examples/sync/vitest.config.ts | 24 + 55 files changed, 6061 insertions(+) create mode 100644 examples/cli-demo/.gitignore create mode 100644 examples/cli-demo/README.md create mode 100644 examples/cli-demo/package.json create mode 100644 examples/cli-demo/playwright.config.ts create mode 100644 examples/cli-demo/pnpm-lock.yaml create mode 100644 examples/cli-demo/pnpm-workspace.yaml create mode 100644 examples/cli-demo/src/cli.ts create mode 100644 examples/cli-demo/src/cli/arguments.ts create mode 100644 examples/cli-demo/src/cli/commands/exec.ts create mode 100644 examples/cli-demo/src/cli/commands/ir.ts create mode 100644 examples/cli-demo/src/cli/commands/query.ts create mode 100644 examples/cli-demo/src/cli/evaluation.ts create mode 100644 examples/cli-demo/src/cli/help.ts create mode 100644 examples/cli-demo/src/cli/main.ts create mode 100644 examples/cli-demo/src/cli/repository.ts create mode 100644 examples/cli-demo/tests/cli/arguments.test.ts create mode 100644 examples/cli-demo/tests/cli/evaluation.test.ts create mode 100644 examples/cli-demo/tests/server.mjs create mode 100644 examples/cli-demo/tests/sync.spec.ts create mode 100644 examples/cli-demo/tsconfig.build.json create mode 100644 examples/cli-demo/tsconfig.json create mode 100644 examples/sync/.gitignore create mode 100644 examples/sync/README.md create mode 100644 examples/sync/compile.sh create mode 100644 examples/sync/graph.coln create mode 100644 examples/sync/index.html create mode 100644 examples/sync/package.json create mode 100644 examples/sync/playwright.config.ts create mode 100644 examples/sync/pnpm-lock.yaml create mode 100644 examples/sync/pnpm-workspace.yaml create mode 100644 examples/sync/server/server.mjs create mode 100644 examples/sync/server/with-server.mjs create mode 100644 examples/sync/src/browser/App.svelte create mode 100644 examples/sync/src/browser/DocumentLoadError.svelte create mode 100644 examples/sync/src/browser/GraphCanvas.svelte create mode 100644 examples/sync/src/browser/GraphControls.svelte create mode 100644 examples/sync/src/browser/main.ts create mode 100644 examples/sync/src/browser/style.css create mode 100644 examples/sync/src/cli/App.tsx create mode 100644 examples/sync/src/cli/main.tsx create mode 100644 examples/sync/src/generated/Graph.ts create mode 100644 examples/sync/src/generated/GraphRealm.json create mode 100644 examples/sync/src/generated/GraphRealm.pretty create mode 100644 examples/sync/src/generated/GraphRealm.ts create mode 100644 examples/sync/src/lib/coln-handle.svelte.ts create mode 100644 examples/sync/src/lib/graph.ts create mode 100644 examples/sync/src/lib/subduction-sync.svelte.ts create mode 100644 examples/sync/src/query/execute-query.ts create mode 100644 examples/sync/tests/coln-handle.unit.ts create mode 100644 examples/sync/tests/observe.svelte.ts create mode 100644 examples/sync/tests/subduction-sync.unit.ts create mode 100644 examples/sync/tests/sync.spec.ts create mode 100644 examples/sync/tsconfig.json create mode 100644 examples/sync/vite.config.ts create mode 100644 examples/sync/vitest.config.ts diff --git a/examples/cli-demo/.gitignore b/examples/cli-demo/.gitignore new file mode 100644 index 00000000..8166d595 --- /dev/null +++ b/examples/cli-demo/.gitignore @@ -0,0 +1,4 @@ +dist/ +node_modules/ +test-results/ +playwright-report/ diff --git a/examples/cli-demo/README.md b/examples/cli-demo/README.md new file mode 100644 index 00000000..658d06c7 --- /dev/null +++ b/examples/cli-demo/README.md @@ -0,0 +1,35 @@ +# Coln CLI demo + +`coln-repo` is a scriptable CLI for reading and updating an existing synchronized +Coln document. Unlike `examples/sync`, it has no fixed schema or interactive UI. + +Build local packages first: + +```bash +npm ci --prefix packages/coln-js-runtime +npm run --prefix packages/coln-js-runtime build +pnpm --dir packages/coln-repo install +pnpm --dir packages/coln-repo build +pnpm --dir examples/cli-demo install +pnpm --dir examples/cli-demo build +``` + +Run it with an Automerge URL: + +```bash +node examples/cli-demo/dist/cli.js ir +node examples/cli-demo/dist/cli.js query 'store.scanTable("GraphRealm.V")' +node examples/cli-demo/dist/cli.js exec 'txn.add("GraphRealm.V", [])' +``` + +Use `-` as the query or script argument to read from stdin. `query` receives a +read-only `store`; `exec` receives an add-only `txn` and waits for the change to +sync before exiting. Set `SUBDUCTION_ENDPOINT` to use another WebSocket relay. + +JavaScript is trusted local code and must complete synchronously. + +Verify with: + +```bash +pnpm --dir examples/cli-demo test +``` diff --git a/examples/cli-demo/package.json b/examples/cli-demo/package.json new file mode 100644 index 00000000..40dd022e --- /dev/null +++ b/examples/cli-demo/package.json @@ -0,0 +1,30 @@ +{ + "name": "coln-cli-demo", + "private": true, + "type": "module", + "license": "(Apache-2.0 OR MIT)", + "bin": { + "coln-repo": "./dist/cli.js" + }, + "scripts": { + "build": "tsc -p tsconfig.build.json", + "cli": "tsx src/cli.ts", + "typecheck": "tsc --noEmit", + "test": "pnpm run typecheck && pnpm run test:unit && pnpm run test:e2e", + "test:unit": "tsx --test \"tests/cli/**/*.test.ts\"", + "test:e2e": "pnpm run build && playwright test" + }, + "dependencies": { + "@automerge/automerge-repo": "github:automerge/automerge-repo#doctypes&path:packages/automerge-repo", + "@coln-project/repo": "link:../../packages/coln-repo" + }, + "devDependencies": { + "@automerge/automerge-subduction": "0.16.1", + "@playwright/test": "^1.62.1", + "@types/node": "^25.9.5", + "@types/ws": "^8.18.1", + "tsx": "^4.23.13", + "typescript": "^6.0.3", + "ws": "^8.21.3" + } +} diff --git a/examples/cli-demo/playwright.config.ts b/examples/cli-demo/playwright.config.ts new file mode 100644 index 00000000..31262adf --- /dev/null +++ b/examples/cli-demo/playwright.config.ts @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { defineConfig } from "@playwright/test" + +export default defineConfig({ + testDir: "./tests", + testMatch: "**/*.spec.ts", + timeout: 30_000, + expect: { timeout: 10_000 }, + fullyParallel: false, + retries: 0, + webServer: { + command: "node tests/server.mjs", + port: 3031, + reuseExistingServer: false, + stdout: "pipe", + stderr: "pipe", + }, +}) diff --git a/examples/cli-demo/pnpm-lock.yaml b/examples/cli-demo/pnpm-lock.yaml new file mode 100644 index 00000000..1d1faeb1 --- /dev/null +++ b/examples/cli-demo/pnpm-lock.yaml @@ -0,0 +1,614 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-subduction': 0.16.1 + +importers: + + .: + dependencies: + '@automerge/automerge-repo': + specifier: github:automerge/automerge-repo#doctypes&path:packages/automerge-repo + version: https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo + '@coln-project/repo': + specifier: link:../../packages/coln-repo + version: link:../../packages/coln-repo + devDependencies: + '@automerge/automerge-subduction': + specifier: 0.16.1 + version: 0.16.1 + '@playwright/test': + specifier: ^1.62.1 + version: 1.62.1 + '@types/node': + specifier: ^25.9.5 + version: 25.9.5 + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 + tsx: + specifier: ^4.23.13 + version: 4.23.13 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + ws: + specifier: ^8.21.3 + version: 8.21.3 + +packages: + + '@automerge/automerge-repo@https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo': + resolution: {gitHosted: true, path: packages/automerge-repo, tarball: https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5} + version: 2.6.0-subduction.47 + engines: {node: '>=22.13'} + + '@automerge/automerge-subduction@0.16.1': + resolution: {integrity: sha512-alH7U4eYn0O7sT6hNLv7CJhNODJi+QD6qfRQTNfZCxL3Q5JZdJ8lFpdvXUkBf+I3bu0GLvVlTaYPWGrBihjpIw==} + + '@automerge/automerge@3.3.2': + resolution: {integrity: sha512-9vCdCL7pdQwUra66SBxPVHr+/t9epXKni9KDeak2rNBFMzABVh2u6gSpcwxi3jkR5qr047jVqZv9DlrOfVxLFw==} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} + cpu: [arm64] + os: [darwin] + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + resolution: {integrity: sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==} + cpu: [x64] + os: [darwin] + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + resolution: {integrity: sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==} + cpu: [arm64] + os: [linux] + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + resolution: {integrity: sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==} + cpu: [arm] + os: [linux] + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + resolution: {integrity: sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==} + cpu: [x64] + os: [linux] + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + resolution: {integrity: sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==} + cpu: [x64] + os: [win32] + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@playwright/test@1.62.1': + resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} + engines: {node: '>=20'} + hasBin: true + + '@types/node@25.9.5': + resolution: {integrity: sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==} + + '@types/node@26.2.0': + resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + + bs58check@4.0.0: + resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} + + cbor-extract@2.2.2: + resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} + hasBin: true + + cbor-x@1.6.6: + resolution: {integrity: sha512-8QiD9PGOxyQHo7s2pzwTBH6lTjqekxPdl9Aq6fXvZgCuCJHOht1puDEA/fTr6mciB76c+M+Gi0qT2i1a4pm4Wg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + + playwright-core@1.62.1: + resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.62.1: + resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} + engines: {node: '>=20'} + hasBin: true + + tsx@4.23.13: + resolution: {integrity: sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + uuid@14.0.2: + resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} + hasBin: true + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xstate@5.32.6: + resolution: {integrity: sha512-WfA8WNrh6r9osuGwVm+aIPM4jmMW2LKHV1Yv9thYpOtL4HVF/kobh95K/O8v2jDCpDmP2EoY+6uvuqHXCZ6ZLw==} + +snapshots: + + '@automerge/automerge-repo@https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo': + dependencies: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-subduction': 0.16.1 + bs58check: 4.0.0 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + isomorphic-ws: 5.0.0(ws@8.21.3) + uuid: 14.0.2 + ws: 8.21.3 + xstate: 5.32.6 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-subduction@0.16.1': {} + + '@automerge/automerge@3.3.2': {} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@noble/hashes@1.8.0': {} + + '@playwright/test@1.62.1': + dependencies: + playwright: 1.62.1 + + '@types/node@25.9.5': + dependencies: + undici-types: 7.24.6 + + '@types/node@26.2.0': + dependencies: + undici-types: 8.3.0 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 26.2.0 + + base-x@5.0.1: {} + + bs58@6.0.0: + dependencies: + base-x: 5.0.1 + + bs58check@4.0.0: + dependencies: + '@noble/hashes': 1.8.0 + bs58: 6.0.0 + + cbor-extract@2.2.2: + dependencies: + node-gyp-build-optional-packages: 5.1.1 + optionalDependencies: + '@cbor-extract/cbor-extract-darwin-arm64': 2.2.2 + '@cbor-extract/cbor-extract-darwin-x64': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm64': 2.2.2 + '@cbor-extract/cbor-extract-linux-x64': 2.2.2 + '@cbor-extract/cbor-extract-win32-x64': 2.2.2 + optional: true + + cbor-x@1.6.6: + optionalDependencies: + cbor-extract: 2.2.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + detect-libc@2.1.2: + optional: true + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + eventemitter3@5.0.4: {} + + fast-sha256@1.3.0: {} + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + isomorphic-ws@5.0.0(ws@8.21.3): + dependencies: + ws: 8.21.3 + + ms@2.1.3: {} + + node-gyp-build-optional-packages@5.1.1: + dependencies: + detect-libc: 2.1.2 + optional: true + + playwright-core@1.62.1: {} + + playwright@1.62.1: + dependencies: + playwright-core: 1.62.1 + optionalDependencies: + fsevents: 2.3.2 + + tsx@4.23.13: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + + typescript@6.0.3: {} + + undici-types@7.24.6: {} + + undici-types@8.3.0: {} + + uuid@14.0.2: {} + + ws@8.21.3: {} + + xstate@5.32.6: {} diff --git a/examples/cli-demo/pnpm-workspace.yaml b/examples/cli-demo/pnpm-workspace.yaml new file mode 100644 index 00000000..087ce062 --- /dev/null +++ b/examples/cli-demo/pnpm-workspace.yaml @@ -0,0 +1,7 @@ +allowBuilds: + cbor-extract: false + '@automerge/automerge-repo@git+https://github.com/automerge/automerge-repo.git': true + esbuild: true +overrides: + "@automerge/automerge": 3.3.2 + "@automerge/automerge-subduction": 0.16.1 diff --git a/examples/cli-demo/src/cli.ts b/examples/cli-demo/src/cli.ts new file mode 100644 index 00000000..4c8add9a --- /dev/null +++ b/examples/cli-demo/src/cli.ts @@ -0,0 +1,23 @@ +#!/usr/bin/env node +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { main } from "./cli/main.js" + +let exitCode: number +try { + exitCode = await main(process.argv.slice(2)) +} catch (error) { + console.error(error instanceof Error ? error.message : String(error)) + exitCode = 1 +} + +await Promise.all([drain(process.stdout), drain(process.stderr)]) +process.exit(exitCode) + +function drain(stream: NodeJS.WritableStream): Promise { + return new Promise((resolve, reject) => { + stream.write("", error => error ? reject(error) : resolve()) + }) +} diff --git a/examples/cli-demo/src/cli/arguments.ts b/examples/cli-demo/src/cli/arguments.ts new file mode 100644 index 00000000..e58281f1 --- /dev/null +++ b/examples/cli-demo/src/cli/arguments.ts @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +export type Command = "ir" | "query" | "exec" + +export type ParsedArguments = + | { kind: "help"; command?: Command } + | { + kind: "command" + command: Command + documentUrl: string + source?: string + verbose: boolean + } + +export function parseArguments(arguments_: string[]): ParsedArguments { + const positional: string[] = [] + let verbose = false + let help = false + let optionsEnded = false + + for (const argument of arguments_) { + if (!optionsEnded && argument === "--") optionsEnded = true + else if (!optionsEnded && (argument === "-v" || argument === "--verbose")) verbose = true + else if (!optionsEnded && (argument === "-h" || argument === "--help")) help = true + else positional.push(argument) + } + + if (positional.length === 0 || positional[0] === "help") { + const command = positional[1] + if (command !== undefined && !isCommand(command)) { + throw new Error(`Unknown command: ${command}`) + } + return { kind: "help", command } + } + + const [documentUrl, command, source, ...extra] = positional + if (!isCommand(command)) { + throw new Error(command === undefined ? "Missing command" : `Unknown command: ${command}`) + } + if (help) return { kind: "help", command } + if (extra.length > 0) throw new Error(`Unexpected argument: ${extra[0]}`) + if (command !== "ir" && source === undefined) { + throw new Error(`${command} requires JavaScript or - for stdin`) + } + if (command === "ir" && source !== undefined) throw new Error(`Unexpected argument: ${source}`) + + return { kind: "command", command, documentUrl, source, verbose } +} + +function isCommand(value: string | undefined): value is Command { + return value === "ir" || value === "query" || value === "exec" +} diff --git a/examples/cli-demo/src/cli/commands/exec.ts b/examples/cli-demo/src/cli/commands/exec.ts new file mode 100644 index 00000000..9bd6c86e --- /dev/null +++ b/examples/cli-demo/src/cli/commands/exec.ts @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { evaluateExec } from "../evaluation.js" +import type { ColnHandle } from "@coln-project/repo" + +export function runExec(handle: ColnHandle, source: string): void { + handle.change(transaction => evaluateExec(transaction, source)) +} diff --git a/examples/cli-demo/src/cli/commands/ir.ts b/examples/cli-demo/src/cli/commands/ir.ts new file mode 100644 index 00000000..1720e174 --- /dev/null +++ b/examples/cli-demo/src/cli/commands/ir.ts @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { ColnHandle } from "@coln-project/repo" + +export function runIr(handle: ColnHandle): void { + console.log(JSON.stringify(JSON.parse(handle.doc().jsonIR()), null, 2)) +} diff --git a/examples/cli-demo/src/cli/commands/query.ts b/examples/cli-demo/src/cli/commands/query.ts new file mode 100644 index 00000000..56b5c71e --- /dev/null +++ b/examples/cli-demo/src/cli/commands/query.ts @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { evaluateQuery } from "../evaluation.js" +import type { ColnHandle } from "@coln-project/repo" + +export function runQuery(handle: ColnHandle, source: string): void { + const result = evaluateQuery(handle.doc(), source) + const json = JSON.stringify(result, null, 2) + if (json === undefined) throw new TypeError("Query result is not JSON-serializable") + console.log(json) +} diff --git a/examples/cli-demo/src/cli/evaluation.ts b/examples/cli-demo/src/cli/evaluation.ts new file mode 100644 index 00000000..3bf46002 --- /dev/null +++ b/examples/cli-demo/src/cli/evaluation.ts @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { ColnDocument, ColnTransaction } from "@coln-project/repo" + +export function evaluateQuery(store: ColnDocument, source: string): unknown { + const evaluate = new Function("store", `"use strict"; return (${source}\n)`) as ( + store: ColnDocument, + ) => unknown + const result = evaluate(store) + assertSynchronous(result) + return result +} + +export function evaluateExec( + transaction: ColnTransaction, + source: string, +): void { + const evaluate = new Function("txn", `"use strict";\n${source}`) as ( + transaction: ColnTransaction, + ) => unknown + assertSynchronous(evaluate(transaction)) +} + +function assertSynchronous(value: unknown): void { + if ( + (typeof value === "object" || typeof value === "function") + && value !== null + && "then" in value + && typeof value.then === "function" + ) { + throw new TypeError("JavaScript must complete synchronously") + } +} diff --git a/examples/cli-demo/src/cli/help.ts b/examples/cli-demo/src/cli/help.ts new file mode 100644 index 00000000..a9a2d1a1 --- /dev/null +++ b/examples/cli-demo/src/cli/help.ts @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { Command } from "./arguments.js" + +const overview = `coln-repo reads and updates synchronized Coln documents. + +A Coln theory describes the entities and rules in a document. The compiler lowers +that theory to JSON IR. The runtime Store uses IR table paths and tagged values; +use "ir" first to discover those paths and column types. + +Usage: + coln-repo ir [-v] + coln-repo query [-v] + coln-repo exec [-v] + coln-repo help [command] + +Commands: + ir Print compiled IR as formatted JSON + query Evaluate a read-only expression and print its result as JSON + exec Run a synchronous script in one change transaction + +Options: + -v, --verbose Print connection and sync progress to stderr + -h, --help Print command help + +Environment: + SUBDUCTION_ENDPOINT Sync server WebSocket URL + +JavaScript is trusted code and runs with this Node process's capabilities.` + +const commandHelp: Record = { + ir: `Usage: coln-repo ir [-v] + +Print the document's compiled Coln IR as formatted JSON.`, + query: `Usage: coln-repo query [-v] + +Evaluate a synchronous JavaScript expression with a read-only store in scope. +The result is JSON-encoded on stdout. Use - to read the expression from stdin. + +Store methods: + store.jsonIR() Return the IR JSON string + store.scanTable(path) Return all rows in an IR table + store.rowById(path, rowRef) Return one row, or undefined + store.heads() Return current commit hashes + +Example: + coln-repo automerge:... query 'store.scanTable("GraphRealm.V")'`, + exec: `Usage: coln-repo exec [-v] + +Run a synchronous JavaScript script in one atomic Coln Repo change. The script +receives an add-only transaction named txn. Use - to read the script from stdin. +Successful execution writes no stdout. + +Available operations: + txn.add(path, values) Add a row and return its tagged row reference + +Example: + coln-repo automerge:... exec ' + const a = txn.add("GraphRealm.V", []) + const b = txn.add("GraphRealm.V", []) + txn.add("GraphRealm.E", [a, b]) + ' + +The change is committed only if the script returns synchronously without error. +The CLI waits for the connected server to flush it before exiting successfully.`, +} + +export function helpText(command?: Command): string { + return command === undefined ? overview : commandHelp[command] +} diff --git a/examples/cli-demo/src/cli/main.ts b/examples/cli-demo/src/cli/main.ts new file mode 100644 index 00000000..573c88d0 --- /dev/null +++ b/examples/cli-demo/src/cli/main.ts @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { parseArguments } from "./arguments.js" +import { runExec } from "./commands/exec.js" +import { runIr } from "./commands/ir.js" +import { runQuery } from "./commands/query.js" +import { helpText } from "./help.js" +import { defaultEndpoint, flush, openDocument } from "./repository.js" + +export async function main(arguments_: string[]): Promise { + const argumentsResult = parseArguments(arguments_) + if (argumentsResult.kind === "help") { + console.log(helpText(argumentsResult.command)) + return 0 + } + + const endpoint = process.env.SUBDUCTION_ENDPOINT ?? defaultEndpoint + const source = argumentsResult.source === "-" + ? await readStdin() + : argumentsResult.source + const { repo, handle } = await openDocument( + argumentsResult.documentUrl, + endpoint, + argumentsResult.verbose, + ) + + try { + switch (argumentsResult.command) { + case "ir": + runIr(handle) + break + case "query": + runQuery(handle, source!) + break + case "exec": + runExec(handle, source!) + await flush(repo, endpoint, argumentsResult.verbose) + break + } + return 0 + } finally { + await repo.shutdown() + } +} + +async function readStdin(): Promise { + process.stdin.setEncoding("utf8") + let source = "" + for await (const chunk of process.stdin) source += chunk + return source +} diff --git a/examples/cli-demo/src/cli/repository.ts b/examples/cli-demo/src/cli/repository.ts new file mode 100644 index 00000000..6b852cb3 --- /dev/null +++ b/examples/cli-demo/src/cli/repository.ts @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { + initSubduction, + isValidAutomergeUrl, + Repo, + type AutomergeUrl, +} from "@automerge/automerge-repo" +import { find, type ColnHandle } from "@coln-project/repo" + +export const defaultEndpoint = "wss://subduction.sync.inkandswitch.com" +export const operationTimeoutMs = 10_000 + +export interface OpenDocument { + repo: Repo + handle: ColnHandle +} + +export async function openDocument( + documentUrl: string, + endpoint: string, + verbose: boolean, +): Promise { + if (!isValidAutomergeUrl(documentUrl)) throw new Error(`Invalid Automerge URL: ${documentUrl}`) + validateEndpoint(endpoint) + if (verbose) console.error(`Connecting to ${endpoint}`) + + await initSubduction() + const repo = new Repo({ subductionWebsocketEndpoints: [endpoint] }) + try { + await withTimeout(waitForConnection(repo), `Timed out connecting to ${endpoint}`) + if (verbose) console.error(`Connected to ${endpoint}`) + const handle = await withTimeout( + find(repo, documentUrl as AutomergeUrl), + `Timed out loading ${documentUrl}`, + ) + return { repo, handle } + } catch (error) { + await repo.shutdown() + throw error + } +} + +export async function flush(repo: Repo, endpoint: string, verbose: boolean): Promise { + if (!repo.isSubductionConnected()) throw new Error(`Disconnected from ${endpoint} before sync`) + await withTimeout(repo.flush(), `Timed out syncing to ${endpoint}`) + if (verbose) console.error(`Synced to ${endpoint}`) +} + +function waitForConnection(repo: Repo): Promise { + if (repo.isSubductionConnected()) return Promise.resolve() + return new Promise(resolve => { + const connected = ({ connected: isConnected }: { connected: boolean }) => { + if (!isConnected) return + repo.off("subduction-connection", connected) + resolve() + } + repo.on("subduction-connection", connected) + if (repo.isSubductionConnected()) { + repo.off("subduction-connection", connected) + resolve() + } + }) +} + +async function withTimeout(promise: Promise, message: string): Promise { + let timeout: ReturnType | undefined + try { + return await Promise.race([ + promise, + new Promise((_resolve, reject) => { + timeout = setTimeout(() => reject(new Error(message)), operationTimeoutMs) + }), + ]) + } finally { + if (timeout !== undefined) clearTimeout(timeout) + } +} + +function validateEndpoint(endpoint: string): void { + let url: URL + try { + url = new URL(endpoint) + } catch { + throw new Error(`Invalid Subduction endpoint: ${endpoint}`) + } + if (url.protocol !== "ws:" && url.protocol !== "wss:") { + throw new Error(`Subduction endpoint must use ws: or wss: ${endpoint}`) + } +} diff --git a/examples/cli-demo/tests/cli/arguments.test.ts b/examples/cli-demo/tests/cli/arguments.test.ts new file mode 100644 index 00000000..e9b567e4 --- /dev/null +++ b/examples/cli-demo/tests/cli/arguments.test.ts @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import assert from "node:assert/strict" +import test from "node:test" +import { parseArguments } from "../../src/cli/arguments.js" + +test("parses commands and flags in any position", () => { + assert.deepEqual(parseArguments(["-v", "automerge:abc", "query", "1 + 1"]), { + kind: "command", + command: "query", + documentUrl: "automerge:abc", + source: "1 + 1", + verbose: true, + }) +}) + +test("parses command help", () => { + assert.deepEqual(parseArguments(["automerge:abc", "exec", "--help"]), { + kind: "help", + command: "exec", + }) + assert.deepEqual(parseArguments(["help", "query"]), { + kind: "help", + command: "query", + }) +}) + +test("double dash allows JavaScript matching an option", () => { + assert.deepEqual(parseArguments(["automerge:abc", "query", "--", "--verbose"]), { + kind: "command", + command: "query", + documentUrl: "automerge:abc", + source: "--verbose", + verbose: false, + }) +}) + +test("requires JavaScript for query and exec", () => { + assert.throws(() => parseArguments(["automerge:abc", "exec"]), /requires JavaScript/) +}) + +test("rejects unknown commands and extra arguments", () => { + assert.throws(() => parseArguments(["automerge:abc", "nope"]), /Unknown command/) + assert.throws( + () => parseArguments(["automerge:abc", "query", "1", "2"]), + /Unexpected argument/, + ) +}) diff --git a/examples/cli-demo/tests/cli/evaluation.test.ts b/examples/cli-demo/tests/cli/evaluation.test.ts new file mode 100644 index 00000000..ad724954 --- /dev/null +++ b/examples/cli-demo/tests/cli/evaluation.test.ts @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import assert from "node:assert/strict" +import test from "node:test" +import type { ColnDocument, ColnTransaction } from "@coln-project/repo" +import { evaluateExec, evaluateQuery } from "../../src/cli/evaluation.js" + +test("query evaluates against a read-only store", () => { + const document = { + heads: () => [], + jsonIR: () => "{}", + rowById: () => undefined, + scanTable: () => [{ values: [{ tag: "string", value: "one" }] }], + } as unknown as ColnDocument + + const result = evaluateQuery( + document, + `({ keys: Object.keys(store).sort(), values: store.scanTable("Test.Items").map(row => row.values[0].value) })`, + ) + + assert.deepEqual(result, { + keys: ["heads", "jsonIR", "rowById", "scanTable"], + values: ["one"], + }) +}) + +test("exec writes through its transaction", () => { + const values: string[] = [] + const transaction = { + add: (_path: string, rows: { value: string }[]) => values.push(rows[0].value), + } as unknown as ColnTransaction + + evaluateExec(transaction, `txn.add("Test.Items", [{ tag: "string", value: "one" }])`) + assert.deepEqual(values, ["one"]) +}) + +test("exec receives the current transaction API", () => { + const transaction = { add: () => undefined } as ColnTransaction + + evaluateExec(transaction, ` + if (JSON.stringify(Object.keys(txn)) !== '["add"]') throw new Error("unexpected API") + txn.add("Test.Items", [{ tag: "string", value: "one" }]) + `) +}) + +test("query and exec reject promises", () => { + const document = { + heads: () => [], + jsonIR: () => "{}", + rowById: () => undefined, + scanTable: () => [], + } as ColnDocument + assert.throws(() => evaluateQuery(document, "Promise.resolve(1)"), /synchronously/) + + const transaction = { add: () => undefined } as ColnTransaction + assert.throws( + () => evaluateExec(transaction, "return Promise.resolve()"), + /synchronously/, + ) +}) diff --git a/examples/cli-demo/tests/server.mjs b/examples/cli-demo/tests/server.mjs new file mode 100644 index 00000000..55bbe053 --- /dev/null +++ b/examples/cli-demo/tests/server.mjs @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { WebSocketServer } from "ws" +import { + MemorySigner, + MemoryStorage, + Subduction, +} from "@automerge/automerge-subduction" + +class WebSocketTransport { + #socket + #queue = [] + #waiters = [] + #errorWaiters = [] + #closed = false + #closedPromise + #resolveClosed + #disconnectCallback = null + + constructor(socket) { + this.#socket = socket + this.#closedPromise = new Promise(resolve => { + this.#resolveClosed = resolve + }) + + socket.on("message", data => { + const bytes = toUint8Array(data) + const waiter = this.#waiters.shift() + if (waiter) { + this.#errorWaiters.shift() + waiter(bytes) + } else { + this.#queue.push(bytes) + } + }) + socket.on("close", () => this.#markClosed(new Error("WebSocket closed"))) + socket.on("error", error => this.#markClosed(error)) + } + + onDisconnect(callback) { + this.#disconnectCallback = callback + } + + async sendBytes(bytes) { + if (this.#closed) throw new Error("WebSocket closed") + await new Promise((resolve, reject) => { + this.#socket.send(Buffer.from(bytes), error => (error ? reject(error) : resolve())) + }) + } + + recvBytes() { + const queued = this.#queue.shift() + if (queued) return Promise.resolve(queued) + if (this.#closed) return Promise.reject(new Error("WebSocket closed")) + return new Promise((resolve, reject) => { + this.#waiters.push(resolve) + this.#errorWaiters.push(reject) + }) + } + + async disconnect() { + this.#markClosed(new Error("WebSocket closed"), false) + this.#socket.close() + } + + closed() { + return this.#closedPromise + } + + #markClosed(error, notify = true) { + if (this.#closed) return + this.#closed = true + this.#resolveClosed() + for (const reject of this.#errorWaiters) reject(error) + this.#waiters = [] + this.#errorWaiters = [] + if (notify) this.#disconnectCallback?.() + } +} + +function toUint8Array(data) { + if (data instanceof Uint8Array) { + return new Uint8Array(data.buffer, data.byteOffset, data.byteLength) + } + if (data instanceof ArrayBuffer) return new Uint8Array(data) + if (Array.isArray(data)) return toUint8Array(Buffer.concat(data)) + return new Uint8Array(Buffer.from(data)) +} + +const port = 3031 +const serviceName = `127.0.0.1:${port}` +const subduction = new Subduction({ + signer: new MemorySigner(), + storage: new MemoryStorage(), +}) +const server = new WebSocketServer({ port }) + +server.on("connection", socket => { + subduction.acceptTransport(new WebSocketTransport(socket), serviceName).catch(error => { + if (!String(error?.message ?? error).includes("closed")) console.error(error) + }) +}) + +async function shutdown() { + await subduction.disconnectAll().catch(() => {}) + server.close(() => process.exit(0)) +} + +process.on("SIGINT", shutdown) +process.on("SIGTERM", shutdown) diff --git a/examples/cli-demo/tests/sync.spec.ts b/examples/cli-demo/tests/sync.spec.ts new file mode 100644 index 00000000..9916dd76 --- /dev/null +++ b/examples/cli-demo/tests/sync.spec.ts @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { expect, test } from "@playwright/test" +import { execFile } from "node:child_process" +import { promisify } from "node:util" +import { initSubduction, Repo } from "@automerge/automerge-repo" +import { colnDocType, type ColnSchema, type ColnTransaction } from "@coln-project/repo" + +const execFileAsync = promisify(execFile) +const table = "Test.Items" +const schema = { + entities: [{ + path: [["Test"], ["Items"]], + value: { + entityVariant: { tag: "table" }, + columns: [{ + path: [["value"]], + type: { tag: "builtin", type: "builtinString" }, + }], + primaryKey: null, + }, + }], + rules: [], +} satisfies ColnSchema + +test("CLI exec flushes its change before exiting", async () => { + await initSubduction() + const endpoint = "ws://127.0.0.1:3031" + const creator = new Repo({ subductionWebsocketEndpoints: [endpoint] }) + const verifier = new Repo({ subductionWebsocketEndpoints: [endpoint] }) + + try { + const handle = creator.create(schema, colnDocType) + handle.change((transaction: ColnTransaction) => { + transaction.add(table, [{ tag: "string", value: "before" }]) + }) + await creator.flush() + + const { stdout, stderr } = await execFileAsync( + process.execPath, + [ + "dist/cli.js", + handle.url, + "exec", + `txn.add("${table}", [{ tag: "string", value: "from-cli" }])`, + "--verbose", + ], + { + cwd: process.cwd(), + env: { ...process.env, SUBDUCTION_ENDPOINT: endpoint }, + timeout: 15_000, + }, + ) + + expect(stdout).toBe("") + expect(stderr).toContain(`Connecting to ${endpoint}`) + expect(stderr).toContain(`Synced to ${endpoint}`) + + const verified = await verifier.find(handle.url, colnDocType) + await expect.poll(() => + verified.doc().scanTable(table) + .map(row => row.values[0]) + .filter(value => value?.tag === "string") + .map(value => value.value) + .sort(), + ).toEqual(["before", "from-cli"]) + } finally { + await Promise.all([creator.shutdown(), verifier.shutdown()]) + } +}) diff --git a/examples/cli-demo/tsconfig.build.json b/examples/cli-demo/tsconfig.build.json new file mode 100644 index 00000000..796337f7 --- /dev/null +++ b/examples/cli-demo/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*.ts"] +} diff --git a/examples/cli-demo/tsconfig.json b/examples/cli-demo/tsconfig.json new file mode 100644 index 00000000..2dbc108f --- /dev/null +++ b/examples/cli-demo/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["src/**/*.ts", "tests/**/*.ts", "playwright.config.ts"] +} diff --git a/examples/sync/.gitignore b/examples/sync/.gitignore new file mode 100644 index 00000000..8166d595 --- /dev/null +++ b/examples/sync/.gitignore @@ -0,0 +1,4 @@ +dist/ +node_modules/ +test-results/ +playwright-report/ diff --git a/examples/sync/README.md b/examples/sync/README.md new file mode 100644 index 00000000..2e67ebee --- /dev/null +++ b/examples/sync/README.md @@ -0,0 +1,96 @@ +# Coln graph sync + +This example edits one fixed Coln graph schema from a Svelte browser client or +an Ink terminal client. Both clients create or load the same Automerge document +and synchronize it through the bundled local Subduction relay. + +The graph is directed and supports self-loops and parallel edges. + +## Prerequisites + +Build the local runtime and repo packages from the repository root: + +```bash +npm ci --prefix packages/coln-js-runtime +npm run --prefix packages/coln-js-runtime build +pnpm --dir packages/coln-repo install +pnpm --dir packages/coln-repo build +pnpm --dir examples/sync install +``` + +## Browser + +```bash +pnpm --dir examples/sync dev +``` + +Open the Vite URL. A URL without an Automerge document in its hash creates a +new document. Use the document URL shown in the inspector to open the same graph +in another browser or the CLI. + +## CLI + +Load an existing document: + +```bash +pnpm --dir examples/sync cli +``` + +Omit the URL to create a document in the CLI. Keep the CLI open while opening +its displayed URL in the browser because the bundled relay stores documents in +memory. + +The CLI controls are: + +- `v`: add vertex +- `e`: add edge using the selected source and target +- `Tab`: switch the active endpoint +- `Up`/`Down`: select a vertex for the active endpoint +- `/`: enter a TypeScript query callback body +- `q`: quit + +## TypeScript queries + +The query prompt accepts the body of a synchronous `handle.change` callback. +TypeScript syntax is transpiled but not semantically type-checked before it +runs; invalid transaction operations are reported as runtime errors. +For example: + +```ts +tx.root.V.add() +``` + +Or using the lower-level transaction API: + +```ts +tx.add("GraphRealm.V", []) +``` + +Queries execute as trusted local code and have the same access to the process as +other dynamically evaluated JavaScript. Do not run untrusted input. + +Query compilation and execution live in `src/query/execute-query.ts`. That +module intentionally has no graph, Ink, Svelte, or synchronization imports. Its +public surface only depends on a target with a `change` method, allowing it to be +moved into a future CLI package and replaced here with a package import. + +## Schema + +The fixed schema is `graph.coln`. Regenerate its TypeScript bindings and JSON IR +with: + +```bash +pnpm --dir examples/sync compile +``` + +## Verification + +```bash +pnpm --dir examples/sync build +pnpm --dir examples/sync exec playwright install chromium # first time only +pnpm --dir examples/sync test:e2e +``` + +The end-to-end scenario exercises browser mutations, Node-side TypeScript query +execution, synchronization in both directions, and loading the resulting graph +in a second browser. diff --git a/examples/sync/compile.sh b/examples/sync/compile.sh new file mode 100644 index 00000000..42c305c6 --- /dev/null +++ b/examples/sync/compile.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2026 Coln contributors +# SPDX-License-Identifier: Apache-2.0 OR MIT + +set -euo pipefail + +if command -v coln >/dev/null 2>&1; then + coln_cli=(coln) +elif command -v coln-cli >/dev/null 2>&1; then + coln_cli=(coln-cli) +else + coln_cli=(cabal run coln-cli --) +fi + +"${coln_cli[@]}" generate-ts ./graph.coln --output-dir ./src/generated +"${coln_cli[@]}" generate-ir ./graph.coln --output-dir ./src/generated diff --git a/examples/sync/graph.coln b/examples/sync/graph.coln new file mode 100644 index 00000000..04a0be4c --- /dev/null +++ b/examples/sync/graph.coln @@ -0,0 +1,7 @@ +theory Graph := sig + V : Set + E : V -> V -> Set +end + +realm GraphRealm @ Graph +end diff --git a/examples/sync/index.html b/examples/sync/index.html new file mode 100644 index 00000000..57e78f4d --- /dev/null +++ b/examples/sync/index.html @@ -0,0 +1,15 @@ + + + + + + + + + Coln Graph Sync + + +
+ + + diff --git a/examples/sync/package.json b/examples/sync/package.json new file mode 100644 index 00000000..7e88209a --- /dev/null +++ b/examples/sync/package.json @@ -0,0 +1,42 @@ +{ + "name": "coln-sync", + "private": true, + "type": "module", + "license": "(Apache-2.0 OR MIT)", + "scripts": { + "dev": "node server/with-server.mjs vite --host 127.0.0.1", + "cli": "node server/with-server.mjs tsx src/cli/main.tsx", + "server": "node server/server.mjs", + "compile": "bash ./compile.sh", + "check": "svelte-check --tsconfig ./tsconfig.json && tsc --noEmit", + "build": "pnpm run compile && pnpm run check && vite build", + "test": "vitest run", + "test:e2e": "playwright test" + }, + "dependencies": { + "@automerge/automerge-repo": "github:automerge/automerge-repo#doctypes&path:packages/automerge-repo", + "@automerge/automerge-subduction": "0.16.1", + "@coln-project/repo": "link:../../packages/coln-repo", + "@coln-project/runtime": "link:../../packages/coln-js-runtime", + "ink": "^6.0.0", + "ink-text-input": "^6.0.0", + "react": "^19.0.0", + "svelte": "^5.7.0", + "typescript": "^6.0.0", + "ws": "^8.21.0" + }, + "devDependencies": { + "@playwright/test": "^1.61.0", + "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@tailwindcss/vite": "^4.0.0", + "@types/node": "^26.0.0", + "@types/react": "^19.0.0", + "@vitest/browser-playwright": "^4.1.11", + "svelte-check": "^4.0.0", + "tailwindcss": "^4.0.0", + "tsx": "^4.23.0", + "vite": "^8.0.0", + "vite-plugin-wasm": "^3.6.0", + "vitest": "^4.1.11" + } +} diff --git a/examples/sync/playwright.config.ts b/examples/sync/playwright.config.ts new file mode 100644 index 00000000..e28cf2ab --- /dev/null +++ b/examples/sync/playwright.config.ts @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { defineConfig, devices } from "@playwright/test" + +export default defineConfig({ + testDir: "./tests", + timeout: 30_000, + expect: { timeout: 10_000 }, + fullyParallel: false, + retries: 0, + use: { + baseURL: "http://127.0.0.1:5176", + trace: "retain-on-failure", + }, + webServer: [ + { + command: "SUBDUCTION_SERVICE_NAME=127.0.0.1:3031 PORT=3031 node server/server.mjs", + port: 3031, + reuseExistingServer: false, + stdout: "pipe", + stderr: "pipe", + }, + { + command: "VITE_SUBDUCTION_ENDPOINT=ws://127.0.0.1:3031 ./node_modules/.bin/vite --host 127.0.0.1 --port 5176", + url: "http://127.0.0.1:5176", + reuseExistingServer: false, + stdout: "pipe", + stderr: "pipe", + }, + ], + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], +}) diff --git a/examples/sync/pnpm-lock.yaml b/examples/sync/pnpm-lock.yaml new file mode 100644 index 00000000..3e56fe5a --- /dev/null +++ b/examples/sync/pnpm-lock.yaml @@ -0,0 +1,2290 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-subduction': 0.16.1 + +importers: + + .: + dependencies: + '@automerge/automerge-repo': + specifier: github:automerge/automerge-repo#doctypes&path:packages/automerge-repo + version: https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo + '@automerge/automerge-subduction': + specifier: 0.16.1 + version: 0.16.1 + '@coln-project/repo': + specifier: link:../../packages/coln-repo + version: link:../../packages/coln-repo + '@coln-project/runtime': + specifier: link:../../packages/coln-js-runtime + version: link:../../packages/coln-js-runtime + ink: + specifier: ^6.0.0 + version: 6.8.0(@types/react@19.2.18)(react@19.2.8) + ink-text-input: + specifier: ^6.0.0 + version: 6.0.0(ink@6.8.0(@types/react@19.2.18)(react@19.2.8))(react@19.2.8) + react: + specifier: ^19.0.0 + version: 19.2.8 + svelte: + specifier: ^5.7.0 + version: 5.56.10 + typescript: + specifier: ^6.0.0 + version: 6.0.3 + ws: + specifier: ^8.21.0 + version: 8.21.3 + devDependencies: + '@playwright/test': + specifier: ^1.61.0 + version: 1.62.1 + '@sveltejs/vite-plugin-svelte': + specifier: ^7.0.0 + version: 7.3.0(svelte@5.56.10)(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)) + '@tailwindcss/vite': + specifier: ^4.0.0 + version: 4.3.3(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)) + '@types/node': + specifier: ^26.0.0 + version: 26.3.0 + '@types/react': + specifier: ^19.0.0 + version: 19.2.18 + '@vitest/browser-playwright': + specifier: ^4.1.11 + version: 4.1.11(playwright@1.62.1)(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12))(vitest@4.1.11) + svelte-check: + specifier: ^4.0.0 + version: 4.7.6(picomatch@4.0.7)(svelte@5.56.10)(typescript@6.0.3) + tailwindcss: + specifier: ^4.0.0 + version: 4.3.3 + tsx: + specifier: ^4.23.0 + version: 4.23.12 + vite: + specifier: ^8.0.0 + version: 8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12) + vite-plugin-wasm: + specifier: ^3.6.0 + version: 3.6.0(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)) + vitest: + specifier: ^4.1.11 + version: 4.1.11(@types/node@26.3.0)(@vitest/browser-playwright@4.1.11)(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)) + +packages: + + '@alcalzone/ansi-tokenize@0.2.5': + resolution: {integrity: sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw==} + engines: {node: '>=18'} + + '@automerge/automerge-repo@https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo': + resolution: {gitHosted: true, path: packages/automerge-repo, tarball: https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5} + version: 2.6.0-subduction.47 + engines: {node: '>=22.13'} + + '@automerge/automerge-subduction@0.16.1': + resolution: {integrity: sha512-alH7U4eYn0O7sT6hNLv7CJhNODJi+QD6qfRQTNfZCxL3Q5JZdJ8lFpdvXUkBf+I3bu0GLvVlTaYPWGrBihjpIw==} + + '@automerge/automerge@3.3.2': + resolution: {integrity: sha512-9vCdCL7pdQwUra66SBxPVHr+/t9epXKni9KDeak2rNBFMzABVh2u6gSpcwxi3jkR5qr047jVqZv9DlrOfVxLFw==} + + '@blazediff/core@1.9.1': + resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} + cpu: [arm64] + os: [darwin] + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + resolution: {integrity: sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==} + cpu: [x64] + os: [darwin] + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + resolution: {integrity: sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==} + cpu: [arm64] + os: [linux] + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + resolution: {integrity: sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==} + cpu: [arm] + os: [linux] + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + resolution: {integrity: sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==} + cpu: [x64] + os: [linux] + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + resolution: {integrity: sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==} + cpu: [x64] + os: [win32] + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@oxc-project/types@0.146.0': + resolution: {integrity: sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==} + + '@playwright/test@1.62.1': + resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} + engines: {node: '>=20'} + hasBin: true + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@rolldown/binding-android-arm-eabi@1.2.5': + resolution: {integrity: sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.5': + resolution: {integrity: sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.5': + resolution: {integrity: sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.5': + resolution: {integrity: sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.5': + resolution: {integrity: sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.5': + resolution: {integrity: sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.5': + resolution: {integrity: sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.5': + resolution: {integrity: sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.5': + resolution: {integrity: sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.5': + resolution: {integrity: sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.5': + resolution: {integrity: sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.5': + resolution: {integrity: sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.5': + resolution: {integrity: sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.5': + resolution: {integrity: sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.5': + resolution: {integrity: sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@sveltejs/acorn-typescript@1.0.13': + resolution: {integrity: sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/load-config@0.2.3': + resolution: {integrity: sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ==} + engines: {node: '>= 18.0.0'} + + '@sveltejs/vite-plugin-svelte@7.3.0': + resolution: {integrity: sha512-QbRoJyD92e9R0ufeQIWRHrCC0ObcqSv/aBDdrQMoU+sypav3cDx5wytdQ6GLdXjEMO6xjrXGzfkUygng8JMv0A==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.46.4 + vite: ^8.0.0-beta.7 || ^8.0.0 + + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} + + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@26.3.0': + resolution: {integrity: sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw==} + + '@types/react@19.2.18': + resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@vitest/browser-playwright@4.1.11': + resolution: {integrity: sha512-riLBxPqwnJ0lWs2DN2WeUfYeKLoAjbP2Xx8cLQdSddzMi20sksIa6K2mPz79DyMZKKVKH2ksOC2yJvtNcZg8cg==} + peerDependencies: + playwright: '*' + vitest: 4.1.11 + + '@vitest/browser@4.1.11': + resolution: {integrity: sha512-bwMovvAeuTFOK5kIFevw4VEf+1gVEICv4SYK4k3knJOxl6b1zEWud8mYKD73e1B0odAn174h1MofURy2TPWf3w==} + peerDependencies: + vitest: 4.1.11 + + '@vitest/expect@4.1.11': + resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} + + '@vitest/mocker@4.1.11': + resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.11': + resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} + + '@vitest/runner@4.1.11': + resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} + + '@vitest/snapshot@4.1.11': + resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} + + '@vitest/spy@4.1.11': + resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} + + '@vitest/utils@4.1.11': + resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + + ansi-regex@6.3.0: + resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} + engines: {node: '>=12'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + auto-bind@5.0.1: + resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + + bs58check@4.0.0: + resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} + + cbor-extract@2.2.2: + resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} + hasBin: true + + cbor-x@1.6.6: + resolution: {integrity: sha512-8QiD9PGOxyQHo7s2pzwTBH6lTjqekxPdl9Aq6fXvZgCuCJHOht1puDEA/fTr6mciB76c+M+Gi0qT2i1a4pm4Wg==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + + cli-cursor@4.0.0: + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} + engines: {node: '>=20'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + code-excerpt@4.0.0: + resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + convert-to-spaces@2.0.1: + resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devalue@5.9.1: + resolution: {integrity: sha512-+17vil3EVQRzvtDJSFuTWEb8XJRvXqAiV3qZyQWD398QeXUa6CxsUyMdD1fxzEhUrd4FojitFz7lhIHBTlV4fw==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} + engines: {node: '>=10.13.0'} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + es-toolkit@1.51.0: + resolution: {integrity: sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw==} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + esrap@2.3.6: + resolution: {integrity: sha512-yc0OC12UjPqLoc+fe+v5GNs4TOjAigUw3sTikfC+xeBPGUw7gDRz3DtYaqEhxyMVJojcSWJw7jT0QWR+CbuE/A==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + + ink-text-input@6.0.0: + resolution: {integrity: sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw==} + engines: {node: '>=18'} + peerDependencies: + ink: '>=5' + react: '>=18' + + ink@6.8.0: + resolution: {integrity: sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA==} + engines: {node: '>=20'} + peerDependencies: + '@types/react': '>=19.0.0' + react: '>=19.0.0' + react-devtools-core: '>=6.1.2' + peerDependenciesMeta: + '@types/react': + optional: true + react-devtools-core: + optional: true + + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + + is-in-ci@2.0.0: + resolution: {integrity: sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==} + engines: {node: '>=20'} + hasBin: true + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magic-string@1.2.2: + resolution: {integrity: sha512-veT/+7iXrXzT39XnEN4lOxtNl72dMgJ8Lp+5Bd6YcMSWpb0n0MjBM8Uuooi6jgJr8dhUW2swQgBmoZVMni5SVg==} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + patch-console@2.0.0: + resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + playwright-core@1.62.1: + resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.62.1: + resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} + engines: {node: '>=20'} + hasBin: true + + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + + react-reconciler@0.33.0: + resolution: {integrity: sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==} + engines: {node: '>=0.10.0'} + peerDependencies: + react: ^19.2.0 + + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} + engines: {node: '>=0.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + restore-cursor@4.0.0: + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + rolldown@1.2.5: + resolution: {integrity: sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.2: + resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==} + engines: {node: '>=20'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + svelte-check@4.7.6: + resolution: {integrity: sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: ^5.0.0 || ^6.0.0 + + svelte@5.56.10: + resolution: {integrity: sha512-Lcxbj8I/KAbpY+VjtY4ENQBV0dDCipfGAhqb51XQZ67CIQqXgsv/8dPkbILaj4Fb6/b6JAEM/PIVbILXgDQy2g==} + engines: {node: '>=18'} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + terminal-size@4.0.1: + resolution: {integrity: sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==} + engines: {node: '>=18'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tsx@4.23.12: + resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + type-fest@5.8.0: + resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} + engines: {node: '>=20'} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + uuid@14.0.2: + resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} + hasBin: true + + vite-plugin-wasm@3.6.0: + resolution: {integrity: sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==} + peerDependencies: + vite: ^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + + vite@8.2.2: + resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 || ^0.5.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + + vitest@4.1.11: + resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.11 + '@vitest/browser-preview': 4.1.11 + '@vitest/browser-webdriverio': 4.1.11 + '@vitest/coverage-istanbul': 4.1.11 + '@vitest/coverage-v8': 4.1.11 + '@vitest/ui': 4.1.11 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + widest-line@6.0.0: + resolution: {integrity: sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==} + engines: {node: '>=20'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xstate@5.32.5: + resolution: {integrity: sha512-ULazi1oe6wGrXl0Frb6otSlkm5HLifbbVTkMk5kkSKqz4TkxJaVpnl6jOJwKeid3ORPxYyZQgNLUSYX9q65SIA==} + + yoga-layout@3.2.1: + resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + +snapshots: + + '@alcalzone/ansi-tokenize@0.2.5': + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + '@automerge/automerge-repo@https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo': + dependencies: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-subduction': 0.16.1 + bs58check: 4.0.0 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + isomorphic-ws: 5.0.0(ws@8.21.3) + uuid: 14.0.2 + ws: 8.21.3 + xstate: 5.32.5 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-subduction@0.16.1': {} + + '@automerge/automerge@3.3.2': {} + + '@blazediff/core@1.9.1': {} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@noble/hashes@1.8.0': {} + + '@oxc-project/types@0.146.0': {} + + '@playwright/test@1.62.1': + dependencies: + playwright: 1.62.1 + + '@polka/url@1.0.0-next.29': {} + + '@rolldown/binding-android-arm-eabi@1.2.5': + optional: true + + '@rolldown/binding-android-arm64@1.2.5': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.5': + optional: true + + '@rolldown/binding-darwin-x64@1.2.5': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.5': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.5': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.5': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.5': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.5': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.5': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.5': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.5': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.5': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.5': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.5': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@sveltejs/acorn-typescript@1.0.13(acorn@8.18.0)': + dependencies: + acorn: 8.18.0 + + '@sveltejs/load-config@0.2.3': {} + + '@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10)(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12))': + dependencies: + deepmerge: 4.3.1 + magic-string: 1.2.2 + obug: 2.1.4 + svelte: 5.56.10 + vite: 8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12) + vitefu: 1.1.3(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)) + + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.24.5 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.3 + + '@tailwindcss/oxide-android-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide@4.3.3': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 + + '@tailwindcss/vite@4.3.3(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12))': + dependencies: + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 + vite: 8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12) + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/node@26.3.0': + dependencies: + undici-types: 8.3.0 + + '@types/react@19.2.18': + dependencies: + csstype: 3.2.3 + + '@types/trusted-types@2.0.7': {} + + '@vitest/browser-playwright@4.1.11(playwright@1.62.1)(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12))(vitest@4.1.11)': + dependencies: + '@vitest/browser': 4.1.11(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12))(vitest@4.1.11) + '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)) + playwright: 1.62.1 + tinyrainbow: 3.1.1 + vitest: 4.1.11(@types/node@26.3.0)(@vitest/browser-playwright@4.1.11)(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@4.1.11(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12))(vitest@4.1.11)': + dependencies: + '@blazediff/core': 1.9.1 + '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)) + '@vitest/utils': 4.1.11 + magic-string: 0.30.21 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.1 + vitest: 4.1.11(@types/node@26.3.0)(@vitest/browser-playwright@4.1.11)(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)) + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/expect@4.1.11': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.11(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12))': + dependencies: + '@vitest/spy': 4.1.11 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12) + + '@vitest/pretty-format@4.1.11': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.11': + dependencies: + '@vitest/utils': 4.1.11 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + '@vitest/utils': 4.1.11 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.11': {} + + '@vitest/utils@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + + acorn@8.18.0: {} + + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + + ansi-regex@6.3.0: {} + + ansi-styles@6.2.3: {} + + aria-query@5.3.1: {} + + assertion-error@2.0.1: {} + + auto-bind@5.0.1: {} + + axobject-query@4.1.0: {} + + base-x@5.0.1: {} + + bs58@6.0.0: + dependencies: + base-x: 5.0.1 + + bs58check@4.0.0: + dependencies: + '@noble/hashes': 1.8.0 + bs58: 6.0.0 + + cbor-extract@2.2.2: + dependencies: + node-gyp-build-optional-packages: 5.1.1 + optionalDependencies: + '@cbor-extract/cbor-extract-darwin-arm64': 2.2.2 + '@cbor-extract/cbor-extract-darwin-x64': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm64': 2.2.2 + '@cbor-extract/cbor-extract-linux-x64': 2.2.2 + '@cbor-extract/cbor-extract-win32-x64': 2.2.2 + optional: true + + cbor-x@1.6.6: + optionalDependencies: + cbor-extract: 2.2.2 + + chai@6.2.2: {} + + chalk@5.6.2: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + cli-boxes@3.0.0: {} + + cli-cursor@4.0.0: + dependencies: + restore-cursor: 4.0.0 + + cli-truncate@5.2.0: + dependencies: + slice-ansi: 8.0.0 + string-width: 8.2.2 + + clsx@2.1.1: {} + + code-excerpt@4.0.0: + dependencies: + convert-to-spaces: 2.0.1 + + convert-source-map@2.0.0: {} + + convert-to-spaces@2.0.1: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deepmerge@4.3.1: {} + + detect-libc@2.1.2: {} + + devalue@5.9.1: {} + + emoji-regex@10.6.0: {} + + enhanced-resolve@5.24.5: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + environment@1.1.0: {} + + es-module-lexer@2.3.2: {} + + es-toolkit@1.51.0: {} + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + escape-string-regexp@2.0.0: {} + + esm-env@1.2.2: {} + + esrap@2.3.6: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + eventemitter3@5.0.4: {} + + expect-type@1.4.0: {} + + fast-sha256@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + get-east-asian-width@1.6.0: {} + + graceful-fs@4.2.11: {} + + indent-string@5.0.0: {} + + ink-text-input@6.0.0(ink@6.8.0(@types/react@19.2.18)(react@19.2.8))(react@19.2.8): + dependencies: + chalk: 5.6.2 + ink: 6.8.0(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + type-fest: 4.41.0 + + ink@6.8.0(@types/react@19.2.18)(react@19.2.8): + dependencies: + '@alcalzone/ansi-tokenize': 0.2.5 + ansi-escapes: 7.3.0 + ansi-styles: 6.2.3 + auto-bind: 5.0.1 + chalk: 5.6.2 + cli-boxes: 3.0.0 + cli-cursor: 4.0.0 + cli-truncate: 5.2.0 + code-excerpt: 4.0.0 + es-toolkit: 1.51.0 + indent-string: 5.0.0 + is-in-ci: 2.0.0 + patch-console: 2.0.0 + react: 19.2.8 + react-reconciler: 0.33.0(react@19.2.8) + scheduler: 0.27.0 + signal-exit: 3.0.7 + slice-ansi: 8.0.0 + stack-utils: 2.0.6 + string-width: 8.2.2 + terminal-size: 4.0.1 + type-fest: 5.8.0 + widest-line: 6.0.0 + wrap-ansi: 9.0.2 + ws: 8.21.3 + yoga-layout: 3.2.1 + optionalDependencies: + '@types/react': 19.2.18 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.6.0 + + is-in-ci@2.0.0: {} + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + isomorphic-ws@5.0.0(ws@8.21.3): + dependencies: + ws: 8.21.3 + + jiti@2.7.0: {} + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + locate-character@3.0.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magic-string@1.2.2: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mimic-fn@2.1.0: {} + + mri@1.2.0: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + nanoid@3.3.18: {} + + node-gyp-build-optional-packages@5.1.1: + dependencies: + detect-libc: 2.1.2 + optional: true + + obug@2.1.4: {} + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + patch-console@2.0.0: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + playwright-core@1.62.1: {} + + playwright@1.62.1: + dependencies: + playwright-core: 1.62.1 + optionalDependencies: + fsevents: 2.3.2 + + pngjs@7.0.0: {} + + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + react-reconciler@0.33.0(react@19.2.8): + dependencies: + react: 19.2.8 + scheduler: 0.27.0 + + react@19.2.8: {} + + readdirp@4.1.2: {} + + restore-cursor@4.0.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + rolldown@1.2.5: + dependencies: + '@oxc-project/types': 0.146.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.5 + '@rolldown/binding-android-arm64': 1.2.5 + '@rolldown/binding-darwin-arm64': 1.2.5 + '@rolldown/binding-darwin-x64': 1.2.5 + '@rolldown/binding-freebsd-x64': 1.2.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.5 + '@rolldown/binding-linux-arm64-gnu': 1.2.5 + '@rolldown/binding-linux-arm64-musl': 1.2.5 + '@rolldown/binding-linux-ppc64-gnu': 1.2.5 + '@rolldown/binding-linux-s390x-gnu': 1.2.5 + '@rolldown/binding-linux-x64-gnu': 1.2.5 + '@rolldown/binding-linux-x64-musl': 1.2.5 + '@rolldown/binding-openharmony-arm64': 1.2.5 + '@rolldown/binding-win32-arm64-msvc': 1.2.5 + '@rolldown/binding-win32-x64-msvc': 1.2.5 + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + scheduler@0.27.0: {} + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + slice-ansi@8.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + source-map-js@1.2.1: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + string-width@8.2.2: + dependencies: + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.3.0 + + svelte-check@4.7.6(picomatch@4.0.7)(svelte@5.56.10)(typescript@6.0.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@sveltejs/load-config': 0.2.3 + chokidar: 4.0.3 + fdir: 6.5.0(picomatch@4.0.7) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.56.10 + typescript: 6.0.3 + transitivePeerDependencies: + - picomatch + + svelte@5.56.10: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.13(acorn@8.18.0) + '@types/estree': 1.0.9 + '@types/trusted-types': 2.0.7 + acorn: 8.18.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.9.1 + esm-env: 1.2.2 + esrap: 2.3.6 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + transitivePeerDependencies: + - '@typescript-eslint/types' + + tagged-tag@1.0.0: {} + + tailwindcss@4.3.3: {} + + tapable@2.3.3: {} + + terminal-size@4.0.1: {} + + tinybench@2.9.0: {} + + tinyexec@1.3.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinyrainbow@3.1.1: {} + + totalist@3.0.1: {} + + tsx@4.23.12: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + + type-fest@4.41.0: {} + + type-fest@5.8.0: + dependencies: + tagged-tag: 1.0.0 + + typescript@6.0.3: {} + + undici-types@8.3.0: {} + + uuid@14.0.2: {} + + vite-plugin-wasm@3.6.0(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)): + dependencies: + vite: 8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12) + + vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.26 + rolldown: 1.2.5 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.3.0 + esbuild: 0.28.2 + fsevents: 2.3.3 + jiti: 2.7.0 + tsx: 4.23.12 + + vitefu@1.1.3(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)): + optionalDependencies: + vite: 8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12) + + vitest@4.1.11(@types/node@26.3.0)(@vitest/browser-playwright@4.1.11)(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)): + dependencies: + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 26.3.0 + '@vitest/browser-playwright': 4.1.11(playwright@1.62.1)(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12))(vitest@4.1.11) + transitivePeerDependencies: + - msw + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + widest-line@6.0.0: + dependencies: + string-width: 8.2.2 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + ws@8.21.3: {} + + xstate@5.32.5: {} + + yoga-layout@3.2.1: {} + + zimmerframe@1.1.4: {} diff --git a/examples/sync/pnpm-workspace.yaml b/examples/sync/pnpm-workspace.yaml new file mode 100644 index 00000000..62604741 --- /dev/null +++ b/examples/sync/pnpm-workspace.yaml @@ -0,0 +1,7 @@ +allowBuilds: + cbor-extract: false + esbuild: true + '@automerge/automerge-repo@git+https://github.com/automerge/automerge-repo.git': true +overrides: + "@automerge/automerge": 3.3.2 + "@automerge/automerge-subduction": 0.16.1 diff --git a/examples/sync/server/server.mjs b/examples/sync/server/server.mjs new file mode 100644 index 00000000..fb8fae7f --- /dev/null +++ b/examples/sync/server/server.mjs @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { WebSocketServer } from "ws" +import { + MemorySigner, + MemoryStorage, + Subduction, +} from "@automerge/automerge-subduction" + +class WsTransport { + #ws + #queue = [] + #waiters = [] + #errorWaiters = [] + #closed = false + #closedPromise + #resolveClosed + #disconnectCallback = null + + constructor(ws) { + this.#ws = ws + this.#closedPromise = new Promise(resolve => { + this.#resolveClosed = resolve + }) + + ws.on("message", data => { + const bytes = toUint8Array(data) + const waiter = this.#waiters.shift() + if (waiter) { + this.#errorWaiters.shift() + waiter(bytes) + } else { + this.#queue.push(bytes) + } + }) + + ws.on("close", () => this.#markClosed(new Error("WebSocket closed"))) + ws.on("error", err => this.#markClosed(err instanceof Error ? err : new Error(String(err)))) + } + + onDisconnect(callback) { + this.#disconnectCallback = callback + } + + async sendBytes(bytes) { + if (this.#closed) throw new Error("WebSocket closed") + await new Promise((resolve, reject) => { + this.#ws.send(Buffer.from(bytes), err => (err ? reject(err) : resolve())) + }) + } + + recvBytes() { + const queued = this.#queue.shift() + if (queued) return Promise.resolve(queued) + if (this.#closed) return Promise.reject(new Error("WebSocket closed")) + return new Promise((resolve, reject) => { + this.#waiters.push(resolve) + this.#errorWaiters.push(reject) + }) + } + + async disconnect() { + this.#markClosed(new Error("WebSocket closed"), false) + this.#ws.close() + } + + closed() { + return this.#closedPromise + } + + #markClosed(err, notify = true) { + if (this.#closed) return + this.#closed = true + this.#resolveClosed() + for (const reject of this.#errorWaiters) reject(err) + this.#waiters = [] + this.#errorWaiters = [] + if (notify) this.#disconnectCallback?.() + } +} + +function toUint8Array(data) { + if (data instanceof Uint8Array) { + return new Uint8Array(data.buffer, data.byteOffset, data.byteLength) + } + if (data instanceof ArrayBuffer) return new Uint8Array(data) + if (Array.isArray(data)) return toUint8Array(Buffer.concat(data)) + return new Uint8Array(Buffer.from(data)) +} + +const port = Number(process.env.PORT ?? 3030) +const host = "127.0.0.1" +const serviceName = process.env.SUBDUCTION_SERVICE_NAME ?? `${host}:${port}` +const subduction = new Subduction({ + signer: new MemorySigner(), + storage: new MemoryStorage(), +}) +const wss = new WebSocketServer({ host, port }) +const leases = new Set() +const clients = new Set() +let managed = process.env.COLN_SYNC_MANAGED === "1" +let shutdownTimer +let shuttingDown = false + +wss.on("connection", (ws, request) => { + if (shuttingDown) { + ws.close() + return + } + if (request.url === "/.coln-sync/lease") { + clearTimeout(shutdownTimer) + managed = true + leases.add(ws) + ws.send(JSON.stringify({ protocol: "coln-sync-relay", pid: process.pid })) + ws.once("close", () => { + leases.delete(ws) + scheduleShutdown() + }) + return + } + + const transport = new WsTransport(ws) + clients.add(ws) + ws.once("close", () => clients.delete(ws)) + subduction.acceptTransport(transport, serviceName).catch(err => { + if (!String(err?.message ?? err).includes("closed")) console.error(err) + }) +}) + +function scheduleShutdown() { + if (!managed || leases.size > 0 || shuttingDown) return + clearTimeout(shutdownTimer) + shutdownTimer = setTimeout(shutdown, 500) +} + +async function shutdown() { + if (shuttingDown) return + shuttingDown = true + clearTimeout(shutdownTimer) + const closed = new Promise(resolve => wss.close(resolve)) + for (const lease of leases) lease.close() + for (const client of clients) client.close() + await subduction.disconnectAll().catch(() => {}) + await closed + process.exit(0) +} + +process.on("SIGINT", shutdown) +process.on("SIGTERM", shutdown) + +wss.on("listening", () => { + console.log(`Subduction relay listening on ws://${host}:${port}`) + if (managed) shutdownTimer = setTimeout(shutdown, 10_000) +}) + +wss.on("error", error => { + if (error.code !== "EADDRINUSE") console.error(error) + process.exit(1) +}) diff --git a/examples/sync/server/with-server.mjs b/examples/sync/server/with-server.mjs new file mode 100644 index 00000000..53f9d732 --- /dev/null +++ b/examples/sync/server/with-server.mjs @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { spawn } from "node:child_process" +import { fileURLToPath } from "node:url" +import WebSocket from "ws" + +const [, , command, ...args] = process.argv + +if (!command) { + console.error("Usage: node server/with-server.mjs [args...]") + process.exit(1) +} + +const host = "127.0.0.1" +const port = Number(process.env.SUBDUCTION_PORT ?? 3030) +const endpoint = `ws://${host}:${port}` +const leaseUrl = `${endpoint}/.coln-sync/lease` +const serverPath = fileURLToPath(new URL("server.mjs", import.meta.url)) + +let lease = await acquireLease() +if (!lease) { + const server = spawn(process.execPath, [serverPath], { + detached: true, + env: { + ...process.env, + COLN_SYNC_MANAGED: "1", + PORT: String(port), + SUBDUCTION_SERVICE_NAME: `${host}:${port}`, + }, + stdio: "ignore", + }) + server.unref() + lease = await waitForLease() +} + +if (!lease) { + console.error(`Could not start the local Subduction relay at ${endpoint}`) + process.exit(1) +} + +const child = spawn(command, args, { + env: { + ...process.env, + SUBDUCTION_ENDPOINT: endpoint, + VITE_SUBDUCTION_ENDPOINT: endpoint, + }, + stdio: "inherit", +}) + +for (const signal of ["SIGINT", "SIGTERM"]) { + process.once(signal, () => child.kill(signal)) +} + +const exitCode = await new Promise(resolve => { + child.once("error", error => { + console.error(error.message) + resolve(1) + }) + child.once("exit", (code, signal) => { + resolve(code ?? (signal === "SIGINT" ? 130 : 143)) + }) +}) + +lease.close() +await Promise.race([ + new Promise(resolve => lease.once("close", resolve)), + new Promise(resolve => setTimeout(resolve, 250)), +]) +process.exitCode = exitCode + +async function waitForLease() { + const deadline = Date.now() + 10_000 + while (Date.now() < deadline) { + const lease = await acquireLease() + if (lease) return lease + await new Promise(resolve => setTimeout(resolve, 50)) + } + return null +} + +function acquireLease() { + return new Promise(resolve => { + const socket = new WebSocket(leaseUrl) + const timeout = setTimeout(() => finish(null), 500) + let settled = false + + const finish = result => { + if (settled) return + settled = true + clearTimeout(timeout) + if (!result) socket.close() + resolve(result) + } + + socket.once("message", data => { + try { + const message = JSON.parse(String(data)) + finish(message.protocol === "coln-sync-relay" ? socket : null) + } catch { + finish(null) + } + }) + socket.once("error", () => finish(null)) + socket.once("close", () => finish(null)) + }) +} diff --git a/examples/sync/src/browser/App.svelte b/examples/sync/src/browser/App.svelte new file mode 100644 index 00000000..f88e51ea --- /dev/null +++ b/examples/sync/src/browser/App.svelte @@ -0,0 +1,165 @@ + + + + + + + + + +
+ {#if feedback} + {#key feedback.id} +

+ {feedback.message} +

+ {/key} + {/if} + +
+ + C + COLN / GRAPH LAB + +
+
+ {syncStatus === "error" ? "sync error" : syncStatus} +
+
+ {graph.heads.length} {graph.heads.length === 1 ? "head" : "heads"} + {endpoint} +
+
+
+ +
+ selectedEdgeId = id} + onselectvertex={selectVertex} + /> + fromId = id} + ontochange={id => toId = id} + /> +
+
diff --git a/examples/sync/src/browser/DocumentLoadError.svelte b/examples/sync/src/browser/DocumentLoadError.svelte new file mode 100644 index 00000000..84a0390c --- /dev/null +++ b/examples/sync/src/browser/DocumentLoadError.svelte @@ -0,0 +1,53 @@ + + + + + + + {title} | Coln Graph Lab + + + +
+
+
+ C + COLN / GRAPH LAB +
+ +
+

{title.toUpperCase()}

+

{heading}

+

{detail}

+
+ + {documentUrl} + + + Create a new graph + + +
+
diff --git a/examples/sync/src/browser/GraphCanvas.svelte b/examples/sync/src/browser/GraphCanvas.svelte new file mode 100644 index 00000000..bc9b25aa --- /dev/null +++ b/examples/sync/src/browser/GraphCanvas.svelte @@ -0,0 +1,205 @@ + + + + + + onselectedge("")} /> + +
+
+ DOCUMENT URL +
+ event.currentTarget.select()} data-testid="doc-url" /> + +
+
+ + {#if graph.vertices.length === 0} + + {/if} + + + + + + + + + + + + + + + + {#each drawableEdges as edge (edge.id)} + { + event.stopPropagation() + onselectedge(edge.id) + }} + onkeydown={(event) => event.key === "Enter" && onselectedge(edge.id)} + role="button" + tabindex="0" + aria-label={`Edge from ${vertexLabel(edge.fromId)} to ${vertexLabel(edge.toId)}`} + data-testid="graph-edge" + > + {/each} + + {#if previewEdgePath} + + {/if} + + {#each points as vertex (vertex.id)} + onselectvertex(vertex)} + onkeydown={(event) => event.key === "Enter" && onselectvertex(vertex)} + role="button" + tabindex="0" + data-testid="graph-vertex" + > + {vertex.id} + + {vertex.label} + + {/each} + + +
+ source + target +
+
diff --git a/examples/sync/src/browser/GraphControls.svelte b/examples/sync/src/browser/GraphControls.svelte new file mode 100644 index 00000000..33c02991 --- /dev/null +++ b/examples/sync/src/browser/GraphControls.svelte @@ -0,0 +1,95 @@ + + + + + + diff --git a/examples/sync/src/browser/main.ts b/examples/sync/src/browser/main.ts new file mode 100644 index 00000000..98278611 --- /dev/null +++ b/examples/sync/src/browser/main.ts @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { Repo, isValidAutomergeUrl } from "@automerge/automerge-repo" +import { create, find } from "@coln-project/repo" +// @ts-expect-error initSync is exported at runtime but absent from declarations +import { initSync } from "@automerge/automerge-subduction/slim" +// @ts-expect-error wasm-base64 is exported without declarations +import { wasmBase64 } from "@automerge/automerge-subduction/wasm-base64" +import { mount } from "svelte" +import * as GraphRealm from "../generated/GraphRealm.ts" +import App from "./App.svelte" +import DocumentLoadError from "./DocumentLoadError.svelte" +import "./style.css" + +initSync({ module: Uint8Array.from(atob(wasmBase64), char => char.charCodeAt(0)) }) + +const endpoint = import.meta.env.VITE_SUBDUCTION_ENDPOINT + ?? "wss://subduction.sync.inkandswitch.com" +const hashUrl = location.hash.slice(1) +const documentUrl = isValidAutomergeUrl(hashUrl) ? hashUrl : undefined +const target = document.getElementById("app")! + +if (hashUrl && !documentUrl) { + mount(DocumentLoadError, { + target, + props: { documentUrl: hashUrl, kind: "invalid" }, + }) +} else { + const repo = new Repo({ subductionWebsocketEndpoints: [endpoint] }) + + try { + const handle = documentUrl + ? await find(repo, documentUrl, GraphRealm) + : create(repo, GraphRealm) + + location.hash = handle.url + if (import.meta.env.DEV) Object.assign(window, { repo, handle }) + + mount(App, { + target, + props: { handle, repo, endpoint }, + }) + } catch (cause) { + await repo.shutdown().catch(() => undefined) + if (!isDocumentUnavailable(cause)) throw cause + + mount(DocumentLoadError, { + target, + props: { documentUrl: hashUrl, kind: "unavailable" }, + }) + } +} + +function isDocumentUnavailable(cause: unknown): boolean { + return cause instanceof Error && /^Document .+ is unavailable$/.test(cause.message) +} diff --git a/examples/sync/src/browser/style.css b/examples/sync/src/browser/style.css new file mode 100644 index 00000000..c05c0d43 --- /dev/null +++ b/examples/sync/src/browser/style.css @@ -0,0 +1,5 @@ +/* SPDX-FileCopyrightText: 2026 Coln contributors */ +/* SPDX-License-Identifier: Apache-2.0 OR MIT */ + +@import url("https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;600;700&display=swap"); +@import "tailwindcss"; diff --git a/examples/sync/src/cli/App.tsx b/examples/sync/src/cli/App.tsx new file mode 100644 index 00000000..371fd2c4 --- /dev/null +++ b/examples/sync/src/cli/App.tsx @@ -0,0 +1,219 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { Repo } from "@automerge/automerge-repo" +import { Box, Text, useInput, useStdout } from "ink" +import TextInput from "ink-text-input" +import { useEffect, useRef, useState } from "react" +import { + addEdge, + addVertex, + readGraph, + watchGraph, + type GraphHandle, +} from "../lib/graph.ts" +import { executeQuery, QueryError } from "../query/execute-query.ts" + +type Props = { + handle: GraphHandle + repo: Repo + endpoint: string + onQuit: () => void +} + +type Endpoint = "source" | "target" + +export default function App({ handle, repo, endpoint, onQuit }: Props) { + const { stdout } = useStdout() + const [graph, setGraph] = useState(() => readGraph(handle.doc())) + const [sourceIndex, setSourceIndex] = useState(0) + const [targetIndex, setTargetIndex] = useState(0) + const [activeEndpoint, setActiveEndpoint] = useState("source") + const [queryMode, setQueryMode] = useState(false) + const [query, setQuery] = useState("") + const [history, setHistory] = useState([]) + const [historyIndex, setHistoryIndex] = useState(0) + const [message, setMessage] = useState("Ready") + const [connected, setConnected] = useState(repo.isSubductionConnected()) + const [quitting, setQuitting] = useState(false) + const [terminalHeight, setTerminalHeight] = useState(stdout.rows || 24) + const quittingRef = useRef(false) + + useEffect(() => { + const resize = () => setTerminalHeight(stdout.rows || 24) + stdout.on("resize", resize) + return () => { + stdout.off("resize", resize) + } + }, [stdout]) + useEffect(() => watchGraph(handle, setGraph), [handle]) + useEffect(() => { + const update = ({ connected }: { connected: boolean }) => setConnected(connected) + repo.on("subduction-connection", update) + return () => { + repo.off("subduction-connection", update) + } + }, [repo]) + useEffect(() => { + const last = Math.max(0, graph.vertices.length - 1) + setSourceIndex(index => Math.min(index, last)) + setTargetIndex(index => Math.min(index, last)) + }, [graph.vertices.length]) + useEffect(() => { + if (quitting) onQuit() + }, [onQuit, quitting]) + + useInput((input, key) => { + if (quittingRef.current) return + if (key.ctrl && input === "c") { + quit() + return + } + if (queryMode) { + if (key.escape) { + setQueryMode(false) + setMessage("Query cancelled") + } else if (key.upArrow && history.length > 0) { + const next = Math.max(0, historyIndex - 1) + setHistoryIndex(next) + setQuery(history[next] ?? "") + } else if (key.downArrow && history.length > 0) { + const next = Math.min(history.length, historyIndex + 1) + setHistoryIndex(next) + setQuery(history[next] ?? "") + } + return + } + + if (input === "q") quit() + else if (input === "v") run("Vertex added", () => addVertex(handle)) + else if (input === "e") createEdge() + else if (input === "/") { + setQueryMode(true) + setQuery("") + setHistoryIndex(history.length) + setMessage("Enter a TypeScript change callback body") + } else if (key.tab) { + setActiveEndpoint(value => value === "source" ? "target" : "source") + } else if (key.upArrow) { + moveSelection(-1) + } else if (key.downArrow) { + moveSelection(1) + } + }) + + const source = graph.vertices[sourceIndex] + const target = graph.vertices[targetIndex] + + function quit() { + if (quittingRef.current) return + quittingRef.current = true + setQuitting(true) + } + + function moveSelection(delta: number) { + if (graph.vertices.length === 0) return + const move = (index: number) => + (index + delta + graph.vertices.length) % graph.vertices.length + if (activeEndpoint === "source") setSourceIndex(move) + else setTargetIndex(move) + } + + function createEdge() { + if (!source || !target) { + setMessage("Add a vertex before drawing an edge") + return + } + run(`${source.label} -> ${target.label} added`, () => addEdge(handle, source, target)) + } + + function submitQuery(source: string) { + if (!source.trim()) { + setQueryMode(false) + return + } + try { + executeQuery(handle, source) + setHistory(values => [...values, source]) + setHistoryIndex(history.length + 1) + setMessage("Query committed") + setQuery("") + setQueryMode(false) + } catch (cause) { + const prefix = cause instanceof QueryError ? cause.phase : "query" + setMessage(`${prefix}: ${cause instanceof Error ? cause.message : String(cause)}`) + } + } + + function run(success: string, action: () => void) { + try { + action() + setMessage(success) + } catch (cause) { + setMessage(cause instanceof Error ? cause.message : String(cause)) + } + } + + const names = new Map(graph.vertices.map(vertex => [vertex.id, vertex.label])) + + return ( + + + COLN / GRAPH CLI + + {quitting ? "◌ shutting down" : connected ? "● relay online" : "○ connecting"} + + + + + DOCUMENT + {handle.url} + {endpoint} · {graph.heads.length} synchronized head{graph.heads.length === 1 ? "" : "s"} + + + + + VERTICES {graph.vertices.length} + {graph.vertices.length === 0 + ? (empty) + : graph.vertices.map((vertex, index) => ( + + + {index === sourceIndex ? "S" : " "}{index === targetIndex ? "T" : " "} {vertex.label} + + {vertex.id.slice(0, 8)} + + ))} + + + + EDGES {graph.edges.length} + {graph.edges.length === 0 + ? (empty) + : graph.edges.map(edge => ( + {names.get(edge.fromId)} → {names.get(edge.toId)} + ))} + + + + + + Endpoint: {activeEndpoint} + {" "}S: {source?.label ?? "-"} → T: {target?.label ?? "-"} + + {quitting ? ( + Syncing and quitting… + ) : queryMode ? ( + + change(txn) › + + + ) : ( + v add vertex · e add edge · tab switch endpoint · ↑/↓ select · / TypeScript · q quit + )} + {!quitting && {message}} + + + ) +} diff --git a/examples/sync/src/cli/main.tsx b/examples/sync/src/cli/main.tsx new file mode 100644 index 00000000..40401017 --- /dev/null +++ b/examples/sync/src/cli/main.tsx @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { + Repo, + initSubduction, + isValidAutomergeUrl, + type AutomergeUrl, +} from "@automerge/automerge-repo" +import { create, find } from "@coln-project/repo" +import { render } from "ink" +import * as GraphRealm from "../generated/GraphRealm.ts" +import App from "./App.tsx" + +const endpoint = process.env.SUBDUCTION_ENDPOINT + ?? "wss://subduction.sync.inkandswitch.com" +const url = process.argv[2] + +if (url && !isValidAutomergeUrl(url)) { + console.error("Expected an automerge URL or no argument to create a document.") + process.exit(1) +} +if (!process.stdin.isTTY || !process.stdout.isTTY) { + console.error("The sync CLI requires an interactive terminal.") + process.exit(1) +} + +await initSubduction() +const repo = new Repo({ + subductionWebsocketEndpoints: [endpoint], + subductionTimeouts: { syncMs: 5_000, defaultMs: 5_000 }, +}) +await waitForSubduction(repo, endpoint) +const handle = url + ? await find(repo, url as AutomergeUrl, GraphRealm) + : create(repo, GraphRealm) + +let requestQuit!: () => void +const quitRequested = new Promise(resolve => { + requestQuit = resolve +}) + +let instance: ReturnType | undefined +let alternateScreenActive = true +const signalHandlers = new Map void>() + +process.stdout.write("\u001B[?1049h") +for (const signal of ["SIGHUP", "SIGINT", "SIGTERM"] as const) { + const handler = () => { + removeSignalHandlers() + instance?.unmount() + leaveAlternateScreen() + process.kill(process.pid, signal) + } + signalHandlers.set(signal, handler) + process.once(signal, handler) +} + +try { + instance = render( + , + { exitOnCtrlC: false }, + ) + try { + await Promise.race([quitRequested, instance.waitUntilExit()]) + } finally { + await repo.shutdown() + } +} finally { + removeSignalHandlers() + instance?.unmount() + leaveAlternateScreen() +} + +function removeSignalHandlers(): void { + for (const [signal, handler] of signalHandlers) { + process.off(signal, handler) + } + signalHandlers.clear() +} + +function leaveAlternateScreen(): void { + if (!alternateScreenActive) return + alternateScreenActive = false + process.stdout.write("\u001B[?1049l") +} + +async function waitForSubduction(repo: Repo, endpoint: string): Promise { + if (repo.isSubductionConnected()) return + + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + cleanup() + reject(new Error(`Timed out connecting to ${endpoint}`)) + }, 10_000) + const connected = ({ connected }: { connected: boolean }) => { + if (!connected) return + cleanup() + resolve() + } + const cleanup = () => { + clearTimeout(timeout) + repo.off("subduction-connection", connected) + } + + repo.on("subduction-connection", connected) + if (repo.isSubductionConnected()) { + cleanup() + resolve() + } + }) +} diff --git a/examples/sync/src/generated/Graph.ts b/examples/sync/src/generated/Graph.ts new file mode 100644 index 00000000..b8fc8138 --- /dev/null +++ b/examples/sync/src/generated/Graph.ts @@ -0,0 +1,11 @@ +import * as runtime from "@coln-project/runtime"; + +export interface View { + V: runtime.ColnSet.View; + E: (x: runtime.Value) => (x: runtime.Value) => runtime.ColnSet.View; +} + +export interface Transaction extends View { + V: runtime.ColnSet.Transaction; + E: (x: runtime.Value) => (x: runtime.Value) => runtime.ColnSet.Transaction; +} \ No newline at end of file diff --git a/examples/sync/src/generated/GraphRealm.json b/examples/sync/src/generated/GraphRealm.json new file mode 100644 index 00000000..b83d89f3 --- /dev/null +++ b/examples/sync/src/generated/GraphRealm.json @@ -0,0 +1 @@ +{"entities":[{"path":[["GraphRealm"],["V"]],"value":{"entityVariant":{"tag":"table"},"columns":[],"primaryKey":null}},{"path":[["GraphRealm"],["E"]],"value":{"entityVariant":{"tag":"table"},"columns":[{"path":[["a"]],"type":{"tag":"rowId","path":[["GraphRealm"],["V"]]}},{"path":[["b"]],"type":{"tag":"rowId","path":[["GraphRealm"],["V"]]}}],"primaryKey":null}}],"rules":[{"path":[["GraphRealm"],["V"],["foreignKey"]],"value":{"ruleVariant":"enforced","varNames":[],"varTypes":[],"antecedents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":null,"values":[]}}],"consequents":[]}},{"path":[["GraphRealm"],["E"],["foreignKey"]],"value":{"ruleVariant":"enforced","varNames":[[["a"]],[["b"]]],"varTypes":[{"tag":"rowId","path":[["GraphRealm"],["V"]]},{"tag":"rowId","path":[["GraphRealm"],["V"]]}],"antecedents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["E"]],"rowId":null,"values":[{"column":0,"term":{"tag":"var","index":0}},{"column":1,"term":{"tag":"var","index":1}}]}}],"consequents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":{"tag":"var","index":0},"values":[]}},{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":{"tag":"var","index":1},"values":[]}}]}}]} \ No newline at end of file diff --git a/examples/sync/src/generated/GraphRealm.pretty b/examples/sync/src/generated/GraphRealm.pretty new file mode 100644 index 00000000..b4a7d502 --- /dev/null +++ b/examples/sync/src/generated/GraphRealm.pretty @@ -0,0 +1,13 @@ +flatrealm + entities + table ℜ.V := [] + table ℜ.E := [.a : ℜ.V, .b : ℜ.V] + end + rules + enforced ℜ.V.foreignKey := ℜ.V [] ⊢ ⊤ + enforced ℜ.E.foreignKey a b := ℜ.E [ + .a ↦ a, + .b ↦ b + ] ⊢ a ∈ ℜ.V [] ∧ b ∈ ℜ.V [] + end +end \ No newline at end of file diff --git a/examples/sync/src/generated/GraphRealm.ts b/examples/sync/src/generated/GraphRealm.ts new file mode 100644 index 00000000..9b2db543 --- /dev/null +++ b/examples/sync/src/generated/GraphRealm.ts @@ -0,0 +1,48 @@ +import schema from "./GraphRealm.json"; +export {schema}; +import * as runtime from "@coln-project/runtime"; +import * as Graph from "./Graph.ts"; + +export class View { + root: Graph.View; + + constructor(store: runtime.StoreHandle) { + this.root = { + V: (new runtime.RowIdSet.View(store, "GraphRealm.V", [])), + E: (a: runtime.Value) => { + return (b: runtime.Value) => { + return (new runtime.RowIdSet.View(store, "GraphRealm.E", [a, b])); + }; + } + }; + } +} + +export class Transaction extends View { + root: Graph.Transaction; + + constructor( + store: runtime.StoreHandle, + transaction: runtime.TransactionHandle + ) { + super(store); + this.root = { + V: (new runtime.RowIdSet.Transaction( + store, + "GraphRealm.V", + [], + transaction + )), + E: (a: runtime.Value) => { + return (b: runtime.Value) => { + return (new runtime.RowIdSet.Transaction( + store, + "GraphRealm.E", + [a, b], + transaction + )); + }; + } + }; + } +} \ No newline at end of file diff --git a/examples/sync/src/lib/coln-handle.svelte.ts b/examples/sync/src/lib/coln-handle.svelte.ts new file mode 100644 index 00000000..bc27fd11 --- /dev/null +++ b/examples/sync/src/lib/coln-handle.svelte.ts @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { + ColnChange, + ColnDocument, + ColnHandle as RepoColnHandle, + RealmBindings, +} from "@coln-project/repo"; +import { createSubscriber } from "svelte/reactivity"; + +export class ColnHandle { + readonly #subscribe: () => void; + + constructor(private readonly handle: RepoColnHandle) { + this.#subscribe = createSubscriber((update) => { + handle.on("change", update); + + return () => { + handle.off("change", update); + }; + }); + } + + get state(): ColnDocument { + this.#subscribe(); + return this.handle.doc(); + } + + change(change: ColnChange): void { + this.handle.change(change); + } +} diff --git a/examples/sync/src/lib/graph.ts b/examples/sync/src/lib/graph.ts new file mode 100644 index 00000000..8a3ad270 --- /dev/null +++ b/examples/sync/src/lib/graph.ts @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { ColnDocument, ColnHandle } from "@coln-project/repo" +import type { RowRef, RowView, Value } from "@coln-project/runtime" +import * as GraphRealm from "../generated/GraphRealm.ts" + +export type GraphHandle = ColnHandle +type GraphDocument = ColnDocument +type GraphWriter = Pick +type RowIdValue = Extract + +export type Vertex = { + id: string + label: string + ref: RowRef +} + +export type Edge = { + id: string + fromId: string + toId: string +} + +export type Graph = { + vertices: Vertex[] + edges: Edge[] + heads: string[] +} + +export function readGraph(document: GraphDocument): Graph { + const vertices = iteratorToArray(document.root.V.values()) + .map(rowRef) + .filter(ref => ref !== null) + .map(ref => ({ id: refId(ref), ref })) + .map((vertex, index) => ({ ...vertex, label: `V${index + 1}` })) + const vertexIds = new Set(vertices.map(vertex => vertex.id)) + + const edges = document.scanTable("GraphRealm.E") + .flatMap(row => projectEdge(row, vertexIds)) + .sort((left, right) => left.id.localeCompare(right.id)) + + return { vertices, edges, heads: [...document.heads()].sort() } +} + +export function addVertex(writer: GraphWriter): void { + writer.change(tx => { + tx.root.V.add() + }) +} + +export function addEdge( + writer: GraphWriter, + from: Vertex, + to: Vertex, +): void { + writer.change(tx => { + tx.root.E(rowValue(from.ref))(rowValue(to.ref)).add() + }) +} + +export function watchGraph( + handle: GraphHandle, + update: (graph: Graph) => void, +): () => void { + const refresh = () => update(readGraph(handle.doc())) + handle.on("change", refresh) + handle.on("heads-changed", refresh) + refresh() + + return () => { + handle.off("change", refresh) + handle.off("heads-changed", refresh) + } +} + +function projectEdge(row: RowView, vertexIds: Set): Edge[] { + const from = valueRef(row.values[0]) + const to = valueRef(row.values[1]) + const edge = rowRef(row) + if (!from || !to || !edge) return [] + + const fromId = refId(from) + const toId = refId(to) + if (!vertexIds.has(fromId) || !vertexIds.has(toId)) return [] + + return [{ id: refId(edge), fromId, toId }] +} + +function iteratorToArray(iterator: Iterator): T[] { + const values: T[] = [] + for (let next = iterator.next(); !next.done; next = iterator.next()) { + values.push(next.value) + } + return values +} + +function rowRef(row: RowView): RowRef | null { + return valueRef(row.rowId) +} + +function valueRef(value: Value | undefined): RowRef | null { + return value?.tag === "row_id" ? value.value : null +} + +function rowValue(ref: RowRef): RowIdValue { + return { tag: "row_id", value: ref } +} + +function refId(ref: RowRef): string { + if ("existing" in ref) return `${ref.existing.commit}:${ref.existing.counter}` + return `pending:${ref.pending.txId}:${ref.pending.counter}` +} diff --git a/examples/sync/src/lib/subduction-sync.svelte.ts b/examples/sync/src/lib/subduction-sync.svelte.ts new file mode 100644 index 00000000..99fc4442 --- /dev/null +++ b/examples/sync/src/lib/subduction-sync.svelte.ts @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { Repo } from "@automerge/automerge-repo"; +import type { ColnHandle, RealmBindings } from "@coln-project/repo"; +import { createSubscriber } from "svelte/reactivity"; + +export type SubductionSyncStatus = "offline" | "syncing" | "synced" | "error"; + +export class SubductionSync { + readonly #subscribe: () => void; + #status: SubductionSyncStatus; + #error: unknown; + + constructor( + private readonly repo: Repo, + private readonly handle: ColnHandle, + ) { + this.#status = repo.isSubductionConnected() ? "syncing" : "offline"; + this.#subscribe = createSubscriber((update) => { + let active = true; + let generation = 0; + let currentAttempt: symbol | undefined; + + const setStatus = (status: SubductionSyncStatus, error?: unknown) => { + if (this.#status === status && this.#error === error) return; + this.#status = status; + this.#error = error; + update(); + }; + + const finishFlush = ( + attempt: symbol, + flushGeneration: number, + failed: boolean, + error?: unknown, + ) => { + if (!active || currentAttempt !== attempt) return; + currentAttempt = undefined; + if (!this.repo.isSubductionConnected()) { + setStatus("offline"); + } else if (flushGeneration !== generation) { + startFlush(); + } else if (failed) { + setStatus("error", error); + } else { + setStatus("synced"); + } + }; + + const startFlush = () => { + if (currentAttempt || !active || !this.repo.isSubductionConnected()) return; + const attempt = Symbol(); + const flushGeneration = generation; + currentAttempt = attempt; + void this.repo.flush([this.handle.documentId]).then( + () => finishFlush(attempt, flushGeneration, false), + error => finishFlush(attempt, flushGeneration, true, error), + ); + }; + + const requestFlush = () => { + generation += 1; + if (!this.repo.isSubductionConnected()) { + currentAttempt = undefined; + setStatus("offline"); + return; + } + setStatus("syncing"); + startFlush(); + }; + + const connectionChanged = () => { + requestFlush(); + }; + + this.handle.on("change", requestFlush); + this.repo.on("subduction-connection", connectionChanged); + requestFlush(); + + return () => { + active = false; + generation += 1; + currentAttempt = undefined; + this.handle.off("change", requestFlush); + this.repo.off("subduction-connection", connectionChanged); + }; + }); + } + + get connected(): boolean { + this.#subscribe(); + return this.repo.isSubductionConnected(); + } + + get status(): SubductionSyncStatus { + this.#subscribe(); + return this.#status; + } + + get error(): unknown { + this.#subscribe(); + return this.#error; + } +} diff --git a/examples/sync/src/query/execute-query.ts b/examples/sync/src/query/execute-query.ts new file mode 100644 index 00000000..887259d0 --- /dev/null +++ b/examples/sync/src/query/execute-query.ts @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import ts from "typescript" + +export type ChangeTarget = { + change(callback: (transaction: Transaction) => void): void +} + +export class QueryError extends Error { + constructor( + readonly phase: "compile" | "runtime", + message: string, + options?: ErrorOptions, + ) { + super(message, options) + this.name = "QueryError" + } +} + +export function compileQuery(source: string): (txn: Transaction) => unknown { + const result = ts.transpileModule(`const __colnQuery = (txn) => {\n${source}\n}`, { + compilerOptions: { + target: ts.ScriptTarget.ES2022, + module: ts.ModuleKind.ESNext, + }, + fileName: "query.ts", + reportDiagnostics: true, + }) + const diagnostics = result.diagnostics?.filter( + diagnostic => diagnostic.category === ts.DiagnosticCategory.Error, + ) ?? [] + + if (diagnostics.length > 0) { + throw new QueryError("compile", ts.formatDiagnostics(diagnostics, { + getCanonicalFileName: fileName => fileName, + getCurrentDirectory: () => "", + getNewLine: () => "\n", + }).trim()) + } + + try { + const factory = new Function(`${result.outputText}\nreturn __colnQuery`) + return factory() as (txn: Transaction) => unknown + } catch (cause) { + throw new QueryError("compile", errorMessage(cause), { cause }) + } +} + +export function executeQuery( + target: ChangeTarget, + source: string, +): void { + const query = compileQuery(source) + try { + target.change(transaction => { + const result = query(transaction) + if (isThenable(result)) { + throw new TypeError("Queries must complete synchronously") + } + }) + } catch (cause) { + throw new QueryError("runtime", errorMessage(cause), { cause }) + } +} + +function isThenable(value: unknown): value is PromiseLike { + return typeof value === "object" + && value !== null + && "then" in value + && typeof value.then === "function" +} + +function errorMessage(cause: unknown): string { + return cause instanceof Error ? cause.message : String(cause) +} diff --git a/examples/sync/tests/coln-handle.unit.ts b/examples/sync/tests/coln-handle.unit.ts new file mode 100644 index 00000000..66a7aeae --- /dev/null +++ b/examples/sync/tests/coln-handle.unit.ts @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { flushSync } from "svelte" +import { describe, expect, test, vi } from "vitest" +import type { ColnChange, ColnDocument, ColnHandle as RepoColnHandle } from "@coln-project/repo" +import { ColnHandle } from "../src/lib/coln-handle.svelte.ts" +import * as GraphRealm from "../src/generated/GraphRealm.ts" +import { observe } from "./observe.svelte.ts" + +type GraphHandle = RepoColnHandle +type GraphDocument = ColnDocument +type Change = ColnChange +type ChangePayload = { doc: GraphDocument } + +describe("ColnHandle", () => { + test("returns the current document", () => { + const handle = new FakeHandle() + const colnHandle = new ColnHandle(handle as unknown as GraphHandle) + + expect(colnHandle.state).toBe(handle.document) + + handle.document = fakeDocument() + expect(colnHandle.state).toBe(handle.document) + }) + + test("delegates changes to the handle", () => { + const handle = new FakeHandle() + const colnHandle = new ColnHandle(handle as unknown as GraphHandle) + const change = vi.fn() + + colnHandle.change(change) + + expect(handle.change).toHaveBeenCalledOnce() + expect(handle.change).toHaveBeenCalledWith(change) + }) + + test("reacts to changes and releases its listener", async () => { + const handle = new FakeHandle() + const colnHandle = new ColnHandle(handle as unknown as GraphHandle) + let observed: GraphDocument | undefined + const stop = observe(() => colnHandle.state, state => { + observed = state + }) + flushSync() + + expect(observed).toBe(handle.document) + expect(handle.listenerCount("change")).toBe(1) + handle.document = fakeDocument() + handle.emit("change", { doc: handle.document }) + flushSync() + expect(observed).toBe(handle.document) + + stop() + await Promise.resolve() + expect(handle.listenerCount("change")).toBe(0) + }) +}) + +class FakeHandle { + document = fakeDocument() + readonly change = vi.fn((_change: Change) => {}) + readonly #listeners = new Set<(payload: ChangePayload) => void>() + + doc(): GraphDocument { + return this.document + } + + on(_event: "change", listener: (payload: ChangePayload) => void): void { + this.#listeners.add(listener) + } + + off(_event: "change", listener: (payload: ChangePayload) => void): void { + this.#listeners.delete(listener) + } + + emit(_event: "change", payload: ChangePayload): void { + for (const listener of this.#listeners) listener(payload) + } + + listenerCount(_event: "change"): number { + return this.#listeners.size + } +} + +function fakeDocument(): GraphDocument { + return {} as GraphDocument +} diff --git a/examples/sync/tests/observe.svelte.ts b/examples/sync/tests/observe.svelte.ts new file mode 100644 index 00000000..b9afeb19 --- /dev/null +++ b/examples/sync/tests/observe.svelte.ts @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +export function observe(read: () => T, update: (value: T) => void): () => void { + return $effect.root(() => { + $effect(() => update(read())) + }) +} diff --git a/examples/sync/tests/subduction-sync.unit.ts b/examples/sync/tests/subduction-sync.unit.ts new file mode 100644 index 00000000..52c9ba33 --- /dev/null +++ b/examples/sync/tests/subduction-sync.unit.ts @@ -0,0 +1,259 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { Repo } from "@automerge/automerge-repo" +import type { ColnHandle } from "@coln-project/repo" +import { flushSync } from "svelte" +import { describe, expect, test } from "vitest" +import * as GraphRealm from "../src/generated/GraphRealm.ts" +import { SubductionSync, type SubductionSyncStatus } from "../src/lib/subduction-sync.svelte.ts" +import { observe } from "./observe.svelte.ts" + +type GraphHandle = ColnHandle + +describe("SubductionSync", () => { + test("moves from offline through syncing to synced and cleans up", async () => { + const repo = new FakeRepo(false) + const handle = new FakeHandle() + const sync = new SubductionSync(repo as unknown as Repo, handle as unknown as GraphHandle) + let status: SubductionSyncStatus | undefined + const stop = observe(() => sync.status, value => { + status = value + }) + flushSync() + + expect(status).toBe("offline") + expect(repo.listenerCount()).toBe(1) + expect(handle.listenerCount()).toBe(1) + expect(repo.flushes).toHaveLength(0) + + repo.connect() + flushSync() + expect(status).toBe("syncing") + expect(repo.flushes).toHaveLength(1) + expect(repo.flushedDocuments[0]).toEqual([handle.documentId]) + + await repo.resolveFlush(0) + flushSync() + expect(status).toBe("synced") + + stop() + await Promise.resolve() + expect(repo.listenerCount()).toBe(0) + expect(handle.listenerCount()).toBe(0) + }) + + test("does not report synced while a newer change is pending", async () => { + const repo = new FakeRepo(true) + const handle = new FakeHandle() + const sync = new SubductionSync(repo as unknown as Repo, handle as unknown as GraphHandle) + let status: SubductionSyncStatus | undefined + const stop = observe(() => sync.status, value => { + status = value + }) + flushSync() + + await repo.resolveFlush(0) + flushSync() + expect(status).toBe("synced") + + handle.emitChange() + flushSync() + expect(status).toBe("syncing") + expect(repo.flushes).toHaveLength(2) + + handle.emitChange() + await repo.resolveFlush(1) + flushSync() + expect(status).toBe("syncing") + expect(repo.flushes).toHaveLength(3) + + await repo.resolveFlush(2) + flushSync() + expect(status).toBe("synced") + stop() + }) + + test("reports flush failures and retries after another change", async () => { + const repo = new FakeRepo(true) + const handle = new FakeHandle() + const sync = new SubductionSync(repo as unknown as Repo, handle as unknown as GraphHandle) + let observed: { status: SubductionSyncStatus; error: unknown } | undefined + const stop = observe(() => ({ status: sync.status, error: sync.error }), value => { + observed = value + }) + flushSync() + + const failure = new Error("save failed") + await repo.rejectFlush(0, failure) + flushSync() + expect(observed).toEqual({ status: "error", error: failure }) + + handle.emitChange() + flushSync() + expect(observed?.status).toBe("syncing") + await repo.resolveFlush(1) + flushSync() + expect(observed).toEqual({ status: "synced", error: undefined }) + stop() + }) + + test("reconnects without waiting for an obsolete flush", async () => { + const repo = new FakeRepo(true) + const handle = new FakeHandle() + const sync = new SubductionSync(repo as unknown as Repo, handle as unknown as GraphHandle) + let status: SubductionSyncStatus | undefined + const stop = observe(() => sync.status, value => { + status = value + }) + flushSync() + + expect(repo.flushes).toHaveLength(1) + repo.disconnect() + flushSync() + expect(status).toBe("offline") + + repo.connect() + flushSync() + expect(status).toBe("syncing") + expect(repo.flushes).toHaveLength(2) + + await repo.resolveFlush(1) + flushSync() + expect(status).toBe("synced") + + await repo.rejectFlush(0, new Error("obsolete")) + flushSync() + expect(status).toBe("synced") + stop() + }) + + test("ignores late completion after cleanup and can resubscribe", async () => { + const repo = new FakeRepo(true) + const handle = new FakeHandle() + const sync = new SubductionSync(repo as unknown as Repo, handle as unknown as GraphHandle) + let status: SubductionSyncStatus | undefined + const firstStop = observe(() => sync.status, value => { + status = value + }) + flushSync() + + firstStop() + await Promise.resolve() + await repo.rejectFlush(0, new Error("late")) + expect(repo.listenerCount()).toBe(0) + expect(handle.listenerCount()).toBe(0) + + const secondStop = observe(() => sync.status, value => { + status = value + }) + flushSync() + expect(repo.listenerCount()).toBe(1) + expect(handle.listenerCount()).toBe(1) + expect(repo.flushes).toHaveLength(2) + expect(status).toBe("syncing") + + await repo.resolveFlush(1) + flushSync() + expect(status).toBe("synced") + secondStop() + }) +}) + +class FakeHandle { + readonly documentId = "test-document" + readonly #listeners = new Set<() => void>() + + on(_event: "change", listener: () => void): void { + this.#listeners.add(listener) + } + + off(_event: "change", listener: () => void): void { + this.#listeners.delete(listener) + } + + emitChange(): void { + for (const listener of this.#listeners) listener() + } + + listenerCount(): number { + return this.#listeners.size + } +} + +class FakeRepo { + readonly flushes: Deferred[] = [] + readonly flushedDocuments: unknown[] = [] + #connected: boolean + readonly #listeners = new Set<() => void>() + + constructor(connected: boolean) { + this.#connected = connected + } + + isSubductionConnected(): boolean { + return this.#connected + } + + on(_event: "subduction-connection", listener: () => void): void { + this.#listeners.add(listener) + } + + off(_event: "subduction-connection", listener: () => void): void { + this.#listeners.delete(listener) + } + + flush(documents?: unknown): Promise { + const flush = deferred() + this.flushes.push(flush) + this.flushedDocuments.push(documents) + return flush.promise + } + + connect(): void { + this.#connected = true + for (const listener of this.#listeners) listener() + } + + disconnect(): void { + this.#connected = false + for (const listener of this.#listeners) listener() + } + + async resolveFlush(index: number): Promise { + const flush = this.flushes[index] + if (!flush) throw new Error(`Missing flush ${index}`) + flush.resolve() + await flush.promise + await Promise.resolve() + } + + async rejectFlush(index: number, error: unknown): Promise { + const flush = this.flushes[index] + if (!flush) throw new Error(`Missing flush ${index}`) + flush.reject(error) + await flush.promise.catch(() => undefined) + await Promise.resolve() + } + + listenerCount(): number { + return this.#listeners.size + } +} + +type Deferred = { + promise: Promise + resolve: (value: T | PromiseLike) => void + reject: (error: unknown) => void +} + +function deferred(): Deferred { + let resolve!: Deferred["resolve"] + let reject!: Deferred["reject"] + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise + reject = rejectPromise + }) + return { promise, resolve, reject } +} diff --git a/examples/sync/tests/sync.spec.ts b/examples/sync/tests/sync.spec.ts new file mode 100644 index 00000000..920bc0c9 --- /dev/null +++ b/examples/sync/tests/sync.spec.ts @@ -0,0 +1,154 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { expect, test } from "@playwright/test" +import { + initSubduction, + Repo, + type AutomergeUrl, +} from "@automerge/automerge-repo" +import { colnDocType } from "@coln-project/repo" +import { executeQuery } from "../src/query/execute-query.ts" + +test.beforeAll(async () => { + await initSubduction() +}) + +test("browser controls and TypeScript queries converge", async ({ browser }) => { + const context = await browser.newContext() + await context.grantPermissions(["clipboard-read", "clipboard-write"], { + origin: "http://127.0.0.1:5176", + }) + const page = await context.newPage() + const nodeRepo = new Repo({ + subductionWebsocketEndpoints: ["ws://127.0.0.1:3031"], + }) + + try { + await page.goto("/") + await expect(page.getByTestId("sync-status")).toHaveAttribute("data-status", "synced") + await expect(page.getByTestId("head-count")).toHaveText("1 head") + await expect(page.getByTestId("subduction-url")).toHaveText("ws://127.0.0.1:3031") + await expect(page.getByTestId("doc-url")).toHaveValue(/^automerge:/) + await page.getByTestId("copy-document-url").click() + await expect(page.getByTestId("feedback")).toHaveText("URL copied") + + const url = await page.getByTestId("doc-url").inputValue() as AutomergeUrl + const nodeHandle = await nodeRepo.find(url, colnDocType) + + await page.getByTestId("add-vertex").click() + await expect(page.getByTestId("feedback")).toHaveText("Vertex added") + const firstVertexFeedbackId = await page.getByTestId("feedback").getAttribute("data-feedback-id") + await page.getByTestId("add-vertex").click() + await expect(page.getByTestId("feedback")).not.toHaveAttribute("data-feedback-id", firstVertexFeedbackId!) + await expect(page.getByTestId("graph-vertex")).toHaveCount(2) + const initialVertexLabels = await vertexLabels(page) + await expect(page.getByTestId("sync-status")).toHaveAttribute("data-status", "synced") + await expect.poll(() => nodeHandle.doc().scanTable("GraphRealm.V").length).toBe(2) + + executeQuery(nodeHandle, 'txn.add("GraphRealm.V", [])') + await nodeRepo.flush() + nodeRepo.resyncSubduction(nodeHandle.documentId) + await expect(page.getByTestId("graph-vertex")).toHaveCount(3) + const updatedVertexLabels = await vertexLabels(page) + expect([...initialVertexLabels].map(([id]) => [id, updatedVertexLabels.get(id)])) + .toEqual([...initialVertexLabels]) + + const values = await page.getByTestId("from-select").locator("option").evaluateAll(options => + options.slice(1).map(option => (option as HTMLOptionElement).value), + ) + await page.getByTestId("from-select").selectOption(values[0]) + await page.getByTestId("to-select").selectOption(values[1]) + await expect(page.getByTestId("edge-preview")).toBeVisible() + await expect(page.getByTestId("edge-preview")).toHaveClass(/\[stroke-dasharray:2_9\]/) + await expect(page.getByTestId("edge-preview")).toHaveCSS("pointer-events", "none") + await expect(page.getByTestId("edge-preview")).toHaveAttribute("d", /^M /) + await page.getByTestId("add-edge").click() + await expect(page.getByTestId("feedback")).toHaveText("Edge added") + await expect(page.getByTestId("from-select")).toHaveValue("") + await expect(page.getByTestId("to-select")).toHaveValue("") + await expect(page.getByTestId("edge-preview")).toBeHidden() + + await page.getByTestId("from-select").selectOption(values[0]) + await page.getByTestId("to-select").selectOption(values[1]) + await page.getByTestId("add-edge").click() + await expect(page.getByTestId("graph-edge")).toHaveCount(2) + await expect(page.getByTestId("graph-counts")).toHaveText("3 VERTICES / 2 EDGES") + + const existingPaths = await page.getByTestId("graph-edge").evaluateAll(edges => + edges.map(edge => edge.getAttribute("d")), + ) + await page.getByTestId("from-select").selectOption(values[1]) + await page.getByTestId("to-select").selectOption(values[0]) + await expect(page.getByTestId("edge-preview")).toBeVisible() + expect(existingPaths).not.toContain(await page.getByTestId("edge-preview").getAttribute("d")) + + const selectedEdge = page.getByTestId("graph-edge").first() + await selectedEdge.dispatchEvent("click") + await expect(page.getByTestId("selected-edge-details")).toBeVisible() + await expect(selectedEdge).toHaveCSS("outline-style", "none") + await expect(selectedEdge).toHaveCSS("stroke", "rgb(101, 217, 231)") + await expect(selectedEdge).toHaveClass(/hover:stroke-\[#d8ff57\]/) + + await page.getByTestId("graph-canvas").dispatchEvent("click") + await expect(page.getByTestId("selected-edge-details")).toBeHidden() + + await expect(page.getByTestId("sync-status")).toHaveAttribute("data-status", "synced") + await expect.poll(() => nodeHandle.doc().scanTable("GraphRealm.E").length).toBe(2) + + const secondPage = await context.newPage() + await secondPage.goto(`/#${url}`) + await expect(secondPage.getByTestId("graph-vertex")).toHaveCount(3) + await expect(secondPage.getByTestId("graph-edge")).toHaveCount(2) + } finally { + await nodeRepo.shutdown() + await context.close() + } +}) + +test("shows a dedicated screen when a document is unavailable", async ({ browser }) => { + const isolatedRepo = new Repo() + const unavailableUrl = isolatedRepo.create().url + await isolatedRepo.shutdown() + const context = await browser.newContext() + const page = await context.newPage() + + try { + await page.goto(`/#${unavailableUrl}`) + + await expect(page.getByTestId("document-load-error")).toHaveAttribute("data-error-kind", "unavailable") + await expect(page.getByTestId("failed-document-url")).toHaveText(unavailableUrl) + await expect(page.getByTestId("graph-canvas")).toHaveCount(0) + await expect(page.getByTestId("add-vertex")).toHaveCount(0) + await page.getByRole("link", { name: "Create a new graph" }).click() + await expect(page.getByTestId("document-load-error")).toHaveCount(0) + await expect(page.getByTestId("graph-canvas")).toBeVisible() + await expect(page.getByTestId("doc-url")).toHaveValue(/^automerge:/) + expect(await page.getByTestId("doc-url").inputValue()).not.toBe(unavailableUrl) + } finally { + await context.close() + } +}) + +test("shows the document error screen for an invalid URL", async ({ page }) => { + const invalidUrl = "not-an-automerge-url" + + await page.goto(`/#${invalidUrl}`) + + await expect(page.getByTestId("document-load-error")).toHaveAttribute("data-error-kind", "invalid") + await expect(page.getByRole("heading")).toHaveText("This is not a valid Automerge document URL.") + await expect(page.getByTestId("failed-document-url")).toHaveText(invalidUrl) + await expect(page.getByTestId("graph-canvas")).toHaveCount(0) + await expect(page.getByTestId("add-vertex")).toHaveCount(0) +}) + +async function vertexLabels(page: import("@playwright/test").Page) { + const entries = await page.getByTestId("graph-vertex").evaluateAll(vertices => + vertices.map(vertex => [ + vertex.querySelector("title")?.textContent ?? "", + vertex.querySelector("text")?.textContent ?? "", + ] as const), + ) + return new Map(entries) +} diff --git a/examples/sync/tsconfig.json b/examples/sync/tsconfig.json new file mode 100644 index 00000000..830d03f9 --- /dev/null +++ b/examples/sync/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "types": ["vite/client", "node"] + }, + "include": ["src", "tests", "vite.config.ts", "vitest.config.ts", "playwright.config.ts"] +} diff --git a/examples/sync/vite.config.ts b/examples/sync/vite.config.ts new file mode 100644 index 00000000..d15c32c6 --- /dev/null +++ b/examples/sync/vite.config.ts @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { defineConfig } from "vite" +import { svelte } from "@sveltejs/vite-plugin-svelte" +import tailwindcss from "@tailwindcss/vite" +import wasm from "vite-plugin-wasm" + +export default defineConfig({ + base: process.env.VITE_BASE || "/", + plugins: [tailwindcss(), wasm(), svelte()], + resolve: { dedupe: ["@coln-project/runtime"] }, + build: { target: "esnext" }, +}) diff --git a/examples/sync/vitest.config.ts b/examples/sync/vitest.config.ts new file mode 100644 index 00000000..0bf62d85 --- /dev/null +++ b/examples/sync/vitest.config.ts @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { svelte } from "@sveltejs/vite-plugin-svelte" +import { playwright } from "@vitest/browser-playwright" +import { defineConfig } from "vitest/config" + +export default defineConfig({ + plugins: [svelte()], + resolve: { + conditions: ["browser"], + dedupe: ["svelte"], + }, + test: { + browser: { + enabled: true, + headless: true, + provider: playwright(), + instances: [{ browser: "chromium" }], + }, + include: ["tests/**/*.unit.ts"], + }, +}) From cb8a8f65e53ede48db4c19cc02305c936e72c02b Mon Sep 17 00:00:00 2001 From: Alex Currie-Clark Date: Thu, 3 Sep 2026 08:17:48 +0100 Subject: [PATCH 10/18] First proper pass at an agent targetted cli tool Co-authored-by: acurrieclark <1306728+acurrieclark@users.noreply.github.com> --- examples/cli-demo/README.md | 128 +++++++- examples/cli-demo/package.json | 7 +- examples/cli-demo/pnpm-lock.yaml | 14 +- examples/cli-demo/skills/coln-repo/SKILL.md | 128 ++++++++ examples/cli-demo/src/cli.ts | 26 +- examples/cli-demo/src/cli/arguments.ts | 79 +++-- examples/cli-demo/src/cli/commands/exec.ts | 13 +- examples/cli-demo/src/cli/commands/ir.ts | 4 +- examples/cli-demo/src/cli/commands/query.ts | 7 +- examples/cli-demo/src/cli/evaluation.ts | 61 +++- examples/cli-demo/src/cli/help.ts | 88 ++++-- examples/cli-demo/src/cli/main.ts | 233 +++++++++++++-- examples/cli-demo/src/cli/repository.ts | 21 ++ examples/cli-demo/src/cli/skill.ts | 78 +++++ examples/cli-demo/tests/cli/arguments.test.ts | 50 ---- .../cli-demo/tests/cli/evaluation.test.ts | 62 ---- examples/cli-demo/tests/sync.spec.ts | 277 +++++++++++++++--- 17 files changed, 1005 insertions(+), 271 deletions(-) create mode 100644 examples/cli-demo/skills/coln-repo/SKILL.md create mode 100644 examples/cli-demo/src/cli/skill.ts delete mode 100644 examples/cli-demo/tests/cli/arguments.test.ts delete mode 100644 examples/cli-demo/tests/cli/evaluation.test.ts diff --git a/examples/cli-demo/README.md b/examples/cli-demo/README.md index 658d06c7..69f2c0a9 100644 --- a/examples/cli-demo/README.md +++ b/examples/cli-demo/README.md @@ -1,7 +1,13 @@ -# Coln CLI demo +# Coln CLI Demo -`coln-repo` is a scriptable CLI for reading and updating an existing synchronized -Coln document. Unlike `examples/sync`, it has no fixed schema or interactive UI. +`coln-repo` is an experimental, JSON-only CLI for agents to read and update an +existing synchronized Coln document. It exposes the document's compiled IR +without interpreting its domain and evaluates trusted, synchronous JavaScript +for reads and transactions. + +Nothing in this protocol is stable yet. + +## Build Build local packages first: @@ -14,21 +20,119 @@ pnpm --dir examples/cli-demo install pnpm --dir examples/cli-demo build ``` -Run it with an Automerge URL: +The built binary is `examples/cli-demo/dist/cli.js`, exposed as `coln-repo` by +the package manifest. Put it on your `PATH` with: ```bash -node examples/cli-demo/dist/cli.js ir -node examples/cli-demo/dist/cli.js query 'store.scanTable("GraphRealm.V")' -node examples/cli-demo/dist/cli.js exec 'txn.add("GraphRealm.V", [])' +pnpm --dir examples/cli-demo link --global ``` -Use `-` as the query or script argument to read from stdin. `query` receives a -read-only `store`; `exec` receives an add-only `txn` and waits for the change to -sync before exiting. Set `SUBDUCTION_ENDPOINT` to use another WebSocket relay. +The examples below assume `coln-repo` is on `PATH`; otherwise substitute +`node examples/cli-demo/dist/cli.js`. + +## Agent Skill + +The model-facing workflow lives in [`skills/coln-repo/SKILL.md`](./skills/coln-repo/SKILL.md), +a portable skill (frontmatter plus markdown) that OpenCode, Claude Code, and +similar harnesses can load. Two ways to reach it: + +```bash +coln-repo guide # print the skill body as JSON +coln-repo install-skill # copy to ~/.agents/skills/coln-repo +coln-repo install-skill --dir ~/.claude/skills +``` + +`install-skill` reports `installed`, `updated`, or `unchanged`; rerun it after +upgrading the CLI to refresh the installed copy. No harness configuration or +permission rules are shipped; see Security below. + +## Commands + +Every command takes the document as a named flag. `--endpoint` overrides +`SUBDUCTION_ENDPOINT`; otherwise the public Subduction relay is used. + +```bash +coln-repo ir --document [--endpoint ] +``` + +`ir` returns the compiled JSON IR unchanged inside the response's `ir` field. +Agents should use it as structural evidence, not domain documentation. + +`query` reads one JavaScript expression from stdin. A read-only `store` is in +scope with `jsonIR()`, `scanTable(path)`, `rowById(path, rowRef)`, and `heads()`. + +```bash +coln-repo query --document automerge:... <<'JS' +store.scanTable("Records.Documents") +JS +``` + +`exec` reads a JavaScript function body from stdin. The current transaction API +is in scope as `txn`; today it exposes `txn.add(path, values)`. Return a +JSON-serializable value to include it in the response. + +```bash +coln-repo exec --document automerge:... <<'JS' +const folder = txn.add("Records.Folders", [{ tag: "string", value: "Inbox" }]) +const document = txn.add("Records.Documents", [ + folder, + { tag: "string", value: "Notes" }, +]) +return { folder, document } +JS +``` + +Values passed to `txn.add` are tagged: + +```js +{ tag: "string", value: "hello" } +{ tag: "int", value: 42 } +{ tag: "row_id", value: rowReference } +``` + +`txn.add` returns a complete tagged `row_id` value. Pass that value directly to +later `txn.add` calls; do not wrap it in another `{ tag: "row_id", ... }`. + +Programs must complete synchronously. Their return values must be JSON +serializable. Program `console` output is redirected to stderr so stdout remains +one JSON response. + +## Responses + +Every response contains `protocolVersion: 0`, `ok`, and the command where known. +Successful writes include heads before and after the local commit plus sync +status. Errors identify their phase and whether a mutation was applied locally. + +| Exit | Code | Phase | Mutation state | +|---|---|---|---| +| 2 | `INVALID_ARGUMENTS`, `PROGRAM_REQUIRED` | `arguments` | `not_applied` | +| 3 | `DOCUMENT_OPEN_FAILED` | `open` | `not_applied` | +| 4 | `IR_FAILED`, `QUERY_FAILED` | `read`, `evaluate` | `not_applied` | +| 4 | `GUIDE_FAILED`, `SKILL_INSTALL_FAILED` | `read`, `install` | `not_applied` | +| 5 | `TRANSACTION_FAILED` | `transaction` | `not_applied` | +| 5 | `RESULT_INVALID` | `result` | `not_applied` | +| 5 | `RESULT_FAILED` | `result` | `applied_locally` | +| 6 | `SYNC_FAILED` | `flush` | `applied_locally` | +| 1 | `INTERNAL_ERROR` | `internal` | `unknown` | + +`RESULT_INVALID` means the program's return value was not JSON; the transaction +was rolled back so the caller never loses the row references it asked for. + +If an `exec` response reports `mutationState: "applied_locally"`, do not retry it +automatically: the local commit succeeded, but relay synchronization or result +reporting failed afterwards. The CLI does not judge retryability of +`not_applied` failures; connection timeouts may be transient, schema rejections +are not. + +## Security -JavaScript is trusted local code and must complete synchronously. +`query` and `exec` evaluate trusted JavaScript with this Node process's +capabilities. Agent harnesses should request approval for them by default. +Allowing either command without approval is equivalent to allowing arbitrary +local Node execution. `ir`, `guide`, and `install-skill` do not evaluate user +JavaScript and may be allowlisted separately. -Verify with: +## Verify ```bash pnpm --dir examples/cli-demo test diff --git a/examples/cli-demo/package.json b/examples/cli-demo/package.json index 40dd022e..a19d6558 100644 --- a/examples/cli-demo/package.json +++ b/examples/cli-demo/package.json @@ -6,12 +6,15 @@ "bin": { "coln-repo": "./dist/cli.js" }, + "files": [ + "dist", + "skills" + ], "scripts": { "build": "tsc -p tsconfig.build.json", "cli": "tsx src/cli.ts", "typecheck": "tsc --noEmit", - "test": "pnpm run typecheck && pnpm run test:unit && pnpm run test:e2e", - "test:unit": "tsx --test \"tests/cli/**/*.test.ts\"", + "test": "pnpm run typecheck && pnpm run test:e2e", "test:e2e": "pnpm run build && playwright test" }, "dependencies": { diff --git a/examples/cli-demo/pnpm-lock.yaml b/examples/cli-demo/pnpm-lock.yaml index 1d1faeb1..3831a7cb 100644 --- a/examples/cli-demo/pnpm-lock.yaml +++ b/examples/cli-demo/pnpm-lock.yaml @@ -252,9 +252,6 @@ packages: '@types/node@25.9.5': resolution: {integrity: sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==} - '@types/node@26.2.0': - resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==} - '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -343,9 +340,6 @@ packages: undici-types@7.24.6: resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} - undici-types@8.3.0: - resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} - uuid@14.0.2: resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} hasBin: true @@ -495,13 +489,9 @@ snapshots: dependencies: undici-types: 7.24.6 - '@types/node@26.2.0': - dependencies: - undici-types: 8.3.0 - '@types/ws@8.18.1': dependencies: - '@types/node': 26.2.0 + '@types/node': 25.9.5 base-x@5.0.1: {} @@ -605,8 +595,6 @@ snapshots: undici-types@7.24.6: {} - undici-types@8.3.0: {} - uuid@14.0.2: {} ws@8.21.3: {} diff --git a/examples/cli-demo/skills/coln-repo/SKILL.md b/examples/cli-demo/skills/coln-repo/SKILL.md new file mode 100644 index 00000000..d522047a --- /dev/null +++ b/examples/cli-demo/skills/coln-repo/SKILL.md @@ -0,0 +1,128 @@ +--- +name: coln-repo +description: Read and update a synchronized Coln store from an automerge: URL with the coln-repo CLI. Use whenever a request names an automerge: document, a Coln store, realm, or theory, or asks to add, list, count, or connect records or table entries in one. +--- + +# Working With a Coln Store + +A Coln document is addressed by an `automerge:` URL and holds a store: a set of +tables whose paths come from a compiled theory, such as `Records.Documents`. The +`coln-repo` CLI reads and updates one such document. Every response is a single +JSON object on stdout, including errors. Run `coln-repo help` for usage and +`coln-repo guide` to print this document. + +## Procedure + +### 1. Resolve the sync endpoint + +Pass `--endpoint ` when the user names a relay. Otherwise the CLI uses +`SUBDUCTION_ENDPOINT`, then the public relay `wss://subduction.sync.inkandswitch.com`. +A document held by a local relay is only reachable there; do not invent one. If +the response is `DOCUMENT_OPEN_FAILED`, report which endpoint you tried and ask. + +### 2. Read the IR + +```bash +coln-repo ir --document [--endpoint ] +``` + +The `ir` field is the compiled JSON IR, unchanged. Each entity has a `path` +(render `[["Records"],["Documents"]]` as `Records.Documents`), ordered `columns` with a +`type` of either `{ tag: "builtin", type: "builtinInt" | "builtinString" }` or +`{ tag: "rowId", path }`, and an optional `primaryKey`. Rules describe +constraints the store enforces at commit, such as foreign keys. + +Treat the IR as structural evidence, not domain documentation. A table name or +abbreviation does not establish what its rows represent. + +### 3. Infer a mapping and confirm it + +Never assume that names or abbreviations carry a particular meaning. Before +writing, state the mapping you inferred and ask the user to confirm it, +including argument order when column names do not explain it: + +> The IR defines `Records.Folders` with a `name` string and +> `Records.Documents` with `folder` referencing `Records.Folders` followed by a +> `title` string. I take "add a document to Inbox" to mean adding or finding the +> Inbox folder, then adding a document as `[folderRowId, title]`. Is that right? + +If several entities plausibly match, ask; do not pick the closest name. Copy +entity paths exactly from the IR. + +### 4. Query before writing when the request refers to existing rows + +```bash +coln-repo query --document <<'JS' +store.scanTable("Records.Folders") +JS +``` + +`query` reads one synchronous JavaScript expression from stdin. `store` exposes +`scanTable(path)`, `rowById(path, rowRef)`, `heads()`, and `jsonIR()`. Rows are +`{ rowId, values }`; a `rowId` is already a tagged `row_id` value and can be +passed straight back as a column value in `txn.add`. `rowById` takes the inner +reference instead: `store.rowById(path, row.rowId.value)`. Project or slice +large tables in the expression rather than returning everything. + +### 5. Write in one transaction + +```bash +coln-repo exec --document <<'JS' +const folder = txn.add("Records.Folders", [{ tag: "string", value: "Inbox" }]) +const document = txn.add("Records.Documents", [ + folder, + { tag: "string", value: "Notes" }, +]) +return { folder, document } +JS +``` + +`exec` reads a synchronous JavaScript function body from stdin. `txn` is the +current transaction API; today it exposes `txn.add(path, values)`, which returns +a complete tagged `row_id` value usable directly in later `add` calls within +the same script. Do not wrap that result in another `row_id` value. Return a +JSON-serializable value: it is included in the response with row references +resolved to their committed form. + +Column values are tagged: + +```js +{ tag: "string", value: "hello" } +{ tag: "int", value: 42 } +{ tag: "row_id", value: rowReference } +``` + +### 6. Read the response before doing anything else + +Success: `ok: true`, `document.headsBefore` and `headsAfter`, `sync.status`, and +`result`. + +Failure: `ok: false` and `error` with `code`, `phase`, `message`, and +`mutationState`: + +- `not_applied`: nothing was committed. Fix the cause before trying again; a + schema or rule rejection will not pass on retry. +- `applied_locally`: the transaction committed but synchronization or result + reporting failed afterwards. Never repeat the transaction automatically; tell + the user what committed and what failed. +- `unknown`: internal failure; report it. + +### 7. Verify + +Confirm a write with a `query`, for example `store.scanTable(path).length`, and +quote the returned row references to the user. + +## Limits + +- Add-only: `txn.add` is the whole transaction API today. Say so rather than + simulating updates or deletes. +- Existing documents only: the CLI cannot create a document. +- Synchronous only: a program returning a promise is rejected. +- Trusted code: programs run with the Node process's full capabilities. + +## Permissions + +`ir`, `help`, and `guide` evaluate no user code and are safe to allow without +approval. `query` and `exec` evaluate arbitrary JavaScript; request approval for +them unless the user has explicitly accepted that allowing them is equivalent to +allowing arbitrary local Node execution. diff --git a/examples/cli-demo/src/cli.ts b/examples/cli-demo/src/cli.ts index 4c8add9a..0305b839 100644 --- a/examples/cli-demo/src/cli.ts +++ b/examples/cli-demo/src/cli.ts @@ -5,11 +5,23 @@ import { main } from "./cli/main.js" +ignoreBrokenPipe(process.stdout) +ignoreBrokenPipe(process.stderr) + let exitCode: number try { exitCode = await main(process.argv.slice(2)) } catch (error) { - console.error(error instanceof Error ? error.message : String(error)) + console.log(JSON.stringify({ + protocolVersion: 0, + ok: false, + error: { + code: "INTERNAL_ERROR", + phase: "internal", + message: error instanceof Error ? error.message : String(error), + mutationState: "unknown", + }, + }, null, 2)) exitCode = 1 } @@ -17,7 +29,15 @@ await Promise.all([drain(process.stdout), drain(process.stderr)]) process.exit(exitCode) function drain(stream: NodeJS.WritableStream): Promise { - return new Promise((resolve, reject) => { - stream.write("", error => error ? reject(error) : resolve()) + return new Promise(resolve => { + stream.once("error", () => resolve()) + stream.write("", () => resolve()) + }) +} + +// A consumer such as `head` may close a pipe while main is still running. +function ignoreBrokenPipe(stream: NodeJS.WritableStream): void { + stream.on("error", error => { + if ((error as NodeJS.ErrnoException).code !== "EPIPE") throw error }) } diff --git a/examples/cli-demo/src/cli/arguments.ts b/examples/cli-demo/src/cli/arguments.ts index e58281f1..ebff88b5 100644 --- a/examples/cli-demo/src/cli/arguments.ts +++ b/examples/cli-demo/src/cli/arguments.ts @@ -2,53 +2,92 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT -export type Command = "ir" | "query" | "exec" +export type DocumentCommand = "ir" | "query" | "exec" +export type Command = DocumentCommand | "guide" | "install-skill" export type ParsedArguments = | { kind: "help"; command?: Command } + | { kind: "guide" } + | { kind: "install-skill"; directory?: string } | { kind: "command" - command: Command + command: DocumentCommand documentUrl: string - source?: string verbose: boolean + endpoint?: string } +const documentCommands: DocumentCommand[] = ["ir", "query", "exec"] +const commands: Command[] = [...documentCommands, "guide", "install-skill"] + export function parseArguments(arguments_: string[]): ParsedArguments { const positional: string[] = [] let verbose = false let help = false - let optionsEnded = false + let documentUrl: string | undefined + let endpoint: string | undefined + let directory: string | undefined - for (const argument of arguments_) { - if (!optionsEnded && argument === "--") optionsEnded = true - else if (!optionsEnded && (argument === "-v" || argument === "--verbose")) verbose = true - else if (!optionsEnded && (argument === "-h" || argument === "--help")) help = true + for (let index = 0; index < arguments_.length; index += 1) { + const argument = arguments_[index] + if (argument === "-v" || argument === "--verbose") verbose = true + else if (argument === "-h" || argument === "--help") help = true + else if (argument.startsWith("--document=")) { + documentUrl = argument.slice("--document=".length) + } else if (argument === "--document") { + documentUrl = optionValue(arguments_, ++index, "--document requires an Automerge URL") + } else if (argument.startsWith("--endpoint=")) { + endpoint = argument.slice("--endpoint=".length) + if (endpoint === "") throw new Error("--endpoint requires a WebSocket URL") + } else if (argument === "--endpoint") { + endpoint = optionValue(arguments_, ++index, "--endpoint requires a WebSocket URL") + } else if (argument.startsWith("--dir=")) { + directory = argument.slice("--dir=".length) + if (directory === "") throw new Error("--dir requires a directory path") + } else if (argument === "--dir") { + directory = optionValue(arguments_, ++index, "--dir requires a directory path") + } else if (argument.startsWith("-")) throw new Error(`Unknown option: ${argument}`) else positional.push(argument) } - if (positional.length === 0 || positional[0] === "help") { - const command = positional[1] + const explicitHelp = help || positional[0] === "help" + if (explicitHelp || arguments_.length === 0) { + const command = positional[0] === "help" ? positional[1] : positional[0] if (command !== undefined && !isCommand(command)) { throw new Error(`Unknown command: ${command}`) } return { kind: "help", command } } - const [documentUrl, command, source, ...extra] = positional - if (!isCommand(command)) { - throw new Error(command === undefined ? "Missing command" : `Unknown command: ${command}`) - } - if (help) return { kind: "help", command } + const [command, ...extra] = positional + if (command === undefined) throw new Error("Missing command") + if (!isCommand(command)) throw new Error(`Unknown command: ${command}`) if (extra.length > 0) throw new Error(`Unexpected argument: ${extra[0]}`) - if (command !== "ir" && source === undefined) { - throw new Error(`${command} requires JavaScript or - for stdin`) + + if (command === "guide" || command === "install-skill") { + if (documentUrl !== undefined) throw new Error(`--document is not used by ${command}`) + if (endpoint !== undefined) throw new Error(`--endpoint is not used by ${command}`) + if (command === "guide") { + if (directory !== undefined) throw new Error("--dir is not used by guide") + return { kind: "guide" } + } + return { kind: "install-skill", directory } } - if (command === "ir" && source !== undefined) throw new Error(`Unexpected argument: ${source}`) - return { kind: "command", command, documentUrl, source, verbose } + if (directory !== undefined) throw new Error(`--dir is not used by ${command}`) + if (documentUrl === undefined || documentUrl === "") { + throw new Error("--document requires an Automerge URL") + } + + return { kind: "command", command, documentUrl, verbose, endpoint } } function isCommand(value: string | undefined): value is Command { - return value === "ir" || value === "query" || value === "exec" + return value !== undefined && (commands as string[]).includes(value) +} + +function optionValue(arguments_: string[], index: number, message: string): string { + const value = arguments_[index] + if (value === undefined || value.startsWith("-")) throw new Error(message) + return value } diff --git a/examples/cli-demo/src/cli/commands/exec.ts b/examples/cli-demo/src/cli/commands/exec.ts index 9bd6c86e..8ea4bd1a 100644 --- a/examples/cli-demo/src/cli/commands/exec.ts +++ b/examples/cli-demo/src/cli/commands/exec.ts @@ -2,9 +2,16 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT -import { evaluateExec } from "../evaluation.js" +import { evaluateExec, snapshotJsonResult } from "../evaluation.js" import type { ColnHandle } from "@coln-project/repo" -export function runExec(handle: ColnHandle, source: string): void { - handle.change(transaction => evaluateExec(transaction, source)) +export function runExec(handle: ColnHandle, source: string): unknown { + let result: unknown = null + + handle.change(transaction => { + result = evaluateExec(transaction, source) + }) + + // Commit resolves pending row references in place; snapshot only afterwards. + return snapshotJsonResult(result) } diff --git a/examples/cli-demo/src/cli/commands/ir.ts b/examples/cli-demo/src/cli/commands/ir.ts index 1720e174..5b62109f 100644 --- a/examples/cli-demo/src/cli/commands/ir.ts +++ b/examples/cli-demo/src/cli/commands/ir.ts @@ -4,6 +4,6 @@ import type { ColnHandle } from "@coln-project/repo" -export function runIr(handle: ColnHandle): void { - console.log(JSON.stringify(JSON.parse(handle.doc().jsonIR()), null, 2)) +export function runIr(handle: ColnHandle): unknown { + return JSON.parse(handle.doc().jsonIR()) } diff --git a/examples/cli-demo/src/cli/commands/query.ts b/examples/cli-demo/src/cli/commands/query.ts index 56b5c71e..73481aa3 100644 --- a/examples/cli-demo/src/cli/commands/query.ts +++ b/examples/cli-demo/src/cli/commands/query.ts @@ -5,9 +5,6 @@ import { evaluateQuery } from "../evaluation.js" import type { ColnHandle } from "@coln-project/repo" -export function runQuery(handle: ColnHandle, source: string): void { - const result = evaluateQuery(handle.doc(), source) - const json = JSON.stringify(result, null, 2) - if (json === undefined) throw new TypeError("Query result is not JSON-serializable") - console.log(json) +export function runQuery(handle: ColnHandle, source: string): unknown { + return evaluateQuery(handle.doc(), source) } diff --git a/examples/cli-demo/src/cli/evaluation.ts b/examples/cli-demo/src/cli/evaluation.ts index 3bf46002..9e7b013e 100644 --- a/examples/cli-demo/src/cli/evaluation.ts +++ b/examples/cli-demo/src/cli/evaluation.ts @@ -2,27 +2,45 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT +import { Console } from "node:console" import type { ColnDocument, ColnTransaction } from "@coln-project/repo" +/** A program produced a value the JSON protocol cannot carry. */ +export class InvalidResultError extends TypeError { + constructor() { + super("JavaScript result must be JSON-serializable") + this.name = "InvalidResultError" + } +} + export function evaluateQuery(store: ColnDocument, source: string): unknown { - const evaluate = new Function("store", `"use strict"; return (${source}\n)`) as ( + const evaluate = new Function("store", "console", `"use strict"; return (${source}\n)`) as ( store: ColnDocument, + console: Console, ) => unknown - const result = evaluate(store) + const result = evaluate(store, evaluationConsole) assertSynchronous(result) - return result + const json = jsonResult(result) + return snapshotJsonResult(json) } export function evaluateExec( transaction: ColnTransaction, source: string, -): void { - const evaluate = new Function("txn", `"use strict";\n${source}`) as ( +): unknown { + const evaluate = new Function("txn", "console", `"use strict";\n${source}`) as ( transaction: ColnTransaction, + console: Console, ) => unknown - assertSynchronous(evaluate(transaction)) + const result = evaluate(transaction, evaluationConsole) + assertSynchronous(result) + return jsonResult(result) } +// Keep program logging away from the JSON protocol on stdout: every console +// method, including table/dir/group, writes to stderr. +const evaluationConsole = new Console({ stdout: process.stderr, stderr: process.stderr }) + function assertSynchronous(value: unknown): void { if ( (typeof value === "object" || typeof value === "function") @@ -33,3 +51,34 @@ function assertSynchronous(value: unknown): void { throw new TypeError("JavaScript must complete synchronously") } } + +function jsonResult(value: unknown): unknown { + if (value === undefined) return null + assertJsonValue(value, new WeakSet()) + return value +} + +export function snapshotJsonResult(value: unknown): unknown { + return JSON.parse(JSON.stringify(value)) +} + +function assertJsonValue(value: unknown, ancestors: WeakSet): void { + if (value === null || typeof value === "string" || typeof value === "boolean") return + if (typeof value === "number" && Number.isFinite(value)) return + if (typeof value !== "object") throw new InvalidResultError() + if (ancestors.has(value)) throw new InvalidResultError() + + const prototype = Object.getPrototypeOf(value) + if (prototype !== Object.prototype && prototype !== Array.prototype && prototype !== null) { + throw new InvalidResultError() + } + if (Object.getOwnPropertySymbols(value).length > 0) throw new InvalidResultError() + + ancestors.add(value) + for (const descriptor of Object.values(Object.getOwnPropertyDescriptors(value))) { + if (!descriptor.enumerable) continue + if (!("value" in descriptor)) throw new InvalidResultError() + assertJsonValue(descriptor.value, ancestors) + } + ancestors.delete(value) +} diff --git a/examples/cli-demo/src/cli/help.ts b/examples/cli-demo/src/cli/help.ts index a9a2d1a1..c4dd6234 100644 --- a/examples/cli-demo/src/cli/help.ts +++ b/examples/cli-demo/src/cli/help.ts @@ -7,64 +7,98 @@ import type { Command } from "./arguments.js" const overview = `coln-repo reads and updates synchronized Coln documents. A Coln theory describes the entities and rules in a document. The compiler lowers -that theory to JSON IR. The runtime Store uses IR table paths and tagged values; -use "ir" first to discover those paths and column types. +that theory to JSON IR. Read the IR, confirm inferred domain meanings with the +user, then query or execute a transaction. Usage: - coln-repo ir [-v] - coln-repo query [-v] - coln-repo exec [-v] + coln-repo ir --document [--endpoint ] [-v] + coln-repo query --document [--endpoint ] [-v] + coln-repo exec --document [--endpoint ] [-v] + coln-repo guide + coln-repo install-skill [--dir ] coln-repo help [command] Commands: - ir Print compiled IR as formatted JSON - query Evaluate a read-only expression and print its result as JSON - exec Run a synchronous script in one change transaction + ir Return the full compiled JSON IR + query Read a JavaScript expression from stdin and evaluate it + exec Read a JavaScript body from stdin and run one change transaction + guide Print the agent workflow for this tool (the coln-repo skill) + install-skill Install or update the coln-repo skill for agent harnesses Options: - -v, --verbose Print connection and sync progress to stderr - -h, --help Print command help + --endpoint Sync server WebSocket URL, overriding SUBDUCTION_ENDPOINT + --dir Skills root for install-skill (default ~/.agents/skills) + -v, --verbose Print connection and sync progress to stderr + -h, --help Print command help Environment: - SUBDUCTION_ENDPOINT Sync server WebSocket URL + SUBDUCTION_ENDPOINT Sync server WebSocket URL, used when --endpoint is absent -JavaScript is trusted code and runs with this Node process's capabilities.` +The endpoint defaults to wss://subduction.sync.inkandswitch.com. A document held +by a local relay is only reachable by pointing at that relay, for example +--endpoint ws://127.0.0.1:3030. + +Agents: run "coln-repo guide" first. It explains how to read the IR, confirm +inferred meanings with the user, and interpret responses. + +Every result, including an error, is JSON on stdout. JavaScript is trusted code +and runs with this Node process's capabilities.` const commandHelp: Record = { - ir: `Usage: coln-repo ir [-v] + guide: `Usage: coln-repo guide + +Print the coln-repo skill body: the step-by-step workflow an agent should follow +to read a document's IR, confirm its interpretation, query, write, and read the +response. The same text is what install-skill installs.`, + "install-skill": `Usage: coln-repo install-skill [--dir ] -Print the document's compiled Coln IR as formatted JSON.`, - query: `Usage: coln-repo query [-v] +Copy the coln-repo skill directory to /coln-repo, creating it or +replacing it when the installed copy differs. The default root is +~/.agents/skills, which OpenCode reads. Other harnesses use their own root, for +example --dir ~/.claude/skills for Claude Code. + +The response reports status "installed", "updated", or "unchanged".`, + ir: `Usage: coln-repo ir --document [--endpoint ] [-v] + +Return the document's compiled JSON IR without interpreting its schema.`, + query: `Usage: coln-repo query --document [--endpoint ] [-v] Evaluate a synchronous JavaScript expression with a read-only store in scope. -The result is JSON-encoded on stdout. Use - to read the expression from stdin. +Read the expression from stdin and return its value in the JSON response. Store methods: store.jsonIR() Return the IR JSON string store.scanTable(path) Return all rows in an IR table - store.rowById(path, rowRef) Return one row, or undefined + store.rowById(path, rowRef) Return one row, or undefined (rowRef = row.rowId.value) store.heads() Return current commit hashes Example: - coln-repo automerge:... query 'store.scanTable("GraphRealm.V")'`, - exec: `Usage: coln-repo exec [-v] + printf 'store.scanTable("Records.Documents")' | coln-repo query --document automerge:...`, + exec: `Usage: coln-repo exec --document [--endpoint ] [-v] Run a synchronous JavaScript script in one atomic Coln Repo change. The script -receives an add-only transaction named txn. Use - to read the script from stdin. -Successful execution writes no stdout. +is read from stdin and receives the current transaction API as txn. Return a +JSON-serializable value to include it in the response. Available operations: txn.add(path, values) Add a row and return its tagged row reference +Values are tagged: { tag: "int", value } | { tag: "string", value } | +{ tag: "row_id", value: rowRef }. +txn.add returns a complete tagged row_id value; pass it directly to later adds. + Example: - coln-repo automerge:... exec ' - const a = txn.add("GraphRealm.V", []) - const b = txn.add("GraphRealm.V", []) - txn.add("GraphRealm.E", [a, b]) - ' + coln-repo exec --document automerge:... <<'JS' + const folder = txn.add("Records.Folders", [{ tag: "string", value: "Inbox" }]) + const document = txn.add("Records.Documents", [ + folder, + { tag: "string", value: "Notes" }, + ]) + return { folder, document } +JS The change is committed only if the script returns synchronously without error. -The CLI waits for the connected server to flush it before exiting successfully.`, +The CLI waits for the connected server to flush it before reporting success.`, } export function helpText(command?: Command): string { diff --git a/examples/cli-demo/src/cli/main.ts b/examples/cli-demo/src/cli/main.ts index 573c88d0..96f22e6d 100644 --- a/examples/cli-demo/src/cli/main.ts +++ b/examples/cli-demo/src/cli/main.ts @@ -2,49 +2,234 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT -import { parseArguments } from "./arguments.js" +import { join } from "node:path" +import { parseArguments, type Command } from "./arguments.js" import { runExec } from "./commands/exec.js" import { runIr } from "./commands/ir.js" import { runQuery } from "./commands/query.js" +import { InvalidResultError } from "./evaluation.js" import { helpText } from "./help.js" import { defaultEndpoint, flush, openDocument } from "./repository.js" +import { defaultSkillInstallRoot, installSkill, readGuide, skillName } from "./skill.js" + +const protocolVersion = 0 export async function main(arguments_: string[]): Promise { - const argumentsResult = parseArguments(arguments_) + let argumentsResult + try { + argumentsResult = parseArguments(arguments_) + } catch (error) { + writeJson(failure(undefined, "INVALID_ARGUMENTS", "arguments", error, "not_applied")) + return 2 + } + if (argumentsResult.kind === "help") { - console.log(helpText(argumentsResult.command)) + writeJson({ + protocolVersion, + ok: true, + command: "help", + help: helpText(argumentsResult.command), + }) return 0 } - const endpoint = process.env.SUBDUCTION_ENDPOINT ?? defaultEndpoint - const source = argumentsResult.source === "-" - ? await readStdin() - : argumentsResult.source - const { repo, handle } = await openDocument( - argumentsResult.documentUrl, - endpoint, - argumentsResult.verbose, - ) + if (argumentsResult.kind === "guide") { + try { + writeJson({ ...success("guide"), guide: readGuide() }) + return 0 + } catch (error) { + writeJson(failure("guide", "GUIDE_FAILED", "read", error, "not_applied")) + return 4 + } + } + + if (argumentsResult.kind === "install-skill") { + const root = argumentsResult.directory ?? defaultSkillInstallRoot + try { + const installed = installSkill(root) + writeJson({ ...success("install-skill"), skill: { name: skillName, ...installed } }) + return 0 + } catch (error) { + writeJson({ + ...failure("install-skill", "SKILL_INSTALL_FAILED", "install", error, "not_applied"), + skill: { name: skillName, target: join(root, skillName) }, + }) + return 4 + } + } + const { command, documentUrl, verbose } = argumentsResult + const endpoint = argumentsResult.endpoint ?? process.env.SUBDUCTION_ENDPOINT ?? defaultEndpoint + let source: string | undefined + if (command === "query" || command === "exec") { + if (process.stdin.isTTY) { + writeJson(failure(command, "PROGRAM_REQUIRED", "arguments", new Error( + `${command} reads JavaScript from stdin; pipe or redirect a program into it`, + ), "not_applied")) + return 2 + } + source = await readStdin() + if (source.trim() === "") { + writeJson(failure(command, "PROGRAM_REQUIRED", "arguments", new Error( + `${command} requires JavaScript on stdin`, + ), "not_applied")) + return 2 + } + } + + let opened try { - switch (argumentsResult.command) { - case "ir": - runIr(handle) - break - case "query": - runQuery(handle, source!) - break - case "exec": - runExec(handle, source!) - await flush(repo, endpoint, argumentsResult.verbose) - break + opened = await openDocument(documentUrl, endpoint, verbose) + } catch (error) { + writeJson({ + ...failure(command, "DOCUMENT_OPEN_FAILED", "open", error, "not_applied"), + document: { url: documentUrl, endpoint }, + }) + return 3 + } + + const { repo, handle } = opened + try { + if (command === "ir") { + let heads: string[] | undefined + try { + heads = handle.doc().heads() + writeJson({ + ...success(command), + document: documentState(documentUrl, endpoint, heads), + ir: runIr(handle), + }) + return 0 + } catch (error) { + writeJson({ + ...failure(command, "IR_FAILED", "read", error, "not_applied"), + document: heads === undefined + ? { url: documentUrl, endpoint } + : documentState(documentUrl, endpoint, heads), + }) + return 4 + } + } + + if (command === "query") { + let heads: string[] | undefined + try { + heads = handle.doc().heads() + const result = runQuery(handle, source!) + writeJson({ + ...success(command), + document: documentState(documentUrl, endpoint, heads), + result, + }) + return 0 + } catch (error) { + writeJson({ + ...failure(command, "QUERY_FAILED", "evaluate", error, "not_applied"), + document: heads === undefined + ? { url: documentUrl, endpoint } + : documentState(documentUrl, endpoint, heads), + }) + return 4 + } + } + + const headsBefore = handle.doc().heads() + let result: unknown + try { + result = runExec(handle, source!) + } catch (error) { + const headsAfter = handle.doc().heads() + // Three failure shapes: the transaction rejected the program (rolled back), + // the program's return value is not JSON (rolled back before commit), or + // snapshotting the value after commit failed (committed locally). + const applied = !sameHeads(headsBefore, headsAfter) + const invalidResult = error instanceof InvalidResultError + writeJson({ + ...failure( + command, + applied ? "RESULT_FAILED" : invalidResult ? "RESULT_INVALID" : "TRANSACTION_FAILED", + applied || invalidResult ? "result" : "transaction", + error, + applied ? "applied_locally" : "not_applied", + ), + document: transactionState(documentUrl, endpoint, headsBefore, headsAfter), + }) + return 5 + } + + const headsAfter = handle.doc().heads() + try { + await flush(repo, endpoint, verbose) + } catch (error) { + writeJson({ + ...failure(command, "SYNC_FAILED", "flush", error, "applied_locally"), + document: transactionState(documentUrl, endpoint, headsBefore, headsAfter), + result, + }) + return 6 } + + writeJson({ + ...success(command), + document: transactionState(documentUrl, endpoint, headsBefore, headsAfter), + sync: { status: "flushed" }, + result, + }) return 0 } finally { - await repo.shutdown() + await repo.shutdown().catch(error => console.error(errorMessage(error))) + } +} + +function success(command: Command): object { + return { protocolVersion, ok: true, command } +} + +function failure( + command: Command | undefined, + code: string, + phase: string, + error: unknown, + mutationState: "not_applied" | "applied_locally", +): object { + return { + protocolVersion, + ok: false, + ...(command === undefined ? {} : { command }), + error: { + code, + phase, + message: errorMessage(error), + mutationState, + }, } } +function documentState(url: string, endpoint: string, heads: string[]): object { + return { url, endpoint, heads } +} + +function transactionState( + url: string, + endpoint: string, + headsBefore: string[], + headsAfter: string[], +): object { + return { url, endpoint, headsBefore, headsAfter } +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error) +} + +function sameHeads(left: string[], right: string[]): boolean { + return left.length === right.length && left.every(head => right.includes(head)) +} + +function writeJson(value: unknown): void { + console.log(JSON.stringify(value, null, 2)) +} + async function readStdin(): Promise { process.stdin.setEncoding("utf8") let source = "" diff --git a/examples/cli-demo/src/cli/repository.ts b/examples/cli-demo/src/cli/repository.ts index 6b852cb3..82077868 100644 --- a/examples/cli-demo/src/cli/repository.ts +++ b/examples/cli-demo/src/cli/repository.ts @@ -6,6 +6,8 @@ import { initSubduction, isValidAutomergeUrl, Repo, + setLoggerFactory, + setSubductionLogLevel, type AutomergeUrl, } from "@automerge/automerge-repo" import { find, type ColnHandle } from "@coln-project/repo" @@ -28,7 +30,11 @@ export async function openDocument( if (verbose) console.error(`Connecting to ${endpoint}`) await initSubduction() + configureLogging(verbose) const repo = new Repo({ subductionWebsocketEndpoints: [endpoint] }) + // Repo construction resets this to warn. Subduction tracing writes to stdout, + // so keep it at error; CLI-owned verbose progress uses stderr. + setSubductionLogLevel("error") try { await withTimeout(waitForConnection(repo), `Timed out connecting to ${endpoint}`) if (verbose) console.error(`Connected to ${endpoint}`) @@ -43,6 +49,21 @@ export async function openDocument( } } +function configureLogging(verbose: boolean): void { + setLoggerFactory(namespace => { + const write = (message: string, ...args: unknown[]) => { + console.error(`[${namespace}]`, message, ...args) + } + const quiet = () => {} + return { + debug: verbose ? write : quiet, + info: verbose ? write : quiet, + warn: verbose ? write : quiet, + error: write, + } + }) +} + export async function flush(repo: Repo, endpoint: string, verbose: boolean): Promise { if (!repo.isSubductionConnected()) throw new Error(`Disconnected from ${endpoint} before sync`) await withTimeout(repo.flush(), `Timed out syncing to ${endpoint}`) diff --git a/examples/cli-demo/src/cli/skill.ts b/examples/cli-demo/src/cli/skill.ts new file mode 100644 index 00000000..442fd094 --- /dev/null +++ b/examples/cli-demo/src/cli/skill.ts @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs" +import { homedir } from "node:os" +import { join, relative } from "node:path" +import { fileURLToPath } from "node:url" + +export const skillName = "coln-repo" + +/** + * The skill directory ships beside `src/` and `dist/`, so the same relative + * path resolves whether this module runs from source (tsx) or from the build. + */ +export const skillSourceDirectory = fileURLToPath( + new URL(`../../skills/${skillName}/`, import.meta.url), +) + +export const defaultSkillInstallRoot = join(homedir(), ".agents", "skills") + +/** Body of SKILL.md with its YAML frontmatter removed. */ +export function readGuide(): string { + const text = readFileSync(join(skillSourceDirectory, "SKILL.md"), "utf8") + const match = /^---\r?\n[\s\S]*?\r?\n---\r?\n/.exec(text) + return (match === null ? text : text.slice(match[0].length)).trim() +} + +export type InstallStatus = "installed" | "updated" | "unchanged" + +export interface InstallResult { + status: InstallStatus + source: string + target: string + files: string[] +} + +/** Copy the skill directory to `/`, replacing stale content. */ +export function installSkill(root: string): InstallResult { + const target = join(root, skillName) + const files = listFiles(skillSourceDirectory) + const existed = existsSync(target) + const status: InstallStatus = !existed + ? "installed" + : sameContent(skillSourceDirectory, target, files) ? "unchanged" : "updated" + + if (status !== "unchanged") { + // Replace, don't merge: files dropped from the skill must not linger. + rmSync(target, { recursive: true, force: true }) + mkdirSync(target, { recursive: true }) + cpSync(skillSourceDirectory, target, { recursive: true, force: true }) + } + + return { status, source: skillSourceDirectory, target, files } +} + +function listFiles(directory: string, prefix = directory): string[] { + const files: string[] = [] + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const path = join(directory, entry.name) + if (entry.isDirectory()) files.push(...listFiles(path, prefix)) + else files.push(relative(prefix, path)) + } + return files.sort() +} + +function sameContent(source: string, target: string, files: string[]): boolean { + if (!existsSync(target) || !statSync(target).isDirectory()) return false + const targetFiles = listFiles(target) + if (targetFiles.length !== files.length || targetFiles.some((file, index) => file !== files[index])) { + return false + } + return files.every(file => { + const targetPath = join(target, file) + if (!existsSync(targetPath) || !statSync(targetPath).isFile()) return false + return readFileSync(join(source, file)).equals(readFileSync(targetPath)) + }) +} diff --git a/examples/cli-demo/tests/cli/arguments.test.ts b/examples/cli-demo/tests/cli/arguments.test.ts deleted file mode 100644 index e9b567e4..00000000 --- a/examples/cli-demo/tests/cli/arguments.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// -// SPDX-License-Identifier: Apache-2.0 OR MIT - -import assert from "node:assert/strict" -import test from "node:test" -import { parseArguments } from "../../src/cli/arguments.js" - -test("parses commands and flags in any position", () => { - assert.deepEqual(parseArguments(["-v", "automerge:abc", "query", "1 + 1"]), { - kind: "command", - command: "query", - documentUrl: "automerge:abc", - source: "1 + 1", - verbose: true, - }) -}) - -test("parses command help", () => { - assert.deepEqual(parseArguments(["automerge:abc", "exec", "--help"]), { - kind: "help", - command: "exec", - }) - assert.deepEqual(parseArguments(["help", "query"]), { - kind: "help", - command: "query", - }) -}) - -test("double dash allows JavaScript matching an option", () => { - assert.deepEqual(parseArguments(["automerge:abc", "query", "--", "--verbose"]), { - kind: "command", - command: "query", - documentUrl: "automerge:abc", - source: "--verbose", - verbose: false, - }) -}) - -test("requires JavaScript for query and exec", () => { - assert.throws(() => parseArguments(["automerge:abc", "exec"]), /requires JavaScript/) -}) - -test("rejects unknown commands and extra arguments", () => { - assert.throws(() => parseArguments(["automerge:abc", "nope"]), /Unknown command/) - assert.throws( - () => parseArguments(["automerge:abc", "query", "1", "2"]), - /Unexpected argument/, - ) -}) diff --git a/examples/cli-demo/tests/cli/evaluation.test.ts b/examples/cli-demo/tests/cli/evaluation.test.ts deleted file mode 100644 index ad724954..00000000 --- a/examples/cli-demo/tests/cli/evaluation.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// -// SPDX-License-Identifier: Apache-2.0 OR MIT - -import assert from "node:assert/strict" -import test from "node:test" -import type { ColnDocument, ColnTransaction } from "@coln-project/repo" -import { evaluateExec, evaluateQuery } from "../../src/cli/evaluation.js" - -test("query evaluates against a read-only store", () => { - const document = { - heads: () => [], - jsonIR: () => "{}", - rowById: () => undefined, - scanTable: () => [{ values: [{ tag: "string", value: "one" }] }], - } as unknown as ColnDocument - - const result = evaluateQuery( - document, - `({ keys: Object.keys(store).sort(), values: store.scanTable("Test.Items").map(row => row.values[0].value) })`, - ) - - assert.deepEqual(result, { - keys: ["heads", "jsonIR", "rowById", "scanTable"], - values: ["one"], - }) -}) - -test("exec writes through its transaction", () => { - const values: string[] = [] - const transaction = { - add: (_path: string, rows: { value: string }[]) => values.push(rows[0].value), - } as unknown as ColnTransaction - - evaluateExec(transaction, `txn.add("Test.Items", [{ tag: "string", value: "one" }])`) - assert.deepEqual(values, ["one"]) -}) - -test("exec receives the current transaction API", () => { - const transaction = { add: () => undefined } as ColnTransaction - - evaluateExec(transaction, ` - if (JSON.stringify(Object.keys(txn)) !== '["add"]') throw new Error("unexpected API") - txn.add("Test.Items", [{ tag: "string", value: "one" }]) - `) -}) - -test("query and exec reject promises", () => { - const document = { - heads: () => [], - jsonIR: () => "{}", - rowById: () => undefined, - scanTable: () => [], - } as ColnDocument - assert.throws(() => evaluateQuery(document, "Promise.resolve(1)"), /synchronously/) - - const transaction = { add: () => undefined } as ColnTransaction - assert.throws( - () => evaluateExec(transaction, "return Promise.resolve()"), - /synchronously/, - ) -}) diff --git a/examples/cli-demo/tests/sync.spec.ts b/examples/cli-demo/tests/sync.spec.ts index 9916dd76..9fd3c2da 100644 --- a/examples/cli-demo/tests/sync.spec.ts +++ b/examples/cli-demo/tests/sync.spec.ts @@ -3,70 +3,263 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT import { expect, test } from "@playwright/test" -import { execFile } from "node:child_process" -import { promisify } from "node:util" +import { spawn } from "node:child_process" +import { existsSync, mkdtempSync, rmSync } from "node:fs" +import { tmpdir } from "node:os" +import { join } from "node:path" import { initSubduction, Repo } from "@automerge/automerge-repo" import { colnDocType, type ColnSchema, type ColnTransaction } from "@coln-project/repo" -const execFileAsync = promisify(execFile) -const table = "Test.Items" +const endpoint = "ws://127.0.0.1:3031" +const peopleTable = "Scheduling.People" +const meetingsTable = "Scheduling.Meetings" const schema = { - entities: [{ - path: [["Test"], ["Items"]], - value: { - entityVariant: { tag: "table" }, - columns: [{ - path: [["value"]], - type: { tag: "builtin", type: "builtinString" }, - }], - primaryKey: null, + entities: [ + { + path: [["Scheduling"], ["People"]], + value: { + entityVariant: { tag: "table" }, + columns: [{ + path: [["name"]], + type: { tag: "builtin", type: "builtinString" }, + }], + primaryKey: null, + }, + }, + { + path: [["Scheduling"], ["Meetings"]], + value: { + entityVariant: { tag: "table" }, + columns: [ + { + path: [["organizer"]], + type: { tag: "rowId", path: [["Scheduling"], ["People"]] }, + }, + { + path: [["attendee"]], + type: { tag: "rowId", path: [["Scheduling"], ["People"]] }, + }, + ], + primaryKey: null, + }, }, - }], + ], rules: [], } satisfies ColnSchema -test("CLI exec flushes its change before exiting", async () => { +interface CliResult { + exitCode: number | null + stdout: string + stderr: string +} + +function runCli(arguments_: string[], input = ""): Promise { + return new Promise((resolve, reject) => { + const child = spawn(process.execPath, ["dist/cli.js", ...arguments_], { + cwd: process.cwd(), + env: process.env, + timeout: 15_000, + }) + let stdout = "" + let stderr = "" + child.stdout.setEncoding("utf8").on("data", chunk => { stdout += chunk }) + child.stderr.setEncoding("utf8").on("data", chunk => { stderr += chunk }) + child.on("error", reject) + child.on("close", exitCode => resolve({ exitCode, stdout, stderr })) + child.stdin.end(input) + }) +} + +function documentArguments(command: string, documentUrl: string, verbose = false): string[] { + return [ + command, + "--document", + documentUrl, + "--endpoint", + endpoint, + ...(verbose ? ["--verbose"] : []), + ] +} + +test("CLI provides help, guide, and skill installation", async () => { + const help = await runCli(["help", "exec"]) + expect(help.exitCode).toBe(0) + expect(JSON.parse(help.stdout)).toMatchObject({ + protocolVersion: 0, + ok: true, + command: "help", + help: expect.any(String), + }) + + const guide = await runCli(["guide"]) + expect(guide.exitCode).toBe(0) + expect(JSON.parse(guide.stdout)).toMatchObject({ + protocolVersion: 0, + ok: true, + command: "guide", + guide: expect.any(String), + }) + + const root = mkdtempSync(join(tmpdir(), "coln-skills-")) + try { + const installed = await runCli(["install-skill", "--dir", root]) + expect(installed.exitCode).toBe(0) + expect(JSON.parse(installed.stdout)).toMatchObject({ + protocolVersion: 0, + ok: true, + command: "install-skill", + skill: { + name: "coln-repo", + status: "installed", + target: join(root, "coln-repo"), + files: expect.arrayContaining(["SKILL.md"]), + }, + }) + expect(existsSync(join(root, "coln-repo", "SKILL.md"))).toBe(true) + } finally { + rmSync(root, { recursive: true, force: true }) + } +}) + +test("CLI reads a store's IR and rows", async () => { await initSubduction() - const endpoint = "ws://127.0.0.1:3031" const creator = new Repo({ subductionWebsocketEndpoints: [endpoint] }) - const verifier = new Repo({ subductionWebsocketEndpoints: [endpoint] }) try { const handle = creator.create(schema, colnDocType) handle.change((transaction: ColnTransaction) => { - transaction.add(table, [{ tag: "string", value: "before" }]) + transaction.add(peopleTable, [{ tag: "string", value: "Ada" }]) }) await creator.flush() - const { stdout, stderr } = await execFileAsync( - process.execPath, - [ - "dist/cli.js", - handle.url, - "exec", - `txn.add("${table}", [{ tag: "string", value: "from-cli" }])`, - "--verbose", - ], - { - cwd: process.cwd(), - env: { ...process.env, SUBDUCTION_ENDPOINT: endpoint }, - timeout: 15_000, - }, + const ir = await runCli(documentArguments("ir", handle.url)) + expect(ir.exitCode).toBe(0) + expect(ir.stderr).toBe("") + const irResponse = JSON.parse(ir.stdout) + expect(irResponse).toMatchObject({ + protocolVersion: 0, + ok: true, + command: "ir", + document: { url: handle.url, endpoint, heads: expect.any(Array) }, + ir: schema, + }) + expect(irResponse.document.heads.length).toBeGreaterThan(0) + + const query = await runCli( + documentArguments("query", handle.url), + `store.scanTable("${peopleTable}").map(row => row.values[0].value)`, ) + expect(query.exitCode).toBe(0) + const queryResponse = JSON.parse(query.stdout) + expect(queryResponse).toMatchObject({ + protocolVersion: 0, + ok: true, + command: "query", + document: { url: handle.url, endpoint, heads: expect.any(Array) }, + result: ["Ada"], + }) + expect(queryResponse.document.heads.length).toBeGreaterThan(0) + } finally { + await creator.shutdown() + } +}) + +test("CLI commits dependent rows in one transaction", async () => { + await initSubduction() + const creator = new Repo({ subductionWebsocketEndpoints: [endpoint] }) + const verifier = new Repo({ subductionWebsocketEndpoints: [endpoint] }) - expect(stdout).toBe("") - expect(stderr).toContain(`Connecting to ${endpoint}`) - expect(stderr).toContain(`Synced to ${endpoint}`) + try { + const handle = creator.create(schema, colnDocType) + await creator.flush() + + const result = await runCli(documentArguments("exec", handle.url, true), ` + const organizer = txn.add("${peopleTable}", [{ tag: "string", value: "Ada" }]) + const attendee = txn.add("${peopleTable}", [{ tag: "string", value: "Lin" }]) + const meeting = txn.add("${meetingsTable}", [organizer, attendee]) + console.log("scheduled") + return { organizer, attendee, meeting } + `) + expect(result.exitCode).toBe(0) + expect(result.stderr).toContain("scheduled") + const response = JSON.parse(result.stdout) + expect(response).toMatchObject({ + protocolVersion: 0, + ok: true, + command: "exec", + document: { + url: handle.url, + endpoint, + headsBefore: expect.any(Array), + headsAfter: expect.any(Array), + }, + sync: { status: "flushed" }, + }) + expect(response.document.headsBefore.length).toBeGreaterThan(0) + expect(response.document.headsAfter.length).toBeGreaterThan(0) + expect(response.document.headsAfter).not.toEqual(response.document.headsBefore) + expect(response.result.organizer.value.existing.commit).toBe(response.document.headsAfter[0]) + expect(response.result.attendee.value.existing.commit).toBe(response.document.headsAfter[0]) + expect(response.result.meeting.value.existing.commit).toBe(response.document.headsAfter[0]) const verified = await verifier.find(handle.url, colnDocType) - await expect.poll(() => - verified.doc().scanTable(table) - .map(row => row.values[0]) - .filter(value => value?.tag === "string") - .map(value => value.value) - .sort(), - ).toEqual(["before", "from-cli"]) + await expect.poll(() => verified.doc().scanTable(meetingsTable).length).toBe(1) + expect(verified.doc().scanTable(peopleTable)).toHaveLength(2) + expect(verified.doc().scanTable(meetingsTable)[0].values).toEqual([ + response.result.organizer, + response.result.attendee, + ]) } finally { await Promise.all([creator.shutdown(), verifier.shutdown()]) } }) + +test("CLI reports a failed transaction without changing the store", async () => { + await initSubduction() + const creator = new Repo({ subductionWebsocketEndpoints: [endpoint] }) + + try { + const handle = creator.create(schema, colnDocType) + await creator.flush() + + const result = await runCli( + documentArguments("exec", handle.url), + 'txn.add("Scheduling.Missing", [])', + ) + expect(result.exitCode).not.toBe(0) + const response = JSON.parse(result.stdout) + expect(response).toMatchObject({ + protocolVersion: 0, + ok: false, + command: "exec", + document: { + url: handle.url, + endpoint, + headsBefore: expect.any(Array), + headsAfter: expect.any(Array), + }, + error: { + code: "TRANSACTION_FAILED", + message: expect.any(String), + mutationState: "not_applied", + }, + }) + expect(response.error.message.length).toBeGreaterThan(0) + expect(response.document.headsAfter).toEqual(response.document.headsBefore) + + const query = await runCli( + documentArguments("query", handle.url), + `store.scanTable("${peopleTable}").length`, + ) + expect(query.exitCode).toBe(0) + expect(JSON.parse(query.stdout)).toMatchObject({ + protocolVersion: 0, + ok: true, + command: "query", + document: { url: handle.url, endpoint, heads: response.document.headsBefore }, + result: 0, + }) + } finally { + await creator.shutdown() + } +}) From c71d539a085c2a8a5757e6eca11e41c8aa5419c4 Mon Sep 17 00:00:00 2001 From: Alex Currie-Clark Date: Thu, 3 Sep 2026 17:57:47 +0100 Subject: [PATCH 11/18] First pass at a compiler demo Co-authored-by: acurrieclark <1306728+acurrieclark@users.noreply.github.com> --- examples/compiler/.gitignore | 4 + examples/compiler/index.html | 19 + examples/compiler/package.json | 23 + examples/compiler/pnpm-lock.yaml | 1045 +++++++++++++++++ examples/compiler/pnpm-workspace.yaml | 2 + examples/compiler/src/browser/App.svelte | 219 ++++ .../compiler/src/browser/OutputPanel.svelte | 18 + .../compiler/src/browser/SourceEditor.svelte | 24 + examples/compiler/src/lib/compiler.ts | 62 + examples/compiler/src/lib/examples.ts | 17 + examples/compiler/src/main.ts | 9 + examples/compiler/src/style.css | 45 + examples/compiler/tsconfig.json | 20 + examples/compiler/vite.config.ts | 17 + examples/justfile | 20 + 15 files changed, 1544 insertions(+) create mode 100644 examples/compiler/.gitignore create mode 100644 examples/compiler/index.html create mode 100644 examples/compiler/package.json create mode 100644 examples/compiler/pnpm-lock.yaml create mode 100644 examples/compiler/pnpm-workspace.yaml create mode 100644 examples/compiler/src/browser/App.svelte create mode 100644 examples/compiler/src/browser/OutputPanel.svelte create mode 100644 examples/compiler/src/browser/SourceEditor.svelte create mode 100644 examples/compiler/src/lib/compiler.ts create mode 100644 examples/compiler/src/lib/examples.ts create mode 100644 examples/compiler/src/main.ts create mode 100644 examples/compiler/src/style.css create mode 100644 examples/compiler/tsconfig.json create mode 100644 examples/compiler/vite.config.ts diff --git a/examples/compiler/.gitignore b/examples/compiler/.gitignore new file mode 100644 index 00000000..17948265 --- /dev/null +++ b/examples/compiler/.gitignore @@ -0,0 +1,4 @@ +dist/ +node_modules/ +public/dist/ +public/examples/ diff --git a/examples/compiler/index.html b/examples/compiler/index.html new file mode 100644 index 00000000..5819e31b --- /dev/null +++ b/examples/compiler/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + Coln Compiler + + +
+ + + diff --git a/examples/compiler/package.json b/examples/compiler/package.json new file mode 100644 index 00000000..de161517 --- /dev/null +++ b/examples/compiler/package.json @@ -0,0 +1,23 @@ +{ + "name": "coln-compiler-example", + "private": true, + "type": "module", + "license": "(Apache-2.0 OR MIT)", + "scripts": { + "dev": "vite --host 127.0.0.1", + "check": "svelte-check --tsconfig ./tsconfig.json && tsc --noEmit", + "build": "pnpm run check && vite build" + }, + "dependencies": { + "svelte": "^5.57.0" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^7.3.0", + "@tailwindcss/vite": "^4.3.3", + "@types/node": "^26.4.1", + "svelte-check": "^4.7.6", + "tailwindcss": "^4.3.3", + "typescript": "^6.0.3", + "vite": "^8.2.2" + } +} diff --git a/examples/compiler/pnpm-lock.yaml b/examples/compiler/pnpm-lock.yaml new file mode 100644 index 00000000..2533f4b7 --- /dev/null +++ b/examples/compiler/pnpm-lock.yaml @@ -0,0 +1,1045 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + svelte: + specifier: ^5.57.0 + version: 5.57.0 + devDependencies: + '@sveltejs/vite-plugin-svelte': + specifier: ^7.3.0 + version: 7.3.0(svelte@5.57.0)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + '@tailwindcss/vite': + specifier: ^4.3.3 + version: 4.3.3(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + '@types/node': + specifier: ^26.4.1 + version: 26.4.1 + svelte-check: + specifier: ^4.7.6 + version: 4.7.6(picomatch@4.0.7)(svelte@5.57.0)(typescript@6.0.3) + tailwindcss: + specifier: ^4.3.3 + version: 4.3.3 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vite: + specifier: ^8.2.2 + version: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + +packages: + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@oxc-project/types@0.148.0': + resolution: {integrity: sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==} + + '@rolldown/binding-android-arm-eabi@1.2.7': + resolution: {integrity: sha512-EypzgnYCwyVY4NDHKzGmNJT5b+XaQEBniHxsMdeIQLB/tcCzZnhqrzHpZFbX9iaxx+5RiB8caATBtfvZP7zVxQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.7': + resolution: {integrity: sha512-l17HE9EweWaqJZhuUuNBN/FzM62xw+DECVnJyvMsxn8vJFAGLy5QfLDoYAcronkAN8VxKZHezDpulHDPx95vFw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.7': + resolution: {integrity: sha512-8ED8ELFvHXc6OCETIn4gXObPiaR6bckM/ipXtbzlPVDRMBfEGjCKgO90F9YtfdpDatVx/ZQw7aZ1vUMf/+T3Mw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.7': + resolution: {integrity: sha512-/WPripjtiAIZ2tWY7ddijORT0Ujg87wxWW/qcoFVCKAWVDPhtY0xr7Dj0M3GyNGz60jGwTElhro/mkF9dT7dDQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.7': + resolution: {integrity: sha512-14DI4NcqpvbICxSnGLx3PmtDaWqRP/KGSGb6C+JLLVPeZRl6dKdHba3pGsqT3vpdTqhEYIPG0MMQ8c0xYqoJxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.7': + resolution: {integrity: sha512-bxrWIRvHWQvbJwi+VIie/kDJmQxcNE6xxWwZdqF/ExVAigtHkv54WTLQPb+QsZdnFy18fg7JPfWGL0RH6vwIlQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.7': + resolution: {integrity: sha512-toOY2BChBZyuxU7OYX6Tn389di4IzAqPTycVcci0O7FSfBqzRB3RZn+K5Is6ANf4tmgRd/K1yZTsNTXbkXsnLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.7': + resolution: {integrity: sha512-lAIXTH/aiLRLxsTgQvfhjo4K1ydWIp00+V0voOr9beb/9ZmkUFrSIb03dXNFRgMNvkE6oGsF10ioQ6UsI+vS5Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.7': + resolution: {integrity: sha512-kdnwS28Pkenp/mZMRwjXXXwxQ7pIsm+bF919LUK93BOyhcLsrVKdP2p9fxpiPNPAbNuch8ypQt0pm2P2LYCAGg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.7': + resolution: {integrity: sha512-516OdsyLdr5E65paF3yBF55t8mfm9+gmtCsK3xI7XKXIT7EfRlHhxL8K/NR6Hu8BWSgF5+1w74lTL0+nxcc8Qw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.7': + resolution: {integrity: sha512-r8/z8n7GFaYRln3xmP1Cxy0HH/HLM0uBUPkEuSVEfKGDA89M0FsZRZJRSwe/tJjRx+fpH/gjorfhB8tmEbSFLA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.7': + resolution: {integrity: sha512-pAsE8iiDxUg1xBqdhrTfg45AVDVpirjz00sblEYClGNNcMnDb+e8beQgqIAw6LvauX/APvgxUnwrgun/YYGBhw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.7': + resolution: {integrity: sha512-lTcIYmmnQQA8Or/2DatS6oSqcdLHvendjS+zLu+FwgToynWMRSmQdpM65fTANJgIS4mjbMOo5KT2lnT9SAb96w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.7': + resolution: {integrity: sha512-e3Gu3WxbNk/UqQhxqU7YIYO+9ZBvWNz3U+h/qRFosscMFzdRPbXYSaSWgSnklv2fz1TgzBTcti2z35c/7irsHw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.7': + resolution: {integrity: sha512-W/jg5qoRSqjsEv0+dZi4e687mcHqmVuU0P4fK6qS/xjetW2Gmc1W8j//z5nAeNcC8Ttm0hV46IjcYeuVwYhuiw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@sveltejs/acorn-typescript@1.0.13': + resolution: {integrity: sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/load-config@0.2.3': + resolution: {integrity: sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ==} + engines: {node: '>= 18.0.0'} + + '@sveltejs/vite-plugin-svelte@7.3.0': + resolution: {integrity: sha512-QbRoJyD92e9R0ufeQIWRHrCC0ObcqSv/aBDdrQMoU+sypav3cDx5wytdQ6GLdXjEMO6xjrXGzfkUygng8JMv0A==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.46.4 + vite: ^8.0.0-beta.7 || ^8.0.0 + + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} + + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@26.4.1': + resolution: {integrity: sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==} + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devalue@5.9.2: + resolution: {integrity: sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w==} + + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} + engines: {node: '>=10.13.0'} + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + esrap@2.3.6: + resolution: {integrity: sha512-yc0OC12UjPqLoc+fe+v5GNs4TOjAigUw3sTikfC+xeBPGUw7gDRz3DtYaqEhxyMVJojcSWJw7jT0QWR+CbuE/A==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magic-string@1.2.3: + resolution: {integrity: sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + rolldown@1.2.7: + resolution: {integrity: sha512-g0EtLvBjTUB7jhyV0S/TCup3v/XSVl45vUIGbOGU4QPiyjTenCe4mKuFvW9fEgYmS2Fo42AUssRmNuMziXdrig==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + svelte-check@4.7.6: + resolution: {integrity: sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: ^5.0.0 || ^6.0.0 + + svelte@5.57.0: + resolution: {integrity: sha512-NdbDn7fl4be1ViUG0oq/lvG6OZy3oENolV2ONjiqqsfVoeAfzaQAKUcEX3MrQod/Bebv1PgwET9rfXhgn9s4Kg==} + engines: {node: '>=18'} + + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + vite@8.2.2: + resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 || ^0.5.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + + zimmerframe@1.1.5: + resolution: {integrity: sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==} + +snapshots: + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@oxc-project/types@0.148.0': {} + + '@rolldown/binding-android-arm-eabi@1.2.7': + optional: true + + '@rolldown/binding-android-arm64@1.2.7': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.7': + optional: true + + '@rolldown/binding-darwin-x64@1.2.7': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.7': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.7': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.7': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.7': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.7': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.7': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.7': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@sveltejs/acorn-typescript@1.0.13(acorn@8.18.0)': + dependencies: + acorn: 8.18.0 + + '@sveltejs/load-config@0.2.3': {} + + '@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))': + dependencies: + deepmerge: 4.3.1 + magic-string: 1.2.3 + obug: 2.1.4 + svelte: 5.57.0 + vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + vitefu: 1.1.3(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.24.5 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.3 + + '@tailwindcss/oxide-android-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide@4.3.3': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 + + '@tailwindcss/vite@4.3.3(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))': + dependencies: + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 + vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + + '@types/estree@1.0.9': {} + + '@types/node@26.4.1': + dependencies: + undici-types: 8.3.0 + + acorn@8.18.0: {} + + aria-query@5.3.1: {} + + axobject-query@4.1.0: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + clsx@2.1.1: {} + + deepmerge@4.3.1: {} + + detect-libc@2.1.2: {} + + devalue@5.9.2: {} + + enhanced-resolve@5.24.5: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + esm-env@1.2.2: {} + + esrap@2.3.6: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.3: + optional: true + + graceful-fs@4.2.11: {} + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + jiti@2.7.0: {} + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + locate-character@3.0.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + magic-string@1.2.3: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + mri@1.2.0: {} + + nanoid@3.3.18: {} + + obug@2.1.4: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + readdirp@4.1.2: {} + + rolldown@1.2.7: + dependencies: + '@oxc-project/types': 0.148.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.7 + '@rolldown/binding-android-arm64': 1.2.7 + '@rolldown/binding-darwin-arm64': 1.2.7 + '@rolldown/binding-darwin-x64': 1.2.7 + '@rolldown/binding-freebsd-x64': 1.2.7 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.7 + '@rolldown/binding-linux-arm64-gnu': 1.2.7 + '@rolldown/binding-linux-arm64-musl': 1.2.7 + '@rolldown/binding-linux-ppc64-gnu': 1.2.7 + '@rolldown/binding-linux-s390x-gnu': 1.2.7 + '@rolldown/binding-linux-x64-gnu': 1.2.7 + '@rolldown/binding-linux-x64-musl': 1.2.7 + '@rolldown/binding-openharmony-arm64': 1.2.7 + '@rolldown/binding-win32-arm64-msvc': 1.2.7 + '@rolldown/binding-win32-x64-msvc': 1.2.7 + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + source-map-js@1.2.1: {} + + svelte-check@4.7.6(picomatch@4.0.7)(svelte@5.57.0)(typescript@6.0.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@sveltejs/load-config': 0.2.3 + chokidar: 4.0.3 + fdir: 6.5.0(picomatch@4.0.7) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.57.0 + typescript: 6.0.3 + transitivePeerDependencies: + - picomatch + + svelte@5.57.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.6.0 + '@sveltejs/acorn-typescript': 1.0.13(acorn@8.18.0) + '@types/estree': 1.0.9 + acorn: 8.18.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.9.2 + esm-env: 1.2.2 + esrap: 2.3.6 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.5 + transitivePeerDependencies: + - '@typescript-eslint/types' + + tailwindcss@4.3.3: {} + + tapable@2.3.3: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + typescript@6.0.3: {} + + undici-types@8.3.0: {} + + vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.26 + rolldown: 1.2.7 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.4.1 + fsevents: 2.3.3 + jiti: 2.7.0 + + vitefu@1.1.3(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)): + optionalDependencies: + vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + + zimmerframe@1.1.5: {} diff --git a/examples/compiler/pnpm-workspace.yaml b/examples/compiler/pnpm-workspace.yaml new file mode 100644 index 00000000..5ed0b5af --- /dev/null +++ b/examples/compiler/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + esbuild: true diff --git a/examples/compiler/src/browser/App.svelte b/examples/compiler/src/browser/App.svelte new file mode 100644 index 00000000..aa8ef8d9 --- /dev/null +++ b/examples/compiler/src/browser/App.svelte @@ -0,0 +1,219 @@ + + + + + + + Coln Compiler + + +
+
+ + C + COLN / COMPILER + +
+
+ {statusLabel} +
+ {sourceLines} {sourceLines === 1 ? "line" : "lines"} +
+
+ +
+
+
+
+

SOURCE

+

Edit source to compile

+
+ +
+ +
+ +
+ {#if error} + + {/if} + + + {#if compilation.diagnosticsHtml.length === 0} +

No diagnostics

+ {:else} +
+ {#each compilation.diagnosticsHtml as diagnostic} +
{@html diagnostic}
+ {/each} +
+ {/if} +
+ + + {#if compilation.prettyIr.length === 0} +

No intermediate representation

+ {:else} +
+ {#each compilation.prettyIr as realm} +
{realm}
+ {/each} +
+ {/if} +
+ + + {#if compilation.irJson === ""} +

No JSON output

+ {:else} +
{compilation.irJson}
+ {/if} +
+
+
+
diff --git a/examples/compiler/src/browser/OutputPanel.svelte b/examples/compiler/src/browser/OutputPanel.svelte new file mode 100644 index 00000000..c6c158a8 --- /dev/null +++ b/examples/compiler/src/browser/OutputPanel.svelte @@ -0,0 +1,18 @@ + + + + + +
+
+

{label}

+ {index} +
+
+ {@render children()} +
+
diff --git a/examples/compiler/src/browser/SourceEditor.svelte b/examples/compiler/src/browser/SourceEditor.svelte new file mode 100644 index 00000000..3e7fcb70 --- /dev/null +++ b/examples/compiler/src/browser/SourceEditor.svelte @@ -0,0 +1,24 @@ + + + + + + diff --git a/examples/compiler/src/lib/compiler.ts b/examples/compiler/src/lib/compiler.ts new file mode 100644 index 00000000..f716f73e --- /dev/null +++ b/examples/compiler/src/lib/compiler.ts @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +export type Compilation = { + diagnosticsHtml: string[] + prettyIr: string[] + irJson: string +} + +type CompileResultPointer = unknown + +type CompilerExports = { + compile(source: string): Promise + freeCompileResult(pointer: CompileResultPointer): Promise | void + getDiagnostics(asHtml: boolean, pointer: CompileResultPointer): Promise | string[] + prettyIr(pointer: CompileResultPointer): Promise | string[] + irToJson(pointer: CompileResultPointer): Promise | string +} + +type WasmLoader = (options: { + wasmUrl: string + ghc_wasm_jsffi: (exports: CompilerExports) => WebAssembly.Imports +}) => Promise + +export type Compiler = { + compile(source: string): Promise +} + +export async function loadCompiler(): Promise { + const distUrl = `${import.meta.env.BASE_URL}dist/` + const [loaderModule, jsffiModule] = await Promise.all([ + import(/* @vite-ignore */ `${distUrl}loadHaskellWasm.js`), + import(/* @vite-ignore */ `${distUrl}ghc_wasm_jsffi.js`), + ]) + const loadHaskellWasm = loaderModule.default as WasmLoader + const exports = await loadHaskellWasm({ + wasmUrl: `${distUrl}coln.wasm`, + ghc_wasm_jsffi: jsffiModule.default, + }) + let previousCompilation = Promise.resolve() + + return { + compile(source) { + const run = async () => { + const pointer = await exports.compile(source) + try { + // GHC reactor exports must not be entered concurrently. + const prettyIr = await exports.prettyIr(pointer) + const diagnosticsHtml = await exports.getDiagnostics(true, pointer) + const irJson = await exports.irToJson(pointer) + return { diagnosticsHtml, prettyIr, irJson } + } finally { + await exports.freeCompileResult(pointer) + } + } + const compilation = previousCompilation.then(run, run) + previousCompilation = compilation.then(() => undefined, () => undefined) + return compilation + }, + } +} diff --git a/examples/compiler/src/lib/examples.ts b/examples/compiler/src/lib/examples.ts new file mode 100644 index 00000000..270efd24 --- /dev/null +++ b/examples/compiler/src/lib/examples.ts @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +const examplesUrl = `${import.meta.env.BASE_URL}examples/` + +export async function loadExampleNames(): Promise { + const response = await fetch(`${examplesUrl}index.json`) + if (!response.ok) throw new Error(`Could not load examples (${response.status})`) + return response.json() as Promise +} + +export async function loadExample(name: string): Promise { + const response = await fetch(`${examplesUrl}${encodeURIComponent(name)}`) + if (!response.ok) throw new Error(`Could not load ${name} (${response.status})`) + return response.text() +} diff --git a/examples/compiler/src/main.ts b/examples/compiler/src/main.ts new file mode 100644 index 00000000..794836e4 --- /dev/null +++ b/examples/compiler/src/main.ts @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { mount } from "svelte" +import App from "./browser/App.svelte" +import "./style.css" + +mount(App, { target: document.getElementById("app")! }) diff --git a/examples/compiler/src/style.css b/examples/compiler/src/style.css new file mode 100644 index 00000000..231e85d1 --- /dev/null +++ b/examples/compiler/src/style.css @@ -0,0 +1,45 @@ +/* SPDX-FileCopyrightText: 2026 Coln contributors */ +/* SPDX-License-Identifier: Apache-2.0 OR MIT */ + +@import url("https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;600;700&display=swap"); +@import "tailwindcss"; + +html { + background: #101718; +} + +body { + margin: 0; +} + +.diagnostics pre { + margin: 0; + overflow-x: auto; + white-space: pre-wrap; +} + +.diagnostics > :not(:last-child) { + border-bottom: 1px solid #304041; + padding-bottom: 1rem; +} + +.diag-debug { + --diagnostic-color: #aab6b7; +} + +.diag-info { + --diagnostic-color: #77b7ff; +} + +.diag-warning { + --diagnostic-color: #ffd166; +} + +.diag-error { + --diagnostic-color: #ff9a86; +} + +[class^="ann-"] { + color: var(--diagnostic-color); + font-weight: 500; +} diff --git a/examples/compiler/tsconfig.json b/examples/compiler/tsconfig.json new file mode 100644 index 00000000..55f0158a --- /dev/null +++ b/examples/compiler/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + "isolatedModules": true, + "noEmit": true, + "types": ["vite/client", "node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/compiler/vite.config.ts b/examples/compiler/vite.config.ts new file mode 100644 index 00000000..d1aa6594 --- /dev/null +++ b/examples/compiler/vite.config.ts @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { defineConfig } from "vite" +import { svelte } from "@sveltejs/vite-plugin-svelte" +import tailwindcss from "@tailwindcss/vite" + +export default defineConfig({ + base: process.env.VITE_BASE || "/", + plugins: [tailwindcss(), svelte()], + build: { + emptyOutDir: true, + outDir: "../../_build/web/compiler-app", + target: "esnext", + }, +}) diff --git a/examples/justfile b/examples/justfile index d4274e0f..507ed526 100644 --- a/examples/justfile +++ b/examples/justfile @@ -1,6 +1,7 @@ mod coln-compiler-wasm '../packages/coln-compiler-wasm' build_dir := "../_build/web/compiler" +compiler_app_dir := "compiler" build-web-compiler: coln-compiler-wasm::build #!/usr/bin/env bash @@ -14,3 +15,22 @@ build-web-compiler: coln-compiler-wasm::build serve-web-compiler port="8000": build-web-compiler simple-http-server --nocache --index --open --port {{ port }} {{ build_dir }} + +stage-web-compiler-app: + #!/usr/bin/env bash + set -euxo pipefail + if [[ ! -f "{{ build_dir }}/dist/coln.wasm" || ! -f "{{ build_dir }}/dist/ghc_wasm_jsffi.js" || ! -f "{{ build_dir }}/dist/loadHaskellWasm.js" || ! -f "{{ build_dir }}/examples/index.json" ]] || ! compgen -G "{{ build_dir }}/examples/*.coln" > /dev/null; then + just build-web-compiler + fi + mkdir -p "{{ compiler_app_dir }}/public/dist" "{{ compiler_app_dir }}/public/examples" + cp "{{ build_dir }}/dist/coln.wasm" "{{ compiler_app_dir }}/public/dist/coln.wasm" + cp "{{ build_dir }}/dist/ghc_wasm_jsffi.js" "{{ compiler_app_dir }}/public/dist/ghc_wasm_jsffi.js" + cp "{{ build_dir }}/dist/loadHaskellWasm.js" "{{ compiler_app_dir }}/public/dist/loadHaskellWasm.js" + cp "{{ build_dir }}/examples/index.json" "{{ compiler_app_dir }}/public/examples/index.json" + cp "{{ build_dir }}"/examples/*.coln "{{ compiler_app_dir }}/public/examples/" + +build-web-compiler-app: stage-web-compiler-app + pnpm --dir {{ compiler_app_dir }} build + +dev-web-compiler-app: stage-web-compiler-app + pnpm --dir {{ compiler_app_dir }} dev From 5af17a8f7309c912c61a66133dee94fe8f87fca7 Mon Sep 17 00:00:00 2001 From: Alex Currie-Clark Date: Fri, 4 Sep 2026 08:11:17 +0100 Subject: [PATCH 12/18] Codemirror editor Co-authored-by: acurrieclark <1306728+acurrieclark@users.noreply.github.com> --- examples/compiler/package.json | 3 + examples/compiler/pnpm-lock.yaml | 128 ++++++++++++ .../compiler/src/browser/SourceEditor.svelte | 187 +++++++++++++++++- 3 files changed, 309 insertions(+), 9 deletions(-) diff --git a/examples/compiler/package.json b/examples/compiler/package.json index de161517..4fa957c1 100644 --- a/examples/compiler/package.json +++ b/examples/compiler/package.json @@ -9,6 +9,9 @@ "build": "pnpm run check && vite build" }, "dependencies": { + "@codemirror/state": "^6.7.2", + "@codemirror/view": "^6.43.10", + "codemirror": "^6.0.2", "svelte": "^5.57.0" }, "devDependencies": { diff --git a/examples/compiler/pnpm-lock.yaml b/examples/compiler/pnpm-lock.yaml index 2533f4b7..ac97fdb6 100644 --- a/examples/compiler/pnpm-lock.yaml +++ b/examples/compiler/pnpm-lock.yaml @@ -8,6 +8,15 @@ importers: .: dependencies: + '@codemirror/state': + specifier: ^6.7.2 + version: 6.7.2 + '@codemirror/view': + specifier: ^6.43.10 + version: 6.43.10 + codemirror: + specifier: ^6.0.2 + version: 6.0.2 svelte: specifier: ^5.57.0 version: 5.57.0 @@ -36,6 +45,27 @@ importers: packages: + '@codemirror/autocomplete@6.20.3': + resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} + + '@codemirror/commands@6.11.0': + resolution: {integrity: sha512-/K4Rl5BN0OtTiPWmJCdqODu38XnDMsDxKY5rgrPnCkutPTJf2wVbkoixLfealF5Kwse/s8P8M5jAiURiwSwnFA==} + + '@codemirror/language@6.12.4': + resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} + + '@codemirror/lint@6.9.7': + resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==} + + '@codemirror/search@6.7.2': + resolution: {integrity: sha512-gUYkYhT2+n/+VGZ+8EzE5WFkYZUZYm1VOKDudIsNqh42uRVQJ0a6Yss9sdKT3MeOYfuL1N6AZA57oza0Oyr0LA==} + + '@codemirror/state@6.7.2': + resolution: {integrity: sha512-U3RiPX62Wl/Gx4ftQ7UxLlloSfsFTQqKa+7vFBYteZGCzkp6oBqcsD7iSwniWRGobCAmDcwZSY+Six3+3ztdfg==} + + '@codemirror/view@6.43.10': + resolution: {integrity: sha512-vVWLvd4fKWYN/pMcwUrg6dWeublxmSz+V+52ZjW3h64IVN9cRUYLk5Km4JDT9vifxAFfDtNOIFxKYENthcolJQ==} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -52,6 +82,18 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@lezer/common@1.5.2': + resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/lr@1.4.10': + resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} + + '@marijn/find-cluster-break@1.0.4': + resolution: {integrity: sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==} + '@oxc-project/types@0.148.0': resolution: {integrity: sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==} @@ -291,6 +333,12 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + codemirror@6.0.2: + resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + + crelt@1.0.7: + resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -539,6 +587,9 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + svelte-check@4.7.6: resolution: {integrity: sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw==} engines: {node: '>= 18.0.0'} @@ -621,11 +672,60 @@ packages: vite: optional: true + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + zimmerframe@1.1.5: resolution: {integrity: sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==} snapshots: + '@codemirror/autocomplete@6.20.3': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.2 + '@codemirror/view': 6.43.10 + '@lezer/common': 1.5.2 + + '@codemirror/commands@6.11.0': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.2 + '@codemirror/view': 6.43.10 + '@lezer/common': 1.5.2 + + '@codemirror/language@6.12.4': + dependencies: + '@codemirror/state': 6.7.2 + '@codemirror/view': 6.43.10 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + style-mod: 4.1.3 + + '@codemirror/lint@6.9.7': + dependencies: + '@codemirror/state': 6.7.2 + '@codemirror/view': 6.43.10 + crelt: 1.0.7 + + '@codemirror/search@6.7.2': + dependencies: + '@codemirror/state': 6.7.2 + '@codemirror/view': 6.43.10 + crelt: 1.0.7 + + '@codemirror/state@6.7.2': + dependencies: + '@marijn/find-cluster-break': 1.0.4 + + '@codemirror/view@6.43.10': + dependencies: + '@codemirror/state': 6.7.2 + crelt: 1.0.7 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.6.0 @@ -645,6 +745,18 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.6.0 + '@lezer/common@1.5.2': {} + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/lr@1.4.10': + dependencies: + '@lezer/common': 1.5.2 + + '@marijn/find-cluster-break@1.0.4': {} + '@oxc-project/types@0.148.0': {} '@rolldown/binding-android-arm-eabi@1.2.7': @@ -795,6 +907,18 @@ snapshots: clsx@2.1.1: {} + codemirror@6.0.2: + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/commands': 6.11.0 + '@codemirror/language': 6.12.4 + '@codemirror/lint': 6.9.7 + '@codemirror/search': 6.7.2 + '@codemirror/state': 6.7.2 + '@codemirror/view': 6.43.10 + + crelt@1.0.7: {} + deepmerge@4.3.1: {} detect-libc@2.1.2: {} @@ -980,6 +1104,8 @@ snapshots: source-map-js@1.2.1: {} + style-mod@4.1.3: {} + svelte-check@4.7.6(picomatch@4.0.7)(svelte@5.57.0)(typescript@6.0.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -1042,4 +1168,6 @@ snapshots: optionalDependencies: vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + w3c-keyname@2.2.8: {} + zimmerframe@1.1.5: {} diff --git a/examples/compiler/src/browser/SourceEditor.svelte b/examples/compiler/src/browser/SourceEditor.svelte index 3e7fcb70..a72f870d 100644 --- a/examples/compiler/src/browser/SourceEditor.svelte +++ b/examples/compiler/src/browser/SourceEditor.svelte @@ -2,6 +2,11 @@ - +
From 1639e38ad3a0c98e5d0c4959c663b2cc00bc01d8 Mon Sep 17 00:00:00 2001 From: Alex Currie-Clark Date: Fri, 4 Sep 2026 11:24:32 +0100 Subject: [PATCH 13/18] Add json viewer Co-authored-by: acurrieclark <1306728+acurrieclark@users.noreply.github.com> --- examples/compiler/package.json | 1 + examples/compiler/pnpm-lock.yaml | 8 ++ examples/compiler/src/browser/App.svelte | 3 +- .../compiler/src/browser/JsonViewer.svelte | 84 +++++++++++++++++++ 4 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 examples/compiler/src/browser/JsonViewer.svelte diff --git a/examples/compiler/package.json b/examples/compiler/package.json index 4fa957c1..b7514ac9 100644 --- a/examples/compiler/package.json +++ b/examples/compiler/package.json @@ -12,6 +12,7 @@ "@codemirror/state": "^6.7.2", "@codemirror/view": "^6.43.10", "codemirror": "^6.0.2", + "json-formatter-js": "^2.5.23", "svelte": "^5.57.0" }, "devDependencies": { diff --git a/examples/compiler/pnpm-lock.yaml b/examples/compiler/pnpm-lock.yaml index ac97fdb6..838c5ebe 100644 --- a/examples/compiler/pnpm-lock.yaml +++ b/examples/compiler/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: codemirror: specifier: ^6.0.2 version: 6.0.2 + json-formatter-js: + specifier: ^2.5.23 + version: 2.5.23 svelte: specifier: ^5.57.0 version: 5.57.0 @@ -389,6 +392,9 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + json-formatter-js@2.5.23: + resolution: {integrity: sha512-Cbm8wHXjo/C56aCePP1VuKvjxoMEmL7g7Ckss1oWFFlCsvOEEbye1kTeaNNaqba1Cl6YpIOYAnK65pUQ8mDIUQ==} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -951,6 +957,8 @@ snapshots: jiti@2.7.0: {} + json-formatter-js@2.5.23: {} + lightningcss-android-arm64@1.32.0: optional: true diff --git a/examples/compiler/src/browser/App.svelte b/examples/compiler/src/browser/App.svelte index aa8ef8d9..d76f2bae 100644 --- a/examples/compiler/src/browser/App.svelte +++ b/examples/compiler/src/browser/App.svelte @@ -5,6 +5,7 @@ import { onMount } from "svelte" import { loadCompiler, type Compilation, type Compiler } from "../lib/compiler.ts" import { loadExample, loadExampleNames } from "../lib/examples.ts" + import JsonViewer from "./JsonViewer.svelte" import OutputPanel from "./OutputPanel.svelte" import SourceEditor from "./SourceEditor.svelte" @@ -211,7 +212,7 @@ {#if compilation.irJson === ""}

No JSON output

{:else} -
{compilation.irJson}
+ {/if} diff --git a/examples/compiler/src/browser/JsonViewer.svelte b/examples/compiler/src/browser/JsonViewer.svelte new file mode 100644 index 00000000..3247c6f3 --- /dev/null +++ b/examples/compiler/src/browser/JsonViewer.svelte @@ -0,0 +1,84 @@ + + + + + +
+ + From 2fae1e1b5313d1bd3bc945671bc03b44415c8d5e Mon Sep 17 00:00:00 2001 From: Alex Currie-Clark Date: Fri, 4 Sep 2026 11:24:57 +0100 Subject: [PATCH 14/18] add oxfmt Co-authored-by: acurrieclark <1306728+acurrieclark@users.noreply.github.com> --- packages/coln-repo/.oxfmtrc.json | 5 + packages/coln-repo/README.md | 36 +-- packages/coln-repo/package.json | 5 +- packages/coln-repo/pnpm-lock.yaml | 225 +++++++++++++++++ packages/coln-repo/pnpm-workspace.yaml | 2 +- packages/coln-repo/src/colnDocType.ts | 59 +++-- packages/coln-repo/src/colnHandle.ts | 68 ++++-- packages/coln-repo/src/index.ts | 7 +- packages/coln-repo/tests/browser/main.ts | 13 +- packages/coln-repo/tests/fixtures/itemFfi.ts | 5 +- packages/coln-repo/tests/generated/Graph.ts | 12 +- .../coln-repo/tests/generated/GraphRealm.json | 93 ++++++- .../coln-repo/tests/generated/GraphRealm.ts | 19 +- packages/coln-repo/tests/handle.test.ts | 228 ++++++++++-------- .../coln-repo/tests/helpers/TestRepoPair.ts | 20 +- packages/coln-repo/tests/package/consumer.ts | 45 +++- packages/coln-repo/tests/server.mjs | 21 +- packages/coln-repo/tests/sync.spec.ts | 51 ++-- 18 files changed, 675 insertions(+), 239 deletions(-) create mode 100644 packages/coln-repo/.oxfmtrc.json diff --git a/packages/coln-repo/.oxfmtrc.json b/packages/coln-repo/.oxfmtrc.json new file mode 100644 index 00000000..2764424d --- /dev/null +++ b/packages/coln-repo/.oxfmtrc.json @@ -0,0 +1,5 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 80, + "semi": false +} diff --git a/packages/coln-repo/README.md b/packages/coln-repo/README.md index f680cc4a..9aa0dd69 100644 --- a/packages/coln-repo/README.md +++ b/packages/coln-repo/README.md @@ -9,10 +9,10 @@ Coln Repo currently requires the experimental [doctypes](https://github.com/auto Create a Coln store using generated bindings. ```ts -import * as Bindings from "./ExampleRealm.js"; -import { create } from "@coln-project/repo"; +import * as Bindings from "./ExampleRealm.js" +import { create } from "@coln-project/repo" -const handle = create(repo, Bindings); +const handle = create(repo, Bindings) ``` ## Find @@ -20,20 +20,20 @@ const handle = create(repo, Bindings); Find a Coln store by Automerge URL. Bindings are optional and must match the document schema when supplied. ```ts -import * as Bindings from "./ExampleRealm.js"; -import { find } from "@coln-project/repo"; +import * as Bindings from "./ExampleRealm.js" +import { find } from "@coln-project/repo" -const handle = await find(repo, url, Bindings); +const handle = await find(repo, url, Bindings) ``` Apply bindings to a raw handle later if needed: ```ts -import * as Bindings from "./ExampleRealm.js"; -import { applyBindings, find } from "@coln-project/repo"; +import * as Bindings from "./ExampleRealm.js" +import { applyBindings, find } from "@coln-project/repo" -const rawHandle = await find(repo, url); -const handle = applyBindings(rawHandle, Bindings); +const rawHandle = await find(repo, url) +const handle = applyBindings(rawHandle, Bindings) ``` > [!IMPORTANT] @@ -44,14 +44,14 @@ const handle = applyBindings(rawHandle, Bindings); Read the current document synchronously or subscribe to changes. ```ts -const doc = handle.doc(); -doc.scanTable("Example.Items"); -doc.root.Items.values(); +const doc = handle.doc() +doc.scanTable("Example.Items") +doc.root.Items.values() handle.on("change", ({ doc }) => { - doc?.scanTable("Example.Items"); - doc?.root.Items.values(); -}); + doc?.scanTable("Example.Items") + doc?.root.Items.values() +}) ``` Documents expose `heads()`, `jsonIR()`, `rowById()`, and `scanTable()`. @@ -62,8 +62,8 @@ Changes are synchronous and atomic. A thrown error aborts the transaction. ```ts handle.change((tx) => { - tx.root.Items.add(); -}); + tx.root.Items.add() +}) ``` Transactions expose `add(path, values)`; bound transactions also expose `root`. diff --git a/packages/coln-repo/package.json b/packages/coln-repo/package.json index ad226573..b73e558f 100644 --- a/packages/coln-repo/package.json +++ b/packages/coln-repo/package.json @@ -16,8 +16,10 @@ }, "scripts": { "build": "tsc -p tsconfig.build.json", + "format": "oxfmt --write .", + "format:check": "oxfmt --check .", "typecheck": "tsc --noEmit", - "test": "pnpm run typecheck && pnpm run test:unit && pnpm run test:e2e", + "test": "pnpm run format:check && pnpm run typecheck && pnpm run test:unit && pnpm run test:e2e", "test:unit": "tsx --test \"tests/**/*.test.ts\"", "test:package": "pnpm run build && tsc -p tsconfig.package.json && node --input-type=module --eval \"const pkg = await import('@coln-project/repo'); if (typeof pkg.find !== 'function') throw new Error('missing find export')\"", "test:e2e": "pnpm run test:package && playwright test" @@ -31,6 +33,7 @@ "@playwright/test": "^1.62.1", "@types/node": "^25.9.5", "@types/ws": "^8.18.1", + "oxfmt": "0.66.0", "tsx": "^4.23.13", "typescript": "^6.0.3", "vite": "^8.2.2", diff --git a/packages/coln-repo/pnpm-lock.yaml b/packages/coln-repo/pnpm-lock.yaml index 41883aa0..c28899eb 100644 --- a/packages/coln-repo/pnpm-lock.yaml +++ b/packages/coln-repo/pnpm-lock.yaml @@ -31,6 +31,9 @@ importers: '@types/ws': specifier: ^8.18.1 version: 8.18.1 + oxfmt: + specifier: 0.66.0 + version: 0.66.0 tsx: specifier: ^4.23.13 version: 4.23.13 @@ -253,6 +256,128 @@ packages: '@oxc-project/types@0.147.0': resolution: {integrity: sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==} + '@oxfmt/binding-android-arm-eabi@0.66.0': + resolution: {integrity: sha512-2Me9eoptv6ERdEuI2P8AOlYdHHraXebJaM6SC0kc2Dfb+mLrep2db+fedBPKaYn673h/vBgvP4tkOdAbaudX6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.66.0': + resolution: {integrity: sha512-u7O+bSSF0HGsDKkQQxBqvLGVepu93RA+JKu+ONqvfh4sCnCEbj31wZj4iG5gk3XfRwrmYj0/8catkO2LcblQKQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.66.0': + resolution: {integrity: sha512-/ikyMIVjX/sdo7KtjxoEsSUosfPzveVhT9RWMx9yGqFDKFJ89JAEKuEeLBmurDjrkb4w8tOnAdSO3SBaplY3bw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.66.0': + resolution: {integrity: sha512-q5xUsKeFqawa9NXa6ZGXWimFV19m8MogKPdTaSVDAAk2EQKBmBZRDeluwcl1p8ty/OFc9s9888OKEh3xfPVH0g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.66.0': + resolution: {integrity: sha512-CR+x4VzMY0pRXLK/xFQ/RzsSFkP5t2Z2mef0QY6OP/rTRcMUoMLCOM62/3Fp/t0K+UDoBKxvMyeb6D0zPMjleA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.66.0': + resolution: {integrity: sha512-ZEYmO/LbH9tTQCADILHGZE4GeOXOAj2VzedHkASNwjmwlwtutJCLpCJbIs37wRGTFgWRoEcD72jpMX+IBJUGjQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.66.0': + resolution: {integrity: sha512-hNtR9/oU0CeTkq7JnRkmBQwqe17v2ZaAMLC4VcN7IIOWeRyWDk0knSPWS9iiLmtbZ2RRBBtsG01jQgkZmKCJeQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.66.0': + resolution: {integrity: sha512-uwOVQ8i6I1LT/+eDzfsgrrcZp8Fn6NPVUPn8fF5gdFGekFf0PddF+LEuwsD0/pbNUcKZhDj2rQ5UpITh9gF4iQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-arm64-musl@0.66.0': + resolution: {integrity: sha512-tTkF2Dmx4nGAjmBlZb+UtTGqR/EK4ZrW9qBfzte07a9XWqzoGGKzpFFlyNDhQe+Uwql94+ReCTeNbhOXscw1Dg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-ppc64-gnu@0.66.0': + resolution: {integrity: sha512-F3cKHUav4yXOHn6GFnwpBhSYsJOYKKf9eqO/9jlEuqPxNw9zb98E9ZFct79gcg8pibUGkbveEu9WDlmXJpDzKw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-gnu@0.66.0': + resolution: {integrity: sha512-K5fDaNZfDyQMYA/3qL21bqyN0X9T15LLwwbFPt2aHc94+ZG7bh0vZEsy2y7NlRnjjHFSwN+Hzg6ldJtbOriH4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-musl@0.66.0': + resolution: {integrity: sha512-44Yc+I+qOmTElRcEhm5hUKIUJEQIOugymz4ua4tB0Wox7tGAfIbjzmXz/HDAtw1Ij6gmBwZlzh4hc9679RhWeA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-s390x-gnu@0.66.0': + resolution: {integrity: sha512-1e29Eg9hEj2kRBB19M0seIehPbbXHCk35GvImjDvb79rjjYjXCRmtbUNHJcgoktZAMIzXrTbxDBKmTc1V4bg3A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-gnu@0.66.0': + resolution: {integrity: sha512-vODY1UQo10gngn0+D4xHKU84F1Twm1LqrzV4SqPXvmQKSd87paehvZ6jqA5wKs6XQrlWul9clYMDVHcoW9CPMA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-musl@0.66.0': + resolution: {integrity: sha512-YDzXx2JsT4+HL4MdkVrYjO55NS5lUKNm8rLC4ZPou8+seu0v0jhecSh+ufoO6+xEa8gccEezMlI2WHJi4ApUgw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-openharmony-arm64@0.66.0': + resolution: {integrity: sha512-mJjUYd8lj0+j4JkYyEM+5qKBf1Rnrpgjn/SVYKJhicVDqLz566ooa7Fs8zflPqt+dnZDV7X054rVIQX6ZcQNlQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.66.0': + resolution: {integrity: sha512-soV+0vESv7e5ntCHWC61x4gg8OSak6IHHnWsZmHrJFlvMj2AK+kmldErCNkVkrvc1Ts2/++rJXn+IuAb2WMXhw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.66.0': + resolution: {integrity: sha512-YCPi23uRIEYuIKTZohAkKbPFpujQ5QBuUM5iDv+UqbCmTPAkaFsxjsSuB8xlBpRT0G7eP/4HMF+cPDSqHtOD9A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.66.0': + resolution: {integrity: sha512-bwTQcv/JVRPkOqQtMF0X7vpvpncDQiBcXHxZ9S2hR12Hlo8bvBdUR5x5XnxzDZ3kM0qoZw1rv7KaD66Ly+pFWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@playwright/test@1.62.1': resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} engines: {node: '>=20'} @@ -513,6 +638,19 @@ packages: resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} hasBin: true + oxfmt@0.66.0: + resolution: {integrity: sha512-FfvqR8RFtV6JJpRrpkfqyVCQ7HDvZ/VriWFx7veftCgL1B5ZO9qNr+1rvPieycMQnNfVG0PWyJQiy7p0hq1I5w==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + svelte: + optional: true + vite-plus: + optional: true + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -547,6 +685,10 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tinypool@2.1.0: + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + engines: {node: ^20.0.0 || >=22.0.0} + tsx@4.23.13: resolution: {integrity: sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==} engines: {node: '>=18.0.0'} @@ -751,6 +893,63 @@ snapshots: '@oxc-project/types@0.147.0': {} + '@oxfmt/binding-android-arm-eabi@0.66.0': + optional: true + + '@oxfmt/binding-android-arm64@0.66.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.66.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.66.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.66.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.66.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.66.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.66.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.66.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.66.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.66.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.66.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.66.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.66.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.66.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.66.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.66.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.66.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.66.0': + optional: true + '@playwright/test@1.62.1': dependencies: playwright: 1.62.1 @@ -948,6 +1147,30 @@ snapshots: detect-libc: 2.1.2 optional: true + oxfmt@0.66.0: + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.66.0 + '@oxfmt/binding-android-arm64': 0.66.0 + '@oxfmt/binding-darwin-arm64': 0.66.0 + '@oxfmt/binding-darwin-x64': 0.66.0 + '@oxfmt/binding-freebsd-x64': 0.66.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.66.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.66.0 + '@oxfmt/binding-linux-arm64-gnu': 0.66.0 + '@oxfmt/binding-linux-arm64-musl': 0.66.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.66.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.66.0 + '@oxfmt/binding-linux-riscv64-musl': 0.66.0 + '@oxfmt/binding-linux-s390x-gnu': 0.66.0 + '@oxfmt/binding-linux-x64-gnu': 0.66.0 + '@oxfmt/binding-linux-x64-musl': 0.66.0 + '@oxfmt/binding-openharmony-arm64': 0.66.0 + '@oxfmt/binding-win32-arm64-msvc': 0.66.0 + '@oxfmt/binding-win32-ia32-msvc': 0.66.0 + '@oxfmt/binding-win32-x64-msvc': 0.66.0 + picocolors@1.1.1: {} picomatch@4.0.7: {} @@ -994,6 +1217,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.7) picomatch: 4.0.7 + tinypool@2.1.0: {} + tsx@4.23.13: dependencies: esbuild: 0.28.2 diff --git a/packages/coln-repo/pnpm-workspace.yaml b/packages/coln-repo/pnpm-workspace.yaml index 087ce062..40f0aec6 100644 --- a/packages/coln-repo/pnpm-workspace.yaml +++ b/packages/coln-repo/pnpm-workspace.yaml @@ -1,6 +1,6 @@ allowBuilds: cbor-extract: false - '@automerge/automerge-repo@git+https://github.com/automerge/automerge-repo.git': true + "@automerge/automerge-repo@git+https://github.com/automerge/automerge-repo.git": true esbuild: true overrides: "@automerge/automerge": 3.3.2 diff --git a/packages/coln-repo/src/colnDocType.ts b/packages/coln-repo/src/colnDocType.ts index 9f8e690f..1b84ca73 100644 --- a/packages/coln-repo/src/colnDocType.ts +++ b/packages/coln-repo/src/colnDocType.ts @@ -9,7 +9,10 @@ import { type RealmBindings, type TransactionHandle, } from "@coln-project/runtime" -import { defineDocumentType, type SedimentreeMeta } from "@automerge/automerge-repo/slim" +import { + defineDocumentType, + type SedimentreeMeta, +} from "@automerge/automerge-repo/slim" export type { ColnSchema, RealmBindings } from "@coln-project/runtime" @@ -18,22 +21,26 @@ export type ColnState = { bindings?: RealmBindings } -type ColnDocumentBase = Pick +type ColnDocumentBase = Pick< + StoreHandle, + "heads" | "jsonIR" | "rowById" | "scanTable" +> type ColnTransactionBase = Pick -export type ColnDocument = - Bindings extends RealmBindings - ? ColnDocumentBase & Pick, "root"> - : ColnDocumentBase +export type ColnDocument< + Bindings extends RealmBindings | undefined = undefined, +> = Bindings extends RealmBindings + ? ColnDocumentBase & Pick, "root"> + : ColnDocumentBase -export type ColnTransaction = - Bindings extends RealmBindings - ? ColnTransactionBase & Pick, "root"> - : ColnTransactionBase +export type ColnTransaction< + Bindings extends RealmBindings | undefined = undefined, +> = Bindings extends RealmBindings + ? ColnTransactionBase & Pick, "root"> + : ColnTransactionBase -export type ColnChange = ( - transaction: ColnTransaction, -) => void +export type ColnChange = + (transaction: ColnTransaction) => void export const colnDocType = defineDocumentType< ColnState, @@ -43,19 +50,24 @@ export const colnDocType = defineDocumentType< >({ name: "coln", empty: () => ({ store: StoreHandle.empty() }), - init: schema => ({ store: StoreHandle.fromTheory(serializeSchema(schema)) }), + init: (schema) => ({ + store: StoreHandle.fromTheory(serializeSchema(schema)), + }), view: createDocument, change: (state, change) => runTransaction(state, change), - heads: state => state.store.heads(), - hasData: state => state.store.heads().length > 0, + heads: (state) => state.store.heads(), + hasData: (state) => state.store.heads().length > 0, sedimentree: { - metadata: state => commitChunks(state).map(commitMetadata), + metadata: (state) => commitChunks(state).map(commitMetadata), materialize: (state, metadata) => { - const wantedHeads = new Set(metadata.map(entry => entry.head)) + const wantedHeads = new Set(metadata.map((entry) => entry.head)) // TODO: Add a runtime commitChunksByHash API if full-history scans become costly. return commitChunks(state) - .filter(chunk => wantedHeads.has(chunk.hash)) - .map(chunk => ({ ...commitMetadata(chunk), bytes: new Uint8Array(chunk.bytes) })) + .filter((chunk) => wantedHeads.has(chunk.hash)) + .map((chunk) => ({ + ...commitMetadata(chunk), + bytes: new Uint8Array(chunk.bytes), + })) }, apply: (state, blobs) => { if (blobs.length > 0) { @@ -65,7 +77,7 @@ export const colnDocType = defineDocumentType< } return state }, - liveHashes: state => commitChunks(state).map(chunk => chunk.hash), + liveHashes: (state) => commitChunks(state).map((chunk) => chunk.hash), }, }) @@ -75,7 +87,7 @@ function createDocument(state: ColnState): ColnDocumentBase { heads: () => store.heads(), jsonIR: () => store.jsonIR(), rowById: (path, rowId) => store.rowById(path, rowId), - scanTable: path => store.scanTable(path), + scanTable: (path) => store.scanTable(path), } if (bindings) document.root = new bindings.View(store).root return Object.freeze(document) @@ -115,7 +127,8 @@ function runTransaction(document: ColnState, change: ColnChange): ColnState { function serializeSchema(schema: ColnSchema): string { const serialized = JSON.stringify(schema) - if (serialized === undefined) throw new Error("creating a Coln store requires a schema") + if (serialized === undefined) + throw new Error("creating a Coln store requires a schema") return serialized } diff --git a/packages/coln-repo/src/colnHandle.ts b/packages/coln-repo/src/colnHandle.ts index 3a892b9a..c56c3e66 100644 --- a/packages/coln-repo/src/colnHandle.ts +++ b/packages/coln-repo/src/colnHandle.ts @@ -7,76 +7,94 @@ import type { CrdtDocHandle, DocumentType, Repo, -} from "@automerge/automerge-repo"; -import type { RealmBindings } from "@coln-project/runtime"; +} from "@automerge/automerge-repo" +import type { RealmBindings } from "@coln-project/runtime" import { colnDocType, type ColnChange, type ColnDocument, type ColnSchema, type ColnState, -} from "./colnDocType.js"; +} from "./colnDocType.js" type FoundColnHandle = - Bindings extends RealmBindings ? ColnHandle : ColnHandle; + Bindings extends RealmBindings ? ColnHandle : ColnHandle export type ColnHandle = CrdtDocHandle< - DocumentType, ColnChange, ColnSchema> - >; + DocumentType< + ColnState, + ColnDocument, + ColnChange, + ColnSchema + > + > export function applyBindings( handle: ColnHandle | ColnHandle, bindings: Bindings, ): ColnHandle { - const state = handle.fullDoc(); - const existingBindings = state.bindings; + const state = handle.fullDoc() + const existingBindings = state.bindings if (existingBindings) { if (existingBindings !== bindings) { - throw new TypeError("Coln handle already uses different realm bindings"); + throw new TypeError("Coln handle already uses different realm bindings") } - return handle as unknown as ColnHandle; + return handle as unknown as ColnHandle } - const actualSchema = canonicalJson(JSON.parse(handle.doc().jsonIR())); - const bindingsSchema = canonicalJson(bindings.schema); + const actualSchema = canonicalJson(JSON.parse(handle.doc().jsonIR())) + const bindingsSchema = canonicalJson(bindings.schema) if (actualSchema !== bindingsSchema) { - throw new TypeError("Realm bindings schema does not match Coln document schema"); + throw new TypeError( + "Realm bindings schema does not match Coln document schema", + ) } - state.bindings = bindings; + state.bindings = bindings - return handle as unknown as ColnHandle; + return handle as unknown as ColnHandle } export function create( repo: Repo, bindings: Bindings, ): ColnHandle { - const handle = repo.create(bindings.schema, colnDocType); + const handle = repo.create(bindings.schema, colnDocType) - return applyBindings(handle, bindings); + return applyBindings(handle, bindings) } -export async function find( +export async function find< + Bindings extends RealmBindings | undefined = undefined, +>( repo: Repo, url: AutomergeUrl, // a rest tuple (rather than an optional parameter) keeps `undefined` in the // inferred Bindings, so maybe-undefined arguments produce a union return type ...rest: [bindings: Bindings] | [] ): Promise> { - const [bindings] = rest; - const handle = await repo.find(url, colnDocType); + const [bindings] = rest + const handle = await repo.find(url, colnDocType) - return (bindings === undefined ? handle : applyBindings(handle, bindings)) as FoundColnHandle; + return ( + bindings === undefined ? handle : applyBindings(handle, bindings) + ) as FoundColnHandle } function canonicalJson(value: unknown): string | undefined { return JSON.stringify(value, (_key, current: unknown) => { - if (current === null || Array.isArray(current) || typeof current !== "object") return current; + if ( + current === null || + Array.isArray(current) || + typeof current !== "object" + ) + return current return Object.fromEntries( - Object.entries(current).sort(([left], [right]) => left.localeCompare(right)), - ); - }); + Object.entries(current).sort(([left], [right]) => + left.localeCompare(right), + ), + ) + }) } diff --git a/packages/coln-repo/src/index.ts b/packages/coln-repo/src/index.ts index 8afb07f1..7ef90e63 100644 --- a/packages/coln-repo/src/index.ts +++ b/packages/coln-repo/src/index.ts @@ -11,9 +11,4 @@ export { type ColnTransaction, } from "./colnDocType.js" -export { - applyBindings, - create, - find, - type ColnHandle, -} from "./colnHandle.js" +export { applyBindings, create, find, type ColnHandle } from "./colnHandle.js" diff --git a/packages/coln-repo/tests/browser/main.ts b/packages/coln-repo/tests/browser/main.ts index 6c1619dc..e7765dc9 100644 --- a/packages/coln-repo/tests/browser/main.ts +++ b/packages/coln-repo/tests/browser/main.ts @@ -18,7 +18,9 @@ import { } from "../../src/index.js" import * as itemFfi from "../fixtures/itemFfi" -initSync({ module: Uint8Array.from(atob(wasmBase64), char => char.charCodeAt(0)) }) +initSync({ + module: Uint8Array.from(atob(wasmBase64), (char) => char.charCodeAt(0)), +}) type RawHandle = CrdtDocHandle type ItemHandle = ColnHandle @@ -51,7 +53,7 @@ const api = { }, add(path: string, values: Value[]): void { - currentHandle().change(transaction => transaction.add(path, values)) + currentHandle().change((transaction) => transaction.add(path, values)) }, rows(path: string): RowView[] { @@ -59,13 +61,16 @@ const api = { }, addTyped(value: string): void { - currentTypedHandle().change(transaction => { + currentTypedHandle().change((transaction) => { transaction.root.Items(stringValue(value)).add() }) }, typedCount(value: string): number { - const rows = currentTypedHandle().doc().root.Items(stringValue(value)).values() + const rows = currentTypedHandle() + .doc() + .root.Items(stringValue(value)) + .values() let count = 0 for (let next = rows.next(); !next.done; next = rows.next()) count += 1 return count diff --git a/packages/coln-repo/tests/fixtures/itemFfi.ts b/packages/coln-repo/tests/fixtures/itemFfi.ts index f181ec17..20085080 100644 --- a/packages/coln-repo/tests/fixtures/itemFfi.ts +++ b/packages/coln-repo/tests/fixtures/itemFfi.ts @@ -19,7 +19,7 @@ export class View { constructor(store: StoreHandle) { this.root = { - Items: value => new RowIdSet.View(store, "Test.Items", [value]), + Items: (value) => new RowIdSet.View(store, "Test.Items", [value]), } } } @@ -30,7 +30,8 @@ export class Transaction extends View { constructor(store: StoreHandle, transaction: TransactionHandle) { super(store) this.root = { - Items: value => new RowIdSet.Transaction(store, "Test.Items", [value], transaction), + Items: (value) => + new RowIdSet.Transaction(store, "Test.Items", [value], transaction), } } } diff --git a/packages/coln-repo/tests/generated/Graph.ts b/packages/coln-repo/tests/generated/Graph.ts index db000cd9..52151b3e 100644 --- a/packages/coln-repo/tests/generated/Graph.ts +++ b/packages/coln-repo/tests/generated/Graph.ts @@ -2,14 +2,14 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT -import * as runtime from "@coln-project/runtime"; +import * as runtime from "@coln-project/runtime" export interface View { - V: runtime.ColnSet.View; - E: (x: runtime.Value) => (x: runtime.Value) => runtime.ColnSet.View; + V: runtime.ColnSet.View + E: (x: runtime.Value) => (x: runtime.Value) => runtime.ColnSet.View } export interface Transaction extends View { - V: runtime.ColnSet.Transaction; - E: (x: runtime.Value) => (x: runtime.Value) => runtime.ColnSet.Transaction; -} \ No newline at end of file + V: runtime.ColnSet.Transaction + E: (x: runtime.Value) => (x: runtime.Value) => runtime.ColnSet.Transaction +} diff --git a/packages/coln-repo/tests/generated/GraphRealm.json b/packages/coln-repo/tests/generated/GraphRealm.json index b83d89f3..d7f212c4 100644 --- a/packages/coln-repo/tests/generated/GraphRealm.json +++ b/packages/coln-repo/tests/generated/GraphRealm.json @@ -1 +1,92 @@ -{"entities":[{"path":[["GraphRealm"],["V"]],"value":{"entityVariant":{"tag":"table"},"columns":[],"primaryKey":null}},{"path":[["GraphRealm"],["E"]],"value":{"entityVariant":{"tag":"table"},"columns":[{"path":[["a"]],"type":{"tag":"rowId","path":[["GraphRealm"],["V"]]}},{"path":[["b"]],"type":{"tag":"rowId","path":[["GraphRealm"],["V"]]}}],"primaryKey":null}}],"rules":[{"path":[["GraphRealm"],["V"],["foreignKey"]],"value":{"ruleVariant":"enforced","varNames":[],"varTypes":[],"antecedents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":null,"values":[]}}],"consequents":[]}},{"path":[["GraphRealm"],["E"],["foreignKey"]],"value":{"ruleVariant":"enforced","varNames":[[["a"]],[["b"]]],"varTypes":[{"tag":"rowId","path":[["GraphRealm"],["V"]]},{"tag":"rowId","path":[["GraphRealm"],["V"]]}],"antecedents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["E"]],"rowId":null,"values":[{"column":0,"term":{"tag":"var","index":0}},{"column":1,"term":{"tag":"var","index":1}}]}}],"consequents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":{"tag":"var","index":0},"values":[]}},{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":{"tag":"var","index":1},"values":[]}}]}}]} \ No newline at end of file +{ + "entities": [ + { + "path": [["GraphRealm"], ["V"]], + "value": { + "entityVariant": { "tag": "table" }, + "columns": [], + "primaryKey": null + } + }, + { + "path": [["GraphRealm"], ["E"]], + "value": { + "entityVariant": { "tag": "table" }, + "columns": [ + { + "path": [["a"]], + "type": { "tag": "rowId", "path": [["GraphRealm"], ["V"]] } + }, + { + "path": [["b"]], + "type": { "tag": "rowId", "path": [["GraphRealm"], ["V"]] } + } + ], + "primaryKey": null + } + } + ], + "rules": [ + { + "path": [["GraphRealm"], ["V"], ["foreignKey"]], + "value": { + "ruleVariant": "enforced", + "varNames": [], + "varTypes": [], + "antecedents": [ + { + "tag": "atom", + "atom": { + "entity": [["GraphRealm"], ["V"]], + "rowId": null, + "values": [] + } + } + ], + "consequents": [] + } + }, + { + "path": [["GraphRealm"], ["E"], ["foreignKey"]], + "value": { + "ruleVariant": "enforced", + "varNames": [[["a"]], [["b"]]], + "varTypes": [ + { "tag": "rowId", "path": [["GraphRealm"], ["V"]] }, + { "tag": "rowId", "path": [["GraphRealm"], ["V"]] } + ], + "antecedents": [ + { + "tag": "atom", + "atom": { + "entity": [["GraphRealm"], ["E"]], + "rowId": null, + "values": [ + { "column": 0, "term": { "tag": "var", "index": 0 } }, + { "column": 1, "term": { "tag": "var", "index": 1 } } + ] + } + } + ], + "consequents": [ + { + "tag": "atom", + "atom": { + "entity": [["GraphRealm"], ["V"]], + "rowId": { "tag": "var", "index": 0 }, + "values": [] + } + }, + { + "tag": "atom", + "atom": { + "entity": [["GraphRealm"], ["V"]], + "rowId": { "tag": "var", "index": 1 }, + "values": [] + } + } + ] + } + } + ] +} diff --git a/packages/coln-repo/tests/generated/GraphRealm.ts b/packages/coln-repo/tests/generated/GraphRealm.ts index b7ca28db..b0b861a4 100644 --- a/packages/coln-repo/tests/generated/GraphRealm.ts +++ b/packages/coln-repo/tests/generated/GraphRealm.ts @@ -25,13 +25,26 @@ export class View { export class Transaction extends View { root: Graph.Transaction - constructor(store: runtime.StoreHandle, transaction: runtime.TransactionHandle) { + constructor( + store: runtime.StoreHandle, + transaction: runtime.TransactionHandle, + ) { super(store) this.root = { - V: new runtime.RowIdSet.Transaction(store, "GraphRealm.V", [], transaction), + V: new runtime.RowIdSet.Transaction( + store, + "GraphRealm.V", + [], + transaction, + ), E: (a: runtime.Value) => { return (b: runtime.Value) => { - return new runtime.RowIdSet.Transaction(store, "GraphRealm.E", [a, b], transaction) + return new runtime.RowIdSet.Transaction( + store, + "GraphRealm.E", + [a, b], + transaction, + ) } }, } diff --git a/packages/coln-repo/tests/handle.test.ts b/packages/coln-repo/tests/handle.test.ts index a8096050..575225c3 100644 --- a/packages/coln-repo/tests/handle.test.ts +++ b/packages/coln-repo/tests/handle.test.ts @@ -2,81 +2,97 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT -import assert from "node:assert/strict"; -import { afterEach, beforeEach, test } from "node:test"; -import type { Value } from "@coln-project/runtime"; -import { applyBindings, colnDocType, create, find, type ColnDocument } from "../src/index"; -import * as itemFfi from "./fixtures/itemFfi"; -import schema from "./generated/GraphRealm.json" with { type: "json" }; -import * as GraphRealm from "./generated/GraphRealm"; -import { createTestRepoPair, waitForChange, type TestRepoPair } from "./helpers/TestRepoPair"; - -let repos: TestRepoPair; +import assert from "node:assert/strict" +import { afterEach, beforeEach, test } from "node:test" +import type { Value } from "@coln-project/runtime" +import { + applyBindings, + colnDocType, + create, + find, + type ColnDocument, +} from "../src/index" +import * as itemFfi from "./fixtures/itemFfi" +import schema from "./generated/GraphRealm.json" with { type: "json" } +import * as GraphRealm from "./generated/GraphRealm" +import { + createTestRepoPair, + waitForChange, + type TestRepoPair, +} from "./helpers/TestRepoPair" + +let repos: TestRepoPair beforeEach(async () => { - repos = await createTestRepoPair(); -}); + repos = await createTestRepoPair() +}) afterEach(async () => { - await repos.shutdown(); -}); + await repos.shutdown() +}) test("create returns a bound, initialized handle", () => { - const handle = create(repos.source, GraphRealm); - const document = handle.doc(); - - assert.deepEqual(Object.keys(document).sort(), ["heads", "jsonIR", "root", "rowById", "scanTable"]); - assert.deepEqual(JSON.parse(document.jsonIR()), schema); - assert.deepEqual(document.heads(), handle.rawHeads()); - assert.deepEqual(document.scanTable("GraphRealm.V"), []); -}); + const handle = create(repos.source, GraphRealm) + const document = handle.doc() + + assert.deepEqual(Object.keys(document).sort(), [ + "heads", + "jsonIR", + "root", + "rowById", + "scanTable", + ]) + assert.deepEqual(JSON.parse(document.jsonIR()), schema) + assert.deepEqual(document.heads(), handle.rawHeads()) + assert.deepEqual(document.scanTable("GraphRealm.V"), []) +}) test("document can be updated and changes are reflected", () => { - const handle = create(repos.source, GraphRealm); + const handle = create(repos.source, GraphRealm) - let v1!: Value, v2!: Value, e1!: Value; + let v1!: Value, v2!: Value, e1!: Value handle.change((tx) => { - v1 = tx.root.V.add(); - v2 = tx.root.V.add(); - e1 = tx.root.E(v1)(v2).add(); - }); + v1 = tx.root.V.add() + v2 = tx.root.V.add() + e1 = tx.root.E(v1)(v2).add() + }) - const doc = handle.doc(); + const doc = handle.doc() - assert(doc.root.V.has(v1)); - assert(doc.root.V.has(v2)); - assert(doc.root.E(v1)(v2).has(e1)); - assert.equal(v1.tag, "row_id"); - assert.deepEqual(doc.rowById("GraphRealm.V", v1.value)?.rowId, v1); -}); + assert(doc.root.V.has(v1)) + assert(doc.root.V.has(v2)) + assert(doc.root.E(v1)(v2).has(e1)) + assert.equal(v1.tag, "row_id") + assert.deepEqual(doc.rowById("GraphRealm.V", v1.value)?.rowId, v1) +}) test("changes expose only safe Coln operations", () => { - const handle = create(repos.source, GraphRealm); + const handle = create(repos.source, GraphRealm) - handle.change(transaction => { - assert.deepEqual(Object.keys(transaction).sort(), ["add", "root"]); - transaction.add("GraphRealm.V", []); - }); + handle.change((transaction) => { + assert.deepEqual(Object.keys(transaction).sort(), ["add", "root"]) + transaction.add("GraphRealm.V", []) + }) - assert.equal(handle.doc().scanTable("GraphRealm.V").length, 1); -}); + assert.equal(handle.doc().scanTable("GraphRealm.V").length, 1) +}) test("failed changes are aborted and leave the handle usable", () => { - const handle = create(repos.source, GraphRealm); - let committed!: Value; + const handle = create(repos.source, GraphRealm) + let committed!: Value assert.throws( () => handle.change((transaction) => { - transaction.root.V.add(); - throw new Error("abort"); + transaction.root.V.add() + throw new Error("abort") }), new Error("abort"), - ); + ) handle.change((transaction) => { - committed = transaction.root.V.add(); - }); + committed = transaction.root.V.add() + }) assert.deepEqual( handle @@ -84,82 +100,96 @@ test("failed changes are aborted and leave the handle usable", () => { .scanTable("GraphRealm.V") .map((row) => row.rowId), [committed], - ); -}); + ) +}) test("typed handles sync changes", async () => { - const handle = create(repos.source, GraphRealm); - const replica = await find(repos.replica, handle.url, GraphRealm); - let vertex!: Value; - - const changed = new Promise>(resolve => { - replica.once("change", payload => { - if (payload.doc) resolve(payload.doc); - }); - }); + const handle = create(repos.source, GraphRealm) + const replica = await find(repos.replica, handle.url, GraphRealm) + let vertex!: Value + + const changed = new Promise>((resolve) => { + replica.once("change", (payload) => { + if (payload.doc) resolve(payload.doc) + }) + }) handle.change((tx) => { - vertex = tx.root.V.add(); - }); - const changedDocument = await changed; + vertex = tx.root.V.add() + }) + const changedDocument = await changed - assert(changedDocument.root.V.has(vertex)); - assert.deepEqual(changedDocument.scanTable("GraphRealm.V").map(row => row.rowId), [vertex]); - assert(replica.doc().root.V.has(vertex)); -}); + assert(changedDocument.root.V.has(vertex)) + assert.deepEqual( + changedDocument.scanTable("GraphRealm.V").map((row) => row.rowId), + [vertex], + ) + assert(replica.doc().root.V.has(vertex)) +}) test("raw changes sync into a typed handle", async () => { - const handle = create(repos.source, GraphRealm); - const replica = await repos.replica.find(handle.url, colnDocType); - let vertex!: Value; + const handle = create(repos.source, GraphRealm) + const replica = await repos.replica.find(handle.url, colnDocType) + let vertex!: Value - const changed = waitForChange(handle); + const changed = waitForChange(handle) replica.change((transaction) => { - vertex = transaction.add("GraphRealm.V", []); - }); - await changed; + vertex = transaction.add("GraphRealm.V", []) + }) + await changed - assert(handle.doc().root.V.has(vertex)); -}); + assert(handle.doc().root.V.has(vertex)) +}) test("find returns a raw handle when bindings are omitted", async () => { - const handle = create(repos.source, GraphRealm); - const replica = await find(repos.replica, handle.url); + const handle = create(repos.source, GraphRealm) + const replica = await find(repos.replica, handle.url) - assert.deepEqual(Object.keys(replica.doc()).sort(), ["heads", "jsonIR", "rowById", "scanTable"]); -}); + assert.deepEqual(Object.keys(replica.doc()).sort(), [ + "heads", + "jsonIR", + "rowById", + "scanTable", + ]) +}) test("bindings can be applied to a raw found handle later", async () => { - const handle = create(repos.source, GraphRealm); - const rawReplica = await find(repos.replica, handle.url); - const replica = applyBindings(rawReplica, GraphRealm); + const handle = create(repos.source, GraphRealm) + const rawReplica = await find(repos.replica, handle.url) + const replica = applyBindings(rawReplica, GraphRealm) - assert.strictEqual(replica, rawReplica); - assert.strictEqual(applyBindings(replica, GraphRealm), replica); + assert.strictEqual(replica, rawReplica) + assert.strictEqual(applyBindings(replica, GraphRealm), replica) - let vertex!: Value; - replica.change(transaction => { - vertex = transaction.root.V.add(); - }); + let vertex!: Value + replica.change((transaction) => { + vertex = transaction.root.V.add() + }) - assert(replica.doc().root.V.has(vertex)); - assert.deepEqual(replica.doc().scanTable("GraphRealm.V").map(row => row.rowId), [vertex]); -}); + assert(replica.doc().root.V.has(vertex)) + assert.deepEqual( + replica + .doc() + .scanTable("GraphRealm.V") + .map((row) => row.rowId), + [vertex], + ) +}) test("applying different bindings to a bound handle is rejected", () => { - const handle = create(repos.source, GraphRealm); - const otherBindings = { ...GraphRealm }; + const handle = create(repos.source, GraphRealm) + const otherBindings = { ...GraphRealm } assert.throws( () => applyBindings(handle, otherBindings), new TypeError("Coln handle already uses different realm bindings"), - ); -}); + ) +}) test("applying bindings for a different schema is rejected", () => { - const handle = repos.source.create(schema, colnDocType); + const handle = repos.source.create(schema, colnDocType) assert.throws( () => applyBindings(handle, itemFfi), new TypeError("Realm bindings schema does not match Coln document schema"), - ); -}); + ) +}) diff --git a/packages/coln-repo/tests/helpers/TestRepoPair.ts b/packages/coln-repo/tests/helpers/TestRepoPair.ts index 707f79d2..324330c9 100644 --- a/packages/coln-repo/tests/helpers/TestRepoPair.ts +++ b/packages/coln-repo/tests/helpers/TestRepoPair.ts @@ -2,10 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT -import { - initSubduction, - Repo, -} from "@automerge/automerge-repo" +import { initSubduction, Repo } from "@automerge/automerge-repo" import { setTimeout as delay } from "node:timers/promises" import { TestNetworkAdapter } from "./TestNetworkAdapter" @@ -17,9 +14,12 @@ export interface TestRepoPair { export async function createTestRepoPair(): Promise { await initSubduction() - const [sourceAdapter, replicaAdapter] = TestNetworkAdapter.createConnectedPair() + const [sourceAdapter, replicaAdapter] = + TestNetworkAdapter.createConnectedPair() const source = new Repo({ - subductionAdapters: [{ adapter: sourceAdapter, serviceName: "test", role: "connect" }], + subductionAdapters: [ + { adapter: sourceAdapter, serviceName: "test", role: "connect" }, + ], subductionTimeouts: { healMaxAttempts: 0, syncMs: 1_000 }, }) const replica = new Repo({ @@ -41,8 +41,8 @@ export async function createTestRepoPair(): Promise { async shutdown() { await Promise.all([source.flush(), replica.flush()]) await Promise.all([ - source.subduction.then(subduction => subduction.disconnectAll()), - replica.subduction.then(subduction => subduction.disconnectAll()), + source.subduction.then((subduction) => subduction.disconnectAll()), + replica.subduction.then((subduction) => subduction.disconnectAll()), ]) await Promise.all([source.shutdown(), replica.shutdown()]) }, @@ -53,7 +53,7 @@ export function waitForChange(handle: { once(event: "change", listener: () => void): unknown }): Promise { return withTimeout( - new Promise(resolve => handle.once("change", () => resolve())), + new Promise((resolve) => handle.once("change", () => resolve())), "handle change", ) } @@ -61,7 +61,7 @@ export function waitForChange(handle: { function waitForConnection(repo: Repo): Promise { if (repo.isSubductionConnected()) return Promise.resolve() return withTimeout( - new Promise(resolve => { + new Promise((resolve) => { const onConnection = ({ connected }: { connected: boolean }) => { if (!connected) return repo.off("subduction-connection", onConnection) diff --git a/packages/coln-repo/tests/package/consumer.ts b/packages/coln-repo/tests/package/consumer.ts index e7d36bb0..aefeed0f 100644 --- a/packages/coln-repo/tests/package/consumer.ts +++ b/packages/coln-repo/tests/package/consumer.ts @@ -15,7 +15,9 @@ import { } from "@coln-project/repo" type Equal = - (() => Type extends Left ? 1 : 2) extends () => Type extends Right ? 1 : 2 + (() => Type extends Left ? 1 : 2) extends () => Type extends Right + ? 1 + : 2 ? true : false type Expect = Type @@ -30,24 +32,24 @@ const explicitUndefined = find(repo, url, undefined) const typed = find(repo, url, bindings) const optional = find(repo, url, optionalBindings) -void raw.then(handle => { +void raw.then((handle) => { handle.doc().scanTable("Example.Items") handle.fullDoc().store.heads() }) -void typed.then(handle => { +void typed.then((handle) => { handle.doc().root - handle.on("change", payload => { + handle.on("change", (payload) => { payload.doc?.root payload.handle.doc().root }) - handle.addListener("change", payload => payload.doc?.root) - handle.listeners("change").forEach(listener => listener) - handle.on("heads-changed", payload => { + handle.addListener("change", (payload) => payload.doc?.root) + handle.listeners("change").forEach((listener) => listener) + handle.on("heads-changed", (payload) => { payload.doc.store payload.handle.doc().root }) - handle.change(transaction => { + handle.change((transaction) => { const value: Value = transaction.add("Example.Items", []) value transaction.root @@ -60,14 +62,31 @@ export type TypeChecks = [ Expect>>, Expect>>, Expect>>>, - Expect>>>, - Expect["doc"]>, ColnDocument>>, Expect< - Equal["change"]>[0], ColnChange> + Equal>> + >, + Expect< + Equal< + ReturnType["doc"]>, + ColnDocument + > + >, + Expect< + Equal< + Parameters["change"]>[0], + ColnChange + > + >, + Expect< + Equal< + Parameters>[0], + ColnTransaction + > >, - Expect>[0], ColnTransaction>>, Expect>, - Expect, DocumentOperation | "root">>, + Expect< + Equal, DocumentOperation | "root"> + >, Expect>, Expect, "add" | "root">>, ] diff --git a/packages/coln-repo/tests/server.mjs b/packages/coln-repo/tests/server.mjs index d91e2fc5..a8cef1f9 100644 --- a/packages/coln-repo/tests/server.mjs +++ b/packages/coln-repo/tests/server.mjs @@ -21,11 +21,11 @@ class WebSocketTransport { constructor(socket) { this.#socket = socket - this.#closedPromise = new Promise(resolve => { + this.#closedPromise = new Promise((resolve) => { this.#resolveClosed = resolve }) - socket.on("message", data => { + socket.on("message", (data) => { const bytes = toUint8Array(data) const waiter = this.#waiters.shift() if (waiter) { @@ -36,7 +36,7 @@ class WebSocketTransport { } }) socket.on("close", () => this.#markClosed(new Error("WebSocket closed"))) - socket.on("error", error => this.#markClosed(error)) + socket.on("error", (error) => this.#markClosed(error)) } onDisconnect(callback) { @@ -46,7 +46,9 @@ class WebSocketTransport { async sendBytes(bytes) { if (this.#closed) throw new Error("WebSocket closed") await new Promise((resolve, reject) => { - this.#socket.send(Buffer.from(bytes), error => (error ? reject(error) : resolve())) + this.#socket.send(Buffer.from(bytes), (error) => + error ? reject(error) : resolve(), + ) }) } @@ -97,10 +99,13 @@ const subduction = new Subduction({ }) const server = new WebSocketServer({ port }) -server.on("connection", socket => { - subduction.acceptTransport(new WebSocketTransport(socket), serviceName).catch(error => { - if (!String(error?.message ?? error).includes("closed")) console.error(error) - }) +server.on("connection", (socket) => { + subduction + .acceptTransport(new WebSocketTransport(socket), serviceName) + .catch((error) => { + if (!String(error?.message ?? error).includes("closed")) + console.error(error) + }) }) async function shutdown() { diff --git a/packages/coln-repo/tests/sync.spec.ts b/packages/coln-repo/tests/sync.spec.ts index c4a154e5..9d22089d 100644 --- a/packages/coln-repo/tests/sync.spec.ts +++ b/packages/coln-repo/tests/sync.spec.ts @@ -15,7 +15,9 @@ import { itemSchema } from "./fixtures/schema" const table = "Test.Items" type RawHandle = CrdtDocHandle -test("browser and Node repos create, find, and update each other's stores", async ({ browser }) => { +test("browser and Node repos create, find, and update each other's stores", async ({ + browser, +}) => { await initSubduction() const context = await browser.newContext() const pages: Page[] = [] @@ -28,7 +30,7 @@ test("browser and Node repos create, find, and update each other's stores", asyn const browserCreator = await openPage(context.newPage()) pages.push(browserCreator) const browserUrl = await browserCreator.evaluate( - schema => window.colnTest.create(schema), + (schema) => window.colnTest.create(schema), itemSchema, ) await add(browserCreator, "browser-one") @@ -49,7 +51,7 @@ test("browser and Node repos create, find, and update each other's stores", asyn const browserFinder = await openPage(context.newPage()) pages.push(browserFinder) await browserFinder.evaluate( - url => window.colnTest.find(url), + (url) => window.colnTest.find(url), nodeCreator.url as AutomergeUrl, ) await expectRows(browserFinder, ["node-one"]) @@ -71,7 +73,9 @@ test("browser and Node repos create, find, and update each other's stores", asyn } }) -test("reloads without schema and applies typed bindings", async ({ browser }) => { +test("reloads without schema and applies typed bindings", async ({ + browser, +}) => { const context = await browser.newContext() const pages: Page[] = [] @@ -85,7 +89,7 @@ test("reloads without schema and applies typed bindings", async ({ browser }) => const finder = await openPage(context.newPage()) pages.push(finder) - await finder.evaluate(url => window.colnTest.find(url), url) + await finder.evaluate((url) => window.colnTest.find(url), url) await expectRows(finder, ["one"]) await finder.evaluate(() => window.colnTest.applyTypedBindings()) await expectTypedCount(finder, "one", 1) @@ -100,7 +104,7 @@ test("reloads without schema and applies typed bindings", async ({ browser }) => const reloaded = await openPage(context.newPage()) pages.push(reloaded) - await reloaded.evaluate(url => window.colnTest.find(url), url) + await reloaded.evaluate((url) => window.colnTest.find(url), url) await expectRows(reloaded, ["one", "two"]) await reloaded.evaluate(() => window.colnTest.applyTypedBindings()) await expectTypedCount(reloaded, "one", 1) @@ -124,14 +128,17 @@ async function openPage(pagePromise: Promise): Promise { async function shutdownPages(pages: Page[]): Promise { await Promise.all( pages - .filter(page => !page.isClosed()) - .map(page => page.evaluate(() => window.colnTest.shutdown()).catch(() => undefined)), + .filter((page) => !page.isClosed()) + .map((page) => + page.evaluate(() => window.colnTest.shutdown()).catch(() => undefined), + ), ) } async function add(page: Page, value: string) { await page.evaluate( - ({ table, value }) => window.colnTest.add(table, [{ tag: "string", value }]), + ({ table, value }) => + window.colnTest.add(table, [{ tag: "string", value }]), { table, value }, ) } @@ -140,12 +147,12 @@ async function expectRows(page: Page, values: string[]) { await expect .poll(() => page.evaluate( - table => + (table) => window.colnTest .rows(table) - .map(row => row.values[0]) - .filter(value => value?.tag === "string") - .map(value => value.value) + .map((row) => row.values[0]) + .filter((value) => value?.tag === "string") + .map((value) => value.value) .sort(), table, ), @@ -154,7 +161,9 @@ async function expectRows(page: Page, values: string[]) { } function addInNode(handle: RawHandle, value: string) { - handle.change(transaction => transaction.add(table, [{ tag: "string", value }])) + handle.change((transaction) => + transaction.add(table, [{ tag: "string", value }]), + ) } async function expectNodeRows(handle: RawHandle, values: string[]) { @@ -163,9 +172,9 @@ async function expectNodeRows(handle: RawHandle, values: string[]) { handle .doc() .scanTable(table) - .map(row => row.values[0]) - .filter(value => value?.tag === "string") - .map(value => value.value) + .map((row) => row.values[0]) + .filter((value) => value?.tag === "string") + .map((value) => value.value) .sort(), ) .toEqual([...values].sort()) @@ -174,7 +183,9 @@ async function expectNodeRows(handle: RawHandle, values: string[]) { async function expectSameHeads(page: Page, handle: RawHandle) { await expect .poll(async () => { - const browserHeads = (await page.evaluate(() => window.colnTest.heads())).sort() + const browserHeads = ( + await page.evaluate(() => window.colnTest.heads()) + ).sort() const nodeHeads = handle.doc().heads().sort() return browserHeads.join(",") === nodeHeads.join(",") ? "equal" @@ -185,6 +196,8 @@ async function expectSameHeads(page: Page, handle: RawHandle) { async function expectTypedCount(page: Page, value: string, count: number) { await expect - .poll(() => page.evaluate(value => window.colnTest.typedCount(value), value)) + .poll(() => + page.evaluate((value) => window.colnTest.typedCount(value), value), + ) .toBe(count) } From 33db8ba2ac10214618b66d51b53b9279fabd64bf Mon Sep 17 00:00:00 2001 From: Alex Currie-Clark Date: Fri, 4 Sep 2026 14:14:37 +0100 Subject: [PATCH 15/18] Shared sync server Automerge for the compiler demo Co-authored-by: acurrieclark <1306728+acurrieclark@users.noreply.github.com> --- examples/compiler/.gitignore | 1 - examples/compiler/package.json | 10 +- examples/compiler/pnpm-lock.yaml | 266 ++++++++++++++++++ examples/compiler/pnpm-workspace.yaml | 5 + examples/compiler/src/browser/App.svelte | 168 ++++++----- .../src/browser/DocumentLoadError.svelte | 62 ++++ .../compiler/src/browser/SourceEditor.svelte | 67 +---- examples/compiler/src/lib/examples.ts | 17 -- examples/compiler/src/lib/theory-document.ts | 39 +++ .../compiler/src/lib/theory-handle.svelte.ts | 22 ++ .../compiler/src/lib/theory-sync.svelte.ts | 97 +++++++ examples/compiler/src/main.ts | 70 ++++- examples/compiler/vite.config.ts | 3 +- examples/justfile | 6 +- examples/sync-server/README.md | 28 ++ examples/sync-server/package.json | 14 + .../{sync/server => sync-server}/server.mjs | 4 +- .../server => sync-server}/with-server.mjs | 3 +- examples/sync/README.md | 14 +- examples/sync/package.json | 11 +- examples/sync/playwright.config.ts | 2 +- examples/sync/pnpm-lock.yaml | 55 +++- examples/sync/src/browser/main.ts | 6 +- examples/sync/tests/sync.spec.ts | 48 ++++ 24 files changed, 839 insertions(+), 179 deletions(-) create mode 100644 examples/compiler/src/browser/DocumentLoadError.svelte delete mode 100644 examples/compiler/src/lib/examples.ts create mode 100644 examples/compiler/src/lib/theory-document.ts create mode 100644 examples/compiler/src/lib/theory-handle.svelte.ts create mode 100644 examples/compiler/src/lib/theory-sync.svelte.ts create mode 100644 examples/sync-server/README.md create mode 100644 examples/sync-server/package.json rename examples/{sync/server => sync-server}/server.mjs (98%) mode change 100644 => 100755 rename examples/{sync/server => sync-server}/with-server.mjs (96%) mode change 100644 => 100755 diff --git a/examples/compiler/.gitignore b/examples/compiler/.gitignore index 17948265..fc52e11d 100644 --- a/examples/compiler/.gitignore +++ b/examples/compiler/.gitignore @@ -1,4 +1,3 @@ dist/ node_modules/ public/dist/ -public/examples/ diff --git a/examples/compiler/package.json b/examples/compiler/package.json index b7514ac9..d167d8f1 100644 --- a/examples/compiler/package.json +++ b/examples/compiler/package.json @@ -4,11 +4,15 @@ "type": "module", "license": "(Apache-2.0 OR MIT)", "scripts": { - "dev": "vite --host 127.0.0.1", + "dev": "coln-with-sync-server vite --host 127.0.0.1", "check": "svelte-check --tsconfig ./tsconfig.json && tsc --noEmit", "build": "pnpm run check && vite build" }, "dependencies": { + "@automerge/automerge-codemirror": "0.2.0", + "@automerge/automerge-repo": "2.6.0-subduction.47", + "@automerge/automerge-repo-storage-indexeddb": "2.6.0-subduction.47", + "@automerge/automerge-subduction": "0.16.1", "@codemirror/state": "^6.7.2", "@codemirror/view": "^6.43.10", "codemirror": "^6.0.2", @@ -16,12 +20,14 @@ "svelte": "^5.57.0" }, "devDependencies": { + "@coln-project/demo-sync-server": "file:../sync-server", "@sveltejs/vite-plugin-svelte": "^7.3.0", "@tailwindcss/vite": "^4.3.3", "@types/node": "^26.4.1", "svelte-check": "^4.7.6", "tailwindcss": "^4.3.3", "typescript": "^6.0.3", - "vite": "^8.2.2" + "vite": "^8.2.2", + "vite-plugin-wasm": "^3.6.0" } } diff --git a/examples/compiler/pnpm-lock.yaml b/examples/compiler/pnpm-lock.yaml index 838c5ebe..dd9dcdf1 100644 --- a/examples/compiler/pnpm-lock.yaml +++ b/examples/compiler/pnpm-lock.yaml @@ -4,10 +4,27 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-repo': 2.6.0-subduction.47 + '@automerge/automerge-subduction': 0.16.1 + importers: .: dependencies: + '@automerge/automerge-codemirror': + specifier: 0.2.0 + version: 0.2.0 + '@automerge/automerge-repo': + specifier: 2.6.0-subduction.47 + version: 2.6.0-subduction.47 + '@automerge/automerge-repo-storage-indexeddb': + specifier: 2.6.0-subduction.47 + version: 2.6.0-subduction.47 + '@automerge/automerge-subduction': + specifier: 0.16.1 + version: 0.16.1 '@codemirror/state': specifier: ^6.7.2 version: 6.7.2 @@ -24,6 +41,9 @@ importers: specifier: ^5.57.0 version: 5.57.0 devDependencies: + '@coln-project/demo-sync-server': + specifier: file:../sync-server + version: file:../sync-server '@sveltejs/vite-plugin-svelte': specifier: ^7.3.0 version: 7.3.0(svelte@5.57.0)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) @@ -45,9 +65,59 @@ importers: vite: specifier: ^8.2.2 version: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + vite-plugin-wasm: + specifier: ^3.6.0 + version: 3.6.0(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) packages: + '@automerge/automerge-codemirror@0.2.0': + resolution: {integrity: sha512-GBNh4ESZHFNHZKLznf/2G2KdmBD+unEdk64+l621LsjIsJs6HBmQmO8RCY1v7vIq31kFyoVHNcBMGQOnLgy4GA==} + + '@automerge/automerge-repo-storage-indexeddb@2.6.0-subduction.47': + resolution: {integrity: sha512-fYn6nse7jQnVb9EploTsBuYUzW7xFin2HZLneSEQ7w4pRCilIU92ghZEjZSWRKEPunSN0MYEvfJJJdlLyYTdkQ==} + engines: {node: '>=22.13'} + + '@automerge/automerge-repo@2.6.0-subduction.47': + resolution: {integrity: sha512-NGBQUjGH67Kyrc8a6zoafvDKT1+pnFi2/yiMgULS7l+apWQT2QkgQZ3vQGms/ngdrmM6ye1fhcwHwBn/DGJ6tA==} + engines: {node: '>=22.13'} + + '@automerge/automerge-subduction@0.16.1': + resolution: {integrity: sha512-alH7U4eYn0O7sT6hNLv7CJhNODJi+QD6qfRQTNfZCxL3Q5JZdJ8lFpdvXUkBf+I3bu0GLvVlTaYPWGrBihjpIw==} + + '@automerge/automerge@3.3.2': + resolution: {integrity: sha512-9vCdCL7pdQwUra66SBxPVHr+/t9epXKni9KDeak2rNBFMzABVh2u6gSpcwxi3jkR5qr047jVqZv9DlrOfVxLFw==} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} + cpu: [arm64] + os: [darwin] + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + resolution: {integrity: sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==} + cpu: [x64] + os: [darwin] + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + resolution: {integrity: sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==} + cpu: [arm64] + os: [linux] + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + resolution: {integrity: sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==} + cpu: [arm] + os: [linux] + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + resolution: {integrity: sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==} + cpu: [x64] + os: [linux] + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + resolution: {integrity: sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==} + cpu: [x64] + os: [win32] + '@codemirror/autocomplete@6.20.3': resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} @@ -69,6 +139,10 @@ packages: '@codemirror/view@6.43.10': resolution: {integrity: sha512-vVWLvd4fKWYN/pMcwUrg6dWeublxmSz+V+52ZjW3h64IVN9cRUYLk5Km4JDT9vifxAFfDtNOIFxKYENthcolJQ==} + '@coln-project/demo-sync-server@file:../sync-server': + resolution: {directory: ../sync-server, type: directory} + hasBin: true + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -97,6 +171,10 @@ packages: '@marijn/find-cluster-break@1.0.4': resolution: {integrity: sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==} + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + '@oxc-project/types@0.148.0': resolution: {integrity: sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==} @@ -328,6 +406,22 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + + bs58check@4.0.0: + resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} + + cbor-extract@2.2.2: + resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} + hasBin: true + + cbor-x@1.6.6: + resolution: {integrity: sha512-8QiD9PGOxyQHo7s2pzwTBH6lTjqekxPdl9Aq6fXvZgCuCJHOht1puDEA/fTr6mciB76c+M+Gi0qT2i1a4pm4Wg==} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -342,6 +436,15 @@ packages: crelt@1.0.7: resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -368,6 +471,12 @@ packages: '@typescript-eslint/types': optional: true + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -388,6 +497,11 @@ packages: is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true @@ -556,11 +670,18 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.18: resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + obug@2.1.4: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} @@ -627,6 +748,15 @@ packages: undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + uuid@14.0.2: + resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} + hasBin: true + + vite-plugin-wasm@3.6.0: + resolution: {integrity: sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==} + peerDependencies: + vite: ^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + vite@8.2.2: resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -681,11 +811,81 @@ packages: w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xstate@5.32.6: + resolution: {integrity: sha512-WfA8WNrh6r9osuGwVm+aIPM4jmMW2LKHV1Yv9thYpOtL4HVF/kobh95K/O8v2jDCpDmP2EoY+6uvuqHXCZ6ZLw==} + zimmerframe@1.1.5: resolution: {integrity: sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==} snapshots: + '@automerge/automerge-codemirror@0.2.0': + dependencies: + '@automerge/automerge': 3.3.2 + '@codemirror/state': 6.7.2 + '@codemirror/view': 6.43.10 + codemirror: 6.0.2 + + '@automerge/automerge-repo-storage-indexeddb@2.6.0-subduction.47': + dependencies: + '@automerge/automerge-repo': 2.6.0-subduction.47 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-repo@2.6.0-subduction.47': + dependencies: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-subduction': 0.16.1 + bs58check: 4.0.0 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + isomorphic-ws: 5.0.0(ws@8.21.3) + uuid: 14.0.2 + ws: 8.21.3 + xstate: 5.32.6 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-subduction@0.16.1': {} + + '@automerge/automerge@3.3.2': {} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + optional: true + '@codemirror/autocomplete@6.20.3': dependencies: '@codemirror/language': 6.12.4 @@ -732,6 +932,14 @@ snapshots: style-mod: 4.1.3 w3c-keyname: 2.2.8 + '@coln-project/demo-sync-server@file:../sync-server': + dependencies: + '@automerge/automerge-subduction': 0.16.1 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.6.0 @@ -763,6 +971,8 @@ snapshots: '@marijn/find-cluster-break@1.0.4': {} + '@noble/hashes@1.8.0': {} + '@oxc-project/types@0.148.0': {} '@rolldown/binding-android-arm-eabi@1.2.7': @@ -907,6 +1117,33 @@ snapshots: axobject-query@4.1.0: {} + base-x@5.0.1: {} + + bs58@6.0.0: + dependencies: + base-x: 5.0.1 + + bs58check@4.0.0: + dependencies: + '@noble/hashes': 1.8.0 + bs58: 6.0.0 + + cbor-extract@2.2.2: + dependencies: + node-gyp-build-optional-packages: 5.1.1 + optionalDependencies: + '@cbor-extract/cbor-extract-darwin-arm64': 2.2.2 + '@cbor-extract/cbor-extract-darwin-x64': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm64': 2.2.2 + '@cbor-extract/cbor-extract-linux-x64': 2.2.2 + '@cbor-extract/cbor-extract-win32-x64': 2.2.2 + optional: true + + cbor-x@1.6.6: + optionalDependencies: + cbor-extract: 2.2.2 + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -925,6 +1162,10 @@ snapshots: crelt@1.0.7: {} + debug@4.4.3: + dependencies: + ms: 2.1.3 + deepmerge@4.3.1: {} detect-libc@2.1.2: {} @@ -942,6 +1183,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 + eventemitter3@5.0.4: {} + + fast-sha256@1.3.0: {} + fdir@6.5.0(picomatch@4.0.7): optionalDependencies: picomatch: 4.0.7 @@ -955,6 +1200,10 @@ snapshots: dependencies: '@types/estree': 1.0.9 + isomorphic-ws@5.0.0(ws@8.21.3): + dependencies: + ws: 8.21.3 + jiti@2.7.0: {} json-formatter-js@2.5.23: {} @@ -1069,8 +1318,15 @@ snapshots: mri@1.2.0: {} + ms@2.1.3: {} + nanoid@3.3.18: {} + node-gyp-build-optional-packages@5.1.1: + dependencies: + detect-libc: 2.1.2 + optional: true + obug@2.1.4: {} picocolors@1.1.1: {} @@ -1160,6 +1416,12 @@ snapshots: undici-types@8.3.0: {} + uuid@14.0.2: {} + + vite-plugin-wasm@3.6.0(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)): + dependencies: + vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0): dependencies: lightningcss: 1.33.0 @@ -1178,4 +1440,8 @@ snapshots: w3c-keyname@2.2.8: {} + ws@8.21.3: {} + + xstate@5.32.6: {} + zimmerframe@1.1.5: {} diff --git a/examples/compiler/pnpm-workspace.yaml b/examples/compiler/pnpm-workspace.yaml index 5ed0b5af..f2b949cd 100644 --- a/examples/compiler/pnpm-workspace.yaml +++ b/examples/compiler/pnpm-workspace.yaml @@ -1,2 +1,7 @@ allowBuilds: + cbor-extract: false esbuild: true +overrides: + "@automerge/automerge": 3.3.2 + "@automerge/automerge-repo": 2.6.0-subduction.47 + "@automerge/automerge-subduction": 0.16.1 diff --git a/examples/compiler/src/browser/App.svelte b/examples/compiler/src/browser/App.svelte index d76f2bae..98b91ed2 100644 --- a/examples/compiler/src/browser/App.svelte +++ b/examples/compiler/src/browser/App.svelte @@ -2,37 +2,49 @@ @@ -138,16 +149,30 @@
+ {#if feedback} +

+ {feedback} +

+ {/if} +
- + C COLN / COMPILER -
-
- {statusLabel} +
+ +
+
+ {statusLabel} +
+
+ {syncStatus} + / {sourceLines} {sourceLines === 1 ? "line" : "lines"} +
- {sourceLines} {sourceLines === 1 ? "line" : "lines"}
@@ -156,25 +181,11 @@

SOURCE

-

Edit source to compile

+

Collaborative Automerge document

- + Live theory
- +
@@ -183,6 +194,11 @@ {error} {/if} + {#if syncStatus === "error"} + + {/if} {#if compilation.diagnosticsHtml.length === 0} diff --git a/examples/compiler/src/browser/DocumentLoadError.svelte b/examples/compiler/src/browser/DocumentLoadError.svelte new file mode 100644 index 00000000..4c6c9537 --- /dev/null +++ b/examples/compiler/src/browser/DocumentLoadError.svelte @@ -0,0 +1,62 @@ + + + + + + + {title} | Coln Compiler + + +
+
+
+ C + COLN / COMPILER +
+ +
+

{title.toUpperCase()}

+

{heading}

+

{detail}

+
+ + {documentUrl} + + + Create a new theory + + +
+
diff --git a/examples/compiler/src/browser/SourceEditor.svelte b/examples/compiler/src/browser/SourceEditor.svelte index a72f870d..2a011ced 100644 --- a/examples/compiler/src/browser/SourceEditor.svelte +++ b/examples/compiler/src/browser/SourceEditor.svelte @@ -2,26 +2,15 @@
diff --git a/examples/compiler/src/lib/examples.ts b/examples/compiler/src/lib/examples.ts deleted file mode 100644 index 270efd24..00000000 --- a/examples/compiler/src/lib/examples.ts +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// -// SPDX-License-Identifier: Apache-2.0 OR MIT - -const examplesUrl = `${import.meta.env.BASE_URL}examples/` - -export async function loadExampleNames(): Promise { - const response = await fetch(`${examplesUrl}index.json`) - if (!response.ok) throw new Error(`Could not load examples (${response.status})`) - return response.json() as Promise -} - -export async function loadExample(name: string): Promise { - const response = await fetch(`${examplesUrl}${encodeURIComponent(name)}`) - if (!response.ok) throw new Error(`Could not load ${name} (${response.status})`) - return response.text() -} diff --git a/examples/compiler/src/lib/theory-document.ts b/examples/compiler/src/lib/theory-document.ts new file mode 100644 index 00000000..f5f9b2b5 --- /dev/null +++ b/examples/compiler/src/lib/theory-document.ts @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { DocHandle } from "@automerge/automerge-repo" + +export type RealmSchema = { + entities: unknown[] + rules: unknown[] +} + +export type StoreRecord = { + url: string + createdAt: number + sourceHeads: string[] + realmIndex: number + realmName: string + ir: RealmSchema +} + +export type TheoryDocument = { + version: 1 + source: string + stores: StoreRecord[] +} + +export type TheoryDocumentHandle = DocHandle + +export function newTheoryDocument(): TheoryDocument { + return { version: 1, source: "", stores: [] } +} + +export function isTheoryDocument(value: unknown): value is TheoryDocument { + if (typeof value !== "object" || value === null) return false + const document = value as Partial + return document.version === 1 + && typeof document.source === "string" + && Array.isArray(document.stores) +} diff --git a/examples/compiler/src/lib/theory-handle.svelte.ts b/examples/compiler/src/lib/theory-handle.svelte.ts new file mode 100644 index 00000000..6402fafb --- /dev/null +++ b/examples/compiler/src/lib/theory-handle.svelte.ts @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { createSubscriber } from "svelte/reactivity" +import type { TheoryDocument, TheoryDocumentHandle } from "./theory-document.ts" + +export class TheoryHandle { + readonly #subscribe: () => void + + constructor(private readonly handle: TheoryDocumentHandle) { + this.#subscribe = createSubscriber(update => { + handle.on("change", update) + return () => handle.off("change", update) + }) + } + + get state(): TheoryDocument { + this.#subscribe() + return this.handle.doc() + } +} diff --git a/examples/compiler/src/lib/theory-sync.svelte.ts b/examples/compiler/src/lib/theory-sync.svelte.ts new file mode 100644 index 00000000..bb7dfd53 --- /dev/null +++ b/examples/compiler/src/lib/theory-sync.svelte.ts @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { Repo } from "@automerge/automerge-repo" +import { createSubscriber } from "svelte/reactivity" +import type { TheoryDocumentHandle } from "./theory-document.ts" + +export type SyncStatus = "offline" | "syncing" | "synced" | "error" + +export class TheorySync { + readonly #subscribe: () => void + #status: SyncStatus + #error: unknown + + constructor( + private readonly repo: Repo, + private readonly handle: TheoryDocumentHandle, + ) { + this.#status = repo.isSubductionConnected() ? "syncing" : "offline" + this.#subscribe = createSubscriber(update => { + let active = true + let generation = 0 + let currentAttempt: symbol | undefined + + const setStatus = (status: SyncStatus, error?: unknown) => { + if (this.#status === status && this.#error === error) return + this.#status = status + this.#error = error + update() + } + + const startFlush = () => { + if (currentAttempt || !active || !this.repo.isSubductionConnected()) return + const attempt = Symbol() + const flushGeneration = generation + currentAttempt = attempt + void this.repo.flush([this.handle.documentId]).then( + () => finishFlush(attempt, flushGeneration, false), + error => finishFlush(attempt, flushGeneration, true, error), + ) + } + + const finishFlush = ( + attempt: symbol, + flushGeneration: number, + failed: boolean, + error?: unknown, + ) => { + if (!active || currentAttempt !== attempt) return + currentAttempt = undefined + if (!this.repo.isSubductionConnected()) { + setStatus("offline") + } else if (flushGeneration !== generation) { + startFlush() + } else if (failed) { + setStatus("error", error) + } else { + setStatus("synced") + } + } + + const requestFlush = () => { + generation += 1 + if (!this.repo.isSubductionConnected()) { + currentAttempt = undefined + setStatus("offline") + return + } + setStatus("syncing") + startFlush() + } + + this.handle.on("change", requestFlush) + this.repo.on("subduction-connection", requestFlush) + requestFlush() + + return () => { + active = false + generation += 1 + currentAttempt = undefined + this.handle.off("change", requestFlush) + this.repo.off("subduction-connection", requestFlush) + } + }) + } + + get status(): SyncStatus { + this.#subscribe() + return this.#status + } + + get error(): unknown { + this.#subscribe() + return this.#error + } +} diff --git a/examples/compiler/src/main.ts b/examples/compiler/src/main.ts index 794836e4..47632c19 100644 --- a/examples/compiler/src/main.ts +++ b/examples/compiler/src/main.ts @@ -2,8 +2,76 @@ // // SPDX-License-Identifier: Apache-2.0 OR MIT +import { + Repo, + isValidAutomergeUrl, + type AutomergeUrl, +} from "@automerge/automerge-repo" +import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb" +// @ts-expect-error initSync is exported at runtime but absent from declarations +import { initSync } from "@automerge/automerge-subduction/slim" +// @ts-expect-error wasm-base64 is exported without declarations +import { wasmBase64 } from "@automerge/automerge-subduction/wasm-base64" import { mount } from "svelte" import App from "./browser/App.svelte" +import DocumentLoadError from "./browser/DocumentLoadError.svelte" +import { + isTheoryDocument, + newTheoryDocument, + type TheoryDocument, +} from "./lib/theory-document.ts" import "./style.css" -mount(App, { target: document.getElementById("app")! }) +const target = document.getElementById("app")! +const hashUrl = location.hash.slice(1) + +if (hashUrl && !isValidAutomergeUrl(hashUrl)) { + mount(DocumentLoadError, { + target, + props: { documentUrl: hashUrl, kind: "invalid" }, + }) +} else { + initSync({ module: Uint8Array.from(atob(wasmBase64), char => char.charCodeAt(0)) }) + + const endpoint = import.meta.env.VITE_SUBDUCTION_ENDPOINT + ?? "wss://subduction.sync.inkandswitch.com" + const repo = new Repo({ + storage: new IndexedDBStorageAdapter("coln-compiler", "documents"), + subductionWebsocketEndpoints: [endpoint], + }) + + try { + const handle = hashUrl + ? await repo.find(hashUrl as AutomergeUrl) + : repo.create(newTheoryDocument()) + + if (!isTheoryDocument(handle.doc())) { + await repo.shutdown() + mount(DocumentLoadError, { + target, + props: { documentUrl: hashUrl, kind: "incompatible" }, + }) + } else { + if (!hashUrl) location.hash = handle.url + const flush = () => void repo.flush([handle.documentId]) + addEventListener("pagehide", flush, { once: true }) + + if (import.meta.env.DEV) Object.assign(window, { repo, handle }) + mount(App, { + target, + props: { repo, handle, endpoint }, + }) + } + } catch (cause) { + await repo.shutdown().catch(() => undefined) + if (!isDocumentUnavailable(cause)) throw cause + mount(DocumentLoadError, { + target, + props: { documentUrl: hashUrl, kind: "unavailable" }, + }) + } +} + +function isDocumentUnavailable(cause: unknown): boolean { + return cause instanceof Error && /^Document .+ is unavailable$/.test(cause.message) +} diff --git a/examples/compiler/vite.config.ts b/examples/compiler/vite.config.ts index d1aa6594..c110956d 100644 --- a/examples/compiler/vite.config.ts +++ b/examples/compiler/vite.config.ts @@ -5,10 +5,11 @@ import { defineConfig } from "vite" import { svelte } from "@sveltejs/vite-plugin-svelte" import tailwindcss from "@tailwindcss/vite" +import wasm from "vite-plugin-wasm" export default defineConfig({ base: process.env.VITE_BASE || "/", - plugins: [tailwindcss(), svelte()], + plugins: [tailwindcss(), wasm(), svelte()], build: { emptyOutDir: true, outDir: "../../_build/web/compiler-app", diff --git a/examples/justfile b/examples/justfile index 507ed526..abf5de31 100644 --- a/examples/justfile +++ b/examples/justfile @@ -19,15 +19,13 @@ serve-web-compiler port="8000": build-web-compiler stage-web-compiler-app: #!/usr/bin/env bash set -euxo pipefail - if [[ ! -f "{{ build_dir }}/dist/coln.wasm" || ! -f "{{ build_dir }}/dist/ghc_wasm_jsffi.js" || ! -f "{{ build_dir }}/dist/loadHaskellWasm.js" || ! -f "{{ build_dir }}/examples/index.json" ]] || ! compgen -G "{{ build_dir }}/examples/*.coln" > /dev/null; then + if [[ ! -f "{{ build_dir }}/dist/coln.wasm" || ! -f "{{ build_dir }}/dist/ghc_wasm_jsffi.js" || ! -f "{{ build_dir }}/dist/loadHaskellWasm.js" ]]; then just build-web-compiler fi - mkdir -p "{{ compiler_app_dir }}/public/dist" "{{ compiler_app_dir }}/public/examples" + mkdir -p "{{ compiler_app_dir }}/public/dist" cp "{{ build_dir }}/dist/coln.wasm" "{{ compiler_app_dir }}/public/dist/coln.wasm" cp "{{ build_dir }}/dist/ghc_wasm_jsffi.js" "{{ compiler_app_dir }}/public/dist/ghc_wasm_jsffi.js" cp "{{ build_dir }}/dist/loadHaskellWasm.js" "{{ compiler_app_dir }}/public/dist/loadHaskellWasm.js" - cp "{{ build_dir }}/examples/index.json" "{{ compiler_app_dir }}/public/examples/index.json" - cp "{{ build_dir }}"/examples/*.coln "{{ compiler_app_dir }}/public/examples/" build-web-compiler-app: stage-web-compiler-app pnpm --dir {{ compiler_app_dir }} build diff --git a/examples/sync-server/README.md b/examples/sync-server/README.md new file mode 100644 index 00000000..6df816c4 --- /dev/null +++ b/examples/sync-server/README.md @@ -0,0 +1,28 @@ +# Coln demo sync server + +This private package provides the local, in-memory Subduction relay shared by +the current Coln demos. + +Add it to a demo's development dependencies: + +```json +{ + "devDependencies": { + "@coln-project/demo-sync-server": "file:../sync-server" + }, + "scripts": { + "dev": "coln-with-sync-server vite --host 127.0.0.1", + "server": "coln-sync-server" + } +} +``` + +`coln-with-sync-server` reuses a compatible relay on port 3030 or starts one, +holds a lease while its child command runs, and sets both +`SUBDUCTION_ENDPOINT` and `VITE_SUBDUCTION_ENDPOINT`. This lets browser and Node +demos share one relay. Set `SUBDUCTION_PORT` to use another managed port. + +`coln-sync-server` runs the relay directly. Set `PORT` and +`SUBDUCTION_SERVICE_NAME` to configure it. Data is held in memory and disappears +when the relay exits. A directly started relay remains running when demo leases +close and must be stopped explicitly. diff --git a/examples/sync-server/package.json b/examples/sync-server/package.json new file mode 100644 index 00000000..19c38a91 --- /dev/null +++ b/examples/sync-server/package.json @@ -0,0 +1,14 @@ +{ + "name": "@coln-project/demo-sync-server", + "private": true, + "type": "module", + "license": "(Apache-2.0 OR MIT)", + "bin": { + "coln-sync-server": "./server.mjs", + "coln-with-sync-server": "./with-server.mjs" + }, + "dependencies": { + "@automerge/automerge-subduction": "0.16.1", + "ws": "^8.21.0" + } +} diff --git a/examples/sync/server/server.mjs b/examples/sync-server/server.mjs old mode 100644 new mode 100755 similarity index 98% rename from examples/sync/server/server.mjs rename to examples/sync-server/server.mjs index fb8fae7f..77ed6890 --- a/examples/sync/server/server.mjs +++ b/examples/sync-server/server.mjs @@ -1,3 +1,4 @@ +#!/usr/bin/env node // SPDX-FileCopyrightText: 2026 Coln contributors // // SPDX-License-Identifier: Apache-2.0 OR MIT @@ -100,7 +101,7 @@ const subduction = new Subduction({ const wss = new WebSocketServer({ host, port }) const leases = new Set() const clients = new Set() -let managed = process.env.COLN_SYNC_MANAGED === "1" +const managed = process.env.COLN_SYNC_MANAGED === "1" let shutdownTimer let shuttingDown = false @@ -111,7 +112,6 @@ wss.on("connection", (ws, request) => { } if (request.url === "/.coln-sync/lease") { clearTimeout(shutdownTimer) - managed = true leases.add(ws) ws.send(JSON.stringify({ protocol: "coln-sync-relay", pid: process.pid })) ws.once("close", () => { diff --git a/examples/sync/server/with-server.mjs b/examples/sync-server/with-server.mjs old mode 100644 new mode 100755 similarity index 96% rename from examples/sync/server/with-server.mjs rename to examples/sync-server/with-server.mjs index 53f9d732..d149aa4a --- a/examples/sync/server/with-server.mjs +++ b/examples/sync-server/with-server.mjs @@ -1,3 +1,4 @@ +#!/usr/bin/env node // SPDX-FileCopyrightText: 2026 Coln contributors // // SPDX-License-Identifier: Apache-2.0 OR MIT @@ -9,7 +10,7 @@ import WebSocket from "ws" const [, , command, ...args] = process.argv if (!command) { - console.error("Usage: node server/with-server.mjs [args...]") + console.error("Usage: coln-with-sync-server [args...]") process.exit(1) } diff --git a/examples/sync/README.md b/examples/sync/README.md index 2e67ebee..c991edb8 100644 --- a/examples/sync/README.md +++ b/examples/sync/README.md @@ -24,9 +24,15 @@ pnpm --dir examples/sync install pnpm --dir examples/sync dev ``` +The development command starts or reuses the shared relay from +`examples/sync-server`. Other current demos use the same managed relay, which +stays alive until the last demo exits. + Open the Vite URL. A URL without an Automerge document in its hash creates a new document. Use the document URL shown in the inspector to open the same graph -in another browser or the CLI. +in another browser or the CLI. The browser persists graph and synchronization +data in IndexedDB so it survives page reloads. The current custom-document Repo +still requires a successful relay connection before hydrating cached graph data. ## CLI @@ -91,6 +97,6 @@ pnpm --dir examples/sync exec playwright install chromium # first time only pnpm --dir examples/sync test:e2e ``` -The end-to-end scenario exercises browser mutations, Node-side TypeScript query -execution, synchronization in both directions, and loading the resulting graph -in a second browser. +The end-to-end scenarios exercise browser mutations, Node-side TypeScript query +execution, synchronization in both directions, loading the resulting graph in a +second browser, and writing durable browser data to IndexedDB. diff --git a/examples/sync/package.json b/examples/sync/package.json index 7e88209a..b3e96cb8 100644 --- a/examples/sync/package.json +++ b/examples/sync/package.json @@ -4,9 +4,9 @@ "type": "module", "license": "(Apache-2.0 OR MIT)", "scripts": { - "dev": "node server/with-server.mjs vite --host 127.0.0.1", - "cli": "node server/with-server.mjs tsx src/cli/main.tsx", - "server": "node server/server.mjs", + "dev": "coln-with-sync-server vite --host 127.0.0.1", + "cli": "coln-with-sync-server tsx src/cli/main.tsx", + "server": "coln-sync-server", "compile": "bash ./compile.sh", "check": "svelte-check --tsconfig ./tsconfig.json && tsc --noEmit", "build": "pnpm run compile && pnpm run check && vite build", @@ -15,6 +15,7 @@ }, "dependencies": { "@automerge/automerge-repo": "github:automerge/automerge-repo#doctypes&path:packages/automerge-repo", + "@automerge/automerge-repo-storage-indexeddb": "2.6.0-subduction.47", "@automerge/automerge-subduction": "0.16.1", "@coln-project/repo": "link:../../packages/coln-repo", "@coln-project/runtime": "link:../../packages/coln-js-runtime", @@ -22,10 +23,10 @@ "ink-text-input": "^6.0.0", "react": "^19.0.0", "svelte": "^5.7.0", - "typescript": "^6.0.0", - "ws": "^8.21.0" + "typescript": "^6.0.0" }, "devDependencies": { + "@coln-project/demo-sync-server": "file:../sync-server", "@playwright/test": "^1.61.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", "@tailwindcss/vite": "^4.0.0", diff --git a/examples/sync/playwright.config.ts b/examples/sync/playwright.config.ts index e28cf2ab..4b69ea4e 100644 --- a/examples/sync/playwright.config.ts +++ b/examples/sync/playwright.config.ts @@ -16,7 +16,7 @@ export default defineConfig({ }, webServer: [ { - command: "SUBDUCTION_SERVICE_NAME=127.0.0.1:3031 PORT=3031 node server/server.mjs", + command: "SUBDUCTION_SERVICE_NAME=127.0.0.1:3031 PORT=3031 coln-sync-server", port: 3031, reuseExistingServer: false, stdout: "pipe", diff --git a/examples/sync/pnpm-lock.yaml b/examples/sync/pnpm-lock.yaml index 3e56fe5a..da9ad9dc 100644 --- a/examples/sync/pnpm-lock.yaml +++ b/examples/sync/pnpm-lock.yaml @@ -15,6 +15,9 @@ importers: '@automerge/automerge-repo': specifier: github:automerge/automerge-repo#doctypes&path:packages/automerge-repo version: https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo + '@automerge/automerge-repo-storage-indexeddb': + specifier: 2.6.0-subduction.47 + version: 2.6.0-subduction.47 '@automerge/automerge-subduction': specifier: 0.16.1 version: 0.16.1 @@ -39,10 +42,10 @@ importers: typescript: specifier: ^6.0.0 version: 6.0.3 - ws: - specifier: ^8.21.0 - version: 8.21.3 devDependencies: + '@coln-project/demo-sync-server': + specifier: file:../sync-server + version: file:../sync-server '@playwright/test': specifier: ^1.61.0 version: 1.62.1 @@ -86,6 +89,14 @@ packages: resolution: {integrity: sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw==} engines: {node: '>=18'} + '@automerge/automerge-repo-storage-indexeddb@2.6.0-subduction.47': + resolution: {integrity: sha512-fYn6nse7jQnVb9EploTsBuYUzW7xFin2HZLneSEQ7w4pRCilIU92ghZEjZSWRKEPunSN0MYEvfJJJdlLyYTdkQ==} + engines: {node: '>=22.13'} + + '@automerge/automerge-repo@2.6.0-subduction.47': + resolution: {integrity: sha512-NGBQUjGH67Kyrc8a6zoafvDKT1+pnFi2/yiMgULS7l+apWQT2QkgQZ3vQGms/ngdrmM6ye1fhcwHwBn/DGJ6tA==} + engines: {node: '>=22.13'} + '@automerge/automerge-repo@https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo': resolution: {gitHosted: true, path: packages/automerge-repo, tarball: https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5} version: 2.6.0-subduction.47 @@ -130,6 +141,10 @@ packages: cpu: [x64] os: [win32] + '@coln-project/demo-sync-server@file:../sync-server': + resolution: {directory: ../sync-server, type: directory} + hasBin: true + '@esbuild/aix-ppc64@0.28.2': resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} engines: {node: '>=18'} @@ -1319,6 +1334,32 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + '@automerge/automerge-repo-storage-indexeddb@2.6.0-subduction.47': + dependencies: + '@automerge/automerge-repo': 2.6.0-subduction.47 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-repo@2.6.0-subduction.47': + dependencies: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-subduction': 0.16.1 + bs58check: 4.0.0 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + isomorphic-ws: 5.0.0(ws@8.21.3) + uuid: 14.0.2 + ws: 8.21.3 + xstate: 5.32.5 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + '@automerge/automerge-repo@https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo': dependencies: '@automerge/automerge': 3.3.2 @@ -1361,6 +1402,14 @@ snapshots: '@cbor-extract/cbor-extract-win32-x64@2.2.2': optional: true + '@coln-project/demo-sync-server@file:../sync-server': + dependencies: + '@automerge/automerge-subduction': 0.16.1 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@esbuild/aix-ppc64@0.28.2': optional: true diff --git a/examples/sync/src/browser/main.ts b/examples/sync/src/browser/main.ts index 98278611..f0d24ad8 100644 --- a/examples/sync/src/browser/main.ts +++ b/examples/sync/src/browser/main.ts @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT import { Repo, isValidAutomergeUrl } from "@automerge/automerge-repo" +import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb" import { create, find } from "@coln-project/repo" // @ts-expect-error initSync is exported at runtime but absent from declarations import { initSync } from "@automerge/automerge-subduction/slim" @@ -28,7 +29,10 @@ if (hashUrl && !documentUrl) { props: { documentUrl: hashUrl, kind: "invalid" }, }) } else { - const repo = new Repo({ subductionWebsocketEndpoints: [endpoint] }) + const repo = new Repo({ + storage: new IndexedDBStorageAdapter("coln-sync"), + subductionWebsocketEndpoints: [endpoint], + }) try { const handle = documentUrl diff --git a/examples/sync/tests/sync.spec.ts b/examples/sync/tests/sync.spec.ts index 920bc0c9..660c5176 100644 --- a/examples/sync/tests/sync.spec.ts +++ b/examples/sync/tests/sync.spec.ts @@ -16,6 +16,7 @@ test.beforeAll(async () => { }) test("browser controls and TypeScript queries converge", async ({ browser }) => { + test.setTimeout(60_000) const context = await browser.newContext() await context.grantPermissions(["clipboard-read", "clipboard-write"], { origin: "http://127.0.0.1:5176", @@ -107,6 +108,37 @@ test("browser controls and TypeScript queries converge", async ({ browser }) => } }) +test("browser persists graph data in IndexedDB", async ({ browser }) => { + const context = await browser.newContext() + const page = await context.newPage() + + try { + await page.goto("/") + await expect(page.getByTestId("sync-status")).toHaveAttribute("data-status", "synced") + + await page.getByTestId("add-vertex").click() + await page.getByTestId("add-vertex").click() + const vertices = await page.getByTestId("from-select").locator("option").evaluateAll(options => + options.slice(1).map(option => (option as HTMLOptionElement).value), + ) + await page.getByTestId("from-select").selectOption(vertices[0]) + await page.getByTestId("to-select").selectOption(vertices[1]) + await page.getByTestId("add-edge").click() + await expect(page.getByTestId("sync-status")).toHaveAttribute("data-status", "synced") + await expect.poll(() => indexedDbRecordCount(page, "coln-sync", "documents")).toBeGreaterThan(0) + + const url = await page.getByTestId("doc-url").inputValue() + await page.close() + + const reloadedPage = await context.newPage() + await reloadedPage.goto(`/#${url}`) + await expect(reloadedPage.getByTestId("graph-vertex")).toHaveCount(2) + await expect(reloadedPage.getByTestId("graph-edge")).toHaveCount(1) + } finally { + await context.close() + } +}) + test("shows a dedicated screen when a document is unavailable", async ({ browser }) => { const isolatedRepo = new Repo() const unavailableUrl = isolatedRepo.create().url @@ -152,3 +184,19 @@ async function vertexLabels(page: import("@playwright/test").Page) { ) return new Map(entries) } + +async function indexedDbRecordCount( + page: import("@playwright/test").Page, + database: string, + store: string, +) { + return page.evaluate(({ database, store }) => new Promise((resolve, reject) => { + const request = indexedDB.open(database) + request.onerror = () => reject(request.error) + request.onsuccess = () => { + const count = request.result.transaction(store).objectStore(store).count() + count.onerror = () => reject(count.error) + count.onsuccess = () => resolve(count.result) + } + }), { database, store }) +} From 8a1f5012075d86f18050106670ef3803fe68bf12 Mon Sep 17 00:00:00 2001 From: Alex Currie-Clark Date: Fri, 4 Sep 2026 22:29:23 +0100 Subject: [PATCH 16/18] Editor and compiler store generation Co-authored-by: acurrieclark <1306728+acurrieclark@users.noreply.github.com> --- examples/compiler/package.json | 3 +- examples/compiler/pnpm-lock.yaml | 31 +- examples/compiler/pnpm-workspace.yaml | 1 - examples/compiler/src/browser/App.svelte | 99 +- .../compiler/src/browser/StoresPanel.svelte | 110 + examples/compiler/src/lib/compiled-realms.ts | 34 + examples/compiler/src/lib/stores.ts | 37 + examples/compiler/src/lib/theory-document.ts | 20 + .../compiler/src/lib/theory-handle.svelte.ts | 20 +- examples/compiler/src/main.ts | 2 +- examples/editor/.gitignore | 3 + examples/editor/README.md | 16 + examples/editor/index.html | 19 + examples/editor/package.json | 39 + examples/editor/playwright.config.ts | 40 + examples/editor/pnpm-lock.yaml | 1846 +++++++++++++++++ examples/editor/pnpm-workspace.yaml | 7 + examples/editor/src/browser/App.svelte | 237 +++ examples/editor/src/browser/ReplEditor.svelte | 114 + examples/editor/src/browser/ReplOutput.svelte | 32 + .../editor/src/browser/StoreLoader.svelte | 56 + .../editor/src/browser/TableBrowser.svelte | 97 + examples/editor/src/lib/coln-handle.svelte.ts | 24 + examples/editor/src/lib/evaluate.ts | 96 + examples/editor/src/lib/format.ts | 61 + examples/editor/src/lib/schema.ts | 109 + examples/editor/src/lib/snapshot.ts | 81 + examples/editor/src/lib/source-storage.ts | 24 + .../editor/src/lib/subduction-sync.svelte.ts | 88 + examples/editor/src/main.ts | 23 + examples/editor/src/style.css | 23 + examples/editor/tests/editor.spec.ts | 129 ++ examples/editor/tests/evaluate.unit.ts | 34 + examples/editor/tests/format.unit.ts | 28 + examples/editor/tests/schema.unit.ts | 57 + examples/editor/tests/snapshot.unit.ts | 41 + examples/editor/tests/source-storage.unit.ts | 31 + examples/editor/tsconfig.json | 26 + examples/editor/vite.config.ts | 18 + examples/editor/vitest.config.ts | 11 + 40 files changed, 3755 insertions(+), 12 deletions(-) create mode 100644 examples/compiler/src/browser/StoresPanel.svelte create mode 100644 examples/compiler/src/lib/compiled-realms.ts create mode 100644 examples/compiler/src/lib/stores.ts create mode 100644 examples/editor/.gitignore create mode 100644 examples/editor/README.md create mode 100644 examples/editor/index.html create mode 100644 examples/editor/package.json create mode 100644 examples/editor/playwright.config.ts create mode 100644 examples/editor/pnpm-lock.yaml create mode 100644 examples/editor/pnpm-workspace.yaml create mode 100644 examples/editor/src/browser/App.svelte create mode 100644 examples/editor/src/browser/ReplEditor.svelte create mode 100644 examples/editor/src/browser/ReplOutput.svelte create mode 100644 examples/editor/src/browser/StoreLoader.svelte create mode 100644 examples/editor/src/browser/TableBrowser.svelte create mode 100644 examples/editor/src/lib/coln-handle.svelte.ts create mode 100644 examples/editor/src/lib/evaluate.ts create mode 100644 examples/editor/src/lib/format.ts create mode 100644 examples/editor/src/lib/schema.ts create mode 100644 examples/editor/src/lib/snapshot.ts create mode 100644 examples/editor/src/lib/source-storage.ts create mode 100644 examples/editor/src/lib/subduction-sync.svelte.ts create mode 100644 examples/editor/src/main.ts create mode 100644 examples/editor/src/style.css create mode 100644 examples/editor/tests/editor.spec.ts create mode 100644 examples/editor/tests/evaluate.unit.ts create mode 100644 examples/editor/tests/format.unit.ts create mode 100644 examples/editor/tests/schema.unit.ts create mode 100644 examples/editor/tests/snapshot.unit.ts create mode 100644 examples/editor/tests/source-storage.unit.ts create mode 100644 examples/editor/tsconfig.json create mode 100644 examples/editor/vite.config.ts create mode 100644 examples/editor/vitest.config.ts diff --git a/examples/compiler/package.json b/examples/compiler/package.json index d167d8f1..387cbd60 100644 --- a/examples/compiler/package.json +++ b/examples/compiler/package.json @@ -10,11 +10,12 @@ }, "dependencies": { "@automerge/automerge-codemirror": "0.2.0", - "@automerge/automerge-repo": "2.6.0-subduction.47", + "@automerge/automerge-repo": "github:automerge/automerge-repo#doctypes&path:packages/automerge-repo", "@automerge/automerge-repo-storage-indexeddb": "2.6.0-subduction.47", "@automerge/automerge-subduction": "0.16.1", "@codemirror/state": "^6.7.2", "@codemirror/view": "^6.43.10", + "@coln-project/repo": "link:../../packages/coln-repo", "codemirror": "^6.0.2", "json-formatter-js": "^2.5.23", "svelte": "^5.57.0" diff --git a/examples/compiler/pnpm-lock.yaml b/examples/compiler/pnpm-lock.yaml index dd9dcdf1..e94f837a 100644 --- a/examples/compiler/pnpm-lock.yaml +++ b/examples/compiler/pnpm-lock.yaml @@ -6,7 +6,6 @@ settings: overrides: '@automerge/automerge': 3.3.2 - '@automerge/automerge-repo': 2.6.0-subduction.47 '@automerge/automerge-subduction': 0.16.1 importers: @@ -17,8 +16,8 @@ importers: specifier: 0.2.0 version: 0.2.0 '@automerge/automerge-repo': - specifier: 2.6.0-subduction.47 - version: 2.6.0-subduction.47 + specifier: github:automerge/automerge-repo#doctypes&path:packages/automerge-repo + version: https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo '@automerge/automerge-repo-storage-indexeddb': specifier: 2.6.0-subduction.47 version: 2.6.0-subduction.47 @@ -31,6 +30,9 @@ importers: '@codemirror/view': specifier: ^6.43.10 version: 6.43.10 + '@coln-project/repo': + specifier: link:../../packages/coln-repo + version: link:../../packages/coln-repo codemirror: specifier: ^6.0.2 version: 6.0.2 @@ -82,6 +84,11 @@ packages: resolution: {integrity: sha512-NGBQUjGH67Kyrc8a6zoafvDKT1+pnFi2/yiMgULS7l+apWQT2QkgQZ3vQGms/ngdrmM6ye1fhcwHwBn/DGJ6tA==} engines: {node: '>=22.13'} + '@automerge/automerge-repo@https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo': + resolution: {gitHosted: true, path: packages/automerge-repo, tarball: https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5} + version: 2.6.0-subduction.47 + engines: {node: '>=22.13'} + '@automerge/automerge-subduction@0.16.1': resolution: {integrity: sha512-alH7U4eYn0O7sT6hNLv7CJhNODJi+QD6qfRQTNfZCxL3Q5JZdJ8lFpdvXUkBf+I3bu0GLvVlTaYPWGrBihjpIw==} @@ -864,6 +871,24 @@ snapshots: - supports-color - utf-8-validate + '@automerge/automerge-repo@https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo': + dependencies: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-subduction': 0.16.1 + bs58check: 4.0.0 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + isomorphic-ws: 5.0.0(ws@8.21.3) + uuid: 14.0.2 + ws: 8.21.3 + xstate: 5.32.6 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + '@automerge/automerge-subduction@0.16.1': {} '@automerge/automerge@3.3.2': {} diff --git a/examples/compiler/pnpm-workspace.yaml b/examples/compiler/pnpm-workspace.yaml index f2b949cd..c5a59d06 100644 --- a/examples/compiler/pnpm-workspace.yaml +++ b/examples/compiler/pnpm-workspace.yaml @@ -3,5 +3,4 @@ allowBuilds: esbuild: true overrides: "@automerge/automerge": 3.3.2 - "@automerge/automerge-repo": 2.6.0-subduction.47 "@automerge/automerge-subduction": 0.16.1 diff --git a/examples/compiler/src/browser/App.svelte b/examples/compiler/src/browser/App.svelte index 98b91ed2..7c88a5d4 100644 --- a/examples/compiler/src/browser/App.svelte +++ b/examples/compiler/src/browser/App.svelte @@ -4,13 +4,16 @@ + + diff --git a/examples/compiler/src/lib/compiled-realms.ts b/examples/compiler/src/lib/compiled-realms.ts new file mode 100644 index 00000000..b2c8de94 --- /dev/null +++ b/examples/compiler/src/lib/compiled-realms.ts @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { isRealmSchema, type RealmSchema } from "./theory-document.ts" + +export type CompiledRealm = { + index: number + name: string + schema: RealmSchema +} + +export function parseCompiledRealms(irJson: string): CompiledRealm[] { + if (irJson === "") return [] + const value: unknown = JSON.parse(irJson) + if (!Array.isArray(value)) throw new TypeError("Compiler IR must be an array of realms") + + return value.map((schema, index) => { + if (!isRealmSchema(schema)) { + throw new TypeError(`Compiler IR realm ${index + 1} is malformed`) + } + return { index, name: realmName(schema, index), schema } + }) +} + +function realmName(schema: RealmSchema, index: number): string { + for (const entry of [...schema.entities, ...schema.rules]) { + if (typeof entry !== "object" || entry === null) continue + const path = (entry as { path?: unknown }).path + if (!Array.isArray(path) || !Array.isArray(path[0])) continue + if (path[0].every(part => typeof part === "string")) return path[0].join(".") + } + return `Realm ${index + 1}` +} diff --git a/examples/compiler/src/lib/stores.ts b/examples/compiler/src/lib/stores.ts new file mode 100644 index 00000000..60a4d4dc --- /dev/null +++ b/examples/compiler/src/lib/stores.ts @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { Repo } from "@automerge/automerge-repo" +import { colnDocType } from "@coln-project/repo" +import type { CompiledRealm } from "./compiled-realms.ts" +import type { TheoryHandle } from "./theory-handle.svelte.ts" +import type { StoreRecord } from "./theory-document.ts" + +export async function createStore( + repo: Repo, + theory: TheoryHandle, + realm: CompiledRealm, +): Promise<{ record: StoreRecord; theoryFlushError?: unknown }> { + const createdAt = Date.now() + const sourceHeads = theory.heads + const store = repo.create(realm.schema, colnDocType) + const record: StoreRecord = { + url: store.url, + createdAt, + sourceHeads, + realmIndex: realm.index, + realmName: realm.name, + ir: realm.schema, + } + + await repo.flush([store.documentId]) + theory.appendStore(record) + try { + await repo.flush([theory.documentId]) + } catch (theoryFlushError) { + return { record, theoryFlushError } + } + + return { record } +} diff --git a/examples/compiler/src/lib/theory-document.ts b/examples/compiler/src/lib/theory-document.ts index f5f9b2b5..9804d5c0 100644 --- a/examples/compiler/src/lib/theory-document.ts +++ b/examples/compiler/src/lib/theory-document.ts @@ -36,4 +36,24 @@ export function isTheoryDocument(value: unknown): value is TheoryDocument { return document.version === 1 && typeof document.source === "string" && Array.isArray(document.stores) + && document.stores.every(isStoreRecord) +} + +export function isRealmSchema(value: unknown): value is RealmSchema { + if (typeof value !== "object" || value === null) return false + const schema = value as Partial + return Array.isArray(schema.entities) && Array.isArray(schema.rules) +} + +function isStoreRecord(value: unknown): value is StoreRecord { + if (typeof value !== "object" || value === null) return false + const store = value as Partial + return typeof store.url === "string" + && typeof store.createdAt === "number" + && Number.isFinite(store.createdAt) + && Array.isArray(store.sourceHeads) + && store.sourceHeads.every(head => typeof head === "string") + && Number.isInteger(store.realmIndex) + && typeof store.realmName === "string" + && isRealmSchema(store.ir) } diff --git a/examples/compiler/src/lib/theory-handle.svelte.ts b/examples/compiler/src/lib/theory-handle.svelte.ts index 6402fafb..3e0cc119 100644 --- a/examples/compiler/src/lib/theory-handle.svelte.ts +++ b/examples/compiler/src/lib/theory-handle.svelte.ts @@ -3,7 +3,11 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT import { createSubscriber } from "svelte/reactivity" -import type { TheoryDocument, TheoryDocumentHandle } from "./theory-document.ts" +import type { + StoreRecord, + TheoryDocument, + TheoryDocumentHandle, +} from "./theory-document.ts" export class TheoryHandle { readonly #subscribe: () => void @@ -19,4 +23,18 @@ export class TheoryHandle { this.#subscribe() return this.handle.doc() } + + get documentId(): TheoryDocumentHandle["documentId"] { + return this.handle.documentId + } + + get heads(): string[] { + return [...this.handle.heads()] + } + + appendStore(record: StoreRecord): void { + this.handle.change(document => { + document.stores.push(record) + }) + } } diff --git a/examples/compiler/src/main.ts b/examples/compiler/src/main.ts index 47632c19..5a269886 100644 --- a/examples/compiler/src/main.ts +++ b/examples/compiler/src/main.ts @@ -36,7 +36,7 @@ if (hashUrl && !isValidAutomergeUrl(hashUrl)) { const endpoint = import.meta.env.VITE_SUBDUCTION_ENDPOINT ?? "wss://subduction.sync.inkandswitch.com" const repo = new Repo({ - storage: new IndexedDBStorageAdapter("coln-compiler", "documents"), + storage: new IndexedDBStorageAdapter("coln-lab", "documents"), subductionWebsocketEndpoints: [endpoint], }) diff --git a/examples/editor/.gitignore b/examples/editor/.gitignore new file mode 100644 index 00000000..45695782 --- /dev/null +++ b/examples/editor/.gitignore @@ -0,0 +1,3 @@ +node_modules +test-results +playwright-report diff --git a/examples/editor/README.md b/examples/editor/README.md new file mode 100644 index 00000000..a0d82454 --- /dev/null +++ b/examples/editor/README.md @@ -0,0 +1,16 @@ +# Coln Store Lab + +A Svelte 5 workbench for inspecting and programming synchronized Coln stores. + +```console +pnpm install +pnpm dev +``` + +The development command uses the shared managed relay in `examples/sync-server`. +Set `SUBDUCTION_PORT` to choose another local relay port. Production builds use +`VITE_SUBDUCTION_ENDPOINT`, falling back to the public Subduction relay. + +The JavaScript REPL runs trusted code on the main browser thread with the loaded +Coln `handle` in scope. Its outer program may be asynchronous, but callbacks +passed to `handle.change` must remain synchronous. diff --git a/examples/editor/index.html b/examples/editor/index.html new file mode 100644 index 00000000..6f21ce9a --- /dev/null +++ b/examples/editor/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + Coln Store Lab + + +
+ + + diff --git a/examples/editor/package.json b/examples/editor/package.json new file mode 100644 index 00000000..c5420dff --- /dev/null +++ b/examples/editor/package.json @@ -0,0 +1,39 @@ +{ + "name": "coln-store-lab", + "private": true, + "license": "(Apache-2.0 OR MIT)", + "type": "module", + "scripts": { + "dev": "coln-with-sync-server vite --host 127.0.0.1", + "server": "coln-sync-server", + "check": "svelte-check --tsconfig ./tsconfig.json && tsc --noEmit", + "build": "pnpm run check && vite build", + "test": "vitest run", + "test:e2e": "playwright test" + }, + "dependencies": { + "@automerge/automerge-repo": "github:automerge/automerge-repo#doctypes&path:packages/automerge-repo", + "@automerge/automerge-repo-storage-indexeddb": "2.6.0-subduction.47", + "@automerge/automerge-subduction": "0.16.1", + "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/state": "^6.7.2", + "@codemirror/view": "^6.43.10", + "@coln-project/repo": "link:../../packages/coln-repo", + "@coln-project/runtime": "link:../../packages/coln-js-runtime", + "codemirror": "^6.0.2", + "svelte": "^5.57.0" + }, + "devDependencies": { + "@coln-project/demo-sync-server": "file:../sync-server", + "@playwright/test": "^1.62.1", + "@sveltejs/vite-plugin-svelte": "^7.3.0", + "@tailwindcss/vite": "^4.3.3", + "@types/node": "^26.4.1", + "svelte-check": "^4.7.6", + "tailwindcss": "^4.3.3", + "typescript": "^6.0.3", + "vite": "^8.2.2", + "vite-plugin-wasm": "^3.6.0", + "vitest": "^4.1.11" + } +} diff --git a/examples/editor/playwright.config.ts b/examples/editor/playwright.config.ts new file mode 100644 index 00000000..05ac8d38 --- /dev/null +++ b/examples/editor/playwright.config.ts @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { defineConfig, devices } from "@playwright/test" + +export default defineConfig({ + testDir: "./tests", + timeout: 120_000, + expect: { timeout: 10_000 }, + fullyParallel: false, + retries: 0, + use: { + baseURL: "http://127.0.0.1:5177", + trace: "retain-on-failure", + }, + webServer: [ + { + command: + "SUBDUCTION_SERVICE_NAME=127.0.0.1:3032 PORT=3032 coln-sync-server", + port: 3032, + reuseExistingServer: false, + stdout: "pipe", + stderr: "pipe", + }, + { + command: + "VITE_SUBDUCTION_ENDPOINT=ws://127.0.0.1:3032 ./node_modules/.bin/vite --host 127.0.0.1 --port 5177", + url: "http://127.0.0.1:5177", + reuseExistingServer: false, + stdout: "pipe", + stderr: "pipe", + }, + ], + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], +}) diff --git a/examples/editor/pnpm-lock.yaml b/examples/editor/pnpm-lock.yaml new file mode 100644 index 00000000..3768fbd0 --- /dev/null +++ b/examples/editor/pnpm-lock.yaml @@ -0,0 +1,1846 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-subduction': 0.16.1 + +importers: + + .: + dependencies: + '@automerge/automerge-repo': + specifier: github:automerge/automerge-repo#doctypes&path:packages/automerge-repo + version: https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo + '@automerge/automerge-repo-storage-indexeddb': + specifier: 2.6.0-subduction.47 + version: 2.6.0-subduction.47 + '@automerge/automerge-subduction': + specifier: 0.16.1 + version: 0.16.1 + '@codemirror/lang-javascript': + specifier: ^6.2.4 + version: 6.2.5 + '@codemirror/state': + specifier: ^6.7.2 + version: 6.7.2 + '@codemirror/view': + specifier: ^6.43.10 + version: 6.43.11 + '@coln-project/repo': + specifier: link:../../packages/coln-repo + version: link:../../packages/coln-repo + '@coln-project/runtime': + specifier: link:../../packages/coln-js-runtime + version: link:../../packages/coln-js-runtime + codemirror: + specifier: ^6.0.2 + version: 6.0.2 + svelte: + specifier: ^5.57.0 + version: 5.57.0 + devDependencies: + '@coln-project/demo-sync-server': + specifier: file:../sync-server + version: file:../sync-server + '@playwright/test': + specifier: ^1.62.1 + version: 1.62.1 + '@sveltejs/vite-plugin-svelte': + specifier: ^7.3.0 + version: 7.3.0(svelte@5.57.0)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + '@tailwindcss/vite': + specifier: ^4.3.3 + version: 4.3.3(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + '@types/node': + specifier: ^26.4.1 + version: 26.4.1 + svelte-check: + specifier: ^4.7.6 + version: 4.7.6(picomatch@4.0.7)(svelte@5.57.0)(typescript@6.0.3) + tailwindcss: + specifier: ^4.3.3 + version: 4.3.3 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vite: + specifier: ^8.2.2 + version: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + vite-plugin-wasm: + specifier: ^3.6.0 + version: 3.6.0(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + vitest: + specifier: ^4.1.11 + version: 4.1.11(@types/node@26.4.1)(@vitest/browser-playwright@4.1.11)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + +packages: + + '@automerge/automerge-repo-storage-indexeddb@2.6.0-subduction.47': + resolution: {integrity: sha512-fYn6nse7jQnVb9EploTsBuYUzW7xFin2HZLneSEQ7w4pRCilIU92ghZEjZSWRKEPunSN0MYEvfJJJdlLyYTdkQ==} + engines: {node: '>=22.13'} + + '@automerge/automerge-repo@2.6.0-subduction.47': + resolution: {integrity: sha512-NGBQUjGH67Kyrc8a6zoafvDKT1+pnFi2/yiMgULS7l+apWQT2QkgQZ3vQGms/ngdrmM6ye1fhcwHwBn/DGJ6tA==} + engines: {node: '>=22.13'} + + '@automerge/automerge-repo@https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo': + resolution: {gitHosted: true, path: packages/automerge-repo, tarball: https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5} + version: 2.6.0-subduction.47 + engines: {node: '>=22.13'} + + '@automerge/automerge-subduction@0.16.1': + resolution: {integrity: sha512-alH7U4eYn0O7sT6hNLv7CJhNODJi+QD6qfRQTNfZCxL3Q5JZdJ8lFpdvXUkBf+I3bu0GLvVlTaYPWGrBihjpIw==} + + '@automerge/automerge@3.3.2': + resolution: {integrity: sha512-9vCdCL7pdQwUra66SBxPVHr+/t9epXKni9KDeak2rNBFMzABVh2u6gSpcwxi3jkR5qr047jVqZv9DlrOfVxLFw==} + + '@blazediff/core@1.9.1': + resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} + cpu: [arm64] + os: [darwin] + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + resolution: {integrity: sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==} + cpu: [x64] + os: [darwin] + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + resolution: {integrity: sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==} + cpu: [arm64] + os: [linux] + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + resolution: {integrity: sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==} + cpu: [arm] + os: [linux] + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + resolution: {integrity: sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==} + cpu: [x64] + os: [linux] + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + resolution: {integrity: sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==} + cpu: [x64] + os: [win32] + + '@codemirror/autocomplete@6.20.3': + resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} + + '@codemirror/commands@6.11.0': + resolution: {integrity: sha512-/K4Rl5BN0OtTiPWmJCdqODu38XnDMsDxKY5rgrPnCkutPTJf2wVbkoixLfealF5Kwse/s8P8M5jAiURiwSwnFA==} + + '@codemirror/lang-javascript@6.2.5': + resolution: {integrity: sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==} + + '@codemirror/language@6.12.4': + resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} + + '@codemirror/lint@6.9.7': + resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==} + + '@codemirror/search@6.7.2': + resolution: {integrity: sha512-gUYkYhT2+n/+VGZ+8EzE5WFkYZUZYm1VOKDudIsNqh42uRVQJ0a6Yss9sdKT3MeOYfuL1N6AZA57oza0Oyr0LA==} + + '@codemirror/state@6.7.2': + resolution: {integrity: sha512-U3RiPX62Wl/Gx4ftQ7UxLlloSfsFTQqKa+7vFBYteZGCzkp6oBqcsD7iSwniWRGobCAmDcwZSY+Six3+3ztdfg==} + + '@codemirror/view@6.43.11': + resolution: {integrity: sha512-2+esucbQX6wB2JYi1eDvdCPFTA31BN8oSy6xCmk3G6CloV11yOvEjYk+gH7kLrP0MuHG94E8WDhjs5oMiu3+Wg==} + + '@coln-project/demo-sync-server@file:../sync-server': + resolution: {directory: ../sync-server, type: directory} + hasBin: true + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@lezer/common@1.5.2': + resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/javascript@1.5.4': + resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==} + + '@lezer/lr@1.4.10': + resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} + + '@marijn/find-cluster-break@1.0.4': + resolution: {integrity: sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@oxc-project/types@0.148.0': + resolution: {integrity: sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==} + + '@playwright/test@1.62.1': + resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} + engines: {node: '>=20'} + hasBin: true + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@rolldown/binding-android-arm-eabi@1.2.7': + resolution: {integrity: sha512-EypzgnYCwyVY4NDHKzGmNJT5b+XaQEBniHxsMdeIQLB/tcCzZnhqrzHpZFbX9iaxx+5RiB8caATBtfvZP7zVxQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.7': + resolution: {integrity: sha512-l17HE9EweWaqJZhuUuNBN/FzM62xw+DECVnJyvMsxn8vJFAGLy5QfLDoYAcronkAN8VxKZHezDpulHDPx95vFw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.7': + resolution: {integrity: sha512-8ED8ELFvHXc6OCETIn4gXObPiaR6bckM/ipXtbzlPVDRMBfEGjCKgO90F9YtfdpDatVx/ZQw7aZ1vUMf/+T3Mw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.7': + resolution: {integrity: sha512-/WPripjtiAIZ2tWY7ddijORT0Ujg87wxWW/qcoFVCKAWVDPhtY0xr7Dj0M3GyNGz60jGwTElhro/mkF9dT7dDQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.7': + resolution: {integrity: sha512-14DI4NcqpvbICxSnGLx3PmtDaWqRP/KGSGb6C+JLLVPeZRl6dKdHba3pGsqT3vpdTqhEYIPG0MMQ8c0xYqoJxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.7': + resolution: {integrity: sha512-bxrWIRvHWQvbJwi+VIie/kDJmQxcNE6xxWwZdqF/ExVAigtHkv54WTLQPb+QsZdnFy18fg7JPfWGL0RH6vwIlQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.7': + resolution: {integrity: sha512-toOY2BChBZyuxU7OYX6Tn389di4IzAqPTycVcci0O7FSfBqzRB3RZn+K5Is6ANf4tmgRd/K1yZTsNTXbkXsnLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.7': + resolution: {integrity: sha512-lAIXTH/aiLRLxsTgQvfhjo4K1ydWIp00+V0voOr9beb/9ZmkUFrSIb03dXNFRgMNvkE6oGsF10ioQ6UsI+vS5Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.7': + resolution: {integrity: sha512-kdnwS28Pkenp/mZMRwjXXXwxQ7pIsm+bF919LUK93BOyhcLsrVKdP2p9fxpiPNPAbNuch8ypQt0pm2P2LYCAGg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.7': + resolution: {integrity: sha512-516OdsyLdr5E65paF3yBF55t8mfm9+gmtCsK3xI7XKXIT7EfRlHhxL8K/NR6Hu8BWSgF5+1w74lTL0+nxcc8Qw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.7': + resolution: {integrity: sha512-r8/z8n7GFaYRln3xmP1Cxy0HH/HLM0uBUPkEuSVEfKGDA89M0FsZRZJRSwe/tJjRx+fpH/gjorfhB8tmEbSFLA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.7': + resolution: {integrity: sha512-pAsE8iiDxUg1xBqdhrTfg45AVDVpirjz00sblEYClGNNcMnDb+e8beQgqIAw6LvauX/APvgxUnwrgun/YYGBhw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.7': + resolution: {integrity: sha512-lTcIYmmnQQA8Or/2DatS6oSqcdLHvendjS+zLu+FwgToynWMRSmQdpM65fTANJgIS4mjbMOo5KT2lnT9SAb96w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.7': + resolution: {integrity: sha512-e3Gu3WxbNk/UqQhxqU7YIYO+9ZBvWNz3U+h/qRFosscMFzdRPbXYSaSWgSnklv2fz1TgzBTcti2z35c/7irsHw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.7': + resolution: {integrity: sha512-W/jg5qoRSqjsEv0+dZi4e687mcHqmVuU0P4fK6qS/xjetW2Gmc1W8j//z5nAeNcC8Ttm0hV46IjcYeuVwYhuiw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@sveltejs/acorn-typescript@1.0.13': + resolution: {integrity: sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/load-config@0.2.3': + resolution: {integrity: sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ==} + engines: {node: '>= 18.0.0'} + + '@sveltejs/vite-plugin-svelte@7.3.0': + resolution: {integrity: sha512-QbRoJyD92e9R0ufeQIWRHrCC0ObcqSv/aBDdrQMoU+sypav3cDx5wytdQ6GLdXjEMO6xjrXGzfkUygng8JMv0A==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.46.4 + vite: ^8.0.0-beta.7 || ^8.0.0 + + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} + + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@26.4.1': + resolution: {integrity: sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==} + + '@vitest/browser-playwright@4.1.11': + resolution: {integrity: sha512-riLBxPqwnJ0lWs2DN2WeUfYeKLoAjbP2Xx8cLQdSddzMi20sksIa6K2mPz79DyMZKKVKH2ksOC2yJvtNcZg8cg==} + peerDependencies: + playwright: '*' + vitest: 4.1.11 + + '@vitest/browser@4.1.11': + resolution: {integrity: sha512-bwMovvAeuTFOK5kIFevw4VEf+1gVEICv4SYK4k3knJOxl6b1zEWud8mYKD73e1B0odAn174h1MofURy2TPWf3w==} + peerDependencies: + vitest: 4.1.11 + + '@vitest/expect@4.1.11': + resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} + + '@vitest/mocker@4.1.11': + resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.11': + resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} + + '@vitest/runner@4.1.11': + resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} + + '@vitest/snapshot@4.1.11': + resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} + + '@vitest/spy@4.1.11': + resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} + + '@vitest/utils@4.1.11': + resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + + bs58check@4.0.0: + resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} + + cbor-extract@2.2.2: + resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} + hasBin: true + + cbor-x@1.6.6: + resolution: {integrity: sha512-8QiD9PGOxyQHo7s2pzwTBH6lTjqekxPdl9Aq6fXvZgCuCJHOht1puDEA/fTr6mciB76c+M+Gi0qT2i1a4pm4Wg==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + codemirror@6.0.2: + resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + crelt@1.0.7: + resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devalue@5.9.2: + resolution: {integrity: sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w==} + + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} + engines: {node: '>=10.13.0'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + esrap@2.3.6: + resolution: {integrity: sha512-yc0OC12UjPqLoc+fe+v5GNs4TOjAigUw3sTikfC+xeBPGUw7gDRz3DtYaqEhxyMVJojcSWJw7jT0QWR+CbuE/A==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magic-string@1.2.3: + resolution: {integrity: sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + playwright-core@1.62.1: + resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.62.1: + resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} + engines: {node: '>=20'} + hasBin: true + + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + + postcss@8.5.27: + resolution: {integrity: sha512-79Iho8QeYyooJ8e9lCRyTVlyTAkS/kXBYKff6TMzS3kEWGQ8Ds5UEtXpGrSUDLUWok6QTvxeYy0GO8fopHnaSA==} + engines: {node: ^10 || ^12 || >=14} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + rolldown@1.2.7: + resolution: {integrity: sha512-g0EtLvBjTUB7jhyV0S/TCup3v/XSVl45vUIGbOGU4QPiyjTenCe4mKuFvW9fEgYmS2Fo42AUssRmNuMziXdrig==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + + svelte-check@4.7.6: + resolution: {integrity: sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: ^5.0.0 || ^6.0.0 + + svelte@5.57.0: + resolution: {integrity: sha512-NdbDn7fl4be1ViUG0oq/lvG6OZy3oENolV2ONjiqqsfVoeAfzaQAKUcEX3MrQod/Bebv1PgwET9rfXhgn9s4Kg==} + engines: {node: '>=18'} + + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + uuid@14.0.2: + resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} + hasBin: true + + vite-plugin-wasm@3.6.0: + resolution: {integrity: sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==} + peerDependencies: + vite: ^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + + vite@8.2.2: + resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 || ^0.5.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + + vitest@4.1.11: + resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.11 + '@vitest/browser-preview': 4.1.11 + '@vitest/browser-webdriverio': 4.1.11 + '@vitest/coverage-istanbul': 4.1.11 + '@vitest/coverage-v8': 4.1.11 + '@vitest/ui': 4.1.11 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xstate@5.32.6: + resolution: {integrity: sha512-WfA8WNrh6r9osuGwVm+aIPM4jmMW2LKHV1Yv9thYpOtL4HVF/kobh95K/O8v2jDCpDmP2EoY+6uvuqHXCZ6ZLw==} + + zimmerframe@1.1.5: + resolution: {integrity: sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==} + +snapshots: + + '@automerge/automerge-repo-storage-indexeddb@2.6.0-subduction.47': + dependencies: + '@automerge/automerge-repo': 2.6.0-subduction.47 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-repo@2.6.0-subduction.47': + dependencies: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-subduction': 0.16.1 + bs58check: 4.0.0 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + isomorphic-ws: 5.0.0(ws@8.21.3) + uuid: 14.0.2 + ws: 8.21.3 + xstate: 5.32.6 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-repo@https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo': + dependencies: + '@automerge/automerge': 3.3.2 + '@automerge/automerge-subduction': 0.16.1 + bs58check: 4.0.0 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + isomorphic-ws: 5.0.0(ws@8.21.3) + uuid: 14.0.2 + ws: 8.21.3 + xstate: 5.32.6 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-subduction@0.16.1': {} + + '@automerge/automerge@3.3.2': {} + + '@blazediff/core@1.9.1': + optional: true + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + optional: true + + '@codemirror/autocomplete@6.20.3': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.2 + '@codemirror/view': 6.43.11 + '@lezer/common': 1.5.2 + + '@codemirror/commands@6.11.0': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.2 + '@codemirror/view': 6.43.11 + '@lezer/common': 1.5.2 + + '@codemirror/lang-javascript@6.2.5': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/lint': 6.9.7 + '@codemirror/state': 6.7.2 + '@codemirror/view': 6.43.11 + '@lezer/common': 1.5.2 + '@lezer/javascript': 1.5.4 + + '@codemirror/language@6.12.4': + dependencies: + '@codemirror/state': 6.7.2 + '@codemirror/view': 6.43.11 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + style-mod: 4.1.3 + + '@codemirror/lint@6.9.7': + dependencies: + '@codemirror/state': 6.7.2 + '@codemirror/view': 6.43.11 + crelt: 1.0.7 + + '@codemirror/search@6.7.2': + dependencies: + '@codemirror/state': 6.7.2 + '@codemirror/view': 6.43.11 + crelt: 1.0.7 + + '@codemirror/state@6.7.2': + dependencies: + '@marijn/find-cluster-break': 1.0.4 + + '@codemirror/view@6.43.11': + dependencies: + '@codemirror/state': 6.7.2 + crelt: 1.0.7 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + + '@coln-project/demo-sync-server@file:../sync-server': + dependencies: + '@automerge/automerge-subduction': 0.16.1 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@lezer/common@1.5.2': {} + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/javascript@1.5.4': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/lr@1.4.10': + dependencies: + '@lezer/common': 1.5.2 + + '@marijn/find-cluster-break@1.0.4': {} + + '@noble/hashes@1.8.0': {} + + '@oxc-project/types@0.148.0': {} + + '@playwright/test@1.62.1': + dependencies: + playwright: 1.62.1 + + '@polka/url@1.0.0-next.29': + optional: true + + '@rolldown/binding-android-arm-eabi@1.2.7': + optional: true + + '@rolldown/binding-android-arm64@1.2.7': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.7': + optional: true + + '@rolldown/binding-darwin-x64@1.2.7': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.7': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.7': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.7': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.7': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.7': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.7': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.7': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.7': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@sveltejs/acorn-typescript@1.0.13(acorn@8.18.0)': + dependencies: + acorn: 8.18.0 + + '@sveltejs/load-config@0.2.3': {} + + '@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))': + dependencies: + deepmerge: 4.3.1 + magic-string: 1.2.3 + obug: 2.1.4 + svelte: 5.57.0 + vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + vitefu: 1.1.3(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.24.5 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.3 + + '@tailwindcss/oxide-android-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide@4.3.3': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 + + '@tailwindcss/vite@4.3.3(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))': + dependencies: + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 + vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/node@26.4.1': + dependencies: + undici-types: 8.3.0 + + '@vitest/browser-playwright@4.1.11(playwright@1.62.1)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))(vitest@4.1.11)': + dependencies: + '@vitest/browser': 4.1.11(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))(vitest@4.1.11) + '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + playwright: 1.62.1 + tinyrainbow: 3.1.1 + vitest: 4.1.11(@types/node@26.4.1)(@vitest/browser-playwright@4.1.11)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true + + '@vitest/browser@4.1.11(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))(vitest@4.1.11)': + dependencies: + '@blazediff/core': 1.9.1 + '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + '@vitest/utils': 4.1.11 + magic-string: 0.30.21 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.1 + vitest: 4.1.11(@types/node@26.4.1)(@vitest/browser-playwright@4.1.11)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true + + '@vitest/expect@4.1.11': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.11(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))': + dependencies: + '@vitest/spy': 4.1.11 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + + '@vitest/pretty-format@4.1.11': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.11': + dependencies: + '@vitest/utils': 4.1.11 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + '@vitest/utils': 4.1.11 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.11': {} + + '@vitest/utils@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + + acorn@8.18.0: {} + + aria-query@5.3.1: {} + + assertion-error@2.0.1: {} + + axobject-query@4.1.0: {} + + base-x@5.0.1: {} + + bs58@6.0.0: + dependencies: + base-x: 5.0.1 + + bs58check@4.0.0: + dependencies: + '@noble/hashes': 1.8.0 + bs58: 6.0.0 + + cbor-extract@2.2.2: + dependencies: + node-gyp-build-optional-packages: 5.1.1 + optionalDependencies: + '@cbor-extract/cbor-extract-darwin-arm64': 2.2.2 + '@cbor-extract/cbor-extract-darwin-x64': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm64': 2.2.2 + '@cbor-extract/cbor-extract-linux-x64': 2.2.2 + '@cbor-extract/cbor-extract-win32-x64': 2.2.2 + optional: true + + cbor-x@1.6.6: + optionalDependencies: + cbor-extract: 2.2.2 + + chai@6.2.2: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + clsx@2.1.1: {} + + codemirror@6.0.2: + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/commands': 6.11.0 + '@codemirror/language': 6.12.4 + '@codemirror/lint': 6.9.7 + '@codemirror/search': 6.7.2 + '@codemirror/state': 6.7.2 + '@codemirror/view': 6.43.11 + + convert-source-map@2.0.0: {} + + crelt@1.0.7: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deepmerge@4.3.1: {} + + detect-libc@2.1.2: {} + + devalue@5.9.2: {} + + enhanced-resolve@5.24.5: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + es-module-lexer@2.3.2: {} + + esm-env@1.2.2: {} + + esrap@2.3.6: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + eventemitter3@5.0.4: {} + + expect-type@1.4.0: {} + + fast-sha256@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + graceful-fs@4.2.11: {} + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + isomorphic-ws@5.0.0(ws@8.21.3): + dependencies: + ws: 8.21.3 + + jiti@2.7.0: {} + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + locate-character@3.0.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + magic-string@1.2.3: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + mri@1.2.0: {} + + mrmime@2.0.1: + optional: true + + ms@2.1.3: {} + + nanoid@3.3.18: {} + + node-gyp-build-optional-packages@5.1.1: + dependencies: + detect-libc: 2.1.2 + optional: true + + obug@2.1.4: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + playwright-core@1.62.1: {} + + playwright@1.62.1: + dependencies: + playwright-core: 1.62.1 + optionalDependencies: + fsevents: 2.3.2 + + pngjs@7.0.0: + optional: true + + postcss@8.5.27: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + readdirp@4.1.2: {} + + rolldown@1.2.7: + dependencies: + '@oxc-project/types': 0.148.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.7 + '@rolldown/binding-android-arm64': 1.2.7 + '@rolldown/binding-darwin-arm64': 1.2.7 + '@rolldown/binding-darwin-x64': 1.2.7 + '@rolldown/binding-freebsd-x64': 1.2.7 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.7 + '@rolldown/binding-linux-arm64-gnu': 1.2.7 + '@rolldown/binding-linux-arm64-musl': 1.2.7 + '@rolldown/binding-linux-ppc64-gnu': 1.2.7 + '@rolldown/binding-linux-s390x-gnu': 1.2.7 + '@rolldown/binding-linux-x64-gnu': 1.2.7 + '@rolldown/binding-linux-x64-musl': 1.2.7 + '@rolldown/binding-openharmony-arm64': 1.2.7 + '@rolldown/binding-win32-arm64-msvc': 1.2.7 + '@rolldown/binding-win32-x64-msvc': 1.2.7 + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + siginfo@2.0.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + optional: true + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + style-mod@4.1.3: {} + + svelte-check@4.7.6(picomatch@4.0.7)(svelte@5.57.0)(typescript@6.0.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@sveltejs/load-config': 0.2.3 + chokidar: 4.0.3 + fdir: 6.5.0(picomatch@4.0.7) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.57.0 + typescript: 6.0.3 + transitivePeerDependencies: + - picomatch + + svelte@5.57.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.6.0 + '@sveltejs/acorn-typescript': 1.0.13(acorn@8.18.0) + '@types/estree': 1.0.9 + acorn: 8.18.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.9.2 + esm-env: 1.2.2 + esrap: 2.3.6 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.5 + transitivePeerDependencies: + - '@typescript-eslint/types' + + tailwindcss@4.3.3: {} + + tapable@2.3.3: {} + + tinybench@2.9.0: {} + + tinyexec@1.3.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinyrainbow@3.1.1: {} + + totalist@3.0.1: + optional: true + + typescript@6.0.3: {} + + undici-types@8.3.0: {} + + uuid@14.0.2: {} + + vite-plugin-wasm@3.6.0(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)): + dependencies: + vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + + vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.27 + rolldown: 1.2.7 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.4.1 + fsevents: 2.3.3 + jiti: 2.7.0 + + vitefu@1.1.3(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)): + optionalDependencies: + vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + + vitest@4.1.11(@types/node@26.4.1)(@vitest/browser-playwright@4.1.11)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)): + dependencies: + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 26.4.1 + '@vitest/browser-playwright': 4.1.11(playwright@1.62.1)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))(vitest@4.1.11) + transitivePeerDependencies: + - msw + + w3c-keyname@2.2.8: {} + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + ws@8.21.3: {} + + xstate@5.32.6: {} + + zimmerframe@1.1.5: {} diff --git a/examples/editor/pnpm-workspace.yaml b/examples/editor/pnpm-workspace.yaml new file mode 100644 index 00000000..62604741 --- /dev/null +++ b/examples/editor/pnpm-workspace.yaml @@ -0,0 +1,7 @@ +allowBuilds: + cbor-extract: false + esbuild: true + '@automerge/automerge-repo@git+https://github.com/automerge/automerge-repo.git': true +overrides: + "@automerge/automerge": 3.3.2 + "@automerge/automerge-subduction": 0.16.1 diff --git a/examples/editor/src/browser/App.svelte b/examples/editor/src/browser/App.svelte new file mode 100644 index 00000000..89e90692 --- /dev/null +++ b/examples/editor/src/browser/App.svelte @@ -0,0 +1,237 @@ + + + + + + + Coln Store Lab + + +{#if !store} + +{:else} +
+ {#if actionError} + + {/if} +
+ +
+ + + +
+
+ +
+ (selectedTableName = name)} /> +
+
+
+

JAVASCRIPT REPL

+

Trusted code / handle in scope / explicit return

+
+ +
+ (source = value)} onrun={runProgram} /> + +
+
+
+{/if} diff --git a/examples/editor/src/browser/ReplEditor.svelte b/examples/editor/src/browser/ReplEditor.svelte new file mode 100644 index 00000000..baf9dc34 --- /dev/null +++ b/examples/editor/src/browser/ReplEditor.svelte @@ -0,0 +1,114 @@ + + + + + +
diff --git a/examples/editor/src/browser/ReplOutput.svelte b/examples/editor/src/browser/ReplOutput.svelte new file mode 100644 index 00000000..ad7dd3d9 --- /dev/null +++ b/examples/editor/src/browser/ReplOutput.svelte @@ -0,0 +1,32 @@ + + + + + +
+ {#if !evaluation} +

Run a program to see its result and console output.

+ {:else} + {#each evaluation.console as entry} +
+ {entry.level} +
{entry.values.map(render).join(" ")}
+
+ {/each} +
+
+ {evaluation.ok ? "Result" : `${evaluation.phase} error`} + {evaluation.durationMs.toFixed(1)} ms +
+
{render(evaluation.ok ? evaluation.result : evaluation.error)}
+
+ {/if} +
diff --git a/examples/editor/src/browser/StoreLoader.svelte b/examples/editor/src/browser/StoreLoader.svelte new file mode 100644 index 00000000..d70085c6 --- /dev/null +++ b/examples/editor/src/browser/StoreLoader.svelte @@ -0,0 +1,56 @@ + + + + + +
+
+
+ C + COLN / STORE LAB +
+
+
+
{ + event.preventDefault() + onload(documentUrl) + }} + > +
+

01 / OPEN STORE

+

Inspect a synchronized Coln store.

+

Enter an Automerge document URL. Store Lab will resolve its Coln schema, tables, and live rows.

+
+
+ + + {#if error} + + {/if} +
+
+
+
diff --git a/examples/editor/src/browser/TableBrowser.svelte b/examples/editor/src/browser/TableBrowser.svelte new file mode 100644 index 00000000..9bf768e7 --- /dev/null +++ b/examples/editor/src/browser/TableBrowser.svelte @@ -0,0 +1,97 @@ + + + + + +
+
+
+

STORE CONTENTS

+

{tables.length} {tables.length === 1 ? "table" : "tables"}

+
+ +
+ +
+ + +
+ {#if selected} + + + + + {#each selected.columns as column} + + {/each} + + + + {#each rows as row (displayRowRef(row.rowId.value).full)} + {@const rowId = displayRowRef(row.rowId.value)} + + + {#each row.values as value} + {@const displayed = displayValue(value)} + + {/each} + + {:else} + + + + {/each} + +
Row ID + {column.name}{column.primary ? " *" : ""} + {column.type} +
{rowId.compact} + {displayed.compact} +
No rows in {selected.name}
+ {:else} +
This store has no tables.
+ {/if} +
+
+
diff --git a/examples/editor/src/lib/coln-handle.svelte.ts b/examples/editor/src/lib/coln-handle.svelte.ts new file mode 100644 index 00000000..b6ae3a94 --- /dev/null +++ b/examples/editor/src/lib/coln-handle.svelte.ts @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { + ColnDocument, + ColnHandle as RepoColnHandle, +} from "@coln-project/repo" +import { createSubscriber } from "svelte/reactivity" + +export class ColnHandleState { + readonly #subscribe: () => void + + constructor(private readonly handle: RepoColnHandle) { + this.#subscribe = createSubscriber((update) => { + handle.on("change", update) + return () => handle.off("change", update) + }) + } + + get current(): ColnDocument { + this.#subscribe() + return this.handle.doc() + } +} diff --git a/examples/editor/src/lib/evaluate.ts b/examples/editor/src/lib/evaluate.ts new file mode 100644 index 00000000..86e48930 --- /dev/null +++ b/examples/editor/src/lib/evaluate.ts @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { ColnHandle } from "@coln-project/repo" +import { snapshot, type Snapshot } from "./snapshot.ts" + +export type ConsoleLevel = "debug" | "info" | "log" | "warn" | "error" + +export interface ConsoleEntry { + level: ConsoleLevel + values: Snapshot[] +} + +export interface EvaluationSuccess { + ok: true + result: Snapshot + console: ConsoleEntry[] + durationMs: number +} + +export interface EvaluationFailure { + ok: false + phase: "syntax" | "runtime" + error: Snapshot + console: ConsoleEntry[] + durationMs: number +} + +export type Evaluation = EvaluationSuccess | EvaluationFailure + +type AsyncProgram = ( + handle: ColnHandle, + console: Pick, +) => Promise + +const AsyncFunction = Object.getPrototypeOf(async function () {}) + .constructor as new (...parameters: string[]) => AsyncProgram + +export async function evaluate( + source: string, + handle: ColnHandle, +): Promise { + const entries: ConsoleEntry[] = [] + let active = true + const capturedConsole = Object.fromEntries( + (["debug", "info", "log", "warn", "error"] as const).map((level) => [ + level, + (...values: unknown[]) => { + if (active) + entries.push({ + level, + values: values.map((value) => snapshot(value)), + }) + }, + ]), + ) as Pick + + let program: AsyncProgram + const started = performance.now() + try { + program = new AsyncFunction( + "handle", + "console", + `"use strict";\n${source}\n//# sourceURL=coln-store-lab-repl.js`, + ) + } catch (error) { + active = false + return { + ok: false, + phase: "syntax", + error: snapshot(error), + console: entries, + durationMs: performance.now() - started, + } + } + + try { + const result = await program(handle, capturedConsole) + active = false + return { + ok: true, + result: snapshot(result), + console: entries, + durationMs: performance.now() - started, + } + } catch (error) { + active = false + return { + ok: false, + phase: "runtime", + error: snapshot(error), + console: entries, + durationMs: performance.now() - started, + } + } +} diff --git a/examples/editor/src/lib/format.ts b/examples/editor/src/lib/format.ts new file mode 100644 index 00000000..16f796a9 --- /dev/null +++ b/examples/editor/src/lib/format.ts @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +export interface DisplayValue { + compact: string + full: string + kind: "int" | "string" | "ref" | "unknown" +} + +type JsonObject = Record + +export function displayValue(value: unknown): DisplayValue { + if (!isObject(value) || typeof value.tag !== "string") { + return unknownValue(value) + } + if (value.tag === "int" && typeof value.value === "number") { + const rendered = String(value.value) + return { compact: rendered, full: rendered, kind: "int" } + } + if (value.tag === "string" && typeof value.value === "string") { + return { compact: value.value, full: value.value, kind: "string" } + } + if (value.tag === "row_id") return displayRowRef(value.value) + return unknownValue(value) +} + +export function displayRowRef(value: unknown): DisplayValue { + if (!isObject(value)) return unknownValue(value) + if (isObject(value.existing)) { + const commit = value.existing.commit + const counter = value.existing.counter + if (typeof commit === "string" && typeof counter === "number") { + const full = `${commit}:${counter}` + const compact = `${commit.slice(0, 8)}…${commit.slice(-4)}:${counter}` + return { compact, full, kind: "ref" } + } + } + if (isObject(value.pending)) { + const txId = value.pending.txId + const counter = value.pending.counter + if (typeof txId === "number" && typeof counter === "number") { + const rendered = `pending:${txId}:${counter}` + return { compact: rendered, full: rendered, kind: "ref" } + } + } + return unknownValue(value) +} + +function unknownValue(value: unknown): DisplayValue { + let rendered: string + try { + rendered = JSON.stringify(value) ?? String(value) + } catch { + rendered = String(value) + } + return { compact: rendered, full: rendered, kind: "unknown" } +} + +function isObject(value: unknown): value is JsonObject { + return value !== null && typeof value === "object" && !Array.isArray(value) +} diff --git a/examples/editor/src/lib/schema.ts b/examples/editor/src/lib/schema.ts new file mode 100644 index 00000000..351e23dd --- /dev/null +++ b/examples/editor/src/lib/schema.ts @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +export interface StoreColumn { + name: string + type: string + primary: boolean +} + +export interface StoreTable { + name: string + columns: StoreColumn[] +} + +type JsonObject = Record +type IrPath = string[][] + +export function readTables(json: string): StoreTable[] { + const schema: unknown = JSON.parse(json) + if (!isObject(schema) || !Array.isArray(schema.entities)) { + throw new TypeError("Store IR does not contain an entities array") + } + + return schema.entities.map((entry, index) => readTable(entry, index)) +} + +function readTable(entry: unknown, index: number): StoreTable { + if (!isObject(entry)) throw new TypeError(`Entity ${index} is not an object`) + const path = readPath(entry.path, `Entity ${index} path`) + const definition = entry.value + if (!isObject(definition)) { + throw new TypeError(`Entity ${pathName(path)} has no definition`) + } + const variant = definition.entityVariant + if (!isObject(variant) || variant.tag !== "table") { + throw new TypeError(`Entity ${pathName(path)} is not a table`) + } + if (!Array.isArray(definition.columns)) { + throw new TypeError(`Table ${pathName(path)} has no columns array`) + } + + const primaryKey = definition.primaryKey + const primaryPaths = Array.isArray(primaryKey) + ? primaryKey.map((value, column) => + pathName(readPath(value, `Primary key ${column}`)), + ) + : [] + + return { + name: pathName(path), + columns: definition.columns.map((column, columnIndex) => { + if (!isObject(column)) { + throw new TypeError( + `Column ${columnIndex} in ${pathName(path)} is invalid`, + ) + } + const columnPath = readPath( + column.path, + `Column ${columnIndex} in ${pathName(path)}`, + ) + const name = pathName(columnPath) + return { + name, + type: typeName(column.type), + primary: primaryPaths.includes(name), + } + }), + } +} + +function readPath(value: unknown, label: string): IrPath { + if ( + !Array.isArray(value) || + value.length === 0 || + value.some( + (segment) => + !Array.isArray(segment) || + segment.length === 0 || + segment.some((part) => typeof part !== "string" || part === ""), + ) + ) { + throw new TypeError(`${label} is invalid`) + } + return value as IrPath +} + +function pathName(path: IrPath): string { + return path.flat().join(".") +} + +function typeName(value: unknown): string { + if (!isObject(value) || typeof value.tag !== "string") return "unknown" + if (value.tag === "builtin") { + if (value.type === "builtinInt") return "int" + if (value.type === "builtinString") return "string" + } + if (value.tag === "rowId") { + try { + return `ref ${pathName(readPath(value.path, "Row reference path"))}` + } catch { + return "ref" + } + } + return value.tag +} + +function isObject(value: unknown): value is JsonObject { + return value !== null && typeof value === "object" && !Array.isArray(value) +} diff --git a/examples/editor/src/lib/snapshot.ts b/examples/editor/src/lib/snapshot.ts new file mode 100644 index 00000000..5fc6b9cd --- /dev/null +++ b/examples/editor/src/lib/snapshot.ts @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +export type Snapshot = + | null + | boolean + | number + | string + | Snapshot[] + | { [key: string]: Snapshot } + +export function snapshot(value: unknown, maxDepth = 8): Snapshot { + try { + return visit(value, new WeakSet(), 0, maxDepth) + } catch { + return tagged("uninspectable") + } +} + +function visit( + value: unknown, + seen: WeakSet, + depth: number, + maxDepth: number, +): Snapshot { + if ( + value === null || + typeof value === "boolean" || + typeof value === "string" + ) { + return value + } + if (typeof value === "number") { + return Number.isFinite(value) ? value : tagged("number", String(value)) + } + if (typeof value === "undefined") return tagged("undefined") + if (typeof value === "bigint") return tagged("bigint", value.toString()) + if (typeof value === "symbol") + return tagged("symbol", value.description ?? "") + if (typeof value === "function") return tagged("function", value.name) + if (typeof value !== "object") return tagged("unknown", String(value)) + if (seen.has(value)) return tagged("circular") + if (depth >= maxDepth) return tagged("truncated") + + seen.add(value) + try { + if (value instanceof Error) { + return { + $type: "error", + name: value.name, + message: value.message, + ...(value.stack ? { stack: value.stack } : {}), + } + } + if (Array.isArray(value)) { + return value.map((item) => visit(item, seen, depth + 1, maxDepth)) + } + + const prototype = Object.getPrototypeOf(value) + if (prototype !== Object.prototype && prototype !== null) { + return tagged("instance", prototype?.constructor?.name ?? "Object") + } + + return Object.fromEntries( + Object.entries(Object.getOwnPropertyDescriptors(value)).map( + ([key, descriptor]) => [ + key, + "value" in descriptor + ? visit(descriptor.value, seen, depth + 1, maxDepth) + : tagged("accessor"), + ], + ), + ) + } finally { + seen.delete(value) + } +} + +function tagged(type: string, value?: string): { [key: string]: Snapshot } { + return value === undefined ? { $type: type } : { $type: type, value } +} diff --git a/examples/editor/src/lib/source-storage.ts b/examples/editor/src/lib/source-storage.ts new file mode 100644 index 00000000..1f1c5556 --- /dev/null +++ b/examples/editor/src/lib/source-storage.ts @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +const prefix = "coln-store-lab:source:" + +export const starterSource = `const doc = handle.doc() +console.log(JSON.parse(doc.jsonIR())) +return doc.heads()` + +export function loadSource(documentUrl: string): string { + try { + return localStorage.getItem(prefix + documentUrl) ?? starterSource + } catch { + return starterSource + } +} + +export function saveSource(documentUrl: string, source: string): void { + try { + localStorage.setItem(prefix + documentUrl, source) + } catch { + // The REPL remains usable when storage is unavailable. + } +} diff --git a/examples/editor/src/lib/subduction-sync.svelte.ts b/examples/editor/src/lib/subduction-sync.svelte.ts new file mode 100644 index 00000000..437674f0 --- /dev/null +++ b/examples/editor/src/lib/subduction-sync.svelte.ts @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { Repo } from "@automerge/automerge-repo" +import type { ColnHandle } from "@coln-project/repo" +import { createSubscriber } from "svelte/reactivity" + +export type SyncStatus = "offline" | "syncing" | "synced" | "error" + +export class SubductionSync { + readonly #subscribe: () => void + #status: SyncStatus + #error: unknown + + constructor( + private readonly repo: Repo, + private readonly handle: ColnHandle, + ) { + this.#status = repo.isSubductionConnected() ? "syncing" : "offline" + this.#subscribe = createSubscriber((update) => { + let active = true + let generation = 0 + let currentAttempt: symbol | undefined + + const setStatus = (status: SyncStatus, error?: unknown) => { + if (this.#status === status && this.#error === error) return + this.#status = status + this.#error = error + update() + } + const finishFlush = ( + attempt: symbol, + flushGeneration: number, + failed: boolean, + error?: unknown, + ) => { + if (!active || currentAttempt !== attempt) return + currentAttempt = undefined + if (!this.repo.isSubductionConnected()) setStatus("offline") + else if (flushGeneration !== generation) startFlush() + else if (failed) setStatus("error", error) + else setStatus("synced") + } + const startFlush = () => { + if (currentAttempt || !active || !this.repo.isSubductionConnected()) + return + const attempt = Symbol() + const flushGeneration = generation + currentAttempt = attempt + void this.repo.flush([this.handle.documentId]).then( + () => finishFlush(attempt, flushGeneration, false), + (error) => finishFlush(attempt, flushGeneration, true, error), + ) + } + const requestFlush = () => { + generation += 1 + if (!this.repo.isSubductionConnected()) { + currentAttempt = undefined + setStatus("offline") + return + } + setStatus("syncing") + startFlush() + } + + this.handle.on("change", requestFlush) + this.repo.on("subduction-connection", requestFlush) + requestFlush() + return () => { + active = false + generation += 1 + currentAttempt = undefined + this.handle.off("change", requestFlush) + this.repo.off("subduction-connection", requestFlush) + } + }) + } + + get status(): SyncStatus { + this.#subscribe() + return this.#status + } + + get error(): unknown { + this.#subscribe() + return this.#error + } +} diff --git a/examples/editor/src/main.ts b/examples/editor/src/main.ts new file mode 100644 index 00000000..40c51475 --- /dev/null +++ b/examples/editor/src/main.ts @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +// @ts-expect-error initSync is exported at runtime but absent from declarations +import { initSync } from "@automerge/automerge-subduction/slim" +// @ts-expect-error wasm-base64 is exported without declarations +import { wasmBase64 } from "@automerge/automerge-subduction/wasm-base64" +import { mount } from "svelte" +import App from "./browser/App.svelte" +import "./style.css" + +initSync({ + module: Uint8Array.from(atob(wasmBase64), (char) => char.charCodeAt(0)), +}) + +const endpoint = + import.meta.env.VITE_SUBDUCTION_ENDPOINT ?? + "wss://subduction.sync.inkandswitch.com" + +mount(App, { + target: document.getElementById("app")!, + props: { endpoint }, +}) diff --git a/examples/editor/src/style.css b/examples/editor/src/style.css new file mode 100644 index 00000000..b5072711 --- /dev/null +++ b/examples/editor/src/style.css @@ -0,0 +1,23 @@ +/* SPDX-FileCopyrightText: 2026 Coln contributors */ +/* SPDX-License-Identifier: Apache-2.0 OR MIT */ + +@import url("https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;600;700&display=swap"); +@import "tailwindcss"; + +html { + background: #101718; +} + +body { + margin: 0; +} + +button, +input { + font: inherit; +} + +::selection { + background: #657a32; + color: #e8ece8; +} diff --git a/examples/editor/tests/editor.spec.ts b/examples/editor/tests/editor.spec.ts new file mode 100644 index 00000000..bc9be402 --- /dev/null +++ b/examples/editor/tests/editor.spec.ts @@ -0,0 +1,129 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { expect, test } from "@playwright/test" +import { Repo, initSubduction } from "@automerge/automerge-repo" +import { + colnDocType, + type ColnHandle, + type ColnSchema, +} from "@coln-project/repo" + +const endpoint = "ws://127.0.0.1:3032" +const schema = { + entities: [ + { + path: [["Lab"], ["V"]], + value: { + entityVariant: { tag: "table" }, + columns: [], + primaryKey: null, + }, + }, + { + path: [["Lab"], ["profile"]], + value: { + entityVariant: { tag: "table" }, + columns: [ + { path: [["owner"]], type: { tag: "rowId", path: [["Lab"], ["V"]] } }, + { path: [["name"]], type: { tag: "builtin", type: "builtinString" } }, + { path: [["rank"]], type: { tag: "builtin", type: "builtinInt" } }, + ], + primaryKey: [[["owner"]]], + }, + }, + ], + rules: [], +} satisfies ColnSchema + +let repo: Repo +let handle: ColnHandle +let secondHandle: ColnHandle + +test.beforeAll(async () => { + await initSubduction() + repo = new Repo({ subductionWebsocketEndpoints: [endpoint] }) + handle = repo.create(schema, colnDocType) + handle.change((transaction) => { + const ada = transaction.add("Lab.V", []) + transaction.add("Lab.V", []) + transaction.add("Lab.profile", [ + ada, + { tag: "string", value: "Ada" }, + { tag: "int", value: 37 }, + ]) + }) + await repo.flush([handle.documentId]) + secondHandle = repo.create(schema, colnDocType) + secondHandle.change((transaction) => { + transaction.add("Lab.V", []) + }) + await repo.flush([secondHandle.documentId]) +}) + +test.afterAll(async () => { + await repo.shutdown() +}) + +test("loads, inspects, and programs a Coln store", async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }) + await page.goto("/") + await expect( + page.getByText("Inspect a synchronized Coln store."), + ).toBeVisible() + await page.getByTestId("document-url-input").fill("not-a-document") + await page.getByTestId("open-store").click() + await expect(page.getByTestId("load-error")).toContainText("valid automerge:") + await page.getByTestId("document-url-input").fill(handle.url) + await page.getByTestId("open-store").click() + + await expect(page).toHaveURL(new RegExp(`#${handle.url}$`)) + await expect(page.getByTestId("table-option")).toHaveCount(2) + const tableBox = await page.getByTestId("table-grid").boundingBox() + const replBox = await page.getByTestId("run-program").boundingBox() + expect(tableBox).not.toBeNull() + expect(replBox).not.toBeNull() + expect(replBox!.y).toBeGreaterThan(tableBox!.y) + await page.setViewportSize({ width: 1280, height: 800 }) + + await page + .getByTestId("table-option") + .filter({ hasText: "Lab.profile" }) + .click() + await expect(page.getByTestId("table-row")).toHaveCount(1) + await expect(page.getByTestId("table-grid")).toContainText("Ada") + await expect(page.getByTestId("table-grid")).toContainText("37") + + const program = `console.log("before", handle.doc().scanTable("Lab.V").length) +handle.change(transaction => transaction.add("Lab.V", [])) +return handle.doc().scanTable("Lab.V").length` + const editor = page.locator(".cm-content") + await editor.click() + await page.keyboard.press("ControlOrMeta+A") + await page.keyboard.insertText(program) + await page.getByTestId("run-program").click() + + await expect(page.getByTestId("console-entry")).toContainText("before") + await expect(page.getByTestId("repl-result")).toContainText("3") + await page.getByTestId("table-option").filter({ hasText: "Lab.V" }).click() + await expect(page.getByTestId("table-row")).toHaveCount(3) + await expect.poll(() => handle.doc().scanTable("Lab.V").length).toBe(3) + + handle.change((transaction) => { + transaction.add("Lab.V", []) + }) + await repo.flush([handle.documentId]) + repo.resyncSubduction(handle.documentId) + await expect(page.getByTestId("table-row")).toHaveCount(4) + + await page.reload() + await expect(page.locator(".cm-content")).toContainText("handle.change") + await page.getByRole("button", { name: "Switch" }).click() + await expect(page.getByTestId("document-url-input")).toBeVisible() + await page.getByTestId("document-url-input").fill(secondHandle.url) + await page.getByTestId("open-store").click() + await expect(page.getByTestId("table-option")).toHaveCount(2) + await page.getByTestId("table-option").filter({ hasText: "Lab.V" }).click() + await expect(page.getByTestId("table-row")).toHaveCount(1) + await expect(page.locator(".cm-content")).not.toContainText("handle.change") +}) diff --git a/examples/editor/tests/evaluate.unit.ts b/examples/editor/tests/evaluate.unit.ts new file mode 100644 index 00000000..4b0ee00a --- /dev/null +++ b/examples/editor/tests/evaluate.unit.ts @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import type { ColnHandle } from "@coln-project/repo" +import { describe, expect, it } from "vitest" +import { evaluate } from "../src/lib/evaluate.ts" + +const handle = { marker: "handle" } as unknown as ColnHandle + +describe("evaluate", () => { + it("awaits programs and captures console output", async () => { + const result = await evaluate( + 'console.log("ready", 2); await Promise.resolve(); return handle.marker', + handle, + ) + expect(result).toMatchObject({ + ok: true, + result: "handle", + console: [{ level: "log", values: ["ready", 2] }], + }) + }) + + it("separates syntax and runtime failures", async () => { + expect(await evaluate("return )", handle)).toMatchObject({ + ok: false, + phase: "syntax", + }) + expect(await evaluate('throw new Error("broken")', handle)).toMatchObject({ + ok: false, + phase: "runtime", + error: { $type: "error", message: "broken" }, + }) + }) +}) diff --git a/examples/editor/tests/format.unit.ts b/examples/editor/tests/format.unit.ts new file mode 100644 index 00000000..1d56fe54 --- /dev/null +++ b/examples/editor/tests/format.unit.ts @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { describe, expect, it } from "vitest" +import { displayRowRef, displayValue } from "../src/lib/format.ts" + +describe("value formatting", () => { + it("formats primitive values", () => { + expect(displayValue({ tag: "string", value: "Ada" })).toMatchObject({ + compact: "Ada", + kind: "string", + }) + expect(displayValue({ tag: "int", value: 37 })).toMatchObject({ + compact: "37", + kind: "int", + }) + }) + + it("compacts committed references without losing the full ID", () => { + const commit = + "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + expect(displayRowRef({ existing: { commit, counter: 4 } })).toEqual({ + compact: "12345678…cdef:4", + full: `${commit}:4`, + kind: "ref", + }) + }) +}) diff --git a/examples/editor/tests/schema.unit.ts b/examples/editor/tests/schema.unit.ts new file mode 100644 index 00000000..ba0ee1c6 --- /dev/null +++ b/examples/editor/tests/schema.unit.ts @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { describe, expect, it } from "vitest" +import { readTables } from "../src/lib/schema.ts" + +describe("readTables", () => { + it("extracts paths, types, and primary columns", () => { + const tables = readTables( + JSON.stringify({ + entities: [ + { + path: [["Realm"], ["profile"]], + value: { + entityVariant: { tag: "table" }, + columns: [ + { + path: [["owner"]], + type: { tag: "rowId", path: [["Realm"], ["Person"]] }, + }, + { + path: [["details"], ["name"]], + type: { tag: "builtin", type: "builtinString" }, + }, + ], + primaryKey: [[["owner"]]], + }, + }, + ], + rules: [], + }), + ) + + expect(tables).toEqual([ + { + name: "Realm.profile", + columns: [ + { name: "owner", type: "ref Realm.Person", primary: true }, + { name: "details.name", type: "string", primary: false }, + ], + }, + ]) + }) + + it("rejects malformed IR", () => { + expect(() => readTables('{"rules":[]}')).toThrow("entities array") + expect(() => + readTables( + JSON.stringify({ + entities: [ + { path: [["Realm"]], value: { entityVariant: { tag: "view" } } }, + ], + }), + ), + ).toThrow("is not a table") + }) +}) diff --git a/examples/editor/tests/snapshot.unit.ts b/examples/editor/tests/snapshot.unit.ts new file mode 100644 index 00000000..df3b5043 --- /dev/null +++ b/examples/editor/tests/snapshot.unit.ts @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { describe, expect, it } from "vitest" +import { snapshot } from "../src/lib/snapshot.ts" + +describe("snapshot", () => { + it("copies plain values and marks cycles", () => { + const value: Record = { answer: 42 } + value.self = value + expect(snapshot(value)).toEqual({ answer: 42, self: { $type: "circular" } }) + }) + + it("does not invoke accessors or retain class instances", () => { + let accessed = false + const value = Object.defineProperty({}, "secret", { + enumerable: true, + get() { + accessed = true + return 1 + }, + }) + expect(snapshot(value)).toEqual({ secret: { $type: "accessor" } }) + expect(accessed).toBe(false) + expect(snapshot(new Date(0))).toEqual({ $type: "instance", value: "Date" }) + }) + + it("contains objects that throw during inspection", () => { + const { proxy, revoke } = Proxy.revocable({}, {}) + revoke() + expect(snapshot(proxy)).toEqual({ $type: "uninspectable" }) + }) + + it("preserves an own __proto__ property", () => { + const value = Object.create(null) as Record + value.__proto__ = "data" + const result = snapshot(value) + expect(Object.prototype.hasOwnProperty.call(result, "__proto__")).toBe(true) + expect((result as Record).__proto__).toBe("data") + }) +}) diff --git a/examples/editor/tests/source-storage.unit.ts b/examples/editor/tests/source-storage.unit.ts new file mode 100644 index 00000000..81de5d17 --- /dev/null +++ b/examples/editor/tests/source-storage.unit.ts @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" +import { + loadSource, + saveSource, + starterSource, +} from "../src/lib/source-storage.ts" + +const values = new Map() + +beforeEach(() => { + values.clear() + vi.stubGlobal("localStorage", { + getItem: (key: string) => values.get(key) ?? null, + setItem: (key: string, value: string) => values.set(key, value), + }) +}) + +afterEach(() => vi.unstubAllGlobals()) + +describe("source storage", () => { + it("stores source independently by document URL", () => { + expect(loadSource("automerge:one")).toBe(starterSource) + saveSource("automerge:one", "return 1") + saveSource("automerge:two", "return 2") + expect(loadSource("automerge:one")).toBe("return 1") + expect(loadSource("automerge:two")).toBe("return 2") + }) +}) diff --git a/examples/editor/tsconfig.json b/examples/editor/tsconfig.json new file mode 100644 index 00000000..b53da86b --- /dev/null +++ b/examples/editor/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + "isolatedModules": true, + "noEmit": true, + "types": ["vite/client", "node"] + }, + "include": [ + "src", + "tests", + "vite.config.ts", + "vitest.config.ts", + "playwright.config.ts" + ] +} diff --git a/examples/editor/vite.config.ts b/examples/editor/vite.config.ts new file mode 100644 index 00000000..d11f1c14 --- /dev/null +++ b/examples/editor/vite.config.ts @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { svelte } from "@sveltejs/vite-plugin-svelte" +import tailwindcss from "@tailwindcss/vite" +import { defineConfig } from "vite" +import wasm from "vite-plugin-wasm" + +export default defineConfig({ + base: process.env.VITE_BASE || "/", + plugins: [tailwindcss(), wasm(), svelte()], + resolve: { dedupe: ["@coln-project/runtime", "svelte"] }, + build: { + emptyOutDir: true, + outDir: "../../_build/web/editor-app", + target: "esnext", + }, +}) diff --git a/examples/editor/vitest.config.ts b/examples/editor/vitest.config.ts new file mode 100644 index 00000000..90893bd9 --- /dev/null +++ b/examples/editor/vitest.config.ts @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { defineConfig } from "vitest/config" + +export default defineConfig({ + resolve: { conditions: ["browser"] }, + test: { + include: ["tests/**/*.unit.ts"], + }, +}) From 6441156bc324d5ced6c52b451c0cb101513af65e Mon Sep 17 00:00:00 2001 From: Alex Currie-Clark Date: Sun, 6 Sep 2026 07:05:03 +0100 Subject: [PATCH 17/18] Simplified examples and release action Co-authored-by: acurrieclark <1306728+acurrieclark@users.noreply.github.com> --- .github/workflows/web-demos.yml | 192 +- .gitignore | 2 + examples/cli-demo/skills/coln-repo/SKILL.md | 2 +- examples/cli-demo/src/cli/help.ts | 2 +- examples/cli-demo/src/cli/repository.ts | 2 +- examples/compiler-demo/index.html | 115 - examples/compiler/.gitignore | 3 - examples/compiler/index.html | 19 - examples/compiler/package.json | 34 - examples/compiler/pnpm-lock.yaml | 1472 ----------- examples/compiler/src/browser/App.svelte | 325 --- .../src/browser/DocumentLoadError.svelte | 62 - .../compiler/src/lib/theory-sync.svelte.ts | 97 - examples/compiler/src/main.ts | 77 - examples/compiler/src/style.css | 45 - examples/compiler/tsconfig.json | 20 - examples/compiler/vite.config.ts | 18 - examples/editor/README.md | 16 - examples/editor/playwright.config.ts | 40 - examples/editor/pnpm-workspace.yaml | 7 - .../editor/src/browser/StoreLoader.svelte | 56 - examples/editor/src/main.ts | 23 - examples/editor/src/style.css | 23 - examples/editor/tests/editor.spec.ts | 129 - examples/editor/vite.config.ts | 18 - examples/editor/vitest.config.ts | 11 - examples/index.html | 17 - examples/justfile | 35 +- examples/lab/.env.example | 2 + examples/{editor => lab}/.gitignore | 1 + examples/lab/README.md | 27 + examples/{editor => lab}/index.html | 9 +- examples/{editor => lab}/package.json | 21 +- examples/lab/playwright.config.ts | 37 + examples/{editor => lab}/pnpm-lock.yaml | 72 +- .../{compiler => lab}/pnpm-workspace.yaml | 1 + .../{sync-server => lab/scripts}/server.mjs | 0 .../scripts}/with-server.mjs | 0 examples/lab/src/app/Home.svelte | 48 + examples/lab/src/app/LabApp.svelte | 149 ++ .../src/lib/document-sync.svelte.ts} | 13 +- examples/lab/src/lib/router.svelte.ts | 123 + examples/lab/src/main.ts | 43 + examples/lab/src/style.css | 27 + .../src/tools/compiler/CompilerTool.svelte | 336 +++ .../src/tools/compiler}/JsonViewer.svelte | 0 .../src/tools/compiler}/OutputPanel.svelte | 0 .../src/tools/compiler}/SourceEditor.svelte | 2 +- .../src/tools/compiler}/StoresPanel.svelte | 15 +- .../src/tools/compiler}/compiled-realms.ts | 0 .../src/tools/compiler}/compiler.ts | 12 +- .../lib => lab/src/tools/compiler}/stores.ts | 0 .../src/tools/compiler}/theory-document.ts | 0 .../tools/compiler}/theory-handle.svelte.ts | 0 .../src/tools/editor/EditorTool.svelte} | 142 +- .../src/tools/editor}/ReplEditor.svelte | 6 +- .../src/tools/editor}/ReplOutput.svelte | 17 +- .../lab/src/tools/editor/StoreLoader.svelte | 51 + .../src/tools/editor}/TableBrowser.svelte | 4 +- .../src/tools/editor/ValueInspector.svelte | 104 + .../src/tools/editor}/coln-handle.svelte.ts | 0 .../lib => lab/src/tools/editor}/evaluate.ts | 0 .../lib => lab/src/tools/editor}/format.ts | 0 .../lib => lab/src/tools/editor}/schema.ts | 0 .../lib => lab/src/tools/editor}/snapshot.ts | 0 .../src/tools/editor}/source-storage.ts | 0 .../src/tools/graph}/GraphCanvas.svelte | 2 +- .../src/tools/graph}/GraphControls.svelte | 2 +- examples/lab/src/tools/graph/GraphTool.svelte | 261 ++ .../src/tools/graph}/coln-handle.svelte.ts | 0 .../{sync => lab/src/tools/graph}/compile.sh | 6 +- .../src/tools/graph}/generated/Graph.ts | 0 .../tools/graph}/generated/GraphRealm.json | 0 .../tools/graph}/generated/GraphRealm.pretty | 0 .../src/tools/graph}/generated/GraphRealm.ts | 0 .../src/tools/graph}/graph.coln | 0 .../src/lib => lab/src/tools/graph}/graph.ts | 2 +- .../{sync => lab}/tests/coln-handle.unit.ts | 4 +- .../tests/document-sync.unit.ts} | 92 +- .../{editor => lab}/tests/evaluate.unit.ts | 2 +- examples/{editor => lab}/tests/format.unit.ts | 2 +- examples/lab/tests/lab.spec.ts | 118 + .../{sync => lab}/tests/observe.svelte.ts | 0 examples/lab/tests/router.unit.ts | 47 + examples/{editor => lab}/tests/schema.unit.ts | 2 +- .../{editor => lab}/tests/snapshot.unit.ts | 2 +- .../tests/source-storage.unit.ts | 2 +- examples/{editor => lab}/tsconfig.json | 0 examples/lab/vite.config.ts | 63 + examples/{sync => lab}/vitest.config.ts | 6 +- examples/style.css | 115 - examples/sync-demo/.gitignore | 2 - examples/sync-demo/README.md | 69 - examples/sync-demo/compile.sh | 14 - examples/sync-demo/index.html | 12 - examples/sync-demo/package.json | 30 - examples/sync-demo/playwright.config.ts | 39 - examples/sync-demo/pnpm-lock.yaml | 847 ------ examples/sync-demo/pnpm-workspace.yaml | 5 - examples/sync-demo/server.mjs | 108 - examples/sync-demo/src/App.tsx | 320 --- examples/sync-demo/src/colnDocType.ts | 161 -- examples/sync-demo/src/generated/Graph.ts | 11 - .../sync-demo/src/generated/GraphRealm.json | 1 - .../sync-demo/src/generated/GraphRealm.ts | 48 - examples/sync-demo/src/main.tsx | 42 - examples/sync-demo/src/style.css | 158 -- .../sync-demo/test-results/.last-run.json | 4 - examples/sync-demo/tests/sync.spec.ts | 68 - examples/sync-demo/tsconfig.json | 23 - ...rge-automerge-repo-2.6.0-subduction.33.tgz | Bin 329865 -> 0 bytes examples/sync-demo/vite.config.ts | 13 - examples/sync-server/README.md | 28 - examples/sync-server/package.json | 14 - examples/sync/.gitignore | 4 - examples/sync/README.md | 102 - examples/sync/graph.coln | 7 - examples/sync/index.html | 15 - examples/sync/package.json | 43 - examples/sync/playwright.config.ts | 39 - examples/sync/pnpm-lock.yaml | 2339 ----------------- examples/sync/pnpm-workspace.yaml | 7 - examples/sync/src/browser/App.svelte | 165 -- .../sync/src/browser/DocumentLoadError.svelte | 53 - examples/sync/src/browser/main.ts | 62 - examples/sync/src/browser/style.css | 5 - examples/sync/src/cli/App.tsx | 219 -- examples/sync/src/cli/main.tsx | 113 - .../sync/src/lib/subduction-sync.svelte.ts | 106 - examples/sync/src/query/execute-query.ts | 77 - examples/sync/tests/sync.spec.ts | 202 -- examples/sync/tsconfig.json | 21 - examples/sync/vite.config.ts | 15 - flake.nix | 38 - justfile | 2 +- packages/coln-compiler-wasm/README.md | 2 +- 136 files changed, 1754 insertions(+), 8751 deletions(-) delete mode 100644 examples/compiler-demo/index.html delete mode 100644 examples/compiler/.gitignore delete mode 100644 examples/compiler/index.html delete mode 100644 examples/compiler/package.json delete mode 100644 examples/compiler/pnpm-lock.yaml delete mode 100644 examples/compiler/src/browser/App.svelte delete mode 100644 examples/compiler/src/browser/DocumentLoadError.svelte delete mode 100644 examples/compiler/src/lib/theory-sync.svelte.ts delete mode 100644 examples/compiler/src/main.ts delete mode 100644 examples/compiler/src/style.css delete mode 100644 examples/compiler/tsconfig.json delete mode 100644 examples/compiler/vite.config.ts delete mode 100644 examples/editor/README.md delete mode 100644 examples/editor/playwright.config.ts delete mode 100644 examples/editor/pnpm-workspace.yaml delete mode 100644 examples/editor/src/browser/StoreLoader.svelte delete mode 100644 examples/editor/src/main.ts delete mode 100644 examples/editor/src/style.css delete mode 100644 examples/editor/tests/editor.spec.ts delete mode 100644 examples/editor/vite.config.ts delete mode 100644 examples/editor/vitest.config.ts delete mode 100644 examples/index.html create mode 100644 examples/lab/.env.example rename examples/{editor => lab}/.gitignore (78%) create mode 100644 examples/lab/README.md rename examples/{editor => lab}/index.html (66%) rename examples/{editor => lab}/package.json (65%) create mode 100644 examples/lab/playwright.config.ts rename examples/{editor => lab}/pnpm-lock.yaml (97%) rename examples/{compiler => lab}/pnpm-workspace.yaml (61%) rename examples/{sync-server => lab/scripts}/server.mjs (100%) mode change 100755 => 100644 rename examples/{sync-server => lab/scripts}/with-server.mjs (100%) mode change 100755 => 100644 create mode 100644 examples/lab/src/app/Home.svelte create mode 100644 examples/lab/src/app/LabApp.svelte rename examples/{editor/src/lib/subduction-sync.svelte.ts => lab/src/lib/document-sync.svelte.ts} (89%) create mode 100644 examples/lab/src/lib/router.svelte.ts create mode 100644 examples/lab/src/main.ts create mode 100644 examples/lab/src/style.css create mode 100644 examples/lab/src/tools/compiler/CompilerTool.svelte rename examples/{compiler/src/browser => lab/src/tools/compiler}/JsonViewer.svelte (100%) rename examples/{compiler/src/browser => lab/src/tools/compiler}/OutputPanel.svelte (100%) rename examples/{compiler/src/browser => lab/src/tools/compiler}/SourceEditor.svelte (98%) rename examples/{compiler/src/browser => lab/src/tools/compiler}/StoresPanel.svelte (82%) rename examples/{compiler/src/lib => lab/src/tools/compiler}/compiled-realms.ts (100%) rename examples/{compiler/src/lib => lab/src/tools/compiler}/compiler.ts (87%) rename examples/{compiler/src/lib => lab/src/tools/compiler}/stores.ts (100%) rename examples/{compiler/src/lib => lab/src/tools/compiler}/theory-document.ts (100%) rename examples/{compiler/src/lib => lab/src/tools/compiler}/theory-handle.svelte.ts (100%) rename examples/{editor/src/browser/App.svelte => lab/src/tools/editor/EditorTool.svelte} (69%) rename examples/{editor/src/browser => lab/src/tools/editor}/ReplEditor.svelte (95%) rename examples/{editor/src/browser => lab/src/tools/editor}/ReplOutput.svelte (76%) create mode 100644 examples/lab/src/tools/editor/StoreLoader.svelte rename examples/{editor/src/browser => lab/src/tools/editor}/TableBrowser.svelte (97%) create mode 100644 examples/lab/src/tools/editor/ValueInspector.svelte rename examples/{editor/src/lib => lab/src/tools/editor}/coln-handle.svelte.ts (100%) rename examples/{editor/src/lib => lab/src/tools/editor}/evaluate.ts (100%) rename examples/{editor/src/lib => lab/src/tools/editor}/format.ts (100%) rename examples/{editor/src/lib => lab/src/tools/editor}/schema.ts (100%) rename examples/{editor/src/lib => lab/src/tools/editor}/snapshot.ts (100%) rename examples/{editor/src/lib => lab/src/tools/editor}/source-storage.ts (100%) rename examples/{sync/src/browser => lab/src/tools/graph}/GraphCanvas.svelte (99%) rename examples/{sync/src/browser => lab/src/tools/graph}/GraphControls.svelte (98%) create mode 100644 examples/lab/src/tools/graph/GraphTool.svelte rename examples/{sync/src/lib => lab/src/tools/graph}/coln-handle.svelte.ts (100%) rename examples/{sync => lab/src/tools/graph}/compile.sh (55%) rename examples/{sync/src => lab/src/tools/graph}/generated/Graph.ts (100%) rename examples/{sync/src => lab/src/tools/graph}/generated/GraphRealm.json (100%) rename examples/{sync/src => lab/src/tools/graph}/generated/GraphRealm.pretty (100%) rename examples/{sync/src => lab/src/tools/graph}/generated/GraphRealm.ts (100%) rename examples/{sync-demo => lab/src/tools/graph}/graph.coln (100%) rename examples/{sync/src/lib => lab/src/tools/graph}/graph.ts (98%) rename examples/{sync => lab}/tests/coln-handle.unit.ts (94%) rename examples/{sync/tests/subduction-sync.unit.ts => lab/tests/document-sync.unit.ts} (68%) rename examples/{editor => lab}/tests/evaluate.unit.ts (94%) rename examples/{editor => lab}/tests/format.unit.ts (91%) create mode 100644 examples/lab/tests/lab.spec.ts rename examples/{sync => lab}/tests/observe.svelte.ts (100%) create mode 100644 examples/lab/tests/router.unit.ts rename examples/{editor => lab}/tests/schema.unit.ts (96%) rename examples/{editor => lab}/tests/snapshot.unit.ts (95%) rename examples/{editor => lab}/tests/source-storage.unit.ts (94%) rename examples/{editor => lab}/tsconfig.json (100%) create mode 100644 examples/lab/vite.config.ts rename examples/{sync => lab}/vitest.config.ts (87%) delete mode 100644 examples/style.css delete mode 100644 examples/sync-demo/.gitignore delete mode 100644 examples/sync-demo/README.md delete mode 100755 examples/sync-demo/compile.sh delete mode 100644 examples/sync-demo/index.html delete mode 100644 examples/sync-demo/package.json delete mode 100644 examples/sync-demo/playwright.config.ts delete mode 100644 examples/sync-demo/pnpm-lock.yaml delete mode 100644 examples/sync-demo/pnpm-workspace.yaml delete mode 100644 examples/sync-demo/server.mjs delete mode 100644 examples/sync-demo/src/App.tsx delete mode 100644 examples/sync-demo/src/colnDocType.ts delete mode 100644 examples/sync-demo/src/generated/Graph.ts delete mode 100644 examples/sync-demo/src/generated/GraphRealm.json delete mode 100644 examples/sync-demo/src/generated/GraphRealm.ts delete mode 100644 examples/sync-demo/src/main.tsx delete mode 100644 examples/sync-demo/src/style.css delete mode 100644 examples/sync-demo/test-results/.last-run.json delete mode 100644 examples/sync-demo/tests/sync.spec.ts delete mode 100644 examples/sync-demo/tsconfig.json delete mode 100644 examples/sync-demo/vendor/automerge-automerge-repo-2.6.0-subduction.33.tgz delete mode 100644 examples/sync-demo/vite.config.ts delete mode 100644 examples/sync-server/README.md delete mode 100644 examples/sync-server/package.json delete mode 100644 examples/sync/.gitignore delete mode 100644 examples/sync/README.md delete mode 100644 examples/sync/graph.coln delete mode 100644 examples/sync/index.html delete mode 100644 examples/sync/package.json delete mode 100644 examples/sync/playwright.config.ts delete mode 100644 examples/sync/pnpm-lock.yaml delete mode 100644 examples/sync/pnpm-workspace.yaml delete mode 100644 examples/sync/src/browser/App.svelte delete mode 100644 examples/sync/src/browser/DocumentLoadError.svelte delete mode 100644 examples/sync/src/browser/main.ts delete mode 100644 examples/sync/src/browser/style.css delete mode 100644 examples/sync/src/cli/App.tsx delete mode 100644 examples/sync/src/cli/main.tsx delete mode 100644 examples/sync/src/lib/subduction-sync.svelte.ts delete mode 100644 examples/sync/src/query/execute-query.ts delete mode 100644 examples/sync/tests/sync.spec.ts delete mode 100644 examples/sync/tsconfig.json delete mode 100644 examples/sync/vite.config.ts diff --git a/.github/workflows/web-demos.yml b/.github/workflows/web-demos.yml index 6979b250..1bf8d023 100644 --- a/.github/workflows/web-demos.yml +++ b/.github/workflows/web-demos.yml @@ -1,110 +1,94 @@ +name: Coln Lab + on: - pull_request: push: - branches: [main] - -name: Web demos - -permissions: - contents: read - pull-requests: write + branches: [live-demos] concurrency: - group: web-demos-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + group: pages + cancel-in-progress: false jobs: - # build-and-deploy: - # runs-on: ubuntu-latest - # env: - # CARGO_TARGET_DIR: ${{ github.workspace }}/target - # NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - # NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} - - # steps: - # - name: Checkout ⬇️ - # uses: actions/checkout@v6 - - # - name: Install Nix ❄️ - # uses: cachix/install-nix-action@v31 - - # - name: Set up Cachix ♻️ - # if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - # uses: cachix/cachix-action@v17 - # with: - # name: coln - # authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} - - # - name: Restore build caches - # uses: actions/cache@v4 - # with: - # path: | - # .pnpm-store - # ~/.npm - # ~/.cargo/registry - # ~/.cargo/git - # ~/.ghc-wasm/.cabal - # target - # dist-newstyle - # key: web-demos-${{ runner.os }}-${{ hashFiles('flake.lock', 'Cargo.lock', 'cabal.project', 'packages/**/*.cabal', 'packages/coln-js-runtime/package-lock.json', 'examples/sync-demo/pnpm-lock.yaml') }} - # restore-keys: | - # web-demos-${{ runner.os }}- - - # - name: Build sync demo - # run: nix run --accept-flake-config .#sync-demo -- "$GITHUB_WORKSPACE" - # env: - # VITE_BASE: /sync/ - - # - name: Build web compiler - # run: | - # nix develop --accept-flake-config --command wasm32-wasi-cabal update 'hackage.haskell.org,2026-07-15T17:07:49Z' - # nix develop --accept-flake-config --command just examples/build-web-compiler - - # - name: Assemble deploy directory - # run: | - # cp examples/index.html examples/style.css _build/web/ - # cp -r examples/sync-demo/dist _build/web/sync - - # - name: Deploy PR preview to Netlify - # id: deploy-preview - # if: github.event_name == 'pull_request' && env.NETLIFY_AUTH_TOKEN != '' && env.NETLIFY_SITE_ID != '' - # run: | - # npx --yes netlify-cli@latest deploy \ - # --auth "$NETLIFY_AUTH_TOKEN" \ - # --site "$NETLIFY_SITE_ID" \ - # --dir _build/web \ - # --no-build \ - # --alias "pr-${{ github.event.pull_request.number }}" \ - # --message "PR #${{ github.event.pull_request.number }} web demos" \ - # --json > netlify-deploy.json - - # cat netlify-deploy.json - # deploy_url=$(node -e "const d = require('./netlify-deploy.json'); console.log(d.deploy_ssl_url || d.ssl_url || d.deploy_url || d.url)") - # echo "deploy-url=$deploy_url" >> "$GITHUB_OUTPUT" - - # - name: Comment with deploy preview URL - # if: github.event_name == 'pull_request' && steps.deploy-preview.outputs.deploy-url != '' - # uses: actions/github-script@v8 - # with: - # script: | - # const marker = '' - # const body = `${marker}\nWeb demo preview: ${{ steps.deploy-preview.outputs.deploy-url }}` - # const { owner, repo } = context.repo - # const issue_number = context.issue.number - # const comments = await github.rest.issues.listComments({ owner, repo, issue_number, per_page: 100 }) - # const existing = comments.data.find(comment => comment.body?.includes(marker)) - # if (existing) { - # await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }) - # } else { - # await github.rest.issues.createComment({ owner, repo, issue_number, body }) - # } - # - name: Deploy main to Netlify production - # if: github.event_name == 'push' && env.NETLIFY_AUTH_TOKEN != '' && env.NETLIFY_SITE_ID != '' - # run: | - # npx --yes netlify-cli@latest deploy \ - # --auth "$NETLIFY_AUTH_TOKEN" \ - # --site "$NETLIFY_SITE_ID" \ - # --dir _build/web \ - # --no-build \ - # --prod \ - # --message "web demos ${{ github.sha }}" + build: + runs-on: ubuntu-latest + permissions: + contents: read + env: + CARGO_TARGET_DIR: ${{ github.workspace }}/target + CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} + PNPM_STORE_DIR: ${{ github.workspace }}/.pnpm-store + VITE_BASE: /coln/ + VITE_SUBDUCTION_ENDPOINT: wss://coln.sync.inkandswitch.com + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install Nix + uses: cachix/install-nix-action@v31 + + - name: Set up Cachix + if: ${{ env.CACHIX_AUTH_TOKEN != '' }} + uses: cachix/cachix-action@v17 + with: + name: coln + authToken: ${{ env.CACHIX_AUTH_TOKEN }} + + - name: Restore build caches + uses: actions/cache@v4 + with: + path: | + .pnpm-store + ~/.npm + ~/.cargo/registry + ~/.cargo/git + ~/.ghc-wasm/.cabal + target + dist-newstyle + key: lab-${{ runner.os }}-${{ hashFiles('flake.lock', 'Cargo.lock', 'cabal.project', 'packages/**/*.cabal', 'packages/coln-js-runtime/package-lock.json', 'packages/coln-repo/pnpm-lock.yaml', 'examples/lab/pnpm-lock.yaml') }} + restore-keys: | + lab-${{ runner.os }}- + + - name: Build linked packages + run: | + nix develop --accept-flake-config --command npm ci --prefix packages/coln-js-runtime + nix develop --accept-flake-config --command npm run --prefix packages/coln-js-runtime build + nix develop --accept-flake-config --command pnpm --dir packages/coln-repo install --frozen-lockfile --store-dir "$PNPM_STORE_DIR" + nix develop --accept-flake-config --command pnpm --dir packages/coln-repo build + + - name: Install Lab dependencies + run: nix develop --accept-flake-config --command pnpm --dir examples/lab install --frozen-lockfile --store-dir "$PNPM_STORE_DIR" + + - name: Build compiler WASM + run: | + nix develop --accept-flake-config --command wasm32-wasi-cabal update 'hackage.haskell.org,2026-07-15T17:07:49Z' + nix develop --accept-flake-config --command just examples/stage-web-lab + + - name: Test Lab + run: nix develop --accept-flake-config --command pnpm --dir examples/lab test + + - name: Build Lab + run: nix develop --accept-flake-config --command pnpm --dir examples/lab build + + - name: Configure Pages + uses: actions/configure-pages@v5 + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v4 + with: + path: _build/web/lab-app + + deploy: + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 38a4c7b6..3ecb61c6 100644 --- a/.gitignore +++ b/.gitignore @@ -337,6 +337,8 @@ TSWLatexianTemp* .shake _build/ +.env +.env.local /cabal.project.local # nix result diff --git a/examples/cli-demo/skills/coln-repo/SKILL.md b/examples/cli-demo/skills/coln-repo/SKILL.md index d522047a..0dad660d 100644 --- a/examples/cli-demo/skills/coln-repo/SKILL.md +++ b/examples/cli-demo/skills/coln-repo/SKILL.md @@ -16,7 +16,7 @@ JSON object on stdout, including errors. Run `coln-repo help` for usage and ### 1. Resolve the sync endpoint Pass `--endpoint ` when the user names a relay. Otherwise the CLI uses -`SUBDUCTION_ENDPOINT`, then the public relay `wss://subduction.sync.inkandswitch.com`. +`SUBDUCTION_ENDPOINT`, then the public relay `wss://coln.sync.inkandswitch.com`. A document held by a local relay is only reachable there; do not invent one. If the response is `DOCUMENT_OPEN_FAILED`, report which endpoint you tried and ask. diff --git a/examples/cli-demo/src/cli/help.ts b/examples/cli-demo/src/cli/help.ts index c4dd6234..6d38d64e 100644 --- a/examples/cli-demo/src/cli/help.ts +++ b/examples/cli-demo/src/cli/help.ts @@ -34,7 +34,7 @@ Options: Environment: SUBDUCTION_ENDPOINT Sync server WebSocket URL, used when --endpoint is absent -The endpoint defaults to wss://subduction.sync.inkandswitch.com. A document held +The endpoint defaults to wss://coln.sync.inkandswitch.com. A document held by a local relay is only reachable by pointing at that relay, for example --endpoint ws://127.0.0.1:3030. diff --git a/examples/cli-demo/src/cli/repository.ts b/examples/cli-demo/src/cli/repository.ts index 82077868..250778ed 100644 --- a/examples/cli-demo/src/cli/repository.ts +++ b/examples/cli-demo/src/cli/repository.ts @@ -12,7 +12,7 @@ import { } from "@automerge/automerge-repo" import { find, type ColnHandle } from "@coln-project/repo" -export const defaultEndpoint = "wss://subduction.sync.inkandswitch.com" +export const defaultEndpoint = "wss://coln.sync.inkandswitch.com" export const operationTimeoutMs = 10_000 export interface OpenDocument { diff --git a/examples/compiler-demo/index.html b/examples/compiler-demo/index.html deleted file mode 100644 index 12fc5b72..00000000 --- a/examples/compiler-demo/index.html +++ /dev/null @@ -1,115 +0,0 @@ - - - - - Coln - - - -
-
-

Coln

- -
-

Source

- -

Diagnostics

-
-

IR

-
-

JSON

-

-    
- - - - diff --git a/examples/compiler/.gitignore b/examples/compiler/.gitignore deleted file mode 100644 index fc52e11d..00000000 --- a/examples/compiler/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -dist/ -node_modules/ -public/dist/ diff --git a/examples/compiler/index.html b/examples/compiler/index.html deleted file mode 100644 index 5819e31b..00000000 --- a/examples/compiler/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - Coln Compiler - - -
- - - diff --git a/examples/compiler/package.json b/examples/compiler/package.json deleted file mode 100644 index 387cbd60..00000000 --- a/examples/compiler/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "coln-compiler-example", - "private": true, - "type": "module", - "license": "(Apache-2.0 OR MIT)", - "scripts": { - "dev": "coln-with-sync-server vite --host 127.0.0.1", - "check": "svelte-check --tsconfig ./tsconfig.json && tsc --noEmit", - "build": "pnpm run check && vite build" - }, - "dependencies": { - "@automerge/automerge-codemirror": "0.2.0", - "@automerge/automerge-repo": "github:automerge/automerge-repo#doctypes&path:packages/automerge-repo", - "@automerge/automerge-repo-storage-indexeddb": "2.6.0-subduction.47", - "@automerge/automerge-subduction": "0.16.1", - "@codemirror/state": "^6.7.2", - "@codemirror/view": "^6.43.10", - "@coln-project/repo": "link:../../packages/coln-repo", - "codemirror": "^6.0.2", - "json-formatter-js": "^2.5.23", - "svelte": "^5.57.0" - }, - "devDependencies": { - "@coln-project/demo-sync-server": "file:../sync-server", - "@sveltejs/vite-plugin-svelte": "^7.3.0", - "@tailwindcss/vite": "^4.3.3", - "@types/node": "^26.4.1", - "svelte-check": "^4.7.6", - "tailwindcss": "^4.3.3", - "typescript": "^6.0.3", - "vite": "^8.2.2", - "vite-plugin-wasm": "^3.6.0" - } -} diff --git a/examples/compiler/pnpm-lock.yaml b/examples/compiler/pnpm-lock.yaml deleted file mode 100644 index e94f837a..00000000 --- a/examples/compiler/pnpm-lock.yaml +++ /dev/null @@ -1,1472 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -overrides: - '@automerge/automerge': 3.3.2 - '@automerge/automerge-subduction': 0.16.1 - -importers: - - .: - dependencies: - '@automerge/automerge-codemirror': - specifier: 0.2.0 - version: 0.2.0 - '@automerge/automerge-repo': - specifier: github:automerge/automerge-repo#doctypes&path:packages/automerge-repo - version: https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo - '@automerge/automerge-repo-storage-indexeddb': - specifier: 2.6.0-subduction.47 - version: 2.6.0-subduction.47 - '@automerge/automerge-subduction': - specifier: 0.16.1 - version: 0.16.1 - '@codemirror/state': - specifier: ^6.7.2 - version: 6.7.2 - '@codemirror/view': - specifier: ^6.43.10 - version: 6.43.10 - '@coln-project/repo': - specifier: link:../../packages/coln-repo - version: link:../../packages/coln-repo - codemirror: - specifier: ^6.0.2 - version: 6.0.2 - json-formatter-js: - specifier: ^2.5.23 - version: 2.5.23 - svelte: - specifier: ^5.57.0 - version: 5.57.0 - devDependencies: - '@coln-project/demo-sync-server': - specifier: file:../sync-server - version: file:../sync-server - '@sveltejs/vite-plugin-svelte': - specifier: ^7.3.0 - version: 7.3.0(svelte@5.57.0)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) - '@tailwindcss/vite': - specifier: ^4.3.3 - version: 4.3.3(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) - '@types/node': - specifier: ^26.4.1 - version: 26.4.1 - svelte-check: - specifier: ^4.7.6 - version: 4.7.6(picomatch@4.0.7)(svelte@5.57.0)(typescript@6.0.3) - tailwindcss: - specifier: ^4.3.3 - version: 4.3.3 - typescript: - specifier: ^6.0.3 - version: 6.0.3 - vite: - specifier: ^8.2.2 - version: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) - vite-plugin-wasm: - specifier: ^3.6.0 - version: 3.6.0(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) - -packages: - - '@automerge/automerge-codemirror@0.2.0': - resolution: {integrity: sha512-GBNh4ESZHFNHZKLznf/2G2KdmBD+unEdk64+l621LsjIsJs6HBmQmO8RCY1v7vIq31kFyoVHNcBMGQOnLgy4GA==} - - '@automerge/automerge-repo-storage-indexeddb@2.6.0-subduction.47': - resolution: {integrity: sha512-fYn6nse7jQnVb9EploTsBuYUzW7xFin2HZLneSEQ7w4pRCilIU92ghZEjZSWRKEPunSN0MYEvfJJJdlLyYTdkQ==} - engines: {node: '>=22.13'} - - '@automerge/automerge-repo@2.6.0-subduction.47': - resolution: {integrity: sha512-NGBQUjGH67Kyrc8a6zoafvDKT1+pnFi2/yiMgULS7l+apWQT2QkgQZ3vQGms/ngdrmM6ye1fhcwHwBn/DGJ6tA==} - engines: {node: '>=22.13'} - - '@automerge/automerge-repo@https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo': - resolution: {gitHosted: true, path: packages/automerge-repo, tarball: https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5} - version: 2.6.0-subduction.47 - engines: {node: '>=22.13'} - - '@automerge/automerge-subduction@0.16.1': - resolution: {integrity: sha512-alH7U4eYn0O7sT6hNLv7CJhNODJi+QD6qfRQTNfZCxL3Q5JZdJ8lFpdvXUkBf+I3bu0GLvVlTaYPWGrBihjpIw==} - - '@automerge/automerge@3.3.2': - resolution: {integrity: sha512-9vCdCL7pdQwUra66SBxPVHr+/t9epXKni9KDeak2rNBFMzABVh2u6gSpcwxi3jkR5qr047jVqZv9DlrOfVxLFw==} - - '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': - resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} - cpu: [arm64] - os: [darwin] - - '@cbor-extract/cbor-extract-darwin-x64@2.2.2': - resolution: {integrity: sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==} - cpu: [x64] - os: [darwin] - - '@cbor-extract/cbor-extract-linux-arm64@2.2.2': - resolution: {integrity: sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==} - cpu: [arm64] - os: [linux] - - '@cbor-extract/cbor-extract-linux-arm@2.2.2': - resolution: {integrity: sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==} - cpu: [arm] - os: [linux] - - '@cbor-extract/cbor-extract-linux-x64@2.2.2': - resolution: {integrity: sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==} - cpu: [x64] - os: [linux] - - '@cbor-extract/cbor-extract-win32-x64@2.2.2': - resolution: {integrity: sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==} - cpu: [x64] - os: [win32] - - '@codemirror/autocomplete@6.20.3': - resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} - - '@codemirror/commands@6.11.0': - resolution: {integrity: sha512-/K4Rl5BN0OtTiPWmJCdqODu38XnDMsDxKY5rgrPnCkutPTJf2wVbkoixLfealF5Kwse/s8P8M5jAiURiwSwnFA==} - - '@codemirror/language@6.12.4': - resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} - - '@codemirror/lint@6.9.7': - resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==} - - '@codemirror/search@6.7.2': - resolution: {integrity: sha512-gUYkYhT2+n/+VGZ+8EzE5WFkYZUZYm1VOKDudIsNqh42uRVQJ0a6Yss9sdKT3MeOYfuL1N6AZA57oza0Oyr0LA==} - - '@codemirror/state@6.7.2': - resolution: {integrity: sha512-U3RiPX62Wl/Gx4ftQ7UxLlloSfsFTQqKa+7vFBYteZGCzkp6oBqcsD7iSwniWRGobCAmDcwZSY+Six3+3ztdfg==} - - '@codemirror/view@6.43.10': - resolution: {integrity: sha512-vVWLvd4fKWYN/pMcwUrg6dWeublxmSz+V+52ZjW3h64IVN9cRUYLk5Km4JDT9vifxAFfDtNOIFxKYENthcolJQ==} - - '@coln-project/demo-sync-server@file:../sync-server': - resolution: {directory: ../sync-server, type: directory} - hasBin: true - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.6.0': - resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} - - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - - '@lezer/common@1.5.2': - resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} - - '@lezer/highlight@1.2.3': - resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} - - '@lezer/lr@1.4.10': - resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} - - '@marijn/find-cluster-break@1.0.4': - resolution: {integrity: sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==} - - '@noble/hashes@1.8.0': - resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} - engines: {node: ^14.21.3 || >=16} - - '@oxc-project/types@0.148.0': - resolution: {integrity: sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==} - - '@rolldown/binding-android-arm-eabi@1.2.7': - resolution: {integrity: sha512-EypzgnYCwyVY4NDHKzGmNJT5b+XaQEBniHxsMdeIQLB/tcCzZnhqrzHpZFbX9iaxx+5RiB8caATBtfvZP7zVxQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [android] - - '@rolldown/binding-android-arm64@1.2.7': - resolution: {integrity: sha512-l17HE9EweWaqJZhuUuNBN/FzM62xw+DECVnJyvMsxn8vJFAGLy5QfLDoYAcronkAN8VxKZHezDpulHDPx95vFw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@rolldown/binding-darwin-arm64@1.2.7': - resolution: {integrity: sha512-8ED8ELFvHXc6OCETIn4gXObPiaR6bckM/ipXtbzlPVDRMBfEGjCKgO90F9YtfdpDatVx/ZQw7aZ1vUMf/+T3Mw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.2.7': - resolution: {integrity: sha512-/WPripjtiAIZ2tWY7ddijORT0Ujg87wxWW/qcoFVCKAWVDPhtY0xr7Dj0M3GyNGz60jGwTElhro/mkF9dT7dDQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@rolldown/binding-freebsd-x64@1.2.7': - resolution: {integrity: sha512-14DI4NcqpvbICxSnGLx3PmtDaWqRP/KGSGb6C+JLLVPeZRl6dKdHba3pGsqT3vpdTqhEYIPG0MMQ8c0xYqoJxA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-linux-arm-gnueabihf@1.2.7': - resolution: {integrity: sha512-bxrWIRvHWQvbJwi+VIie/kDJmQxcNE6xxWwZdqF/ExVAigtHkv54WTLQPb+QsZdnFy18fg7JPfWGL0RH6vwIlQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.2.7': - resolution: {integrity: sha512-toOY2BChBZyuxU7OYX6Tn389di4IzAqPTycVcci0O7FSfBqzRB3RZn+K5Is6ANf4tmgRd/K1yZTsNTXbkXsnLg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-arm64-musl@1.2.7': - resolution: {integrity: sha512-lAIXTH/aiLRLxsTgQvfhjo4K1ydWIp00+V0voOr9beb/9ZmkUFrSIb03dXNFRgMNvkE6oGsF10ioQ6UsI+vS5Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rolldown/binding-linux-ppc64-gnu@1.2.7': - resolution: {integrity: sha512-kdnwS28Pkenp/mZMRwjXXXwxQ7pIsm+bF919LUK93BOyhcLsrVKdP2p9fxpiPNPAbNuch8ypQt0pm2P2LYCAGg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-s390x-gnu@1.2.7': - resolution: {integrity: sha512-516OdsyLdr5E65paF3yBF55t8mfm9+gmtCsK3xI7XKXIT7EfRlHhxL8K/NR6Hu8BWSgF5+1w74lTL0+nxcc8Qw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-gnu@1.2.7': - resolution: {integrity: sha512-r8/z8n7GFaYRln3xmP1Cxy0HH/HLM0uBUPkEuSVEfKGDA89M0FsZRZJRSwe/tJjRx+fpH/gjorfhB8tmEbSFLA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-musl@1.2.7': - resolution: {integrity: sha512-pAsE8iiDxUg1xBqdhrTfg45AVDVpirjz00sblEYClGNNcMnDb+e8beQgqIAw6LvauX/APvgxUnwrgun/YYGBhw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rolldown/binding-openharmony-arm64@1.2.7': - resolution: {integrity: sha512-lTcIYmmnQQA8Or/2DatS6oSqcdLHvendjS+zLu+FwgToynWMRSmQdpM65fTANJgIS4mjbMOo5KT2lnT9SAb96w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@rolldown/binding-win32-arm64-msvc@1.2.7': - resolution: {integrity: sha512-e3Gu3WxbNk/UqQhxqU7YIYO+9ZBvWNz3U+h/qRFosscMFzdRPbXYSaSWgSnklv2fz1TgzBTcti2z35c/7irsHw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.2.7': - resolution: {integrity: sha512-W/jg5qoRSqjsEv0+dZi4e687mcHqmVuU0P4fK6qS/xjetW2Gmc1W8j//z5nAeNcC8Ttm0hV46IjcYeuVwYhuiw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@rolldown/pluginutils@1.0.1': - resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - - '@sveltejs/acorn-typescript@1.0.13': - resolution: {integrity: sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==} - peerDependencies: - acorn: ^8.9.0 - - '@sveltejs/load-config@0.2.3': - resolution: {integrity: sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ==} - engines: {node: '>= 18.0.0'} - - '@sveltejs/vite-plugin-svelte@7.3.0': - resolution: {integrity: sha512-QbRoJyD92e9R0ufeQIWRHrCC0ObcqSv/aBDdrQMoU+sypav3cDx5wytdQ6GLdXjEMO6xjrXGzfkUygng8JMv0A==} - engines: {node: ^20.19 || ^22.12 || >=24} - peerDependencies: - svelte: ^5.46.4 - vite: ^8.0.0-beta.7 || ^8.0.0 - - '@tailwindcss/node@4.3.3': - resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} - - '@tailwindcss/oxide-android-arm64@4.3.3': - resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [android] - - '@tailwindcss/oxide-darwin-arm64@4.3.3': - resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [darwin] - - '@tailwindcss/oxide-darwin-x64@4.3.3': - resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} - engines: {node: '>= 20'} - cpu: [x64] - os: [darwin] - - '@tailwindcss/oxide-freebsd-x64@4.3.3': - resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} - engines: {node: '>= 20'} - cpu: [x64] - os: [freebsd] - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': - resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} - engines: {node: '>= 20'} - cpu: [arm] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': - resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@tailwindcss/oxide-linux-arm64-musl@4.3.3': - resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@tailwindcss/oxide-linux-x64-gnu@4.3.3': - resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@tailwindcss/oxide-linux-x64-musl@4.3.3': - resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@tailwindcss/oxide-wasm32-wasi@4.3.3': - resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - bundledDependencies: - - '@napi-rs/wasm-runtime' - - '@emnapi/core' - - '@emnapi/runtime' - - '@tybys/wasm-util' - - '@emnapi/wasi-threads' - - tslib - - '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': - resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [win32] - - '@tailwindcss/oxide-win32-x64-msvc@4.3.3': - resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} - engines: {node: '>= 20'} - cpu: [x64] - os: [win32] - - '@tailwindcss/oxide@4.3.3': - resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} - engines: {node: '>= 20'} - - '@tailwindcss/vite@4.3.3': - resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} - peerDependencies: - vite: ^5.2.0 || ^6 || ^7 || ^8 - - '@types/estree@1.0.9': - resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - - '@types/node@26.4.1': - resolution: {integrity: sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==} - - acorn@8.18.0: - resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} - engines: {node: '>=0.4.0'} - hasBin: true - - aria-query@5.3.1: - resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} - engines: {node: '>= 0.4'} - - axobject-query@4.1.0: - resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} - engines: {node: '>= 0.4'} - - base-x@5.0.1: - resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} - - bs58@6.0.0: - resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} - - bs58check@4.0.0: - resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} - - cbor-extract@2.2.2: - resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} - hasBin: true - - cbor-x@1.6.6: - resolution: {integrity: sha512-8QiD9PGOxyQHo7s2pzwTBH6lTjqekxPdl9Aq6fXvZgCuCJHOht1puDEA/fTr6mciB76c+M+Gi0qT2i1a4pm4Wg==} - - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} - - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} - - codemirror@6.0.2: - resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} - - crelt@1.0.7: - resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - devalue@5.9.2: - resolution: {integrity: sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w==} - - enhanced-resolve@5.24.5: - resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} - engines: {node: '>=10.13.0'} - - esm-env@1.2.2: - resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} - - esrap@2.3.6: - resolution: {integrity: sha512-yc0OC12UjPqLoc+fe+v5GNs4TOjAigUw3sTikfC+xeBPGUw7gDRz3DtYaqEhxyMVJojcSWJw7jT0QWR+CbuE/A==} - peerDependencies: - '@typescript-eslint/types': ^8.2.0 - peerDependenciesMeta: - '@typescript-eslint/types': - optional: true - - eventemitter3@5.0.4: - resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} - - fast-sha256@1.3.0: - resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - is-reference@3.0.3: - resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} - - isomorphic-ws@5.0.0: - resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} - peerDependencies: - ws: '*' - - jiti@2.7.0: - resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} - hasBin: true - - json-formatter-js@2.5.23: - resolution: {integrity: sha512-Cbm8wHXjo/C56aCePP1VuKvjxoMEmL7g7Ckss1oWFFlCsvOEEbye1kTeaNNaqba1Cl6YpIOYAnK65pUQ8mDIUQ==} - - lightningcss-android-arm64@1.32.0: - resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-android-arm64@1.33.0: - resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-darwin-arm64@1.32.0: - resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-arm64@1.33.0: - resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.32.0: - resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-darwin-x64@1.33.0: - resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.32.0: - resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-freebsd-x64@1.33.0: - resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm-gnueabihf@1.33.0: - resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.32.0: - resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - lightningcss-linux-arm64-gnu@1.33.0: - resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - lightningcss-linux-arm64-musl@1.32.0: - resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - lightningcss-linux-arm64-musl@1.33.0: - resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - lightningcss-linux-x64-gnu@1.32.0: - resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - lightningcss-linux-x64-gnu@1.33.0: - resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - lightningcss-linux-x64-musl@1.32.0: - resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - lightningcss-linux-x64-musl@1.33.0: - resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - lightningcss-win32-arm64-msvc@1.32.0: - resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-arm64-msvc@1.33.0: - resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.32.0: - resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss-win32-x64-msvc@1.33.0: - resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.32.0: - resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} - engines: {node: '>= 12.0.0'} - - lightningcss@1.33.0: - resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} - engines: {node: '>= 12.0.0'} - - locate-character@3.0.0: - resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - magic-string@1.2.3: - resolution: {integrity: sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==} - - mri@1.2.0: - resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} - engines: {node: '>=4'} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - nanoid@3.3.18: - resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - node-gyp-build-optional-packages@5.1.1: - resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} - hasBin: true - - obug@2.1.4: - resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} - engines: {node: '>=12.20.0'} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@4.0.7: - resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} - engines: {node: '>=12'} - - postcss@8.5.26: - resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} - engines: {node: ^10 || ^12 || >=14} - - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} - - rolldown@1.2.7: - resolution: {integrity: sha512-g0EtLvBjTUB7jhyV0S/TCup3v/XSVl45vUIGbOGU4QPiyjTenCe4mKuFvW9fEgYmS2Fo42AUssRmNuMziXdrig==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - sade@1.8.1: - resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} - engines: {node: '>=6'} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - style-mod@4.1.3: - resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} - - svelte-check@4.7.6: - resolution: {integrity: sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw==} - engines: {node: '>= 18.0.0'} - hasBin: true - peerDependencies: - svelte: ^4.0.0 || ^5.0.0-next.0 - typescript: ^5.0.0 || ^6.0.0 - - svelte@5.57.0: - resolution: {integrity: sha512-NdbDn7fl4be1ViUG0oq/lvG6OZy3oENolV2ONjiqqsfVoeAfzaQAKUcEX3MrQod/Bebv1PgwET9rfXhgn9s4Kg==} - engines: {node: '>=18'} - - tailwindcss@4.3.3: - resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} - - tapable@2.3.3: - resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} - engines: {node: '>=6'} - - tinyglobby@0.2.17: - resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} - engines: {node: '>=12.0.0'} - - typescript@6.0.3: - resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} - engines: {node: '>=14.17'} - hasBin: true - - undici-types@8.3.0: - resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} - - uuid@14.0.2: - resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} - hasBin: true - - vite-plugin-wasm@3.6.0: - resolution: {integrity: sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==} - peerDependencies: - vite: ^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - - vite@8.2.2: - resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.4.0 || ^0.5.0 - esbuild: ^0.27.0 || ^0.28.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vitefu@1.1.3: - resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} - peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - vite: - optional: true - - w3c-keyname@2.2.8: - resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} - - ws@8.21.3: - resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - xstate@5.32.6: - resolution: {integrity: sha512-WfA8WNrh6r9osuGwVm+aIPM4jmMW2LKHV1Yv9thYpOtL4HVF/kobh95K/O8v2jDCpDmP2EoY+6uvuqHXCZ6ZLw==} - - zimmerframe@1.1.5: - resolution: {integrity: sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==} - -snapshots: - - '@automerge/automerge-codemirror@0.2.0': - dependencies: - '@automerge/automerge': 3.3.2 - '@codemirror/state': 6.7.2 - '@codemirror/view': 6.43.10 - codemirror: 6.0.2 - - '@automerge/automerge-repo-storage-indexeddb@2.6.0-subduction.47': - dependencies: - '@automerge/automerge-repo': 2.6.0-subduction.47 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - '@automerge/automerge-repo@2.6.0-subduction.47': - dependencies: - '@automerge/automerge': 3.3.2 - '@automerge/automerge-subduction': 0.16.1 - bs58check: 4.0.0 - cbor-x: 1.6.6 - debug: 4.4.3 - eventemitter3: 5.0.4 - fast-sha256: 1.3.0 - isomorphic-ws: 5.0.0(ws@8.21.3) - uuid: 14.0.2 - ws: 8.21.3 - xstate: 5.32.6 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - '@automerge/automerge-repo@https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo': - dependencies: - '@automerge/automerge': 3.3.2 - '@automerge/automerge-subduction': 0.16.1 - bs58check: 4.0.0 - cbor-x: 1.6.6 - debug: 4.4.3 - eventemitter3: 5.0.4 - fast-sha256: 1.3.0 - isomorphic-ws: 5.0.0(ws@8.21.3) - uuid: 14.0.2 - ws: 8.21.3 - xstate: 5.32.6 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - '@automerge/automerge-subduction@0.16.1': {} - - '@automerge/automerge@3.3.2': {} - - '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-darwin-x64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-linux-arm64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-linux-arm@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-linux-x64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-win32-x64@2.2.2': - optional: true - - '@codemirror/autocomplete@6.20.3': - dependencies: - '@codemirror/language': 6.12.4 - '@codemirror/state': 6.7.2 - '@codemirror/view': 6.43.10 - '@lezer/common': 1.5.2 - - '@codemirror/commands@6.11.0': - dependencies: - '@codemirror/language': 6.12.4 - '@codemirror/state': 6.7.2 - '@codemirror/view': 6.43.10 - '@lezer/common': 1.5.2 - - '@codemirror/language@6.12.4': - dependencies: - '@codemirror/state': 6.7.2 - '@codemirror/view': 6.43.10 - '@lezer/common': 1.5.2 - '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.10 - style-mod: 4.1.3 - - '@codemirror/lint@6.9.7': - dependencies: - '@codemirror/state': 6.7.2 - '@codemirror/view': 6.43.10 - crelt: 1.0.7 - - '@codemirror/search@6.7.2': - dependencies: - '@codemirror/state': 6.7.2 - '@codemirror/view': 6.43.10 - crelt: 1.0.7 - - '@codemirror/state@6.7.2': - dependencies: - '@marijn/find-cluster-break': 1.0.4 - - '@codemirror/view@6.43.10': - dependencies: - '@codemirror/state': 6.7.2 - crelt: 1.0.7 - style-mod: 4.1.3 - w3c-keyname: 2.2.8 - - '@coln-project/demo-sync-server@file:../sync-server': - dependencies: - '@automerge/automerge-subduction': 0.16.1 - ws: 8.21.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.6.0 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.6.0': {} - - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.6.0 - - '@lezer/common@1.5.2': {} - - '@lezer/highlight@1.2.3': - dependencies: - '@lezer/common': 1.5.2 - - '@lezer/lr@1.4.10': - dependencies: - '@lezer/common': 1.5.2 - - '@marijn/find-cluster-break@1.0.4': {} - - '@noble/hashes@1.8.0': {} - - '@oxc-project/types@0.148.0': {} - - '@rolldown/binding-android-arm-eabi@1.2.7': - optional: true - - '@rolldown/binding-android-arm64@1.2.7': - optional: true - - '@rolldown/binding-darwin-arm64@1.2.7': - optional: true - - '@rolldown/binding-darwin-x64@1.2.7': - optional: true - - '@rolldown/binding-freebsd-x64@1.2.7': - optional: true - - '@rolldown/binding-linux-arm-gnueabihf@1.2.7': - optional: true - - '@rolldown/binding-linux-arm64-gnu@1.2.7': - optional: true - - '@rolldown/binding-linux-arm64-musl@1.2.7': - optional: true - - '@rolldown/binding-linux-ppc64-gnu@1.2.7': - optional: true - - '@rolldown/binding-linux-s390x-gnu@1.2.7': - optional: true - - '@rolldown/binding-linux-x64-gnu@1.2.7': - optional: true - - '@rolldown/binding-linux-x64-musl@1.2.7': - optional: true - - '@rolldown/binding-openharmony-arm64@1.2.7': - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.2.7': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.2.7': - optional: true - - '@rolldown/pluginutils@1.0.1': {} - - '@sveltejs/acorn-typescript@1.0.13(acorn@8.18.0)': - dependencies: - acorn: 8.18.0 - - '@sveltejs/load-config@0.2.3': {} - - '@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))': - dependencies: - deepmerge: 4.3.1 - magic-string: 1.2.3 - obug: 2.1.4 - svelte: 5.57.0 - vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) - vitefu: 1.1.3(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)) - - '@tailwindcss/node@4.3.3': - dependencies: - '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.24.5 - jiti: 2.7.0 - lightningcss: 1.32.0 - magic-string: 0.30.21 - source-map-js: 1.2.1 - tailwindcss: 4.3.3 - - '@tailwindcss/oxide-android-arm64@4.3.3': - optional: true - - '@tailwindcss/oxide-darwin-arm64@4.3.3': - optional: true - - '@tailwindcss/oxide-darwin-x64@4.3.3': - optional: true - - '@tailwindcss/oxide-freebsd-x64@4.3.3': - optional: true - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': - optional: true - - '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': - optional: true - - '@tailwindcss/oxide-linux-arm64-musl@4.3.3': - optional: true - - '@tailwindcss/oxide-linux-x64-gnu@4.3.3': - optional: true - - '@tailwindcss/oxide-linux-x64-musl@4.3.3': - optional: true - - '@tailwindcss/oxide-wasm32-wasi@4.3.3': - optional: true - - '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': - optional: true - - '@tailwindcss/oxide-win32-x64-msvc@4.3.3': - optional: true - - '@tailwindcss/oxide@4.3.3': - optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.3.3 - '@tailwindcss/oxide-darwin-arm64': 4.3.3 - '@tailwindcss/oxide-darwin-x64': 4.3.3 - '@tailwindcss/oxide-freebsd-x64': 4.3.3 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 - '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 - '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 - '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 - '@tailwindcss/oxide-linux-x64-musl': 4.3.3 - '@tailwindcss/oxide-wasm32-wasi': 4.3.3 - '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 - '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - - '@tailwindcss/vite@4.3.3(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))': - dependencies: - '@tailwindcss/node': 4.3.3 - '@tailwindcss/oxide': 4.3.3 - tailwindcss: 4.3.3 - vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) - - '@types/estree@1.0.9': {} - - '@types/node@26.4.1': - dependencies: - undici-types: 8.3.0 - - acorn@8.18.0: {} - - aria-query@5.3.1: {} - - axobject-query@4.1.0: {} - - base-x@5.0.1: {} - - bs58@6.0.0: - dependencies: - base-x: 5.0.1 - - bs58check@4.0.0: - dependencies: - '@noble/hashes': 1.8.0 - bs58: 6.0.0 - - cbor-extract@2.2.2: - dependencies: - node-gyp-build-optional-packages: 5.1.1 - optionalDependencies: - '@cbor-extract/cbor-extract-darwin-arm64': 2.2.2 - '@cbor-extract/cbor-extract-darwin-x64': 2.2.2 - '@cbor-extract/cbor-extract-linux-arm': 2.2.2 - '@cbor-extract/cbor-extract-linux-arm64': 2.2.2 - '@cbor-extract/cbor-extract-linux-x64': 2.2.2 - '@cbor-extract/cbor-extract-win32-x64': 2.2.2 - optional: true - - cbor-x@1.6.6: - optionalDependencies: - cbor-extract: 2.2.2 - - chokidar@4.0.3: - dependencies: - readdirp: 4.1.2 - - clsx@2.1.1: {} - - codemirror@6.0.2: - dependencies: - '@codemirror/autocomplete': 6.20.3 - '@codemirror/commands': 6.11.0 - '@codemirror/language': 6.12.4 - '@codemirror/lint': 6.9.7 - '@codemirror/search': 6.7.2 - '@codemirror/state': 6.7.2 - '@codemirror/view': 6.43.10 - - crelt@1.0.7: {} - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - deepmerge@4.3.1: {} - - detect-libc@2.1.2: {} - - devalue@5.9.2: {} - - enhanced-resolve@5.24.5: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.3 - - esm-env@1.2.2: {} - - esrap@2.3.6: - dependencies: - '@jridgewell/sourcemap-codec': 1.6.0 - - eventemitter3@5.0.4: {} - - fast-sha256@1.3.0: {} - - fdir@6.5.0(picomatch@4.0.7): - optionalDependencies: - picomatch: 4.0.7 - - fsevents@2.3.3: - optional: true - - graceful-fs@4.2.11: {} - - is-reference@3.0.3: - dependencies: - '@types/estree': 1.0.9 - - isomorphic-ws@5.0.0(ws@8.21.3): - dependencies: - ws: 8.21.3 - - jiti@2.7.0: {} - - json-formatter-js@2.5.23: {} - - lightningcss-android-arm64@1.32.0: - optional: true - - lightningcss-android-arm64@1.33.0: - optional: true - - lightningcss-darwin-arm64@1.32.0: - optional: true - - lightningcss-darwin-arm64@1.33.0: - optional: true - - lightningcss-darwin-x64@1.32.0: - optional: true - - lightningcss-darwin-x64@1.33.0: - optional: true - - lightningcss-freebsd-x64@1.32.0: - optional: true - - lightningcss-freebsd-x64@1.33.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.32.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.33.0: - optional: true - - lightningcss-linux-arm64-gnu@1.32.0: - optional: true - - lightningcss-linux-arm64-gnu@1.33.0: - optional: true - - lightningcss-linux-arm64-musl@1.32.0: - optional: true - - lightningcss-linux-arm64-musl@1.33.0: - optional: true - - lightningcss-linux-x64-gnu@1.32.0: - optional: true - - lightningcss-linux-x64-gnu@1.33.0: - optional: true - - lightningcss-linux-x64-musl@1.32.0: - optional: true - - lightningcss-linux-x64-musl@1.33.0: - optional: true - - lightningcss-win32-arm64-msvc@1.32.0: - optional: true - - lightningcss-win32-arm64-msvc@1.33.0: - optional: true - - lightningcss-win32-x64-msvc@1.32.0: - optional: true - - lightningcss-win32-x64-msvc@1.33.0: - optional: true - - lightningcss@1.32.0: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.32.0 - lightningcss-darwin-arm64: 1.32.0 - lightningcss-darwin-x64: 1.32.0 - lightningcss-freebsd-x64: 1.32.0 - lightningcss-linux-arm-gnueabihf: 1.32.0 - lightningcss-linux-arm64-gnu: 1.32.0 - lightningcss-linux-arm64-musl: 1.32.0 - lightningcss-linux-x64-gnu: 1.32.0 - lightningcss-linux-x64-musl: 1.32.0 - lightningcss-win32-arm64-msvc: 1.32.0 - lightningcss-win32-x64-msvc: 1.32.0 - - lightningcss@1.33.0: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.33.0 - lightningcss-darwin-arm64: 1.33.0 - lightningcss-darwin-x64: 1.33.0 - lightningcss-freebsd-x64: 1.33.0 - lightningcss-linux-arm-gnueabihf: 1.33.0 - lightningcss-linux-arm64-gnu: 1.33.0 - lightningcss-linux-arm64-musl: 1.33.0 - lightningcss-linux-x64-gnu: 1.33.0 - lightningcss-linux-x64-musl: 1.33.0 - lightningcss-win32-arm64-msvc: 1.33.0 - lightningcss-win32-x64-msvc: 1.33.0 - - locate-character@3.0.0: {} - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.6.0 - - magic-string@1.2.3: - dependencies: - '@jridgewell/sourcemap-codec': 1.6.0 - - mri@1.2.0: {} - - ms@2.1.3: {} - - nanoid@3.3.18: {} - - node-gyp-build-optional-packages@5.1.1: - dependencies: - detect-libc: 2.1.2 - optional: true - - obug@2.1.4: {} - - picocolors@1.1.1: {} - - picomatch@4.0.7: {} - - postcss@8.5.26: - dependencies: - nanoid: 3.3.18 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - readdirp@4.1.2: {} - - rolldown@1.2.7: - dependencies: - '@oxc-project/types': 0.148.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm-eabi': 1.2.7 - '@rolldown/binding-android-arm64': 1.2.7 - '@rolldown/binding-darwin-arm64': 1.2.7 - '@rolldown/binding-darwin-x64': 1.2.7 - '@rolldown/binding-freebsd-x64': 1.2.7 - '@rolldown/binding-linux-arm-gnueabihf': 1.2.7 - '@rolldown/binding-linux-arm64-gnu': 1.2.7 - '@rolldown/binding-linux-arm64-musl': 1.2.7 - '@rolldown/binding-linux-ppc64-gnu': 1.2.7 - '@rolldown/binding-linux-s390x-gnu': 1.2.7 - '@rolldown/binding-linux-x64-gnu': 1.2.7 - '@rolldown/binding-linux-x64-musl': 1.2.7 - '@rolldown/binding-openharmony-arm64': 1.2.7 - '@rolldown/binding-win32-arm64-msvc': 1.2.7 - '@rolldown/binding-win32-x64-msvc': 1.2.7 - - sade@1.8.1: - dependencies: - mri: 1.2.0 - - source-map-js@1.2.1: {} - - style-mod@4.1.3: {} - - svelte-check@4.7.6(picomatch@4.0.7)(svelte@5.57.0)(typescript@6.0.3): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - '@sveltejs/load-config': 0.2.3 - chokidar: 4.0.3 - fdir: 6.5.0(picomatch@4.0.7) - picocolors: 1.1.1 - sade: 1.8.1 - svelte: 5.57.0 - typescript: 6.0.3 - transitivePeerDependencies: - - picomatch - - svelte@5.57.0: - dependencies: - '@jridgewell/remapping': 2.3.5 - '@jridgewell/sourcemap-codec': 1.6.0 - '@sveltejs/acorn-typescript': 1.0.13(acorn@8.18.0) - '@types/estree': 1.0.9 - acorn: 8.18.0 - aria-query: 5.3.1 - axobject-query: 4.1.0 - clsx: 2.1.1 - devalue: 5.9.2 - esm-env: 1.2.2 - esrap: 2.3.6 - is-reference: 3.0.3 - locate-character: 3.0.0 - magic-string: 0.30.21 - zimmerframe: 1.1.5 - transitivePeerDependencies: - - '@typescript-eslint/types' - - tailwindcss@4.3.3: {} - - tapable@2.3.3: {} - - tinyglobby@0.2.17: - dependencies: - fdir: 6.5.0(picomatch@4.0.7) - picomatch: 4.0.7 - - typescript@6.0.3: {} - - undici-types@8.3.0: {} - - uuid@14.0.2: {} - - vite-plugin-wasm@3.6.0(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)): - dependencies: - vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) - - vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0): - dependencies: - lightningcss: 1.33.0 - picomatch: 4.0.7 - postcss: 8.5.26 - rolldown: 1.2.7 - tinyglobby: 0.2.17 - optionalDependencies: - '@types/node': 26.4.1 - fsevents: 2.3.3 - jiti: 2.7.0 - - vitefu@1.1.3(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0)): - optionalDependencies: - vite: 8.2.2(@types/node@26.4.1)(jiti@2.7.0) - - w3c-keyname@2.2.8: {} - - ws@8.21.3: {} - - xstate@5.32.6: {} - - zimmerframe@1.1.5: {} diff --git a/examples/compiler/src/browser/App.svelte b/examples/compiler/src/browser/App.svelte deleted file mode 100644 index 7c88a5d4..00000000 --- a/examples/compiler/src/browser/App.svelte +++ /dev/null @@ -1,325 +0,0 @@ - - - - - - - Coln Compiler - - -
- {#if feedback} -

- {feedback} -

- {/if} - -
- - C - COLN / COMPILER - -
- -
-
- {statusLabel} -
-
- {syncStatus} - / {sourceLines} {sourceLines === 1 ? "line" : "lines"} -
-
-
-
- -
-
-
-
-

SOURCE

-

Collaborative Automerge document

-
- Live theory -
- -
- -
- {#if error} - - {/if} - {#if syncStatus === "error"} - - {/if} - - - {#if compilation.diagnosticsHtml.length === 0} -

No diagnostics

- {:else} -
- {#each compilation.diagnosticsHtml as diagnostic} -
{@html diagnostic}
- {/each} -
- {/if} -
- - - {#if compilation.prettyIr.length === 0} -

No intermediate representation

- {:else} -
- {#each compilation.prettyIr as realm} -
{realm}
- {/each} -
- {/if} -
- - - {#if compilation.irJson === ""} -

No JSON output

- {:else} - - {/if} -
-
- - selectedRealmName = name} - oncreate={createSelectedStore} - oncopy={url => void copyStoreUrl(url)} - /> -
-
diff --git a/examples/compiler/src/browser/DocumentLoadError.svelte b/examples/compiler/src/browser/DocumentLoadError.svelte deleted file mode 100644 index 4c6c9537..00000000 --- a/examples/compiler/src/browser/DocumentLoadError.svelte +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - {title} | Coln Compiler - - -
-
-
- C - COLN / COMPILER -
- -
-

{title.toUpperCase()}

-

{heading}

-

{detail}

-
- - {documentUrl} - - - Create a new theory + - -
-
diff --git a/examples/compiler/src/lib/theory-sync.svelte.ts b/examples/compiler/src/lib/theory-sync.svelte.ts deleted file mode 100644 index bb7dfd53..00000000 --- a/examples/compiler/src/lib/theory-sync.svelte.ts +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// -// SPDX-License-Identifier: Apache-2.0 OR MIT - -import type { Repo } from "@automerge/automerge-repo" -import { createSubscriber } from "svelte/reactivity" -import type { TheoryDocumentHandle } from "./theory-document.ts" - -export type SyncStatus = "offline" | "syncing" | "synced" | "error" - -export class TheorySync { - readonly #subscribe: () => void - #status: SyncStatus - #error: unknown - - constructor( - private readonly repo: Repo, - private readonly handle: TheoryDocumentHandle, - ) { - this.#status = repo.isSubductionConnected() ? "syncing" : "offline" - this.#subscribe = createSubscriber(update => { - let active = true - let generation = 0 - let currentAttempt: symbol | undefined - - const setStatus = (status: SyncStatus, error?: unknown) => { - if (this.#status === status && this.#error === error) return - this.#status = status - this.#error = error - update() - } - - const startFlush = () => { - if (currentAttempt || !active || !this.repo.isSubductionConnected()) return - const attempt = Symbol() - const flushGeneration = generation - currentAttempt = attempt - void this.repo.flush([this.handle.documentId]).then( - () => finishFlush(attempt, flushGeneration, false), - error => finishFlush(attempt, flushGeneration, true, error), - ) - } - - const finishFlush = ( - attempt: symbol, - flushGeneration: number, - failed: boolean, - error?: unknown, - ) => { - if (!active || currentAttempt !== attempt) return - currentAttempt = undefined - if (!this.repo.isSubductionConnected()) { - setStatus("offline") - } else if (flushGeneration !== generation) { - startFlush() - } else if (failed) { - setStatus("error", error) - } else { - setStatus("synced") - } - } - - const requestFlush = () => { - generation += 1 - if (!this.repo.isSubductionConnected()) { - currentAttempt = undefined - setStatus("offline") - return - } - setStatus("syncing") - startFlush() - } - - this.handle.on("change", requestFlush) - this.repo.on("subduction-connection", requestFlush) - requestFlush() - - return () => { - active = false - generation += 1 - currentAttempt = undefined - this.handle.off("change", requestFlush) - this.repo.off("subduction-connection", requestFlush) - } - }) - } - - get status(): SyncStatus { - this.#subscribe() - return this.#status - } - - get error(): unknown { - this.#subscribe() - return this.#error - } -} diff --git a/examples/compiler/src/main.ts b/examples/compiler/src/main.ts deleted file mode 100644 index 5a269886..00000000 --- a/examples/compiler/src/main.ts +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// -// SPDX-License-Identifier: Apache-2.0 OR MIT - -import { - Repo, - isValidAutomergeUrl, - type AutomergeUrl, -} from "@automerge/automerge-repo" -import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb" -// @ts-expect-error initSync is exported at runtime but absent from declarations -import { initSync } from "@automerge/automerge-subduction/slim" -// @ts-expect-error wasm-base64 is exported without declarations -import { wasmBase64 } from "@automerge/automerge-subduction/wasm-base64" -import { mount } from "svelte" -import App from "./browser/App.svelte" -import DocumentLoadError from "./browser/DocumentLoadError.svelte" -import { - isTheoryDocument, - newTheoryDocument, - type TheoryDocument, -} from "./lib/theory-document.ts" -import "./style.css" - -const target = document.getElementById("app")! -const hashUrl = location.hash.slice(1) - -if (hashUrl && !isValidAutomergeUrl(hashUrl)) { - mount(DocumentLoadError, { - target, - props: { documentUrl: hashUrl, kind: "invalid" }, - }) -} else { - initSync({ module: Uint8Array.from(atob(wasmBase64), char => char.charCodeAt(0)) }) - - const endpoint = import.meta.env.VITE_SUBDUCTION_ENDPOINT - ?? "wss://subduction.sync.inkandswitch.com" - const repo = new Repo({ - storage: new IndexedDBStorageAdapter("coln-lab", "documents"), - subductionWebsocketEndpoints: [endpoint], - }) - - try { - const handle = hashUrl - ? await repo.find(hashUrl as AutomergeUrl) - : repo.create(newTheoryDocument()) - - if (!isTheoryDocument(handle.doc())) { - await repo.shutdown() - mount(DocumentLoadError, { - target, - props: { documentUrl: hashUrl, kind: "incompatible" }, - }) - } else { - if (!hashUrl) location.hash = handle.url - const flush = () => void repo.flush([handle.documentId]) - addEventListener("pagehide", flush, { once: true }) - - if (import.meta.env.DEV) Object.assign(window, { repo, handle }) - mount(App, { - target, - props: { repo, handle, endpoint }, - }) - } - } catch (cause) { - await repo.shutdown().catch(() => undefined) - if (!isDocumentUnavailable(cause)) throw cause - mount(DocumentLoadError, { - target, - props: { documentUrl: hashUrl, kind: "unavailable" }, - }) - } -} - -function isDocumentUnavailable(cause: unknown): boolean { - return cause instanceof Error && /^Document .+ is unavailable$/.test(cause.message) -} diff --git a/examples/compiler/src/style.css b/examples/compiler/src/style.css deleted file mode 100644 index 231e85d1..00000000 --- a/examples/compiler/src/style.css +++ /dev/null @@ -1,45 +0,0 @@ -/* SPDX-FileCopyrightText: 2026 Coln contributors */ -/* SPDX-License-Identifier: Apache-2.0 OR MIT */ - -@import url("https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;600;700&display=swap"); -@import "tailwindcss"; - -html { - background: #101718; -} - -body { - margin: 0; -} - -.diagnostics pre { - margin: 0; - overflow-x: auto; - white-space: pre-wrap; -} - -.diagnostics > :not(:last-child) { - border-bottom: 1px solid #304041; - padding-bottom: 1rem; -} - -.diag-debug { - --diagnostic-color: #aab6b7; -} - -.diag-info { - --diagnostic-color: #77b7ff; -} - -.diag-warning { - --diagnostic-color: #ffd166; -} - -.diag-error { - --diagnostic-color: #ff9a86; -} - -[class^="ann-"] { - color: var(--diagnostic-color); - font-weight: 500; -} diff --git a/examples/compiler/tsconfig.json b/examples/compiler/tsconfig.json deleted file mode 100644 index 55f0158a..00000000 --- a/examples/compiler/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "useDefineForClassFields": true, - "lib": ["DOM", "DOM.Iterable", "ES2022"], - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "ESNext", - "moduleResolution": "Bundler", - "resolveJsonModule": true, - "allowImportingTsExtensions": true, - "isolatedModules": true, - "noEmit": true, - "types": ["vite/client", "node"] - }, - "include": ["src", "vite.config.ts"] -} diff --git a/examples/compiler/vite.config.ts b/examples/compiler/vite.config.ts deleted file mode 100644 index c110956d..00000000 --- a/examples/compiler/vite.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// -// SPDX-License-Identifier: Apache-2.0 OR MIT - -import { defineConfig } from "vite" -import { svelte } from "@sveltejs/vite-plugin-svelte" -import tailwindcss from "@tailwindcss/vite" -import wasm from "vite-plugin-wasm" - -export default defineConfig({ - base: process.env.VITE_BASE || "/", - plugins: [tailwindcss(), wasm(), svelte()], - build: { - emptyOutDir: true, - outDir: "../../_build/web/compiler-app", - target: "esnext", - }, -}) diff --git a/examples/editor/README.md b/examples/editor/README.md deleted file mode 100644 index a0d82454..00000000 --- a/examples/editor/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Coln Store Lab - -A Svelte 5 workbench for inspecting and programming synchronized Coln stores. - -```console -pnpm install -pnpm dev -``` - -The development command uses the shared managed relay in `examples/sync-server`. -Set `SUBDUCTION_PORT` to choose another local relay port. Production builds use -`VITE_SUBDUCTION_ENDPOINT`, falling back to the public Subduction relay. - -The JavaScript REPL runs trusted code on the main browser thread with the loaded -Coln `handle` in scope. Its outer program may be asynchronous, but callbacks -passed to `handle.change` must remain synchronous. diff --git a/examples/editor/playwright.config.ts b/examples/editor/playwright.config.ts deleted file mode 100644 index 05ac8d38..00000000 --- a/examples/editor/playwright.config.ts +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// SPDX-License-Identifier: Apache-2.0 OR MIT - -import { defineConfig, devices } from "@playwright/test" - -export default defineConfig({ - testDir: "./tests", - timeout: 120_000, - expect: { timeout: 10_000 }, - fullyParallel: false, - retries: 0, - use: { - baseURL: "http://127.0.0.1:5177", - trace: "retain-on-failure", - }, - webServer: [ - { - command: - "SUBDUCTION_SERVICE_NAME=127.0.0.1:3032 PORT=3032 coln-sync-server", - port: 3032, - reuseExistingServer: false, - stdout: "pipe", - stderr: "pipe", - }, - { - command: - "VITE_SUBDUCTION_ENDPOINT=ws://127.0.0.1:3032 ./node_modules/.bin/vite --host 127.0.0.1 --port 5177", - url: "http://127.0.0.1:5177", - reuseExistingServer: false, - stdout: "pipe", - stderr: "pipe", - }, - ], - projects: [ - { - name: "chromium", - use: { ...devices["Desktop Chrome"] }, - }, - ], -}) diff --git a/examples/editor/pnpm-workspace.yaml b/examples/editor/pnpm-workspace.yaml deleted file mode 100644 index 62604741..00000000 --- a/examples/editor/pnpm-workspace.yaml +++ /dev/null @@ -1,7 +0,0 @@ -allowBuilds: - cbor-extract: false - esbuild: true - '@automerge/automerge-repo@git+https://github.com/automerge/automerge-repo.git': true -overrides: - "@automerge/automerge": 3.3.2 - "@automerge/automerge-subduction": 0.16.1 diff --git a/examples/editor/src/browser/StoreLoader.svelte b/examples/editor/src/browser/StoreLoader.svelte deleted file mode 100644 index d70085c6..00000000 --- a/examples/editor/src/browser/StoreLoader.svelte +++ /dev/null @@ -1,56 +0,0 @@ - - - - - -
-
-
- C - COLN / STORE LAB -
-
-
-
{ - event.preventDefault() - onload(documentUrl) - }} - > -
-

01 / OPEN STORE

-

Inspect a synchronized Coln store.

-

Enter an Automerge document URL. Store Lab will resolve its Coln schema, tables, and live rows.

-
-
- - - {#if error} - - {/if} -
-
-
-
diff --git a/examples/editor/src/main.ts b/examples/editor/src/main.ts deleted file mode 100644 index 40c51475..00000000 --- a/examples/editor/src/main.ts +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// SPDX-License-Identifier: Apache-2.0 OR MIT - -// @ts-expect-error initSync is exported at runtime but absent from declarations -import { initSync } from "@automerge/automerge-subduction/slim" -// @ts-expect-error wasm-base64 is exported without declarations -import { wasmBase64 } from "@automerge/automerge-subduction/wasm-base64" -import { mount } from "svelte" -import App from "./browser/App.svelte" -import "./style.css" - -initSync({ - module: Uint8Array.from(atob(wasmBase64), (char) => char.charCodeAt(0)), -}) - -const endpoint = - import.meta.env.VITE_SUBDUCTION_ENDPOINT ?? - "wss://subduction.sync.inkandswitch.com" - -mount(App, { - target: document.getElementById("app")!, - props: { endpoint }, -}) diff --git a/examples/editor/src/style.css b/examples/editor/src/style.css deleted file mode 100644 index b5072711..00000000 --- a/examples/editor/src/style.css +++ /dev/null @@ -1,23 +0,0 @@ -/* SPDX-FileCopyrightText: 2026 Coln contributors */ -/* SPDX-License-Identifier: Apache-2.0 OR MIT */ - -@import url("https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;600;700&display=swap"); -@import "tailwindcss"; - -html { - background: #101718; -} - -body { - margin: 0; -} - -button, -input { - font: inherit; -} - -::selection { - background: #657a32; - color: #e8ece8; -} diff --git a/examples/editor/tests/editor.spec.ts b/examples/editor/tests/editor.spec.ts deleted file mode 100644 index bc9be402..00000000 --- a/examples/editor/tests/editor.spec.ts +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// SPDX-License-Identifier: Apache-2.0 OR MIT - -import { expect, test } from "@playwright/test" -import { Repo, initSubduction } from "@automerge/automerge-repo" -import { - colnDocType, - type ColnHandle, - type ColnSchema, -} from "@coln-project/repo" - -const endpoint = "ws://127.0.0.1:3032" -const schema = { - entities: [ - { - path: [["Lab"], ["V"]], - value: { - entityVariant: { tag: "table" }, - columns: [], - primaryKey: null, - }, - }, - { - path: [["Lab"], ["profile"]], - value: { - entityVariant: { tag: "table" }, - columns: [ - { path: [["owner"]], type: { tag: "rowId", path: [["Lab"], ["V"]] } }, - { path: [["name"]], type: { tag: "builtin", type: "builtinString" } }, - { path: [["rank"]], type: { tag: "builtin", type: "builtinInt" } }, - ], - primaryKey: [[["owner"]]], - }, - }, - ], - rules: [], -} satisfies ColnSchema - -let repo: Repo -let handle: ColnHandle -let secondHandle: ColnHandle - -test.beforeAll(async () => { - await initSubduction() - repo = new Repo({ subductionWebsocketEndpoints: [endpoint] }) - handle = repo.create(schema, colnDocType) - handle.change((transaction) => { - const ada = transaction.add("Lab.V", []) - transaction.add("Lab.V", []) - transaction.add("Lab.profile", [ - ada, - { tag: "string", value: "Ada" }, - { tag: "int", value: 37 }, - ]) - }) - await repo.flush([handle.documentId]) - secondHandle = repo.create(schema, colnDocType) - secondHandle.change((transaction) => { - transaction.add("Lab.V", []) - }) - await repo.flush([secondHandle.documentId]) -}) - -test.afterAll(async () => { - await repo.shutdown() -}) - -test("loads, inspects, and programs a Coln store", async ({ page }) => { - await page.setViewportSize({ width: 390, height: 844 }) - await page.goto("/") - await expect( - page.getByText("Inspect a synchronized Coln store."), - ).toBeVisible() - await page.getByTestId("document-url-input").fill("not-a-document") - await page.getByTestId("open-store").click() - await expect(page.getByTestId("load-error")).toContainText("valid automerge:") - await page.getByTestId("document-url-input").fill(handle.url) - await page.getByTestId("open-store").click() - - await expect(page).toHaveURL(new RegExp(`#${handle.url}$`)) - await expect(page.getByTestId("table-option")).toHaveCount(2) - const tableBox = await page.getByTestId("table-grid").boundingBox() - const replBox = await page.getByTestId("run-program").boundingBox() - expect(tableBox).not.toBeNull() - expect(replBox).not.toBeNull() - expect(replBox!.y).toBeGreaterThan(tableBox!.y) - await page.setViewportSize({ width: 1280, height: 800 }) - - await page - .getByTestId("table-option") - .filter({ hasText: "Lab.profile" }) - .click() - await expect(page.getByTestId("table-row")).toHaveCount(1) - await expect(page.getByTestId("table-grid")).toContainText("Ada") - await expect(page.getByTestId("table-grid")).toContainText("37") - - const program = `console.log("before", handle.doc().scanTable("Lab.V").length) -handle.change(transaction => transaction.add("Lab.V", [])) -return handle.doc().scanTable("Lab.V").length` - const editor = page.locator(".cm-content") - await editor.click() - await page.keyboard.press("ControlOrMeta+A") - await page.keyboard.insertText(program) - await page.getByTestId("run-program").click() - - await expect(page.getByTestId("console-entry")).toContainText("before") - await expect(page.getByTestId("repl-result")).toContainText("3") - await page.getByTestId("table-option").filter({ hasText: "Lab.V" }).click() - await expect(page.getByTestId("table-row")).toHaveCount(3) - await expect.poll(() => handle.doc().scanTable("Lab.V").length).toBe(3) - - handle.change((transaction) => { - transaction.add("Lab.V", []) - }) - await repo.flush([handle.documentId]) - repo.resyncSubduction(handle.documentId) - await expect(page.getByTestId("table-row")).toHaveCount(4) - - await page.reload() - await expect(page.locator(".cm-content")).toContainText("handle.change") - await page.getByRole("button", { name: "Switch" }).click() - await expect(page.getByTestId("document-url-input")).toBeVisible() - await page.getByTestId("document-url-input").fill(secondHandle.url) - await page.getByTestId("open-store").click() - await expect(page.getByTestId("table-option")).toHaveCount(2) - await page.getByTestId("table-option").filter({ hasText: "Lab.V" }).click() - await expect(page.getByTestId("table-row")).toHaveCount(1) - await expect(page.locator(".cm-content")).not.toContainText("handle.change") -}) diff --git a/examples/editor/vite.config.ts b/examples/editor/vite.config.ts deleted file mode 100644 index d11f1c14..00000000 --- a/examples/editor/vite.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// SPDX-License-Identifier: Apache-2.0 OR MIT - -import { svelte } from "@sveltejs/vite-plugin-svelte" -import tailwindcss from "@tailwindcss/vite" -import { defineConfig } from "vite" -import wasm from "vite-plugin-wasm" - -export default defineConfig({ - base: process.env.VITE_BASE || "/", - plugins: [tailwindcss(), wasm(), svelte()], - resolve: { dedupe: ["@coln-project/runtime", "svelte"] }, - build: { - emptyOutDir: true, - outDir: "../../_build/web/editor-app", - target: "esnext", - }, -}) diff --git a/examples/editor/vitest.config.ts b/examples/editor/vitest.config.ts deleted file mode 100644 index 90893bd9..00000000 --- a/examples/editor/vitest.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// SPDX-License-Identifier: Apache-2.0 OR MIT - -import { defineConfig } from "vitest/config" - -export default defineConfig({ - resolve: { conditions: ["browser"] }, - test: { - include: ["tests/**/*.unit.ts"], - }, -}) diff --git a/examples/index.html b/examples/index.html deleted file mode 100644 index 9dee93e3..00000000 --- a/examples/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Coln - - - -
-

Coln demos

- -
- - diff --git a/examples/justfile b/examples/justfile index abf5de31..343c0121 100644 --- a/examples/justfile +++ b/examples/justfile @@ -1,34 +1,21 @@ mod coln-compiler-wasm '../packages/coln-compiler-wasm' build_dir := "../_build/web/compiler" -compiler_app_dir := "compiler" +lab_app_dir := "lab" -build-web-compiler: coln-compiler-wasm::build - #!/usr/bin/env bash - set -euxo pipefail - cp compiler-demo/index.html {{ build_dir }}/index.html - cp style.css {{ build_dir }}/style.css - examples_dir="{{ build_dir }}/examples" - mkdir -p "$examples_dir" - cp ../packages/coln-compiler/test/golden/*.coln "$examples_dir/" - ls -1 "$examples_dir"/*.coln | xargs -n1 basename | jq -nR '[inputs]' > "$examples_dir/index.json" - -serve-web-compiler port="8000": build-web-compiler - simple-http-server --nocache --index --open --port {{ port }} {{ build_dir }} - -stage-web-compiler-app: +stage-web-lab: #!/usr/bin/env bash set -euxo pipefail if [[ ! -f "{{ build_dir }}/dist/coln.wasm" || ! -f "{{ build_dir }}/dist/ghc_wasm_jsffi.js" || ! -f "{{ build_dir }}/dist/loadHaskellWasm.js" ]]; then - just build-web-compiler + just coln-compiler-wasm::build fi - mkdir -p "{{ compiler_app_dir }}/public/dist" - cp "{{ build_dir }}/dist/coln.wasm" "{{ compiler_app_dir }}/public/dist/coln.wasm" - cp "{{ build_dir }}/dist/ghc_wasm_jsffi.js" "{{ compiler_app_dir }}/public/dist/ghc_wasm_jsffi.js" - cp "{{ build_dir }}/dist/loadHaskellWasm.js" "{{ compiler_app_dir }}/public/dist/loadHaskellWasm.js" + mkdir -p "{{ lab_app_dir }}/public/dist" + cp "{{ build_dir }}/dist/coln.wasm" "{{ lab_app_dir }}/public/dist/coln.wasm" + cp "{{ build_dir }}/dist/ghc_wasm_jsffi.js" "{{ lab_app_dir }}/public/dist/ghc_wasm_jsffi.js" + cp "{{ build_dir }}/dist/loadHaskellWasm.js" "{{ lab_app_dir }}/public/dist/loadHaskellWasm.js" -build-web-compiler-app: stage-web-compiler-app - pnpm --dir {{ compiler_app_dir }} build +build-web-lab: stage-web-lab + pnpm --dir {{ lab_app_dir }} build -dev-web-compiler-app: stage-web-compiler-app - pnpm --dir {{ compiler_app_dir }} dev +dev-web-lab: stage-web-lab + pnpm --dir {{ lab_app_dir }} dev diff --git a/examples/lab/.env.example b/examples/lab/.env.example new file mode 100644 index 00000000..1c65153c --- /dev/null +++ b/examples/lab/.env.example @@ -0,0 +1,2 @@ +# Browser-visible configuration for Coln Lab. +VITE_SUBDUCTION_ENDPOINT=ws://127.0.0.1:3030 diff --git a/examples/editor/.gitignore b/examples/lab/.gitignore similarity index 78% rename from examples/editor/.gitignore rename to examples/lab/.gitignore index 45695782..4611f692 100644 --- a/examples/editor/.gitignore +++ b/examples/lab/.gitignore @@ -1,3 +1,4 @@ node_modules +public/dist test-results playwright-report diff --git a/examples/lab/README.md b/examples/lab/README.md new file mode 100644 index 00000000..833ba0a2 --- /dev/null +++ b/examples/lab/README.md @@ -0,0 +1,27 @@ +# Coln Lab + +One local workbench for Coln theories, stores, and synchronized graphs. + +From the repository root: + +```console +just examples/dev-web-lab +``` + +The command stages the browser compiler and starts or reuses Lab's local +Subduction relay. Build-time browser configuration can be placed in +`examples/lab/.env`; exported shell variables take precedence. See +`examples/lab/.env.example` for available settings. + +The Store tool executes trusted JavaScript on the browser main thread. Do not +run programs from untrusted sources. + +Lab owns its complete browser implementation, relay scripts, and graph schema. +Regenerate the checked-in graph bindings after changing `src/tools/graph/graph.coln`: + +```console +pnpm --dir examples/lab graph:generate +``` + +Run unit and browser tests with `pnpm --dir examples/lab test` and +`pnpm --dir examples/lab test:e2e`. diff --git a/examples/editor/index.html b/examples/lab/index.html similarity index 66% rename from examples/editor/index.html rename to examples/lab/index.html index 6f21ce9a..f12fd1eb 100644 --- a/examples/editor/index.html +++ b/examples/lab/index.html @@ -5,12 +5,9 @@ - - - Coln Store Lab + + + Coln Lab
diff --git a/examples/editor/package.json b/examples/lab/package.json similarity index 65% rename from examples/editor/package.json rename to examples/lab/package.json index c5420dff..ad699fcf 100644 --- a/examples/editor/package.json +++ b/examples/lab/package.json @@ -1,34 +1,39 @@ { - "name": "coln-store-lab", + "name": "coln-lab", "private": true, "license": "(Apache-2.0 OR MIT)", "type": "module", "scripts": { - "dev": "coln-with-sync-server vite --host 127.0.0.1", - "server": "coln-sync-server", + "dev": "node scripts/with-server.mjs vite --host 127.0.0.1", + "server": "node scripts/server.mjs", + "graph:generate": "bash src/tools/graph/compile.sh", "check": "svelte-check --tsconfig ./tsconfig.json && tsc --noEmit", "build": "pnpm run check && vite build", "test": "vitest run", - "test:e2e": "playwright test" + "test:e2e": "playwright test", + "test:e2e:dev": "PLAYWRIGHT_DEV=1 playwright test" }, "dependencies": { + "@automerge/automerge-codemirror": "0.2.0", "@automerge/automerge-repo": "github:automerge/automerge-repo#doctypes&path:packages/automerge-repo", "@automerge/automerge-repo-storage-indexeddb": "2.6.0-subduction.47", "@automerge/automerge-subduction": "0.16.1", - "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-javascript": "^6.2.5", "@codemirror/state": "^6.7.2", - "@codemirror/view": "^6.43.10", + "@codemirror/view": "^6.43.11", "@coln-project/repo": "link:../../packages/coln-repo", "@coln-project/runtime": "link:../../packages/coln-js-runtime", "codemirror": "^6.0.2", - "svelte": "^5.57.0" + "json-formatter-js": "^2.5.23", + "svelte": "^5.57.0", + "ws": "^8.21.0" }, "devDependencies": { - "@coln-project/demo-sync-server": "file:../sync-server", "@playwright/test": "^1.62.1", "@sveltejs/vite-plugin-svelte": "^7.3.0", "@tailwindcss/vite": "^4.3.3", "@types/node": "^26.4.1", + "@vitest/browser-playwright": "^4.1.11", "svelte-check": "^4.7.6", "tailwindcss": "^4.3.3", "typescript": "^6.0.3", diff --git a/examples/lab/playwright.config.ts b/examples/lab/playwright.config.ts new file mode 100644 index 00000000..e112c911 --- /dev/null +++ b/examples/lab/playwright.config.ts @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { defineConfig, devices } from "@playwright/test" + +export default defineConfig({ + testDir: "./tests", + timeout: 360_000, + expect: { timeout: 60_000 }, + fullyParallel: false, + retries: 0, + use: { + baseURL: "http://127.0.0.1:5180", + trace: "retain-on-failure", + }, + webServer: [ + { + command: + "SUBDUCTION_SERVICE_NAME=127.0.0.1:3033 PORT=3033 node scripts/server.mjs", + port: 3033, + reuseExistingServer: false, + stdout: "pipe", + stderr: "pipe", + }, + { + command: process.env.PLAYWRIGHT_DEV + ? "VITE_SUBDUCTION_ENDPOINT=ws://127.0.0.1:3033 ./node_modules/.bin/vite --host 127.0.0.1 --port 5180 --force" + : "VITE_SUBDUCTION_ENDPOINT=ws://127.0.0.1:3033 ./node_modules/.bin/vite build && ./node_modules/.bin/vite preview --host 127.0.0.1 --port 5180", + url: "http://127.0.0.1:5180", + timeout: 300_000, + reuseExistingServer: false, + stdout: "pipe", + stderr: "pipe", + }, + ], + projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], +}) diff --git a/examples/editor/pnpm-lock.yaml b/examples/lab/pnpm-lock.yaml similarity index 97% rename from examples/editor/pnpm-lock.yaml rename to examples/lab/pnpm-lock.yaml index 3768fbd0..f788186c 100644 --- a/examples/editor/pnpm-lock.yaml +++ b/examples/lab/pnpm-lock.yaml @@ -12,6 +12,9 @@ importers: .: dependencies: + '@automerge/automerge-codemirror': + specifier: 0.2.0 + version: 0.2.0 '@automerge/automerge-repo': specifier: github:automerge/automerge-repo#doctypes&path:packages/automerge-repo version: https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo @@ -22,13 +25,13 @@ importers: specifier: 0.16.1 version: 0.16.1 '@codemirror/lang-javascript': - specifier: ^6.2.4 + specifier: ^6.2.5 version: 6.2.5 '@codemirror/state': specifier: ^6.7.2 version: 6.7.2 '@codemirror/view': - specifier: ^6.43.10 + specifier: ^6.43.11 version: 6.43.11 '@coln-project/repo': specifier: link:../../packages/coln-repo @@ -39,13 +42,16 @@ importers: codemirror: specifier: ^6.0.2 version: 6.0.2 + json-formatter-js: + specifier: ^2.5.23 + version: 2.5.23 svelte: specifier: ^5.57.0 version: 5.57.0 + ws: + specifier: ^8.21.0 + version: 8.21.3 devDependencies: - '@coln-project/demo-sync-server': - specifier: file:../sync-server - version: file:../sync-server '@playwright/test': specifier: ^1.62.1 version: 1.62.1 @@ -58,6 +64,9 @@ importers: '@types/node': specifier: ^26.4.1 version: 26.4.1 + '@vitest/browser-playwright': + specifier: ^4.1.11 + version: 4.1.11(playwright@1.62.1)(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))(vitest@4.1.11) svelte-check: specifier: ^4.7.6 version: 4.7.6(picomatch@4.0.7)(svelte@5.57.0)(typescript@6.0.3) @@ -79,6 +88,9 @@ importers: packages: + '@automerge/automerge-codemirror@0.2.0': + resolution: {integrity: sha512-GBNh4ESZHFNHZKLznf/2G2KdmBD+unEdk64+l621LsjIsJs6HBmQmO8RCY1v7vIq31kFyoVHNcBMGQOnLgy4GA==} + '@automerge/automerge-repo-storage-indexeddb@2.6.0-subduction.47': resolution: {integrity: sha512-fYn6nse7jQnVb9EploTsBuYUzW7xFin2HZLneSEQ7w4pRCilIU92ghZEjZSWRKEPunSN0MYEvfJJJdlLyYTdkQ==} engines: {node: '>=22.13'} @@ -155,10 +167,6 @@ packages: '@codemirror/view@6.43.11': resolution: {integrity: sha512-2+esucbQX6wB2JYi1eDvdCPFTA31BN8oSy6xCmk3G6CloV11yOvEjYk+gH7kLrP0MuHG94E8WDhjs5oMiu3+Wg==} - '@coln-project/demo-sync-server@file:../sync-server': - resolution: {directory: ../sync-server, type: directory} - hasBin: true - '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -608,6 +616,9 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + json-formatter-js@2.5.23: + resolution: {integrity: sha512-Cbm8wHXjo/C56aCePP1VuKvjxoMEmL7g7Ckss1oWFFlCsvOEEbye1kTeaNNaqba1Cl6YpIOYAnK65pUQ8mDIUQ==} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -813,8 +824,8 @@ packages: resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} engines: {node: '>=14.19.0'} - postcss@8.5.27: - resolution: {integrity: sha512-79Iho8QeYyooJ8e9lCRyTVlyTAkS/kXBYKff6TMzS3kEWGQ8Ds5UEtXpGrSUDLUWok6QTvxeYy0GO8fopHnaSA==} + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} engines: {node: ^10 || ^12 || >=14} readdirp@4.1.2: @@ -1025,6 +1036,13 @@ packages: snapshots: + '@automerge/automerge-codemirror@0.2.0': + dependencies: + '@automerge/automerge': 3.3.2 + '@codemirror/state': 6.7.2 + '@codemirror/view': 6.43.11 + codemirror: 6.0.2 + '@automerge/automerge-repo-storage-indexeddb@2.6.0-subduction.47': dependencies: '@automerge/automerge-repo': 2.6.0-subduction.47 @@ -1073,8 +1091,7 @@ snapshots: '@automerge/automerge@3.3.2': {} - '@blazediff/core@1.9.1': - optional: true + '@blazediff/core@1.9.1': {} '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': optional: true @@ -1150,14 +1167,6 @@ snapshots: style-mod: 4.1.3 w3c-keyname: 2.2.8 - '@coln-project/demo-sync-server@file:../sync-server': - dependencies: - '@automerge/automerge-subduction': 0.16.1 - ws: 8.21.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.6.0 @@ -1203,8 +1212,7 @@ snapshots: dependencies: playwright: 1.62.1 - '@polka/url@1.0.0-next.29': - optional: true + '@polka/url@1.0.0-next.29': {} '@rolldown/binding-android-arm-eabi@1.2.7': optional: true @@ -1363,7 +1371,6 @@ snapshots: - msw - utf-8-validate - vite - optional: true '@vitest/browser@4.1.11(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))(vitest@4.1.11)': dependencies: @@ -1381,7 +1388,6 @@ snapshots: - msw - utf-8-validate - vite - optional: true '@vitest/expect@4.1.11': dependencies: @@ -1536,6 +1542,8 @@ snapshots: jiti@2.7.0: {} + json-formatter-js@2.5.23: {} + lightningcss-android-arm64@1.32.0: optional: true @@ -1646,8 +1654,7 @@ snapshots: mri@1.2.0: {} - mrmime@2.0.1: - optional: true + mrmime@2.0.1: {} ms@2.1.3: {} @@ -1674,10 +1681,9 @@ snapshots: optionalDependencies: fsevents: 2.3.2 - pngjs@7.0.0: - optional: true + pngjs@7.0.0: {} - postcss@8.5.27: + postcss@8.5.28: dependencies: nanoid: 3.3.18 picocolors: 1.1.1 @@ -1717,7 +1723,6 @@ snapshots: '@polka/url': 1.0.0-next.29 mrmime: 2.0.1 totalist: 3.0.1 - optional: true source-map-js@1.2.1: {} @@ -1775,8 +1780,7 @@ snapshots: tinyrainbow@3.1.1: {} - totalist@3.0.1: - optional: true + totalist@3.0.1: {} typescript@6.0.3: {} @@ -1792,7 +1796,7 @@ snapshots: dependencies: lightningcss: 1.33.0 picomatch: 4.0.7 - postcss: 8.5.27 + postcss: 8.5.28 rolldown: 1.2.7 tinyglobby: 0.2.17 optionalDependencies: diff --git a/examples/compiler/pnpm-workspace.yaml b/examples/lab/pnpm-workspace.yaml similarity index 61% rename from examples/compiler/pnpm-workspace.yaml rename to examples/lab/pnpm-workspace.yaml index c5a59d06..c1673c4c 100644 --- a/examples/compiler/pnpm-workspace.yaml +++ b/examples/lab/pnpm-workspace.yaml @@ -1,6 +1,7 @@ allowBuilds: cbor-extract: false esbuild: true + "@automerge/automerge-repo@git+https://github.com/automerge/automerge-repo.git": true overrides: "@automerge/automerge": 3.3.2 "@automerge/automerge-subduction": 0.16.1 diff --git a/examples/sync-server/server.mjs b/examples/lab/scripts/server.mjs old mode 100755 new mode 100644 similarity index 100% rename from examples/sync-server/server.mjs rename to examples/lab/scripts/server.mjs diff --git a/examples/sync-server/with-server.mjs b/examples/lab/scripts/with-server.mjs old mode 100755 new mode 100644 similarity index 100% rename from examples/sync-server/with-server.mjs rename to examples/lab/scripts/with-server.mjs diff --git a/examples/lab/src/app/Home.svelte b/examples/lab/src/app/Home.svelte new file mode 100644 index 00000000..025d2b4c --- /dev/null +++ b/examples/lab/src/app/Home.svelte @@ -0,0 +1,48 @@ + + + + + +
+
+
+
+

Ledger 000 / Counting room

+

A workbench for things that follow.

+

Coln takes its name from the pebble: a small, durable mark for counting. Use the ledger below to define theories, inspect stores, and test relations.

+
+ +
+ + +
+
diff --git a/examples/lab/src/app/LabApp.svelte b/examples/lab/src/app/LabApp.svelte new file mode 100644 index 00000000..77ac7356 --- /dev/null +++ b/examples/lab/src/app/LabApp.svelte @@ -0,0 +1,149 @@ + + + + + +Coln Lab + +
+
+ router.follow(event, "home")}> + + + + +
+ +
+ {#if route.tool === "home"} + + {:else if route.tool === "compiler"} + {#await loadCompilerTool()} +
OPENING THEORY INSTRUMENT
+ {:then CompilerTool} + {#key `compiler:${route.documentUrl}`} + + {/key} + {/await} + {:else if route.tool === "editor"} + {#await loadEditorTool()} +
OPENING STORE INSTRUMENT
+ {:then EditorTool} + {#key `editor:${route.documentUrl}`} + + {/key} + {/await} + {:else if route.tool === "sync"} + {#await loadGraphTool()} +
OPENING GRAPH INSTRUMENT
+ {:then GraphTool} + {#key `sync:${route.documentUrl}`} + + {/key} + {/await} + {:else} +

ENTRY NOT FOUND

No instrument at this address.

router.follow(event, "home")}>Return to the ledger
+ {/if} +
+
diff --git a/examples/editor/src/lib/subduction-sync.svelte.ts b/examples/lab/src/lib/document-sync.svelte.ts similarity index 89% rename from examples/editor/src/lib/subduction-sync.svelte.ts rename to examples/lab/src/lib/document-sync.svelte.ts index 437674f0..0c27d452 100644 --- a/examples/editor/src/lib/subduction-sync.svelte.ts +++ b/examples/lab/src/lib/document-sync.svelte.ts @@ -1,20 +1,25 @@ // SPDX-FileCopyrightText: 2026 Coln contributors // SPDX-License-Identifier: Apache-2.0 OR MIT -import type { Repo } from "@automerge/automerge-repo" -import type { ColnHandle } from "@coln-project/repo" +import type { DocumentId, Repo } from "@automerge/automerge-repo" import { createSubscriber } from "svelte/reactivity" +type FlushableHandle = { + documentId: DocumentId + on(event: "change", listener: () => void): void + off(event: "change", listener: () => void): void +} + export type SyncStatus = "offline" | "syncing" | "synced" | "error" -export class SubductionSync { +export class DocumentSync { readonly #subscribe: () => void #status: SyncStatus #error: unknown constructor( private readonly repo: Repo, - private readonly handle: ColnHandle, + private readonly handle: FlushableHandle, ) { this.#status = repo.isSubductionConnected() ? "syncing" : "offline" this.#subscribe = createSubscriber((update) => { diff --git a/examples/lab/src/lib/router.svelte.ts b/examples/lab/src/lib/router.svelte.ts new file mode 100644 index 00000000..26b33a67 --- /dev/null +++ b/examples/lab/src/lib/router.svelte.ts @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { createSubscriber } from "svelte/reactivity" + +export type Tool = "home" | "compiler" | "editor" | "sync" | "not-found" + +export interface Route { + tool: Tool + documentUrl: string + theoryUrl: string + pathname: string +} + +const paths: Record, string> = { + home: "", + compiler: "compiler/", + editor: "editor/", + sync: "sync/", +} + +export class Router { + readonly #subscribe: () => void + + constructor() { + this.#subscribe = createSubscriber((update) => { + addEventListener("popstate", update) + addEventListener("hashchange", update) + return () => { + removeEventListener("popstate", update) + removeEventListener("hashchange", update) + } + }) + } + + get current(): Route { + this.#subscribe() + return routeFromUrl(new URL(location.href), import.meta.env.BASE_URL) + } + + href( + tool: Exclude, + documentUrl = "", + theoryUrl = "", + ): string { + const url = new URL(paths[tool], appBaseUrl()) + if (theoryUrl) url.searchParams.set("theory", theoryUrl) + url.hash = documentUrl + return `${url.pathname}${url.search}${url.hash}` + } + + navigate( + tool: Exclude, + documentUrl = "", + theoryUrl = "", + ): void { + history.pushState(null, "", this.href(tool, documentUrl, theoryUrl)) + dispatchEvent(new PopStateEvent("popstate")) + } + + follow( + event: MouseEvent, + tool: Exclude, + documentUrl = "", + theoryUrl = "", + ): void { + if ( + event.defaultPrevented || + event.button !== 0 || + event.metaKey || + event.ctrlKey || + event.shiftKey || + event.altKey + ) + return + event.preventDefault() + this.navigate(tool, documentUrl, theoryUrl) + } + + replace( + tool: Exclude, + documentUrl = "", + theoryUrl = "", + ): void { + history.replaceState(null, "", this.href(tool, documentUrl, theoryUrl)) + dispatchEvent(new PopStateEvent("popstate")) + } +} + +export function readRoute(): Route { + return routeFromUrl(new URL(location.href), import.meta.env.BASE_URL) +} + +export function routeFromUrl(url: URL, basePath = "/"): Route { + const base = new URL(basePath, url.origin).pathname + const relativePath = url.pathname.startsWith(base) + ? url.pathname.slice(base.length) + : url.pathname.slice(1) + const segments = relativePath + .replace(/^\/+|\/+$/g, "") + .split("/") + .filter(Boolean) + const segment = segments[0] ?? "" + const tool: Tool = + segments.length > 1 + ? "not-found" + : segment === "" + ? "home" + : segment === "compiler" || segment === "editor" || segment === "sync" + ? segment + : "not-found" + const theoryUrl = url.searchParams.get("theory") ?? "" + return { + tool, + documentUrl: url.hash.slice(1), + theoryUrl, + pathname: url.pathname, + } +} + +function appBaseUrl(): URL { + return new URL(import.meta.env.BASE_URL, location.origin) +} diff --git a/examples/lab/src/main.ts b/examples/lab/src/main.ts new file mode 100644 index 00000000..cd00f59f --- /dev/null +++ b/examples/lab/src/main.ts @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { Repo, type DocumentId } from "@automerge/automerge-repo" +import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb" +// @ts-expect-error initSync is exported at runtime but absent from declarations +import { initSync } from "@automerge/automerge-subduction/slim" +// @ts-expect-error wasm-base64 is exported without declarations +import { wasmBase64 } from "@automerge/automerge-subduction/wasm-base64" +import { mount } from "svelte" +import LabApp from "./app/LabApp.svelte" +import "./style.css" + +initSync({ + module: Uint8Array.from(atob(wasmBase64), (char) => char.charCodeAt(0)), +}) + +const endpoint = + import.meta.env.VITE_SUBDUCTION_ENDPOINT ?? + "wss://coln.sync.inkandswitch.com" +const repo = new Repo({ + storage: new IndexedDBStorageAdapter("coln-lab", "documents"), + subductionWebsocketEndpoints: [endpoint], +}) +const openedDocuments = new Set() +const trackDocument = (documentId: string) => + openedDocuments.add(documentId as DocumentId) +const flush = () => { + if (openedDocuments.size > 0) void repo.flush([...openedDocuments]) +} + +addEventListener("pagehide", flush) +mount(LabApp, { + target: document.getElementById("app")!, + props: { repo, endpoint, trackDocument }, +}) + +if (import.meta.hot) { + import.meta.hot.dispose(() => { + removeEventListener("pagehide", flush) + void repo.shutdown() + }) +} diff --git a/examples/lab/src/style.css b/examples/lab/src/style.css new file mode 100644 index 00000000..0f5fc0db --- /dev/null +++ b/examples/lab/src/style.css @@ -0,0 +1,27 @@ +/* SPDX-FileCopyrightText: 2026 Coln contributors */ +/* SPDX-License-Identifier: Apache-2.0 OR MIT */ + +@import url("https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;600;700&display=swap"); +@import "tailwindcss"; + +:root { color-scheme: dark; font-variant-numeric: tabular-nums; } +html { background: #0d1211; } +body { margin: 0; } +button, input, select { font: inherit; } +::selection { background: #657a32; color: #f0f1e8; } + +.ledger-field { + background-color: #0f1513; + background-image: + linear-gradient(#26312d66 1px, transparent 1px), + linear-gradient(90deg, #26312d33 1px, transparent 1px); + background-size: 100% 32px, 32px 100%; +} + +.diagnostics pre { margin: 0; overflow-x: auto; white-space: pre-wrap; } +.diagnostics > :not(:last-child) { border-bottom: 1px solid #304041; padding-bottom: 1rem; } +.diag-debug { --diagnostic-color: #aab6b7; } +.diag-info { --diagnostic-color: #77b7ff; } +.diag-warning { --diagnostic-color: #ffd166; } +.diag-error { --diagnostic-color: #ff9a86; } +[class^="ann-"] { color: var(--diagnostic-color); font-weight: 500; } diff --git a/examples/lab/src/tools/compiler/CompilerTool.svelte b/examples/lab/src/tools/compiler/CompilerTool.svelte new file mode 100644 index 00000000..6fa14371 --- /dev/null +++ b/examples/lab/src/tools/compiler/CompilerTool.svelte @@ -0,0 +1,336 @@ + + + + + +{#if loadError} +
+
+
+

{errorCopy[loadError][0]}

+

{errorCopy[loadError][1]}

+

{errorCopy[loadError][2]}

+
+ {#if documentUrl}{documentUrl}{/if} + +
+
+{:else if handle && theoryHandle} + {@const currentTheoryUrl = handle.url} +
+ {#if feedback}

{feedback}

{/if} +
+ +
+ +
+
{statusLabel}
+
{syncStatus}/ {sourceLines} {sourceLines === 1 ? "line" : "lines"}
+
+
+
+
+

SOURCE

Collaborative Automerge document

Live theory
+
+ {#if error}{/if} + {#if syncStatus === "error"}{/if} + {#if compilation.diagnosticsHtml.length === 0}

No diagnostics

{:else}
{#each compilation.diagnosticsHtml as diagnostic}
{@html diagnostic}
{/each}
{/if}
+ {#if compilation.prettyIr.length === 0}

No intermediate representation

{:else}
{#each compilation.prettyIr as realm}
{realm}
{/each}
{/if}
+ {#if compilation.irJson === ""}

No JSON output

{:else}{/if}
+
+ (selectedRealmName = name)} oncreate={createSelectedStore} oncopy={(url) => void copyText(url, "Store URL copied")} openhref={(url) => router.href("editor", url, currentTheoryUrl)} onopen={(event, url) => router.follow(event, "editor", url, currentTheoryUrl)} /> +
+
+{:else} +

LOADING COMPILER

+{/if} diff --git a/examples/compiler/src/browser/JsonViewer.svelte b/examples/lab/src/tools/compiler/JsonViewer.svelte similarity index 100% rename from examples/compiler/src/browser/JsonViewer.svelte rename to examples/lab/src/tools/compiler/JsonViewer.svelte diff --git a/examples/compiler/src/browser/OutputPanel.svelte b/examples/lab/src/tools/compiler/OutputPanel.svelte similarity index 100% rename from examples/compiler/src/browser/OutputPanel.svelte rename to examples/lab/src/tools/compiler/OutputPanel.svelte diff --git a/examples/compiler/src/browser/SourceEditor.svelte b/examples/lab/src/tools/compiler/SourceEditor.svelte similarity index 98% rename from examples/compiler/src/browser/SourceEditor.svelte rename to examples/lab/src/tools/compiler/SourceEditor.svelte index 2a011ced..9cc21a08 100644 --- a/examples/compiler/src/browser/SourceEditor.svelte +++ b/examples/lab/src/tools/compiler/SourceEditor.svelte @@ -6,7 +6,7 @@ import { basicSetup } from "codemirror" import { EditorView, placeholder } from "@codemirror/view" import { onMount } from "svelte" - import type { TheoryDocumentHandle } from "../lib/theory-document.ts" + import type { TheoryDocumentHandle } from "./theory-document.ts" let { handle }: { handle: TheoryDocumentHandle } = $props() diff --git a/examples/compiler/src/browser/StoresPanel.svelte b/examples/lab/src/tools/compiler/StoresPanel.svelte similarity index 82% rename from examples/compiler/src/browser/StoresPanel.svelte rename to examples/lab/src/tools/compiler/StoresPanel.svelte index fd537289..918ea998 100644 --- a/examples/compiler/src/browser/StoresPanel.svelte +++ b/examples/lab/src/tools/compiler/StoresPanel.svelte @@ -2,8 +2,8 @@ - - Coln Store Lab - - {#if !store} - + {:else} -
+
{#if actionError} - + {/if} +
- @@ -214,12 +199,15 @@
{store.handle.url}
+ {#if theoryHref} + router.follow(event, "compiler", theoryUrl)}>Return to Theory + {/if} - +
-
+
(selectedTableName = name)} />
diff --git a/examples/editor/src/browser/ReplEditor.svelte b/examples/lab/src/tools/editor/ReplEditor.svelte similarity index 95% rename from examples/editor/src/browser/ReplEditor.svelte rename to examples/lab/src/tools/editor/ReplEditor.svelte index baf9dc34..811cd14b 100644 --- a/examples/editor/src/browser/ReplEditor.svelte +++ b/examples/lab/src/tools/editor/ReplEditor.svelte @@ -58,11 +58,7 @@ { dark: true }, ) - $effect(() => { - if (!view || view.state.doc.toString() === value) return - view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: value } }) - }) - + // CodeMirror owns an imperative editable facet, so prop changes must reconfigure it. $effect(() => { view?.dispatch({ effects: editable.reconfigure(EditorView.editable.of(!disabled)) }) }) diff --git a/examples/editor/src/browser/ReplOutput.svelte b/examples/lab/src/tools/editor/ReplOutput.svelte similarity index 76% rename from examples/editor/src/browser/ReplOutput.svelte rename to examples/lab/src/tools/editor/ReplOutput.svelte index ad7dd3d9..8d1f2aba 100644 --- a/examples/editor/src/browser/ReplOutput.svelte +++ b/examples/lab/src/tools/editor/ReplOutput.svelte @@ -2,13 +2,10 @@
@@ -18,7 +15,11 @@ {#each evaluation.console as entry}
{entry.level} -
{entry.values.map(render).join(" ")}
+
+ {#each entry.values as value} + + {/each} +
{/each}
@@ -26,7 +27,9 @@ {evaluation.ok ? "Result" : `${evaluation.phase} error`} {evaluation.durationMs.toFixed(1)} ms
-
{render(evaluation.ok ? evaluation.result : evaluation.error)}
+
+ +
{/if}
diff --git a/examples/lab/src/tools/editor/StoreLoader.svelte b/examples/lab/src/tools/editor/StoreLoader.svelte new file mode 100644 index 00000000..f5a2341d --- /dev/null +++ b/examples/lab/src/tools/editor/StoreLoader.svelte @@ -0,0 +1,51 @@ + + + + + +
+
{ + event.preventDefault() + onload(documentUrl) + }} + > +
+

01 / OPEN STORE

+

Inspect a synchronized Coln store.

+

Enter an Automerge document URL. Store Lab will resolve its Coln schema, tables, and live rows.

+
+
+ + + {#if error} + + {/if} +
+
+
diff --git a/examples/editor/src/browser/TableBrowser.svelte b/examples/lab/src/tools/editor/TableBrowser.svelte similarity index 97% rename from examples/editor/src/browser/TableBrowser.svelte rename to examples/lab/src/tools/editor/TableBrowser.svelte index 9bf768e7..08defd13 100644 --- a/examples/editor/src/browser/TableBrowser.svelte +++ b/examples/lab/src/tools/editor/TableBrowser.svelte @@ -3,8 +3,8 @@ + +
+
+ {#if structured} + + + {/if} + +
+ + {#if structured} +
+ {:else} + {primitive} + {/if} +
+ + diff --git a/examples/editor/src/lib/coln-handle.svelte.ts b/examples/lab/src/tools/editor/coln-handle.svelte.ts similarity index 100% rename from examples/editor/src/lib/coln-handle.svelte.ts rename to examples/lab/src/tools/editor/coln-handle.svelte.ts diff --git a/examples/editor/src/lib/evaluate.ts b/examples/lab/src/tools/editor/evaluate.ts similarity index 100% rename from examples/editor/src/lib/evaluate.ts rename to examples/lab/src/tools/editor/evaluate.ts diff --git a/examples/editor/src/lib/format.ts b/examples/lab/src/tools/editor/format.ts similarity index 100% rename from examples/editor/src/lib/format.ts rename to examples/lab/src/tools/editor/format.ts diff --git a/examples/editor/src/lib/schema.ts b/examples/lab/src/tools/editor/schema.ts similarity index 100% rename from examples/editor/src/lib/schema.ts rename to examples/lab/src/tools/editor/schema.ts diff --git a/examples/editor/src/lib/snapshot.ts b/examples/lab/src/tools/editor/snapshot.ts similarity index 100% rename from examples/editor/src/lib/snapshot.ts rename to examples/lab/src/tools/editor/snapshot.ts diff --git a/examples/editor/src/lib/source-storage.ts b/examples/lab/src/tools/editor/source-storage.ts similarity index 100% rename from examples/editor/src/lib/source-storage.ts rename to examples/lab/src/tools/editor/source-storage.ts diff --git a/examples/sync/src/browser/GraphCanvas.svelte b/examples/lab/src/tools/graph/GraphCanvas.svelte similarity index 99% rename from examples/sync/src/browser/GraphCanvas.svelte rename to examples/lab/src/tools/graph/GraphCanvas.svelte index bc9b25aa..74eab6cd 100644 --- a/examples/sync/src/browser/GraphCanvas.svelte +++ b/examples/lab/src/tools/graph/GraphCanvas.svelte @@ -2,7 +2,7 @@ + +{#if loadError} +
+
+
+

{errorCopy[loadError].label}

+

{errorCopy[loadError].heading}

+

{errorCopy[loadError].detail}

+
+ {#if documentUrl} + {documentUrl} + {/if} + +
+
+{:else if handle} + {@const currentGraphUrl = handle.url} +
+ {#if feedback} + {#key feedback.id} +

+ {feedback.message} +

+ {/key} + {/if} + +
+ +
+ router.follow(event, "editor", currentGraphUrl)}>Open in Store +
+
+ {syncStatus === "error" ? "sync error" : syncStatus} +
+
+ {graph.heads.length} {graph.heads.length === 1 ? "head" : "heads"} + +
+
+
+
+ +
+ selectedEdgeId = id} + onselectvertex={selectVertex} + /> + fromId = id} + ontochange={id => toId = id} + /> +
+
+{:else} +
+

LOADING GRAPH

+
+{/if} diff --git a/examples/sync/src/lib/coln-handle.svelte.ts b/examples/lab/src/tools/graph/coln-handle.svelte.ts similarity index 100% rename from examples/sync/src/lib/coln-handle.svelte.ts rename to examples/lab/src/tools/graph/coln-handle.svelte.ts diff --git a/examples/sync/compile.sh b/examples/lab/src/tools/graph/compile.sh similarity index 55% rename from examples/sync/compile.sh rename to examples/lab/src/tools/graph/compile.sh index 42c305c6..3ad2e163 100644 --- a/examples/sync/compile.sh +++ b/examples/lab/src/tools/graph/compile.sh @@ -4,6 +4,8 @@ set -euo pipefail +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + if command -v coln >/dev/null 2>&1; then coln_cli=(coln) elif command -v coln-cli >/dev/null 2>&1; then @@ -12,5 +14,5 @@ else coln_cli=(cabal run coln-cli --) fi -"${coln_cli[@]}" generate-ts ./graph.coln --output-dir ./src/generated -"${coln_cli[@]}" generate-ir ./graph.coln --output-dir ./src/generated +"${coln_cli[@]}" generate-ts "$script_dir/graph.coln" --output-dir "$script_dir/generated" +"${coln_cli[@]}" generate-ir "$script_dir/graph.coln" --output-dir "$script_dir/generated" diff --git a/examples/sync/src/generated/Graph.ts b/examples/lab/src/tools/graph/generated/Graph.ts similarity index 100% rename from examples/sync/src/generated/Graph.ts rename to examples/lab/src/tools/graph/generated/Graph.ts diff --git a/examples/sync/src/generated/GraphRealm.json b/examples/lab/src/tools/graph/generated/GraphRealm.json similarity index 100% rename from examples/sync/src/generated/GraphRealm.json rename to examples/lab/src/tools/graph/generated/GraphRealm.json diff --git a/examples/sync/src/generated/GraphRealm.pretty b/examples/lab/src/tools/graph/generated/GraphRealm.pretty similarity index 100% rename from examples/sync/src/generated/GraphRealm.pretty rename to examples/lab/src/tools/graph/generated/GraphRealm.pretty diff --git a/examples/sync/src/generated/GraphRealm.ts b/examples/lab/src/tools/graph/generated/GraphRealm.ts similarity index 100% rename from examples/sync/src/generated/GraphRealm.ts rename to examples/lab/src/tools/graph/generated/GraphRealm.ts diff --git a/examples/sync-demo/graph.coln b/examples/lab/src/tools/graph/graph.coln similarity index 100% rename from examples/sync-demo/graph.coln rename to examples/lab/src/tools/graph/graph.coln diff --git a/examples/sync/src/lib/graph.ts b/examples/lab/src/tools/graph/graph.ts similarity index 98% rename from examples/sync/src/lib/graph.ts rename to examples/lab/src/tools/graph/graph.ts index 8a3ad270..e84f5df4 100644 --- a/examples/sync/src/lib/graph.ts +++ b/examples/lab/src/tools/graph/graph.ts @@ -4,7 +4,7 @@ import type { ColnDocument, ColnHandle } from "@coln-project/repo" import type { RowRef, RowView, Value } from "@coln-project/runtime" -import * as GraphRealm from "../generated/GraphRealm.ts" +import * as GraphRealm from "./generated/GraphRealm.ts" export type GraphHandle = ColnHandle type GraphDocument = ColnDocument diff --git a/examples/sync/tests/coln-handle.unit.ts b/examples/lab/tests/coln-handle.unit.ts similarity index 94% rename from examples/sync/tests/coln-handle.unit.ts rename to examples/lab/tests/coln-handle.unit.ts index 66a7aeae..a58e734f 100644 --- a/examples/sync/tests/coln-handle.unit.ts +++ b/examples/lab/tests/coln-handle.unit.ts @@ -5,8 +5,8 @@ import { flushSync } from "svelte" import { describe, expect, test, vi } from "vitest" import type { ColnChange, ColnDocument, ColnHandle as RepoColnHandle } from "@coln-project/repo" -import { ColnHandle } from "../src/lib/coln-handle.svelte.ts" -import * as GraphRealm from "../src/generated/GraphRealm.ts" +import { ColnHandle } from "../src/tools/graph/coln-handle.svelte.ts" +import * as GraphRealm from "../src/tools/graph/generated/GraphRealm.ts" import { observe } from "./observe.svelte.ts" type GraphHandle = RepoColnHandle diff --git a/examples/sync/tests/subduction-sync.unit.ts b/examples/lab/tests/document-sync.unit.ts similarity index 68% rename from examples/sync/tests/subduction-sync.unit.ts rename to examples/lab/tests/document-sync.unit.ts index 52c9ba33..cadfe4da 100644 --- a/examples/sync/tests/subduction-sync.unit.ts +++ b/examples/lab/tests/document-sync.unit.ts @@ -1,26 +1,22 @@ // SPDX-FileCopyrightText: 2026 Coln contributors -// // SPDX-License-Identifier: Apache-2.0 OR MIT import type { Repo } from "@automerge/automerge-repo" -import type { ColnHandle } from "@coln-project/repo" import { flushSync } from "svelte" import { describe, expect, test } from "vitest" -import * as GraphRealm from "../src/generated/GraphRealm.ts" -import { SubductionSync, type SubductionSyncStatus } from "../src/lib/subduction-sync.svelte.ts" +import { + DocumentSync, + type SyncStatus, +} from "../src/lib/document-sync.svelte.ts" import { observe } from "./observe.svelte.ts" -type GraphHandle = ColnHandle - -describe("SubductionSync", () => { +describe("DocumentSync", () => { test("moves from offline through syncing to synced and cleans up", async () => { const repo = new FakeRepo(false) const handle = new FakeHandle() - const sync = new SubductionSync(repo as unknown as Repo, handle as unknown as GraphHandle) - let status: SubductionSyncStatus | undefined - const stop = observe(() => sync.status, value => { - status = value - }) + const sync = new DocumentSync(repo as unknown as Repo, handle) + let status: SyncStatus | undefined + const stop = observe(() => sync.status, value => (status = value)) flushSync() expect(status).toBe("offline") @@ -31,9 +27,7 @@ describe("SubductionSync", () => { repo.connect() flushSync() expect(status).toBe("syncing") - expect(repo.flushes).toHaveLength(1) expect(repo.flushedDocuments[0]).toEqual([handle.documentId]) - await repo.resolveFlush(0) flushSync() expect(status).toBe("synced") @@ -44,45 +38,34 @@ describe("SubductionSync", () => { expect(handle.listenerCount()).toBe(0) }) - test("does not report synced while a newer change is pending", async () => { + test("flushes a newer change before reporting synced", async () => { const repo = new FakeRepo(true) const handle = new FakeHandle() - const sync = new SubductionSync(repo as unknown as Repo, handle as unknown as GraphHandle) - let status: SubductionSyncStatus | undefined - const stop = observe(() => sync.status, value => { - status = value - }) - flushSync() - - await repo.resolveFlush(0) + const sync = new DocumentSync(repo as unknown as Repo, handle) + let status: SyncStatus | undefined + const stop = observe(() => sync.status, value => (status = value)) flushSync() - expect(status).toBe("synced") handle.emitChange() + await repo.resolveFlush(0) flushSync() expect(status).toBe("syncing") expect(repo.flushes).toHaveLength(2) - - handle.emitChange() await repo.resolveFlush(1) flushSync() - expect(status).toBe("syncing") - expect(repo.flushes).toHaveLength(3) - - await repo.resolveFlush(2) - flushSync() expect(status).toBe("synced") stop() }) - test("reports flush failures and retries after another change", async () => { + test("reports failures and retries after another change", async () => { const repo = new FakeRepo(true) const handle = new FakeHandle() - const sync = new SubductionSync(repo as unknown as Repo, handle as unknown as GraphHandle) - let observed: { status: SubductionSyncStatus; error: unknown } | undefined - const stop = observe(() => ({ status: sync.status, error: sync.error }), value => { - observed = value - }) + const sync = new DocumentSync(repo as unknown as Repo, handle) + let observed: { status: SyncStatus; error: unknown } | undefined + const stop = observe( + () => ({ status: sync.status, error: sync.error }), + value => (observed = value), + ) flushSync() const failure = new Error("save failed") @@ -92,7 +75,6 @@ describe("SubductionSync", () => { handle.emitChange() flushSync() - expect(observed?.status).toBe("syncing") await repo.resolveFlush(1) flushSync() expect(observed).toEqual({ status: "synced", error: undefined }) @@ -102,41 +84,32 @@ describe("SubductionSync", () => { test("reconnects without waiting for an obsolete flush", async () => { const repo = new FakeRepo(true) const handle = new FakeHandle() - const sync = new SubductionSync(repo as unknown as Repo, handle as unknown as GraphHandle) - let status: SubductionSyncStatus | undefined - const stop = observe(() => sync.status, value => { - status = value - }) + const sync = new DocumentSync(repo as unknown as Repo, handle) + let status: SyncStatus | undefined + const stop = observe(() => sync.status, value => (status = value)) flushSync() - expect(repo.flushes).toHaveLength(1) repo.disconnect() flushSync() expect(status).toBe("offline") - repo.connect() flushSync() - expect(status).toBe("syncing") expect(repo.flushes).toHaveLength(2) - await repo.resolveFlush(1) flushSync() expect(status).toBe("synced") - await repo.rejectFlush(0, new Error("obsolete")) flushSync() expect(status).toBe("synced") stop() }) - test("ignores late completion after cleanup and can resubscribe", async () => { + test("ignores completion after cleanup and can resubscribe", async () => { const repo = new FakeRepo(true) const handle = new FakeHandle() - const sync = new SubductionSync(repo as unknown as Repo, handle as unknown as GraphHandle) - let status: SubductionSyncStatus | undefined - const firstStop = observe(() => sync.status, value => { - status = value - }) + const sync = new DocumentSync(repo as unknown as Repo, handle) + let status: SyncStatus | undefined + const firstStop = observe(() => sync.status, value => (status = value)) flushSync() firstStop() @@ -145,15 +118,10 @@ describe("SubductionSync", () => { expect(repo.listenerCount()).toBe(0) expect(handle.listenerCount()).toBe(0) - const secondStop = observe(() => sync.status, value => { - status = value - }) + const secondStop = observe(() => sync.status, value => (status = value)) flushSync() - expect(repo.listenerCount()).toBe(1) - expect(handle.listenerCount()).toBe(1) expect(repo.flushes).toHaveLength(2) expect(status).toBe("syncing") - await repo.resolveFlush(1) flushSync() expect(status).toBe("synced") @@ -162,7 +130,7 @@ describe("SubductionSync", () => { }) class FakeHandle { - readonly documentId = "test-document" + readonly documentId = "test-document" as never readonly #listeners = new Set<() => void>() on(_event: "change", listener: () => void): void { @@ -224,7 +192,7 @@ class FakeRepo { async resolveFlush(index: number): Promise { const flush = this.flushes[index] if (!flush) throw new Error(`Missing flush ${index}`) - flush.resolve() + flush.resolve(undefined) await flush.promise await Promise.resolve() } diff --git a/examples/editor/tests/evaluate.unit.ts b/examples/lab/tests/evaluate.unit.ts similarity index 94% rename from examples/editor/tests/evaluate.unit.ts rename to examples/lab/tests/evaluate.unit.ts index 4b0ee00a..52ee2dad 100644 --- a/examples/editor/tests/evaluate.unit.ts +++ b/examples/lab/tests/evaluate.unit.ts @@ -3,7 +3,7 @@ import type { ColnHandle } from "@coln-project/repo" import { describe, expect, it } from "vitest" -import { evaluate } from "../src/lib/evaluate.ts" +import { evaluate } from "../src/tools/editor/evaluate.ts" const handle = { marker: "handle" } as unknown as ColnHandle diff --git a/examples/editor/tests/format.unit.ts b/examples/lab/tests/format.unit.ts similarity index 91% rename from examples/editor/tests/format.unit.ts rename to examples/lab/tests/format.unit.ts index 1d56fe54..1956c007 100644 --- a/examples/editor/tests/format.unit.ts +++ b/examples/lab/tests/format.unit.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT import { describe, expect, it } from "vitest" -import { displayRowRef, displayValue } from "../src/lib/format.ts" +import { displayRowRef, displayValue } from "../src/tools/editor/format.ts" describe("value formatting", () => { it("formats primitive values", () => { diff --git a/examples/lab/tests/lab.spec.ts b/examples/lab/tests/lab.spec.ts new file mode 100644 index 00000000..9bb26f7c --- /dev/null +++ b/examples/lab/tests/lab.spec.ts @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { expect, test } from "@playwright/test" + +const graphTheory = `theory Graph := sig + V : Set + E : V -> V -> Set +end + +realm GraphRealm @ Graph +end` + +test("moves from theory and graph ledgers into their stores", async ({ + page, +}) => { + await page.setViewportSize({ width: 390, height: 844 }) + await page.goto("/") + await expect( + page.getByRole("heading", { name: "A workbench for things that follow." }), + ).toBeVisible() + await expect( + page.getByRole("navigation", { name: "Lab tools" }), + ).toBeVisible() + expect( + await page.evaluate( + () => document.documentElement.scrollWidth <= innerWidth, + ), + ).toBe(true) + await page.setViewportSize({ width: 1280, height: 800 }) + + await page.getByRole("link", { name: /Start an experiment/ }).click() + await expect(page).toHaveURL(/\/sync\/#automerge:/) + const graphUrl = page.url() + await expect(page.getByText("COLN / GRAPH LAB")).toBeVisible() + await expect(page.getByTestId("sync-status")).toBeVisible() + await expect(page.getByTestId("head-count")).toHaveText(/^\d+ heads?$/) + const graphCanvasBox = await page.getByTestId("graph-canvas").boundingBox() + const graphControlsBox = await page + .locator("aside") + .filter({ has: page.getByTestId("add-vertex") }) + .boundingBox() + expect(graphCanvasBox).not.toBeNull() + expect(graphControlsBox).not.toBeNull() + expect(graphControlsBox!.x).toBeGreaterThan(graphCanvasBox!.x) + expect(Math.abs(graphControlsBox!.y - graphCanvasBox!.y)).toBeLessThan(2) + expect( + await page + .getByTestId("graph-canvas") + .evaluate((element) => getComputedStyle(element).minHeight), + ).toBe("680px") + await page.getByTestId("add-vertex").click() + await expect(page.getByTestId("graph-vertex")).toHaveCount(1) + const graphStoreHref = await page + .getByRole("link", { name: "Open in Store" }) + .getAttribute("href") + expect(graphStoreHref).toMatch(/^\/editor\/#automerge:/) + await page.getByRole("link", { name: "Open in Store" }).click() + await expect(page.getByText("COLN / STORE LAB")).toBeVisible() + await expect(page.getByText("JAVASCRIPT REPL")).toBeVisible() + await expect(page.getByTestId("table-option")).toHaveCount(2) + await expect( + page.getByTestId("table-option").filter({ hasText: "GraphRealm.V" }), + ).toBeVisible() + + await page + .getByRole("link", { name: /Theory/ }) + .first() + .click() + await expect(page).toHaveURL(/\/compiler\/#automerge:/) + const theoryUrl = page.url() + await expect(page.getByText("COLN / COMPILER")).toBeVisible() + await expect(page.getByText("SOURCE", { exact: true })).toBeVisible() + await expect(page.getByText("Diagnostics", { exact: true })).toBeVisible() + const editor = page.locator(".cm-content") + await editor.click() + await page.keyboard.press("ControlOrMeta+A") + await page.keyboard.insertText(graphTheory) + const createStore = page.getByRole("button", { name: /Create empty store/ }) + await expect(createStore).toBeEnabled({ timeout: 60_000 }) + const sourcePanelBox = await page + .getByText("SOURCE", { exact: true }) + .locator("xpath=ancestor::section[1]") + .boundingBox() + const storesPanelBox = await page + .locator("aside") + .filter({ hasText: "STORES" }) + .boundingBox() + expect(sourcePanelBox).not.toBeNull() + expect(storesPanelBox).not.toBeNull() + expect(storesPanelBox!.x).toBeGreaterThan(sourcePanelBox!.x) + expect(Math.abs(storesPanelBox!.y - sourcePanelBox!.y)).toBeLessThan(2) + await createStore.click() + await expect( + page.getByRole("link", { name: "Open", exact: true }), + ).toBeVisible() + + await page.getByRole("link", { name: "Open", exact: true }).click() + await expect(page).toHaveURL(/\/editor\/\?theory=automerge%3A.*#automerge:/) + await expect(page.getByTestId("table-option")).toHaveCount(2) + await expect( + page.getByRole("link", { name: "Return to Theory", exact: true }), + ).toBeVisible() + const storeUrl = page.url() + + const labTools = page.getByRole("navigation", { name: "Lab tools" }) + await labTools.getByRole("link", { name: /Graph/ }).click() + await expect(page).toHaveURL(graphUrl) + await expect(page.getByTestId("graph-vertex")).toHaveCount(1) + + await labTools.getByRole("link", { name: /Theory/ }).click() + await expect(page).toHaveURL(theoryUrl) + await expect(page.locator(".cm-content")).toContainText("theory Graph") + + await labTools.getByRole("link", { name: /Store/ }).click() + await expect(page).toHaveURL(storeUrl) + await expect(page.getByTestId("table-option")).toHaveCount(2) +}) diff --git a/examples/sync/tests/observe.svelte.ts b/examples/lab/tests/observe.svelte.ts similarity index 100% rename from examples/sync/tests/observe.svelte.ts rename to examples/lab/tests/observe.svelte.ts diff --git a/examples/lab/tests/router.unit.ts b/examples/lab/tests/router.unit.ts new file mode 100644 index 00000000..de4d30d3 --- /dev/null +++ b/examples/lab/tests/router.unit.ts @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { describe, expect, it } from "vitest" +import { routeFromUrl } from "../src/lib/router.svelte.ts" + +describe("Coln Lab routes", () => { + it("separates tool paths from document hashes", () => { + expect( + routeFromUrl( + new URL( + "https://lab.example/editor/?theory=automerge%3Atheory#automerge:store", + ), + ), + ).toEqual({ + tool: "editor", + documentUrl: "automerge:store", + theoryUrl: "automerge:theory", + pathname: "/editor/", + }) + }) + + it("supports a static deployment base", () => { + expect( + routeFromUrl( + new URL("https://example.test/lab/sync/#automerge:graph"), + "/lab/", + ), + ).toMatchObject({ tool: "sync", documentUrl: "automerge:graph" }) + }) + + it("reports unknown instruments", () => { + expect(routeFromUrl(new URL("https://lab.example/unknown/"))).toMatchObject( + { + tool: "not-found", + }, + ) + }) + + it("rejects extra path segments", () => { + expect( + routeFromUrl(new URL("https://lab.example/editor/extra/")), + ).toMatchObject({ + tool: "not-found", + }) + }) +}) diff --git a/examples/editor/tests/schema.unit.ts b/examples/lab/tests/schema.unit.ts similarity index 96% rename from examples/editor/tests/schema.unit.ts rename to examples/lab/tests/schema.unit.ts index ba0ee1c6..28414d5a 100644 --- a/examples/editor/tests/schema.unit.ts +++ b/examples/lab/tests/schema.unit.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT import { describe, expect, it } from "vitest" -import { readTables } from "../src/lib/schema.ts" +import { readTables } from "../src/tools/editor/schema.ts" describe("readTables", () => { it("extracts paths, types, and primary columns", () => { diff --git a/examples/editor/tests/snapshot.unit.ts b/examples/lab/tests/snapshot.unit.ts similarity index 95% rename from examples/editor/tests/snapshot.unit.ts rename to examples/lab/tests/snapshot.unit.ts index df3b5043..3344e200 100644 --- a/examples/editor/tests/snapshot.unit.ts +++ b/examples/lab/tests/snapshot.unit.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT import { describe, expect, it } from "vitest" -import { snapshot } from "../src/lib/snapshot.ts" +import { snapshot } from "../src/tools/editor/snapshot.ts" describe("snapshot", () => { it("copies plain values and marks cycles", () => { diff --git a/examples/editor/tests/source-storage.unit.ts b/examples/lab/tests/source-storage.unit.ts similarity index 94% rename from examples/editor/tests/source-storage.unit.ts rename to examples/lab/tests/source-storage.unit.ts index 81de5d17..59b1fa60 100644 --- a/examples/editor/tests/source-storage.unit.ts +++ b/examples/lab/tests/source-storage.unit.ts @@ -6,7 +6,7 @@ import { loadSource, saveSource, starterSource, -} from "../src/lib/source-storage.ts" +} from "../src/tools/editor/source-storage.ts" const values = new Map() diff --git a/examples/editor/tsconfig.json b/examples/lab/tsconfig.json similarity index 100% rename from examples/editor/tsconfig.json rename to examples/lab/tsconfig.json diff --git a/examples/lab/vite.config.ts b/examples/lab/vite.config.ts new file mode 100644 index 00000000..972cd3ea --- /dev/null +++ b/examples/lab/vite.config.ts @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2026 Coln contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +import { svelte } from "@sveltejs/vite-plugin-svelte" +import tailwindcss from "@tailwindcss/vite" +import { copyFile, mkdir } from "node:fs/promises" +import { defineConfig, type Plugin } from "vite" +import wasm from "vite-plugin-wasm" + +function staticToolRoutes(): Plugin { + let outputDirectory = "" + return { + name: "coln-lab-static-routes", + configResolved(config) { + outputDirectory = config.build.outDir + }, + async closeBundle() { + await Promise.all( + ["compiler", "editor", "sync"].map(async (route) => { + const directory = `${outputDirectory}/${route}` + await mkdir(directory, { recursive: true }) + await copyFile( + `${outputDirectory}/index.html`, + `${directory}/index.html`, + ) + }), + ) + }, + } +} + +export default defineConfig({ + base: process.env.VITE_BASE || "/", + plugins: [tailwindcss(), wasm(), svelte(), staticToolRoutes()], + resolve: { + dedupe: [ + "svelte", + "@coln-project/runtime", + "@automerge/automerge", + "@automerge/automerge-codemirror", + "@automerge/automerge-repo", + "@codemirror/state", + "@codemirror/view", + "codemirror", + ], + }, + optimizeDeps: { + include: [ + "@automerge/automerge-repo/slim", + "@automerge/automerge-codemirror", + "@codemirror/lang-javascript", + "@codemirror/state", + "@codemirror/view", + "codemirror", + "json-formatter-js", + ], + }, + build: { + emptyOutDir: true, + outDir: "../../_build/web/lab-app", + target: "esnext", + }, +}) diff --git a/examples/sync/vitest.config.ts b/examples/lab/vitest.config.ts similarity index 87% rename from examples/sync/vitest.config.ts rename to examples/lab/vitest.config.ts index 0bf62d85..224ba833 100644 --- a/examples/sync/vitest.config.ts +++ b/examples/lab/vitest.config.ts @@ -1,5 +1,4 @@ // SPDX-FileCopyrightText: 2026 Coln contributors -// // SPDX-License-Identifier: Apache-2.0 OR MIT import { svelte } from "@sveltejs/vite-plugin-svelte" @@ -8,10 +7,7 @@ import { defineConfig } from "vitest/config" export default defineConfig({ plugins: [svelte()], - resolve: { - conditions: ["browser"], - dedupe: ["svelte"], - }, + resolve: { conditions: ["browser"], dedupe: ["svelte"] }, test: { browser: { enabled: true, diff --git a/examples/style.css b/examples/style.css deleted file mode 100644 index 87035491..00000000 --- a/examples/style.css +++ /dev/null @@ -1,115 +0,0 @@ -body { - --hue: 220; - --sat: 32%; - --main-bg: hsl(var(--hue), var(--sat), 16%); - --input-bg: hsl(var(--hue), var(--sat), 8%); - --alt-bg: hsl(var(--hue), var(--sat), 13%); - --border: hsl(var(--hue), var(--sat), 30%); - --accent: hsl(var(--hue), var(--sat), 75%); - --text-hue: 39; - --text-sat: 70%; - --text: hsl(var(--text-hue), var(--text-sat), 88%); - --debug: hsl(290, var(--text-sat), 55%); - --info: hsl(215, var(--text-sat), 55%); - --warning: hsl(45, var(--text-sat), 55%); - --error: hsl(355, var(--text-sat), 55%); - font-family: Helvetica, Arial, sans-serif; - background: var(--main-bg); - color: var(--text); - max-width: 60em; - margin: 2em auto; - > * { - margin: 0 1em; - } -} -textarea, -pre { - padding: 0.8em 1em; -} -a { - color: var(--accent); - &:hover { - color: hsl(var(--hue), var(--sat), 90%); - } -} -textarea, -select { - color: inherit; - &:focus { - border-color: var(--accent); - outline: none; - } -} -textarea { - background: var(--input-bg); - resize: vertical; - border: solid 1px var(--border); -} -pre { - margin: 0; - white-space: pre-wrap; -} -select { - background: var(--input-bg); - border: solid 1px var(--border); - padding: 0.5em 1em; -} -body[data-compiling] { - & .output { - opacity: 0.5; - } -} -.main { - display: grid; - grid-template-columns: auto 1fr; - grid-template-rows: auto auto repeat(3, minmax(2.5em, auto)); - gap: 1em 1em; - > h2 { - text-align: right; - } - > * { - margin: 0; - } -} -.header { - grid-column: 1 / -1; - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1em; - > h1 { - padding-left: 3em; - margin: 0; - } -} -#input, -#examples, -.output { - border-radius: 0.3em; -} -.output { - border: solid 1px var(--border); - background: var(--alt-bg); - > :not(:last-child) { - border-bottom: solid 1px var(--border); - } -} -#json { - padding: 1em; -} -.diag-debug { - --diag-colour: var(--debug); -} -.diag-info { - --diag-colour: var(--info); -} -.diag-warning { - --diag-colour: var(--warning); -} -.diag-error { - --diag-colour: var(--error); -} -[class^="ann-"] { - color: var(--diag-colour); - font-weight: bold; -} diff --git a/examples/sync-demo/.gitignore b/examples/sync-demo/.gitignore deleted file mode 100644 index f06235c4..00000000 --- a/examples/sync-demo/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -dist diff --git a/examples/sync-demo/README.md b/examples/sync-demo/README.md deleted file mode 100644 index 071626c7..00000000 --- a/examples/sync-demo/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# Coln + automerge-repo web demo - -This demo stores a compiler-generated Coln realm in `automerge-repo` using a -custom document type. Custom document types aren't actually a thing in -`automerge-repo` (yet), so we use a vendored version of `automerge-repo` from -the `doctypes` branch which adds support for custom document types. - -We define a function which takes a compiled FFI bindings from compiler -and turns it into an automerge-repo document type. This is the `colnDocType` -function. - -Then, we can create and find handles containing these documents, modify and -render them, and synchronize them using the public subduction sycn server. -That's what most of the code here does. - -## Compiling the schema - -The demo source schema is: - -- `graph.coln` - -The generated FFI/IR artifacts live in: - -- `src/generated/Graph.ts` -- `src/generated/GraphRealm.ts` -- `src/generated/GraphRealm.json` - -`GraphRealm.ts` exports the generated `View`, `Transaction`, and `schema`. The -generic `src/colnDocType.ts` consumes that compiled FFI directly: - -```ts -const coln = colnDocType(GraphRealm) -``` - -`GraphRealm.json` is passed directly to `StoreHandle.fromTheory(...)`; no local -IR-to-FlatTheory translation is needed anymore. - -You can compile them by running `./compile.sh` - -## Run - -From `coln/examples/sync-demo/`: - -```bash -pnpm install -pnpm dev -``` - -Open the Vite URL, then open the hash URL shown in the page in another tab. Each -tab creates its own Repo and syncs via Subduction. - -## Build - -```bash -pnpm build -``` - -The build script runs: - -1. `./compile.sh` to regenerate `src/generated/*` from `graph.coln` -2. `tsc --noEmit` -3. `vite build` - -## Test - -```bash -pnpm exec playwright install chromium # first time only -pnpm test:e2e -``` diff --git a/examples/sync-demo/compile.sh b/examples/sync-demo/compile.sh deleted file mode 100755 index f19b6bd8..00000000 --- a/examples/sync-demo/compile.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if command -v coln >/dev/null 2>&1; then - coln_cli=(coln) -elif command -v coln-cli >/dev/null 2>&1; then - coln_cli=(coln-cli) -else - coln_cli=(cabal run coln-cli --) -fi - -"${coln_cli[@]}" generate-ts ./graph.coln --output-dir ./src/generated -"${coln_cli[@]}" generate-ir ./graph.coln --output-dir ./src/generated - diff --git a/examples/sync-demo/index.html b/examples/sync-demo/index.html deleted file mode 100644 index 7f7f6654..00000000 --- a/examples/sync-demo/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Coln automerge-repo demo - - -
- - - diff --git a/examples/sync-demo/package.json b/examples/sync-demo/package.json deleted file mode 100644 index eaf37cb6..00000000 --- a/examples/sync-demo/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "coln-sync-demo", - "private": true, - "type": "module", - "scripts": { - "dev": "vite --host 0.0.0.0", - "server": "node server.mjs", - "compile": "bash ./compile.sh", - "build": "pnpm run compile && tsc --noEmit && vite build", - "test:e2e": "playwright test" - }, - "dependencies": { - "@automerge/automerge-repo": "file:./vendor/automerge-automerge-repo-2.6.0-subduction.33.tgz", - "@automerge/automerge-subduction": "0.16.0", - "@coln-project/coln-js-runtime": "link:../../packages/coln-js-runtime", - "@coln-project/runtime": "link:../../packages/coln-js-runtime", - "@vitejs/plugin-react": "^6.0.2", - "react": "^19.2.6", - "react-dom": "^19.2.6", - "typescript": "^6.0.3", - "vite": "^8.0.14", - "vite-plugin-wasm": "^3.6.0", - "ws": "^8.21.0" - }, - "devDependencies": { - "@playwright/test": "^1.61.0", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3" - } -} diff --git a/examples/sync-demo/playwright.config.ts b/examples/sync-demo/playwright.config.ts deleted file mode 100644 index 9374d023..00000000 --- a/examples/sync-demo/playwright.config.ts +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// -// SPDX-License-Identifier: Apache-2.0 OR MIT - -import { defineConfig, devices } from "@playwright/test" - -export default defineConfig({ - testDir: "./tests", - timeout: 30_000, - expect: { timeout: 10_000 }, - fullyParallel: false, - retries: 0, - use: { - baseURL: "http://127.0.0.1:5173", - trace: "retain-on-failure", - }, - webServer: [ - { - command: "pnpm server", - port: 3030, - reuseExistingServer: !process.env.CI, - stdout: "pipe", - stderr: "pipe", - }, - { - command: "pnpm dev -- --port 5173", - url: "http://127.0.0.1:5173", - reuseExistingServer: !process.env.CI, - stdout: "pipe", - stderr: "pipe", - }, - ], - projects: [ - { - name: "chromium", - use: { ...devices["Desktop Chrome"] }, - }, - ], -}) diff --git a/examples/sync-demo/pnpm-lock.yaml b/examples/sync-demo/pnpm-lock.yaml deleted file mode 100644 index b77306e2..00000000 --- a/examples/sync-demo/pnpm-lock.yaml +++ /dev/null @@ -1,847 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@automerge/automerge-repo': - specifier: file:./vendor/automerge-automerge-repo-2.6.0-subduction.33.tgz - version: file:vendor/automerge-automerge-repo-2.6.0-subduction.33.tgz - '@automerge/automerge-subduction': - specifier: 0.16.0 - version: 0.16.0 - '@coln-project/coln-js-runtime': - specifier: link:../../packages/coln-js-runtime - version: link:../../packages/coln-js-runtime - '@coln-project/runtime': - specifier: link:../../packages/coln-js-runtime - version: link:../../packages/coln-js-runtime - '@vitejs/plugin-react': - specifier: ^6.0.2 - version: 6.0.2(vite@8.0.16) - react: - specifier: ^19.2.6 - version: 19.2.7 - react-dom: - specifier: ^19.2.6 - version: 19.2.7(react@19.2.7) - typescript: - specifier: ^6.0.3 - version: 6.0.3 - vite: - specifier: ^8.0.14 - version: 8.0.16 - vite-plugin-wasm: - specifier: ^3.6.0 - version: 3.6.0(vite@8.0.16) - ws: - specifier: ^8.21.0 - version: 8.21.0 - devDependencies: - '@playwright/test': - specifier: ^1.61.0 - version: 1.61.0 - '@types/react': - specifier: ^19.2.17 - version: 19.2.17 - '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.17) - -packages: - - '@automerge/automerge-repo@file:vendor/automerge-automerge-repo-2.6.0-subduction.33.tgz': - resolution: {integrity: sha512-zz6O4jJTKHy1bncoGn2pq0OKfW0DxkCJ5ccjoPiaya5v0gDyMnqbEpB5/UU5NJ7TKIBXU97NutNTFJDHWvwdvg==, tarball: file:vendor/automerge-automerge-repo-2.6.0-subduction.33.tgz} - version: 2.6.0-subduction.33 - engines: {node: '>=22.13'} - - '@automerge/automerge-subduction@0.16.0': - resolution: {integrity: sha512-BOOn/4p8cF4LhjDUQdgM3qyZFya+M7ZuZywuC99x/kbhk4UK7dw/QcsifcHG5E1TAW4R/K17ZGFUC5nCVUSSWg==} - - '@automerge/automerge@3.3.0-fragments.1': - resolution: {integrity: sha512-Im2n3qcYSuIsLDSjofTRo7CZyyvFn5pKLSCCO5sjWzESw+9q9G25ZIrL2mczDX43cXtyzi+w/4b9VUaiNdY56Q==} - - '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': - resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} - cpu: [arm64] - os: [darwin] - - '@cbor-extract/cbor-extract-darwin-x64@2.2.2': - resolution: {integrity: sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==} - cpu: [x64] - os: [darwin] - - '@cbor-extract/cbor-extract-linux-arm64@2.2.2': - resolution: {integrity: sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==} - cpu: [arm64] - os: [linux] - - '@cbor-extract/cbor-extract-linux-arm@2.2.2': - resolution: {integrity: sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==} - cpu: [arm] - os: [linux] - - '@cbor-extract/cbor-extract-linux-x64@2.2.2': - resolution: {integrity: sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==} - cpu: [x64] - os: [linux] - - '@cbor-extract/cbor-extract-win32-x64@2.2.2': - resolution: {integrity: sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==} - cpu: [x64] - os: [win32] - - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - - '@napi-rs/wasm-runtime@1.1.5': - resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - - '@noble/hashes@1.8.0': - resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} - engines: {node: ^14.21.3 || >=16} - - '@oxc-project/types@0.133.0': - resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} - - '@playwright/test@1.61.0': - resolution: {integrity: sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==} - engines: {node: '>=18'} - hasBin: true - - '@rolldown/binding-android-arm64@1.0.3': - resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@rolldown/binding-darwin-arm64@1.0.3': - resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.0.3': - resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@rolldown/binding-freebsd-x64@1.0.3': - resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': - resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.0.3': - resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-arm64-musl@1.0.3': - resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rolldown/binding-linux-ppc64-gnu@1.0.3': - resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-s390x-gnu@1.0.3': - resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-gnu@1.0.3': - resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-musl@1.0.3': - resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rolldown/binding-openharmony-arm64@1.0.3': - resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@rolldown/binding-wasm32-wasi@1.0.3': - resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.0.3': - resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.0.3': - resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@rolldown/pluginutils@1.0.1': - resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - - '@tybys/wasm-util@0.10.2': - resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} - - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} - peerDependencies: - '@types/react': ^19.2.0 - - '@types/react@19.2.17': - resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} - - '@vitejs/plugin-react@6.0.2': - resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} - engines: {node: ^20.19.0 || >=22.12.0} - peerDependencies: - '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 - babel-plugin-react-compiler: ^1.0.0 - vite: ^8.0.0 - peerDependenciesMeta: - '@rolldown/plugin-babel': - optional: true - babel-plugin-react-compiler: - optional: true - - base-x@5.0.1: - resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} - - bs58@6.0.0: - resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} - - bs58check@4.0.0: - resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} - - cbor-extract@2.2.2: - resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} - hasBin: true - - cbor-x@1.6.4: - resolution: {integrity: sha512-UGKHjp6RHC6QuZ2yy5LCKm7MojM4716DwoSaqwQpaH4DvZvbBTGcoDNTiG9Y2lByXZYFEs9WRkS5tLl96IrF1Q==} - - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - eventemitter3@5.0.4: - resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} - - fast-sha256@1.3.0: - resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - isomorphic-ws@5.0.0: - resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} - peerDependencies: - ws: '*' - - lightningcss-android-arm64@1.32.0: - resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-darwin-arm64@1.32.0: - resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.32.0: - resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.32.0: - resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.32.0: - resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - lightningcss-linux-arm64-musl@1.32.0: - resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - lightningcss-linux-x64-gnu@1.32.0: - resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - lightningcss-linux-x64-musl@1.32.0: - resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - lightningcss-win32-arm64-msvc@1.32.0: - resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.32.0: - resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.32.0: - resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} - engines: {node: '>= 12.0.0'} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - nanoid@3.3.12: - resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - node-gyp-build-optional-packages@5.1.1: - resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} - hasBin: true - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} - - playwright-core@1.61.0: - resolution: {integrity: sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==} - engines: {node: '>=18'} - hasBin: true - - playwright@1.61.0: - resolution: {integrity: sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==} - engines: {node: '>=18'} - hasBin: true - - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} - engines: {node: ^10 || ^12 || >=14} - - react-dom@19.2.7: - resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} - peerDependencies: - react: ^19.2.7 - - react@19.2.7: - resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} - engines: {node: '>=0.10.0'} - - rolldown@1.0.3: - resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - tinyglobby@0.2.17: - resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} - engines: {node: '>=12.0.0'} - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - typescript@6.0.3: - resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} - engines: {node: '>=14.17'} - hasBin: true - - uuid@14.0.0: - resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} - hasBin: true - - vite-plugin-wasm@3.6.0: - resolution: {integrity: sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==} - peerDependencies: - vite: ^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - - vite@8.0.16: - resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.18 - esbuild: ^0.27.0 || ^0.28.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - ws@8.21.0: - resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - xstate@5.32.1: - resolution: {integrity: sha512-IGX9q5vEOplWjVq79edfgjJfVV/lXCup9p/fQGgUabAveZrlRwWJ/mC2iZEE7wswXbWITBCoS7gmoFfcuWAwsQ==} - -snapshots: - - '@automerge/automerge-repo@file:vendor/automerge-automerge-repo-2.6.0-subduction.33.tgz': - dependencies: - '@automerge/automerge': 3.3.0-fragments.1 - '@automerge/automerge-subduction': 0.16.0 - bs58check: 4.0.0 - cbor-x: 1.6.4 - debug: 4.4.3 - eventemitter3: 5.0.4 - fast-sha256: 1.3.0 - isomorphic-ws: 5.0.0(ws@8.21.0) - uuid: 14.0.0 - ws: 8.21.0 - xstate: 5.32.1 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - '@automerge/automerge-subduction@0.16.0': {} - - '@automerge/automerge@3.3.0-fragments.1': {} - - '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-darwin-x64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-linux-arm64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-linux-arm@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-linux-x64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-win32-x64@2.2.2': - optional: true - - '@emnapi/core@1.10.0': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.2 - optional: true - - '@noble/hashes@1.8.0': {} - - '@oxc-project/types@0.133.0': {} - - '@playwright/test@1.61.0': - dependencies: - playwright: 1.61.0 - - '@rolldown/binding-android-arm64@1.0.3': - optional: true - - '@rolldown/binding-darwin-arm64@1.0.3': - optional: true - - '@rolldown/binding-darwin-x64@1.0.3': - optional: true - - '@rolldown/binding-freebsd-x64@1.0.3': - optional: true - - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': - optional: true - - '@rolldown/binding-linux-arm64-gnu@1.0.3': - optional: true - - '@rolldown/binding-linux-arm64-musl@1.0.3': - optional: true - - '@rolldown/binding-linux-ppc64-gnu@1.0.3': - optional: true - - '@rolldown/binding-linux-s390x-gnu@1.0.3': - optional: true - - '@rolldown/binding-linux-x64-gnu@1.0.3': - optional: true - - '@rolldown/binding-linux-x64-musl@1.0.3': - optional: true - - '@rolldown/binding-openharmony-arm64@1.0.3': - optional: true - - '@rolldown/binding-wasm32-wasi@1.0.3': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.0.3': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.0.3': - optional: true - - '@rolldown/pluginutils@1.0.1': {} - - '@tybys/wasm-util@0.10.2': - dependencies: - tslib: 2.8.1 - optional: true - - '@types/react-dom@19.2.3(@types/react@19.2.17)': - dependencies: - '@types/react': 19.2.17 - - '@types/react@19.2.17': - dependencies: - csstype: 3.2.3 - - '@vitejs/plugin-react@6.0.2(vite@8.0.16)': - dependencies: - '@rolldown/pluginutils': 1.0.1 - vite: 8.0.16 - - base-x@5.0.1: {} - - bs58@6.0.0: - dependencies: - base-x: 5.0.1 - - bs58check@4.0.0: - dependencies: - '@noble/hashes': 1.8.0 - bs58: 6.0.0 - - cbor-extract@2.2.2: - dependencies: - node-gyp-build-optional-packages: 5.1.1 - optionalDependencies: - '@cbor-extract/cbor-extract-darwin-arm64': 2.2.2 - '@cbor-extract/cbor-extract-darwin-x64': 2.2.2 - '@cbor-extract/cbor-extract-linux-arm': 2.2.2 - '@cbor-extract/cbor-extract-linux-arm64': 2.2.2 - '@cbor-extract/cbor-extract-linux-x64': 2.2.2 - '@cbor-extract/cbor-extract-win32-x64': 2.2.2 - optional: true - - cbor-x@1.6.4: - optionalDependencies: - cbor-extract: 2.2.2 - - csstype@3.2.3: {} - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - detect-libc@2.1.2: {} - - eventemitter3@5.0.4: {} - - fast-sha256@1.3.0: {} - - fdir@6.5.0(picomatch@4.0.4): - optionalDependencies: - picomatch: 4.0.4 - - fsevents@2.3.2: - optional: true - - fsevents@2.3.3: - optional: true - - isomorphic-ws@5.0.0(ws@8.21.0): - dependencies: - ws: 8.21.0 - - lightningcss-android-arm64@1.32.0: - optional: true - - lightningcss-darwin-arm64@1.32.0: - optional: true - - lightningcss-darwin-x64@1.32.0: - optional: true - - lightningcss-freebsd-x64@1.32.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.32.0: - optional: true - - lightningcss-linux-arm64-gnu@1.32.0: - optional: true - - lightningcss-linux-arm64-musl@1.32.0: - optional: true - - lightningcss-linux-x64-gnu@1.32.0: - optional: true - - lightningcss-linux-x64-musl@1.32.0: - optional: true - - lightningcss-win32-arm64-msvc@1.32.0: - optional: true - - lightningcss-win32-x64-msvc@1.32.0: - optional: true - - lightningcss@1.32.0: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.32.0 - lightningcss-darwin-arm64: 1.32.0 - lightningcss-darwin-x64: 1.32.0 - lightningcss-freebsd-x64: 1.32.0 - lightningcss-linux-arm-gnueabihf: 1.32.0 - lightningcss-linux-arm64-gnu: 1.32.0 - lightningcss-linux-arm64-musl: 1.32.0 - lightningcss-linux-x64-gnu: 1.32.0 - lightningcss-linux-x64-musl: 1.32.0 - lightningcss-win32-arm64-msvc: 1.32.0 - lightningcss-win32-x64-msvc: 1.32.0 - - ms@2.1.3: {} - - nanoid@3.3.12: {} - - node-gyp-build-optional-packages@5.1.1: - dependencies: - detect-libc: 2.1.2 - optional: true - - picocolors@1.1.1: {} - - picomatch@4.0.4: {} - - playwright-core@1.61.0: {} - - playwright@1.61.0: - dependencies: - playwright-core: 1.61.0 - optionalDependencies: - fsevents: 2.3.2 - - postcss@8.5.15: - dependencies: - nanoid: 3.3.12 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - react-dom@19.2.7(react@19.2.7): - dependencies: - react: 19.2.7 - scheduler: 0.27.0 - - react@19.2.7: {} - - rolldown@1.0.3: - dependencies: - '@oxc-project/types': 0.133.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.3 - '@rolldown/binding-darwin-arm64': 1.0.3 - '@rolldown/binding-darwin-x64': 1.0.3 - '@rolldown/binding-freebsd-x64': 1.0.3 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 - '@rolldown/binding-linux-arm64-gnu': 1.0.3 - '@rolldown/binding-linux-arm64-musl': 1.0.3 - '@rolldown/binding-linux-ppc64-gnu': 1.0.3 - '@rolldown/binding-linux-s390x-gnu': 1.0.3 - '@rolldown/binding-linux-x64-gnu': 1.0.3 - '@rolldown/binding-linux-x64-musl': 1.0.3 - '@rolldown/binding-openharmony-arm64': 1.0.3 - '@rolldown/binding-wasm32-wasi': 1.0.3 - '@rolldown/binding-win32-arm64-msvc': 1.0.3 - '@rolldown/binding-win32-x64-msvc': 1.0.3 - - scheduler@0.27.0: {} - - source-map-js@1.2.1: {} - - tinyglobby@0.2.17: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - - tslib@2.8.1: - optional: true - - typescript@6.0.3: {} - - uuid@14.0.0: {} - - vite-plugin-wasm@3.6.0(vite@8.0.16): - dependencies: - vite: 8.0.16 - - vite@8.0.16: - dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.15 - rolldown: 1.0.3 - tinyglobby: 0.2.17 - optionalDependencies: - fsevents: 2.3.3 - - ws@8.21.0: {} - - xstate@5.32.1: {} diff --git a/examples/sync-demo/pnpm-workspace.yaml b/examples/sync-demo/pnpm-workspace.yaml deleted file mode 100644 index f91e0fa2..00000000 --- a/examples/sync-demo/pnpm-workspace.yaml +++ /dev/null @@ -1,5 +0,0 @@ -allowBuilds: - cbor-extract: false -minimumReleaseAgeExclude: - - '@automerge/automerge-repo@2.6.0-subduction.33' - - '@automerge/automerge-subduction@0.16.0' diff --git a/examples/sync-demo/server.mjs b/examples/sync-demo/server.mjs deleted file mode 100644 index 6a8de6a2..00000000 --- a/examples/sync-demo/server.mjs +++ /dev/null @@ -1,108 +0,0 @@ -import { WebSocketServer } from "ws" -import { - MemorySigner, - MemoryStorage, - Subduction, -} from "@automerge/automerge-subduction" - -class WsTransport { - #ws - #queue = [] - #waiters = [] - #errorWaiters = [] - #closed = false - #closedPromise - #resolveClosed - #disconnectCallback = null - - constructor(ws) { - this.#ws = ws - this.#closedPromise = new Promise(resolve => { - this.#resolveClosed = resolve - }) - - ws.on("message", data => { - const bytes = toUint8Array(data) - const waiter = this.#waiters.shift() - if (waiter) { - this.#errorWaiters.shift() - waiter(bytes) - } else { - this.#queue.push(bytes) - } - }) - - ws.on("close", () => this.#markClosed(new Error("WebSocket closed"))) - ws.on("error", err => this.#markClosed(err instanceof Error ? err : new Error(String(err)))) - } - - onDisconnect(callback) { - this.#disconnectCallback = callback - } - - async sendBytes(bytes) { - if (this.#closed) throw new Error("WebSocket closed") - await new Promise((resolve, reject) => { - this.#ws.send(Buffer.from(bytes), err => (err ? reject(err) : resolve())) - }) - } - - recvBytes() { - const queued = this.#queue.shift() - if (queued) return Promise.resolve(queued) - if (this.#closed) return Promise.reject(new Error("WebSocket closed")) - return new Promise((resolve, reject) => { - this.#waiters.push(resolve) - this.#errorWaiters.push(reject) - }) - } - - async disconnect() { - this.#markClosed(new Error("WebSocket closed"), false) - this.#ws.close() - } - - closed() { - return this.#closedPromise - } - - #markClosed(err, notify = true) { - if (this.#closed) return - this.#closed = true - this.#resolveClosed() - for (const reject of this.#errorWaiters) reject(err) - this.#waiters = [] - this.#errorWaiters = [] - if (notify) this.#disconnectCallback?.() - } -} - -function toUint8Array(data) { - if (data instanceof Uint8Array) { - return new Uint8Array(data.buffer, data.byteOffset, data.byteLength) - } - if (data instanceof ArrayBuffer) return new Uint8Array(data) - if (Array.isArray(data)) return toUint8Array(Buffer.concat(data)) - return new Uint8Array(Buffer.from(data)) -} - -const port = Number(process.env.PORT ?? 3030) -const serviceName = `localhost:${port}` -const subduction = new Subduction({ - signer: new MemorySigner(), - storage: new MemoryStorage(), -}) -const wss = new WebSocketServer({ port }) - -wss.on("connection", ws => { - subduction.acceptTransport(new WsTransport(ws), serviceName).catch(err => { - if (!String(err?.message ?? err).includes("closed")) console.error(err) - }) -}) - -process.on("SIGINT", async () => { - await subduction.disconnectAll().catch(() => {}) - wss.close(() => process.exit(0)) -}) - -console.log(`Subduction relay listening on ws://localhost:${port}`) diff --git a/examples/sync-demo/src/App.tsx b/examples/sync-demo/src/App.tsx deleted file mode 100644 index 9b8faa63..00000000 --- a/examples/sync-demo/src/App.tsx +++ /dev/null @@ -1,320 +0,0 @@ -import { useEffect, useMemo, useState, type ReactNode } from "react" -import type { RowRef, RowView, Value } from "@coln-project/runtime" -import type { CrdtDocHandle } from "@automerge/automerge-repo/slim" -import * as GraphRealm from "./generated/GraphRealm.ts" -import { - refId, - rowValue, - type ColnDocType, - type ColnDocument, -} from "./colnDocType" -import "./style.css" - -type GraphFfi = typeof GraphRealm -type GraphDocType = ColnDocType -type GraphDoc = ColnDocument - -type Props = { - handle: CrdtDocHandle -} - -type UiGraph = { id: string; ref: RowRef | null } -type UiVertex = { - id: string - ref: RowRef - graphId: string - graph: RowRef | null -} -type UiEdge = { - id: string - ref: RowRef - graphId: string - graph: RowRef | null - fromId: string - from: RowRef - toId: string - to: RowRef -} - -type UiGraphProjection = { - graphs: UiGraph[] - vertices: UiVertex[] - edges: UiEdge[] - heads: string[] -} - -const SYNTHETIC_GRAPH_ID = "GraphRealm" - -export function App({ handle }: Props) { - const [doc, setDoc] = useState(() => handle.doc()) - const graph = useMemo(() => projectGraph(doc), [doc]) - const [selectedGraphId, setSelectedGraphId] = useState("") - const [fromId, setFromId] = useState("") - const [toId, setToId] = useState("") - const [error, setError] = useState("") - - useEffect(() => { - const update = () => setDoc(handle.doc()) - handle.on("change", update) - handle.on("heads-changed", update) - return () => { - handle.off("change", update) - handle.off("heads-changed", update) - } - }, [handle]) - - const selectedGraph = useMemo( - () => graph.graphs.find(graph => graph.id === selectedGraphId) ?? graph.graphs[0], - [graph.graphs, selectedGraphId] - ) - - useEffect(() => { - if (!selectedGraph) { - setSelectedGraphId("") - return - } - if (selectedGraph.id !== selectedGraphId) setSelectedGraphId(selectedGraph.id) - }, [selectedGraph, selectedGraphId]) - - const vertices = useMemo( - () => graph.vertices.filter(vertex => vertex.graphId === selectedGraph?.id), - [graph.vertices, selectedGraph] - ) - const edges = useMemo( - () => graph.edges.filter(edge => edge.graphId === selectedGraph?.id), - [graph.edges, selectedGraph] - ) - - useEffect(() => { - if (!vertices.some(vertex => vertex.id === fromId)) setFromId(vertices[0]?.id ?? "") - if (!vertices.some(vertex => vertex.id === toId)) setToId(vertices[1]?.id ?? vertices[0]?.id ?? "") - }, [vertices, fromId, toId]) - - const run = (fn: () => void) => { - setError("") - try { - fn() - } catch (e) { - setError(e instanceof Error ? e.message : String(e)) - } - } - - const addVertex = (graph: UiGraph | undefined) => - run(() => { - if (!graph) throw new Error("Create/select a graph first") - handle.change(tx => { - tx.root.V.add() - }) - }) - - const addEdge = () => - run(() => { - if (!selectedGraph) throw new Error("Create/select a graph first") - const from = vertices.find(vertex => vertex.id === fromId) - const to = vertices.find(vertex => vertex.id === toId) - if (!from || !to) throw new Error("Select two vertices first") - handle.change(tx => { - tx.root.E(rowValue(from.ref))(rowValue(to.ref)).add() - }) - }) - - return ( -
-
-

Coln + automerge-repo

-

- Open this URL in another tab, then edit from either side. -

- e.currentTarget.select()} /> -
- - {error &&
{error}
} - -
- - -
- -
- - - -
- -
- - {graph.graphs.length === 0 ? ( -

No visible graphs yet. The document may still be syncing.

- ) : ( -
    - {graph.graphs.map((graph, index) => ( -
  • Graph {index + 1}: {shortId(graph.id)}
  • - ))} -
- )} -
- - - {vertices.length === 0 ? ( -

No vertices in this graph.

- ) : ( -
    - {vertices.map((vertex, index) => ( -
  • {vertexLabel(vertex, vertices, index)}
  • - ))} -
- )} -
- - - {edges.length === 0 ? ( -

No edges in this graph.

- ) : ( -
    - {edges.map(edge => ( -
  • - {vertexName(edge.fromId, vertices)} → {vertexName(edge.toId, vertices)} -
  • - ))} -
- )} -
-
- -
- {graph.heads.length} head{graph.heads.length === 1 ? "" : "s"} - {graph.vertices.length} visible vertices - {graph.edges.length} visible edges -
-
- ) -} - -function projectGraph(doc: GraphDoc): UiGraphProjection { - const vertexRows = iteratorToArray(doc.realm.root.V.values()) - const vertices = vertexRows.map(row => { - const ref = valueRef(row.rowId) - if (!ref) throw new Error("vertex row is missing a row id") - return { - id: refId(ref), - ref, - graphId: SYNTHETIC_GRAPH_ID, - graph: null, - } - }) - const visibleVertexIds = new Set(vertices.map(vertex => vertex.id)) - - return { - graphs: [{ id: SYNTHETIC_GRAPH_ID, ref: null }], - vertices, - edges: doc.store - .scanTable("GraphRealm.E") - .flatMap(row => projectEdge(row, visibleVertexIds)), - heads: doc.heads, - } -} - -function projectEdge(row: RowView, visibleVertexIds: Set): UiEdge[] { - const from = valueRef(row.values[0]) - const to = valueRef(row.values[1]) - if (!from || !to) return [] - - const fromId = refId(from) - const toId = refId(to) - if (!visibleVertexIds.has(fromId) || !visibleVertexIds.has(toId)) return [] - - const ref = valueRef(row.rowId) - if (!ref) return [] - - return [ - { - id: refId(ref), - ref, - graphId: SYNTHETIC_GRAPH_ID, - graph: null, - fromId, - from, - toId, - to, - }, - ] -} - -function iteratorToArray(iterator: Iterator): T[] { - const values: T[] = [] - for (let next = iterator.next(); !next.done; next = iterator.next()) { - values.push(next.value) - } - return values -} - -function valueRef(value: Value | undefined): RowRef | undefined { - return value?.tag === "row_id" ? value.value : undefined -} - -function VertexSelect({ - testId, - vertices, - value, - onChange, -}: { - testId: string - vertices: UiVertex[] - value: string - onChange(value: string): void -}) { - return ( - - ) -} - -function Panel({ title, children }: { title: string; children: ReactNode }) { - return ( -
-

{title}

- {children} -
- ) -} - -function vertexLabel(vertex: UiVertex, vertices: UiVertex[], fallbackIndex: number) { - return `${vertexName(vertex.id, vertices, fallbackIndex)} · ${shortId(vertex.id)}` -} - -function vertexName(id: string, vertices: UiVertex[], fallbackIndex = -1) { - const index = vertices.findIndex(vertex => vertex.id === id) - return `V${(index >= 0 ? index : fallbackIndex) + 1}` -} - -function shortId(id: string) { - return id.slice(0, 8) -} diff --git a/examples/sync-demo/src/colnDocType.ts b/examples/sync-demo/src/colnDocType.ts deleted file mode 100644 index a27f520a..00000000 --- a/examples/sync-demo/src/colnDocType.ts +++ /dev/null @@ -1,161 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// -// SPDX-License-Identifier: Apache-2.0 OR MIT - -import { - StoreHandle, - type CommitChunk, - type RowRef, - type TransactionHandle, - type Value, -} from "@coln-project/runtime" -import { - defineDocumentType, - type DocumentType, - type DocumentTypeContext, - type SedimentreeMeta, -} from "@automerge/automerge-repo/slim" - -export type RowIdValue = Extract - -export interface ColnState { - store: StoreHandle - actor: string -} - -export type ColnFfi = { - schema: unknown - View: new (store: StoreHandle) => View - Transaction: new (store: StoreHandle, transaction: TransactionHandle) => Transaction -} - -export type AnyColnFfi = ColnFfi -export type ColnFfiView = Ffi extends ColnFfi ? View : never -export type ColnFfiTransaction = Ffi extends ColnFfi ? Transaction : never - -export type ColnDocument = { - realm: ColnFfiView - store: StoreHandle - heads: string[] -} - -export type ColnChange = (tx: ColnFfiTransaction) => void - -export type ColnDocType< - Ffi extends AnyColnFfi, - View = ColnDocument, - Change = ColnChange, - Init = undefined, -> = DocumentType - -export type ColnDocTypeOptions< - Ffi extends AnyColnFfi, - View = ColnDocument, - Change = ColnChange, - Init = undefined, -> = { - name?: string - view?: (doc: ColnDocument, state: ColnState) => View - change?: (tx: ColnFfiTransaction, change: Change, state: ColnState) => void - init?: (init: Init, tx: ColnFfiTransaction, state: ColnState) => void - hasData?: (state: ColnState) => boolean -} - -export function colnDocType< - Ffi extends AnyColnFfi, - View = ColnDocument, - Change = ColnChange, - Init = undefined, ->( - ffi: Ffi, - options: ColnDocTypeOptions = {} -): ColnDocType { - const schemaJson = JSON.stringify(ffi.schema) - const projectView = options.view ?? ((doc: ColnDocument) => doc as unknown as View) - const applyChange = - options.change ?? - ((tx: ColnFfiTransaction, change: Change) => { - ;(change as unknown as ColnChange)(tx) - }) - - const makeState = (ctx: DocumentTypeContext): ColnState => ({ - store: StoreHandle.fromTheory(schemaJson), - actor: ctx.peerId, - }) - - const makeDocument = (state: ColnState): ColnDocument => ({ - realm: new ffi.View(state.store), - store: state.store, - heads: state.store.heads(), - }) - - const runTransaction = ( - state: ColnState, - body: (tx: ColnFfiTransaction) => void - ): ColnState => { - const tx = state.store.beginTransaction() - try { - const typedTx = new ffi.Transaction(state.store, tx) - body(typedTx) - const result = tx.commit() - return { ...state, store: result.takeStore() } - } catch (error) { - try { - state.store = tx.takeStore() - } catch { - // ignore; preserve the original error - } - throw error - } - } - - return defineDocumentType({ - name: options.name ?? "coln", - empty: ctx => makeState(ctx), - init: (init, ctx) => { - const state = makeState(ctx) - return options.init - ? runTransaction(state, tx => options.init?.(init, tx, state)) - : state - }, - view: state => projectView(makeDocument(state), state), - change: (state, change) => - runTransaction(state, tx => applyChange(tx, change, state)), - heads: state => state.store.heads(), - hasData: state => options.hasData?.(state) ?? state.store.heads().length > 0, - sedimentree: { - metadata: state => chunks(state).map(chunkToMeta), - materialize: (state, metas) => { - const wanted = new Set(metas.map(meta => meta.head)) - return chunks(state) - .filter(chunk => wanted.has(chunk.hash)) - .map(chunk => ({ ...chunkToMeta(chunk), bytes: new Uint8Array(chunk.bytes) })) - }, - apply: (state, blobs) => { - if (blobs.length > 0) { - state.store.applyChunkBytes(blobs.map(blob => Array.from(blob))) - } - return state - }, - liveHashes: state => chunks(state).map(chunk => chunk.hash), - }, - }) -} - -export function rowValue(ref: RowRef | null): RowIdValue { - if (!ref) throw new Error("cannot convert a synthetic row to a row_id value") - return { tag: "row_id", value: ref } -} - -export function refId(ref: RowRef): string { - if ("existing" in ref) return `${ref.existing.commit}:${ref.existing.counter}` - return `pending:${ref.pending.txId}:${ref.pending.counter}` -} - -function chunks(state: ColnState): CommitChunk[] { - return state.store.commitChunksAfter([]) -} - -function chunkToMeta(chunk: CommitChunk): SedimentreeMeta { - return { kind: "commit", head: chunk.hash, parents: chunk.parents } -} diff --git a/examples/sync-demo/src/generated/Graph.ts b/examples/sync-demo/src/generated/Graph.ts deleted file mode 100644 index 3257e861..00000000 --- a/examples/sync-demo/src/generated/Graph.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as runtime from "@coln-project/runtime"; - -export interface View { - V: runtime.ColnSet.View; - E: (x: runtime.Value) => (x: runtime.Value) => runtime.ColnSet.View; -} - -export interface Transaction extends View { - V: runtime.ColnSet.Transaction; - E: (x: runtime.Value) => (x: runtime.Value) => runtime.ColnSet.Transaction; -} diff --git a/examples/sync-demo/src/generated/GraphRealm.json b/examples/sync-demo/src/generated/GraphRealm.json deleted file mode 100644 index 7ca1beae..00000000 --- a/examples/sync-demo/src/generated/GraphRealm.json +++ /dev/null @@ -1 +0,0 @@ -{"entities":[{"path":[["GraphRealm"],["E"]],"value":{"entityVariant":{"tag":"table"},"columns":[{"path":[["a"]],"type":{"tag":"rowId","path":[["GraphRealm"],["V"]]}},{"path":[["b"]],"type":{"tag":"rowId","path":[["GraphRealm"],["V"]]}}],"primaryKey":null}},{"path":[["GraphRealm"],["V"]],"value":{"entityVariant":{"tag":"table"},"columns":[],"primaryKey":null}}],"rules":[{"path":[["GraphRealm"],["E"],["foreignKey"]],"value":{"ruleVariant":"enforced","varNames":[[["a"]],[["b"]]],"varTypes":[{"tag":"rowId","path":[["GraphRealm"],["V"]]},{"tag":"rowId","path":[["GraphRealm"],["V"]]}],"antecedents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["E"]],"rowId":null,"values":[{"column":0,"term":{"tag":"var","index":0}},{"column":1,"term":{"tag":"var","index":1}}]}}],"consequents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":{"tag":"var","index":0},"values":[]}},{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":{"tag":"var","index":1},"values":[]}}]}},{"path":[["GraphRealm"],["V"],["foreignKey"]],"value":{"ruleVariant":"enforced","varNames":[],"varTypes":[],"antecedents":[{"tag":"atom","atom":{"entity":[["GraphRealm"],["V"]],"rowId":null,"values":[]}}],"consequents":[]}}]} \ No newline at end of file diff --git a/examples/sync-demo/src/generated/GraphRealm.ts b/examples/sync-demo/src/generated/GraphRealm.ts deleted file mode 100644 index e7a54556..00000000 --- a/examples/sync-demo/src/generated/GraphRealm.ts +++ /dev/null @@ -1,48 +0,0 @@ -import schema from "./GraphRealm.json"; -export {schema}; -import * as runtime from "@coln-project/runtime"; -import * as Graph from "./Graph.ts"; - -export class View { - root: Graph.View; - - constructor(store: runtime.StoreHandle) { - this.root = { - V: (new runtime.RowIdSet.View(store, "GraphRealm.V", [])), - E: (a: runtime.Value) => { - return (b: runtime.Value) => { - return (new runtime.RowIdSet.View(store, "GraphRealm.E", [a, b])); - }; - } - }; - } -} - -export class Transaction extends View { - root: Graph.Transaction; - - constructor( - store: runtime.StoreHandle, - transaction: runtime.TransactionHandle - ) { - super(store); - this.root = { - V: (new runtime.RowIdSet.Transaction( - store, - "GraphRealm.V", - [], - transaction - )), - E: (a: runtime.Value) => { - return (b: runtime.Value) => { - return (new runtime.RowIdSet.Transaction( - store, - "GraphRealm.E", - [a, b], - transaction - )); - }; - } - }; - } -} diff --git a/examples/sync-demo/src/main.tsx b/examples/sync-demo/src/main.tsx deleted file mode 100644 index 16931209..00000000 --- a/examples/sync-demo/src/main.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from "react" -import ReactDOM from "react-dom/client" -import { - Repo, - isValidAutomergeUrl, - type CrdtDocHandle, -} from "@automerge/automerge-repo" -// @ts-ignore initSync is exported at runtime but may be missing from declarations -import { initSync } from "@automerge/automerge-subduction/slim" -// @ts-ignore wasm-base64 may not have declarations -import { wasmBase64 } from "@automerge/automerge-subduction/wasm-base64" -import { App } from "./App" -import { colnDocType, type ColnDocType } from "./colnDocType" -import * as GraphRealm from "./generated/GraphRealm.ts" - -initSync({ module: Uint8Array.from(atob(wasmBase64), c => c.charCodeAt(0)) }) - -type GraphDocType = ColnDocType - -const coln = colnDocType(GraphRealm) -const repo = new Repo({ - subductionWebsocketEndpoints: ["wss://subduction.sync.inkandswitch.com"], -}) - -const hashUrl = location.hash.slice(1) -let handle: CrdtDocHandle - -if (isValidAutomergeUrl(hashUrl)) { - handle = await repo.find(hashUrl, coln) -} else { - handle = repo.create(undefined, coln) -} - -location.hash = handle.url - -Object.assign(window, { repo, handle }) - -ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - - -) diff --git a/examples/sync-demo/src/style.css b/examples/sync-demo/src/style.css deleted file mode 100644 index a1591adb..00000000 --- a/examples/sync-demo/src/style.css +++ /dev/null @@ -1,158 +0,0 @@ -:root { - color-scheme: light dark; - font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - line-height: 1.5; - font-weight: 400; - background: #111827; - color: #f9fafb; -} - -* { - box-sizing: border-box; -} - -body { - margin: 0; - min-width: 320px; - min-height: 100vh; -} - -button, -select, -input { - font: inherit; -} - -button { - border: 0; - border-radius: 10px; - padding: 0.6rem 0.9rem; - color: #08111f; - background: #67e8f9; - font-weight: 700; - cursor: pointer; -} - -button:hover { - background: #22d3ee; -} - -button:disabled { - cursor: not-allowed; - opacity: 0.5; -} - -select, -input { - border: 1px solid #334155; - border-radius: 10px; - padding: 0.55rem 0.7rem; - background: #020617; - color: #f9fafb; -} - -main { - width: min(1100px, calc(100vw - 32px)); - margin: 0 auto; - padding: 32px 0; -} - -header { - margin-bottom: 24px; -} - -h1 { - margin: 0 0 8px; - font-size: clamp(2rem, 4vw, 3.5rem); - line-height: 1; -} - -h2 { - margin: 0 0 12px; -} - -p { - color: #cbd5e1; -} - -code { - color: #a7f3d0; -} - -.url { - width: 100%; - margin-top: 8px; - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; -} - -.error { - margin: 16px 0; - padding: 12px 14px; - border: 1px solid #fca5a5; - border-radius: 12px; - color: #fecaca; - background: #450a0a; -} - -.toolbar { - display: flex; - flex-wrap: wrap; - align-items: end; - gap: 12px; - margin: 14px 0; - padding: 14px; - border: 1px solid #1f2937; - border-radius: 16px; - background: #0f172a; -} - -label { - display: grid; - gap: 4px; - color: #cbd5e1; - font-size: 0.9rem; -} - -.grid { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 16px; - margin-top: 18px; -} - -.panel { - min-height: 180px; - padding: 18px; - border: 1px solid #1f2937; - border-radius: 18px; - background: #0f172a; -} - -ul { - margin: 0; - padding-left: 1.25rem; -} - -li { - margin: 0.35rem 0; - overflow-wrap: anywhere; -} - -.muted { - color: #94a3b8; -} - -footer { - display: flex; - gap: 14px; - flex-wrap: wrap; - margin-top: 18px; - color: #94a3b8; - font-size: 0.9rem; -} - -@media (max-width: 800px) { - .grid { - grid-template-columns: 1fr; - } -} diff --git a/examples/sync-demo/test-results/.last-run.json b/examples/sync-demo/test-results/.last-run.json deleted file mode 100644 index cbcc1fba..00000000 --- a/examples/sync-demo/test-results/.last-run.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "status": "passed", - "failedTests": [] -} \ No newline at end of file diff --git a/examples/sync-demo/tests/sync.spec.ts b/examples/sync-demo/tests/sync.spec.ts deleted file mode 100644 index c9efc9f6..00000000 --- a/examples/sync-demo/tests/sync.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// -// SPDX-License-Identifier: Apache-2.0 OR MIT - -import { expect, test, type Page } from "@playwright/test" - -test("a tab opened after edits loads the coln graph", async ({ browser }) => { - const context = await browser.newContext() - const alice = await context.newPage() - - await alice.goto("/") - await expect(alice.getByTestId("doc-url")).toHaveValue(/^automerge:/) - await expect(alice.getByTestId("graph-item")).toHaveCount(1) - - await addTwoVerticesAndEdge(alice) - - const url = await alice.getByTestId("doc-url").inputValue() - const bob = await context.newPage() - await bob.goto(`/#${url}`) - - await expect(bob.getByTestId("doc-url")).toHaveValue(url) - await expect(bob.getByTestId("graph-item")).toHaveCount(1) - await expect(bob.getByTestId("vertex-item")).toHaveCount(2) - await expect(bob.getByTestId("edge-item")).toHaveCount(1) - - await bob.getByTestId("add-vertex").click() - await expect(bob.getByTestId("vertex-item")).toHaveCount(3) - await expect(alice.getByTestId("vertex-item")).toHaveCount(3) -}) - -test("a tab already open on the doc receives later coln edits", async ({ browser }) => { - const context = await browser.newContext() - const alice = await context.newPage() - - await alice.goto("/") - await expect(alice.getByTestId("doc-url")).toHaveValue(/^automerge:/) - await expect(alice.getByTestId("graph-item")).toHaveCount(1) - - const url = await alice.getByTestId("doc-url").inputValue() - const bob = await context.newPage() - await bob.goto(`/#${url}`) - await expect(bob.getByTestId("doc-url")).toHaveValue(url) - await expect(bob.getByTestId("graph-item")).toHaveCount(1) - - await addTwoVerticesAndEdge(alice) - - await expect(bob.getByTestId("vertex-item")).toHaveCount(2) - await expect(bob.getByTestId("edge-item")).toHaveCount(1) -}) - -async function addTwoVerticesAndEdge(page: Page) { - await page.getByTestId("add-vertex").click() - await page.getByTestId("add-vertex").click() - await expect(page.getByTestId("vertex-item")).toHaveCount(2) - - await selectByIndex(page, "from-select", 0) - await selectByIndex(page, "to-select", 1) - await page.getByTestId("add-edge").click() - await expect(page.getByTestId("edge-item")).toHaveCount(1) -} - -async function selectByIndex(page: Page, testId: string, index: number) { - const values = await page.getByTestId(testId).locator("option").evaluateAll(options => - options.map(option => (option as HTMLOptionElement).value) - ) - expect(values.length).toBeGreaterThan(index) - await page.getByTestId(testId).selectOption(values[index]) -} diff --git a/examples/sync-demo/tsconfig.json b/examples/sync-demo/tsconfig.json deleted file mode 100644 index 2609a539..00000000 --- a/examples/sync-demo/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "useDefineForClassFields": true, - "lib": ["DOM", "DOM.Iterable", "ES2022"], - "allowJs": false, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "ESNext", - "moduleResolution": "Bundler", - "resolveJsonModule": true, - "allowImportingTsExtensions": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - "types": ["vite/client"] - }, - "include": ["src"], - "references": [] -} diff --git a/examples/sync-demo/vendor/automerge-automerge-repo-2.6.0-subduction.33.tgz b/examples/sync-demo/vendor/automerge-automerge-repo-2.6.0-subduction.33.tgz deleted file mode 100644 index 325ccbee3904244863bbe7ef0b233715286fd4d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 329865 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 -``` - -Omit the URL to create a document in the CLI. Keep the CLI open while opening -its displayed URL in the browser because the bundled relay stores documents in -memory. - -The CLI controls are: - -- `v`: add vertex -- `e`: add edge using the selected source and target -- `Tab`: switch the active endpoint -- `Up`/`Down`: select a vertex for the active endpoint -- `/`: enter a TypeScript query callback body -- `q`: quit - -## TypeScript queries - -The query prompt accepts the body of a synchronous `handle.change` callback. -TypeScript syntax is transpiled but not semantically type-checked before it -runs; invalid transaction operations are reported as runtime errors. -For example: - -```ts -tx.root.V.add() -``` - -Or using the lower-level transaction API: - -```ts -tx.add("GraphRealm.V", []) -``` - -Queries execute as trusted local code and have the same access to the process as -other dynamically evaluated JavaScript. Do not run untrusted input. - -Query compilation and execution live in `src/query/execute-query.ts`. That -module intentionally has no graph, Ink, Svelte, or synchronization imports. Its -public surface only depends on a target with a `change` method, allowing it to be -moved into a future CLI package and replaced here with a package import. - -## Schema - -The fixed schema is `graph.coln`. Regenerate its TypeScript bindings and JSON IR -with: - -```bash -pnpm --dir examples/sync compile -``` - -## Verification - -```bash -pnpm --dir examples/sync build -pnpm --dir examples/sync exec playwright install chromium # first time only -pnpm --dir examples/sync test:e2e -``` - -The end-to-end scenarios exercise browser mutations, Node-side TypeScript query -execution, synchronization in both directions, loading the resulting graph in a -second browser, and writing durable browser data to IndexedDB. diff --git a/examples/sync/graph.coln b/examples/sync/graph.coln deleted file mode 100644 index 04a0be4c..00000000 --- a/examples/sync/graph.coln +++ /dev/null @@ -1,7 +0,0 @@ -theory Graph := sig - V : Set - E : V -> V -> Set -end - -realm GraphRealm @ Graph -end diff --git a/examples/sync/index.html b/examples/sync/index.html deleted file mode 100644 index 57e78f4d..00000000 --- a/examples/sync/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - Coln Graph Sync - - -

- - - diff --git a/examples/sync/package.json b/examples/sync/package.json deleted file mode 100644 index b3e96cb8..00000000 --- a/examples/sync/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "coln-sync", - "private": true, - "type": "module", - "license": "(Apache-2.0 OR MIT)", - "scripts": { - "dev": "coln-with-sync-server vite --host 127.0.0.1", - "cli": "coln-with-sync-server tsx src/cli/main.tsx", - "server": "coln-sync-server", - "compile": "bash ./compile.sh", - "check": "svelte-check --tsconfig ./tsconfig.json && tsc --noEmit", - "build": "pnpm run compile && pnpm run check && vite build", - "test": "vitest run", - "test:e2e": "playwright test" - }, - "dependencies": { - "@automerge/automerge-repo": "github:automerge/automerge-repo#doctypes&path:packages/automerge-repo", - "@automerge/automerge-repo-storage-indexeddb": "2.6.0-subduction.47", - "@automerge/automerge-subduction": "0.16.1", - "@coln-project/repo": "link:../../packages/coln-repo", - "@coln-project/runtime": "link:../../packages/coln-js-runtime", - "ink": "^6.0.0", - "ink-text-input": "^6.0.0", - "react": "^19.0.0", - "svelte": "^5.7.0", - "typescript": "^6.0.0" - }, - "devDependencies": { - "@coln-project/demo-sync-server": "file:../sync-server", - "@playwright/test": "^1.61.0", - "@sveltejs/vite-plugin-svelte": "^7.0.0", - "@tailwindcss/vite": "^4.0.0", - "@types/node": "^26.0.0", - "@types/react": "^19.0.0", - "@vitest/browser-playwright": "^4.1.11", - "svelte-check": "^4.0.0", - "tailwindcss": "^4.0.0", - "tsx": "^4.23.0", - "vite": "^8.0.0", - "vite-plugin-wasm": "^3.6.0", - "vitest": "^4.1.11" - } -} diff --git a/examples/sync/playwright.config.ts b/examples/sync/playwright.config.ts deleted file mode 100644 index 4b69ea4e..00000000 --- a/examples/sync/playwright.config.ts +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// -// SPDX-License-Identifier: Apache-2.0 OR MIT - -import { defineConfig, devices } from "@playwright/test" - -export default defineConfig({ - testDir: "./tests", - timeout: 30_000, - expect: { timeout: 10_000 }, - fullyParallel: false, - retries: 0, - use: { - baseURL: "http://127.0.0.1:5176", - trace: "retain-on-failure", - }, - webServer: [ - { - command: "SUBDUCTION_SERVICE_NAME=127.0.0.1:3031 PORT=3031 coln-sync-server", - port: 3031, - reuseExistingServer: false, - stdout: "pipe", - stderr: "pipe", - }, - { - command: "VITE_SUBDUCTION_ENDPOINT=ws://127.0.0.1:3031 ./node_modules/.bin/vite --host 127.0.0.1 --port 5176", - url: "http://127.0.0.1:5176", - reuseExistingServer: false, - stdout: "pipe", - stderr: "pipe", - }, - ], - projects: [ - { - name: "chromium", - use: { ...devices["Desktop Chrome"] }, - }, - ], -}) diff --git a/examples/sync/pnpm-lock.yaml b/examples/sync/pnpm-lock.yaml deleted file mode 100644 index da9ad9dc..00000000 --- a/examples/sync/pnpm-lock.yaml +++ /dev/null @@ -1,2339 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -overrides: - '@automerge/automerge': 3.3.2 - '@automerge/automerge-subduction': 0.16.1 - -importers: - - .: - dependencies: - '@automerge/automerge-repo': - specifier: github:automerge/automerge-repo#doctypes&path:packages/automerge-repo - version: https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo - '@automerge/automerge-repo-storage-indexeddb': - specifier: 2.6.0-subduction.47 - version: 2.6.0-subduction.47 - '@automerge/automerge-subduction': - specifier: 0.16.1 - version: 0.16.1 - '@coln-project/repo': - specifier: link:../../packages/coln-repo - version: link:../../packages/coln-repo - '@coln-project/runtime': - specifier: link:../../packages/coln-js-runtime - version: link:../../packages/coln-js-runtime - ink: - specifier: ^6.0.0 - version: 6.8.0(@types/react@19.2.18)(react@19.2.8) - ink-text-input: - specifier: ^6.0.0 - version: 6.0.0(ink@6.8.0(@types/react@19.2.18)(react@19.2.8))(react@19.2.8) - react: - specifier: ^19.0.0 - version: 19.2.8 - svelte: - specifier: ^5.7.0 - version: 5.56.10 - typescript: - specifier: ^6.0.0 - version: 6.0.3 - devDependencies: - '@coln-project/demo-sync-server': - specifier: file:../sync-server - version: file:../sync-server - '@playwright/test': - specifier: ^1.61.0 - version: 1.62.1 - '@sveltejs/vite-plugin-svelte': - specifier: ^7.0.0 - version: 7.3.0(svelte@5.56.10)(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)) - '@tailwindcss/vite': - specifier: ^4.0.0 - version: 4.3.3(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)) - '@types/node': - specifier: ^26.0.0 - version: 26.3.0 - '@types/react': - specifier: ^19.0.0 - version: 19.2.18 - '@vitest/browser-playwright': - specifier: ^4.1.11 - version: 4.1.11(playwright@1.62.1)(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12))(vitest@4.1.11) - svelte-check: - specifier: ^4.0.0 - version: 4.7.6(picomatch@4.0.7)(svelte@5.56.10)(typescript@6.0.3) - tailwindcss: - specifier: ^4.0.0 - version: 4.3.3 - tsx: - specifier: ^4.23.0 - version: 4.23.12 - vite: - specifier: ^8.0.0 - version: 8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12) - vite-plugin-wasm: - specifier: ^3.6.0 - version: 3.6.0(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)) - vitest: - specifier: ^4.1.11 - version: 4.1.11(@types/node@26.3.0)(@vitest/browser-playwright@4.1.11)(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)) - -packages: - - '@alcalzone/ansi-tokenize@0.2.5': - resolution: {integrity: sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw==} - engines: {node: '>=18'} - - '@automerge/automerge-repo-storage-indexeddb@2.6.0-subduction.47': - resolution: {integrity: sha512-fYn6nse7jQnVb9EploTsBuYUzW7xFin2HZLneSEQ7w4pRCilIU92ghZEjZSWRKEPunSN0MYEvfJJJdlLyYTdkQ==} - engines: {node: '>=22.13'} - - '@automerge/automerge-repo@2.6.0-subduction.47': - resolution: {integrity: sha512-NGBQUjGH67Kyrc8a6zoafvDKT1+pnFi2/yiMgULS7l+apWQT2QkgQZ3vQGms/ngdrmM6ye1fhcwHwBn/DGJ6tA==} - engines: {node: '>=22.13'} - - '@automerge/automerge-repo@https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo': - resolution: {gitHosted: true, path: packages/automerge-repo, tarball: https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5} - version: 2.6.0-subduction.47 - engines: {node: '>=22.13'} - - '@automerge/automerge-subduction@0.16.1': - resolution: {integrity: sha512-alH7U4eYn0O7sT6hNLv7CJhNODJi+QD6qfRQTNfZCxL3Q5JZdJ8lFpdvXUkBf+I3bu0GLvVlTaYPWGrBihjpIw==} - - '@automerge/automerge@3.3.2': - resolution: {integrity: sha512-9vCdCL7pdQwUra66SBxPVHr+/t9epXKni9KDeak2rNBFMzABVh2u6gSpcwxi3jkR5qr047jVqZv9DlrOfVxLFw==} - - '@blazediff/core@1.9.1': - resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==} - - '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': - resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} - cpu: [arm64] - os: [darwin] - - '@cbor-extract/cbor-extract-darwin-x64@2.2.2': - resolution: {integrity: sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==} - cpu: [x64] - os: [darwin] - - '@cbor-extract/cbor-extract-linux-arm64@2.2.2': - resolution: {integrity: sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==} - cpu: [arm64] - os: [linux] - - '@cbor-extract/cbor-extract-linux-arm@2.2.2': - resolution: {integrity: sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==} - cpu: [arm] - os: [linux] - - '@cbor-extract/cbor-extract-linux-x64@2.2.2': - resolution: {integrity: sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==} - cpu: [x64] - os: [linux] - - '@cbor-extract/cbor-extract-win32-x64@2.2.2': - resolution: {integrity: sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==} - cpu: [x64] - os: [win32] - - '@coln-project/demo-sync-server@file:../sync-server': - resolution: {directory: ../sync-server, type: directory} - hasBin: true - - '@esbuild/aix-ppc64@0.28.2': - resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.28.2': - resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.28.2': - resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.28.2': - resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.28.2': - resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.28.2': - resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.28.2': - resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.28.2': - resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.28.2': - resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.28.2': - resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.28.2': - resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.28.2': - resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.28.2': - resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.28.2': - resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.28.2': - resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.28.2': - resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.28.2': - resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.28.2': - resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.28.2': - resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.28.2': - resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.28.2': - resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.28.2': - resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.28.2': - resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.28.2': - resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.28.2': - resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.28.2': - resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - - '@noble/hashes@1.8.0': - resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} - engines: {node: ^14.21.3 || >=16} - - '@oxc-project/types@0.146.0': - resolution: {integrity: sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==} - - '@playwright/test@1.62.1': - resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} - engines: {node: '>=20'} - hasBin: true - - '@polka/url@1.0.0-next.29': - resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - - '@rolldown/binding-android-arm-eabi@1.2.5': - resolution: {integrity: sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [android] - - '@rolldown/binding-android-arm64@1.2.5': - resolution: {integrity: sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@rolldown/binding-darwin-arm64@1.2.5': - resolution: {integrity: sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.2.5': - resolution: {integrity: sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@rolldown/binding-freebsd-x64@1.2.5': - resolution: {integrity: sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-linux-arm-gnueabihf@1.2.5': - resolution: {integrity: sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.2.5': - resolution: {integrity: sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-arm64-musl@1.2.5': - resolution: {integrity: sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rolldown/binding-linux-ppc64-gnu@1.2.5': - resolution: {integrity: sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-s390x-gnu@1.2.5': - resolution: {integrity: sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-gnu@1.2.5': - resolution: {integrity: sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-musl@1.2.5': - resolution: {integrity: sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rolldown/binding-openharmony-arm64@1.2.5': - resolution: {integrity: sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@rolldown/binding-win32-arm64-msvc@1.2.5': - resolution: {integrity: sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.2.5': - resolution: {integrity: sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@rolldown/pluginutils@1.0.1': - resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - - '@sveltejs/acorn-typescript@1.0.13': - resolution: {integrity: sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==} - peerDependencies: - acorn: ^8.9.0 - - '@sveltejs/load-config@0.2.3': - resolution: {integrity: sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ==} - engines: {node: '>= 18.0.0'} - - '@sveltejs/vite-plugin-svelte@7.3.0': - resolution: {integrity: sha512-QbRoJyD92e9R0ufeQIWRHrCC0ObcqSv/aBDdrQMoU+sypav3cDx5wytdQ6GLdXjEMO6xjrXGzfkUygng8JMv0A==} - engines: {node: ^20.19 || ^22.12 || >=24} - peerDependencies: - svelte: ^5.46.4 - vite: ^8.0.0-beta.7 || ^8.0.0 - - '@tailwindcss/node@4.3.3': - resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} - - '@tailwindcss/oxide-android-arm64@4.3.3': - resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [android] - - '@tailwindcss/oxide-darwin-arm64@4.3.3': - resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [darwin] - - '@tailwindcss/oxide-darwin-x64@4.3.3': - resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} - engines: {node: '>= 20'} - cpu: [x64] - os: [darwin] - - '@tailwindcss/oxide-freebsd-x64@4.3.3': - resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} - engines: {node: '>= 20'} - cpu: [x64] - os: [freebsd] - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': - resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} - engines: {node: '>= 20'} - cpu: [arm] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': - resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@tailwindcss/oxide-linux-arm64-musl@4.3.3': - resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@tailwindcss/oxide-linux-x64-gnu@4.3.3': - resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@tailwindcss/oxide-linux-x64-musl@4.3.3': - resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@tailwindcss/oxide-wasm32-wasi@4.3.3': - resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - bundledDependencies: - - '@napi-rs/wasm-runtime' - - '@emnapi/core' - - '@emnapi/runtime' - - '@tybys/wasm-util' - - '@emnapi/wasi-threads' - - tslib - - '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': - resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [win32] - - '@tailwindcss/oxide-win32-x64-msvc@4.3.3': - resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} - engines: {node: '>= 20'} - cpu: [x64] - os: [win32] - - '@tailwindcss/oxide@4.3.3': - resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} - engines: {node: '>= 20'} - - '@tailwindcss/vite@4.3.3': - resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} - peerDependencies: - vite: ^5.2.0 || ^6 || ^7 || ^8 - - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - - '@types/estree@1.0.9': - resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - - '@types/node@26.3.0': - resolution: {integrity: sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw==} - - '@types/react@19.2.18': - resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} - - '@types/trusted-types@2.0.7': - resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - - '@vitest/browser-playwright@4.1.11': - resolution: {integrity: sha512-riLBxPqwnJ0lWs2DN2WeUfYeKLoAjbP2Xx8cLQdSddzMi20sksIa6K2mPz79DyMZKKVKH2ksOC2yJvtNcZg8cg==} - peerDependencies: - playwright: '*' - vitest: 4.1.11 - - '@vitest/browser@4.1.11': - resolution: {integrity: sha512-bwMovvAeuTFOK5kIFevw4VEf+1gVEICv4SYK4k3knJOxl6b1zEWud8mYKD73e1B0odAn174h1MofURy2TPWf3w==} - peerDependencies: - vitest: 4.1.11 - - '@vitest/expect@4.1.11': - resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} - - '@vitest/mocker@4.1.11': - resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} - peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/pretty-format@4.1.11': - resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} - - '@vitest/runner@4.1.11': - resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} - - '@vitest/snapshot@4.1.11': - resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} - - '@vitest/spy@4.1.11': - resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} - - '@vitest/utils@4.1.11': - resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} - - acorn@8.18.0: - resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} - engines: {node: '>=0.4.0'} - hasBin: true - - ansi-escapes@7.3.0: - resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} - engines: {node: '>=18'} - - ansi-regex@6.3.0: - resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} - engines: {node: '>=12'} - - ansi-styles@6.2.3: - resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} - engines: {node: '>=12'} - - aria-query@5.3.1: - resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} - engines: {node: '>= 0.4'} - - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - - auto-bind@5.0.1: - resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - axobject-query@4.1.0: - resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} - engines: {node: '>= 0.4'} - - base-x@5.0.1: - resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} - - bs58@6.0.0: - resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} - - bs58check@4.0.0: - resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} - - cbor-extract@2.2.2: - resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} - hasBin: true - - cbor-x@1.6.6: - resolution: {integrity: sha512-8QiD9PGOxyQHo7s2pzwTBH6lTjqekxPdl9Aq6fXvZgCuCJHOht1puDEA/fTr6mciB76c+M+Gi0qT2i1a4pm4Wg==} - - chai@6.2.2: - resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} - engines: {node: '>=18'} - - chalk@5.6.2: - resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} - - cli-boxes@3.0.0: - resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} - engines: {node: '>=10'} - - cli-cursor@4.0.0: - resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - cli-truncate@5.2.0: - resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} - engines: {node: '>=20'} - - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} - - code-excerpt@4.0.0: - resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - convert-to-spaces@2.0.1: - resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - devalue@5.9.1: - resolution: {integrity: sha512-+17vil3EVQRzvtDJSFuTWEb8XJRvXqAiV3qZyQWD398QeXUa6CxsUyMdD1fxzEhUrd4FojitFz7lhIHBTlV4fw==} - - emoji-regex@10.6.0: - resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} - - enhanced-resolve@5.24.5: - resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} - engines: {node: '>=10.13.0'} - - environment@1.1.0: - resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} - engines: {node: '>=18'} - - es-module-lexer@2.3.2: - resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} - - es-toolkit@1.51.0: - resolution: {integrity: sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw==} - - esbuild@0.28.2: - resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} - engines: {node: '>=18'} - hasBin: true - - escape-string-regexp@2.0.0: - resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} - engines: {node: '>=8'} - - esm-env@1.2.2: - resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} - - esrap@2.3.6: - resolution: {integrity: sha512-yc0OC12UjPqLoc+fe+v5GNs4TOjAigUw3sTikfC+xeBPGUw7gDRz3DtYaqEhxyMVJojcSWJw7jT0QWR+CbuE/A==} - peerDependencies: - '@typescript-eslint/types': ^8.2.0 - peerDependenciesMeta: - '@typescript-eslint/types': - optional: true - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - eventemitter3@5.0.4: - resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} - - expect-type@1.4.0: - resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} - engines: {node: '>=12.0.0'} - - fast-sha256@1.3.0: - resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - get-east-asian-width@1.6.0: - resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} - engines: {node: '>=18'} - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - indent-string@5.0.0: - resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} - engines: {node: '>=12'} - - ink-text-input@6.0.0: - resolution: {integrity: sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw==} - engines: {node: '>=18'} - peerDependencies: - ink: '>=5' - react: '>=18' - - ink@6.8.0: - resolution: {integrity: sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA==} - engines: {node: '>=20'} - peerDependencies: - '@types/react': '>=19.0.0' - react: '>=19.0.0' - react-devtools-core: '>=6.1.2' - peerDependenciesMeta: - '@types/react': - optional: true - react-devtools-core: - optional: true - - is-fullwidth-code-point@5.1.0: - resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} - engines: {node: '>=18'} - - is-in-ci@2.0.0: - resolution: {integrity: sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==} - engines: {node: '>=20'} - hasBin: true - - is-reference@3.0.3: - resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} - - isomorphic-ws@5.0.0: - resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} - peerDependencies: - ws: '*' - - jiti@2.7.0: - resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} - hasBin: true - - lightningcss-android-arm64@1.32.0: - resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-android-arm64@1.33.0: - resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-darwin-arm64@1.32.0: - resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-arm64@1.33.0: - resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.32.0: - resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-darwin-x64@1.33.0: - resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.32.0: - resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-freebsd-x64@1.33.0: - resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm-gnueabihf@1.33.0: - resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.32.0: - resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - lightningcss-linux-arm64-gnu@1.33.0: - resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - lightningcss-linux-arm64-musl@1.32.0: - resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - lightningcss-linux-arm64-musl@1.33.0: - resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - lightningcss-linux-x64-gnu@1.32.0: - resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - lightningcss-linux-x64-gnu@1.33.0: - resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - lightningcss-linux-x64-musl@1.32.0: - resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - lightningcss-linux-x64-musl@1.33.0: - resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - lightningcss-win32-arm64-msvc@1.32.0: - resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-arm64-msvc@1.33.0: - resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.32.0: - resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss-win32-x64-msvc@1.33.0: - resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.32.0: - resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} - engines: {node: '>= 12.0.0'} - - lightningcss@1.33.0: - resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} - engines: {node: '>= 12.0.0'} - - locate-character@3.0.0: - resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - magic-string@1.2.2: - resolution: {integrity: sha512-veT/+7iXrXzT39XnEN4lOxtNl72dMgJ8Lp+5Bd6YcMSWpb0n0MjBM8Uuooi6jgJr8dhUW2swQgBmoZVMni5SVg==} - - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - - mri@1.2.0: - resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} - engines: {node: '>=4'} - - mrmime@2.0.1: - resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} - engines: {node: '>=10'} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - nanoid@3.3.18: - resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - node-gyp-build-optional-packages@5.1.1: - resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} - hasBin: true - - obug@2.1.4: - resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} - engines: {node: '>=12.20.0'} - - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - - patch-console@2.0.0: - resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@4.0.7: - resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} - engines: {node: '>=12'} - - playwright-core@1.62.1: - resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} - engines: {node: '>=20'} - hasBin: true - - playwright@1.62.1: - resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} - engines: {node: '>=20'} - hasBin: true - - pngjs@7.0.0: - resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} - engines: {node: '>=14.19.0'} - - postcss@8.5.26: - resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} - engines: {node: ^10 || ^12 || >=14} - - react-reconciler@0.33.0: - resolution: {integrity: sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==} - engines: {node: '>=0.10.0'} - peerDependencies: - react: ^19.2.0 - - react@19.2.8: - resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} - engines: {node: '>=0.10.0'} - - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} - - restore-cursor@4.0.0: - resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - rolldown@1.2.5: - resolution: {integrity: sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - sade@1.8.1: - resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} - engines: {node: '>=6'} - - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - - sirv@3.0.2: - resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} - engines: {node: '>=18'} - - slice-ansi@8.0.0: - resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} - engines: {node: '>=20'} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - stack-utils@2.0.6: - resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} - engines: {node: '>=10'} - - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - - std-env@4.2.0: - resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} - - string-width@7.2.0: - resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} - engines: {node: '>=18'} - - string-width@8.2.2: - resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==} - engines: {node: '>=20'} - - strip-ansi@7.2.0: - resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} - engines: {node: '>=12'} - - svelte-check@4.7.6: - resolution: {integrity: sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw==} - engines: {node: '>= 18.0.0'} - hasBin: true - peerDependencies: - svelte: ^4.0.0 || ^5.0.0-next.0 - typescript: ^5.0.0 || ^6.0.0 - - svelte@5.56.10: - resolution: {integrity: sha512-Lcxbj8I/KAbpY+VjtY4ENQBV0dDCipfGAhqb51XQZ67CIQqXgsv/8dPkbILaj4Fb6/b6JAEM/PIVbILXgDQy2g==} - engines: {node: '>=18'} - - tagged-tag@1.0.0: - resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} - engines: {node: '>=20'} - - tailwindcss@4.3.3: - resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} - - tapable@2.3.3: - resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} - engines: {node: '>=6'} - - terminal-size@4.0.1: - resolution: {integrity: sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==} - engines: {node: '>=18'} - - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - - tinyexec@1.3.0: - resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} - engines: {node: '>=18'} - - tinyglobby@0.2.17: - resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} - engines: {node: '>=12.0.0'} - - tinyrainbow@3.1.1: - resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} - engines: {node: '>=14.0.0'} - - totalist@3.0.1: - resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} - engines: {node: '>=6'} - - tsx@4.23.12: - resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==} - engines: {node: '>=18.0.0'} - hasBin: true - - type-fest@4.41.0: - resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} - engines: {node: '>=16'} - - type-fest@5.8.0: - resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} - engines: {node: '>=20'} - - typescript@6.0.3: - resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} - engines: {node: '>=14.17'} - hasBin: true - - undici-types@8.3.0: - resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} - - uuid@14.0.2: - resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} - hasBin: true - - vite-plugin-wasm@3.6.0: - resolution: {integrity: sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==} - peerDependencies: - vite: ^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - - vite@8.2.2: - resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.4.0 || ^0.5.0 - esbuild: ^0.27.0 || ^0.28.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vitefu@1.1.3: - resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} - peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - vite: - optional: true - - vitest@4.1.11: - resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.11 - '@vitest/browser-preview': 4.1.11 - '@vitest/browser-webdriverio': 4.1.11 - '@vitest/coverage-istanbul': 4.1.11 - '@vitest/coverage-v8': 4.1.11 - '@vitest/ui': 4.1.11 - happy-dom: '*' - jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@opentelemetry/api': - optional: true - '@types/node': - optional: true - '@vitest/browser-playwright': - optional: true - '@vitest/browser-preview': - optional: true - '@vitest/browser-webdriverio': - optional: true - '@vitest/coverage-istanbul': - optional: true - '@vitest/coverage-v8': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - - widest-line@6.0.0: - resolution: {integrity: sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==} - engines: {node: '>=20'} - - wrap-ansi@9.0.2: - resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} - engines: {node: '>=18'} - - ws@8.21.3: - resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - xstate@5.32.5: - resolution: {integrity: sha512-ULazi1oe6wGrXl0Frb6otSlkm5HLifbbVTkMk5kkSKqz4TkxJaVpnl6jOJwKeid3ORPxYyZQgNLUSYX9q65SIA==} - - yoga-layout@3.2.1: - resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} - - zimmerframe@1.1.4: - resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} - -snapshots: - - '@alcalzone/ansi-tokenize@0.2.5': - dependencies: - ansi-styles: 6.2.3 - is-fullwidth-code-point: 5.1.0 - - '@automerge/automerge-repo-storage-indexeddb@2.6.0-subduction.47': - dependencies: - '@automerge/automerge-repo': 2.6.0-subduction.47 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - '@automerge/automerge-repo@2.6.0-subduction.47': - dependencies: - '@automerge/automerge': 3.3.2 - '@automerge/automerge-subduction': 0.16.1 - bs58check: 4.0.0 - cbor-x: 1.6.6 - debug: 4.4.3 - eventemitter3: 5.0.4 - fast-sha256: 1.3.0 - isomorphic-ws: 5.0.0(ws@8.21.3) - uuid: 14.0.2 - ws: 8.21.3 - xstate: 5.32.5 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - '@automerge/automerge-repo@https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo': - dependencies: - '@automerge/automerge': 3.3.2 - '@automerge/automerge-subduction': 0.16.1 - bs58check: 4.0.0 - cbor-x: 1.6.6 - debug: 4.4.3 - eventemitter3: 5.0.4 - fast-sha256: 1.3.0 - isomorphic-ws: 5.0.0(ws@8.21.3) - uuid: 14.0.2 - ws: 8.21.3 - xstate: 5.32.5 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - '@automerge/automerge-subduction@0.16.1': {} - - '@automerge/automerge@3.3.2': {} - - '@blazediff/core@1.9.1': {} - - '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-darwin-x64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-linux-arm64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-linux-arm@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-linux-x64@2.2.2': - optional: true - - '@cbor-extract/cbor-extract-win32-x64@2.2.2': - optional: true - - '@coln-project/demo-sync-server@file:../sync-server': - dependencies: - '@automerge/automerge-subduction': 0.16.1 - ws: 8.21.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@esbuild/aix-ppc64@0.28.2': - optional: true - - '@esbuild/android-arm64@0.28.2': - optional: true - - '@esbuild/android-arm@0.28.2': - optional: true - - '@esbuild/android-x64@0.28.2': - optional: true - - '@esbuild/darwin-arm64@0.28.2': - optional: true - - '@esbuild/darwin-x64@0.28.2': - optional: true - - '@esbuild/freebsd-arm64@0.28.2': - optional: true - - '@esbuild/freebsd-x64@0.28.2': - optional: true - - '@esbuild/linux-arm64@0.28.2': - optional: true - - '@esbuild/linux-arm@0.28.2': - optional: true - - '@esbuild/linux-ia32@0.28.2': - optional: true - - '@esbuild/linux-loong64@0.28.2': - optional: true - - '@esbuild/linux-mips64el@0.28.2': - optional: true - - '@esbuild/linux-ppc64@0.28.2': - optional: true - - '@esbuild/linux-riscv64@0.28.2': - optional: true - - '@esbuild/linux-s390x@0.28.2': - optional: true - - '@esbuild/linux-x64@0.28.2': - optional: true - - '@esbuild/netbsd-arm64@0.28.2': - optional: true - - '@esbuild/netbsd-x64@0.28.2': - optional: true - - '@esbuild/openbsd-arm64@0.28.2': - optional: true - - '@esbuild/openbsd-x64@0.28.2': - optional: true - - '@esbuild/openharmony-arm64@0.28.2': - optional: true - - '@esbuild/sunos-x64@0.28.2': - optional: true - - '@esbuild/win32-arm64@0.28.2': - optional: true - - '@esbuild/win32-ia32@0.28.2': - optional: true - - '@esbuild/win32-x64@0.28.2': - optional: true - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@noble/hashes@1.8.0': {} - - '@oxc-project/types@0.146.0': {} - - '@playwright/test@1.62.1': - dependencies: - playwright: 1.62.1 - - '@polka/url@1.0.0-next.29': {} - - '@rolldown/binding-android-arm-eabi@1.2.5': - optional: true - - '@rolldown/binding-android-arm64@1.2.5': - optional: true - - '@rolldown/binding-darwin-arm64@1.2.5': - optional: true - - '@rolldown/binding-darwin-x64@1.2.5': - optional: true - - '@rolldown/binding-freebsd-x64@1.2.5': - optional: true - - '@rolldown/binding-linux-arm-gnueabihf@1.2.5': - optional: true - - '@rolldown/binding-linux-arm64-gnu@1.2.5': - optional: true - - '@rolldown/binding-linux-arm64-musl@1.2.5': - optional: true - - '@rolldown/binding-linux-ppc64-gnu@1.2.5': - optional: true - - '@rolldown/binding-linux-s390x-gnu@1.2.5': - optional: true - - '@rolldown/binding-linux-x64-gnu@1.2.5': - optional: true - - '@rolldown/binding-linux-x64-musl@1.2.5': - optional: true - - '@rolldown/binding-openharmony-arm64@1.2.5': - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.2.5': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.2.5': - optional: true - - '@rolldown/pluginutils@1.0.1': {} - - '@standard-schema/spec@1.1.0': {} - - '@sveltejs/acorn-typescript@1.0.13(acorn@8.18.0)': - dependencies: - acorn: 8.18.0 - - '@sveltejs/load-config@0.2.3': {} - - '@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10)(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12))': - dependencies: - deepmerge: 4.3.1 - magic-string: 1.2.2 - obug: 2.1.4 - svelte: 5.56.10 - vite: 8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12) - vitefu: 1.1.3(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)) - - '@tailwindcss/node@4.3.3': - dependencies: - '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.24.5 - jiti: 2.7.0 - lightningcss: 1.32.0 - magic-string: 0.30.21 - source-map-js: 1.2.1 - tailwindcss: 4.3.3 - - '@tailwindcss/oxide-android-arm64@4.3.3': - optional: true - - '@tailwindcss/oxide-darwin-arm64@4.3.3': - optional: true - - '@tailwindcss/oxide-darwin-x64@4.3.3': - optional: true - - '@tailwindcss/oxide-freebsd-x64@4.3.3': - optional: true - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': - optional: true - - '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': - optional: true - - '@tailwindcss/oxide-linux-arm64-musl@4.3.3': - optional: true - - '@tailwindcss/oxide-linux-x64-gnu@4.3.3': - optional: true - - '@tailwindcss/oxide-linux-x64-musl@4.3.3': - optional: true - - '@tailwindcss/oxide-wasm32-wasi@4.3.3': - optional: true - - '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': - optional: true - - '@tailwindcss/oxide-win32-x64-msvc@4.3.3': - optional: true - - '@tailwindcss/oxide@4.3.3': - optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.3.3 - '@tailwindcss/oxide-darwin-arm64': 4.3.3 - '@tailwindcss/oxide-darwin-x64': 4.3.3 - '@tailwindcss/oxide-freebsd-x64': 4.3.3 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 - '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 - '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 - '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 - '@tailwindcss/oxide-linux-x64-musl': 4.3.3 - '@tailwindcss/oxide-wasm32-wasi': 4.3.3 - '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 - '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - - '@tailwindcss/vite@4.3.3(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12))': - dependencies: - '@tailwindcss/node': 4.3.3 - '@tailwindcss/oxide': 4.3.3 - tailwindcss: 4.3.3 - vite: 8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12) - - '@types/chai@5.2.3': - dependencies: - '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 - - '@types/deep-eql@4.0.2': {} - - '@types/estree@1.0.9': {} - - '@types/node@26.3.0': - dependencies: - undici-types: 8.3.0 - - '@types/react@19.2.18': - dependencies: - csstype: 3.2.3 - - '@types/trusted-types@2.0.7': {} - - '@vitest/browser-playwright@4.1.11(playwright@1.62.1)(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12))(vitest@4.1.11)': - dependencies: - '@vitest/browser': 4.1.11(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12))(vitest@4.1.11) - '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)) - playwright: 1.62.1 - tinyrainbow: 3.1.1 - vitest: 4.1.11(@types/node@26.3.0)(@vitest/browser-playwright@4.1.11)(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)) - transitivePeerDependencies: - - bufferutil - - msw - - utf-8-validate - - vite - - '@vitest/browser@4.1.11(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12))(vitest@4.1.11)': - dependencies: - '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)) - '@vitest/utils': 4.1.11 - magic-string: 0.30.21 - pngjs: 7.0.0 - sirv: 3.0.2 - tinyrainbow: 3.1.1 - vitest: 4.1.11(@types/node@26.3.0)(@vitest/browser-playwright@4.1.11)(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)) - ws: 8.21.3 - transitivePeerDependencies: - - bufferutil - - msw - - utf-8-validate - - vite - - '@vitest/expect@4.1.11': - dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 - chai: 6.2.2 - tinyrainbow: 3.1.1 - - '@vitest/mocker@4.1.11(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12))': - dependencies: - '@vitest/spy': 4.1.11 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12) - - '@vitest/pretty-format@4.1.11': - dependencies: - tinyrainbow: 3.1.1 - - '@vitest/runner@4.1.11': - dependencies: - '@vitest/utils': 4.1.11 - pathe: 2.0.3 - - '@vitest/snapshot@4.1.11': - dependencies: - '@vitest/pretty-format': 4.1.11 - '@vitest/utils': 4.1.11 - magic-string: 0.30.21 - pathe: 2.0.3 - - '@vitest/spy@4.1.11': {} - - '@vitest/utils@4.1.11': - dependencies: - '@vitest/pretty-format': 4.1.11 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.1 - - acorn@8.18.0: {} - - ansi-escapes@7.3.0: - dependencies: - environment: 1.1.0 - - ansi-regex@6.3.0: {} - - ansi-styles@6.2.3: {} - - aria-query@5.3.1: {} - - assertion-error@2.0.1: {} - - auto-bind@5.0.1: {} - - axobject-query@4.1.0: {} - - base-x@5.0.1: {} - - bs58@6.0.0: - dependencies: - base-x: 5.0.1 - - bs58check@4.0.0: - dependencies: - '@noble/hashes': 1.8.0 - bs58: 6.0.0 - - cbor-extract@2.2.2: - dependencies: - node-gyp-build-optional-packages: 5.1.1 - optionalDependencies: - '@cbor-extract/cbor-extract-darwin-arm64': 2.2.2 - '@cbor-extract/cbor-extract-darwin-x64': 2.2.2 - '@cbor-extract/cbor-extract-linux-arm': 2.2.2 - '@cbor-extract/cbor-extract-linux-arm64': 2.2.2 - '@cbor-extract/cbor-extract-linux-x64': 2.2.2 - '@cbor-extract/cbor-extract-win32-x64': 2.2.2 - optional: true - - cbor-x@1.6.6: - optionalDependencies: - cbor-extract: 2.2.2 - - chai@6.2.2: {} - - chalk@5.6.2: {} - - chokidar@4.0.3: - dependencies: - readdirp: 4.1.2 - - cli-boxes@3.0.0: {} - - cli-cursor@4.0.0: - dependencies: - restore-cursor: 4.0.0 - - cli-truncate@5.2.0: - dependencies: - slice-ansi: 8.0.0 - string-width: 8.2.2 - - clsx@2.1.1: {} - - code-excerpt@4.0.0: - dependencies: - convert-to-spaces: 2.0.1 - - convert-source-map@2.0.0: {} - - convert-to-spaces@2.0.1: {} - - csstype@3.2.3: {} - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - deepmerge@4.3.1: {} - - detect-libc@2.1.2: {} - - devalue@5.9.1: {} - - emoji-regex@10.6.0: {} - - enhanced-resolve@5.24.5: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.3 - - environment@1.1.0: {} - - es-module-lexer@2.3.2: {} - - es-toolkit@1.51.0: {} - - esbuild@0.28.2: - optionalDependencies: - '@esbuild/aix-ppc64': 0.28.2 - '@esbuild/android-arm': 0.28.2 - '@esbuild/android-arm64': 0.28.2 - '@esbuild/android-x64': 0.28.2 - '@esbuild/darwin-arm64': 0.28.2 - '@esbuild/darwin-x64': 0.28.2 - '@esbuild/freebsd-arm64': 0.28.2 - '@esbuild/freebsd-x64': 0.28.2 - '@esbuild/linux-arm': 0.28.2 - '@esbuild/linux-arm64': 0.28.2 - '@esbuild/linux-ia32': 0.28.2 - '@esbuild/linux-loong64': 0.28.2 - '@esbuild/linux-mips64el': 0.28.2 - '@esbuild/linux-ppc64': 0.28.2 - '@esbuild/linux-riscv64': 0.28.2 - '@esbuild/linux-s390x': 0.28.2 - '@esbuild/linux-x64': 0.28.2 - '@esbuild/netbsd-arm64': 0.28.2 - '@esbuild/netbsd-x64': 0.28.2 - '@esbuild/openbsd-arm64': 0.28.2 - '@esbuild/openbsd-x64': 0.28.2 - '@esbuild/openharmony-arm64': 0.28.2 - '@esbuild/sunos-x64': 0.28.2 - '@esbuild/win32-arm64': 0.28.2 - '@esbuild/win32-ia32': 0.28.2 - '@esbuild/win32-x64': 0.28.2 - - escape-string-regexp@2.0.0: {} - - esm-env@1.2.2: {} - - esrap@2.3.6: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.9 - - eventemitter3@5.0.4: {} - - expect-type@1.4.0: {} - - fast-sha256@1.3.0: {} - - fdir@6.5.0(picomatch@4.0.7): - optionalDependencies: - picomatch: 4.0.7 - - fsevents@2.3.2: - optional: true - - fsevents@2.3.3: - optional: true - - get-east-asian-width@1.6.0: {} - - graceful-fs@4.2.11: {} - - indent-string@5.0.0: {} - - ink-text-input@6.0.0(ink@6.8.0(@types/react@19.2.18)(react@19.2.8))(react@19.2.8): - dependencies: - chalk: 5.6.2 - ink: 6.8.0(@types/react@19.2.18)(react@19.2.8) - react: 19.2.8 - type-fest: 4.41.0 - - ink@6.8.0(@types/react@19.2.18)(react@19.2.8): - dependencies: - '@alcalzone/ansi-tokenize': 0.2.5 - ansi-escapes: 7.3.0 - ansi-styles: 6.2.3 - auto-bind: 5.0.1 - chalk: 5.6.2 - cli-boxes: 3.0.0 - cli-cursor: 4.0.0 - cli-truncate: 5.2.0 - code-excerpt: 4.0.0 - es-toolkit: 1.51.0 - indent-string: 5.0.0 - is-in-ci: 2.0.0 - patch-console: 2.0.0 - react: 19.2.8 - react-reconciler: 0.33.0(react@19.2.8) - scheduler: 0.27.0 - signal-exit: 3.0.7 - slice-ansi: 8.0.0 - stack-utils: 2.0.6 - string-width: 8.2.2 - terminal-size: 4.0.1 - type-fest: 5.8.0 - widest-line: 6.0.0 - wrap-ansi: 9.0.2 - ws: 8.21.3 - yoga-layout: 3.2.1 - optionalDependencies: - '@types/react': 19.2.18 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - is-fullwidth-code-point@5.1.0: - dependencies: - get-east-asian-width: 1.6.0 - - is-in-ci@2.0.0: {} - - is-reference@3.0.3: - dependencies: - '@types/estree': 1.0.9 - - isomorphic-ws@5.0.0(ws@8.21.3): - dependencies: - ws: 8.21.3 - - jiti@2.7.0: {} - - lightningcss-android-arm64@1.32.0: - optional: true - - lightningcss-android-arm64@1.33.0: - optional: true - - lightningcss-darwin-arm64@1.32.0: - optional: true - - lightningcss-darwin-arm64@1.33.0: - optional: true - - lightningcss-darwin-x64@1.32.0: - optional: true - - lightningcss-darwin-x64@1.33.0: - optional: true - - lightningcss-freebsd-x64@1.32.0: - optional: true - - lightningcss-freebsd-x64@1.33.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.32.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.33.0: - optional: true - - lightningcss-linux-arm64-gnu@1.32.0: - optional: true - - lightningcss-linux-arm64-gnu@1.33.0: - optional: true - - lightningcss-linux-arm64-musl@1.32.0: - optional: true - - lightningcss-linux-arm64-musl@1.33.0: - optional: true - - lightningcss-linux-x64-gnu@1.32.0: - optional: true - - lightningcss-linux-x64-gnu@1.33.0: - optional: true - - lightningcss-linux-x64-musl@1.32.0: - optional: true - - lightningcss-linux-x64-musl@1.33.0: - optional: true - - lightningcss-win32-arm64-msvc@1.32.0: - optional: true - - lightningcss-win32-arm64-msvc@1.33.0: - optional: true - - lightningcss-win32-x64-msvc@1.32.0: - optional: true - - lightningcss-win32-x64-msvc@1.33.0: - optional: true - - lightningcss@1.32.0: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.32.0 - lightningcss-darwin-arm64: 1.32.0 - lightningcss-darwin-x64: 1.32.0 - lightningcss-freebsd-x64: 1.32.0 - lightningcss-linux-arm-gnueabihf: 1.32.0 - lightningcss-linux-arm64-gnu: 1.32.0 - lightningcss-linux-arm64-musl: 1.32.0 - lightningcss-linux-x64-gnu: 1.32.0 - lightningcss-linux-x64-musl: 1.32.0 - lightningcss-win32-arm64-msvc: 1.32.0 - lightningcss-win32-x64-msvc: 1.32.0 - - lightningcss@1.33.0: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.33.0 - lightningcss-darwin-arm64: 1.33.0 - lightningcss-darwin-x64: 1.33.0 - lightningcss-freebsd-x64: 1.33.0 - lightningcss-linux-arm-gnueabihf: 1.33.0 - lightningcss-linux-arm64-gnu: 1.33.0 - lightningcss-linux-arm64-musl: 1.33.0 - lightningcss-linux-x64-gnu: 1.33.0 - lightningcss-linux-x64-musl: 1.33.0 - lightningcss-win32-arm64-msvc: 1.33.0 - lightningcss-win32-x64-msvc: 1.33.0 - - locate-character@3.0.0: {} - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - magic-string@1.2.2: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - mimic-fn@2.1.0: {} - - mri@1.2.0: {} - - mrmime@2.0.1: {} - - ms@2.1.3: {} - - nanoid@3.3.18: {} - - node-gyp-build-optional-packages@5.1.1: - dependencies: - detect-libc: 2.1.2 - optional: true - - obug@2.1.4: {} - - onetime@5.1.2: - dependencies: - mimic-fn: 2.1.0 - - patch-console@2.0.0: {} - - pathe@2.0.3: {} - - picocolors@1.1.1: {} - - picomatch@4.0.7: {} - - playwright-core@1.62.1: {} - - playwright@1.62.1: - dependencies: - playwright-core: 1.62.1 - optionalDependencies: - fsevents: 2.3.2 - - pngjs@7.0.0: {} - - postcss@8.5.26: - dependencies: - nanoid: 3.3.18 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - react-reconciler@0.33.0(react@19.2.8): - dependencies: - react: 19.2.8 - scheduler: 0.27.0 - - react@19.2.8: {} - - readdirp@4.1.2: {} - - restore-cursor@4.0.0: - dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 - - rolldown@1.2.5: - dependencies: - '@oxc-project/types': 0.146.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm-eabi': 1.2.5 - '@rolldown/binding-android-arm64': 1.2.5 - '@rolldown/binding-darwin-arm64': 1.2.5 - '@rolldown/binding-darwin-x64': 1.2.5 - '@rolldown/binding-freebsd-x64': 1.2.5 - '@rolldown/binding-linux-arm-gnueabihf': 1.2.5 - '@rolldown/binding-linux-arm64-gnu': 1.2.5 - '@rolldown/binding-linux-arm64-musl': 1.2.5 - '@rolldown/binding-linux-ppc64-gnu': 1.2.5 - '@rolldown/binding-linux-s390x-gnu': 1.2.5 - '@rolldown/binding-linux-x64-gnu': 1.2.5 - '@rolldown/binding-linux-x64-musl': 1.2.5 - '@rolldown/binding-openharmony-arm64': 1.2.5 - '@rolldown/binding-win32-arm64-msvc': 1.2.5 - '@rolldown/binding-win32-x64-msvc': 1.2.5 - - sade@1.8.1: - dependencies: - mri: 1.2.0 - - scheduler@0.27.0: {} - - siginfo@2.0.0: {} - - signal-exit@3.0.7: {} - - sirv@3.0.2: - dependencies: - '@polka/url': 1.0.0-next.29 - mrmime: 2.0.1 - totalist: 3.0.1 - - slice-ansi@8.0.0: - dependencies: - ansi-styles: 6.2.3 - is-fullwidth-code-point: 5.1.0 - - source-map-js@1.2.1: {} - - stack-utils@2.0.6: - dependencies: - escape-string-regexp: 2.0.0 - - stackback@0.0.2: {} - - std-env@4.2.0: {} - - string-width@7.2.0: - dependencies: - emoji-regex: 10.6.0 - get-east-asian-width: 1.6.0 - strip-ansi: 7.2.0 - - string-width@8.2.2: - dependencies: - get-east-asian-width: 1.6.0 - strip-ansi: 7.2.0 - - strip-ansi@7.2.0: - dependencies: - ansi-regex: 6.3.0 - - svelte-check@4.7.6(picomatch@4.0.7)(svelte@5.56.10)(typescript@6.0.3): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - '@sveltejs/load-config': 0.2.3 - chokidar: 4.0.3 - fdir: 6.5.0(picomatch@4.0.7) - picocolors: 1.1.1 - sade: 1.8.1 - svelte: 5.56.10 - typescript: 6.0.3 - transitivePeerDependencies: - - picomatch - - svelte@5.56.10: - dependencies: - '@jridgewell/remapping': 2.3.5 - '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.13(acorn@8.18.0) - '@types/estree': 1.0.9 - '@types/trusted-types': 2.0.7 - acorn: 8.18.0 - aria-query: 5.3.1 - axobject-query: 4.1.0 - clsx: 2.1.1 - devalue: 5.9.1 - esm-env: 1.2.2 - esrap: 2.3.6 - is-reference: 3.0.3 - locate-character: 3.0.0 - magic-string: 0.30.21 - zimmerframe: 1.1.4 - transitivePeerDependencies: - - '@typescript-eslint/types' - - tagged-tag@1.0.0: {} - - tailwindcss@4.3.3: {} - - tapable@2.3.3: {} - - terminal-size@4.0.1: {} - - tinybench@2.9.0: {} - - tinyexec@1.3.0: {} - - tinyglobby@0.2.17: - dependencies: - fdir: 6.5.0(picomatch@4.0.7) - picomatch: 4.0.7 - - tinyrainbow@3.1.1: {} - - totalist@3.0.1: {} - - tsx@4.23.12: - dependencies: - esbuild: 0.28.2 - optionalDependencies: - fsevents: 2.3.3 - - type-fest@4.41.0: {} - - type-fest@5.8.0: - dependencies: - tagged-tag: 1.0.0 - - typescript@6.0.3: {} - - undici-types@8.3.0: {} - - uuid@14.0.2: {} - - vite-plugin-wasm@3.6.0(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)): - dependencies: - vite: 8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12) - - vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12): - dependencies: - lightningcss: 1.33.0 - picomatch: 4.0.7 - postcss: 8.5.26 - rolldown: 1.2.5 - tinyglobby: 0.2.17 - optionalDependencies: - '@types/node': 26.3.0 - esbuild: 0.28.2 - fsevents: 2.3.3 - jiti: 2.7.0 - tsx: 4.23.12 - - vitefu@1.1.3(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)): - optionalDependencies: - vite: 8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12) - - vitest@4.1.11(@types/node@26.3.0)(@vitest/browser-playwright@4.1.11)(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)): - dependencies: - '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12)) - '@vitest/pretty-format': 4.1.11 - '@vitest/runner': 4.1.11 - '@vitest/snapshot': 4.1.11 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 - es-module-lexer: 2.3.2 - expect-type: 1.4.0 - magic-string: 0.30.21 - obug: 2.1.4 - pathe: 2.0.3 - picomatch: 4.0.7 - std-env: 4.2.0 - tinybench: 2.9.0 - tinyexec: 1.3.0 - tinyglobby: 0.2.17 - tinyrainbow: 3.1.1 - vite: 8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 26.3.0 - '@vitest/browser-playwright': 4.1.11(playwright@1.62.1)(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.12))(vitest@4.1.11) - transitivePeerDependencies: - - msw - - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - - widest-line@6.0.0: - dependencies: - string-width: 8.2.2 - - wrap-ansi@9.0.2: - dependencies: - ansi-styles: 6.2.3 - string-width: 7.2.0 - strip-ansi: 7.2.0 - - ws@8.21.3: {} - - xstate@5.32.5: {} - - yoga-layout@3.2.1: {} - - zimmerframe@1.1.4: {} diff --git a/examples/sync/pnpm-workspace.yaml b/examples/sync/pnpm-workspace.yaml deleted file mode 100644 index 62604741..00000000 --- a/examples/sync/pnpm-workspace.yaml +++ /dev/null @@ -1,7 +0,0 @@ -allowBuilds: - cbor-extract: false - esbuild: true - '@automerge/automerge-repo@git+https://github.com/automerge/automerge-repo.git': true -overrides: - "@automerge/automerge": 3.3.2 - "@automerge/automerge-subduction": 0.16.1 diff --git a/examples/sync/src/browser/App.svelte b/examples/sync/src/browser/App.svelte deleted file mode 100644 index f88e51ea..00000000 --- a/examples/sync/src/browser/App.svelte +++ /dev/null @@ -1,165 +0,0 @@ - - - - - - - - - -
- {#if feedback} - {#key feedback.id} -

- {feedback.message} -

- {/key} - {/if} - -
- - C - COLN / GRAPH LAB - -
-
- {syncStatus === "error" ? "sync error" : syncStatus} -
-
- {graph.heads.length} {graph.heads.length === 1 ? "head" : "heads"} - {endpoint} -
-
-
- -
- selectedEdgeId = id} - onselectvertex={selectVertex} - /> - fromId = id} - ontochange={id => toId = id} - /> -
-
diff --git a/examples/sync/src/browser/DocumentLoadError.svelte b/examples/sync/src/browser/DocumentLoadError.svelte deleted file mode 100644 index 84a0390c..00000000 --- a/examples/sync/src/browser/DocumentLoadError.svelte +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - {title} | Coln Graph Lab - - - -
-
-
- C - COLN / GRAPH LAB -
- -
-

{title.toUpperCase()}

-

{heading}

-

{detail}

-
- - {documentUrl} - - - Create a new graph + - -
-
diff --git a/examples/sync/src/browser/main.ts b/examples/sync/src/browser/main.ts deleted file mode 100644 index f0d24ad8..00000000 --- a/examples/sync/src/browser/main.ts +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// -// SPDX-License-Identifier: Apache-2.0 OR MIT - -import { Repo, isValidAutomergeUrl } from "@automerge/automerge-repo" -import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb" -import { create, find } from "@coln-project/repo" -// @ts-expect-error initSync is exported at runtime but absent from declarations -import { initSync } from "@automerge/automerge-subduction/slim" -// @ts-expect-error wasm-base64 is exported without declarations -import { wasmBase64 } from "@automerge/automerge-subduction/wasm-base64" -import { mount } from "svelte" -import * as GraphRealm from "../generated/GraphRealm.ts" -import App from "./App.svelte" -import DocumentLoadError from "./DocumentLoadError.svelte" -import "./style.css" - -initSync({ module: Uint8Array.from(atob(wasmBase64), char => char.charCodeAt(0)) }) - -const endpoint = import.meta.env.VITE_SUBDUCTION_ENDPOINT - ?? "wss://subduction.sync.inkandswitch.com" -const hashUrl = location.hash.slice(1) -const documentUrl = isValidAutomergeUrl(hashUrl) ? hashUrl : undefined -const target = document.getElementById("app")! - -if (hashUrl && !documentUrl) { - mount(DocumentLoadError, { - target, - props: { documentUrl: hashUrl, kind: "invalid" }, - }) -} else { - const repo = new Repo({ - storage: new IndexedDBStorageAdapter("coln-sync"), - subductionWebsocketEndpoints: [endpoint], - }) - - try { - const handle = documentUrl - ? await find(repo, documentUrl, GraphRealm) - : create(repo, GraphRealm) - - location.hash = handle.url - if (import.meta.env.DEV) Object.assign(window, { repo, handle }) - - mount(App, { - target, - props: { handle, repo, endpoint }, - }) - } catch (cause) { - await repo.shutdown().catch(() => undefined) - if (!isDocumentUnavailable(cause)) throw cause - - mount(DocumentLoadError, { - target, - props: { documentUrl: hashUrl, kind: "unavailable" }, - }) - } -} - -function isDocumentUnavailable(cause: unknown): boolean { - return cause instanceof Error && /^Document .+ is unavailable$/.test(cause.message) -} diff --git a/examples/sync/src/browser/style.css b/examples/sync/src/browser/style.css deleted file mode 100644 index c05c0d43..00000000 --- a/examples/sync/src/browser/style.css +++ /dev/null @@ -1,5 +0,0 @@ -/* SPDX-FileCopyrightText: 2026 Coln contributors */ -/* SPDX-License-Identifier: Apache-2.0 OR MIT */ - -@import url("https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;600;700&display=swap"); -@import "tailwindcss"; diff --git a/examples/sync/src/cli/App.tsx b/examples/sync/src/cli/App.tsx deleted file mode 100644 index 371fd2c4..00000000 --- a/examples/sync/src/cli/App.tsx +++ /dev/null @@ -1,219 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// -// SPDX-License-Identifier: Apache-2.0 OR MIT - -import type { Repo } from "@automerge/automerge-repo" -import { Box, Text, useInput, useStdout } from "ink" -import TextInput from "ink-text-input" -import { useEffect, useRef, useState } from "react" -import { - addEdge, - addVertex, - readGraph, - watchGraph, - type GraphHandle, -} from "../lib/graph.ts" -import { executeQuery, QueryError } from "../query/execute-query.ts" - -type Props = { - handle: GraphHandle - repo: Repo - endpoint: string - onQuit: () => void -} - -type Endpoint = "source" | "target" - -export default function App({ handle, repo, endpoint, onQuit }: Props) { - const { stdout } = useStdout() - const [graph, setGraph] = useState(() => readGraph(handle.doc())) - const [sourceIndex, setSourceIndex] = useState(0) - const [targetIndex, setTargetIndex] = useState(0) - const [activeEndpoint, setActiveEndpoint] = useState("source") - const [queryMode, setQueryMode] = useState(false) - const [query, setQuery] = useState("") - const [history, setHistory] = useState([]) - const [historyIndex, setHistoryIndex] = useState(0) - const [message, setMessage] = useState("Ready") - const [connected, setConnected] = useState(repo.isSubductionConnected()) - const [quitting, setQuitting] = useState(false) - const [terminalHeight, setTerminalHeight] = useState(stdout.rows || 24) - const quittingRef = useRef(false) - - useEffect(() => { - const resize = () => setTerminalHeight(stdout.rows || 24) - stdout.on("resize", resize) - return () => { - stdout.off("resize", resize) - } - }, [stdout]) - useEffect(() => watchGraph(handle, setGraph), [handle]) - useEffect(() => { - const update = ({ connected }: { connected: boolean }) => setConnected(connected) - repo.on("subduction-connection", update) - return () => { - repo.off("subduction-connection", update) - } - }, [repo]) - useEffect(() => { - const last = Math.max(0, graph.vertices.length - 1) - setSourceIndex(index => Math.min(index, last)) - setTargetIndex(index => Math.min(index, last)) - }, [graph.vertices.length]) - useEffect(() => { - if (quitting) onQuit() - }, [onQuit, quitting]) - - useInput((input, key) => { - if (quittingRef.current) return - if (key.ctrl && input === "c") { - quit() - return - } - if (queryMode) { - if (key.escape) { - setQueryMode(false) - setMessage("Query cancelled") - } else if (key.upArrow && history.length > 0) { - const next = Math.max(0, historyIndex - 1) - setHistoryIndex(next) - setQuery(history[next] ?? "") - } else if (key.downArrow && history.length > 0) { - const next = Math.min(history.length, historyIndex + 1) - setHistoryIndex(next) - setQuery(history[next] ?? "") - } - return - } - - if (input === "q") quit() - else if (input === "v") run("Vertex added", () => addVertex(handle)) - else if (input === "e") createEdge() - else if (input === "/") { - setQueryMode(true) - setQuery("") - setHistoryIndex(history.length) - setMessage("Enter a TypeScript change callback body") - } else if (key.tab) { - setActiveEndpoint(value => value === "source" ? "target" : "source") - } else if (key.upArrow) { - moveSelection(-1) - } else if (key.downArrow) { - moveSelection(1) - } - }) - - const source = graph.vertices[sourceIndex] - const target = graph.vertices[targetIndex] - - function quit() { - if (quittingRef.current) return - quittingRef.current = true - setQuitting(true) - } - - function moveSelection(delta: number) { - if (graph.vertices.length === 0) return - const move = (index: number) => - (index + delta + graph.vertices.length) % graph.vertices.length - if (activeEndpoint === "source") setSourceIndex(move) - else setTargetIndex(move) - } - - function createEdge() { - if (!source || !target) { - setMessage("Add a vertex before drawing an edge") - return - } - run(`${source.label} -> ${target.label} added`, () => addEdge(handle, source, target)) - } - - function submitQuery(source: string) { - if (!source.trim()) { - setQueryMode(false) - return - } - try { - executeQuery(handle, source) - setHistory(values => [...values, source]) - setHistoryIndex(history.length + 1) - setMessage("Query committed") - setQuery("") - setQueryMode(false) - } catch (cause) { - const prefix = cause instanceof QueryError ? cause.phase : "query" - setMessage(`${prefix}: ${cause instanceof Error ? cause.message : String(cause)}`) - } - } - - function run(success: string, action: () => void) { - try { - action() - setMessage(success) - } catch (cause) { - setMessage(cause instanceof Error ? cause.message : String(cause)) - } - } - - const names = new Map(graph.vertices.map(vertex => [vertex.id, vertex.label])) - - return ( - - - COLN / GRAPH CLI - - {quitting ? "◌ shutting down" : connected ? "● relay online" : "○ connecting"} - - - - - DOCUMENT - {handle.url} - {endpoint} · {graph.heads.length} synchronized head{graph.heads.length === 1 ? "" : "s"} - - - - - VERTICES {graph.vertices.length} - {graph.vertices.length === 0 - ? (empty) - : graph.vertices.map((vertex, index) => ( - - - {index === sourceIndex ? "S" : " "}{index === targetIndex ? "T" : " "} {vertex.label} - - {vertex.id.slice(0, 8)} - - ))} - - - - EDGES {graph.edges.length} - {graph.edges.length === 0 - ? (empty) - : graph.edges.map(edge => ( - {names.get(edge.fromId)} → {names.get(edge.toId)} - ))} - - - - - - Endpoint: {activeEndpoint} - {" "}S: {source?.label ?? "-"} → T: {target?.label ?? "-"} - - {quitting ? ( - Syncing and quitting… - ) : queryMode ? ( - - change(txn) › - - - ) : ( - v add vertex · e add edge · tab switch endpoint · ↑/↓ select · / TypeScript · q quit - )} - {!quitting && {message}} - - - ) -} diff --git a/examples/sync/src/cli/main.tsx b/examples/sync/src/cli/main.tsx deleted file mode 100644 index 40401017..00000000 --- a/examples/sync/src/cli/main.tsx +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// -// SPDX-License-Identifier: Apache-2.0 OR MIT - -import { - Repo, - initSubduction, - isValidAutomergeUrl, - type AutomergeUrl, -} from "@automerge/automerge-repo" -import { create, find } from "@coln-project/repo" -import { render } from "ink" -import * as GraphRealm from "../generated/GraphRealm.ts" -import App from "./App.tsx" - -const endpoint = process.env.SUBDUCTION_ENDPOINT - ?? "wss://subduction.sync.inkandswitch.com" -const url = process.argv[2] - -if (url && !isValidAutomergeUrl(url)) { - console.error("Expected an automerge URL or no argument to create a document.") - process.exit(1) -} -if (!process.stdin.isTTY || !process.stdout.isTTY) { - console.error("The sync CLI requires an interactive terminal.") - process.exit(1) -} - -await initSubduction() -const repo = new Repo({ - subductionWebsocketEndpoints: [endpoint], - subductionTimeouts: { syncMs: 5_000, defaultMs: 5_000 }, -}) -await waitForSubduction(repo, endpoint) -const handle = url - ? await find(repo, url as AutomergeUrl, GraphRealm) - : create(repo, GraphRealm) - -let requestQuit!: () => void -const quitRequested = new Promise(resolve => { - requestQuit = resolve -}) - -let instance: ReturnType | undefined -let alternateScreenActive = true -const signalHandlers = new Map void>() - -process.stdout.write("\u001B[?1049h") -for (const signal of ["SIGHUP", "SIGINT", "SIGTERM"] as const) { - const handler = () => { - removeSignalHandlers() - instance?.unmount() - leaveAlternateScreen() - process.kill(process.pid, signal) - } - signalHandlers.set(signal, handler) - process.once(signal, handler) -} - -try { - instance = render( - , - { exitOnCtrlC: false }, - ) - try { - await Promise.race([quitRequested, instance.waitUntilExit()]) - } finally { - await repo.shutdown() - } -} finally { - removeSignalHandlers() - instance?.unmount() - leaveAlternateScreen() -} - -function removeSignalHandlers(): void { - for (const [signal, handler] of signalHandlers) { - process.off(signal, handler) - } - signalHandlers.clear() -} - -function leaveAlternateScreen(): void { - if (!alternateScreenActive) return - alternateScreenActive = false - process.stdout.write("\u001B[?1049l") -} - -async function waitForSubduction(repo: Repo, endpoint: string): Promise { - if (repo.isSubductionConnected()) return - - await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - cleanup() - reject(new Error(`Timed out connecting to ${endpoint}`)) - }, 10_000) - const connected = ({ connected }: { connected: boolean }) => { - if (!connected) return - cleanup() - resolve() - } - const cleanup = () => { - clearTimeout(timeout) - repo.off("subduction-connection", connected) - } - - repo.on("subduction-connection", connected) - if (repo.isSubductionConnected()) { - cleanup() - resolve() - } - }) -} diff --git a/examples/sync/src/lib/subduction-sync.svelte.ts b/examples/sync/src/lib/subduction-sync.svelte.ts deleted file mode 100644 index 99fc4442..00000000 --- a/examples/sync/src/lib/subduction-sync.svelte.ts +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// -// SPDX-License-Identifier: Apache-2.0 OR MIT - -import type { Repo } from "@automerge/automerge-repo"; -import type { ColnHandle, RealmBindings } from "@coln-project/repo"; -import { createSubscriber } from "svelte/reactivity"; - -export type SubductionSyncStatus = "offline" | "syncing" | "synced" | "error"; - -export class SubductionSync { - readonly #subscribe: () => void; - #status: SubductionSyncStatus; - #error: unknown; - - constructor( - private readonly repo: Repo, - private readonly handle: ColnHandle, - ) { - this.#status = repo.isSubductionConnected() ? "syncing" : "offline"; - this.#subscribe = createSubscriber((update) => { - let active = true; - let generation = 0; - let currentAttempt: symbol | undefined; - - const setStatus = (status: SubductionSyncStatus, error?: unknown) => { - if (this.#status === status && this.#error === error) return; - this.#status = status; - this.#error = error; - update(); - }; - - const finishFlush = ( - attempt: symbol, - flushGeneration: number, - failed: boolean, - error?: unknown, - ) => { - if (!active || currentAttempt !== attempt) return; - currentAttempt = undefined; - if (!this.repo.isSubductionConnected()) { - setStatus("offline"); - } else if (flushGeneration !== generation) { - startFlush(); - } else if (failed) { - setStatus("error", error); - } else { - setStatus("synced"); - } - }; - - const startFlush = () => { - if (currentAttempt || !active || !this.repo.isSubductionConnected()) return; - const attempt = Symbol(); - const flushGeneration = generation; - currentAttempt = attempt; - void this.repo.flush([this.handle.documentId]).then( - () => finishFlush(attempt, flushGeneration, false), - error => finishFlush(attempt, flushGeneration, true, error), - ); - }; - - const requestFlush = () => { - generation += 1; - if (!this.repo.isSubductionConnected()) { - currentAttempt = undefined; - setStatus("offline"); - return; - } - setStatus("syncing"); - startFlush(); - }; - - const connectionChanged = () => { - requestFlush(); - }; - - this.handle.on("change", requestFlush); - this.repo.on("subduction-connection", connectionChanged); - requestFlush(); - - return () => { - active = false; - generation += 1; - currentAttempt = undefined; - this.handle.off("change", requestFlush); - this.repo.off("subduction-connection", connectionChanged); - }; - }); - } - - get connected(): boolean { - this.#subscribe(); - return this.repo.isSubductionConnected(); - } - - get status(): SubductionSyncStatus { - this.#subscribe(); - return this.#status; - } - - get error(): unknown { - this.#subscribe(); - return this.#error; - } -} diff --git a/examples/sync/src/query/execute-query.ts b/examples/sync/src/query/execute-query.ts deleted file mode 100644 index 887259d0..00000000 --- a/examples/sync/src/query/execute-query.ts +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// -// SPDX-License-Identifier: Apache-2.0 OR MIT - -import ts from "typescript" - -export type ChangeTarget = { - change(callback: (transaction: Transaction) => void): void -} - -export class QueryError extends Error { - constructor( - readonly phase: "compile" | "runtime", - message: string, - options?: ErrorOptions, - ) { - super(message, options) - this.name = "QueryError" - } -} - -export function compileQuery(source: string): (txn: Transaction) => unknown { - const result = ts.transpileModule(`const __colnQuery = (txn) => {\n${source}\n}`, { - compilerOptions: { - target: ts.ScriptTarget.ES2022, - module: ts.ModuleKind.ESNext, - }, - fileName: "query.ts", - reportDiagnostics: true, - }) - const diagnostics = result.diagnostics?.filter( - diagnostic => diagnostic.category === ts.DiagnosticCategory.Error, - ) ?? [] - - if (diagnostics.length > 0) { - throw new QueryError("compile", ts.formatDiagnostics(diagnostics, { - getCanonicalFileName: fileName => fileName, - getCurrentDirectory: () => "", - getNewLine: () => "\n", - }).trim()) - } - - try { - const factory = new Function(`${result.outputText}\nreturn __colnQuery`) - return factory() as (txn: Transaction) => unknown - } catch (cause) { - throw new QueryError("compile", errorMessage(cause), { cause }) - } -} - -export function executeQuery( - target: ChangeTarget, - source: string, -): void { - const query = compileQuery(source) - try { - target.change(transaction => { - const result = query(transaction) - if (isThenable(result)) { - throw new TypeError("Queries must complete synchronously") - } - }) - } catch (cause) { - throw new QueryError("runtime", errorMessage(cause), { cause }) - } -} - -function isThenable(value: unknown): value is PromiseLike { - return typeof value === "object" - && value !== null - && "then" in value - && typeof value.then === "function" -} - -function errorMessage(cause: unknown): string { - return cause instanceof Error ? cause.message : String(cause) -} diff --git a/examples/sync/tests/sync.spec.ts b/examples/sync/tests/sync.spec.ts deleted file mode 100644 index 660c5176..00000000 --- a/examples/sync/tests/sync.spec.ts +++ /dev/null @@ -1,202 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// -// SPDX-License-Identifier: Apache-2.0 OR MIT - -import { expect, test } from "@playwright/test" -import { - initSubduction, - Repo, - type AutomergeUrl, -} from "@automerge/automerge-repo" -import { colnDocType } from "@coln-project/repo" -import { executeQuery } from "../src/query/execute-query.ts" - -test.beforeAll(async () => { - await initSubduction() -}) - -test("browser controls and TypeScript queries converge", async ({ browser }) => { - test.setTimeout(60_000) - const context = await browser.newContext() - await context.grantPermissions(["clipboard-read", "clipboard-write"], { - origin: "http://127.0.0.1:5176", - }) - const page = await context.newPage() - const nodeRepo = new Repo({ - subductionWebsocketEndpoints: ["ws://127.0.0.1:3031"], - }) - - try { - await page.goto("/") - await expect(page.getByTestId("sync-status")).toHaveAttribute("data-status", "synced") - await expect(page.getByTestId("head-count")).toHaveText("1 head") - await expect(page.getByTestId("subduction-url")).toHaveText("ws://127.0.0.1:3031") - await expect(page.getByTestId("doc-url")).toHaveValue(/^automerge:/) - await page.getByTestId("copy-document-url").click() - await expect(page.getByTestId("feedback")).toHaveText("URL copied") - - const url = await page.getByTestId("doc-url").inputValue() as AutomergeUrl - const nodeHandle = await nodeRepo.find(url, colnDocType) - - await page.getByTestId("add-vertex").click() - await expect(page.getByTestId("feedback")).toHaveText("Vertex added") - const firstVertexFeedbackId = await page.getByTestId("feedback").getAttribute("data-feedback-id") - await page.getByTestId("add-vertex").click() - await expect(page.getByTestId("feedback")).not.toHaveAttribute("data-feedback-id", firstVertexFeedbackId!) - await expect(page.getByTestId("graph-vertex")).toHaveCount(2) - const initialVertexLabels = await vertexLabels(page) - await expect(page.getByTestId("sync-status")).toHaveAttribute("data-status", "synced") - await expect.poll(() => nodeHandle.doc().scanTable("GraphRealm.V").length).toBe(2) - - executeQuery(nodeHandle, 'txn.add("GraphRealm.V", [])') - await nodeRepo.flush() - nodeRepo.resyncSubduction(nodeHandle.documentId) - await expect(page.getByTestId("graph-vertex")).toHaveCount(3) - const updatedVertexLabels = await vertexLabels(page) - expect([...initialVertexLabels].map(([id]) => [id, updatedVertexLabels.get(id)])) - .toEqual([...initialVertexLabels]) - - const values = await page.getByTestId("from-select").locator("option").evaluateAll(options => - options.slice(1).map(option => (option as HTMLOptionElement).value), - ) - await page.getByTestId("from-select").selectOption(values[0]) - await page.getByTestId("to-select").selectOption(values[1]) - await expect(page.getByTestId("edge-preview")).toBeVisible() - await expect(page.getByTestId("edge-preview")).toHaveClass(/\[stroke-dasharray:2_9\]/) - await expect(page.getByTestId("edge-preview")).toHaveCSS("pointer-events", "none") - await expect(page.getByTestId("edge-preview")).toHaveAttribute("d", /^M /) - await page.getByTestId("add-edge").click() - await expect(page.getByTestId("feedback")).toHaveText("Edge added") - await expect(page.getByTestId("from-select")).toHaveValue("") - await expect(page.getByTestId("to-select")).toHaveValue("") - await expect(page.getByTestId("edge-preview")).toBeHidden() - - await page.getByTestId("from-select").selectOption(values[0]) - await page.getByTestId("to-select").selectOption(values[1]) - await page.getByTestId("add-edge").click() - await expect(page.getByTestId("graph-edge")).toHaveCount(2) - await expect(page.getByTestId("graph-counts")).toHaveText("3 VERTICES / 2 EDGES") - - const existingPaths = await page.getByTestId("graph-edge").evaluateAll(edges => - edges.map(edge => edge.getAttribute("d")), - ) - await page.getByTestId("from-select").selectOption(values[1]) - await page.getByTestId("to-select").selectOption(values[0]) - await expect(page.getByTestId("edge-preview")).toBeVisible() - expect(existingPaths).not.toContain(await page.getByTestId("edge-preview").getAttribute("d")) - - const selectedEdge = page.getByTestId("graph-edge").first() - await selectedEdge.dispatchEvent("click") - await expect(page.getByTestId("selected-edge-details")).toBeVisible() - await expect(selectedEdge).toHaveCSS("outline-style", "none") - await expect(selectedEdge).toHaveCSS("stroke", "rgb(101, 217, 231)") - await expect(selectedEdge).toHaveClass(/hover:stroke-\[#d8ff57\]/) - - await page.getByTestId("graph-canvas").dispatchEvent("click") - await expect(page.getByTestId("selected-edge-details")).toBeHidden() - - await expect(page.getByTestId("sync-status")).toHaveAttribute("data-status", "synced") - await expect.poll(() => nodeHandle.doc().scanTable("GraphRealm.E").length).toBe(2) - - const secondPage = await context.newPage() - await secondPage.goto(`/#${url}`) - await expect(secondPage.getByTestId("graph-vertex")).toHaveCount(3) - await expect(secondPage.getByTestId("graph-edge")).toHaveCount(2) - } finally { - await nodeRepo.shutdown() - await context.close() - } -}) - -test("browser persists graph data in IndexedDB", async ({ browser }) => { - const context = await browser.newContext() - const page = await context.newPage() - - try { - await page.goto("/") - await expect(page.getByTestId("sync-status")).toHaveAttribute("data-status", "synced") - - await page.getByTestId("add-vertex").click() - await page.getByTestId("add-vertex").click() - const vertices = await page.getByTestId("from-select").locator("option").evaluateAll(options => - options.slice(1).map(option => (option as HTMLOptionElement).value), - ) - await page.getByTestId("from-select").selectOption(vertices[0]) - await page.getByTestId("to-select").selectOption(vertices[1]) - await page.getByTestId("add-edge").click() - await expect(page.getByTestId("sync-status")).toHaveAttribute("data-status", "synced") - await expect.poll(() => indexedDbRecordCount(page, "coln-sync", "documents")).toBeGreaterThan(0) - - const url = await page.getByTestId("doc-url").inputValue() - await page.close() - - const reloadedPage = await context.newPage() - await reloadedPage.goto(`/#${url}`) - await expect(reloadedPage.getByTestId("graph-vertex")).toHaveCount(2) - await expect(reloadedPage.getByTestId("graph-edge")).toHaveCount(1) - } finally { - await context.close() - } -}) - -test("shows a dedicated screen when a document is unavailable", async ({ browser }) => { - const isolatedRepo = new Repo() - const unavailableUrl = isolatedRepo.create().url - await isolatedRepo.shutdown() - const context = await browser.newContext() - const page = await context.newPage() - - try { - await page.goto(`/#${unavailableUrl}`) - - await expect(page.getByTestId("document-load-error")).toHaveAttribute("data-error-kind", "unavailable") - await expect(page.getByTestId("failed-document-url")).toHaveText(unavailableUrl) - await expect(page.getByTestId("graph-canvas")).toHaveCount(0) - await expect(page.getByTestId("add-vertex")).toHaveCount(0) - await page.getByRole("link", { name: "Create a new graph" }).click() - await expect(page.getByTestId("document-load-error")).toHaveCount(0) - await expect(page.getByTestId("graph-canvas")).toBeVisible() - await expect(page.getByTestId("doc-url")).toHaveValue(/^automerge:/) - expect(await page.getByTestId("doc-url").inputValue()).not.toBe(unavailableUrl) - } finally { - await context.close() - } -}) - -test("shows the document error screen for an invalid URL", async ({ page }) => { - const invalidUrl = "not-an-automerge-url" - - await page.goto(`/#${invalidUrl}`) - - await expect(page.getByTestId("document-load-error")).toHaveAttribute("data-error-kind", "invalid") - await expect(page.getByRole("heading")).toHaveText("This is not a valid Automerge document URL.") - await expect(page.getByTestId("failed-document-url")).toHaveText(invalidUrl) - await expect(page.getByTestId("graph-canvas")).toHaveCount(0) - await expect(page.getByTestId("add-vertex")).toHaveCount(0) -}) - -async function vertexLabels(page: import("@playwright/test").Page) { - const entries = await page.getByTestId("graph-vertex").evaluateAll(vertices => - vertices.map(vertex => [ - vertex.querySelector("title")?.textContent ?? "", - vertex.querySelector("text")?.textContent ?? "", - ] as const), - ) - return new Map(entries) -} - -async function indexedDbRecordCount( - page: import("@playwright/test").Page, - database: string, - store: string, -) { - return page.evaluate(({ database, store }) => new Promise((resolve, reject) => { - const request = indexedDB.open(database) - request.onerror = () => reject(request.error) - request.onsuccess = () => { - const count = request.result.transaction(store).objectStore(store).count() - count.onerror = () => reject(count.error) - count.onsuccess = () => resolve(count.result) - } - }), { database, store }) -} diff --git a/examples/sync/tsconfig.json b/examples/sync/tsconfig.json deleted file mode 100644 index 830d03f9..00000000 --- a/examples/sync/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "useDefineForClassFields": true, - "lib": ["DOM", "DOM.Iterable", "ES2022"], - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "ESNext", - "moduleResolution": "Bundler", - "resolveJsonModule": true, - "allowImportingTsExtensions": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - "types": ["vite/client", "node"] - }, - "include": ["src", "tests", "vite.config.ts", "vitest.config.ts", "playwright.config.ts"] -} diff --git a/examples/sync/vite.config.ts b/examples/sync/vite.config.ts deleted file mode 100644 index d15c32c6..00000000 --- a/examples/sync/vite.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Coln contributors -// -// SPDX-License-Identifier: Apache-2.0 OR MIT - -import { defineConfig } from "vite" -import { svelte } from "@sveltejs/vite-plugin-svelte" -import tailwindcss from "@tailwindcss/vite" -import wasm from "vite-plugin-wasm" - -export default defineConfig({ - base: process.env.VITE_BASE || "/", - plugins: [tailwindcss(), wasm(), svelte()], - resolve: { dedupe: ["@coln-project/runtime"] }, - build: { target: "esnext" }, -}) diff --git a/flake.nix b/flake.nix index 860d0be0..071ca194 100644 --- a/flake.nix +++ b/flake.nix @@ -107,35 +107,6 @@ doCheck = false; }; - build-sync-demo = pkgs.writeShellApplication { - name = "build-sync-demo"; - runtimeInputs = [ - coln-cli - pkgs.binaryen - pkgs.esbuild - pkgs.nodejs_24 - pkgs.pnpm - rustToolchain - wasm-bindgen-cli - wasm-bodge - ]; - text = '' - repo_root="''${1:-$PWD}" - cd "$repo_root" - - export CI="''${CI:-1}" - pnpm_store_dir="''${PNPM_STORE_DIR:-$repo_root/.pnpm-store}" - - npm ci --prefix packages/coln-js-runtime - npm run --prefix packages/coln-js-runtime build - - pnpm --dir examples/sync-demo install --frozen-lockfile --store-dir "$pnpm_store_dir" - pnpm --dir examples/sync-demo build - - echo "Built sync demo at $repo_root/examples/sync-demo/dist" - ''; - }; - format-hs = nuShellCheck [pkgs.fourmolu] ./nix/checks/format-hs.nu; format-cabal = nuShellCheck [pkgs.haskellPackages.cabal-gild] ./nix/checks/format-cabal.nu; @@ -202,15 +173,6 @@ in { inherit packages; - apps = let - buildSyncDemo = { - type = "app"; - program = "${pkgs.lib.getExe packages.build-sync-demo}"; - }; - in { - build-sync-demo = buildSyncDemo; - sync-demo = buildSyncDemo; - }; devShells.default = pkgs.mkShell { name = "coln"; buildInputs = with pkgs; [ diff --git a/justfile b/justfile index 3e8b1a12..01a5aec6 100644 --- a/justfile +++ b/justfile @@ -3,7 +3,7 @@ # just # list available recipes # just check # run all package checks -generated_file_pathspecs := "':(exclude,glob)packages/coln-compiler/test/golden/basic-ir/*.ts.output/**' ':(exclude,glob)packages/coln-compiler/test/golden/*.ts.output/**' ':(exclude,glob)examples/sync-demo/src/generated/**'" +generated_file_pathspecs := "':(exclude,glob)packages/coln-compiler/test/golden/basic-ir/*.ts.output/**' ':(exclude,glob)packages/coln-compiler/test/golden/*.ts.output/**' ':(exclude,glob)examples/lab/src/tools/graph/generated/**'" default: @just --list diff --git a/packages/coln-compiler-wasm/README.md b/packages/coln-compiler-wasm/README.md index b949daa2..33a941c3 100644 --- a/packages/coln-compiler-wasm/README.md +++ b/packages/coln-compiler-wasm/README.md @@ -1,3 +1,3 @@ # Coln compiler Wasm library -This package can be compiled with the [GHC WebAssembly backend](https://downloads.haskell.org/ghc/latest/docs/users_guide/wasm.html) to produce a [WASI reactor module](https://github.com/WebAssembly/WASI/blob/wasi-0.1/application-abi.md#current-unstable-abi). See [this example](../../examples/compiler-demo/index.html) for how to call it from JavaScript. The example app can be launched with `just examples/serve-web-compiler` from the project root. All necessary tools, including a Wasm-targeting GHC and Cabal, are provided by this repository's top-level Nix shell. +This package can be compiled with the [GHC WebAssembly backend](https://downloads.haskell.org/ghc/latest/docs/users_guide/wasm.html) to produce a [WASI reactor module](https://github.com/WebAssembly/WASI/blob/wasi-0.1/application-abi.md#current-unstable-abi). The [Lab](../../examples/lab/README.md) uses it from JavaScript and can be launched with `just examples/dev-web-lab` from the project root. All necessary tools, including a Wasm-targeting GHC and Cabal, are provided by this repository's top-level Nix shell. From f47a57a38f0c8792ed0e33a85d3aea25f3f0a464 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:16:46 +0000 Subject: [PATCH 18/18] Fix pnpm allowBuilds selectors Co-authored-by: acurrieclark <1306728+acurrieclark@users.noreply.github.com> --- examples/lab/pnpm-workspace.yaml | 2 +- packages/coln-repo/pnpm-workspace.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/lab/pnpm-workspace.yaml b/examples/lab/pnpm-workspace.yaml index c1673c4c..b82bd8d9 100644 --- a/examples/lab/pnpm-workspace.yaml +++ b/examples/lab/pnpm-workspace.yaml @@ -1,7 +1,7 @@ allowBuilds: cbor-extract: false esbuild: true - "@automerge/automerge-repo@git+https://github.com/automerge/automerge-repo.git": true + "@automerge/automerge-repo@https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo": true overrides: "@automerge/automerge": 3.3.2 "@automerge/automerge-subduction": 0.16.1 diff --git a/packages/coln-repo/pnpm-workspace.yaml b/packages/coln-repo/pnpm-workspace.yaml index 40f0aec6..48974ebe 100644 --- a/packages/coln-repo/pnpm-workspace.yaml +++ b/packages/coln-repo/pnpm-workspace.yaml @@ -1,6 +1,6 @@ allowBuilds: cbor-extract: false - "@automerge/automerge-repo@git+https://github.com/automerge/automerge-repo.git": true + "@automerge/automerge-repo@https://codeload.github.com/automerge/automerge-repo/tar.gz/7a8dbfe020aaeb73c7146d4b2b508212d37b71f5#path:packages/automerge-repo": true esbuild: true overrides: "@automerge/automerge": 3.3.2