Skip to content

Commit c054ded

Browse files
Merge branch 'main' into fix/bun-esm-generic-kinds-tea-shape
2 parents 2671928 + 35a6794 commit c054ded

21 files changed

Lines changed: 2682 additions & 1 deletion

File tree

‎.github/workflows/ci.yml‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,28 @@ jobs:
116116
run: opam exec -- ./tools/run_codegen_deno_tests.sh
117117
- name: Run native Bun-ESM tests (issue #734)
118118
run: opam exec -- ./tools/run_codegen_bun_tests.sh
119+
- name: affinescript-tea browser tests (headless Chromium)
120+
# The AffineScript TEA runtime (affinescript-tea/src/Tea.affine) driven
121+
# through its reference app in a real browser: mount, controlled
122+
# input, async commands, keyed reorder preserving DOM-node identity,
123+
# subscriptions. Playwright is pinned in e2e/bun.lock.
124+
working-directory: affinescript-tea/e2e
125+
# Chromium comes from the e2e package's `browsers` script (the pinned
126+
# Playwright's own download; no package lifecycle scripts run).
127+
run: |
128+
bun install --frozen-lockfile --ignore-scripts
129+
bun run browsers
130+
opam exec -- bun test --timeout 60000
131+
- name: affinescript-router + affinescript-crdt tests
132+
# Router.affine's pure functions and Crdt.affine's property tests
133+
# (convergence, merge laws) on Bun-ESM; the Nav example in headless
134+
# Chromium: navigate/replace/back, hash edits, deep links.
135+
run: |
136+
opam exec -- bun test affinescript-router/test
137+
opam exec -- bun test affinescript-crdt/test
138+
cd affinescript-router/e2e
139+
bun install --frozen-lockfile --ignore-scripts
140+
opam exec -- bun test --timeout 60000
119141
- name: Run face-transformer regression tests
120142
run: opam exec -- ./tools/run_face_transformer_tests.sh
121143
- name: Issue #35 Phase 3 — block extension.ts regression

‎.gitignore‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,7 @@ tools/vendor/
117117
/.claude/worktrees/
118118

119119
.editorconfig
120+
121+
# affinescript-tea browser test deps
122+
/affinescript-tea/e2e/node_modules/
123+
/affinescript-router/e2e/node_modules/

‎affinescript-crdt/README.adoc‎

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// SPDX-License-Identifier: CC-BY-SA-4.0
2+
// SPDX-FileCopyrightText: 2026 hyperpolymath
3+
= affinescript-crdt
4+
:toc: macro
5+
6+
Conflict-free replicated data types for local-first AffineScript apps.
7+
Replicas update their own copy and exchange whole states. `merge` is
8+
commutative, associative and idempotent, so replicas that have seen the same
9+
updates hold equal states, whatever order they merged in and however many
10+
times.
11+
12+
toc::[]
13+
14+
== Types
15+
16+
[cols="1,3"]
17+
|===
18+
| `Clock`, `Stamp`
19+
| A Lamport clock and its timestamps, totally ordered by `(counter, replica)`.
20+
`tick(clock)` stamps a local write. `observe(clock, stamp)` accounts for a
21+
stamp seen from another replica, so later local writes are ordered after it.
22+
23+
| `Lww<T>`
24+
| A last-writer-wins register (`lww`, `lww_set`, `lww_value`, `lww_merge`).
25+
26+
| `LwwMap<V>`
27+
| A string-keyed map of LWW entries (`map_set`, `map_remove`, `map_get`,
28+
`map_keys`, `map_merge`). Removal is a timestamped tombstone, so a later set
29+
revives the key and an earlier concurrent set loses.
30+
31+
| `OrSet`
32+
| An observed-remove set of strings (`or_add`, `or_remove`, `or_contains`,
33+
`or_elements`, `or_merge`). An add wins over a concurrent remove; a remove
34+
deletes only the adds it has observed.
35+
36+
| `PnCounter`
37+
| An increment/decrement counter (`pn_add`, `pn_value`, `pn_merge`).
38+
|===
39+
40+
States are plain values (sorted arrays of tuples and records), so they
41+
serialise directly, e.g. to JSON, for storage and sync. Each replica needs a
42+
unique id; stamps from one replica's clock are unique by construction, which
43+
is the precondition every LWW type relies on.
44+
45+
== Example
46+
47+
[source,affinescript]
48+
----
49+
use Crdt::{clock, tick, map_empty, map_set, map_merge, map_get};
50+
51+
let (c1, s1) = tick(clock("laptop"));
52+
let laptop = map_set(map_empty(), "title", "Draft", s1);
53+
let (c2, s2) = tick(clock("phone"));
54+
let phone = map_set(map_empty(), "title", "Final", s2);
55+
// Both replicas converge on the same winner ("phone" breaks the tie):
56+
map_get(map_merge(laptop, phone), "title") == map_get(map_merge(phone, laptop), "title")
57+
----
58+
59+
== Performance
60+
61+
Merges are linear merge-joins over sorted entries. On the JS target, though,
62+
building an array with `++` copies it, so construction and merges cost
63+
O(n²) in the number of entries. A merge of a 2,000-entry map with a
64+
1,000-entry one measured 7–47 ms locally. That's fine for sync events; for
65+
very large states, shard by key.
66+
67+
== Tests
68+
69+
`test/crdt.test.js` contains property tests on the module compiled with
70+
Bun-ESM. Each uses deterministic seeds, and they cover:
71+
72+
* three replicas applying 300 random operations each with random partial
73+
gossip, then converging in all six merge orders (LWW map, OR-set,
74+
PN-counter);
75+
* LWW maps whose key values equal the highest-stamped write across all
76+
replicas;
77+
* the merge laws (commutative, associative, idempotent) on random states for
78+
every type;
79+
* OR-set add-wins and observed-remove semantics;
80+
* PN-counter sums.
81+
82+
Breaking `map_merge`'s tie rule makes the convergence and law tests fail.
83+
84+
[source,sh]
85+
----
86+
dune build # at the repo root
87+
bun test affinescript-crdt/test
88+
----

0 commit comments

Comments
 (0)