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

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
@@ -6,18 +6,20 @@ state. JavaScript provides loading and a small synchronous API.
6
6
 
7
7
  ## Build from this repository
8
8
 
9
- Install Rust, Node.js 22+, and `wasm-pack`, then run:
9
+ Install Rust, Node.js 22.12+ (or 24+), and `wasm-pack`, then run:
10
10
 
11
11
  ```sh
12
12
  rustup target add wasm32-unknown-unknown
13
13
  cd doxsync-js
14
+ npm ci
14
15
  npm run build
15
16
  node examples/node.mjs
16
17
  ```
17
18
 
18
19
  `npm run build` runs `wasm-pack` in release mode with the locked Cargo
19
- dependencies and the `wasm` feature, then copies the JS wrapper and declarations
20
- into `dist/`. There are no npm runtime dependencies. To create a local npm
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
21
23
  tarball, run `npm pack`; it builds first and includes the WASM artifact.
22
24
  Installing that tarball does not require Rust or wasm-pack.
23
25
 
@@ -37,10 +39,10 @@ also generate the JS glue required to load it.
37
39
  After installing the package from the local tarball:
38
40
 
39
41
  ```js
40
- import { init, Producer, Consumer } from "@doxsync/core";
42
+ import { init, Producer, Consumer, supportedProtocols } from "@doxsync/core";
41
43
 
42
44
  await init();
43
- const producer = new Producer({ count: 1n, ratio: 0.5 }, [1]);
45
+ const producer = new Producer({ count: 1n, ratio: 0.5 }, supportedProtocols());
44
46
  const consumer = new Consumer();
45
47
 
46
48
  try {
@@ -77,13 +79,16 @@ provide garbage-collection cleanup, but its timing is not deterministic.
77
79
 
78
80
  ## WASM initialization
79
81
 
80
- Call and await `init()` before creating either class. Initialization is the
82
+ Call and await `init()` before creating either class or calling
83
+ `supportedProtocols()`. Initialization is the
81
84
  only asynchronous operation. Concurrent calls share the first call's promise
82
85
  and source; successful initialization is reused, and failed initialization
83
86
  can be retried.
84
87
 
85
88
  - Browsers fetch the WASM next to the generated JS. Serve `dist/` over HTTP;
86
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.
87
92
  - Node.js uses the package's conditional export to read the same packaged
88
93
  WASM from disk. Browser entry points contain no Node.js imports.
89
94
  - `init(source)` also accepts a URL string, `URL`, `ArrayBuffer`, `Uint8Array`,
@@ -117,6 +122,7 @@ CommonJS entry point or polyfills are supplied.
117
122
  | `null`, `boolean`, `string` | Null, Bool, UTF-8 text |
118
123
  | `number` | Float, including `-0`, NaN, and infinities |
119
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 |
120
126
  | `Uint8Array` (including Node Buffer) | Binary string, copied |
121
127
  | Dense arrays | Arrays, copied recursively |
122
128
  | Plain or null-prototype objects | Maps with string keys |
@@ -135,8 +141,8 @@ Unsupported values are rejected: `undefined`, functions, symbols (including
135
141
  enumerable symbol map keys), sparse arrays, cyclic references, accessor
136
142
  properties used as document values, out-of-range integers, unpaired UTF-16
137
143
  surrogates in strings or keys, and objects such as Date, Map, Set, or class
138
- instances. Use values from the same JS realm as the library; custom prototypes
139
- and proxies are not supported as a document model.
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.
140
146
 
141
147
  Rust and conversion errors are JS `Error` instances with `kind` equal to
142
148
  `Internal`, `InvalidData`, or `UnexpectedType`. Initialization, lifecycle,
@@ -151,29 +157,111 @@ try {
151
157
  }
152
158
  ```
153
159
 
154
- The JS package exposes only `init`, `Producer`, and `Consumer` at runtime.
160
+ The JS package exposes `init`, `supportedProtocols`, `Producer`, `Consumer`, and `Decimal`
161
+ at runtime.
155
162
  Rust's lower-level Document, Value, and Message APIs remain internal to this
156
163
  binding, and the wire protocol is shared with native Rust.
157
164
 
165
+ ### Decimal values
166
+
167
+ ```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
+ }
180
+ ```
181
+
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
+
158
208
  ### Protocol negotiation
159
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.
220
+
160
221
  The second `Producer` constructor argument is required: pass an array of protocol
