@doxsync/core 0.1.0-alpha.3 → 0.1.0-alpha.4

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 CHANGED
@@ -1,309 +1,96 @@
1
1
  # @doxsync/core
2
2
 
3
- ESM bindings for the doxsync Rust synchronization engine, for modern browsers
4
- and Node.js 22+. Rust owns documents, diffing, message encoding, decoding, and
5
- state. JavaScript provides loading and a small synchronous API.
3
+ Synchronize documents with compact updates in modern browsers and Node.js 22+.
4
+ ESM and TypeScript are supported; your application supplies the transport.
6
5
 
7
- ## Build from this repository
8
-
9
- Install Rust, Node.js 22.12+ (or 24+), and `wasm-pack`, then run:
10
-
11
- ```sh
12
- rustup target add wasm32-unknown-unknown
13
- cd doxsync-js
14
- npm ci
15
- npm run build
16
- node examples/node.mjs
17
- ```
18
-
19
- `npm run build` runs `wasm-pack` in release mode with the locked Cargo
20
- dependencies and the `wasm` feature, then builds the TypeScript wrapper with Vite in library mode.
21
- `vite-plugin-dts` generates type declarations in `dist/`. The runtime dependency
22
- `decimal.js` supplies exact JS decimal values. To create a local npm
23
- tarball, run `npm pack`; it builds first and includes the WASM artifact.
24
- Installing that tarball does not require Rust or wasm-pack.
25
-
26
- The Rust crate continues to support native use without JS dependencies:
6
+ ## Install
27
7
 
28
8
  ```sh
29
- cargo build --manifest-path ../doxsync-rs/Cargo.toml
30
- cargo build --manifest-path ../doxsync-rs/Cargo.toml \
31
- --target wasm32-unknown-unknown --features wasm --release
9
+ npm install @doxsync/core
32
10
  ```
33
11
 
34
- The second command produces the Rust WASM artifact. Use `npm run build` to
35
- also generate the JS glue required to load it.
36
-
37
- ## Synchronize a document
38
-
39
- After installing the package from the local tarball:
12
+ ## Quick start
40
13
 
41
14
  ```js
42
15
  import { init, Producer, Consumer, supportedProtocols } from "@doxsync/core";
43
16
 
44
17
  await init();
45
- const producer = new Producer({ count: 1n, ratio: 0.5 }, supportedProtocols());
18
+ const producer = new Producer({ count: 1n }, supportedProtocols());
46
19
  const consumer = new Consumer();
47
20
 
48
21
  try {
49
- consumer.consumeDiff(producer.produceDiff()); // First message is a snapshot.
50
- producer.replace({ count: 2n, ratio: 0.75 });
22
+ consumer.consumeDiff(producer.produceDiff());
23
+ producer.replace({ count: 2n });
51
24
 
52
25
  const message = producer.produceDiff();
53
26
  if (message !== undefined) consumer.consumeDiff(message);
54
27
 
55
- console.log(consumer.document()); // { count: 2n, ratio: 0.75 }
56
- console.log(producer.produceDiff()); // undefined: no changes
28
+ console.log(consumer.document()); // { count: 2n }
57
29
  } finally {
58
30
  producer.free();
59
31
  consumer.free();
60
32
  }
61
33
  ```
62
34
 
