@crdt-sync/core 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,346 @@
1
+ # CRDT-sync
2
+
3
+ [![Crates.io](https://img.shields.io/crates/v/crdt-sync.svg)](https://crates.io/crates/crdt-sync)
4
+ [![Docs.rs](https://docs.rs/crdt-sync/badge.svg)](https://docs.rs/crdt-sync)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+
7
+ A **generic state synchronization engine** built in Rust, based on
8
+ **CRDTs (Conflict-free Replicated Data Types)**.
9
+
10
+ Designed to keep a UI (digital twin) and a backend (AI/robotics logic) in
11
+ **perfect harmony** — even when multiple agents write concurrently or the
12
+ network drops for a moment. No locks, no merge conflicts, no data loss.
13
+
14
+ ---
15
+
16
+ ## Features
17
+
18
+ | CRDT | Type | Use case |
19
+ |------|------|----------|
20
+ | [`LWWRegister`] | Last-Writer-Wins Register | Scalar key-value properties (`robot.x = 10`) |
21
+ | [`ORSet`] | Observed-Remove Set | Collections of unique items |
22
+ | [`RGA`] | Replicated Growable Array | Ordered sequences / lists |
23
+ | [`StateStore`] | Composite sync engine | Hosts all CRDTs under one roof with Lamport clocks and network `Envelope`s |
24
+ | [`StateProxy`] | State observation proxy | Intercepts field mutations and auto-queues CRDT operations (no manual `Envelope` handling) |
25
+ | [`crdt_state!`] | Typed state proxy macro | Generates a typed proxy struct from a field declaration; each field gets `set_<field>` / `get_<field>` methods with compile-time type safety |
26
+
27
+ ### Logical Clocks
28
+
29
+ Physical wall-clock time (NTP) is unreliable for distributed synchronization.
30
+ This library provides two implementations that track causal ordering without
31
+ relying on system time:
32
+
33
+ | Clock | Module | Use case |
34
+ |-------|--------|----------|
35
+ | [`LamportClock`] | `lamport_clock` | Scalar logical clock; used internally by `StateStore` for total-order timestamps |
36
+ | [`VectorClock`] | `vector_clock` | Per-node vector clock; detects **concurrent** events in addition to causal ordering |
37
+
38
+ All CRDTs are **operation-based (CmRDT)** and satisfy:
39
+ - **Commutativity** – apply operations in any order, always converge.
40
+ - **Idempotency** – replaying an operation is safe.
41
+ - **Causal buffering** (RGA) – out-of-order delivery is handled automatically.
42
+
43
+ ---
44
+
45
+ ## Installation
46
+
47
+ Add to your `Cargo.toml`:
48
+
49
+ ```toml
50
+ [dependencies]
51
+ crdt-sync = "0.1"
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Quick Start
57
+
58
+ ### LWW-Register (Last-Writer-Wins)
59
+
60
+ ```rust
61
+ use crdt_sync::LWWRegister;
62
+
63
+ let mut node_a: LWWRegister<f64> = LWWRegister::new("node-A");
64
+ let mut node_b: LWWRegister<f64> = LWWRegister::new("node-B");
65
+
66
+ // node-A writes robot.x
67
+ let op = node_a.set_and_apply(10.0, 1);
68
+
69
+ // Broadcast op to node-B
70
+ node_b.apply(op);
71
+
72
+ assert_eq!(node_b.get(), Some(&10.0));
73
+ ```
74
+
75
+ ### OR-Set (Observed-Remove Set)
76
+
77
+ ```rust
78
+ use crdt_sync::ORSet;
79
+
80
+ let mut node_a: ORSet<String> = ORSet::new("node-A");
81
+ let mut node_b: ORSet<String> = ORSet::new("node-B");
82
+
83
+ // Add a robot to the fleet
84
+ let op = node_a.add("robot-1".to_string());
85
+ node_a.apply(op.clone());
86
+ node_b.apply(op);
87
+
88
+ assert!(node_b.contains(&"robot-1".to_string()));
89
+ ```
90
+
91
+ ### RGA (Replicated Growable Array)
92
+
93
+ ```rust
94
+ use crdt_sync::RGA;
95
+
96
+ let mut node_a: RGA<char> = RGA::new("node-A");
97
+ let mut node_b: RGA<char> = RGA::new("node-B");
98
+
99
+ // node-A builds a sequence
100
+ let op1 = node_a.insert(0, 'H');
101
+ let op2 = node_a.insert(1, 'i');
102
+
103
+ // node-B receives operations in reverse order — handled automatically
104
+ node_b.apply(op2);
105
+ node_b.apply(op1);
106
+
107
+ assert_eq!(node_a.to_vec(), node_b.to_vec()); // ['H', 'i']
108
+ ```
109
+
110
+ ### StateStore (Composite Sync Engine)
111
+
112
+ The `StateStore` is the recommended high-level API. It:
113
+ - Manages named LWW registers, OR-Sets and RGAs in one place.
114
+ - Assigns **Lamport timestamps** automatically (via the built-in `LamportClock`).
115
+ - Produces **`Envelope`** messages ready to send over a network channel.
116
+
117
+ ```rust
118
+ use crdt_sync::StateStore;
119
+
120
+ let mut node_a = StateStore::new("node-A");
121
+ let mut node_b = StateStore::new("node-B");
122
+
123
+ // Write a scalar property
124
+ let env = node_a.set_register("robot.x", 42.0_f64);
125
+ node_b.apply_envelope(env);
126
+ assert_eq!(node_b.get_register::<f64>("robot.x"), Some(42.0));
127
+
128
+ // Add to a set
129
+ let env = node_a.set_add("fleet", "unit-1");
130
+ node_b.apply_envelope(env);
131
+ assert!(node_b.set_contains("fleet", &"unit-1"));
132
+
133
+ // Append to a sequence
134
+ let env1 = node_a.seq_insert("log", 0, "boot");
135
+ let env2 = node_a.seq_insert("log", 1, "ready");
136
+ node_b.apply_envelope(env2);
137
+ node_b.apply_envelope(env1); // out-of-order — still converges
138
+ assert_eq!(
139
+ node_a.seq_items::<String>("log"),
140
+ node_b.seq_items::<String>("log"),
141
+ );
142
+ ```
143
+
144
+ ### LamportClock
145
+
146
+ ```rust
147
+ use crdt_sync::LamportClock;
148
+
149
+ let mut node_a = LamportClock::new();
150
+ let mut node_b = LamportClock::new();
151
+
152
+ // node_a produces and sends an event
153
+ let ts = node_a.tick(); // ts = 1
154
+
155
+ // node_b receives it, then produces its own event
156
+ node_b.update(ts); // b advances to max(0, 1) + 1 = 2
157
+ let ts_b = node_b.tick(); // ts_b = 3
158
+
159
+ assert!(ts_b > ts);
160
+ ```
161
+
162
+ ### VectorClock
163
+
164
+ ```rust
165
+ use crdt_sync::VectorClock;
166
+
167
+ let mut a = VectorClock::new("A");
168
+ let mut b = VectorClock::new("B");
169
+
170
+ let v_a = a.increment(); // A sends {A:1}
171
+ let v_b = b.increment(); // B sends {B:1} – concurrent with v_a
172
+
173
+ // Neither causally precedes the other
174
+ assert!(v_a.concurrent_with(&v_b));
175
+
176
+ // B receives A's event
177
+ b.update(&v_a);
178
+ let v_b2 = b.increment(); // {A:1, B:2} – causally after v_a
179
+
180
+ assert!(v_a.happened_before(&v_b2));
181
+ ```
182
+
183
+ ---
184
+
185
+ ### StateProxy (State Observation)
186
+
187
+ `StateProxy` is the Rust equivalent of JavaScript Proxy-based state observation.
188
+ It wraps a `StateStore` and **intercepts every field mutation**, automatically
189
+ converting it into a CRDT operation and queuing it for broadcast. Developers
190
+ work with plain `set` / `get` calls and never touch `Envelope` values directly.
191
+
192
+ ```rust
193
+ use crdt_sync::state_store::StateStore;
194
+ use crdt_sync::proxy::StateProxy;
195
+
196
+ let mut store_a = StateStore::new("node-A");
197
+ let mut store_b = StateStore::new("node-B");
198
+
199
+ // Use the proxy – no manual Envelope handling required.
200
+ let ops = {
201
+ let mut proxy = store_a.proxy(); // or StateProxy::new(&mut store_a)
202
+ proxy
203
+ .set("robot.x", 10.0_f64) // scalar field
204
+ .set("robot.y", 20.0_f64)
205
+ .set_add("fleet", "unit-1") // set field
206
+ .seq_push("log", "boot"); // sequence field
207
+
208
+ proxy.drain_pending() // collect queued ops for broadcast
209
+ };
210
+
211
+ // Broadcast to all peers.
212
+ for env in ops {
213
+ store_b.apply_envelope(env);
214
+ }
215
+
216
+ assert_eq!(store_b.get_register::<f64>("robot.x"), Some(10.0));
217
+ assert!(store_b.set_contains("fleet", &"unit-1"));
218
+ assert_eq!(store_b.seq_items::<String>("log"), vec!["boot"]);
219
+ ```
220
+
221
+ ---
222
+
223
+ ### `crdt_state!` Macro (Typed State Proxy)
224
+
225
+ The `crdt_state!` macro generates a **typed proxy struct** from a plain struct-like
226
+ field declaration. Instead of using string keys (`proxy.set("x", 10.0)`), you get
227
+ compile-time-checked `set_x` / `get_x` methods for every declared field.
228
+
229
+ ```rust
230
+ use crdt_sync::state_store::StateStore;
231
+ use crdt_sync::crdt_state;
232
+
233
+ // Declare a typed state proxy struct.
234
+ crdt_state! {
235
+ pub struct RobotState {
236
+ x: f64,
237
+ y: f64,
238
+ speed: f64,
239
+ name: String,
240
+ active: bool,
241
+ }
242
+ }
243
+
244
+ let mut store_a = StateStore::new("node-A");
245
+ let mut store_b = StateStore::new("node-B");
246
+
247
+ // Mutations are intercepted; no Envelope handling required.
248
+ let ops = {
249
+ let mut state = RobotState::new(&mut store_a);
250
+ state
251
+ .set_x(3.0)
252
+ .set_y(4.0)
253
+ .set_speed(5.0)
254
+ .set_name("unit-7".to_string())
255
+ .set_active(true);
256
+
257
+ state.drain_pending() // collect queued ops for broadcast
258
+ };
259
+
260
+ // Broadcast to all peers.
261
+ for env in ops {
262
+ store_b.apply_envelope(env);
263
+ }
264
+
265
+ assert_eq!(store_b.get_register::<f64>("x"), Some(3.0));
266
+ assert_eq!(store_b.get_register::<f64>("speed"), Some(5.0));
267
+ assert_eq!(store_b.get_register::<bool>("active"), Some(true));
268
+ ```
269
+
270
+ The generated struct also exposes:
271
+ - `pending_count() -> usize` – number of ops queued for broadcast.
272
+ - `store() -> &StateStore` – read-only access to the underlying store.
273
+
274
+ ---
275
+
276
+ ```
277
+ crdt-sync
278
+ ├── lamport_clock – Scalar Lamport logical clock (tick / update rules)
279
+ ├── vector_clock – Per-node vector clock (happened-before / concurrent detection)
280
+ ├── lww_register – LWW-Register CmRDT
281
+ ├── or_set – OR-Set CmRDT
282
+ ├── rga – RGA CmRDT (with causal buffering)
283
+ ├── state_store – Composite sync engine (LamportClock + Envelope)
284
+ ├── proxy – StateProxy: intercepts mutations, auto-queues CRDT ops
285
+ └── macros – crdt_state! macro: typed proxy structs from field declarations
286
+ ```
287
+
288
+ Each module is self-contained and can be used independently. The `StateStore`
289
+ type-erases values via `serde_json::Value` so heterogeneous data can be stored
290
+ without dynamic dispatch.
291
+
292
+ ---
293
+
294
+ ## TypeScript SDK & React Integration
295
+
296
+ `crdt-sync` provides a seamless TypeScript monorepo offering a framework-agnostic core (`@crdt-sync/core`) and framework-specific adapters (`@crdt-sync/react`), built on top of WebAssembly.
297
+
298
+ ### Installation
299
+
300
+ ```bash
301
+ npm install @crdt-sync/core @crdt-sync/react
302
+ ```
303
+
304
+ ### React usage (`useCrdtState`)
305
+
306
+ The React hook magically handles Wasm initialization, WebSocket network sync, CRDT proxying, and React component re-renders:
307
+
308
+ ```tsx
309
+ import { useCrdtState } from '@crdt-sync/react';
310
+
311
+ export function RobotDashboard() {
312
+ // Binds the CRDT Wasm engine and networking directly to React state
313
+ const { state, proxy, status } = useCrdtState('wss://api.example.com/sync', {
314
+ robot: { speed: 0, active: true }
315
+ });
316
+
317
+ if (status !== 'open') return <p>Connecting to sync engine...</p>;
318
+
319
+ return (
320
+ <div>
321
+ <h1>Speed: {state.robot.speed}</h1>
322
+ {/*
323
+ Direct mutation is intercepted, applied as a CRDT operation,
324
+ broadcast over WebSocket, and triggers a local React re-render.
325
+ */}
326
+ <button onClick={() => proxy!.state.robot.speed += 10}>
327
+ Increase Speed
328
+ </button>
329
+ </div>
330
+ );
331
+ }
332
+ ```
333
+
334
+ ---
335
+
336
+ ## Running Tests
337
+
338
+ ```bash
339
+ cargo test
340
+ ```
341
+
342
+ ---
343
+
344
+ ## License
345
+
346
+ MIT
@@ -0,0 +1,145 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ /**
5
+ * A WebAssembly-compatible wrapper around [`StateStore`].
6
+ *
7
+ * Methods accept and return JSON-encoded strings at the Wasm boundary so that
8
+ * [`Envelope`] payloads and CRDT values can be exchanged between Rust and
9
+ * JavaScript without sharing memory structures directly.
10
+ */
11
+ export class WasmStateStore {
12
+ free(): void;
13
+ [Symbol.dispose](): void;
14
+ /**
15
+ * Apply a remote [`Envelope`] (serialised as a JSON string) to this store.
16
+ *
17
+ * Throws a JavaScript error if the JSON cannot be deserialised.
18
+ */
19
+ apply_envelope(envelope_json: string): void;
20
+ /**
21
+ * Return the current Lamport clock value.
22
+ *
23
+ * Returned as `f64` because JavaScript's `Number` type cannot safely
24
+ * represent all `u64` values. Values up to `2^53 − 1`
25
+ * (`Number.MAX_SAFE_INTEGER`) are represented exactly. For distributed
26
+ * systems that could conceivably tick the clock beyond that threshold,
27
+ * treat the returned value as approximate or use `BigInt` on the JS side.
28
+ */
29
+ clock(): number;
30
+ /**
31
+ * Read the current value of a named LWW register as a JSON string.
32
+ *
33
+ * Returns `undefined` in JavaScript if the key has never been written.
34
+ */
35
+ get_register(key: string): string | undefined;
36
+ /**
37
+ * Create a new `WasmStateStore` for the given node identifier.
38
+ */
39
+ constructor(node_id: string);
40
+ /**
41
+ * Delete the element at visible `index` in the named RGA sequence.
42
+ *
43
+ * Returns the resulting [`Envelope`] as a JSON string, or `undefined` if
44
+ * `index` is out of bounds.
45
+ */
46
+ seq_delete(key: string, index: number): string | undefined;
47
+ /**
48
+ * Insert a JSON-encoded element at `index` in the named RGA sequence.
49
+ *
50
+ * Returns the resulting [`Envelope`] as a JSON string.
51
+ */
52
+ seq_insert(key: string, index: number, value_json: string): string;
53
+ /**
54
+ * Return all visible elements of the named sequence as a JSON array string.
55
+ *
56
+ * Throws a JavaScript error if serialisation fails.
57
+ */
58
+ seq_items(key: string): string;
59
+ /**
60
+ * Return the number of visible elements in the named sequence.
61
+ */
62
+ seq_len(key: string): number;
63
+ /**
64
+ * Add a JSON-encoded element to the named OR-Set.
65
+ *
66
+ * Returns the resulting [`Envelope`] as a JSON string.
67
+ */
68
+ set_add(key: string, value_json: string): string;
69
+ /**
70
+ * Returns `true` if the named OR-Set contains the JSON-encoded `value`.
71
+ */
72
+ set_contains(key: string, value_json: string): boolean;
73
+ /**
74
+ * Return all elements of the named OR-Set as a JSON array string.
75
+ *
76
+ * Throws a JavaScript error if serialisation fails.
77
+ */
78
+ set_items(key: string): string;
79
+ /**
80
+ * Write a JSON-encoded value to the named LWW register.
81
+ *
82
+ * Returns the resulting [`Envelope`] serialised as a JSON string, ready
83
+ * to broadcast to peer nodes.
84
+ */
85
+ set_register(key: string, value_json: string): string;
86
+ /**
87
+ * Remove a JSON-encoded element from the named OR-Set.
88
+ *
89
+ * Returns the resulting [`Envelope`] as a JSON string, or `undefined` if
90
+ * the element was not present in the set.
91
+ *
92
+ * Throws a JavaScript error if `value_json` is not valid JSON.
93
+ */
94
+ set_remove(key: string, value_json: string): string | undefined;
95
+ }
96
+
97
+ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
98
+
99
+ export interface InitOutput {
100
+ readonly memory: WebAssembly.Memory;
101
+ readonly __wbg_wasmstatestore_free: (a: number, b: number) => void;
102
+ readonly wasmstatestore_apply_envelope: (a: number, b: number, c: number) => [number, number];
103
+ readonly wasmstatestore_clock: (a: number) => number;
104
+ readonly wasmstatestore_get_register: (a: number, b: number, c: number) => [number, number];
105
+ readonly wasmstatestore_new: (a: number, b: number) => number;
106
+ readonly wasmstatestore_seq_delete: (a: number, b: number, c: number, d: number) => [number, number];
107
+ readonly wasmstatestore_seq_insert: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number, number];
108
+ readonly wasmstatestore_seq_items: (a: number, b: number, c: number) => [number, number, number, number];
109
+ readonly wasmstatestore_seq_len: (a: number, b: number, c: number) => number;
110
+ readonly wasmstatestore_set_add: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
111
+ readonly wasmstatestore_set_contains: (a: number, b: number, c: number, d: number, e: number) => number;
112
+ readonly wasmstatestore_set_items: (a: number, b: number, c: number) => [number, number, number, number];
113
+ readonly wasmstatestore_set_register: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
114
+ readonly wasmstatestore_set_remove: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
115
+ readonly __wbindgen_exn_store: (a: number) => void;
116
+ readonly __externref_table_alloc: () => number;
117
+ readonly __wbindgen_externrefs: WebAssembly.Table;
118
+ readonly __wbindgen_malloc: (a: number, b: number) => number;
119
+ readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
120
+ readonly __externref_table_dealloc: (a: number) => void;
121
+ readonly __wbindgen_free: (a: number, b: number, c: number) => void;
122
+ readonly __wbindgen_start: () => void;
123
+ }
124
+
125
+ export type SyncInitInput = BufferSource | WebAssembly.Module;
126
+
127
+ /**
128
+ * Instantiates the given `module`, which can either be bytes or
129
+ * a precompiled `WebAssembly.Module`.
130
+ *
131
+ * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
132
+ *
133
+ * @returns {InitOutput}
134
+ */
135
+ export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
136
+
137
+ /**
138
+ * If `module_or_path` is {RequestInfo} or {URL}, makes a request and
139
+ * for everything else, calls `WebAssembly.instantiate` directly.
140
+ *
141
+ * @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
142
+ *
143
+ * @returns {Promise<InitOutput>}
144
+ */
145
+ export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;