161
222
  versions supported by the consumer, such as `[1]`. Versions must be integers in
162
223
  `[0, 2^32 - 1]`. The producer selects the highest version supported by both peers;
163
- order and duplicates do not matter. Currently only version `1` is implemented.
224
+ order and duplicates do not matter. Versions `1` and `2` are implemented.
164
225
  Empty or incompatible lists, omitted arguments, and invalid values throw an error
165
226
  with `kind: "InvalidData"`.
166
227
 
167
228
  The first packed message starts its metadata with protocol instruction `2` followed
168
- by the selected version, currently `1`; both are unsigned variable-length integers.
229
+ by the selected version; both are unsigned variable-length integers.
169
230
  Later messages omit this declaration and retain the same protocol for the stream.
170
231
  The consumer rejects first messages without a leading protocol declaration,
171
232
  unsupported versions, and repeated declarations. Legacy messages without version
172
- metadata are no longer accepted. Both peers must be upgraded together.
233
+ metadata are not accepted. Existing protocol 1 peers can continue to receive
234
+ updates, with decimals represented as strings.
173
235
 
174
236
  Failed encoding or consumption preserves protocol initialization state as well as
175
237
  pool state, allowing retries. Keep and deliver packed messages in order as usual.
176
238
 
239
+ ### Decimal wire encoding
240
+
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.
245
+
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
+
177
265
  ### Path key encoding
178
266
 
179
267
  Protocol v1 path definitions use the low two bits of each segment's unsigned
@@ -191,3 +279,31 @@ through message encoding and are resolved when the consumer reads the definition
191
279
 
192
280
  This extends v1 directly. Both peers must be upgraded together: older decoders
193
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.
303
+
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.
@@ -0,0 +1,3 @@
1
+ import { Decimal } from 'decimal.js';
2
+ export declare function decimalToString(value: unknown): string | undefined;
3
+ export declare function decimalFromString(value: string): Decimal;
package/dist/index.d.ts CHANGED
@@ -1,64 +1,11 @@
1
- /** Numbers are floats; bigint values are integers in [-2^64, 2^64 - 1]. */
2
- export type SyncValue =
3
- | null
4
- | boolean
5
- | string
6
- | number
7
- | bigint
8
- | Uint8Array
9
- | SyncValue[]
10
- | { [key: string]: SyncValue };
11
-
12
- export type InitSource =
13
- | string
14
- | URL
15
- | ArrayBuffer
16
- | Uint8Array
17
- | WebAssembly.Module;
18
-
19
- /** Rust errors thrown by synchronization and value conversion. */
20
- export interface DoxsyncError extends Error {
21
- kind: "Internal" | "InvalidData" | "UnexpectedType";
22
- }
23
-
1
+ import { InitSource } from './types.js';
2
+ export type { SyncValue, InitSource, DoxsyncError } from './types.js';
3
+ export { Decimal } from 'decimal.js';
4
+ export { Producer, Consumer, supportedProtocols } from './runtime.js';
24
5
  /**
25
- * Initialize once before constructing Producer or Consumer.
6
+ * Initialize once before constructing Producer or Consumer or calling supportedProtocols.
26
7
  * Defaults to the packaged WASM. Concurrent calls share the first call's source
27
8
  * and promise; failures can be retried. Later successful calls are no-ops.
28
9
  * Strings are URLs, not filesystem paths; Node also accepts file: URLs.
29
10
  */