63
- Each `consumeDiff` call takes one complete `Uint8Array` message. Transport is
64
- up to the application. Messages must be delivered reliably, exactly once, and
65
- in order to the corresponding consumer: the producer advances its baseline
66
- when it returns a message. Retain those bytes until delivery; calling
67
- `produceDiff` again does not retransmit them. For a new connection without
68
- shared state, create a new producer and consumer and start with a snapshot.
69
-
70
- `replace` copies and validates the input before changing the producer. It
71
- does not emit a message. `document()` returns `undefined` before a snapshot,
72
- and an independent JS value afterwards (including `null` for a null document).
73
- Mutating an input, returned document, or returned message does not change the
74
- engine's stored document. Invalid messages leave consumer state unchanged.
75
-
76
- Call `free()` when finished to release WASM resources promptly. It is
77
- idempotent; business methods throw after release. The generated bindings also
78
- provide garbage-collection cleanup, but its timing is not deterministic.
79
-
80
- ## WASM initialization
81
-
82
- Call and await `init()` before creating either class or calling
83
- `supportedProtocols()`. Initialization is the
84
- only asynchronous operation. Concurrent calls share the first call's promise
85
- and source; successful initialization is reused, and failed initialization
86
- can be retried.
87
-
88
- - Browsers fetch the WASM next to the generated JS. Serve `dist/` over HTTP;
89
- retain the `dist/wasm/` directory and serve `.wasm` as `application/wasm`.
90
- When loading without a bundler, provide an import map for `decimal.js`;
91
- `examples/browser.html` maps it to the local npm installation.
92
- - Node.js uses the package's conditional export to read the same packaged
93
- WASM from disk. Browser entry points contain no Node.js imports.
94
- - `init(source)` also accepts a URL string, `URL`, `ArrayBuffer`, `Uint8Array`,
95
- or compiled `WebAssembly.Module`. Strings are URLs, not filesystem paths.
96
- Node.js additionally reads `file:` URLs from disk.
97
- - Bundlers must deploy the WASM asset. If their asset handling changes its
98
- location, pass the deployed URL or bytes explicitly; the package exports
99
- the asset as `@doxsync/core/doxsync.wasm` for resolution by build tools.
100
-
101
- For a browser example, serve this directory and open `/examples/browser.html`:
102
-
103
- ```sh
104
- python3 -m http.server 8000
105
- ```
106
-
107
- The browser demo changes the producer randomly every 1.5 seconds and delivers
108
- messages after a simulated 1-second delay. It shows both documents, highlights
109
- updated lines, and lists the actual message bytes and delivery status. Adjust
110
- the delay or update interval, pause automatic changes, step manually, or reset
111
- the session. Pausing lets queued messages finish; changing latency never
112
- reorders messages. Some iterations deliberately leave the document unchanged
113
- to demonstrate that no message is produced.
114
-
115
- Browsers must support WebAssembly, BigInt, ESM, and private class fields. No
116
- CommonJS entry point or polyfills are supplied.
117
-
118
- ## Values and errors
35
+ ## API and delivery
119
36
 
120
- | JavaScript | doxsync |
121
- | --- | --- |
122
- | `null`, `boolean`, `string` | Null, Bool, UTF-8 text |
123
- | `number` | Float, including `-0`, NaN, and infinities |
124
- | `bigint` | Int in `[-2^64, 2^64 - 1]` |
125
- | `Decimal` from `decimal.js` | Exact Decimal with a 96-bit coefficient and scale 0–28 |
126
- | `Uint8Array` (including Node Buffer) | Binary string, copied |
127
- | Dense arrays | Arrays, copied recursively |
128
- | Plain or null-prototype objects | Maps with string keys |
37
+ - `await init(source?)`: initialize once before using the API. Other calls are synchronous.
38
+ - `supportedProtocols()`: get supported versions. For remote peers, pass the
39
+ **consumer's** advertised list to `new Producer(value, protocols)`.
40
+ - `producer.replace(value)`: validate and copy a new document without sending it.
41
+ - `producer.produceDiff()`: return a `Uint8Array`, or `undefined` if unchanged.
42
+ The first message contains the complete document.
43
+ - `consumer.consumeDiff(message)`: apply one complete message. Invalid messages
44
+ throw and leave the consumer unchanged.
45
+ - `consumer.document()`: return an independent copy, or `undefined` before the first message.
46
+ - `free()`: release each instance when finished.
129
47
 
130
- `42` and `42n` are different document values. Integers from Rust always read
131
- back as `bigint`, including small integers. Floating-point NaN values remain
132
- NaN, but JS does not promise to preserve NaN payload bits.
48
+ Deliver messages exactly once and in order to the matching consumer. Keep each
49
+ message until delivered: calling `produceDiff()` again does not retransmit it.
50
+ If either side loses its session state, create a new producer and consumer.
133
51
 
134
- Maps use own enumerable string properties, not inherited or non-enumerable
135
- properties. Output maps are ordinary objects; keys such as `__proto__` are
136
- defined as safe own data properties. Arrays serialize their indexed elements;
137
- extra named properties are not part of the document. Object identity and
138
- insertion order are not preserved. Shared references are copied by value.
139
-
140
- Unsupported values are rejected: `undefined`, functions, symbols (including
141
- enumerable symbol map keys), sparse arrays, cyclic references, accessor
142
- properties used as document values, out-of-range integers, unpaired UTF-16
143
- surrogates in strings or keys, and objects such as Date, Map, Set, or class
144
- instances other than Decimal. Use values from the same JS realm as the library;
145
- custom prototypes and proxies are not supported as a document model.
146
-
147
- Rust and conversion errors are JS `Error` instances with `kind` equal to
148
- `Internal`, `InvalidData`, or `UnexpectedType`. Initialization, lifecycle,
149
- and host JavaScript exceptions may be ordinary errors without `kind`.
150
- TypeScript declarations include `SyncValue`, `InitSource`, and `DoxsyncError`.
151
-
152
- ```js
153
- try {
154
- consumer.consumeDiff(receivedBytes);
155
- } catch (error) {
156
- console.error(error.kind, error.message);
157
- }
158
- ```
52
+ ## Supported values
159
53
 
