Skip to content

Repository files navigation

Calcium workflow

template for mini realtime apps, based on calcit-js eco. But with calcit-rs runtime.

Usages

Install Node.js, Yarn, Calcit to start.

Clone dependencies into ~/.config/calcit/modules/ with caps command from Calcit.

Code with calcit-editor.

Run frontend:

# dev mode
corepack enable && corepack prepare yarn@4.12.0 --activate
yarn install --immutable
caps
yarn watch-page
yarn vite # for browser app

# release mode
caps --ci
yarn install --immutable
yarn compile-page
yarn release-page

Run backend in calcit-rs:

# dev mode
mode=dev calcit --compat-types -w calcit.cirru --entry server

# release mode
calcit --compat-types calcit.cirru --entry server

Calcit 0.14.0 enables strict preprocessing by default. Calcium currently uses the documented --compat-types migration bridge while its remaining dynamic UI boundaries are migrated to fully strict schemas.

Realtime sync lifecycle

Calcium keeps realtime synchronization state per browser connection. A visible page sends heartbeats and receives revisioned patches. A hidden page, or a connection that misses heartbeats, becomes idle: the server records only the latest dirty revision and releases its previous Twig cache. When the page is visible again, the server sends a fresh snapshot before resuming patches.

Transport recovery has one owner: ws-edn manages visibility/online reconnect, bounded backoff, generation replacement, and its 75-second receive deadline. Calcium reuses cumulo-util's cleanup-backed browser lifecycle only for application signals: visible/hidden activity and one 30-second revision heartbeat. Main and hot reload replace the previous cleanup capability, so no listener or timer accumulates. The older page-touch reconnect path and local visibility interval have been removed.

传输恢复只有一个 owner:ws-edn 管理 visibility/online 重连、有界退避、generation 替换及 75 秒接收 deadline。Calcium 复用 cumulo-util 带 cleanup 的浏览器生命周期, 但只处理应用信号:visible/hidden 活跃状态和 30 秒 revision heartbeat。main 与热更新 都会先执行旧 cleanup,因此 listener/timer 不会累积;旧 page-touch 重连路径及本地 visibility interval 已删除。

The server allows only one unacknowledged snapshot or patch per connection. Patches carry base-revision and revision; a client that cannot apply the base requests a new snapshot. Even when the revision matches, the client applies the batch through Recollect's validated Result API; a missing path, container or payload type mismatch, invalid index, or unsupported operation rejects the whole batch and deterministically requests a full snapshot. No partial tree is published. Diffs above the configured operation threshold fall back to snapshots, bounding the retained patch and client-side patch work. Server diff traversal also uses deterministic budgets of 50,000 visited nodes and 80,000 operation-construction units. A budget-exceeded result contains no partial patch and goes directly to the existing snapshot/send policy. These limits bound counted traversal work, not wall-clock time: map key materialization, comparisons, large leaves, snapshot encoding, queue admission, and backpressure remain independently bounded or observed.

服务端 diff 遍历还使用 50,000 个 visited node 与 80,000 个 operation-construction unit 的确定性预算。超限结果不携带 partial patch,直接进入既有 snapshot/send 策略。这些上限约束的是已计数的遍历工作,而不是 wall-clock:map key materialization、比较、大叶子、snapshot 编码、queue admission 与 backpressure 仍需分别设限或观测。

The per-client cache is the last acknowledged store, not merely the last value offered to the socket. An accepted send keeps its candidate store separately until the matching acknowledgement arrives. (:backpressured) leaves the acknowledged baseline untouched, marks the client slow, and requeues only the latest desired revision; the next attempt recomputes one patch from that stable baseline. (:too-large) requests snapshot policy without spinning on the same oversized payload, while (:closed) clears pending send state.

每个客户端的 cache 表示“已确认 store”,而不是最近一次尝试发送的值。只有 (:accepted) 且收到匹配 ack 后,pending store 才会成为新的 diff 基线。 (:backpressured) 不推进基线,标记 slow client 并合并到最新 dirty revision; 下一次发送从稳定的确认基线重新计算。(:too-large) 保持 snapshot 策略但避免 重复忙循环,(:closed) 则清理 pending send 状态。

A native slow-reader regression deliberately pauses the TCP reader while asking for repeated 192 KiB snapshots. With calcit-wss 0.2.25, 16 requests produce only 9 delivered baseline snapshots before the bounded transport coalesces pressure; the next acknowledged snapshot contains the final coalesced application state, and the following patch uses that acknowledged revision as its base. Retryable socket WouldBlock/timeout results keep the connection alive instead of being reported as a disconnect.

原生慢读端回归会暂停 TCP reader,同时重复请求 192 KiB snapshot。使用 calcit-wss 0.2.25 时,16 次请求只实际收到 9 个 baseline snapshot,证明有界 transport 已触发背压;随后确认的 snapshot 包含最终合并后的业务状态,下一条 patch 也从该已确认 revision 继续。socket 的 WouldBlock/timeout 作为可重试结果处理, 不会再把普通慢读端误判为断线。

