Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9152a8a
fix(typecheck,bun-esm): generic enum kinds; qualified ctors; sync rec…
hyperpolymath Oct 5, 2026
7832f47
fix(typecheck,bun-esm): thunk lambdas, parametric extern types, math …
hyperpolymath Oct 5, 2026
5e0279e
feat(module_loader): $AFFINESCRIPT_PATH package search path
hyperpolymath Oct 5, 2026
9d4e00b
fix(ast): find_free_vars walks loops, assignments and pattern binders
hyperpolymath Oct 5, 2026
c23c4e3
fix(typecheck,resolve,bun-esm): record update typing, imported types,…
hyperpolymath Oct 5, 2026
65dd7ca
feat(affinescript-tea): The Elm Architecture written in AffineScript
hyperpolymath Oct 5, 2026
5f16144
feat(affinescript-tea): local pointer coords, editable-target test, v…
hyperpolymath Oct 5, 2026
e822cef
fix(affinescript-tea): pages import the app module, never call main()
hyperpolymath Oct 5, 2026
f069138
fix(module_loader): carry directly-imported enums when flattening
hyperpolymath Oct 5, 2026
c239711
Merge remote-tracking branch 'origin/fix/bun-esm-generic-kinds-tea-sh…
hyperpolymath Oct 5, 2026
efa8683
fix(bun-esm,typecheck): Int division in loops; generic externs used b…
hyperpolymath Oct 5, 2026
c5de72c
wip(affinescript-tea): lazy subtrees, LIS keyed diff (tests pending)
hyperpolymath Oct 5, 2026
520436c
Merge remote-tracking branch 'origin/fix/bun-esm-generic-kinds-tea-sh…
hyperpolymath Oct 5, 2026
ac4afa4
fix(typecheck): scope a function body's let-bindings, not just its pa…
hyperpolymath Oct 5, 2026
587d8bb
Merge remote-tracking branch 'origin/fix/bun-esm-generic-kinds-tea-sh…
hyperpolymath Oct 5, 2026
7c8ea31
fix(module_loader): flatten imports transitively
hyperpolymath Oct 5, 2026
d049d06
Merge remote-tracking branch 'origin/fix/bun-esm-generic-kinds-tea-sh…
hyperpolymath Oct 5, 2026
f3368d9
feat(affinescript-router): URL routing for affinescript-tea apps
hyperpolymath Oct 5, 2026
7e23984
feat(affinescript-crdt): state-based CRDTs for local-first apps
hyperpolymath Oct 5, 2026
2f080d9
docs(ecosystem): register Tea.affine runtime, affinescript-router, af…
hyperpolymath Oct 5, 2026
8cb8c53
ci(affinescript-tea): use `bun x` (the pinned Bun zip has no bunx shim)
hyperpolymath Oct 5, 2026
7eddd39
fix(bun-esm): emit enum constructor bindings before top-level consts;…
hyperpolymath Oct 5, 2026
a060ca6
fix(affinescript-tea,router): review/scanner findings
hyperpolymath Oct 5, 2026
f21fdda
Merge remote-tracking branch 'origin/fix/bun-esm-generic-kinds-tea-sh…
hyperpolymath Oct 5, 2026
2671928
fix(module_loader): memoise transitive flattening; assert field-not-f…
hyperpolymath Oct 5, 2026
c3f7cae
Merge remote-tracking branch 'origin/fix/bun-esm-generic-kinds-tea-sh…
hyperpolymath Oct 5, 2026
b8fbb48
ci(affinescript-tea): download Chromium via the e2e package's browser…
hyperpolymath Oct 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,28 @@ jobs:
run: opam exec -- ./tools/run_codegen_deno_tests.sh
- name: Run native Bun-ESM tests (issue #734)
run: opam exec -- ./tools/run_codegen_bun_tests.sh
- name: affinescript-tea browser tests (headless Chromium)
# The AffineScript TEA runtime (affinescript-tea/src/Tea.affine) driven
# through its reference app in a real browser: mount, controlled
# input, async commands, keyed reorder preserving DOM-node identity,
# subscriptions. Playwright is pinned in e2e/bun.lock.
working-directory: affinescript-tea/e2e
# Chromium comes from the e2e package's `browsers` script (the pinned
# Playwright's own download; no package lifecycle scripts run).
run: |
bun install --frozen-lockfile --ignore-scripts
bun run browsers
opam exec -- bun test --timeout 60000
- name: affinescript-router + affinescript-crdt tests
# Router.affine's pure functions and Crdt.affine's property tests
# (convergence, merge laws) on Bun-ESM; the Nav example in headless
# Chromium: navigate/replace/back, hash edits, deep links.
run: |
opam exec -- bun test affinescript-router/test
opam exec -- bun test affinescript-crdt/test
cd affinescript-router/e2e
bun install --frozen-lockfile --ignore-scripts
opam exec -- bun test --timeout 60000
- name: Run face-transformer regression tests
run: opam exec -- ./tools/run_face_transformer_tests.sh
- name: Issue #35 Phase 3 — block extension.ts regression
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,7 @@ tools/vendor/
/.claude/worktrees/

.editorconfig

# affinescript-tea browser test deps
/affinescript-tea/e2e/node_modules/
/affinescript-router/e2e/node_modules/
88 changes: 88 additions & 0 deletions affinescript-crdt/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// SPDX-License-Identifier: CC-BY-SA-4.0
// SPDX-FileCopyrightText: 2026 hyperpolymath
= affinescript-crdt
:toc: macro

Conflict-free replicated data types for local-first AffineScript apps.
Replicas update their own copy and exchange whole states. `merge` is
commutative, associative and idempotent, so replicas that have seen the same
updates hold equal states, whatever order they merged in and however many
times.

toc::[]

== Types

[cols="1,3"]
|===
| `Clock`, `Stamp`
| A Lamport clock and its timestamps, totally ordered by `(counter, replica)`.
`tick(clock)` stamps a local write. `observe(clock, stamp)` accounts for a
stamp seen from another replica, so later local writes are ordered after it.

| `Lww<T>`
| A last-writer-wins register (`lww`, `lww_set`, `lww_value`, `lww_merge`).

| `LwwMap<V>`
| A string-keyed map of LWW entries (`map_set`, `map_remove`, `map_get`,
`map_keys`, `map_merge`). Removal is a timestamped tombstone, so a later set
revives the key and an earlier concurrent set loses.

| `OrSet`
| An observed-remove set of strings (`or_add`, `or_remove`, `or_contains`,
`or_elements`, `or_merge`). An add wins over a concurrent remove; a remove
deletes only the adds it has observed.

| `PnCounter`
| An increment/decrement counter (`pn_add`, `pn_value`, `pn_merge`).
|===

States are plain values (sorted arrays of tuples and records), so they
serialise directly, e.g. to JSON, for storage and sync. Each replica needs a
unique id; stamps from one replica's clock are unique by construction, which
is the precondition every LWW type relies on.

== Example

[source,affinescript]
----
use Crdt::{clock, tick, map_empty, map_set, map_merge, map_get};

let (c1, s1) = tick(clock("laptop"));
let laptop = map_set(map_empty(), "title", "Draft", s1);
let (c2, s2) = tick(clock("phone"));
let phone = map_set(map_empty(), "title", "Final", s2);
// Both replicas converge on the same winner ("phone" breaks the tie):
map_get(map_merge(laptop, phone), "title") == map_get(map_merge(phone, laptop), "title")
----

== Performance

Merges are linear merge-joins over sorted entries. On the JS target, though,
building an array with `++` copies it, so construction and merges cost
O(n²) in the number of entries. A merge of a 2,000-entry map with a
1,000-entry one measured 7–47 ms locally. That's fine for sync events; for
very large states, shard by key.

== Tests

`test/crdt.test.js` contains property tests on the module compiled with
Bun-ESM. Each uses deterministic seeds, and they cover:

* three replicas applying 300 random operations each with random partial
gossip, then converging in all six merge orders (LWW map, OR-set,
PN-counter);
* LWW maps whose key values equal the highest-stamped write across all
replicas;
* the merge laws (commutative, associative, idempotent) on random states for
every type;
* OR-set add-wins and observed-remove semantics;
* PN-counter sums.

Breaking `map_merge`'s tie rule makes the convergence and law tests fail.

[source,sh]
----
dune build # at the repo root
bun test affinescript-crdt/test
----
Loading
Loading