160
- The JS package exposes `init`, `supportedProtocols`, `Producer`, `Consumer`, and `Decimal`
161
- at runtime.
162
- Rust's lower-level Document, Value, and Message APIs remain internal to this
163
- binding, and the wire protocol is shared with native Rust.
54
+ Use `null`, booleans, strings, numbers, `bigint`, `Uint8Array`, dense arrays,
55
+ and plain objects with string keys. Nested values are copied by value.
56
+ `number` and `bigint` are distinct; integers must be in `[-2^64, 2^64 - 1]`.
57
+ `undefined`, cycles, sparse arrays, getters, functions, and objects such as
58
+ `Date`, `Map`, or custom class instances are not supported.
164
59
 
165
- ### Decimal values
60
+ For exact decimals, import `Decimal` from this package and use strings:
166
61
 
167
62
  ```js
168
- import { init, Producer, Consumer, Decimal, supportedProtocols } from "@doxsync/core";
169
-
170
- await init();
171
- const producer = new Producer({ price: new Decimal("12.34") }, supportedProtocols());
172
- const consumer = new Consumer();
173
- try {
174
- consumer.consumeDiff(producer.produceDiff());
175
- console.log(consumer.document().price.toFixed(2)); // "12.34"
176
- } finally {
177
- producer.free();
178
- consumer.free();
179
- }
63
+ import { Decimal } from "@doxsync/core";
64
+ const price = new Decimal("12.34");
180
65
  ```
181
66
 
182
- Pass decimal strings to the Decimal constructor to avoid introducing binary
183
- floating-point rounding before synchronization. Instances imported directly from
184
- `decimal.js` or created by `Decimal.clone()` are also accepted. Values must be
185
- finite and exactly representable using a sign and a 96-bit unsigned coefficient
186
- divided by `10^scale`, with scale from 0 through 28. Invalid values throw `InvalidData`;
187
- conversion never silently rounds. Input `number` values remain Float values.
188
-
189
- Protocol 2 returns decimal.js instances, constructed independently of the user's
190
- global decimal.js configuration with 29 significant digits of arithmetic
191
- precision and an exponent range of -100 through 100. Their initial values are
192
- exact. Arithmetic follows decimal.js precision rules; synchronization does not
193
- implement arithmetic or recover precision already lost by the caller.
194
-
195
- Rust preserves scale: `Decimal::from_str_exact("1.20")` remains distinct from
196
- `Decimal::from_str_exact("1.2")` when wrapped with `Value::decimal`. It also
197
- provides `as_decimal`, `as_decimal_and_modify`, and `value!` conversions for owned
198
- or borrowed Decimal values. Negative zero is normalized without changing scale.
199
- JS decimal.js instances do not retain trailing zeros: a Rust value of `1.20`
200
- read through `document()` and sent back through JS may become `1.2`.
201
-
202
- With a protocol 1 producer (`new Producer(value, [1])`), decimals arrive as
203
- ordinary strings. Rust preserves the source scale in those strings; JS can only
204
- send the scale still present in its decimal.js value. Strings are never
205
- automatically parsed as decimals. Switching between a Decimal and its identical
206
- fallback string produces no change under protocol 1.
207
-
208
- ### Protocol negotiation
209
-
210
- Call `supportedProtocols()` after `await init()` to get all protocol versions
211
- supported by the local build, currently `[1, 2]`. Each call returns an independent
212
- ordinary `number[]`; modifying it does not affect subsequent calls or the engine.
213
- No producer or consumer instance is needed. Rust callers can use
214
- `doxsync::supported_protocols() -> &'static [u32]` without initialization.
215
-
216
- For peers running in separate processes, have the consumer advertise its list
217
- and pass that received list to the producer. The examples above use the local
218
- list because both peers share the same build. This query reports supported
219
- versions, not the version negotiated for an individual stream.
67
+ Decimals must be finite, fit a 96-bit coefficient, and have scale 0–28;
68
+ unsupported values are rejected without rounding. Trailing zeros are not preserved.
69
+ Protocol 1 delivers decimals as strings; protocols 2 and 3 preserve Decimal values.
70
+ The producer automatically selects the highest version supported by both peers.
220
71
 