Ordinary state changes schedule one sync within a 16 ms coalescing window. The first change starts the timer, so a continuous dispatch stream cannot postpone the flush indefinitely. WebSocket backpressure uses an independent 200 ms retry timer; it never blocks a new ordinary update from taking the fast path. Server coordination iterates the synchronous *client-states and *dirty-clients registries. Do not use asynchronous wss-each! callbacks as if they completed before the next statement; reserve WebSocket FFI for transport operations.

The pure cumulo-util.realtime/Coalescer was evaluated here but is intentionally not inserted into this server path: the existing fixed-window single timer is already bounded and cannot starve, while the generic planner would still need the same application-owned timer and add clock/state transitions without removing duplicated behavior. RetryBackoff, HeartbeatLease, and browser lifecycle do remove duplicated state and are reused through ws-edn/cumulo-util.

普通状态变更会在 16ms 合并窗口内安排一次同步。首个变更立即启动 timer,因此 持续 dispatch 不会无限推迟 flush。WebSocket 背压使用独立的 200ms 重试 timer, 不会阻塞新的普通更新走快速路径。服务端协调同步遍历自身维护的 *client-states*dirty-clients;不要假设异步 wss-each! 回调会在下一条 语句之前完成,WebSocket FFI 只负责实际传输。

这里也评估了 cumulo-util.realtime/Coalescer,但没有强行接入:现有固定窗口单 timer 已经有界且不会饥饿,通用 planner 仍需应用持有同一个 timer,并额外增加 clock/state 转换,不能减少重复行为。RetryBackoff、HeartbeatLease 与 browser lifecycle 确实删除了重复状态,因此已通过 ws-edn/cumulo-util 复用。

When migrating an older (:patch changes) application, first accept revisioned (:snapshot revision store) and (:patch base-revision revision changes) messages on the client, add (:sync/ack revision) and (:sync/resume revision), then switch the server cache to acknowledgement-driven advancement. Do not advance a cache for backpressured, too-large, or closed send outcomes. Use recollect 0.0.37 or newer and route incoming batches through PatchBatch .apply-to; do not restore the old exception-only patch-twig boundary.

The template now represents the wire protocol as nominal ClientMessage and ServerMessage enums. Raw Cirru EDN is decoded once in app.schema; revision, snapshot, patch-list, and string operation payloads are validated before the typed envelope reaches client/server synchronization code. New clients wrap business operations in ClientMessage :dispatch Op. The server still decodes legacy direct Op enums during rolling deployment, while old clients can keep matching the unchanged snapshot/patch variant tags from named ServerMessage.

模板现在用 nominal ClientMessageServerMessage enum 表示线协议。原始 Cirru EDN 只在 app.schema 解码一次;revision、snapshot、patch list 与字符串 操作参数经过校验后,typed envelope 才进入客户端/服务端同步逻辑。新客户端把 业务操作包装为 ClientMessage :dispatch Op。滚动部署期间服务端仍兼容旧客户端 直接发送的 Op,旧客户端也可继续按未变化的 snapshot/patch variant tag 匹配 named ServerMessage

Browser recovery keeps the nominal ws-edn.client/WsClient; ws-edn owns the visibilitychange/online recovery listeners, generation gate, bounded retry, and heartbeat deadline. Calcium's cleanup-backed lifecycle watcher only emits application activity and revision heartbeats, so it never installs a second reconnect path. Every successful open—including a reopened socket—sends ClientMessage :sync/resume with the last applied revision before ordinary activity/login messages. ws-edn generation gating discards callbacks from replaced sockets, and hot reload replaces the active data handler without rebuilding the connection. The template enables a 75-second inbound heartbeat deadline. Its existing 30-second protocol heartbeat now receives a typed ServerMessage :effect/pong, renewing healthy connections while a silent dead socket is actively closed and recovered through backoff.

浏览器恢复会保留 nominal ws-edn.client/WsClientvisibilitychange/online recovery、generation gate、有界重试与 heartbeat deadline 统一由 ws-edn 管理。 Calcium 带 cleanup 的 lifecycle watcher 只发送应用 activity 和 revision heartbeat, 不会安装第二套重连路径。每次成功 open(包括重新连接)都会先携带 最后已应用 revision 发送 ClientMessage :sync/resume,再发送普通 activity/login 消息。ws-edn 的 generation gating 会丢弃已替换 socket 的迟到 callback;热更新 只替换当前 data handler,不重建连接。模板启用 75 秒入站 heartbeat deadline; 已有的 30 秒协议 heartbeat 现在会收到 typed ServerMessage :effect/pong,健康连接 据此续租,静默失效 socket 则被主动关闭并通过 backoff 恢复。

