@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.
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # @crdt-sync/core
2
+
3
+ It acts as a TypeScript proxy wrapper around the powerful, Rust-compiled WebAssembly (Wasm) `StateStore` engine. It handles initializing the Wasm binaries, orchestrating the state sync via CRDTs, and managing the WebSocket network connections under the hood.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @crdt-sync/core
9
+ ```
10
+
11
+ **Note:** If you are using React, we highly recommend using the magic hook adapter instead:
12
+ ```bash
13
+ npm install @crdt-sync/core @crdt-sync/react
14
+ ```
15
+
16
+ ## Features
17
+
18
+ - **Wasm-powered CRDT Engine**: Lightning fast, robust conflict resolution written in Rust.
19
+ - **JavaScript State Proxying**: Wraps your regular JS objects into observed CRDT proxies. Mutations (e.g., `state.x = 10`) are instantly and invisibly converted into CRDT operations and queued.
20
+ - **WebSocket Synchronization**: Comes with a built-in `WebSocketManager` to easily broadcast the encoded Wasm envelopes between your client and your backend server.
21
+
22
+ ## Basic Usage (Vanilla JS/TS)
23
+
24
+ While using framework adapters (like React's) is recommended for DOM-based apps, you can use `@crdt-sync/core` directly in any plain JavaScript or Node.js environment:
25
+
26
+ ```typescript
27
+ import { CrdtStateProxy, WebSocketManager } from '@crdt-sync/core';
28
+ // Import the Wasm initializer
29
+ import init, { WasmStateStore } from '@crdt-sync/core/pkg/web/crdt_sync.js';
30
+
31
+ async function main() {
32
+ // 1. Initialize the Wasm Module
33
+ await init();
34
+
35
+ // 2. Create a unique ID for this client
36
+ const clientId = 'client-' + Math.random().toString(36).substring(2, 11);
37
+ const store = new WasmStateStore(clientId);
38
+
39
+ // 3. Create the State Proxy
40
+ const proxy = new CrdtStateProxy(store);
41
+
42
+ // Set initial state
43
+ proxy.state.robot = {
44
+ speed: 0,
45
+ active: false
46
+ };
47
+
48
+ // 4. Hook up to the network
49
+ const ws = new WebSocket('wss://api.example.com/sync');
50
+ const manager = new WebSocketManager(store, proxy, ws);
51
+
52
+ // 5. To mutate the state and broadcast to peers, simply mutate the proxy!
53
+ proxy.state.robot.speed = 42;
54
+
55
+ // Listen for incoming changes from the server
56
+ proxy.onUpdate(() => {
57
+ console.log("State updated remotely!", proxy.state);
58
+ });
59
+ }
60
+ ```
61
+
62
+ ## License
63
+
64
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crdt-sync/core",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "TypeScript proxy wrapper for the crdt-sync Wasm StateStore",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,7 +12,7 @@
12
12
  "build": "tsc",
13
13
  "build:wasm:bundler": "wasm-pack build ../.. --target bundler --out-dir packages/core/pkg/bundler",
14
14
  "build:wasm:web": "wasm-pack build ../.. --target web --out-dir packages/core/pkg/web",
15
- "build:wasm": "npm run build:wasm:bundler && npm run build:wasm:web",
15
+ "build:wasm": "npm run build:wasm:bundler && npm run build:wasm:web && rm -f pkg/bundler/.gitignore pkg/web/.gitignore",
16
16
  "test": "jest",
17
17
  "lint": "tsc --noEmit"
18
18
  },
@@ -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,95 @@
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
+ }
@@ -0,0 +1,9 @@
1
+ /* @ts-self-types="./crdt_sync.d.ts" */
2
+
3
+ import * as wasm from "./crdt_sync_bg.wasm";
4
+ import { __wbg_set_wasm } from "./crdt_sync_bg.js";
5
+ __wbg_set_wasm(wasm);
6
+ wasm.__wbindgen_start();
7
+ export {
8
+ WasmStateStore
9
+ } from "./crdt_sync_bg.js";