@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 +128 -12
- package/dist/decimal.d.ts +3 -0
- package/dist/index.d.ts +6 -59
- package/dist/index.js +11 -8
- package/dist/node.d.ts +11 -0
- package/dist/node.js +12 -18
- package/dist/runtime-BePF61JJ.js +72 -0
- package/dist/runtime.d.ts +44 -0
- package/dist/types.d.ts +12 -0
- package/dist/wasm/doxsync.d.ts +7 -4
- package/dist/wasm/doxsync.js +18 -4
- package/dist/wasm/doxsync_bg.wasm +0 -0
- package/dist/wasm/doxsync_bg.wasm.d.ts +3 -2
- package/examples/browser.html +3 -0
- package/examples/browser.js +11 -2
- package/examples/node.mjs +4 -3
- package/package.json +11 -2
- package/dist/runtime.js +0 -85
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
|
|
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
|
|
20
|
-
|
|
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 },
|
|
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
|
|
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;
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,64 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
export type SyncValue
|
|
3
|
-
|
|
4
|
-
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/wasm/doxsync.d.ts
CHANGED
|
@@ -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;
|
package/dist/wasm/doxsync.js
CHANGED
|
@@ -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;
|
package/examples/browser.html
CHANGED
|
@@ -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>
|
package/examples/browser.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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]) },
|
|
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.
|
|
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
|
-
}
|