Server synchronization observability is available through app.server/read-sync-metrics. The typed SyncMetrics snapshot records the latest diff latency, the latest patch payload's real UTF-8 byte length, patch and snapshot payload byte lengths, latest visited/emitted diff work, budget fallback count, patch and snapshot send attempts, explicit resync requests, and the latest revision. Pending-ACK and slow-client gauges are calculated only when the snapshot is read, so ordinary dispatch/send paths do not rescan every connection. Attempt counts include transport retries; combine them with calcit-wss wss-metrics when transport admission and queue details are needed.

服务端同步观测可通过 app.server/read-sync-metrics 获取。Typed SyncMetrics snapshot 记录最近 diff 延迟、最近 patch payload 的真实 UTF-8 字节数、最近 snapshot 字节数、visited/emitted 工作量、预算回退次数、 patch/snapshot 发送尝试次数、显式 resync 请求数与最新 revision。 等待 ACK 和 slow-client gauge 只在读取 snapshot 时计算,普通 dispatch/send 热路径不会重新扫描所有连接。发送尝试包含传输重试;需要 transport admission 与队列细节时,可与 calcit-wss 的 wss-metrics 组合使用。

Payload byte accounting uses Calcit's string receiver method .utf8-byte-count; the template no longer carries an interpreted helper on this hot path. 字节统计直接使用 Calcit 的 String 方法 .utf8-byte-count, 不再在模板中维护解释执行的辅助函数。

Twig rendering is split into a shared projection cached once per Reel revision and a session-specific projection. Keep both layers pure and deterministic so unchanged application state does not create artificial patches.

Cache ownership is intentionally narrow. Respo owns component memoization; Recollect owns keyed Twig memoization; Calcium keeps only protocol/business caches such as the revision-indexed shared Twig and per-client patch baselines. sync-clients! opens one Recollect memo frame around the active dirty clients, so entries for idle/disconnected clients are pruned without every application implementing cache bookkeeping. Twig code should use the typed memo-twig-by0, memo-twig-by1, or memo-twig-by2 helpers. The generic memof module is not part of the Calcium dependency graph.

Diff/patch regression workload

The deterministic workload replays the same typed DomainOp sequence over keyed entities: no-op, leaf update, insert, remove, reorder, and full replace. The data runner checks each patched client Store against a fresh projection and also covers a slow client, out-of-order and duplicate acknowledgements, wrong revisions, invalid payloads, recovery convergence, and a deliberately corrupted baseline oracle. CI runs only the small correctness smoke:

yarn workload:smoke

Run the performance workload only on a fixed environment. It warms up first, then records at least 30 samples for both 1,000 and 10,000 entities, including p50, p95, variance, stage throughput, visited/emitted diff work, patch and snapshot bytes, process memory, toolchain/environment metadata, input and dependency hashes, and the raw report hash. Keep raw results outside this repository; allocation data is reported as unavailable because JavaScript does not expose a stable per-stage allocator API. The benchmark script raises Node's stack explicitly because Recollect's 10,000-row vector diff exceeds Node's default JavaScript stack.

yarn workload:benchmark --out /absolute/path/outside/repo/calcium-data.json
yarn workload:browser
# then open:
# http://127.0.0.1:5173/tests/diff-patch-browser.html?mode=full

The browser runner measures VDOM diff and DOM write independently and compares every patched DOM with a fresh render. It also asserts zero no-op DOM mutation, stable keyed node identity and ref behavior, listener continuity, and retained focus/selection. Both commands begin with caps verify --toolchain, so a global Calcit binary that differs from deps.cirru fails before producing a baseline.

差量回归会对固定 seed 的 keyed entity 重放同一组 typed DomainOp:no-op、叶子 更新、插入、删除、重排和全量替换。数据 runner 会逐步比较 patch 后的客户端 Store 与 fresh projection,并覆盖慢客户端、乱序/重复 ack、错误 revision、非法 payload、 恢复收敛以及故意损坏 baseline 的 oracle。CI 只运行小规模正确性 smoke。

完整基线仅在固定环境运行:先 warmup,再分别对 1,000/10,000 entity 采集至少 30 次,输出 p50、p95、方差、阶段工作量、环境/工具链、输入/依赖哈希和 raw hash。原始 结果还包含阶段吞吐、visited/emitted diff 工作量、patch/snapshot 字节数与进程内存; 必须保存在仓库外。JavaScript 没有稳定的逐阶段 allocator API,因此 allocation 明确记录为 unavailable。10,000 行的 Recollect vector diff 会超过 Node 默认的 JavaScript 栈,因此 benchmark 脚本显式提高栈上限。浏览器 runner 独立测量 VDOM diff 与 DOM write,并验证 patched DOM 等于 fresh render、no-op 不产生 DOM mutation,以及 key/ref/listener/focus/selection 语义不变。命令会先执行 caps verify --toolchain,避免静默混用全局 Calcit 版本。

License

MIT

About

Cumulo workflow with calcit-rs runtime

Topics

Resources

Stars

0 stars

Watchers

2 watching

Forks

Used by

Contributors

Languages