@doxsync/core 0.1.0-alpha.2 → 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 +65 -176
- package/dist/decimal.d.ts +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -3
- package/dist/node.d.ts +1 -0
- package/dist/node.js +7 -6
- package/dist/runtime-BePF61JJ.js +72 -0
- package/dist/runtime.d.ts +8 -4
- package/dist/types.d.ts +5 -2
- package/dist/wasm/doxsync.d.ts +4 -4
- package/dist/wasm/doxsync.js +10 -4
- package/dist/wasm/doxsync_bg.wasm +0 -0
- package/dist/wasm/doxsync_bg.wasm.d.ts +2 -2
- package/examples/browser.html +3 -0
- package/examples/browser.js +10 -1
- package/examples/node.mjs +4 -3
- package/package.json +6 -3
- package/dist/runtime-CJqt5vND.js +0 -55
package/README.md
CHANGED
|
@@ -1,207 +1,96 @@
|
|
|
1
1
|
# @doxsync/core
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
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
|
-
##
|
|
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/`. There are no npm runtime dependencies. To create a local npm
|
|
22
|
-
tarball, run `npm pack`; it builds first and includes the WASM artifact.
|
|
23
|
-
Installing that tarball does not require Rust or wasm-pack.
|
|
24
|
-
|
|
25
|
-
The Rust crate continues to support native use without JS dependencies:
|
|
6
|
+
## Install
|
|
26
7
|
|
|
27
8
|
```sh
|
|
28
|
-
|
|
29
|
-
cargo build --manifest-path ../doxsync-rs/Cargo.toml \
|
|
30
|
-
--target wasm32-unknown-unknown --features wasm --release
|
|
9
|
+
npm install @doxsync/core
|
|
31
10
|
```
|
|
32
11
|
|
|
33
|
-
|
|
34
|
-
also generate the JS glue required to load it.
|
|
35
|
-
|
|
36
|
-
## Synchronize a document
|
|
37
|
-
|
|
38
|
-
After installing the package from the local tarball:
|
|
12
|
+
## Quick start
|
|
39
13
|
|
|
40
14
|
```js
|
|
41
15
|
import { init, Producer, Consumer, supportedProtocols } from "@doxsync/core";
|
|
42
16
|
|
|
43
17
|
await init();
|
|
44
|
-
const producer = new Producer({ count: 1n
|
|
18
|
+
const producer = new Producer({ count: 1n }, supportedProtocols());
|
|
45
19
|
const consumer = new Consumer();
|
|
46
20
|
|
|
47
21
|
try {
|
|
48
|
-
consumer.consumeDiff(producer.produceDiff());
|
|
49
|
-
producer.replace({ count: 2n
|
|
22
|
+
consumer.consumeDiff(producer.produceDiff());
|
|
23
|
+
producer.replace({ count: 2n });
|
|
50
24
|
|
|
51
25
|
const message = producer.produceDiff();
|
|
52
26
|
if (message !== undefined) consumer.consumeDiff(message);
|
|
53
27
|
|
|
54
|
-
console.log(consumer.document()); // { count: 2n
|
|
55
|
-
console.log(producer.produceDiff()); // undefined: no changes
|
|
28
|
+
console.log(consumer.document()); // { count: 2n }
|
|
56
29
|
} finally {
|
|
57
30
|
producer.free();
|
|
58
31
|
consumer.free();
|
|
59
32
|
}
|
|
60
33
|
```
|
|
61
34
|
|
|
62
|
-
|
|
63
|
-
up to the application. Messages must be delivered reliably, exactly once, and
|
|
64
|
-
in order to the corresponding consumer: the producer advances its baseline
|
|
65
|
-
when it returns a message. Retain those bytes until delivery; calling
|
|
66
|
-
`produceDiff` again does not retransmit them. For a new connection without
|
|
67
|
-
shared state, create a new producer and consumer and start with a snapshot.
|
|
68
|
-
|
|
69
|
-
`replace` copies and validates the input before changing the producer. It
|
|
70
|
-
does not emit a message. `document()` returns `undefined` before a snapshot,
|
|
71
|
-
and an independent JS value afterwards (including `null` for a null document).
|
|
72
|
-
Mutating an input, returned document, or returned message does not change the
|
|
73
|
-
engine's stored document. Invalid messages leave consumer state unchanged.
|
|
74
|
-
|
|
75
|
-
Call `free()` when finished to release WASM resources promptly. It is
|
|
76
|
-
idempotent; business methods throw after release. The generated bindings also
|
|
77
|
-
provide garbage-collection cleanup, but its timing is not deterministic.
|
|
78
|
-
|
|
79
|
-
## WASM initialization
|
|
80
|
-
|
|
81
|
-
Call and await `init()` before creating either class or calling
|
|
82
|
-
`supportedProtocols()`. Initialization is the
|
|
83
|
-
only asynchronous operation. Concurrent calls share the first call's promise
|
|
84
|
-
and source; successful initialization is reused, and failed initialization
|
|
85
|
-
can be retried.
|
|
86
|
-
|
|
87
|
-
- Browsers fetch the WASM next to the generated JS. Serve `dist/` over HTTP;
|
|
88
|
-
retain the `dist/wasm/` directory and serve `.wasm` as `application/wasm`.
|
|
89
|
-
- Node.js uses the package's conditional export to read the same packaged
|
|
90
|
-
WASM from disk. Browser entry points contain no Node.js imports.
|
|
91
|
-
- `init(source)` also accepts a URL string, `URL`, `ArrayBuffer`, `Uint8Array`,
|
|
92
|
-
or compiled `WebAssembly.Module`. Strings are URLs, not filesystem paths.
|
|
93
|
-
Node.js additionally reads `file:` URLs from disk.
|
|
94
|
-
- Bundlers must deploy the WASM asset. If their asset handling changes its
|
|
95
|
-
location, pass the deployed URL or bytes explicitly; the package exports
|
|
96
|
-
the asset as `@doxsync/core/doxsync.wasm` for resolution by build tools.
|
|
97
|
-
|
|
98
|
-
For a browser example, serve this directory and open `/examples/browser.html`:
|
|
35
|
+
## API and delivery
|
|
99
36
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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.
|
|
47
|
+
|
|
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.
|
|
51
|
+
|
|
52
|
+
## Supported values
|
|
103
53
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
Browsers must support WebAssembly, BigInt, ESM, and private class fields. No
|
|
113
|
-
CommonJS entry point or polyfills are supplied.
|
|
114
|
-
|
|
115
|
-
## Values and errors
|
|
116
|
-
|
|
117
|
-
| JavaScript | doxsync |
|
|
118
|
-
| --- | --- |
|
|
119
|
-
| `null`, `boolean`, `string` | Null, Bool, UTF-8 text |
|
|
120
|
-
| `number` | Float, including `-0`, NaN, and infinities |
|
|
121
|
-
| `bigint` | Int in `[-2^64, 2^64 - 1]` |
|
|
122
|
-
| `Uint8Array` (including Node Buffer) | Binary string, copied |
|
|
123
|
-
| Dense arrays | Arrays, copied recursively |
|
|
124
|
-
| Plain or null-prototype objects | Maps with string keys |
|
|
125
|
-
|
|
126
|
-
`42` and `42n` are different document values. Integers from Rust always read
|
|
127
|
-
back as `bigint`, including small integers. Floating-point NaN values remain
|
|
128
|
-
NaN, but JS does not promise to preserve NaN payload bits.
|
|
129
|
-
|
|
130
|
-
Maps use own enumerable string properties, not inherited or non-enumerable
|
|
131
|
-
properties. Output maps are ordinary objects; keys such as `__proto__` are
|
|
132
|
-
defined as safe own data properties. Arrays serialize their indexed elements;
|
|
133
|
-
extra named properties are not part of the document. Object identity and
|
|
134
|
-
insertion order are not preserved. Shared references are copied by value.
|
|
135
|
-
|
|
136
|
-
Unsupported values are rejected: `undefined`, functions, symbols (including
|
|
137
|
-
enumerable symbol map keys), sparse arrays, cyclic references, accessor
|
|
138
|
-
properties used as document values, out-of-range integers, unpaired UTF-16
|
|
139
|
-
surrogates in strings or keys, and objects such as Date, Map, Set, or class
|
|
140
|
-
instances. Use values from the same JS realm as the library; custom prototypes
|
|
141
|
-
and proxies are not supported as a document model.
|
|
142
|
-
|
|
143
|
-
Rust and conversion errors are JS `Error` instances with `kind` equal to
|
|
144
|
-
`Internal`, `InvalidData`, or `UnexpectedType`. Initialization, lifecycle,
|
|
145
|
-
and host JavaScript exceptions may be ordinary errors without `kind`.
|
|
146
|
-
TypeScript declarations include `SyncValue`, `InitSource`, and `DoxsyncError`.
|
|
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.
|
|
59
|
+
|
|
60
|
+
For exact decimals, import `Decimal` from this package and use strings:
|
|
147
61
|
|
|
148
62
|
```js
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
63
|
+
import { Decimal } from "@doxsync/core";
|
|
64
|
+
const price = new Decimal("12.34");
|
|
65
|
+
```
|
|
66
|
+
|
|
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.
|
|
71
|
+
|
|
72
|
+
Errors may expose `kind`: `InvalidData`, `UnexpectedType`, or `Internal`.
|
|
73
|
+
Initialization and lifecycle errors may be ordinary JavaScript errors.
|
|
74
|
+
|
|
75
|
+
## Browser setup
|
|
76
|
+
|
|
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).
|
|
82
|
+
|
|
83
|
+
## Build from source
|
|
84
|
+
|
|
85
|
+
Requires Rust, wasm-pack, Node.js 22.12+ (or 24+), and pnpm 9.15.0.
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
rustup target add wasm32-unknown-unknown
|
|
89
|
+
cd doxsync-js
|
|
90
|
+
pnpm install --frozen-lockfile
|
|
91
|
+
pnpm build
|
|
92
|
+
pnpm example
|
|
154
93
|
```
|
|
155
94
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
Rust's lower-level Document, Value, and Message APIs remain internal to this
|
|
159
|
-
binding, and the wire protocol is shared with native Rust.
|
|
160
|
-
|
|
161
|
-
### Protocol negotiation
|
|
162
|
-
|
|
163
|
-
Call `supportedProtocols()` after `await init()` to get all protocol versions
|
|
164
|
-
supported by the local build, currently `[1]`. Each call returns an independent
|
|
165
|
-
ordinary `number[]`; modifying it does not affect subsequent calls or the engine.
|
|
166
|
-
No producer or consumer instance is needed. Rust callers can use
|
|
167
|
-
`doxsync::supported_protocols() -> &'static [u32]` without initialization.
|
|
168
|
-
|
|
169
|
-
For peers running in separate processes, have the consumer advertise its list
|
|
170
|
-
and pass that received list to the producer. The examples above use the local
|
|
171
|
-
list because both peers share the same build. This query reports supported
|
|
172
|
-
versions, not the version negotiated for an individual stream.
|
|
173
|
-
|
|
174
|
-
The second `Producer` constructor argument is required: pass an array of protocol
|
|
175
|
-
versions supported by the consumer, such as `[1]`. Versions must be integers in
|
|
176
|
-
`[0, 2^32 - 1]`. The producer selects the highest version supported by both peers;
|
|
177
|
-
order and duplicates do not matter. Currently only version `1` is implemented.
|
|
178
|
-
Empty or incompatible lists, omitted arguments, and invalid values throw an error
|
|
179
|
-
with `kind: "InvalidData"`.
|
|
180
|
-
|
|
181
|
-
The first packed message starts its metadata with protocol instruction `2` followed
|
|
182
|
-
by the selected version, currently `1`; both are unsigned variable-length integers.
|
|
183
|
-
Later messages omit this declaration and retain the same protocol for the stream.
|
|
184
|
-
The consumer rejects first messages without a leading protocol declaration,
|
|
185
|
-
unsupported versions, and repeated declarations. Legacy messages without version
|
|
186
|
-
metadata are no longer accepted. Both peers must be upgraded together.
|
|
187
|
-
|
|
188
|
-
Failed encoding or consumption preserves protocol initialization state as well as
|
|
189
|
-
pool state, allowing retries. Keep and deliver packed messages in order as usual.
|
|
190
|
-
|
|
191
|
-
### Path key encoding
|
|
192
|
-
|
|
193
|
-
Protocol v1 path definitions use the low two bits of each segment's unsigned
|
|
194
|
-
variable-length integer as a tag: `0b00` carries a UTF-8 byte length followed by
|
|
195
|
-
the key bytes, `0b01` carries an array index, and `0b10` carries a string pool
|
|
196
|
-
index. The payload is shifted left by two bits before adding the tag; `0b11`
|
|
197
|
-
is reserved and rejected.
|
|
198
|
-
|
|
199
|
-
A cached key uses `0b10` only when its reference is strictly shorter than its
|
|
200
|
-
literal encoding. Uncached keys remain inline without adding string pool entries.
|
|
201
|
-
The initial snapshot populates the string pool, so subsequent path definitions
|
|
202
|
-
can reuse those keys. Once a whole path is cached, actions reuse its path pool ID.
|
|
203
|
-
String pool updates precede path definitions; referenced strings stay resident
|
|
204
|
-
through message encoding and are resolved when the consumer reads the definition.
|
|
205
|
-
|
|
206
|
-
This extends v1 directly. Both peers must be upgraded together: older decoders
|
|
207
|
-
reject the new `0b10` path segment tag.
|
|
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/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { InitSource } from './types.js';
|
|
2
2
|
export type { SyncValue, InitSource, DoxsyncError } from './types.js';
|
|
3
|
+
export { Decimal } from 'decimal.js';
|
|
3
4
|
export { Producer, Consumer, supportedProtocols } from './runtime.js';
|
|
4
5
|
/**
|
|
5
6
|
* Initialize once before constructing Producer or Consumer or calling supportedProtocols.
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { i as e, n as t, r as n, t as r } from "./runtime-
|
|
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";
|
|
2
3
|
//#region src/index.ts
|
|
3
|
-
function
|
|
4
|
+
function a(e) {
|
|
4
5
|
return n(() => e === void 0 ? new URL(
|
|
5
6
|
/* @vite-ignore */
|
|
6
7
|
"./wasm/doxsync_bg.wasm",
|
|
@@ -8,4 +9,4 @@ function i(e) {
|
|
|
8
9
|
) : e);
|
|
9
10
|
}
|
|
10
11
|
//#endregion
|
|
11
|
-
export { r as Consumer, t as Producer,
|
|
12
|
+
export { r as Consumer, i as Decimal, t as Producer, a as init, e as supportedProtocols };
|
package/dist/node.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { InitSource } from './types.js';
|
|
2
2
|
export type { SyncValue, InitSource, DoxsyncError } from './types.js';
|
|
3
|
+
export { Decimal } from 'decimal.js';
|
|
3
4
|
export { Producer, Consumer, supportedProtocols } from './runtime.js';
|
|
4
5
|
/**
|
|
5
6
|
* Initialize once before constructing Producer or Consumer or calling supportedProtocols.
|
package/dist/node.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { i as e, n as t, r as n, t as r } from "./runtime-
|
|
2
|
-
import {
|
|
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";
|
|
3
4
|
//#region src/node.ts
|
|
4
|
-
function
|
|
5
|
-
return n(() => e === void 0 ?
|
|
5
|
+
function o(e) {
|
|
6
|
+
return n(() => e === void 0 ? a(new URL(
|
|
6
7
|
/* @vite-ignore */
|
|
7
8
|
"./wasm/doxsync_bg.wasm",
|
|
8
9
|
import.meta.url
|
|
9
|
-
)) : e instanceof URL && e.protocol === "file:" ?
|
|
10
|
+
)) : e instanceof URL && e.protocol === "file:" ? a(e) : typeof e == "string" && e.startsWith("file:") ? a(new URL(e)) : e);
|
|
10
11
|
}
|
|
11
12
|
//#endregion
|
|
12
|
-
export { r as Consumer, t as Producer,
|
|
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 };
|
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].
|
|
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,8 +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
|
-
*
|
|
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; protocols 2 and 3 preserve the type.
|
|
17
|
+
* Protocols 2 and 3 also pool binary values; both peers must support that extension.
|
|
16
18
|
*/
|
|
17
19
|
constructor(value: SyncValue, protocols: readonly number[]);
|
|
18
20
|
/** Replaces the document after validating and copying the entire value. */
|
|
@@ -29,11 +31,13 @@ export declare class Consumer {
|
|
|
29
31
|
#private;
|
|
30
32
|
constructor();
|
|
31
33
|
/**
|
|
32
|
-
* Applies one complete message. The first message must declare protocol 1.
|
|
34
|
+
* Applies one complete message. The first message must declare protocol 1, 2, or 3.
|
|
33
35
|
* Failures leave the document, pools, and protocol state intact.
|
|
34
36
|
*/
|
|
35
37
|
consumeDiff(bytes: Uint8Array): void;
|
|
36
|
-
/** Returns an independent copy, or undefined before the first snapshot.
|
|
38
|
+
/** Returns an independent copy, or undefined before the first snapshot.
|
|
39
|
+
* Protocol 2 and 3 decimals become decimal.js instances; their original scale is lost.
|
|
40
|
+
*/
|
|
37
41
|
document(): SyncValue | undefined;
|
|
38
42
|
/** Release WASM resources. Safe to call repeatedly. */
|
|
39
43
|
free(): void;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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[] | {
|
|
3
6
|
[key: string]: SyncValue;
|
|
4
7
|
};
|
|
5
8
|
export type InitSource = string | URL | ArrayBuffer | Uint8Array | WebAssembly.Module;
|
package/dist/wasm/doxsync.d.ts
CHANGED
|
@@ -5,14 +5,14 @@ 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
|
}
|
|
@@ -26,9 +26,9 @@ export interface InitOutput {
|
|
|
26
26
|
readonly __wbg_consumer_free: (a: number, b: number) => void;
|
|
27
27
|
readonly __wbg_producer_free: (a: number, b: number) => void;
|
|
28
28
|
readonly consumer_consumeDiff: (a: number, b: any) => [number, number];
|
|
29
|
-
readonly consumer_document: (a: number) => [number, number, number];
|
|
29
|
+
readonly consumer_document: (a: number, b: any) => [number, number, number];
|
|
30
30
|
readonly consumer_new: () => number;
|
|
31
|
-
readonly producer_new: (a: any, b: any) => [number, number, number];
|
|
31
|
+
readonly producer_new: (a: any, b: any, c: any) => [number, number, number];
|
|
32
32
|
readonly producer_produceDiff: (a: number) => [number, number, number];
|
|
33
33
|
readonly producer_replace: (a: number, b: any) => [number, number];
|
|
34
34
|
readonly supportedProtocols: () => any;
|
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
|
}
|
|
@@ -156,6 +158,10 @@ function __wbg_get_imports() {
|
|
|
156
158
|
const ret = arg0.add(arg1);
|
|
157
159
|
return ret;
|
|
158
160
|
},
|
|
161
|
+
__wbg_call_6bcf8d3e20937e46: function() { return handleError(function (arg0, arg1, arg2) {
|
|
162
|
+
const ret = arg0.call(arg1, arg2);
|
|
163
|
+
return ret;
|
|
164
|
+
}, arguments); },
|
|
159
165
|
__wbg_charCodeAt_90635349fbda9f5f: function(arg0, arg1) {
|
|
160
166
|
const ret = arg0.charCodeAt(arg1 >>> 0);
|
|
161
167
|
return ret;
|
|
Binary file
|
|
@@ -4,9 +4,9 @@ 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
12
|
export const supportedProtocols: () => any;
|
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;
|
package/examples/node.mjs
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import { init, Producer, Consumer, supportedProtocols } 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]) }, supportedProtocols());
|
|
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.4",
|
|
4
4
|
"description": "JavaScript bindings for the doxsync Rust synchronization engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -24,18 +24,21 @@
|
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "node scripts/build-wasm.mjs && vite build",
|
|
27
|
-
"prepack": "
|
|
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",
|
|
37
37
|
"typescript": "^6.0.3",
|
|
38
38
|
"vite": "^8.3.0",
|
|
39
39
|
"vite-plugin-dts": "^5.1.1"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"decimal.js": "^10.6.0"
|
|
40
43
|
}
|
|
41
44
|
}
|
package/dist/runtime-CJqt5vND.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import e, { Consumer as t, Producer as n, supportedProtocols as r } from "./wasm/doxsync.js";
|
|
2
|
-
//#region src/runtime.ts
|
|
3
|
-
var i, a = !1;
|
|
4
|
-
function o(t) {
|
|
5
|
-
return i ||= Promise.resolve().then(t).then((t) => e({ module_or_path: t })).then(() => {
|
|
6
|
-
a = !0;
|
|
7
|
-
}).catch((e) => {
|
|
8
|
-
throw i = void 0, e;
|
|
9
|
-
}), i;
|
|
10
|
-
}
|
|
11
|
-
function s() {
|
|
12
|
-
if (!a) throw Error("Call and await init() before using the doxsync API");
|
|
13
|
-
}
|
|
14
|
-
function c() {
|
|
15
|
-
return s(), r();
|
|
16
|
-
}
|
|
17
|
-
var l = class {
|
|
18
|
-
#e;
|
|
19
|
-
constructor(e, t) {
|
|
20
|
-
s(), this.#e = new n(e, t);
|
|
21
|
-
}
|
|
22
|
-
#t() {
|
|
23
|
-
if (!this.#e) throw Error("Producer has been freed");
|
|
24
|
-
return this.#e;
|
|
25
|
-
}
|
|
26
|
-
replace(e) {
|
|
27
|
-
this.#t().replace(e);
|
|
28
|
-
}
|
|
29
|
-
produceDiff() {
|
|
30
|
-
return this.#t().produceDiff();
|
|
31
|
-
}
|
|
32
|
-
free() {
|
|
33
|
-
this.#e &&= (this.#e.free(), void 0);
|
|
34
|
-
}
|
|
35
|
-
}, u = class {
|
|
36
|
-
#e;
|
|
37
|
-
constructor() {
|
|
38
|
-
s(), this.#e = new t();
|
|
39
|
-
}
|
|
40
|
-
#t() {
|
|
41
|
-
if (!this.#e) throw Error("Consumer has been freed");
|
|
42
|
-
return this.#e;
|
|
43
|
-
}
|
|
44
|
-
consumeDiff(e) {
|
|
45
|
-
this.#t().consumeDiff(e);
|
|
46
|
-
}
|
|
47
|
-
document() {
|
|
48
|
-
return this.#t().document();
|
|
49
|
-
}
|
|
50
|
-
free() {
|
|
51
|
-
this.#e &&= (this.#e.free(), void 0);
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
//#endregion
|
|
55
|
-
export { c as i, l as n, o as r, u as t };
|