221
- The second `Producer` constructor argument is required: pass an array of protocol
222
- versions supported by the consumer, such as `[1]`. Versions must be integers in
223
- `[0, 2^32 - 1]`. The producer selects the highest version supported by both peers;
224
- order and duplicates do not matter. Versions `1` and `2` are implemented.
225
- Empty or incompatible lists, omitted arguments, and invalid values throw an error
226
- with `kind: "InvalidData"`.
72
+ Errors may expose `kind`: `InvalidData`, `UnexpectedType`, or `Internal`.
73
+ Initialization and lifecycle errors may be ordinary JavaScript errors.
227
74
 
228
- The first packed message starts its metadata with protocol instruction `2` followed
229
- by the selected version; both are unsigned variable-length integers.
230
- Later messages omit this declaration and retain the same protocol for the stream.
231
- The consumer rejects first messages without a leading protocol declaration,
232
- unsupported versions, and repeated declarations. Legacy messages without version
233
- metadata are not accepted. Existing protocol 1 peers can continue to receive
234
- updates, with decimals represented as strings.
75
+ ## Browser setup
235
76
 
236
- Failed encoding or consumption preserves protocol initialization state as well as
237
- pool state, allowing retries. Keep and deliver packed messages in order as usual.
77
+ Deploy the packaged WASM asset alongside the JavaScript. If your bundler relocates
78
+ it, pass its deployed URL or bytes to `init(source)`. The asset is exported as
79
+ `@doxsync/core/doxsync.wasm`; serve `.wasm` files as `application/wasm`.
80
+ Node.js loads the packaged asset automatically. Without a bundler, also provide
81
+ an import map for `decimal.js`; see the [browser example](examples/browser.html).
238
82
 
239
- ### Decimal wire encoding
83
+ ## Build from source
240
84
 
241
- Protocol 2 represents `sign × coefficient × 10^-scale`. The first byte's high
242
- nibble is `0x9` for nonnegative decimals or `0xA` for negative decimals; `0x6`
243
- remains reserved. Its low nibble directly contains scales 0–14. A low nibble of
244
- `0xF` is followed by one byte containing the complete scale, 15–28.
85
+ Requires Rust, wasm-pack, Node.js 22.12+ (or 24+), and pnpm 9.15.0.
245
86
 
246
- The absolute coefficient follows as minimal unsigned LEB128: seven data bits
247
- per byte, least significant group first, with the high bit indicating continuation.
248
- Zero uses one `00` byte. There is no ZigZag transform or negative offset.
249
- Coefficients occupy 1–14 bytes and must fit in 96 bits. The fourteenth byte
250
- must have its continuation bit clear and contain at most `0x1F`.
251
-
252
- | Value | Hex bytes (excluding the message header) |
253
- | --- | --- |
254
- | `0.01` | `92 01` |
255
- | `1.20` | `92 78` |
256
- | `12.34` | `92 D2 09` |
257
- | `-12.34` | `A2 D2 09` |
258
- | `0.00` | `92 00` |
259
- | `0.000000000000001` | `9F 0F 01` |
260
-
261
- The maximum value encoding length is 16 bytes. Decoders reject negative zero,
262
- non-minimal coefficients, invalid scales, overflow, truncation, and Decimal
263
- tags in protocol 1. Existing Int encoding is unchanged.
264
-
265
- ### Path key encoding
266
-
267
- Protocol v1 path definitions use the low two bits of each segment's unsigned
268
- variable-length integer as a tag: `0b00` carries a UTF-8 byte length followed by
269
- the key bytes, `0b01` carries an array index, and `0b10` carries a string pool
270
- index. The payload is shifted left by two bits before adding the tag; `0b11`
271
- is reserved and rejected.
272
-
273
- A cached key uses `0b10` only when its reference is strictly shorter than its
274
- literal encoding. Uncached keys remain inline without adding string pool entries.
275
- The initial snapshot populates the string pool, so subsequent path definitions
276
- can reuse those keys. Once a whole path is cached, actions reuse its path pool ID.
277
- String pool updates precede path definitions; referenced strings stay resident
278
- through message encoding and are resolved when the consumer reads the definition.
279
-
280
- This extends v1 directly. Both peers must be upgraded together: older decoders
281
- reject the new `0b10` path segment tag.
282
-
283
- ### Compact floating-point values
284
-
285
- Protocol 2 encodes numbers as binary16, binary32, or
286
- binary64, choosing the shortest representation that preserves the original
287
- binary64 bits. Decoded values remain JavaScript numbers. Signed zero is
288
- preserved; NaNs retain binary64 encoding. Protocol 1 continues to use
289
- binary64 for every number.
290
-
291
- ### Binary value reuse
292
-
293
- Protocol 2 pools binary values by content, so repeated `Uint8Array` values can
294
- share a compact reference within a message and across subsequent messages.
295
- Producers admit payloads from 16 bytes through 64 KiB, using up to 256 slots
296
- and at most 1 MiB of definitions per message. Values that cannot be admitted
297
- remain inline. The first definition has an overhead, so pooling does not make
298
- every message smaller.
299
-
300
- The Rust pool shares immutable payload storage. Each `document()` conversion
301
- still returns independent `Uint8Array` copies: modifying one field cannot change
302
- another field, the consumer's document, or a later message.
87
+ ```sh
88
+ rustup target add wasm32-unknown-unknown
89
+ cd doxsync-js
90
+ pnpm install --frozen-lockfile
91
+ pnpm build
92
+ pnpm example
93
+ ```
303
94
 