30
- export function init(source?: InitSource): Promise<void>;
31
-
32
- export class Producer {
33
- #private;
34
- /**
35
- * Copies the value into WASM and selects the highest common protocol version.
36
- * protocols lists the consumer's supported versions as u32 integers.
37
- * Currently only 1 is supported. Missing, invalid, empty, or incompatible
38
- * lists throw InvalidData errors. Order and duplicates do not matter.
39
- */
40
- constructor(value: SyncValue, protocols: readonly number[]);
41
- /** Replaces the document after validating and copying the entire value. */
42
- replace(value: SyncValue): void;
43
- /**
44
- * Returns a snapshot with protocol metadata on the first call, then changes.
45
- * Returns undefined if unchanged. Retain each message until delivered in order.
46
- */
47
- produceDiff(): Uint8Array | undefined;
48
- /** Release WASM resources. Safe to call repeatedly. */
49
- free(): void;
50
- }
51
-
52
- export class Consumer {
53
- #private;
54
- constructor();
55
- /**
56
- * Applies one complete message. The first message must declare protocol 1.
57
- * Failures leave the document, pools, and protocol state intact.
58
- */
59
- consumeDiff(bytes: Uint8Array): void;
60
- /** Returns an independent copy, or undefined before the first snapshot. */
61
- document(): SyncValue | undefined;
62
- /** Release WASM resources. Safe to call repeatedly. */
63
- free(): void;
64
- }
11
+ export declare function init(source?: InitSource): Promise<void>;
package/dist/index.js CHANGED
@@ -1,9 +1,12 @@
1
- import { initialize } from "./runtime.js";
2
-
3
- export { Producer, Consumer } from "./runtime.js";
4
-
5
- export function init(source) {
6
- return initialize(() => source === undefined
7
- ? new URL("./wasm/doxsync_bg.wasm", import.meta.url)
8
- : source);
1
+ import { i as e, n as t, r as n, t as r } from "./runtime-BePF61JJ.js";
2
+ import { Decimal as i } from "decimal.js";
3
+ //#region src/index.ts
4
+ function a(e) {
5
+ return n(() => e === void 0 ? new URL(
6
+ /* @vite-ignore */
7
+ "./wasm/doxsync_bg.wasm",
8
+ import.meta.url
9
+ ) : e);
9
10
  }
11
+ //#endregion
12
+ export { r as Consumer, i as Decimal, t as Producer, a as init, e as supportedProtocols };
package/dist/node.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { InitSource } from './types.js';
2
+ export type { SyncValue, InitSource, DoxsyncError } from './types.js';
3
+ export { Decimal } from 'decimal.js';
4
+ export { Producer, Consumer, supportedProtocols } from './runtime.js';
5
+ /**
6
+ * Initialize once before constructing Producer or Consumer or calling supportedProtocols.
7
+ * Defaults to the packaged WASM. Concurrent calls share the first call's source
8
+ * and promise; failures can be retried. Later successful calls are no-ops.
9
+ * Strings are URLs, not filesystem paths; Node also accepts file: URLs.
10
+ */
11
+ export declare function init(source?: InitSource): Promise<void>;
package/dist/node.js CHANGED
@@ -1,19 +1,13 @@
1
- import { readFile } from "node:fs/promises";
2
- import { initialize } from "./runtime.js";
3
-
4
- export { Producer, Consumer } from "./runtime.js";
5
-
6
- export function init(source) {
7
- return initialize(() => {
8
- if (source === undefined) {
9
- return readFile(new URL("./wasm/doxsync_bg.wasm", import.meta.url));
10
- }
11
- if (source instanceof URL && source.protocol === "file:") {
12
- return readFile(source);
13
- }
14
- if (typeof source === "string" && source.startsWith("file:")) {
15
- return readFile(new URL(source));
16
- }
17
- return source;
18
- });
1
+ import { i as e, n as t, r as n, t as r } from "./runtime-BePF61JJ.js";
2
+ import { Decimal as i } from "decimal.js";
3
+ import { readFile as a } from "node:fs/promises";
4
+ //#region src/node.ts
5
+ function o(e) {
6
+ return n(() => e === void 0 ? a(new URL(
7
+ /* @vite-ignore */
8
+ "./wasm/doxsync_bg.wasm",
9
+ import.meta.url
10
+ )) : e instanceof URL && e.protocol === "file:" ? a(e) : typeof e == "string" && e.startsWith("file:") ? a(new URL(e)) : e);
19
11
  }
12
+ //#endregion
13
+ export { r as Consumer, i as Decimal, t as Producer, o as init, e as supportedProtocols };
@@ -0,0 +1,72 @@
1
+ import { Decimal as e } from "decimal.js";
2
+ import t, { Consumer as n, Producer as r, supportedProtocols as i } from "./wasm/doxsync.js";
3
+ //#region src/decimal.ts
4
+ var a = e.clone({
5
+ precision: 29,
6
+ minE: -100,
7
+ maxE: 100
8
+ });
9
+ function o(t) {
10
+ if (e.isDecimal(t)) {
11
+ if (!t.isFinite() || t.decimalPlaces() > 28 || t.e > 28) throw Object.assign(/* @__PURE__ */ Error("Decimal outside the supported coefficient or scale range"), { kind: "InvalidData" });
12
+ return t.toFixed();
13
+ }
14
+ }
15
+ function s(e) {
16
+ return new a(e);
17
+ }
18
+ //#endregion
19
+ //#region src/runtime.ts
20
+ var c, l = !1;
21
+ function u(e) {
22
+ return c ||= Promise.resolve().then(e).then((e) => t({ module_or_path: e })).then(() => {
23
+ l = !0;
24
+ }).catch((e) => {
25
+ throw c = void 0, e;
26
+ }), c;
27
+ }
28
+ function d() {
29
+ if (!l) throw Error("Call and await init() before using the doxsync API");
30
+ }
31
+ function f() {
32
+ return d(), i();
33
+ }
34
+ var p = class {
35
+ #e;
36
+ constructor(e, t) {
37
+ d(), this.#e = new r(e, t, o);
38
+ }
39
+ #t() {
40
+ if (!this.#e) throw Error("Producer has been freed");
41
+ return this.#e;
42
+ }
43
+ replace(e) {
44
+ this.#t().replace(e);
45
+ }
46
+ produceDiff() {
47
+ return this.#t().produceDiff();
48
+ }
49
+ free() {
50
+ this.#e &&= (this.#e.free(), void 0);
51
+ }
52
+ }, m = class {
53
+ #e;
54
+ constructor() {
55
+ d(), this.#e = new n();
56
+ }
57
+ #t() {
58
+ if (!this.#e) throw Error("Consumer has been freed");
59
+ return this.#e;
60
+ }
61
+ consumeDiff(e) {
62
+ this.#t().consumeDiff(e);
63
+ }
64
+ document() {
65
+ return this.#t().document(s);
66
+ }
67
+ free() {
68
+ this.#e &&= (this.#e.free(), void 0);
69
+ }
70
+ };
71
+ //#endregion
72
+ export { f as i, p as n, u as r, m as t };
@@ -0,0 +1,44 @@
1
+ import { InitSource, SyncValue } from './types.js';
2
+ export declare function initialize(loadSource: () => InitSource | Promise<InitSource>): Promise<void>;
3
+ /**
4
+ * Returns all protocol versions supported by this build, currently [1, 2].
5
+ * Requires await init(). Each call returns an independent ordinary array.
6
+ * Consumers can advertise this list to producers for protocol negotiation.
7
+ */
8
+ export declare function supportedProtocols(): number[];
9
+ export declare class Producer {
10
+ #private;
11
+ /**
12
+ * Copies the value into WASM and selects the highest common protocol version.
13
+ * protocols lists the consumer's supported versions as u32 integers.
14
+ * Versions 1 and 2 are supported. Missing, invalid, empty, or incompatible
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.
18
+ */
19
+ constructor(value: SyncValue, protocols: readonly number[]);
20
+ /** Replaces the document after validating and copying the entire value. */
21
+ replace(value: SyncValue): void;
22
+ /**
23
+ * Returns a snapshot with protocol metadata on the first call, then changes.
24
+ * Returns undefined if unchanged. Retain each message until delivered in order.
25
+ */
26
+ produceDiff(): Uint8Array | undefined;
27
+ /** Release WASM resources. Safe to call repeatedly. */
28
+ free(): void;
29
+ }
30
+ export declare class Consumer {
31
+ #private;
32
+ constructor();
33
+ /**
34
+ * Applies one complete message. The first message must declare protocol 1 or 2.
35
+ * Failures leave the document, pools, and protocol state intact.
36
+ */
37
+ consumeDiff(bytes: Uint8Array): void;
38
+ /** Returns an independent copy, or undefined before the first snapshot.
39
+ * Protocol 2 decimals become decimal.js instances; their original scale is lost.
40
+ */
41
+ document(): SyncValue | undefined;
42
+ /** Release WASM resources. Safe to call repeatedly. */
43
+ free(): void;
44
+ }
@@ -0,0 +1,12 @@
1
+ import { Decimal } from 'decimal.js';
2
+ /** Numbers are floats; bigint values are integers in [-2^64, 2^64 - 1].
3
+ * Decimal instances preserve numeric value, but not trailing zeros.
4
+ */
5
+ export type SyncValue = null | boolean | string | number | bigint | Decimal | Uint8Array | SyncValue[] | {
6
+ [key: string]: SyncValue;
7
+ };
8
+ export type InitSource = string | URL | ArrayBuffer | Uint8Array | WebAssembly.Module;
9
+ /** Rust errors thrown by synchronization and value conversion. */
10
+ export interface DoxsyncError extends Error {
11
+ kind: "Internal" | "InvalidData" | "UnexpectedType";
12
+ }
@@ -5,18 +5,20 @@ export class Consumer {
5
5
  free(): void;
6
6
  [Symbol.dispose](): void;
7
7
  consumeDiff(bytes: any): void;
8
- document(): any;
8
+ document(decimal_from_string: Function): any;
9
9
  constructor();
10
10
  }
11
11
 
12
12
  export class Producer {
13
13
  free(): void;
14
14
  [Symbol.dispose](): void;
15
- constructor(value: any, protocols: any);
15
+ constructor(value: any, protocols: any, decimal_to_string: Function);
16
16
  produceDiff(): any;
17
17
  replace(value: any): void;
18
18
  }
19
19
 
20
+ export function supportedProtocols(): Array<any>;
21
+
20
22
  export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
21
23
 
22
24
  export interface InitOutput {
@@ -24,11 +26,12 @@ export interface InitOutput {
24
26
  readonly __wbg_consumer_free: (a: number, b: number) => void;
25
27
  readonly __wbg_producer_free: (a: number, b: number) => void;
26
28
  readonly consumer_consumeDiff: (a: number, b: any) => [number, number];
27
- readonly consumer_document: (a: number) => [number, number, number];
29
+ readonly consumer_document: (a: number, b: any) => [number, number, number];
28
30
  readonly consumer_new: () => number;
29
- readonly producer_new: (a: any, b: any) => [number, number, number];
31
+ readonly producer_new: (a: any, b: any, c: any) => [number, number, number];
30
32
  readonly producer_produceDiff: (a: number) => [number, number, number];
31
33
  readonly producer_replace: (a: number, b: any) => [number, number];
34
+ readonly supportedProtocols: () => any;
32
35
  readonly __wbindgen_malloc: (a: number, b: number) => number;
33
36
  readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
34
37
  readonly __wbindgen_exn_store: (a: number) => void;
@@ -21,10 +21,11 @@ export class Consumer {
21
21
  }
22
22
  }
23
23
  /**
24
+ * @param {Function} decimal_from_string
24
25
  * @returns {any}
25
26
  */
26
- document() {
27
- const ret = wasm.consumer_document(this.__wbg_ptr);
27
+ document(decimal_from_string) {
28
+ const ret = wasm.consumer_document(this.__wbg_ptr, decimal_from_string);
28
29
  if (ret[2]) {
29
30
  throw takeFromExternrefTable0(ret[1]);
30
31
  }
@@ -53,9 +54,10 @@ export class Producer {
53
54
  /**
54
55
  * @param {any} value
55
56
  * @param {any} protocols
57
+ * @param {Function} decimal_to_string
56
58
  */
57
- constructor(value, protocols) {
58
- const ret = wasm.producer_new(value, protocols);
59
+ constructor(value, protocols, decimal_to_string) {
60
+ const ret = wasm.producer_new(value, protocols, decimal_to_string);
59
61
  if (ret[2]) {
60
62
  throw takeFromExternrefTable0(ret[1]);
61
63
  }
@@ -84,6 +86,14 @@ export class Producer {
84
86
  }
85
87
  }
86
88
  if (Symbol.dispose) Producer.prototype[Symbol.dispose] = Producer.prototype.free;
89
+
90
+ /**
91
+ * @returns {Array<any>}
92
+ */
93
+ export function supportedProtocols() {
94
+ const ret = wasm.supportedProtocols();
95
+ return ret;
96
+ }
87
97
  function __wbg_get_imports() {
88
98
  const import0 = {
89
99
  __proto__: null,
@@ -148,6 +158,10 @@ function __wbg_get_imports() {
148
158
  const ret = arg0.add(arg1);
149
159
  return ret;
150
160
  },
161
+ __wbg_call_6bcf8d3e20937e46: function() { return handleError(function (arg0, arg1, arg2) {
162
+ const ret = arg0.call(arg1, arg2);
163
+ return ret;
164
+ }, arguments); },
151
165
  __wbg_charCodeAt_90635349fbda9f5f: function(arg0, arg1) {
152
166
  const ret = arg0.charCodeAt(arg1 >>> 0);
153
167
  return ret;
Binary file
@@ -4,11 +4,12 @@ export const memory: WebAssembly.Memory;
4
4
  export const __wbg_consumer_free: (a: number, b: number) => void;
5
5
  export const __wbg_producer_free: (a: number, b: number) => void;
6
6
  export const consumer_consumeDiff: (a: number, b: any) => [number, number];
7
- export const consumer_document: (a: number) => [number, number, number];
7
+ export const consumer_document: (a: number, b: any) => [number, number, number];
8
8
  export const consumer_new: () => number;
9
- export const producer_new: (a: any, b: any) => [number, number, number];
9
+ export const producer_new: (a: any, b: any, c: any) => [number, number, number];
10
10
  export const producer_produceDiff: (a: number) => [number, number, number];
11
11
  export const producer_replace: (a: number, b: any) => [number, number];
12
+ export const supportedProtocols: () => any;
12
13
  export const __wbindgen_malloc: (a: number, b: number) => number;
13
14
  export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
14
15
  export const __wbindgen_exn_store: (a: number) => void;
@@ -5,6 +5,9 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>doxsync · Sync playground</title>
7
7
  <link rel="stylesheet" href="./browser.css">
8
+ <script type="importmap">
9
+ { "imports": { "decimal.js": "../node_modules/decimal.js/decimal.mjs" } }
10
+ </script>
8
11
  <script type="module" src="./browser.js"></script>
9
12
  </head>
10
13
  <body>
@@ -11,6 +11,7 @@ let loading = false;
11
11
  const initialDocument = () => ({
12
12
  title: "doxsync playground",
13
13
  visitors: 12n,
14
+ price: new bindings.Decimal("12.34"),
14
15
  online: true,
15
16
  settings: { theme: "light", volume: 60 },
16
17
  tags: ["rust", "wasm"],
@@ -28,6 +29,7 @@ const delayValue = () => {
28
29
  // suffix preserves bigint's type instead of presenting it as a JSON string.
29
30
  function format(value, depth = 0) {
30
31
  if (typeof value === "bigint") return `${value}n`;
32
+ if (bindings.Decimal.isDecimal(value)) return `Decimal("${value.toFixed()}")`;
31
33
  if (value === null || typeof value !== "object") return JSON.stringify(value);
32
34
  const array = Array.isArray(value);
33
35
  const entries = array ? value : Object.keys(value).sort();
@@ -219,6 +221,7 @@ function send(summary) {
219
221
  }
220
222
 
221
223
  const mutations = [
224
+ (doc) => { doc.price = doc.price.plus("0.01"); return `price → ${doc.price.toFixed(2)}`; },
222
225
  (doc) => { const delta = BigInt(number(1, 5)); doc.visitors += delta; return `visitors increased by ${delta}`; },
223
226
  (doc) => { doc.online = !doc.online; return `online → ${doc.online}`; },
224
227
  (doc) => { doc.settings.theme = pick(["light", "dark", "system"].filter((value) => value !== doc.settings.theme)); return `settings.theme → ${doc.settings.theme}`; },
@@ -246,7 +249,13 @@ function mutate() {
246
249
  try {
247
250
  // Producer owns a copy. Keep this display model in step with successfully
248
251
  // accepted replacements; no JS implementation of the diff algorithm.
249
- const next = structuredClone(state.document);
252
+ // Copy this demo's containers explicitly: structuredClone cannot copy Decimal.
253
+ const next = {
254
+ ...state.document,
255
+ settings: { ...state.document.settings },
256
+ tags: [...state.document.tags],
257
+ price: new bindings.Decimal(state.document.price),
258
+ };
250
259
  const summary = pick(mutations)(next);
251
260
  state.producer.replace(next);
252
261
  state.document = next;
@@ -293,7 +302,7 @@ function reset() {
293
302
  dispose();
294
303
  const document = initialDocument();
295
304
  state = {
296
- document, producer: new bindings.Producer(document, [1]), consumer: new bindings.Consumer(),
305
+ document, producer: new bindings.Producer(document, bindings.supportedProtocols()), consumer: new bindings.Consumer(),
297
306
  producerText: format(document), consumerText: "", queue: [], history: [],
298
307
  selected: undefined, sent: 0, totalBytes: 0, unchanged: 0,
299
308
  running: true, failed: false,
package/examples/node.mjs CHANGED
@@ -1,17 +1,18 @@
1
- import { init, Producer, Consumer } from "@doxsync/core";
1
+ import { init, Producer, Consumer, Decimal, supportedProtocols } from "@doxsync/core";
2
2
 
3
3
  await init();
4
- const producer = new Producer({ count: 1n, data: new Uint8Array([1, 2, 3]) }, [1]);
4
+ const producer = new Producer({ count: 1n, price: new Decimal("12.34"), data: new Uint8Array([1, 2, 3]) }, supportedProtocols());
5
5
  const consumer = new Consumer();
6
6
 
7
7
  try {
8
8
  consumer.consumeDiff(producer.produceDiff());
9
9
  console.log("Snapshot:", consumer.document());
10
10
 
11
- producer.replace({ count: 2n, data: new Uint8Array([1, 2, 3]) });
11
+ producer.replace({ count: 2n, price: new Decimal("12.35"), data: new Uint8Array([1, 2, 3]) });
12
12
  const message = producer.produceDiff();
13
13
  if (message !== undefined) consumer.consumeDiff(message);
14
14
  console.log("Updated:", consumer.document());
15
+ console.log("Exact price:", consumer.document().price.toFixed(2));
15
16
  console.log("Unchanged:", producer.produceDiff());
16
17
  } finally {
17
18
  producer.free();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doxsync/core",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.3",
4
4
  "description": "JavaScript bindings for the doxsync Rust synchronization engine",
5
5
  "type": "module",
6
6
  "engines": {
@@ -23,7 +23,7 @@
23
23
  "LICENSE-APACHE"
24
24
  ],
25
25
  "scripts": {
26
- "build": "node scripts/build.mjs",
26
+ "build": "node scripts/build-wasm.mjs && vite build",
27
27
  "prepack": "npm run build"
28
28
  },
29
29
  "license": "MIT OR Apache-2.0",
@@ -31,5 +31,14 @@
31
31
  "type": "git",
32
32
  "url": "git+https://github.com/PeterlitsZo/doxsync.git",
33
33
  "directory": "doxsync-js"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.20.4",
37
+ "typescript": "^6.0.3",
38
+ "vite": "^8.3.0",
39
+ "vite-plugin-dts": "^5.1.1"
40
+ },
41
+ "dependencies": {
42
+ "decimal.js": "^10.6.0"
34
43
  }
35
44
  }
package/dist/runtime.js DELETED
@@ -1,85 +0,0 @@
1
- import initWasm, {
2
- Producer as WasmProducer,
3
- Consumer as WasmConsumer,
4
- } from "./wasm/doxsync.js";
5
-
6
- let initialization;
7
- let initialized = false;
8
-
9
- export function initialize(loadSource) {
10
- if (!initialization) {
11
- initialization = Promise.resolve()
12
- .then(loadSource)
13
- .then((source) => initWasm({ module_or_path: source }))
14
- .then(() => { initialized = true; })
15
- .catch((error) => {
16
- initialization = undefined;
17
- throw error;
18
- });
19
- }
20
- return initialization;
21
- }
22
-
23
- function requireInitialized() {
24
- if (!initialized) {
25
- throw new Error("Call and await init() before creating a doxsync object");
26
- }
27
- }
28
-
29
- export class Producer {
30
- #handle;
31
-
32
- constructor(value, protocols) {
33
- requireInitialized();
34
- this.#handle = new WasmProducer(value, protocols);
35
- }
36
-
37
- #live() {
38
- if (!this.#handle) throw new Error("Producer has been freed");
39
- return this.#handle;
40
- }
41
-
42
- replace(value) {
43
- this.#live().replace(value);
44
- }
45
-
46
- produceDiff() {
47
- return this.#live().produceDiff();
48
- }
49
-
50
- free() {
51
- if (this.#handle) {
52
- this.#handle.free();
53
- this.#handle = undefined;
54
- }
55
- }
56
- }
57
-
58
- export class Consumer {
59
- #handle;
60
-
61
- constructor() {
62
- requireInitialized();
63
- this.#handle = new WasmConsumer();
64
- }
65
-
66
- #live() {
67
- if (!this.#handle) throw new Error("Consumer has been freed");
68
- return this.#handle;
69
- }
70
-
71
- consumeDiff(bytes) {
72
- this.#live().consumeDiff(bytes);
73
- }
74
-
75
- document() {
76
- return this.#live().document();
77
- }
78
-
79
- free() {
80
- if (this.#handle) {
81
- this.#handle.free();
82
- this.#handle = undefined;
83
- }
84
- }
85
- }