304
- This extends unreleased protocol 2 without adding a version. New consumers accept
305
- earlier v2 messages, but earlier v2 consumers cannot read binary pool definitions
306
- or references. Upgrade both v2 peers together; explicitly select `[1]` when
307
- communicating with an implementation that lacks this extension. Version
308
- negotiation alone cannot distinguish these v2 builds. Protocol 1 continues to
309
- encode binary values inline.
95
+ To run the browser example, serve `doxsync-js/` with `python3 -m http.server 8000`
96
+ and open `/packages/core/examples/browser.html`.
package/dist/runtime.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { InitSource, SyncValue } from './types.js';
2
2
  export declare function initialize(loadSource: () => InitSource | Promise<InitSource>): Promise<void>;
3
3
  /**
4
- * Returns all protocol versions supported by this build, currently [1, 2].
4
+ * Returns all protocol versions supported by this build, currently [1, 2, 3].
5
5
  * Requires await init(). Each call returns an independent ordinary array.
6
6
  * Consumers can advertise this list to producers for protocol negotiation.
7
7
  */
@@ -11,10 +11,10 @@ export declare class Producer {
11
11
  /**
12
12
  * Copies the value into WASM and selects the highest common protocol version.
13
13
  * protocols lists the consumer's supported versions as u32 integers.
14
- * Versions 1 and 2 are supported. Missing, invalid, empty, or incompatible
14
+ * Versions 1, 2, and 3 are supported. Missing, invalid, empty, or incompatible
15
15
  * lists throw InvalidData errors. Order and duplicates do not matter.
16
- * Protocol 1 transmits Decimal values as strings; protocol 2 preserves the type.
17
- * Protocol 2 also pools binary values; both peers must support that extension.
16
+ * Protocol 1 transmits Decimal values as strings; protocols 2 and 3 preserve the type.
17
+ * Protocols 2 and 3 also pool binary values; both peers must support that extension.
18
18
  */
19
19
  constructor(value: SyncValue, protocols: readonly number[]);
20
20
  /** Replaces the document after validating and copying the entire value. */
@@ -31,12 +31,12 @@ export declare class Consumer {
31
31
  #private;
32
32
  constructor();
33
33
  /**
34
- * Applies one complete message. The first message must declare protocol 1 or 2.
34
+ * Applies one complete message. The first message must declare protocol 1, 2, or 3.
35
35
  * Failures leave the document, pools, and protocol state intact.
36
36
  */
37
37
  consumeDiff(bytes: Uint8Array): void;
38
38
  /** Returns an independent copy, or undefined before the first snapshot.
39
- * Protocol 2 decimals become decimal.js instances; their original scale is lost.
39
+ * Protocol 2 and 3 decimals become decimal.js instances; their original scale is lost.
40
40
  */
41
41
  document(): SyncValue | undefined;
42
42
  /** Release WASM resources. Safe to call repeatedly. */
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doxsync/core",
3
- "version": "0.1.0-alpha.3",
3
+ "version": "0.1.0-alpha.4",
4
4
  "description": "JavaScript bindings for the doxsync Rust synchronization engine",
5
5
  "type": "module",
6
6
  "engines": {
@@ -24,13 +24,13 @@
24
24
  ],
25
25
  "scripts": {
26
26
  "build": "node scripts/build-wasm.mjs && vite build",
27
- "prepack": "npm run build"
27
+ "prepack": "pnpm run build"
28
28
  },
29
29
  "license": "MIT OR Apache-2.0",
30
30
  "repository": {
31
31
  "type": "git",
32
32
  "url": "git+https://github.com/PeterlitsZo/doxsync.git",
33
- "directory": "doxsync-js"
33
+ "directory": "doxsync-js/packages/core"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^22.20.4",