Compare commits

..

No commits in common. "main" and "tmp_pr" have entirely different histories.
main ... tmp_pr

317 changed files with 17612 additions and 26919 deletions

View file

@ -2,10 +2,10 @@ name: CI
on:
push:
branches:
- main
- main
pull_request:
branches:
- main
- main
jobs:
fmt:
runs-on: ubuntu-latest
@ -14,7 +14,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.67.0
toolchain: 1.60.0
default: true
components: rustfmt
- uses: Swatinem/rust-cache@v1
@ -28,7 +28,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.67.0
toolchain: 1.60.0
default: true
components: clippy
- uses: Swatinem/rust-cache@v1
@ -42,7 +42,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.67.0
toolchain: 1.60.0
default: true
- uses: Swatinem/rust-cache@v1
- name: Build rust docs
@ -51,6 +51,9 @@ jobs:
- name: Install doxygen
run: sudo apt-get install -y doxygen
shell: bash
- name: Build C docs
run: ./scripts/ci/cmake-docs
shell: bash
cargo-deny:
runs-on: ubuntu-latest
@ -64,7 +67,6 @@ jobs:
- uses: actions/checkout@v2
- uses: EmbarkStudios/cargo-deny-action@v1
with:
arguments: '--manifest-path ./rust/Cargo.toml'
command: check ${{ matrix.checks }}
wasm_tests:
@ -77,28 +79,6 @@ jobs:
run: rustup target add wasm32-unknown-unknown
- name: run tests
run: ./scripts/ci/wasm_tests
deno_tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: denoland/setup-deno@v1
with:
deno-version: v1.x
- name: Install wasm-bindgen-cli
run: cargo install wasm-bindgen-cli wasm-opt
- name: Install wasm32 target
run: rustup target add wasm32-unknown-unknown
- name: run tests
run: ./scripts/ci/deno_tests
js_fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: install
run: yarn global add prettier
- name: format
run: prettier -c javascript/.prettierrc javascript
js_tests:
runs-on: ubuntu-latest
@ -118,7 +98,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2023-01-26
toolchain: 1.60.0
default: true
- uses: Swatinem/rust-cache@v1
- name: Install CMocka
@ -127,8 +107,6 @@ jobs:
uses: jwlawson/actions-setup-cmake@v1.12
with:
cmake-version: latest
- name: Install rust-src
run: rustup component add rust-src
- name: Build and test C bindings
run: ./scripts/ci/cmake-build Release Static
shell: bash
@ -138,7 +116,9 @@ jobs:
strategy:
matrix:
toolchain:
- 1.67.0
- 1.60.0
- nightly
continue-on-error: ${{ matrix.toolchain == 'nightly' }}
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
@ -157,7 +137,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.67.0
toolchain: 1.60.0
default: true
- uses: Swatinem/rust-cache@v1
- run: ./scripts/ci/build-test
@ -170,7 +150,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.67.0
toolchain: 1.60.0
default: true
- uses: Swatinem/rust-cache@v1
- run: ./scripts/ci/build-test

View file

@ -30,16 +30,28 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: clean
args: --manifest-path ./rust/Cargo.toml --doc
args: --doc
- name: Build Rust docs
uses: actions-rs/cargo@v1
with:
command: doc
args: --manifest-path ./rust/Cargo.toml --workspace --all-features --no-deps
args: --workspace --all-features --no-deps
- name: Move Rust docs
run: mkdir -p docs && mv rust/target/doc/* docs/.
run: mkdir -p docs && mv target/doc/* docs/.
shell: bash
- name: Install doxygen
run: sudo apt-get install -y doxygen
shell: bash
- name: Build C docs
run: ./scripts/ci/cmake-docs
shell: bash
- name: Move C docs
run: mkdir -p docs/automerge-c && mv automerge-c/build/src/html/* docs/automerge-c/.
shell: bash
- name: Configure root page

View file

@ -1,214 +0,0 @@
name: Release
on:
push:
branches:
- main
jobs:
check_if_wasm_version_upgraded:
name: Check if WASM version has been upgraded
runs-on: ubuntu-latest
outputs:
wasm_version: ${{ steps.version-updated.outputs.current-package-version }}
wasm_has_updated: ${{ steps.version-updated.outputs.has-updated }}
steps:
- uses: JiPaix/package-json-updated-action@v1.0.5
id: version-updated
with:
path: rust/automerge-wasm/package.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-wasm:
name: Publish WASM package
runs-on: ubuntu-latest
needs:
- check_if_wasm_version_upgraded
# We create release only if the version in the package.json has been upgraded
if: needs.check_if_wasm_version_upgraded.outputs.wasm_has_updated == 'true'
steps:
- uses: actions/setup-node@v3
with:
node-version: '16.x'
registry-url: 'https://registry.npmjs.org'
- uses: denoland/setup-deno@v1
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ github.ref }}
- name: Get rid of local github workflows
run: rm -r .github/workflows
- name: Remove tmp_branch if it exists
run: git push origin :tmp_branch || true
- run: git checkout -b tmp_branch
- name: Install wasm-bindgen-cli
run: cargo install wasm-bindgen-cli wasm-opt
- name: Install wasm32 target
run: rustup target add wasm32-unknown-unknown
- name: run wasm js tests
id: wasm_js_tests
run: ./scripts/ci/wasm_tests
- name: run wasm deno tests
id: wasm_deno_tests
run: ./scripts/ci/deno_tests
- name: build release
id: build_release
run: |
npm --prefix $GITHUB_WORKSPACE/rust/automerge-wasm run release
- name: Collate deno release files
if: steps.wasm_js_tests.outcome == 'success' && steps.wasm_deno_tests.outcome == 'success'
run: |
mkdir $GITHUB_WORKSPACE/deno_wasm_dist
cp $GITHUB_WORKSPACE/rust/automerge-wasm/deno/* $GITHUB_WORKSPACE/deno_wasm_dist
cp $GITHUB_WORKSPACE/rust/automerge-wasm/index.d.ts $GITHUB_WORKSPACE/deno_wasm_dist
cp $GITHUB_WORKSPACE/rust/automerge-wasm/README.md $GITHUB_WORKSPACE/deno_wasm_dist
cp $GITHUB_WORKSPACE/rust/automerge-wasm/LICENSE $GITHUB_WORKSPACE/deno_wasm_dist
sed -i '1i /// <reference types="./index.d.ts" />' $GITHUB_WORKSPACE/deno_wasm_dist/automerge_wasm.js
- name: Create npm release
if: steps.wasm_js_tests.outcome == 'success' && steps.wasm_deno_tests.outcome == 'success'
run: |
if [ "$(npm --prefix $GITHUB_WORKSPACE/rust/automerge-wasm show . version)" = "$VERSION" ]; then
echo "This version is already published"
exit 0
fi
EXTRA_ARGS="--access public"
if [[ $VERSION == *"alpha."* ]] || [[ $VERSION == *"beta."* ]] || [[ $VERSION == *"rc."* ]]; then
echo "Is pre-release version"
EXTRA_ARGS="$EXTRA_ARGS --tag next"
fi
if [ "$NODE_AUTH_TOKEN" = "" ]; then
echo "Can't publish on NPM, You need a NPM_TOKEN secret."
false
fi
npm publish $GITHUB_WORKSPACE/rust/automerge-wasm $EXTRA_ARGS
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
VERSION: ${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
- name: Commit wasm deno release files
run: |
git config --global user.name "actions"
git config --global user.email actions@github.com
git add $GITHUB_WORKSPACE/deno_wasm_dist
git commit -am "Add deno release files"
git push origin tmp_branch
- name: Tag wasm release
if: steps.wasm_js_tests.outcome == 'success' && steps.wasm_deno_tests.outcome == 'success'
uses: softprops/action-gh-release@v1
with:
name: Automerge Wasm v${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
tag_name: js/automerge-wasm-${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
target_commitish: tmp_branch
generate_release_notes: false
draft: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Remove tmp_branch
run: git push origin :tmp_branch
check_if_js_version_upgraded:
name: Check if JS version has been upgraded
runs-on: ubuntu-latest
outputs:
js_version: ${{ steps.version-updated.outputs.current-package-version }}
js_has_updated: ${{ steps.version-updated.outputs.has-updated }}
steps:
- uses: JiPaix/package-json-updated-action@v1.0.5
id: version-updated
with:
path: javascript/package.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-js:
name: Publish JS package
runs-on: ubuntu-latest
needs:
- check_if_js_version_upgraded
- check_if_wasm_version_upgraded
- publish-wasm
# We create release only if the version in the package.json has been upgraded and after the WASM release
if: |
(always() && ! cancelled()) &&
(needs.publish-wasm.result == 'success' || needs.publish-wasm.result == 'skipped') &&
needs.check_if_js_version_upgraded.outputs.js_has_updated == 'true'
steps:
- uses: actions/setup-node@v3
with:
node-version: '16.x'
registry-url: 'https://registry.npmjs.org'
- uses: denoland/setup-deno@v1
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ github.ref }}
- name: Get rid of local github workflows
run: rm -r .github/workflows
- name: Remove js_tmp_branch if it exists
run: git push origin :js_tmp_branch || true
- run: git checkout -b js_tmp_branch
- name: check js formatting
run: |
yarn global add prettier
prettier -c javascript/.prettierrc javascript
- name: run js tests
id: js_tests
run: |
cargo install wasm-bindgen-cli wasm-opt
rustup target add wasm32-unknown-unknown
./scripts/ci/js_tests
- name: build js release
id: build_release
run: |
npm --prefix $GITHUB_WORKSPACE/javascript run build
- name: build js deno release
id: build_deno_release
run: |
VERSION=$WASM_VERSION npm --prefix $GITHUB_WORKSPACE/javascript run deno:build
env:
WASM_VERSION: ${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
- name: run deno tests
id: deno_tests
run: |
npm --prefix $GITHUB_WORKSPACE/javascript run deno:test
- name: Collate deno release files
if: steps.js_tests.outcome == 'success' && steps.deno_tests.outcome == 'success'
run: |
mkdir $GITHUB_WORKSPACE/deno_js_dist
cp $GITHUB_WORKSPACE/javascript/deno_dist/* $GITHUB_WORKSPACE/deno_js_dist
- name: Create npm release
if: steps.js_tests.outcome == 'success' && steps.deno_tests.outcome == 'success'
run: |
if [ "$(npm --prefix $GITHUB_WORKSPACE/javascript show . version)" = "$VERSION" ]; then
echo "This version is already published"
exit 0
fi
EXTRA_ARGS="--access public"
if [[ $VERSION == *"alpha."* ]] || [[ $VERSION == *"beta."* ]] || [[ $VERSION == *"rc."* ]]; then
echo "Is pre-release version"
EXTRA_ARGS="$EXTRA_ARGS --tag next"
fi
if [ "$NODE_AUTH_TOKEN" = "" ]; then
echo "Can't publish on NPM, You need a NPM_TOKEN secret."
false
fi
npm publish $GITHUB_WORKSPACE/javascript $EXTRA_ARGS
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
VERSION: ${{ needs.check_if_js_version_upgraded.outputs.js_version }}
- name: Commit js deno release files
run: |
git config --global user.name "actions"
git config --global user.email actions@github.com
git add $GITHUB_WORKSPACE/deno_js_dist
git commit -am "Add deno js release files"
git push origin js_tmp_branch
- name: Tag JS release
if: steps.js_tests.outcome == 'success' && steps.deno_tests.outcome == 'success'
uses: softprops/action-gh-release@v1
with:
name: Automerge v${{ needs.check_if_js_version_upgraded.outputs.js_version }}
tag_name: js/automerge-${{ needs.check_if_js_version_upgraded.outputs.js_version }}
target_commitish: js_tmp_branch
generate_release_notes: false
draft: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Remove js_tmp_branch
run: git push origin :js_tmp_branch

1
.gitignore vendored
View file

@ -3,4 +3,3 @@ perf.*
/Cargo.lock
build/
.vim/*
/target

View file

@ -25,7 +25,7 @@ If you're familiar with CRDTs and interested in the design of Automerge in
particular take a look at https://automerge.org/docs/how-it-works/backend/
Finally, if you want to talk to us about this project please [join the
Slack](https://join.slack.com/t/automerge/shared_invite/zt-e4p3760n-kKh7r3KRH1YwwNfiZM8ktw)
Slack](https://join.slack.com/t/automerge/shared_invite/zt-1ho1ieas2-DnWZcRR82BRu65vCD4t3Xw)
## Status
@ -40,12 +40,11 @@ in that time.
In general we try and respect semver.
### JavaScript
### JavaScript
A stable release of the javascript package is currently available as
`@automerge/automerge@2.0.0` where. pre-release verisions of the `2.0.1` are
available as `2.0.1-alpha.n`. `2.0.1*` packages are also available for Deno at
https://deno.land/x/automerge
An alpha release of the javascript package is currently available as
`@automerge/automerge@2.0.0-alpha.n` where `n` is an integer. We are gathering
feedback on the API and looking to release a `2.0.0` in the next few weeks.
### Rust
@ -53,90 +52,34 @@ The rust codebase is currently oriented around producing a performant backend
for the Javascript wrapper and as such the API for Rust code is low level and
not well documented. We will be returning to this over the next few months but
for now you will need to be comfortable reading the tests and asking questions
to figure out how to use it. If you are looking to build rust applications which
use automerge you may want to look into
[autosurgeon](https://github.com/alexjg/autosurgeon)
to figure out how to use it.
## Repository Organisation
- `./rust` - the rust rust implementation and also the Rust components of
* `./rust` - the rust rust implementation and also the Rust components of
platform specific wrappers (e.g. `automerge-wasm` for the WASM API or
`automerge-c` for the C FFI bindings)
- `./javascript` - The javascript library which uses `automerge-wasm`
* `./javascript` - The javascript library which uses `automerge-wasm`
internally but presents a more idiomatic javascript interface
- `./scripts` - scripts which are useful to maintenance of the repository.
* `./scripts` - scripts which are useful to maintenance of the repository.
This includes the scripts which are run in CI.
- `./img` - static assets for use in `.md` files
* `./img` - static assets for use in `.md` files
## Building
To build this codebase you will need:
- `rust`
- `wasm-bindgen-cli`
- `wasm-opt`
- `node`
- `yarn`
- `cmake`
- `cmocka`
You will also need to install the following with `cargo install`
- `wasm-bindgen-cli`
- `wasm-opt`
- `cargo-deny`
And ensure you have added the `wasm32-unknown-unknown` target for rust cross-compilation.
The various subprojects (the rust code, the wrapper projects) have their own
build instructions, but to run the tests that will be run in CI you can run
`./scripts/ci/run`.
### For macOS
These instructions worked to build locally on macOS 13.1 (arm64) as of
Nov 29th 2022.
```bash
# clone the repo
git clone https://github.com/automerge/automerge-rs
cd automerge-rs
# install rustup
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# install homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# install cmake, node, cmocka
brew install cmake node cmocka
# install yarn
npm install --global yarn
# install javascript dependencies
yarn --cwd ./javascript
# install rust dependencies
cargo install wasm-bindgen-cli wasm-opt cargo-deny
# get nightly rust to produce optimized automerge-c builds
rustup toolchain install nightly
rustup component add rust-src --toolchain nightly
# add wasm target in addition to current architecture
rustup target add wasm32-unknown-unknown
# Run ci script
./scripts/ci/run
```
If your build fails to find `cmocka.h` you may need to teach it about homebrew's
installation location:
```
export CPATH=/opt/homebrew/include
export LIBRARY_PATH=/opt/homebrew/lib
./scripts/ci/run
```
`./scripts/ci/run`.
## Contributing

View file

@ -2,11 +2,11 @@
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"lastModified": 1642700792,
"narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"rev": "846b2ae0fc4cc943637d3d1def4454213e203cba",
"type": "github"
},
"original": {
@ -17,11 +17,11 @@
},
"flake-utils_2": {
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"lastModified": 1637014545,
"narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4",
"type": "github"
},
"original": {
@ -32,11 +32,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1669542132,
"narHash": "sha256-DRlg++NJAwPh8io3ExBJdNW7Djs3plVI5jgYQ+iXAZQ=",
"lastModified": 1643805626,
"narHash": "sha256-AXLDVMG+UaAGsGSpOtQHPIKB+IZ0KSd9WS77aanGzgc=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "a115bb9bd56831941be3776c8a94005867f316a7",
"rev": "554d2d8aa25b6e583575459c297ec23750adb6cb",
"type": "github"
},
"original": {
@ -48,11 +48,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1665296151,
"narHash": "sha256-uOB0oxqxN9K7XGF1hcnY+PQnlQJ+3bP2vCn/+Ru/bbc=",
"lastModified": 1637453606,
"narHash": "sha256-Gy6cwUswft9xqsjWxFYEnx/63/qzaFUwatcbV5GF/GQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "14ccaaedd95a488dd7ae142757884d8e125b3363",
"rev": "8afc4e543663ca0a6a4f496262cd05233737e732",
"type": "github"
},
"original": {
@ -75,11 +75,11 @@
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1669775522,
"narHash": "sha256-6xxGArBqssX38DdHpDoPcPvB/e79uXyQBwpBcaO/BwY=",
"lastModified": 1643941258,
"narHash": "sha256-uHyEuICSu8qQp6adPTqV33ajiwoF0sCh+Iazaz5r7fo=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "3158e47f6b85a288d12948aeb9a048e0ed4434d6",
"rev": "674156c4c2f46dd6a6846466cb8f9fee84c211ca",
"type": "github"
},
"original": {

108
flake.nix
View file

@ -3,67 +3,63 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
flake-utils = {
url = "github:numtide/flake-utils";
inputs.nixpkgs.follows = "nixpkgs";
};
rust-overlay.url = "github:oxalica/rust-overlay";
};
outputs = {
self,
nixpkgs,
flake-utils,
rust-overlay,
}:
outputs = { self, nixpkgs, flake-utils, rust-overlay }:
flake-utils.lib.eachDefaultSystem
(system: let
pkgs = import nixpkgs {
overlays = [rust-overlay.overlays.default];
inherit system;
};
rust = pkgs.rust-bin.stable.latest.default;
in {
formatter = pkgs.alejandra;
(system:
let
pkgs = import nixpkgs {
overlays = [ rust-overlay.overlay ];
inherit system;
};
lib = pkgs.lib;
rust = pkgs.rust-bin.stable.latest.default;
cargoNix = pkgs.callPackage ./Cargo.nix {
inherit pkgs;
release = true;
};
debugCargoNix = pkgs.callPackage ./Cargo.nix {
inherit pkgs;
release = false;
};
in
{
devShell = pkgs.mkShell {
buildInputs = with pkgs;
[
(rust.override {
extensions = [ "rust-src" ];
targets = [ "wasm32-unknown-unknown" ];
})
cargo-edit
cargo-watch
cargo-criterion
cargo-fuzz
cargo-flamegraph
cargo-deny
crate2nix
wasm-pack
pkgconfig
openssl
gnuplot
packages = {
deadnix = pkgs.runCommand "deadnix" {} ''
${pkgs.deadnix}/bin/deadnix --fail ${./.}
mkdir $out
'';
};
nodejs
yarn
checks = {
inherit (self.packages.${system}) deadnix;
};
# c deps
cmake
cmocka
doxygen
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
(rust.override {
extensions = ["rust-src"];
targets = ["wasm32-unknown-unknown"];
})
cargo-edit
cargo-watch
cargo-criterion
cargo-fuzz
cargo-flamegraph
cargo-deny
crate2nix
wasm-pack
pkgconfig
openssl
gnuplot
nodejs
yarn
deno
# c deps
cmake
cmocka
doxygen
rnix-lsp
nixpkgs-fmt
];
};
});
rnix-lsp
nixpkgs-fmt
];
};
});
}

View file

@ -1,3 +0,0 @@
{
"replacer": "scripts/denoify-replacer.mjs"
}

View file

@ -1,15 +1,11 @@
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
rules: {
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
},
],
},
}
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
};

View file

@ -2,5 +2,3 @@
/yarn.lock
dist
docs/
.vim
deno_dist/

View file

@ -1,4 +0,0 @@
e2e/verdacciodb
dist
docs
deno_dist

View file

@ -1,4 +0,0 @@
{
"semi": false,
"arrowParens": "avoid"
}

View file

@ -8,7 +8,7 @@ Rust codebase and can be found in `~/automerge-wasm`). I.e. the responsibility
of this codebase is
- To map from the javascript data model to the underlying `set`, `make`,
`insert`, and `delete` operations of Automerge.
`insert`, and `delete` operations of Automerge.
- To expose a more convenient interface to functions in `automerge-wasm` which
generate messages to send over the network or compressed file formats to store
on disk
@ -37,3 +37,4 @@ yarn test
If you make changes to the `automerge-wasm` package you will need to re-run
`yarn e2e buildjs`

View file

@ -19,6 +19,7 @@ data](#make-some-data). If you're in a browser you need a bundler
### Bundler setup
`@automerge/automerge` is a wrapper around a core library which is written in
rust, compiled to WebAssembly and distributed as a separate package called
`@automerge/automerge-wasm`. Browsers don't currently support WebAssembly
@ -53,28 +54,28 @@ import * as automerge from "@automerge/automerge"
import * as assert from "assert"
let doc1 = automerge.from({
tasks: [
{ description: "feed fish", done: false },
{ description: "water plants", done: false },
],
tasks: [
{description: "feed fish", done: false},
{description: "water plants", done: false},
]
})
// Create a new thread of execution
// Create a new thread of execution
let doc2 = automerge.clone(doc1)
// Now we concurrently make changes to doc1 and doc2
// Complete a task in doc2
doc2 = automerge.change(doc2, d => {
d.tasks[0].done = true
d.tasks[0].done = true
})
// Add a task in doc1
doc1 = automerge.change(doc1, d => {
d.tasks.push({
description: "water fish",
done: false,
})
d.tasks.push({
description: "water fish",
done: false
})
})
// Merge changes from both docs
@ -83,19 +84,19 @@ doc2 = automerge.merge(doc2, doc1)
// Both docs are merged and identical
assert.deepEqual(doc1, {
tasks: [
{ description: "feed fish", done: true },
{ description: "water plants", done: false },
{ description: "water fish", done: false },
],
tasks: [
{description: "feed fish", done: true},
{description: "water plants", done: false},
{description: "water fish", done: false},
]
})
assert.deepEqual(doc2, {
tasks: [
{ description: "feed fish", done: true },
{ description: "water plants", done: false },
{ description: "water fish", done: false },
],
tasks: [
{description: "feed fish", done: true},
{description: "water plants", done: false},
{description: "water fish", done: false},
]
})
```

View file

@ -1,12 +1,6 @@
{
"extends": "../tsconfig.json",
"exclude": [
"../dist/**/*",
"../node_modules",
"../test/**/*",
"../src/**/*.deno.ts"
],
"compilerOptions": {
"outDir": "../dist/cjs"
}
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../dist/cjs"
}
}

View file

@ -1,13 +0,0 @@
{
"extends": "../tsconfig.json",
"exclude": [
"../dist/**/*",
"../node_modules",
"../test/**/*",
"../src/**/*.deno.ts"
],
"emitDeclarationOnly": true,
"compilerOptions": {
"outDir": "../dist"
}
}

View file

@ -1,14 +1,8 @@
{
"extends": "../tsconfig.json",
"exclude": [
"../dist/**/*",
"../node_modules",
"../test/**/*",
"../src/**/*.deno.ts"
],
"compilerOptions": {
"target": "es6",
"module": "es6",
"outDir": "../dist/mjs"
}
"extends": "../tsconfig.json",
"compilerOptions": {
"target": "es6",
"module": "es6",
"outDir": "../dist/mjs"
}
}

View file

@ -1,10 +0,0 @@
import * as Automerge from "../deno_dist/index.ts"
Deno.test("It should create, clone and free", () => {
let doc1 = Automerge.init()
let doc2 = Automerge.clone(doc1)
// this is only needed if weakrefs are not supported
Automerge.free(doc1)
Automerge.free(doc2)
})

View file

@ -54,7 +54,7 @@ yarn e2e buildexamples -e webpack
If you're experimenting with a project which is not in the `examples` folder
you'll need a running registry. `run-registry` builds and publishes
`automerge-js` and `automerge-wasm` and then runs the registry at
`localhost:4873`.
`localhost:4873`.
```
yarn e2e run-registry
@ -63,6 +63,7 @@ yarn e2e run-registry
You can now run `yarn install --registry http://localhost:4873` to experiment
with the built packages.
## Using the `dev` build of `automerge-wasm`
All the commands above take a `-p` flag which can be either `release` or

View file

@ -1,25 +1,15 @@
import { once } from "events"
import { setTimeout } from "timers/promises"
import { spawn, ChildProcess } from "child_process"
import {once} from "events"
import {setTimeout} from "timers/promises"
import {spawn, ChildProcess} from "child_process"
import * as child_process from "child_process"
import {
command,
subcommands,
run,
array,
multioption,
option,
Type,
} from "cmd-ts"
import {command, subcommands, run, array, multioption, option, Type} from "cmd-ts"
import * as path from "path"
import * as fsPromises from "fs/promises"
import fetch from "node-fetch"
const VERDACCIO_DB_PATH = path.normalize(`${__dirname}/verdacciodb`)
const VERDACCIO_CONFIG_PATH = path.normalize(`${__dirname}/verdaccio.yaml`)
const AUTOMERGE_WASM_PATH = path.normalize(
`${__dirname}/../../rust/automerge-wasm`
)
const AUTOMERGE_WASM_PATH = path.normalize(`${__dirname}/../../rust/automerge-wasm`)
const AUTOMERGE_JS_PATH = path.normalize(`${__dirname}/..`)
const EXAMPLES_DIR = path.normalize(path.join(__dirname, "../", "examples"))
@ -28,286 +18,217 @@ type Example = "webpack" | "vite" | "create-react-app"
// Type to parse strings to `Example` so the types line up for the `buildExamples` commmand
const ReadExample: Type<string, Example> = {
async from(str) {
if (str === "webpack") {
return "webpack"
} else if (str === "vite") {
return "vite"
} else if (str === "create-react-app") {
return "create-react-app"
} else {
throw new Error(`Unknown example type ${str}`)
async from(str) {
if (str === "webpack") {
return "webpack"
} else if (str === "vite") {
return "vite"
} else if (str === "create-react-app") {
return "create-react-app"
} else {
throw new Error(`Unknown example type ${str}`)
}
}
},
}
type Profile = "dev" | "release"
const ReadProfile: Type<string, Profile> = {
async from(str) {
if (str === "dev") {
return "dev"
} else if (str === "release") {
return "release"
} else {
throw new Error(`Unknown profile ${str}`)
async from(str) {
if (str === "dev") {
return "dev"
} else if (str === "release") {
return "release"
} else {
throw new Error(`Unknown profile ${str}`)
}
}
},
}
const buildjs = command({
name: "buildjs",
args: {
profile: option({
type: ReadProfile,
long: "profile",
short: "p",
defaultValue: () => "dev" as Profile,
}),
},
handler: ({ profile }) => {
console.log("building js")
withPublishedWasm(profile, async (registryUrl: string) => {
await buildAndPublishAutomergeJs(registryUrl)
})
},
name: "buildjs",
args: {
profile: option({
type: ReadProfile,
long: "profile",
short: "p",
defaultValue: () => "dev" as Profile
})
},
handler: ({profile}) => {
console.log("building js")
withPublishedWasm(profile, async (registryUrl: string) => {
await buildAndPublishAutomergeJs(registryUrl)
})
}
})
const buildWasm = command({
name: "buildwasm",
args: {
profile: option({
type: ReadProfile,
long: "profile",
short: "p",
defaultValue: () => "dev" as Profile,
}),
},
handler: ({ profile }) => {
console.log("building automerge-wasm")
withRegistry(buildAutomergeWasm(profile))
},
name: "buildwasm",
args: {
profile: option({
type: ReadProfile,
long: "profile",
short: "p",
defaultValue: () => "dev" as Profile
})
},
handler: ({profile}) => {
console.log("building automerge-wasm")
withRegistry(
buildAutomergeWasm(profile),
)
}
})
const buildexamples = command({
name: "buildexamples",
args: {
examples: multioption({
long: "example",
short: "e",
type: array(ReadExample),
}),
profile: option({
type: ReadProfile,
long: "profile",
short: "p",
defaultValue: () => "dev" as Profile,
}),
},
handler: ({ examples, profile }) => {
if (examples.length === 0) {
examples = ["webpack", "vite", "create-react-app"]
name: "buildexamples",
args: {
examples: multioption({
long: "example",
short: "e",
type: array(ReadExample),
}),
profile: option({
type: ReadProfile,
long: "profile",
short: "p",
defaultValue: () => "dev" as Profile
})
},
handler: ({examples, profile}) => {
if (examples.length === 0) {
examples = ["webpack", "vite", "create-react-app"]
}
buildExamples(examples, profile)
}
buildExamples(examples, profile)
},
})
const runRegistry = command({
name: "run-registry",
args: {
profile: option({
type: ReadProfile,
long: "profile",
short: "p",
defaultValue: () => "dev" as Profile,
}),
},
handler: ({ profile }) => {
withPublishedWasm(profile, async (registryUrl: string) => {
await buildAndPublishAutomergeJs(registryUrl)
console.log("\n************************")
console.log(` Verdaccio NPM registry is running at ${registryUrl}`)
console.log(" press CTRL-C to exit ")
console.log("************************")
await once(process, "SIGINT")
}).catch(e => {
console.error(`Failed: ${e}`)
})
},
name: "run-registry",
args: {
profile: option({
type: ReadProfile,
long: "profile",
short: "p",
defaultValue: () => "dev" as Profile
})
},
handler: ({profile}) => {
withPublishedWasm(profile, async (registryUrl: string) => {
await buildAndPublishAutomergeJs(registryUrl)
console.log("\n************************")
console.log(` Verdaccio NPM registry is running at ${registryUrl}`)
console.log(" press CTRL-C to exit ")
console.log("************************")
await once(process, "SIGINT")
}).catch(e => {
console.error(`Failed: ${e}`)
})
}
})
const app = subcommands({
name: "e2e",
cmds: {
buildjs,
buildexamples,
buildwasm: buildWasm,
"run-registry": runRegistry,
},
name: "e2e",
cmds: {buildjs, buildexamples, buildwasm: buildWasm, "run-registry": runRegistry}
})
run(app, process.argv.slice(2))
async function buildExamples(examples: Array<Example>, profile: Profile) {
await withPublishedWasm(profile, async registryUrl => {
printHeader("building and publishing automerge")
await buildAndPublishAutomergeJs(registryUrl)
for (const example of examples) {
printHeader(`building ${example} example`)
if (example === "webpack") {
const projectPath = path.join(EXAMPLES_DIR, example)
await removeExistingAutomerge(projectPath)
await fsPromises.rm(path.join(projectPath, "yarn.lock"), {
force: true,
})
await spawnAndWait(
"yarn",
[
"--cwd",
projectPath,
"install",
"--registry",
registryUrl,
"--check-files",
],
{ stdio: "inherit" }
)
await spawnAndWait("yarn", ["--cwd", projectPath, "build"], {
stdio: "inherit",
})
} else if (example === "vite") {
const projectPath = path.join(EXAMPLES_DIR, example)
await removeExistingAutomerge(projectPath)
await fsPromises.rm(path.join(projectPath, "yarn.lock"), {
force: true,
})
await spawnAndWait(
"yarn",
[
"--cwd",
projectPath,
"install",
"--registry",
registryUrl,
"--check-files",
],
{ stdio: "inherit" }
)
await spawnAndWait("yarn", ["--cwd", projectPath, "build"], {
stdio: "inherit",
})
} else if (example === "create-react-app") {
const projectPath = path.join(EXAMPLES_DIR, example)
await removeExistingAutomerge(projectPath)
await fsPromises.rm(path.join(projectPath, "yarn.lock"), {
force: true,
})
await spawnAndWait(
"yarn",
[
"--cwd",
projectPath,
"install",
"--registry",
registryUrl,
"--check-files",
],
{ stdio: "inherit" }
)
await spawnAndWait("yarn", ["--cwd", projectPath, "build"], {
stdio: "inherit",
})
}
}
})
await withPublishedWasm(profile, async (registryUrl) => {
printHeader("building and publishing automerge")
await buildAndPublishAutomergeJs(registryUrl)
for (const example of examples) {
printHeader(`building ${example} example`)
if (example === "webpack") {
const projectPath = path.join(EXAMPLES_DIR, example)
await removeExistingAutomerge(projectPath)
await fsPromises.rm(path.join(projectPath, "yarn.lock"), {force: true})
await spawnAndWait("yarn", ["--cwd", projectPath, "install", "--registry", registryUrl, "--check-files"], {stdio: "inherit"})
await spawnAndWait("yarn", ["--cwd", projectPath, "build"], {stdio: "inherit"})
} else if (example === "vite") {
const projectPath = path.join(EXAMPLES_DIR, example)
await removeExistingAutomerge(projectPath)
await fsPromises.rm(path.join(projectPath, "yarn.lock"), {force: true})
await spawnAndWait("yarn", ["--cwd", projectPath, "install", "--registry", registryUrl, "--check-files"], {stdio: "inherit"})
await spawnAndWait("yarn", ["--cwd", projectPath, "build"], {stdio: "inherit"})
} else if (example === "create-react-app") {
const projectPath = path.join(EXAMPLES_DIR, example)
await removeExistingAutomerge(projectPath)
await fsPromises.rm(path.join(projectPath, "yarn.lock"), {force: true})
await spawnAndWait("yarn", ["--cwd", projectPath, "install", "--registry", registryUrl, "--check-files"], {stdio: "inherit"})
await spawnAndWait("yarn", ["--cwd", projectPath, "build"], {stdio: "inherit"})
}
}
})
}
type WithRegistryAction = (registryUrl: string) => Promise<void>
async function withRegistry(
action: WithRegistryAction,
...actions: Array<WithRegistryAction>
) {
// First, start verdaccio
printHeader("Starting verdaccio NPM server")
const verd = await VerdaccioProcess.start()
actions.unshift(action)
async function withRegistry(action: WithRegistryAction, ...actions: Array<WithRegistryAction>) {
// First, start verdaccio
printHeader("Starting verdaccio NPM server")
const verd = await VerdaccioProcess.start()
actions.unshift(action)
for (const action of actions) {
try {
type Step = "verd-died" | "action-completed"
const verdDied: () => Promise<Step> = async () => {
await verd.died()
return "verd-died"
}
const actionComplete: () => Promise<Step> = async () => {
await action("http://localhost:4873")
return "action-completed"
}
const result = await Promise.race([verdDied(), actionComplete()])
if (result === "verd-died") {
throw new Error("verdaccio unexpectedly exited")
}
} catch (e) {
await verd.kill()
throw e
for (const action of actions) {
try {
type Step = "verd-died" | "action-completed"
const verdDied: () => Promise<Step> = async () => {
await verd.died()
return "verd-died"
}
const actionComplete: () => Promise<Step> = async () => {
await action("http://localhost:4873")
return "action-completed"
}
const result = await Promise.race([verdDied(), actionComplete()])
if (result === "verd-died") {
throw new Error("verdaccio unexpectedly exited")
}
} catch(e) {
await verd.kill()
throw e
}
}
}
await verd.kill()
await verd.kill()
}
async function withPublishedWasm(profile: Profile, action: WithRegistryAction) {
await withRegistry(buildAutomergeWasm(profile), publishAutomergeWasm, action)
await withRegistry(
buildAutomergeWasm(profile),
publishAutomergeWasm,
action
)
}
function buildAutomergeWasm(profile: Profile): WithRegistryAction {
return async (registryUrl: string) => {
printHeader("building automerge-wasm")
await spawnAndWait(
"yarn",
["--cwd", AUTOMERGE_WASM_PATH, "--registry", registryUrl, "install"],
{ stdio: "inherit" }
)
const cmd = profile === "release" ? "release" : "debug"
await spawnAndWait("yarn", ["--cwd", AUTOMERGE_WASM_PATH, cmd], {
stdio: "inherit",
})
}
return async (registryUrl: string) => {
printHeader("building automerge-wasm")
await spawnAndWait("yarn", ["--cwd", AUTOMERGE_WASM_PATH, "--registry", registryUrl, "install"], {stdio: "inherit"})
const cmd = profile === "release" ? "release" : "debug"
await spawnAndWait("yarn", ["--cwd", AUTOMERGE_WASM_PATH, cmd], {stdio: "inherit"})
}
}
async function publishAutomergeWasm(registryUrl: string) {
printHeader("Publishing automerge-wasm to verdaccio")
await fsPromises.rm(
path.join(VERDACCIO_DB_PATH, "@automerge/automerge-wasm"),
{ recursive: true, force: true }
)
await yarnPublish(registryUrl, AUTOMERGE_WASM_PATH)
printHeader("Publishing automerge-wasm to verdaccio")
await fsPromises.rm(path.join(VERDACCIO_DB_PATH, "@automerge/automerge-wasm"), { recursive: true, force: true} )
await yarnPublish(registryUrl, AUTOMERGE_WASM_PATH)
}
async function buildAndPublishAutomergeJs(registryUrl: string) {
// Build the js package
printHeader("Building automerge")
await removeExistingAutomerge(AUTOMERGE_JS_PATH)
await removeFromVerdaccio("@automerge/automerge")
await fsPromises.rm(path.join(AUTOMERGE_JS_PATH, "yarn.lock"), {
force: true,
})
await spawnAndWait(
"yarn",
[
"--cwd",
AUTOMERGE_JS_PATH,
"install",
"--registry",
registryUrl,
"--check-files",
],
{ stdio: "inherit" }
)
await spawnAndWait("yarn", ["--cwd", AUTOMERGE_JS_PATH, "build"], {
stdio: "inherit",
})
await yarnPublish(registryUrl, AUTOMERGE_JS_PATH)
// Build the js package
printHeader("Building automerge")
await removeExistingAutomerge(AUTOMERGE_JS_PATH)
await removeFromVerdaccio("@automerge/automerge")
await fsPromises.rm(path.join(AUTOMERGE_JS_PATH, "yarn.lock"), {force: true})
await spawnAndWait("yarn", ["--cwd", AUTOMERGE_JS_PATH, "install", "--registry", registryUrl, "--check-files"], {stdio: "inherit"})
await spawnAndWait("yarn", ["--cwd", AUTOMERGE_JS_PATH, "build"], {stdio: "inherit"})
await yarnPublish(registryUrl, AUTOMERGE_JS_PATH)
}
/**
@ -315,110 +236,104 @@ async function buildAndPublishAutomergeJs(registryUrl: string) {
*
*/
class VerdaccioProcess {
child: ChildProcess
stdout: Array<Buffer>
stderr: Array<Buffer>
child: ChildProcess
stdout: Array<Buffer>
stderr: Array<Buffer>
constructor(child: ChildProcess) {
this.child = child
constructor(child: ChildProcess) {
this.child = child
// Collect stdout/stderr otherwise the subprocess gets blocked writing
this.stdout = []
this.stderr = []
this.child.stdout &&
this.child.stdout.on("data", data => this.stdout.push(data))
this.child.stderr &&
this.child.stderr.on("data", data => this.stderr.push(data))
// Collect stdout/stderr otherwise the subprocess gets blocked writing
this.stdout = []
this.stderr = []
this.child.stdout && this.child.stdout.on("data", (data) => this.stdout.push(data))
this.child.stderr && this.child.stderr.on("data", (data) => this.stderr.push(data))
const errCallback = (e: any) => {
console.error("!!!!!!!!!ERROR IN VERDACCIO PROCESS!!!!!!!!!")
console.error(" ", e)
if (this.stdout.length > 0) {
console.log("\n**Verdaccio stdout**")
const stdout = Buffer.concat(this.stdout)
process.stdout.write(stdout)
}
const errCallback = (e: any) => {
console.error("!!!!!!!!!ERROR IN VERDACCIO PROCESS!!!!!!!!!")
console.error(" ", e)
if (this.stdout.length > 0) {
console.log("\n**Verdaccio stdout**")
const stdout = Buffer.concat(this.stdout)
process.stdout.write(stdout)
}
if (this.stderr.length > 0) {
console.log("\n**Verdaccio stderr**")
const stdout = Buffer.concat(this.stderr)
process.stdout.write(stdout)
}
process.exit(-1)
}
this.child.on("error", errCallback)
}
/**
* Spawn a verdaccio process and wait for it to respond succesfully to http requests
*
* The returned `VerdaccioProcess` can be used to control the subprocess
*/
static async start() {
const child = spawn(
"yarn",
["verdaccio", "--config", VERDACCIO_CONFIG_PATH],
{ env: { ...process.env, FORCE_COLOR: "true" } }
)
// Forward stdout and stderr whilst waiting for startup to complete
const stdoutCallback = (data: Buffer) => process.stdout.write(data)
const stderrCallback = (data: Buffer) => process.stderr.write(data)
child.stdout && child.stdout.on("data", stdoutCallback)
child.stderr && child.stderr.on("data", stderrCallback)
const healthCheck = async () => {
while (true) {
try {
const resp = await fetch("http://localhost:4873")
if (resp.status === 200) {
return
} else {
console.log(`Healthcheck failed: bad status ${resp.status}`)
}
} catch (e) {
console.error(`Healthcheck failed: ${e}`)
if (this.stderr.length > 0) {
console.log("\n**Verdaccio stderr**")
const stdout = Buffer.concat(this.stderr)
process.stdout.write(stdout)
}
process.exit(-1)
}
await setTimeout(500)
}
this.child.on("error", errCallback)
}
await withTimeout(healthCheck(), 10000)
// Stop forwarding stdout/stderr
child.stdout && child.stdout.off("data", stdoutCallback)
child.stderr && child.stderr.off("data", stderrCallback)
return new VerdaccioProcess(child)
}
/**
* Spawn a verdaccio process and wait for it to respond succesfully to http requests
*
* The returned `VerdaccioProcess` can be used to control the subprocess
*/
static async start() {
const child = spawn("yarn", ["verdaccio", "--config", VERDACCIO_CONFIG_PATH], {env: { ...process.env, FORCE_COLOR: "true"}})
/**
* Send a SIGKILL to the process and wait for it to stop
*/
async kill() {
this.child.stdout && this.child.stdout.destroy()
this.child.stderr && this.child.stderr.destroy()
this.child.kill()
try {
await withTimeout(once(this.child, "close"), 500)
} catch (e) {
console.error("unable to kill verdaccio subprocess, trying -9")
this.child.kill(9)
await withTimeout(once(this.child, "close"), 500)
// Forward stdout and stderr whilst waiting for startup to complete
const stdoutCallback = (data: Buffer) => process.stdout.write(data)
const stderrCallback = (data: Buffer) => process.stderr.write(data)
child.stdout && child.stdout.on("data", stdoutCallback)
child.stderr && child.stderr.on("data", stderrCallback)
const healthCheck = async () => {
while (true) {
try {
const resp = await fetch("http://localhost:4873")
if (resp.status === 200) {
return
} else {
console.log(`Healthcheck failed: bad status ${resp.status}`)
}
} catch (e) {
console.error(`Healthcheck failed: ${e}`)
}
await setTimeout(500)
}
}
await withTimeout(healthCheck(), 10000)
// Stop forwarding stdout/stderr
child.stdout && child.stdout.off("data", stdoutCallback)
child.stderr && child.stderr.off("data", stderrCallback)
return new VerdaccioProcess(child)
}
}
/**
* A promise which resolves if the subprocess exits for some reason
*/
async died(): Promise<number | null> {
const [exit, _signal] = await once(this.child, "exit")
return exit
}
/**
* Send a SIGKILL to the process and wait for it to stop
*/
async kill() {
this.child.stdout && this.child.stdout.destroy()
this.child.stderr && this.child.stderr.destroy()
this.child.kill();
try {
await withTimeout(once(this.child, "close"), 500)
} catch (e) {
console.error("unable to kill verdaccio subprocess, trying -9")
this.child.kill(9)
await withTimeout(once(this.child, "close"), 500)
}
}
/**
* A promise which resolves if the subprocess exits for some reason
*/
async died(): Promise<number | null> {
const [exit, _signal] = await once(this.child, "exit")
return exit
}
}
function printHeader(header: string) {
console.log("\n===============================")
console.log(` ${header}`)
console.log("===============================")
console.log("\n===============================")
console.log(` ${header}`)
console.log("===============================")
}
/**
@ -432,46 +347,36 @@ function printHeader(header: string) {
* @param packageDir - The directory containing the package.json of the target project
*/
async function removeExistingAutomerge(packageDir: string) {
await fsPromises.rm(path.join(packageDir, "node_modules", "@automerge"), {
recursive: true,
force: true,
})
await fsPromises.rm(path.join(packageDir, "node_modules", "automerge"), {
recursive: true,
force: true,
})
await fsPromises.rm(path.join(packageDir, "node_modules", "@automerge"), {recursive: true, force: true})
await fsPromises.rm(path.join(packageDir, "node_modules", "automerge"), {recursive: true, force: true})
}
type SpawnResult = {
stdout?: Buffer
stderr?: Buffer
stdout?: Buffer,
stderr?: Buffer,
}
async function spawnAndWait(
cmd: string,
args: Array<string>,
options: child_process.SpawnOptions
): Promise<SpawnResult> {
const child = spawn(cmd, args, options)
let stdout = null
let stderr = null
if (child.stdout) {
stdout = []
child.stdout.on("data", data => stdout.push(data))
}
if (child.stderr) {
stderr = []
child.stderr.on("data", data => stderr.push(data))
}
async function spawnAndWait(cmd: string, args: Array<string>, options: child_process.SpawnOptions): Promise<SpawnResult> {
const child = spawn(cmd, args, options)
let stdout = null
let stderr = null
if (child.stdout) {
stdout = []
child.stdout.on("data", data => stdout.push(data))
}
if (child.stderr) {
stderr = []
child.stderr.on("data", data => stderr.push(data))
}
const [exit, _signal] = await once(child, "exit")
if (exit && exit !== 0) {
throw new Error("nonzero exit code")
}
return {
stderr: stderr ? Buffer.concat(stderr) : null,
stdout: stdout ? Buffer.concat(stdout) : null,
}
const [exit, _signal] = await once(child, "exit")
if (exit && exit !== 0) {
throw new Error("nonzero exit code")
}
return {
stderr: stderr? Buffer.concat(stderr) : null,
stdout: stdout ? Buffer.concat(stdout) : null
}
}
/**
@ -482,27 +387,29 @@ async function spawnAndWait(
* okay I Promise.
*/
async function removeFromVerdaccio(packageName: string) {
await fsPromises.rm(path.join(VERDACCIO_DB_PATH, packageName), {
force: true,
recursive: true,
})
await fsPromises.rm(path.join(VERDACCIO_DB_PATH, packageName), {force: true, recursive: true})
}
async function yarnPublish(registryUrl: string, cwd: string) {
await spawnAndWait(
"yarn",
["--registry", registryUrl, "--cwd", cwd, "publish", "--non-interactive"],
{
stdio: "inherit",
env: {
...process.env,
FORCE_COLOR: "true",
// This is a fake token, it just has to be the right format
npm_config__auth:
"//localhost:4873/:_authToken=Gp2Mgxm4faa/7wp0dMSuRA==",
},
}
)
await spawnAndWait(
"yarn",
[
"--registry",
registryUrl,
"--cwd",
cwd,
"publish",
"--non-interactive",
],
{
stdio: "inherit",
env: {
...process.env,
FORCE_COLOR: "true",
// This is a fake token, it just has to be the right format
npm_config__auth: "//localhost:4873/:_authToken=Gp2Mgxm4faa/7wp0dMSuRA=="
}
})
}
/**
@ -512,23 +419,20 @@ async function yarnPublish(registryUrl: string, cwd: string) {
* @param promise - the promise to wait for @param timeout - the delay in
* milliseconds to wait before throwing
*/
async function withTimeout<T>(
promise: Promise<T>,
timeout: number
): Promise<T> {
type Step = "timed-out" | { result: T }
const timedOut: () => Promise<Step> = async () => {
await setTimeout(timeout)
return "timed-out"
}
const succeeded: () => Promise<Step> = async () => {
const result = await promise
return { result }
}
const result = await Promise.race([timedOut(), succeeded()])
if (result === "timed-out") {
throw new Error("timed out")
} else {
return result.result
}
async function withTimeout<T>(promise: Promise<T>, timeout: number): Promise<T> {
type Step = "timed-out" | {result: T}
const timedOut: () => Promise<Step> = async () => {
await setTimeout(timeout)
return "timed-out"
}
const succeeded: () => Promise<Step> = async () => {
const result = await promise
return {result}
}
const result = await Promise.race([timedOut(), succeeded()])
if (result === "timed-out") {
throw new Error("timed out")
} else {
return result.result
}
}

View file

@ -1,6 +1,6 @@
{
"compilerOptions": {
"types": ["node"]
},
"module": "nodenext"
"compilerOptions": {
"types": ["node"]
},
"module": "nodenext"
}

View file

@ -4,22 +4,22 @@ auth:
file: ./htpasswd
publish:
allow_offline: true
logs: { type: stdout, format: pretty, level: info }
packages:
logs: {type: stdout, format: pretty, level: info}
packages:
"@automerge/automerge-wasm":
access: "$all"
publish: "$all"
access: "$all"
publish: "$all"
"@automerge/automerge":
access: "$all"
publish: "$all"
access: "$all"
publish: "$all"
"*":
access: "$all"
publish: "$all"
proxy: npmjs
access: "$all"
publish: "$all"
proxy: npmjs
"@*/*":
access: "$all"
publish: "$all"
proxy: npmjs
access: "$all"
publish: "$all"
proxy: npmjs
uplinks:
npmjs:
url: https://registry.npmjs.org/

View file

@ -54,6 +54,6 @@ In the root of the project add the following contents to `craco.config.js`
const cracoWasm = require("craco-wasm")
module.exports = {
plugins: [cracoWasm()],
plugins: [cracoWasm()]
}
```

View file

@ -1,5 +1,5 @@
const cracoWasm = require("craco-wasm")
module.exports = {
plugins: [cracoWasm()],
plugins: [cracoWasm()]
}

View file

@ -8,7 +8,7 @@
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@automerge/automerge": "2.0.0-alpha.7",
"@automerge/automerge": "2.0.0-alpha.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",

View file

@ -1,11 +1,12 @@
import * as Automerge from "@automerge/automerge"
import logo from "./logo.svg"
import "./App.css"
import logo from './logo.svg';
import './App.css';
let doc = Automerge.init()
doc = Automerge.change(doc, d => (d.hello = "from automerge"))
doc = Automerge.change(doc, (d) => d.hello = "from automerge")
const result = JSON.stringify(doc)
function App() {
return (
<div className="App">
@ -14,7 +15,7 @@ function App() {
<p>{result}</p>
</header>
</div>
)
);
}
export default App
export default App;

View file

@ -1,8 +1,8 @@
import { render, screen } from "@testing-library/react"
import App from "./App"
import { render, screen } from '@testing-library/react';
import App from './App';
test("renders learn react link", () => {
render(<App />)
const linkElement = screen.getByText(/learn react/i)
expect(linkElement).toBeInTheDocument()
})
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View file

@ -1,13 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View file

@ -1,17 +1,17 @@
import React from "react"
import ReactDOM from "react-dom/client"
import "./index.css"
import App from "./App"
import reportWebVitals from "./reportWebVitals"
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById("root"))
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
)
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()
reportWebVitals();

View file

@ -1,13 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry)
getFID(onPerfEntry)
getFCP(onPerfEntry)
getLCP(onPerfEntry)
getTTFB(onPerfEntry)
})
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
}
};
export default reportWebVitals
export default reportWebVitals;

View file

@ -2,4 +2,4 @@
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import "@testing-library/jest-dom"
import '@testing-library/jest-dom';

View file

@ -24,17 +24,17 @@
jsonpointer "^5.0.0"
leven "^3.1.0"
"@automerge/automerge-wasm@0.1.12":
version "0.1.12"
resolved "https://registry.yarnpkg.com/@automerge/automerge-wasm/-/automerge-wasm-0.1.12.tgz#8ce25255d95d4ed6fb387de6858f7b7b7e2ed4a9"
integrity sha512-/xjX1217QYJ+QaoT6iHQw4hGNUIoc3xc65c9eCnfX5v9J9BkTOl05p2Cnr51O2rPc/M6TqZLmlvpvNVdcH9JpA==
"@automerge/automerge-wasm@0.1.9":
version "0.1.9"
resolved "http://localhost:4873/@automerge%2fautomerge-wasm/-/automerge-wasm-0.1.9.tgz#b2def5e8b643f1802bc696843b7755dc444dc2eb"
integrity sha512-S+sjJUJ3aPn2F37vKYAzKxz8CDgbHpOOGVjKSgkLjkAqe1pQ+wp4BpiELXafX73w8DVIrGx1zzru4w3t+Eo8gw==
"@automerge/automerge@2.0.0-alpha.7":
version "2.0.0-alpha.7"
resolved "https://registry.yarnpkg.com/@automerge/automerge/-/automerge-2.0.0-alpha.7.tgz#2ee220d51bcd796074a18af74eeabb5f177e1f36"
integrity sha512-Wd2/GNeqtBybUtXclEE7bWBmmEkhv3q2ITQmLh18V0VvMPbqMBpcOKYzQFnKCyiPyRe5XcYeQAyGyunhE5V0ug==
"@automerge/automerge@2.0.0-alpha.4":
version "2.0.0-alpha.4"
resolved "http://localhost:4873/@automerge%2fautomerge/-/automerge-2.0.0-alpha.4.tgz#df406f5364960a4d21040044da55ebd47406ea3a"
integrity sha512-PVRD1dmLy0U4GttyMvlWr99wyr6xvskJbOkxJDHnp+W2VAFfcqa4QKouaFbJ4W3iIsYX8DfQJ+uhRxa6UnvkHg==
dependencies:
"@automerge/automerge-wasm" "0.1.12"
"@automerge/automerge-wasm" "0.1.9"
uuid "^8.3"
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.8.3":
@ -2827,7 +2827,7 @@ bfj@^7.0.2:
big.js@^5.2.2:
version "5.2.2"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
resolved "http://localhost:4873/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
binary-extensions@^2.0.0:
@ -3817,7 +3817,7 @@ emoji-regex@^9.2.2:
emojis-list@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
resolved "http://localhost:4873/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
encodeurl@~1.0.2:
@ -5845,9 +5845,9 @@ json-stable-stringify-without-jsonify@^1.0.1:
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
json5@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
version "1.0.1"
resolved "http://localhost:4873/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
dependencies:
minimist "^1.2.0"
@ -5942,9 +5942,9 @@ loader-runner@^4.2.0:
integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==
loader-utils@^2.0.0:
version "2.0.4"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c"
integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==
version "2.0.2"
resolved "http://localhost:4873/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129"
integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==
dependencies:
big.js "^5.2.2"
emojis-list "^3.0.0"
@ -6165,9 +6165,9 @@ minimatch@^5.0.1:
brace-expansion "^2.0.1"
minimist@^1.2.0, minimist@^1.2.6:
version "1.2.7"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
version "1.2.6"
resolved "http://localhost:4873/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
mkdirp@~0.5.1:
version "0.5.6"

View file

@ -7,7 +7,6 @@ There are three things you need to do to get WASM packaging working with vite:
3. Exclude `automerge-wasm` from the optimizer
First, install the packages we need:
```bash
yarn add vite-plugin-top-level-await
yarn add vite-plugin-wasm
@ -21,22 +20,22 @@ import wasm from "vite-plugin-wasm"
import topLevelAwait from "vite-plugin-top-level-await"
export default defineConfig({
plugins: [topLevelAwait(), wasm()],
// This is only necessary if you are using `SharedWorker` or `WebWorker`, as
// documented in https://vitejs.dev/guide/features.html#import-with-constructors
worker: {
format: "es",
plugins: [topLevelAwait(), wasm()],
},
// This is only necessary if you are using `SharedWorker` or `WebWorker`, as
// documented in https://vitejs.dev/guide/features.html#import-with-constructors
worker: {
format: "es",
plugins: [topLevelAwait(), wasm()]
},
optimizeDeps: {
// This is necessary because otherwise `vite dev` includes two separate
// versions of the JS wrapper. This causes problems because the JS
// wrapper has a module level variable to track JS side heap
// allocations, initializing this twice causes horrible breakage
exclude: ["@automerge/automerge-wasm"],
},
optimizeDeps: {
// This is necessary because otherwise `vite dev` includes two separate
// versions of the JS wrapper. This causes problems because the JS
// wrapper has a module level variable to track JS side heap
// allocations, initializing this twice causes horrible breakage
exclude: ["@automerge/automerge-wasm"]
}
})
```
@ -52,3 +51,4 @@ yarn vite
yarn install
yarn dev
```

View file

@ -1,15 +1,15 @@
import * as Automerge from "/node_modules/.vite/deps/automerge-js.js?v=6e973f28"
console.log(Automerge)
let doc = Automerge.init()
doc = Automerge.change(doc, d => (d.hello = "from automerge-js"))
console.log(doc)
const result = JSON.stringify(doc)
import * as Automerge from "/node_modules/.vite/deps/automerge-js.js?v=6e973f28";
console.log(Automerge);
let doc = Automerge.init();
doc = Automerge.change(doc, (d) => d.hello = "from automerge-js");
console.log(doc);
const result = JSON.stringify(doc);
if (typeof document !== "undefined") {
const element = document.createElement("div")
element.innerHTML = JSON.stringify(result)
document.body.appendChild(element)
const element = document.createElement("div");
element.innerHTML = JSON.stringify(result);
document.body.appendChild(element);
} else {
console.log("node:", result)
console.log("node:", result);
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi9ob21lL2FsZXgvUHJvamVjdHMvYXV0b21lcmdlL2F1dG9tZXJnZS1ycy9hdXRvbWVyZ2UtanMvZXhhbXBsZXMvdml0ZS9zcmMvbWFpbi50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBBdXRvbWVyZ2UgZnJvbSBcImF1dG9tZXJnZS1qc1wiXG5cbi8vIGhlbGxvIHdvcmxkIGNvZGUgdGhhdCB3aWxsIHJ1biBjb3JyZWN0bHkgb24gd2ViIG9yIG5vZGVcblxuY29uc29sZS5sb2coQXV0b21lcmdlKVxubGV0IGRvYyA9IEF1dG9tZXJnZS5pbml0KClcbmRvYyA9IEF1dG9tZXJnZS5jaGFuZ2UoZG9jLCAoZDogYW55KSA9PiBkLmhlbGxvID0gXCJmcm9tIGF1dG9tZXJnZS1qc1wiKVxuY29uc29sZS5sb2coZG9jKVxuY29uc3QgcmVzdWx0ID0gSlNPTi5zdHJpbmdpZnkoZG9jKVxuXG5pZiAodHlwZW9mIGRvY3VtZW50ICE9PSAndW5kZWZpbmVkJykge1xuICAgIC8vIGJyb3dzZXJcbiAgICBjb25zdCBlbGVtZW50ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnZGl2Jyk7XG4gICAgZWxlbWVudC5pbm5lckhUTUwgPSBKU09OLnN0cmluZ2lmeShyZXN1bHQpXG4gICAgZG9jdW1lbnQuYm9keS5hcHBlbmRDaGlsZChlbGVtZW50KTtcbn0gZWxzZSB7XG4gICAgLy8gc2VydmVyXG4gICAgY29uc29sZS5sb2coXCJub2RlOlwiLCByZXN1bHQpXG59XG5cbiJdLCJtYXBwaW5ncyI6IkFBQUEsWUFBWSxlQUFlO0FBSTNCLFFBQVEsSUFBSSxTQUFTO0FBQ3JCLElBQUksTUFBTSxVQUFVLEtBQUs7QUFDekIsTUFBTSxVQUFVLE9BQU8sS0FBSyxDQUFDLE1BQVcsRUFBRSxRQUFRLG1CQUFtQjtBQUNyRSxRQUFRLElBQUksR0FBRztBQUNmLE1BQU0sU0FBUyxLQUFLLFVBQVUsR0FBRztBQUVqQyxJQUFJLE9BQU8sYUFBYSxhQUFhO0FBRWpDLFFBQU0sVUFBVSxTQUFTLGNBQWMsS0FBSztBQUM1QyxVQUFRLFlBQVksS0FBSyxVQUFVLE1BQU07QUFDekMsV0FBUyxLQUFLLFlBQVksT0FBTztBQUNyQyxPQUFPO0FBRUgsVUFBUSxJQUFJLFNBQVMsTUFBTTtBQUMvQjsiLCJuYW1lcyI6W119
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi9ob21lL2FsZXgvUHJvamVjdHMvYXV0b21lcmdlL2F1dG9tZXJnZS1ycy9hdXRvbWVyZ2UtanMvZXhhbXBsZXMvdml0ZS9zcmMvbWFpbi50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBBdXRvbWVyZ2UgZnJvbSBcImF1dG9tZXJnZS1qc1wiXG5cbi8vIGhlbGxvIHdvcmxkIGNvZGUgdGhhdCB3aWxsIHJ1biBjb3JyZWN0bHkgb24gd2ViIG9yIG5vZGVcblxuY29uc29sZS5sb2coQXV0b21lcmdlKVxubGV0IGRvYyA9IEF1dG9tZXJnZS5pbml0KClcbmRvYyA9IEF1dG9tZXJnZS5jaGFuZ2UoZG9jLCAoZDogYW55KSA9PiBkLmhlbGxvID0gXCJmcm9tIGF1dG9tZXJnZS1qc1wiKVxuY29uc29sZS5sb2coZG9jKVxuY29uc3QgcmVzdWx0ID0gSlNPTi5zdHJpbmdpZnkoZG9jKVxuXG5pZiAodHlwZW9mIGRvY3VtZW50ICE9PSAndW5kZWZpbmVkJykge1xuICAgIC8vIGJyb3dzZXJcbiAgICBjb25zdCBlbGVtZW50ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnZGl2Jyk7XG4gICAgZWxlbWVudC5pbm5lckhUTUwgPSBKU09OLnN0cmluZ2lmeShyZXN1bHQpXG4gICAgZG9jdW1lbnQuYm9keS5hcHBlbmRDaGlsZChlbGVtZW50KTtcbn0gZWxzZSB7XG4gICAgLy8gc2VydmVyXG4gICAgY29uc29sZS5sb2coXCJub2RlOlwiLCByZXN1bHQpXG59XG5cbiJdLCJtYXBwaW5ncyI6IkFBQUEsWUFBWSxlQUFlO0FBSTNCLFFBQVEsSUFBSSxTQUFTO0FBQ3JCLElBQUksTUFBTSxVQUFVLEtBQUs7QUFDekIsTUFBTSxVQUFVLE9BQU8sS0FBSyxDQUFDLE1BQVcsRUFBRSxRQUFRLG1CQUFtQjtBQUNyRSxRQUFRLElBQUksR0FBRztBQUNmLE1BQU0sU0FBUyxLQUFLLFVBQVUsR0FBRztBQUVqQyxJQUFJLE9BQU8sYUFBYSxhQUFhO0FBRWpDLFFBQU0sVUFBVSxTQUFTLGNBQWMsS0FBSztBQUM1QyxVQUFRLFlBQVksS0FBSyxVQUFVLE1BQU07QUFDekMsV0FBUyxLQUFLLFlBQVksT0FBTztBQUNyQyxPQUFPO0FBRUgsVUFBUSxJQUFJLFNBQVMsTUFBTTtBQUMvQjsiLCJuYW1lcyI6W119

View file

@ -9,7 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"@automerge/automerge": "2.0.0-alpha.7"
"@automerge/automerge": "2.0.0-alpha.5"
},
"devDependencies": {
"typescript": "^4.6.4",

View file

@ -4,6 +4,6 @@ export function setupCounter(element: HTMLButtonElement) {
counter = count
element.innerHTML = `count is ${counter}`
}
element.addEventListener("click", () => setCounter(++counter))
element.addEventListener('click', () => setCounter(++counter))
setCounter(0)
}

View file

@ -3,15 +3,16 @@ import * as Automerge from "@automerge/automerge"
// hello world code that will run correctly on web or node
let doc = Automerge.init()
doc = Automerge.change(doc, (d: any) => (d.hello = "from automerge"))
doc = Automerge.change(doc, (d: any) => d.hello = "from automerge")
const result = JSON.stringify(doc)
if (typeof document !== "undefined") {
// browser
const element = document.createElement("div")
element.innerHTML = JSON.stringify(result)
document.body.appendChild(element)
if (typeof document !== 'undefined') {
// browser
const element = document.createElement('div');
element.innerHTML = JSON.stringify(result)
document.body.appendChild(element);
} else {
// server
console.log("node:", result)
// server
console.log("node:", result)
}

View file

@ -3,20 +3,20 @@ import wasm from "vite-plugin-wasm"
import topLevelAwait from "vite-plugin-top-level-await"
export default defineConfig({
plugins: [topLevelAwait(), wasm()],
// This is only necessary if you are using `SharedWorker` or `WebWorker`, as
// documented in https://vitejs.dev/guide/features.html#import-with-constructors
worker: {
format: "es",
plugins: [topLevelAwait(), wasm()],
},
optimizeDeps: {
// This is necessary because otherwise `vite dev` includes two separate
// versions of the JS wrapper. This causes problems because the JS
// wrapper has a module level variable to track JS side heap
// allocations, initializing this twice causes horrible breakage
exclude: ["@automerge/automerge-wasm"],
},
// This is only necessary if you are using `SharedWorker` or `WebWorker`, as
// documented in https://vitejs.dev/guide/features.html#import-with-constructors
worker: {
format: "es",
plugins: [topLevelAwait(), wasm()]
},
optimizeDeps: {
// This is necessary because otherwise `vite dev` includes two separate
// versions of the JS wrapper. This causes problems because the JS
// wrapper has a module level variable to track JS side heap
// allocations, initializing this twice causes horrible breakage
exclude: ["@automerge/automerge-wasm"]
}
})

View file

@ -1,34 +1,36 @@
# Webpack + Automerge
Getting WASM working in webpack 5 is very easy. You just need to enable the
`asyncWebAssembly`
[experiment](https://webpack.js.org/configuration/experiments/). For example:
```javascript
const path = require("path")
const path = require('path');
const clientConfig = {
experiments: { asyncWebAssembly: true },
target: "web",
entry: "./src/index.js",
target: 'web',
entry: './src/index.js',
output: {
filename: "main.js",
path: path.resolve(__dirname, "public"),
filename: 'main.js',
path: path.resolve(__dirname, 'public'),
},
mode: "development", // or production
performance: {
// we dont want the wasm blob to generate warnings
hints: false,
maxEntrypointSize: 512000,
maxAssetSize: 512000,
},
}
performance: { // we dont want the wasm blob to generate warnings
hints: false,
maxEntrypointSize: 512000,
maxAssetSize: 512000
}
};
module.exports = clientConfig
```
## Running the example
```bash
yarn install
yarn start

View file

@ -10,7 +10,7 @@
},
"author": "",
"dependencies": {
"@automerge/automerge": "2.0.0-alpha.7"
"@automerge/automerge": "2.0.0-alpha.5"
},
"devDependencies": {
"serve": "^13.0.2",

View file

@ -3,15 +3,16 @@ import * as Automerge from "@automerge/automerge"
// hello world code that will run correctly on web or node
let doc = Automerge.init()
doc = Automerge.change(doc, d => (d.hello = "from automerge"))
doc = Automerge.change(doc, (d) => d.hello = "from automerge")
const result = JSON.stringify(doc)
if (typeof document !== "undefined") {
if (typeof document !== 'undefined') {
// browser
const element = document.createElement("div")
const element = document.createElement('div');
element.innerHTML = JSON.stringify(result)
document.body.appendChild(element)
document.body.appendChild(element);
} else {
// server
console.log("node:", result)
}

View file

@ -1,37 +1,36 @@
const path = require("path")
const nodeExternals = require("webpack-node-externals")
const path = require('path');
const nodeExternals = require('webpack-node-externals');
// the most basic webpack config for node or web targets for automerge-wasm
const serverConfig = {
// basic setup for bundling a node package
target: "node",
target: 'node',
externals: [nodeExternals()],
externalsPresets: { node: true },
entry: "./src/index.js",
entry: './src/index.js',
output: {
filename: "node.js",
path: path.resolve(__dirname, "dist"),
filename: 'node.js',
path: path.resolve(__dirname, 'dist'),
},
mode: "development", // or production
}
};
const clientConfig = {
experiments: { asyncWebAssembly: true },
target: "web",
entry: "./src/index.js",
target: 'web',
entry: './src/index.js',
output: {
filename: "main.js",
path: path.resolve(__dirname, "public"),
filename: 'main.js',
path: path.resolve(__dirname, 'public'),
},
mode: "development", // or production
performance: {
// we dont want the wasm blob to generate warnings
hints: false,
maxEntrypointSize: 512000,
maxAssetSize: 512000,
},
}
performance: { // we dont want the wasm blob to generate warnings
hints: false,
maxEntrypointSize: 512000,
maxAssetSize: 512000
}
};
module.exports = [serverConfig, clientConfig]
module.exports = [serverConfig, clientConfig];

View file

@ -4,7 +4,7 @@
"Orion Henry <orion@inkandswitch.com>",
"Martin Kleppmann"
],
"version": "2.0.2",
"version": "2.0.0-alpha.5",
"description": "Javascript implementation of automerge, backed by @automerge/automerge-wasm",
"homepage": "https://github.com/automerge/automerge-rs/tree/main/wrappers/javascript",
"repository": "github:automerge/automerge-rs",
@ -12,10 +12,26 @@
"README.md",
"LICENSE",
"package.json",
"dist/index.d.ts",
"dist/cjs/**/*.js",
"dist/mjs/**/*.js",
"dist/*.d.ts"
"index.d.ts",
"dist/*.d.ts",
"dist/cjs/constants.js",
"dist/cjs/types.js",
"dist/cjs/numbers.js",
"dist/cjs/index.js",
"dist/cjs/uuid.js",
"dist/cjs/counter.js",
"dist/cjs/low_level.js",
"dist/cjs/text.js",
"dist/cjs/proxies.js",
"dist/mjs/constants.js",
"dist/mjs/types.js",
"dist/mjs/numbers.js",
"dist/mjs/index.js",
"dist/mjs/uuid.js",
"dist/mjs/counter.js",
"dist/mjs/low_level.js",
"dist/mjs/text.js",
"dist/mjs/proxies.js"
],
"types": "./dist/index.d.ts",
"module": "./dist/mjs/index.js",
@ -23,31 +39,26 @@
"license": "MIT",
"scripts": {
"lint": "eslint src",
"build": "tsc -p config/mjs.json && tsc -p config/cjs.json && tsc -p config/declonly.json --emitDeclarationOnly",
"test": "ts-mocha test/*.ts",
"deno:build": "denoify && node ./scripts/deno-prefixer.mjs",
"deno:test": "deno test ./deno-tests/deno.ts --allow-read --allow-net",
"watch-docs": "typedoc src/index.ts --watch --readme none"
"build": "tsc -p config/mjs.json && tsc -p config/cjs.json && tsc --emitDeclarationOnly",
"test": "ts-mocha test/*.ts"
},
"devDependencies": {
"@types/expect": "^24.3.0",
"@types/mocha": "^10.0.1",
"@types/uuid": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^5.46.0",
"@typescript-eslint/parser": "^5.46.0",
"denoify": "^1.4.5",
"eslint": "^8.29.0",
"@types/mocha": "^9.1.1",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.25.0",
"@typescript-eslint/parser": "^5.25.0",
"eslint": "^8.15.0",
"fast-sha256": "^1.3.0",
"mocha": "^10.2.0",
"pako": "^2.1.0",
"prettier": "^2.8.1",
"mocha": "^10.0.0",
"pako": "^2.0.4",
"ts-mocha": "^10.0.0",
"ts-node": "^10.9.1",
"typedoc": "^0.23.22",
"typescript": "^4.9.4"
"typedoc": "^0.23.16",
"typescript": "^4.6.4"
},
"dependencies": {
"@automerge/automerge-wasm": "0.1.25",
"uuid": "^9.0.0"
"@automerge/automerge-wasm": "0.1.10",
"uuid": "^8.3"
}
}

View file

@ -1,9 +0,0 @@
import * as fs from "fs"
const files = ["./deno_dist/proxies.ts"]
for (const filepath of files) {
const data = fs.readFileSync(filepath)
fs.writeFileSync(filepath, "// @ts-nocheck \n" + data)
console.log('Prepended "// @ts-nocheck" to ' + filepath)
}

View file

@ -1,42 +0,0 @@
// @denoify-ignore
import { makeThisModuleAnExecutableReplacer } from "denoify"
// import { assert } from "tsafe";
// import * as path from "path";
makeThisModuleAnExecutableReplacer(
async ({ parsedImportExportStatement, destDirPath, version }) => {
version = process.env.VERSION || version
switch (parsedImportExportStatement.parsedArgument.nodeModuleName) {
case "@automerge/automerge-wasm":
{
const moduleRoot =
process.env.ROOT_MODULE ||
`https://deno.land/x/automerge_wasm@${version}`
/*
*We expect not to run against statements like
*import(..).then(...)
*or
*export * from "..."
*in our code.
*/
if (
!parsedImportExportStatement.isAsyncImport &&
(parsedImportExportStatement.statementType === "import" ||
parsedImportExportStatement.statementType === "export")
) {
if (parsedImportExportStatement.isTypeOnly) {
return `${parsedImportExportStatement.statementType} type ${parsedImportExportStatement.target} from "${moduleRoot}/index.d.ts";`
} else {
return `${parsedImportExportStatement.statementType} ${parsedImportExportStatement.target} from "${moduleRoot}/automerge_wasm.js";`
}
}
}
break
}
//The replacer should return undefined when we want to let denoify replace the statement
return undefined
}
)

View file

@ -1,100 +0,0 @@
import { Counter, type AutomergeValue } from "./types"
import { Text } from "./text"
import { type AutomergeValue as UnstableAutomergeValue } from "./unstable_types"
import { type Target, Text1Target, Text2Target } from "./proxies"
import { mapProxy, listProxy, ValueType } from "./proxies"
import type { Prop, ObjID } from "@automerge/automerge-wasm"
import { Automerge } from "@automerge/automerge-wasm"
export type ConflictsF<T extends Target> = { [key: string]: ValueType<T> }
export type Conflicts = ConflictsF<Text1Target>
export type UnstableConflicts = ConflictsF<Text2Target>
export function stableConflictAt(
context: Automerge,
objectId: ObjID,
prop: Prop
): Conflicts | undefined {
return conflictAt<Text1Target>(
context,
objectId,
prop,
true,
(context: Automerge, conflictId: ObjID): AutomergeValue => {
return new Text(context.text(conflictId))
}
)
}
export function unstableConflictAt(
context: Automerge,
objectId: ObjID,
prop: Prop
): UnstableConflicts | undefined {
return conflictAt<Text2Target>(
context,
objectId,
prop,
true,
(context: Automerge, conflictId: ObjID): UnstableAutomergeValue => {
return context.text(conflictId)
}
)
}
function conflictAt<T extends Target>(
context: Automerge,
objectId: ObjID,
prop: Prop,
textV2: boolean,
handleText: (a: Automerge, conflictId: ObjID) => ValueType<T>
): ConflictsF<T> | undefined {
const values = context.getAll(objectId, prop)
if (values.length <= 1) {
return
}
const result: ConflictsF<T> = {}
for (const fullVal of values) {
switch (fullVal[0]) {
case "map":
result[fullVal[1]] = mapProxy<T>(
context,
fullVal[1],
textV2,
[prop],
true
)
break
case "list":
result[fullVal[1]] = listProxy<T>(
context,
fullVal[1],
textV2,
[prop],
true
)
break
case "text":
result[fullVal[1]] = handleText(context, fullVal[1] as ObjID)
break
case "str":
case "uint":
case "int":
case "f64":
case "boolean":
case "bytes":
case "null":
result[fullVal[2]] = fullVal[1] as ValueType<T>
break
case "counter":
result[fullVal[2]] = new Counter(fullVal[1]) as ValueType<T>
break
case "timestamp":
result[fullVal[2]] = new Date(fullVal[1]) as ValueType<T>
break
default:
throw RangeError(`datatype ${fullVal[0]} unimplemented`)
}
}
return result
}

View file

@ -1,12 +1,24 @@
// Properties of the document root object
//const OPTIONS = Symbol('_options') // object containing options passed to init()
//const CACHE = Symbol('_cache') // map from objectId to immutable object
//export const STATE = Symbol.for('_am_state') // object containing metadata about current state (e.g. sequence numbers)
export const STATE = Symbol.for('_am_meta') // object containing metadata about current state (e.g. sequence numbers)
export const HEADS = Symbol.for('_am_heads') // object containing metadata about current state (e.g. sequence numbers)
export const TRACE = Symbol.for('_am_trace') // object containing metadata about current state (e.g. sequence numbers)
export const OBJECT_ID = Symbol.for('_am_objectId') // object containing metadata about current state (e.g. sequence numbers)
export const READ_ONLY = Symbol.for('_am_readOnly') // object containing metadata about current state (e.g. sequence numbers)
export const FROZEN = Symbol.for('_am_frozen') // object containing metadata about current state (e.g. sequence numbers)
export const UINT = Symbol.for('_am_uint')
export const INT = Symbol.for('_am_int')
export const F64 = Symbol.for('_am_f64')
export const COUNTER = Symbol.for('_am_counter')
export const TEXT = Symbol.for('_am_text')
// Properties of all Automerge objects
//const OBJECT_ID = Symbol('_objectId') // the object ID of the current object (string)
//const CONFLICTS = Symbol('_conflicts') // map or list (depending on object type) of conflicts
//const CHANGE = Symbol('_change') // the context object on proxy objects used in change callback
//const ELEM_IDS = Symbol('_elemIds') // list containing the element ID of each list element
export const STATE = Symbol.for("_am_meta") // symbol used to hide application metadata on automerge objects
export const TRACE = Symbol.for("_am_trace") // used for debugging
export const OBJECT_ID = Symbol.for("_am_objectId") // symbol used to hide the object id on automerge objects
export const IS_PROXY = Symbol.for("_am_isProxy") // symbol used to test if the document is a proxy object
export const UINT = Symbol.for("_am_uint")
export const INT = Symbol.for("_am_int")
export const F64 = Symbol.for("_am_f64")
export const COUNTER = Symbol.for("_am_counter")
export const TEXT = Symbol.for("_am_text")

View file

@ -1,4 +1,4 @@
import { Automerge, type ObjID, type Prop } from "@automerge/automerge-wasm"
import { Automerge, ObjID, Prop } from "@automerge/automerge-wasm"
import { COUNTER } from "./constants"
/**
* The most basic CRDT: an integer value that can be changed only by
@ -6,7 +6,7 @@ import { COUNTER } from "./constants"
* the value trivially converges.
*/
export class Counter {
value: number
value : number;
constructor(value?: number) {
this.value = value || 0
@ -21,7 +21,7 @@ export class Counter {
* concatenating it with another string, as in `x + ''`.
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/valueOf
*/
valueOf(): number {
valueOf() : number {
return this.value
}
@ -30,7 +30,7 @@ export class Counter {
* this method is called e.g. when you do `['value: ', x].join('')` or when
* you use string interpolation: `value: ${x}`.
*/
toString(): string {
toString() : string {
return this.valueOf().toString()
}
@ -38,7 +38,7 @@ export class Counter {
* Returns the counter value, so that a JSON serialization of an Automerge
* document represents the counter simply as an integer.
*/
toJSON(): number {
toJSON() : number {
return this.value
}
}
@ -49,30 +49,24 @@ export class Counter {
*/
class WriteableCounter extends Counter {
context: Automerge
path: Prop[]
path: string[]
objectId: ObjID
key: Prop
constructor(
value: number,
context: Automerge,
path: Prop[],
objectId: ObjID,
key: Prop
) {
constructor(value: number, context: Automerge, path: string[], objectId: ObjID, key: Prop) {
super(value)
this.context = context
this.path = path
this.objectId = objectId
this.key = key
}
/**
* Increases the value of the counter by `delta`. If `delta` is not given,
* increases the value of the counter by 1.
*/
increment(delta: number): number {
delta = typeof delta === "number" ? delta : 1
increment(delta: number) : number {
delta = typeof delta === 'number' ? delta : 1
this.context.increment(this.objectId, this.key, delta)
this.value += delta
return this.value
@ -82,8 +76,8 @@ class WriteableCounter extends Counter {
* Decreases the value of the counter by `delta`. If `delta` is not given,
* decreases the value of the counter by 1.
*/
decrement(delta: number): number {
return this.increment(typeof delta === "number" ? -delta : -1)
decrement(delta: number) : number {
return this.increment(typeof delta === 'number' ? -delta : -1)
}
}
@ -93,14 +87,8 @@ class WriteableCounter extends Counter {
* `objectId` is the ID of the object containing the counter, and `key` is
* the property name (key in map, or index in list) where the counter is
* located.
*/
export function getWriteableCounter(
value: number,
context: Automerge,
path: Prop[],
objectId: ObjID,
key: Prop
): WriteableCounter {
*/
export function getWriteableCounter(value: number, context: Automerge, path: string[], objectId: ObjID, key: Prop) {
return new WriteableCounter(value, context, path, objectId, key)
}

File diff suppressed because it is too large Load diff

View file

@ -1,43 +0,0 @@
import { type ObjID, type Heads, Automerge } from "@automerge/automerge-wasm"
import { STATE, OBJECT_ID, TRACE, IS_PROXY } from "./constants"
import type { Doc, PatchCallback } from "./types"
export interface InternalState<T> {
handle: Automerge
heads: Heads | undefined
freeze: boolean
patchCallback?: PatchCallback<T>
textV2: boolean
}
export function _state<T>(doc: Doc<T>, checkroot = true): InternalState<T> {
if (typeof doc !== "object") {
throw new RangeError("must be the document root")
}
const state = Reflect.get(doc, STATE) as InternalState<T>
if (
state === undefined ||
state == null ||
(checkroot && _obj(doc) !== "_root")
) {
throw new RangeError("must be the document root")
}
return state
}
export function _trace<T>(doc: Doc<T>): string | undefined {
return Reflect.get(doc, TRACE) as string
}
export function _obj<T>(doc: Doc<T>): ObjID | null {
if (!(typeof doc === "object") || doc === null) {
return null
}
return Reflect.get(doc, OBJECT_ID) as ObjID
}
export function _is_proxy<T>(doc: Doc<T>): boolean {
return !!Reflect.get(doc, IS_PROXY)
}

View file

@ -1,58 +1,25 @@
import {
type API,
Automerge,
type Change,
type DecodedChange,
type Actor,
SyncState,
type SyncMessage,
type JsSyncState,
type DecodedSyncMessage,
type ChangeToEncode,
} from "@automerge/automerge-wasm"
export type { ChangeToEncode } from "@automerge/automerge-wasm"
import { Automerge, Change, DecodedChange, Actor, SyncState, SyncMessage, JsSyncState, DecodedSyncMessage } from "@automerge/automerge-wasm"
import { API } from "@automerge/automerge-wasm"
export function UseApi(api: API) {
for (const k in api) {
// eslint-disable-next-line @typescript-eslint/no-extra-semi,@typescript-eslint/no-explicit-any
;(ApiHandler as any)[k] = (api as any)[k]
ApiHandler[k] = api[k]
}
}
/* eslint-disable */
export const ApiHandler: API = {
create(textV2: boolean, actor?: Actor): Automerge {
throw new RangeError("Automerge.use() not called")
},
load(data: Uint8Array, textV2: boolean, actor?: Actor): Automerge {
throw new RangeError("Automerge.use() not called (load)")
},
encodeChange(change: ChangeToEncode): Change {
throw new RangeError("Automerge.use() not called (encodeChange)")
},
decodeChange(change: Change): DecodedChange {
throw new RangeError("Automerge.use() not called (decodeChange)")
},
initSyncState(): SyncState {
throw new RangeError("Automerge.use() not called (initSyncState)")
},
encodeSyncMessage(message: DecodedSyncMessage): SyncMessage {
throw new RangeError("Automerge.use() not called (encodeSyncMessage)")
},
decodeSyncMessage(msg: SyncMessage): DecodedSyncMessage {
throw new RangeError("Automerge.use() not called (decodeSyncMessage)")
},
encodeSyncState(state: SyncState): Uint8Array {
throw new RangeError("Automerge.use() not called (encodeSyncState)")
},
decodeSyncState(data: Uint8Array): SyncState {
throw new RangeError("Automerge.use() not called (decodeSyncState)")
},
exportSyncState(state: SyncState): JsSyncState {
throw new RangeError("Automerge.use() not called (exportSyncState)")
},
importSyncState(state: JsSyncState): SyncState {
throw new RangeError("Automerge.use() not called (importSyncState)")
},
export const ApiHandler : API = {
create(actor?: Actor): Automerge { throw new RangeError("Automerge.use() not called") },
load(data: Uint8Array, actor?: Actor): Automerge { throw new RangeError("Automerge.use() not called (load)") },
encodeChange(change: DecodedChange): Change { throw new RangeError("Automerge.use() not called (encodeChange)") },
decodeChange(change: Change): DecodedChange { throw new RangeError("Automerge.use() not called (decodeChange)") },
initSyncState(): SyncState { throw new RangeError("Automerge.use() not called (initSyncState)") },
encodeSyncMessage(message: DecodedSyncMessage): SyncMessage { throw new RangeError("Automerge.use() not called (encodeSyncMessage)") },
decodeSyncMessage(msg: SyncMessage): DecodedSyncMessage { throw new RangeError("Automerge.use() not called (decodeSyncMessage)") },
encodeSyncState(state: SyncState): Uint8Array { throw new RangeError("Automerge.use() not called (encodeSyncState)") },
decodeSyncState(data: Uint8Array): SyncState { throw new RangeError("Automerge.use() not called (decodeSyncState)") },
exportSyncState(state: SyncState): JsSyncState { throw new RangeError("Automerge.use() not called (exportSyncState)") },
importSyncState(state: JsSyncState): SyncState { throw new RangeError("Automerge.use() not called (importSyncState)") },
}
/* eslint-enable */

View file

@ -1,18 +1,12 @@
// Convenience classes to allow users to strictly specify the number type they want
// Convience classes to allow users to stricly specify the number type they want
import { INT, UINT, F64 } from "./constants"
export class Int {
value: number
value: number;
constructor(value: number) {
if (
!(
Number.isInteger(value) &&
value <= Number.MAX_SAFE_INTEGER &&
value >= Number.MIN_SAFE_INTEGER
)
) {
if (!(Number.isInteger(value) && value <= Number.MAX_SAFE_INTEGER && value >= Number.MIN_SAFE_INTEGER)) {
throw new RangeError(`Value ${value} cannot be a uint`)
}
this.value = value
@ -22,16 +16,10 @@ export class Int {
}
export class Uint {
value: number
value: number;
constructor(value: number) {
if (
!(
Number.isInteger(value) &&
value <= Number.MAX_SAFE_INTEGER &&
value >= 0
)
) {
if (!(Number.isInteger(value) && value <= Number.MAX_SAFE_INTEGER && value >= 0)) {
throw new RangeError(`Value ${value} cannot be a uint`)
}
this.value = value
@ -41,10 +29,10 @@ export class Uint {
}
export class Float64 {
value: number
value: number;
constructor(value: number) {
if (typeof value !== "number") {
if (typeof value !== 'number') {
throw new RangeError(`Value ${value} cannot be a float64`)
}
this.value = value || 0.0
@ -52,3 +40,4 @@ export class Float64 {
Object.freeze(this)
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,6 +0,0 @@
export class RawString {
val: string
constructor(val: string) {
this.val = val
}
}

View file

@ -1,944 +0,0 @@
/** @hidden **/
export { /** @hidden */ uuid } from "./uuid"
import { rootProxy } from "./proxies"
import { STATE } from "./constants"
import {
type AutomergeValue,
Counter,
type Doc,
type PatchCallback,
} from "./types"
export {
type AutomergeValue,
Counter,
type Doc,
Int,
Uint,
Float64,
type Patch,
type PatchCallback,
type ScalarValue,
} from "./types"
import { Text } from "./text"
export { Text } from "./text"
import type {
API as WasmAPI,
Actor as ActorId,
Prop,
ObjID,
Change,
DecodedChange,
Heads,
MaterializeValue,
JsSyncState,
SyncMessage,
DecodedSyncMessage,
} from "@automerge/automerge-wasm"
export type {
PutPatch,
DelPatch,
SpliceTextPatch,
InsertPatch,
IncPatch,
SyncMessage,
} from "@automerge/automerge-wasm"
/** @hidden **/
type API = WasmAPI
const SyncStateSymbol = Symbol("_syncstate")
/**
* An opaque type tracking the state of sync with a remote peer
*/
type SyncState = JsSyncState & { _opaque: typeof SyncStateSymbol }
import { ApiHandler, type ChangeToEncode, UseApi } from "./low_level"
import { Automerge } from "@automerge/automerge-wasm"
import { RawString } from "./raw_string"
import { _state, _is_proxy, _trace, _obj } from "./internal_state"
import { stableConflictAt } from "./conflicts"
/** Options passed to {@link change}, and {@link emptyChange}
* @typeParam T - The type of value contained in the document
*/
export type ChangeOptions<T> = {
/** A message which describes the changes */
message?: string
/** The unix timestamp of the change (purely advisory, not used in conflict resolution) */
time?: number
/** A callback which will be called to notify the caller of any changes to the document */
patchCallback?: PatchCallback<T>
}
/** Options passed to {@link loadIncremental}, {@link applyChanges}, and {@link receiveSyncMessage}
* @typeParam T - The type of value contained in the document
*/
export type ApplyOptions<T> = { patchCallback?: PatchCallback<T> }
/**
* A List is an extended Array that adds the two helper methods `deleteAt` and `insertAt`.
*/
export interface List<T> extends Array<T> {
insertAt(index: number, ...args: T[]): List<T>
deleteAt(index: number, numDelete?: number): List<T>
}
/**
* To extend an arbitrary type, we have to turn any arrays that are part of the type's definition into Lists.
* So we recurse through the properties of T, turning any Arrays we find into Lists.
*/
export type Extend<T> =
// is it an array? make it a list (we recursively extend the type of the array's elements as well)
T extends Array<infer T>
? List<Extend<T>>
: // is it an object? recursively extend all of its properties
// eslint-disable-next-line @typescript-eslint/ban-types
T extends Object
? { [P in keyof T]: Extend<T[P]> }
: // otherwise leave the type alone
T
/**
* Function which is called by {@link change} when making changes to a `Doc<T>`
* @typeParam T - The type of value contained in the document
*
* This function may mutate `doc`
*/
export type ChangeFn<T> = (doc: Extend<T>) => void
/** @hidden **/
export interface State<T> {
change: DecodedChange
snapshot: T
}
/** @hidden **/
export function use(api: API) {
UseApi(api)
}
import * as wasm from "@automerge/automerge-wasm"
use(wasm)
/**
* Options to be passed to {@link init} or {@link load}
* @typeParam T - The type of the value the document contains
*/
export type InitOptions<T> = {
/** The actor ID to use for this document, a random one will be generated if `null` is passed */
actor?: ActorId
freeze?: boolean
/** A callback which will be called with the initial patch once the document has finished loading */
patchCallback?: PatchCallback<T>
/** @hidden */
enableTextV2?: boolean
}
/** @hidden */
export function getBackend<T>(doc: Doc<T>): Automerge {
return _state(doc).handle
}
function importOpts<T>(_actor?: ActorId | InitOptions<T>): InitOptions<T> {
if (typeof _actor === "object") {
return _actor
} else {
return { actor: _actor }
}
}
/**
* Create a new automerge document
*
* @typeParam T - The type of value contained in the document. This will be the
* type that is passed to the change closure in {@link change}
* @param _opts - Either an actorId or an {@link InitOptions} (which may
* contain an actorId). If this is null the document will be initialised with a
* random actor ID
*/
export function init<T>(_opts?: ActorId | InitOptions<T>): Doc<T> {
const opts = importOpts(_opts)
const freeze = !!opts.freeze
const patchCallback = opts.patchCallback
const handle = ApiHandler.create(opts.enableTextV2 || false, opts.actor)
handle.enablePatches(true)
handle.enableFreeze(!!opts.freeze)
handle.registerDatatype("counter", (n: number) => new Counter(n))
const textV2 = opts.enableTextV2 || false
if (textV2) {
handle.registerDatatype("str", (n: string) => new RawString(n))
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handle.registerDatatype("text", (n: any) => new Text(n))
}
const doc = handle.materialize("/", undefined, {
handle,
heads: undefined,
freeze,
patchCallback,
textV2,
}) as Doc<T>
return doc
}
/**
* Make an immutable view of an automerge document as at `heads`
*
* @remarks
* The document returned from this function cannot be passed to {@link change}.
* This is because it shares the same underlying memory as `doc`, but it is
* consequently a very cheap copy.
*
* Note that this function will throw an error if any of the hashes in `heads`
* are not in the document.
*
* @typeParam T - The type of the value contained in the document
* @param doc - The document to create a view of
* @param heads - The hashes of the heads to create a view at
*/
export function view<T>(doc: Doc<T>, heads: Heads): Doc<T> {
const state = _state(doc)
const handle = state.handle
return state.handle.materialize("/", heads, {
...state,
handle,
heads,
}) as Doc<T>
}
/**
* Make a full writable copy of an automerge document
*
* @remarks
* Unlike {@link view} this function makes a full copy of the memory backing
* the document and can thus be passed to {@link change}. It also generates a
* new actor ID so that changes made in the new document do not create duplicate
* sequence numbers with respect to the old document. If you need control over
* the actor ID which is generated you can pass the actor ID as the second
* argument
*
* @typeParam T - The type of the value contained in the document
* @param doc - The document to clone
* @param _opts - Either an actor ID to use for the new doc or an {@link InitOptions}
*/
export function clone<T>(
doc: Doc<T>,
_opts?: ActorId | InitOptions<T>
): Doc<T> {
const state = _state(doc)
const heads = state.heads
const opts = importOpts(_opts)
const handle = state.handle.fork(opts.actor, heads)
// `change` uses the presence of state.heads to determine if we are in a view
// set it to undefined to indicate that this is a full fat document
const { heads: _oldHeads, ...stateSansHeads } = state
return handle.applyPatches(doc, { ...stateSansHeads, handle })
}
/** Explicity free the memory backing a document. Note that this is note
* necessary in environments which support
* [`FinalizationRegistry`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry)
*/
export function free<T>(doc: Doc<T>) {
return _state(doc).handle.free()
}
/**
* Create an automerge document from a POJO
*
* @param initialState - The initial state which will be copied into the document
* @typeParam T - The type of the value passed to `from` _and_ the type the resulting document will contain
* @typeParam actor - The actor ID of the resulting document, if this is null a random actor ID will be used
*
* @example
* ```
* const doc = automerge.from({
* tasks: [
* {description: "feed dogs", done: false}
* ]
* })
* ```
*/
export function from<T extends Record<string, unknown>>(
initialState: T | Doc<T>,
_opts?: ActorId | InitOptions<T>
): Doc<T> {
return change(init(_opts), d => Object.assign(d, initialState))
}
/**
* Update the contents of an automerge document
* @typeParam T - The type of the value contained in the document
* @param doc - The document to update
* @param options - Either a message, an {@link ChangeOptions}, or a {@link ChangeFn}
* @param callback - A `ChangeFn` to be used if `options` was a `string`
*
* Note that if the second argument is a function it will be used as the `ChangeFn` regardless of what the third argument is.
*
* @example A simple change
* ```
* let doc1 = automerge.init()
* doc1 = automerge.change(doc1, d => {
* d.key = "value"
* })
* assert.equal(doc1.key, "value")
* ```
*
* @example A change with a message
*
* ```
* doc1 = automerge.change(doc1, "add another value", d => {
* d.key2 = "value2"
* })
* ```
*
* @example A change with a message and a timestamp
*
* ```
* doc1 = automerge.change(doc1, {message: "add another value", time: 1640995200}, d => {
* d.key2 = "value2"
* })
* ```
*
* @example responding to a patch callback
* ```
* let patchedPath
* let patchCallback = patch => {
* patchedPath = patch.path
* }
* doc1 = automerge.change(doc1, {message, "add another value", time: 1640995200, patchCallback}, d => {
* d.key2 = "value2"
* })
* assert.equal(patchedPath, ["key2"])
* ```
*/
export function change<T>(
doc: Doc<T>,
options: string | ChangeOptions<T> | ChangeFn<T>,
callback?: ChangeFn<T>
): Doc<T> {
if (typeof options === "function") {
return _change(doc, {}, options)
} else if (typeof callback === "function") {
if (typeof options === "string") {
options = { message: options }
}
return _change(doc, options, callback)
} else {
throw RangeError("Invalid args for change")
}
}
function progressDocument<T>(
doc: Doc<T>,
heads: Heads | null,
callback?: PatchCallback<T>
): Doc<T> {
if (heads == null) {
return doc
}
const state = _state(doc)
const nextState = { ...state, heads: undefined }
const nextDoc = state.handle.applyPatches(doc, nextState, callback)
state.heads = heads
return nextDoc
}
function _change<T>(
doc: Doc<T>,
options: ChangeOptions<T>,
callback: ChangeFn<T>
): Doc<T> {
if (typeof callback !== "function") {
throw new RangeError("invalid change function")
}
const state = _state(doc)
if (doc === undefined || state === undefined) {
throw new RangeError("must be the document root")
}
if (state.heads) {
throw new RangeError(
"Attempting to change an outdated document. Use Automerge.clone() if you wish to make a writable copy."
)
}
if (_is_proxy(doc)) {
throw new RangeError("Calls to Automerge.change cannot be nested")
}
const heads = state.handle.getHeads()
try {
state.heads = heads
const root: T = rootProxy(state.handle, state.textV2)
callback(root as Extend<T>)
if (state.handle.pendingOps() === 0) {
state.heads = undefined
return doc
} else {
state.handle.commit(options.message, options.time)
return progressDocument(
doc,
heads,
options.patchCallback || state.patchCallback
)
}
} catch (e) {
state.heads = undefined
state.handle.rollback()
throw e
}
}
/**
* Make a change to a document which does not modify the document
*
* @param doc - The doc to add the empty change to
* @param options - Either a message or a {@link ChangeOptions} for the new change
*
* Why would you want to do this? One reason might be that you have merged
* changes from some other peers and you want to generate a change which
* depends on those merged changes so that you can sign the new change with all
* of the merged changes as part of the new change.
*/
export function emptyChange<T>(
doc: Doc<T>,
options: string | ChangeOptions<T> | void
) {
if (options === undefined) {
options = {}
}
if (typeof options === "string") {
options = { message: options }
}
const state = _state(doc)
if (state.heads) {
throw new RangeError(
"Attempting to change an outdated document. Use Automerge.clone() if you wish to make a writable copy."
)
}
if (_is_proxy(doc)) {
throw new RangeError("Calls to Automerge.change cannot be nested")
}
const heads = state.handle.getHeads()
state.handle.emptyChange(options.message, options.time)
return progressDocument(doc, heads)
}
/**
* Load an automerge document from a compressed document produce by {@link save}
*
* @typeParam T - The type of the value which is contained in the document.
* Note that no validation is done to make sure this type is in
* fact the type of the contained value so be a bit careful
* @param data - The compressed document
* @param _opts - Either an actor ID or some {@link InitOptions}, if the actor
* ID is null a random actor ID will be created
*
* Note that `load` will throw an error if passed incomplete content (for
* example if you are receiving content over the network and don't know if you
* have the complete document yet). If you need to handle incomplete content use
* {@link init} followed by {@link loadIncremental}.
*/
export function load<T>(
data: Uint8Array,
_opts?: ActorId | InitOptions<T>
): Doc<T> {
const opts = importOpts(_opts)
const actor = opts.actor
const patchCallback = opts.patchCallback
const handle = ApiHandler.load(data, opts.enableTextV2 || false, actor)
handle.enablePatches(true)
handle.enableFreeze(!!opts.freeze)
handle.registerDatatype("counter", (n: number) => new Counter(n))
const textV2 = opts.enableTextV2 || false
if (textV2) {
handle.registerDatatype("str", (n: string) => new RawString(n))
} else {
handle.registerDatatype("text", (n: string) => new Text(n))
}
const doc = handle.materialize("/", undefined, {
handle,
heads: undefined,
patchCallback,
textV2,
}) as Doc<T>
return doc
}
/**
* Load changes produced by {@link saveIncremental}, or partial changes
*
* @typeParam T - The type of the value which is contained in the document.
* Note that no validation is done to make sure this type is in
* fact the type of the contained value so be a bit careful
* @param data - The compressedchanges
* @param opts - an {@link ApplyOptions}
*
* This function is useful when staying up to date with a connected peer.
* Perhaps the other end sent you a full compresed document which you loaded
* with {@link load} and they're sending you the result of
* {@link getLastLocalChange} every time they make a change.
*
* Note that this function will succesfully load the results of {@link save} as
* well as {@link getLastLocalChange} or any other incremental change.
*/
export function loadIncremental<T>(
doc: Doc<T>,
data: Uint8Array,
opts?: ApplyOptions<T>
): Doc<T> {
if (!opts) {
opts = {}
}
const state = _state(doc)
if (state.heads) {
throw new RangeError(
"Attempting to change an out of date document - set at: " + _trace(doc)
)
}
if (_is_proxy(doc)) {
throw new RangeError("Calls to Automerge.change cannot be nested")
}
const heads = state.handle.getHeads()
state.handle.loadIncremental(data)
return progressDocument(doc, heads, opts.patchCallback || state.patchCallback)
}
/**
* Export the contents of a document to a compressed format
*
* @param doc - The doc to save
*
* The returned bytes can be passed to {@link load} or {@link loadIncremental}
*/
export function save<T>(doc: Doc<T>): Uint8Array {
return _state(doc).handle.save()
}
/**
* Merge `local` into `remote`
* @typeParam T - The type of values contained in each document
* @param local - The document to merge changes into
* @param remote - The document to merge changes from
*
* @returns - The merged document
*
* Often when you are merging documents you will also need to clone them. Both
* arguments to `merge` are frozen after the call so you can no longer call
* mutating methods (such as {@link change}) on them. The symtom of this will be
* an error which says "Attempting to change an out of date document". To
* overcome this call {@link clone} on the argument before passing it to {@link
* merge}.
*/
export function merge<T>(local: Doc<T>, remote: Doc<T>): Doc<T> {
const localState = _state(local)
if (localState.heads) {
throw new RangeError(
"Attempting to change an out of date document - set at: " + _trace(local)
)
}
const heads = localState.handle.getHeads()
const remoteState = _state(remote)
const changes = localState.handle.getChangesAdded(remoteState.handle)
localState.handle.applyChanges(changes)
return progressDocument(local, heads, localState.patchCallback)
}
/**
* Get the actor ID associated with the document
*/
export function getActorId<T>(doc: Doc<T>): ActorId {
const state = _state(doc)
return state.handle.getActorId()
}
/**
* The type of conflicts for particular key or index
*
* Maps and sequences in automerge can contain conflicting values for a
* particular key or index. In this case {@link getConflicts} can be used to
* obtain a `Conflicts` representing the multiple values present for the property
*
* A `Conflicts` is a map from a unique (per property or index) key to one of
* the possible conflicting values for the given property.
*/
type Conflicts = { [key: string]: AutomergeValue }
/**
* Get the conflicts associated with a property
*
* The values of properties in a map in automerge can be conflicted if there
* are concurrent "put" operations to the same key. Automerge chooses one value
* arbitrarily (but deterministically, any two nodes who have the same set of
* changes will choose the same value) from the set of conflicting values to
* present as the value of the key.
*
* Sometimes you may want to examine these conflicts, in this case you can use
* {@link getConflicts} to get the conflicts for the key.
*
* @example
* ```
* import * as automerge from "@automerge/automerge"
*
* type Profile = {
* pets: Array<{name: string, type: string}>
* }
*
* let doc1 = automerge.init<Profile>("aaaa")
* doc1 = automerge.change(doc1, d => {
* d.pets = [{name: "Lassie", type: "dog"}]
* })
* let doc2 = automerge.init<Profile>("bbbb")
* doc2 = automerge.merge(doc2, automerge.clone(doc1))
*
* doc2 = automerge.change(doc2, d => {
* d.pets[0].name = "Beethoven"
* })
*
* doc1 = automerge.change(doc1, d => {
* d.pets[0].name = "Babe"
* })
*
* const doc3 = automerge.merge(doc1, doc2)
*
* // Note that here we pass `doc3.pets`, not `doc3`
* let conflicts = automerge.getConflicts(doc3.pets[0], "name")
*
* // The two conflicting values are the keys of the conflicts object
* assert.deepEqual(Object.values(conflicts), ["Babe", Beethoven"])
* ```
*/
export function getConflicts<T>(
doc: Doc<T>,
prop: Prop
): Conflicts | undefined {
const state = _state(doc, false)
if (state.textV2) {
throw new Error("use unstable.getConflicts for an unstable document")
}
const objectId = _obj(doc)
if (objectId != null) {
return stableConflictAt(state.handle, objectId, prop)
} else {
return undefined
}
}
/**
* Get the binary representation of the last change which was made to this doc
*
* This is most useful when staying in sync with other peers, every time you
* make a change locally via {@link change} you immediately call {@link
* getLastLocalChange} and send the result over the network to other peers.
*/
export function getLastLocalChange<T>(doc: Doc<T>): Change | undefined {
const state = _state(doc)
return state.handle.getLastLocalChange() || undefined
}
/**
* Return the object ID of an arbitrary javascript value
*
* This is useful to determine if something is actually an automerge document,
* if `doc` is not an automerge document this will return null.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getObjectId(doc: any, prop?: Prop): ObjID | null {
if (prop) {
const state = _state(doc, false)
const objectId = _obj(doc)
if (!state || !objectId) {
return null
}
return state.handle.get(objectId, prop) as ObjID
} else {
return _obj(doc)
}
}
/**
* Get the changes which are in `newState` but not in `oldState`. The returned
* changes can be loaded in `oldState` via {@link applyChanges}.
*
* Note that this will crash if there are changes in `oldState` which are not in `newState`.
*/
export function getChanges<T>(oldState: Doc<T>, newState: Doc<T>): Change[] {
const n = _state(newState)
return n.handle.getChanges(getHeads(oldState))
}
/**
* Get all the changes in a document
*
* This is different to {@link save} because the output is an array of changes
* which can be individually applied via {@link applyChanges}`
*
*/
export function getAllChanges<T>(doc: Doc<T>): Change[] {
const state = _state(doc)
return state.handle.getChanges([])
}
/**
* Apply changes received from another document
*
* `doc` will be updated to reflect the `changes`. If there are changes which
* we do not have dependencies for yet those will be stored in the document and
* applied when the depended on changes arrive.
*
* You can use the {@link ApplyOptions} to pass a patchcallback which will be
* informed of any changes which occur as a result of applying the changes
*
*/
export function applyChanges<T>(
doc: Doc<T>,
changes: Change[],
opts?: ApplyOptions<T>
): [Doc<T>] {
const state = _state(doc)
if (!opts) {
opts = {}
}
if (state.heads) {
throw new RangeError(
"Attempting to change an outdated document. Use Automerge.clone() if you wish to make a writable copy."
)
}
if (_is_proxy(doc)) {
throw new RangeError("Calls to Automerge.change cannot be nested")
}
const heads = state.handle.getHeads()
state.handle.applyChanges(changes)
state.heads = heads
return [
progressDocument(doc, heads, opts.patchCallback || state.patchCallback),
]
}
/** @hidden */
export function getHistory<T>(doc: Doc<T>): State<T>[] {
const textV2 = _state(doc).textV2
const history = getAllChanges(doc)
return history.map((change, index) => ({
get change() {
return decodeChange(change)
},
get snapshot() {
const [state] = applyChanges(
init({ enableTextV2: textV2 }),
history.slice(0, index + 1)
)
return <T>state
},
}))
}
/** @hidden */
// FIXME : no tests
// FIXME can we just use deep equals now?
export function equals(val1: unknown, val2: unknown): boolean {
if (!isObject(val1) || !isObject(val2)) return val1 === val2
const keys1 = Object.keys(val1).sort(),
keys2 = Object.keys(val2).sort()
if (keys1.length !== keys2.length) return false
for (let i = 0; i < keys1.length; i++) {
if (keys1[i] !== keys2[i]) return false
if (!equals(val1[keys1[i]], val2[keys2[i]])) return false
}
return true
}
/**
* encode a {@link SyncState} into binary to send over the network
*
* @group sync
* */
export function encodeSyncState(state: SyncState): Uint8Array {
const sync = ApiHandler.importSyncState(state)
const result = ApiHandler.encodeSyncState(sync)
sync.free()
return result
}
/**
* Decode some binary data into a {@link SyncState}
*
* @group sync
*/
export function decodeSyncState(state: Uint8Array): SyncState {
const sync = ApiHandler.decodeSyncState(state)
const result = ApiHandler.exportSyncState(sync)
sync.free()
return result as SyncState
}
/**
* Generate a sync message to send to the peer represented by `inState`
* @param doc - The doc to generate messages about
* @param inState - The {@link SyncState} representing the peer we are talking to
*
* @group sync
*
* @returns An array of `[newSyncState, syncMessage | null]` where
* `newSyncState` should replace `inState` and `syncMessage` should be sent to
* the peer if it is not null. If `syncMessage` is null then we are up to date.
*/
export function generateSyncMessage<T>(
doc: Doc<T>,
inState: SyncState
): [SyncState, SyncMessage | null] {
const state = _state(doc)
const syncState = ApiHandler.importSyncState(inState)
const message = state.handle.generateSyncMessage(syncState)
const outState = ApiHandler.exportSyncState(syncState) as SyncState
return [outState, message]
}
/**
* Update a document and our sync state on receiving a sync message
*
* @group sync
*
* @param doc - The doc the sync message is about
* @param inState - The {@link SyncState} for the peer we are communicating with
* @param message - The message which was received
* @param opts - Any {@link ApplyOption}s, used for passing a
* {@link PatchCallback} which will be informed of any changes
* in `doc` which occur because of the received sync message.
*
* @returns An array of `[newDoc, newSyncState, syncMessage | null]` where
* `newDoc` is the updated state of `doc`, `newSyncState` should replace
* `inState` and `syncMessage` should be sent to the peer if it is not null. If
* `syncMessage` is null then we are up to date.
*/
export function receiveSyncMessage<T>(
doc: Doc<T>,
inState: SyncState,
message: SyncMessage,
opts?: ApplyOptions<T>
): [Doc<T>, SyncState, null] {
const syncState = ApiHandler.importSyncState(inState)
if (!opts) {
opts = {}
}
const state = _state(doc)
if (state.heads) {
throw new RangeError(
"Attempting to change an outdated document. Use Automerge.clone() if you wish to make a writable copy."
)
}
if (_is_proxy(doc)) {
throw new RangeError("Calls to Automerge.change cannot be nested")
}
const heads = state.handle.getHeads()
state.handle.receiveSyncMessage(syncState, message)
const outSyncState = ApiHandler.exportSyncState(syncState) as SyncState
return [
progressDocument(doc, heads, opts.patchCallback || state.patchCallback),
outSyncState,
null,
]
}
/**
* Create a new, blank {@link SyncState}
*
* When communicating with a peer for the first time use this to generate a new
* {@link SyncState} for them
*
* @group sync
*/
export function initSyncState(): SyncState {
return ApiHandler.exportSyncState(ApiHandler.initSyncState()) as SyncState
}
/** @hidden */
export function encodeChange(change: ChangeToEncode): Change {
return ApiHandler.encodeChange(change)
}
/** @hidden */
export function decodeChange(data: Change): DecodedChange {
return ApiHandler.decodeChange(data)
}
/** @hidden */
export function encodeSyncMessage(message: DecodedSyncMessage): SyncMessage {
return ApiHandler.encodeSyncMessage(message)
}
/** @hidden */
export function decodeSyncMessage(message: SyncMessage): DecodedSyncMessage {
return ApiHandler.decodeSyncMessage(message)
}
/**
* Get any changes in `doc` which are not dependencies of `heads`
*/
export function getMissingDeps<T>(doc: Doc<T>, heads: Heads): Heads {
const state = _state(doc)
return state.handle.getMissingDeps(heads)
}
/**
* Get the hashes of the heads of this document
*/
export function getHeads<T>(doc: Doc<T>): Heads {
const state = _state(doc)
return state.heads || state.handle.getHeads()
}
/** @hidden */
export function dump<T>(doc: Doc<T>) {
const state = _state(doc)
state.handle.dump()
}
/** @hidden */
export function toJS<T>(doc: Doc<T>): T {
const state = _state(doc)
const enabled = state.handle.enableFreeze(false)
const result = state.handle.materialize()
state.handle.enableFreeze(enabled)
return result as T
}
export function isAutomerge(doc: unknown): boolean {
if (typeof doc == "object" && doc !== null) {
return getObjectId(doc) === "_root" && !!Reflect.get(doc, STATE)
} else {
return false
}
}
function isObject(obj: unknown): obj is Record<string, unknown> {
return typeof obj === "object" && obj !== null
}
export type {
API,
SyncState,
ActorId,
Conflicts,
Prop,
Change,
ObjID,
DecodedChange,
DecodedSyncMessage,
Heads,
MaterializeValue,
}

View file

@ -1,18 +1,13 @@
import type { Value } from "@automerge/automerge-wasm"
import { Value } from "@automerge/automerge-wasm"
import { TEXT, STATE } from "./constants"
import type { InternalState } from "./internal_state"
export class Text {
//eslint-disable-next-line @typescript-eslint/no-explicit-any
elems: Array<any>
elems: Value[]
str: string | undefined
//eslint-disable-next-line @typescript-eslint/no-explicit-any
spans: Array<any> | undefined;
//eslint-disable-next-line @typescript-eslint/no-explicit-any
[STATE]?: InternalState<any>
spans: Value[] | undefined
constructor(text?: string | string[] | Value[]) {
if (typeof text === "string") {
constructor (text?: string | string[] | Value[]) {
if (typeof text === 'string') {
this.elems = [...text]
} else if (Array.isArray(text)) {
this.elems = text
@ -24,12 +19,11 @@ export class Text {
Reflect.defineProperty(this, TEXT, { value: true })
}
get length(): number {
get length () : number {
return this.elems.length
}
//eslint-disable-next-line @typescript-eslint/no-explicit-any
get(index: number): any {
get (index: number) : Value | undefined {
return this.elems[index]
}
@ -37,18 +31,18 @@ export class Text {
* Iterates over the text elements character by character, including any
* inline objects.
*/
[Symbol.iterator]() {
[Symbol.iterator] () {
const elems = this.elems
let index = -1
return {
next() {
next () {
index += 1
if (index < elems.length) {
return { done: false, value: elems[index] }
return {done: false, value: elems[index]}
} else {
return { done: true }
return {done: true}
}
},
}
}
}
@ -56,15 +50,15 @@ export class Text {
* Returns the content of the Text object as a simple string, ignoring any
* non-character elements.
*/
toString(): string {
toString() : string {
if (!this.str) {
// Concatting to a string is faster than creating an array and then
// .join()ing for small (<100KB) arrays.
// https://jsperf.com/join-vs-loop-w-type-test
this.str = ""
this.str = ''
for (const elem of this.elems) {
if (typeof elem === "string") this.str += elem
else this.str += "\uFFFC"
if (typeof elem === 'string') this.str += elem
else this.str += '\uFFFC'
}
}
return this.str
@ -74,20 +68,20 @@ export class Text {
* Returns the content of the Text object as a sequence of strings,
* interleaved with non-character elements.
*
* For example, the value `['a', 'b', {x: 3}, 'c', 'd']` has spans:
* `=> ['ab', {x: 3}, 'cd']`
* For example, the value ['a', 'b', {x: 3}, 'c', 'd'] has spans:
* => ['ab', {x: 3}, 'cd']
*/
toSpans(): Array<Value | object> {
toSpans() : Value[] {
if (!this.spans) {
this.spans = []
let chars = ""
let chars = ''
for (const elem of this.elems) {
if (typeof elem === "string") {
if (typeof elem === 'string') {
chars += elem
} else {
if (chars.length > 0) {
this.spans.push(chars)
chars = ""
chars = ''
}
this.spans.push(elem)
}
@ -103,18 +97,16 @@ export class Text {
* Returns the content of the Text object as a simple string, so that the
* JSON serialization of an Automerge document represents text nicely.
*/
toJSON(): string {
toJSON() : string {
return this.toString()
}
/**
* Updates the list item at position `index` to a new value `value`.
*/
set(index: number, value: Value) {
set (index: number, value: Value) {
if (this[STATE]) {
throw new RangeError(
"object cannot be modified outside of a change block"
)
throw new RangeError("object cannot be modified outside of a change block")
}
this.elems[index] = value
}
@ -122,13 +114,11 @@ export class Text {
/**
* Inserts new list items `values` starting at position `index`.
*/
insertAt(index: number, ...values: Array<Value | object>) {
insertAt(index: number, ...values: Value[]) {
if (this[STATE]) {
throw new RangeError(
"object cannot be modified outside of a change block"
)
throw new RangeError("object cannot be modified outside of a change block")
}
this.elems.splice(index, 0, ...values)
this.elems.splice(index, 0, ... values)
}
/**
@ -137,14 +127,12 @@ export class Text {
*/
deleteAt(index: number, numDelete = 1) {
if (this[STATE]) {
throw new RangeError(
"object cannot be modified outside of a change block"
)
throw new RangeError("object cannot be modified outside of a change block")
}
this.elems.splice(index, numDelete)
}
map<T>(callback: (e: Value | object) => T) {
map<T>(callback: (e: Value) => T) {
this.elems.map(callback)
}
@ -152,31 +140,31 @@ export class Text {
this.elems.lastIndexOf(searchElement, fromIndex)
}
concat(other: Text): Text {
concat(other: Text) : Text {
return new Text(this.elems.concat(other.elems))
}
every(test: (v: Value) => boolean): boolean {
every(test: (Value) => boolean) : boolean {
return this.elems.every(test)
}
filter(test: (v: Value) => boolean): Text {
filter(test: (Value) => boolean) : Text {
return new Text(this.elems.filter(test))
}
find(test: (v: Value) => boolean): Value | undefined {
find(test: (Value) => boolean) : Value | undefined {
return this.elems.find(test)
}
findIndex(test: (v: Value) => boolean): number | undefined {
findIndex(test: (Value) => boolean) : number | undefined {
return this.elems.findIndex(test)
}
forEach(f: (v: Value) => undefined) {
forEach(f: (Value) => undefined) {
this.elems.forEach(f)
}
includes(elem: Value): boolean {
includes(elem: Value) : boolean {
return this.elems.includes(elem)
}
@ -184,37 +172,23 @@ export class Text {
return this.elems.indexOf(elem)
}
join(sep?: string): string {
join(sep?: string) : string{
return this.elems.join(sep)
}
reduce(
f: (
previousValue: Value,
currentValue: Value,
currentIndex: number,
array: Value[]
) => Value
) {
reduce(f: (previousValue: Value, currentValue: Value, currentIndex: number, array: Value[]) => Value) {
this.elems.reduce(f)
}
reduceRight(
f: (
previousValue: Value,
currentValue: Value,
currentIndex: number,
array: Value[]
) => Value
) {
reduceRight(f: (previousValue: Value, currentValue: Value, currentIndex: number, array: Value[]) => Value) {
this.elems.reduceRight(f)
}
slice(start?: number, end?: number) {
new Text(this.elems.slice(start, end))
new Text(this.elems.slice(start,end))
}
some(test: (arg: Value) => boolean): boolean {
some(test: (Value) => boolean) : boolean {
return this.elems.some(test)
}
@ -222,3 +196,4 @@ export class Text {
this.toString()
}
}

View file

@ -1,46 +1,13 @@
export { Text } from "./text"
import { Text } from "./text"
export { Counter } from "./counter"
export { Int, Uint, Float64 } from "./numbers"
export { Text } from "./text"
export { Counter } from "./counter"
export { Int, Uint, Float64 } from "./numbers"
import { Counter } from "./counter"
import type { Patch } from "@automerge/automerge-wasm"
export type { Patch } from "@automerge/automerge-wasm"
export type AutomergeValue =
| ScalarValue
| { [key: string]: AutomergeValue }
| Array<AutomergeValue>
| Text
export type MapValue = { [key: string]: AutomergeValue }
export type ListValue = Array<AutomergeValue>
export type ScalarValue =
| string
| number
| null
| boolean
| Date
| Counter
| Uint8Array
/**
* An automerge document.
* @typeParam T - The type of the value contained in this document
*
* Note that this provides read only access to the fields of the value. To
* modify the value use {@link change}
*/
export type Doc<T> = { readonly [P in keyof T]: T[P] }
/**
* Callback which is called by various methods in this library to notify the
* user of what changes have been made.
* @param patch - A description of the changes made
* @param before - The document before the change was made
* @param after - The document after the change was made
*/
export type PatchCallback<T> = (
patches: Array<Patch>,
before: Doc<T>,
after: Doc<T>
) => void
export type AutomergeValue = ScalarValue | { [key: string]: AutomergeValue } | Array<AutomergeValue> | Text
export type MapValue = { [key: string]: AutomergeValue }
export type ListValue = Array<AutomergeValue>
export type TextValue = Array<AutomergeValue>
export type ScalarValue = string | number | null | boolean | Date | Counter | Uint8Array

View file

@ -1,294 +0,0 @@
/**
* # The unstable API
*
* This module contains new features we are working on which are either not yet
* ready for a stable release and/or which will result in backwards incompatible
* API changes. The API of this module may change in arbitrary ways between
* point releases - we will always document what these changes are in the
* [CHANGELOG](#changelog) below, but only depend on this module if you are prepared to deal
* with frequent changes.
*
* ## Differences from stable
*
* In the stable API text objects are represented using the {@link Text} class.
* This means you must decide up front whether your string data might need
* concurrent merges in the future and if you change your mind you have to
* figure out how to migrate your data. In the unstable API the `Text` class is
* gone and all `string`s are represented using the text CRDT, allowing for
* concurrent changes. Modifying a string is done using the {@link splice}
* function. You can still access the old behaviour of strings which do not
* support merging behaviour via the {@link RawString} class.
*
* This leads to the following differences from `stable`:
*
* * There is no `unstable.Text` class, all strings are text objects
* * Reading strings in an `unstable` document is the same as reading any other
* javascript string
* * To modify strings in an `unstable` document use {@link splice}
* * The {@link AutomergeValue} type does not include the {@link Text}
* class but the {@link RawString} class is included in the {@link ScalarValue}
* type
*
* ## CHANGELOG
* * Introduce this module to expose the new API which has no `Text` class
*
*
* @module
*/
export {
Counter,
type Doc,
Int,
Uint,
Float64,
type Patch,
type PatchCallback,
type AutomergeValue,
type ScalarValue,
} from "./unstable_types"
import type { PatchCallback } from "./stable"
import { type UnstableConflicts as Conflicts } from "./conflicts"
import { unstableConflictAt } from "./conflicts"
export type {
PutPatch,
DelPatch,
SpliceTextPatch,
InsertPatch,
IncPatch,
SyncMessage,
} from "@automerge/automerge-wasm"
export type { ChangeOptions, ApplyOptions, ChangeFn } from "./stable"
export {
view,
free,
getHeads,
change,
emptyChange,
loadIncremental,
save,
merge,
getActorId,
getLastLocalChange,
getChanges,
getAllChanges,
applyChanges,
getHistory,
equals,
encodeSyncState,
decodeSyncState,
generateSyncMessage,
receiveSyncMessage,
initSyncState,
encodeChange,
decodeChange,
encodeSyncMessage,
decodeSyncMessage,
getMissingDeps,
dump,
toJS,
isAutomerge,
getObjectId,
} from "./stable"
export type InitOptions<T> = {
/** The actor ID to use for this document, a random one will be generated if `null` is passed */
actor?: ActorId
freeze?: boolean
/** A callback which will be called with the initial patch once the document has finished loading */
patchCallback?: PatchCallback<T>
}
import { ActorId, Doc } from "./stable"
import * as stable from "./stable"
export { RawString } from "./raw_string"
/** @hidden */
export const getBackend = stable.getBackend
import { _is_proxy, _state, _obj } from "./internal_state"
/**
* Create a new automerge document
*
* @typeParam T - The type of value contained in the document. This will be the
* type that is passed to the change closure in {@link change}
* @param _opts - Either an actorId or an {@link InitOptions} (which may
* contain an actorId). If this is null the document will be initialised with a
* random actor ID
*/
export function init<T>(_opts?: ActorId | InitOptions<T>): Doc<T> {
const opts = importOpts(_opts)
opts.enableTextV2 = true
return stable.init(opts)
}
/**
* Make a full writable copy of an automerge document
*
* @remarks
* Unlike {@link view} this function makes a full copy of the memory backing
* the document and can thus be passed to {@link change}. It also generates a
* new actor ID so that changes made in the new document do not create duplicate
* sequence numbers with respect to the old document. If you need control over
* the actor ID which is generated you can pass the actor ID as the second
* argument
*
* @typeParam T - The type of the value contained in the document
* @param doc - The document to clone
* @param _opts - Either an actor ID to use for the new doc or an {@link InitOptions}
*/
export function clone<T>(
doc: Doc<T>,
_opts?: ActorId | InitOptions<T>
): Doc<T> {
const opts = importOpts(_opts)
opts.enableTextV2 = true
return stable.clone(doc, opts)
}
/**
* Create an automerge document from a POJO
*
* @param initialState - The initial state which will be copied into the document
* @typeParam T - The type of the value passed to `from` _and_ the type the resulting document will contain
* @typeParam actor - The actor ID of the resulting document, if this is null a random actor ID will be used
*
* @example
* ```
* const doc = automerge.from({
* tasks: [
* {description: "feed dogs", done: false}
* ]
* })
* ```
*/
export function from<T extends Record<string, unknown>>(
initialState: T | Doc<T>,
_opts?: ActorId | InitOptions<T>
): Doc<T> {
const opts = importOpts(_opts)
opts.enableTextV2 = true
return stable.from(initialState, opts)
}
/**
* Load an automerge document from a compressed document produce by {@link save}
*
* @typeParam T - The type of the value which is contained in the document.
* Note that no validation is done to make sure this type is in
* fact the type of the contained value so be a bit careful
* @param data - The compressed document
* @param _opts - Either an actor ID or some {@link InitOptions}, if the actor
* ID is null a random actor ID will be created
*
* Note that `load` will throw an error if passed incomplete content (for
* example if you are receiving content over the network and don't know if you
* have the complete document yet). If you need to handle incomplete content use
* {@link init} followed by {@link loadIncremental}.
*/
export function load<T>(
data: Uint8Array,
_opts?: ActorId | InitOptions<T>
): Doc<T> {
const opts = importOpts(_opts)
opts.enableTextV2 = true
return stable.load(data, opts)
}
function importOpts<T>(
_actor?: ActorId | InitOptions<T>
): stable.InitOptions<T> {
if (typeof _actor === "object") {
return _actor
} else {
return { actor: _actor }
}
}
export function splice<T>(
doc: Doc<T>,
prop: stable.Prop,
index: number,
del: number,
newText?: string
) {
if (!_is_proxy(doc)) {
throw new RangeError("object cannot be modified outside of a change block")
}
const state = _state(doc, false)
const objectId = _obj(doc)
if (!objectId) {
throw new RangeError("invalid object for splice")
}
const value = `${objectId}/${prop}`
try {
return state.handle.splice(value, index, del, newText)
} catch (e) {
throw new RangeError(`Cannot splice: ${e}`)
}
}
/**
* Get the conflicts associated with a property
*
* The values of properties in a map in automerge can be conflicted if there
* are concurrent "put" operations to the same key. Automerge chooses one value
* arbitrarily (but deterministically, any two nodes who have the same set of
* changes will choose the same value) from the set of conflicting values to
* present as the value of the key.
*
* Sometimes you may want to examine these conflicts, in this case you can use
* {@link getConflicts} to get the conflicts for the key.
*
* @example
* ```
* import * as automerge from "@automerge/automerge"
*
* type Profile = {
* pets: Array<{name: string, type: string}>
* }
*
* let doc1 = automerge.init<Profile>("aaaa")
* doc1 = automerge.change(doc1, d => {
* d.pets = [{name: "Lassie", type: "dog"}]
* })
* let doc2 = automerge.init<Profile>("bbbb")
* doc2 = automerge.merge(doc2, automerge.clone(doc1))
*
* doc2 = automerge.change(doc2, d => {
* d.pets[0].name = "Beethoven"
* })
*
* doc1 = automerge.change(doc1, d => {
* d.pets[0].name = "Babe"
* })
*
* const doc3 = automerge.merge(doc1, doc2)
*
* // Note that here we pass `doc3.pets`, not `doc3`
* let conflicts = automerge.getConflicts(doc3.pets[0], "name")
*
* // The two conflicting values are the keys of the conflicts object
* assert.deepEqual(Object.values(conflicts), ["Babe", Beethoven"])
* ```
*/
export function getConflicts<T>(
doc: Doc<T>,
prop: stable.Prop
): Conflicts | undefined {
const state = _state(doc, false)
if (!state.textV2) {
throw new Error("use getConflicts for a stable document")
}
const objectId = _obj(doc)
if (objectId != null) {
return unstableConflictAt(state.handle, objectId, prop)
} else {
return undefined
}
}

View file

@ -1,30 +0,0 @@
import { Counter } from "./types"
export {
Counter,
type Doc,
Int,
Uint,
Float64,
type Patch,
type PatchCallback,
} from "./types"
import { RawString } from "./raw_string"
export { RawString } from "./raw_string"
export type AutomergeValue =
| ScalarValue
| { [key: string]: AutomergeValue }
| Array<AutomergeValue>
export type MapValue = { [key: string]: AutomergeValue }
export type ListValue = Array<AutomergeValue>
export type ScalarValue =
| string
| number
| null
| boolean
| Date
| Counter
| Uint8Array
| RawString

View file

@ -1,26 +0,0 @@
import * as v4 from "https://deno.land/x/uuid@v0.1.2/mod.ts"
// this file is a deno only port of the uuid module
function defaultFactory() {
return v4.uuid().replace(/-/g, "")
}
let factory = defaultFactory
interface UUIDFactory extends Function {
setFactory(f: typeof factory): void
reset(): void
}
export const uuid: UUIDFactory = () => {
return factory()
}
uuid.setFactory = newFactory => {
factory = newFactory
}
uuid.reset = () => {
factory = defaultFactory
}

View file

@ -1,24 +1,21 @@
import { v4 } from "uuid"
import { v4 } from 'uuid'
function defaultFactory() {
return v4().replace(/-/g, "")
return v4().replace(/-/g, '')
}
let factory = defaultFactory
interface UUIDFactory extends Function {
setFactory(f: typeof factory): void
reset(): void
setFactory(f: typeof factory): void;
reset(): void;
}
export const uuid: UUIDFactory = () => {
export const uuid : UUIDFactory = () => {
return factory()
}
uuid.setFactory = newFactory => {
factory = newFactory
}
uuid.setFactory = newFactory => { factory = newFactory }
uuid.reset = () => { factory = defaultFactory }
uuid.reset = () => {
factory = defaultFactory
}

View file

@ -1,488 +1,267 @@
import * as assert from "assert"
import { unstable as Automerge } from "../src"
import * as WASM from "@automerge/automerge-wasm"
import * as assert from 'assert'
import {Counter} from 'automerge'
import * as Automerge from '../src'
describe("Automerge", () => {
describe("basics", () => {
it("should init clone and free", () => {
let doc1 = Automerge.init()
let doc2 = Automerge.clone(doc1)
// this is only needed if weakrefs are not supported
Automerge.free(doc1)
Automerge.free(doc2)
})
it("should be able to make a view with specifc heads", () => {
let doc1 = Automerge.init<any>()
let doc2 = Automerge.change(doc1, d => (d.value = 1))
let heads2 = Automerge.getHeads(doc2)
let doc3 = Automerge.change(doc2, d => (d.value = 2))
let doc2_v2 = Automerge.view(doc3, heads2)
assert.deepEqual(doc2, doc2_v2)
let doc2_v2_clone = Automerge.clone(doc2, "aabbcc")
assert.deepEqual(doc2, doc2_v2_clone)
assert.equal(Automerge.getActorId(doc2_v2_clone), "aabbcc")
})
it("should allow you to change a clone of a view", () => {
let doc1 = Automerge.init<any>()
doc1 = Automerge.change(doc1, d => (d.key = "value"))
let heads = Automerge.getHeads(doc1)
doc1 = Automerge.change(doc1, d => (d.key = "value2"))
let fork = Automerge.clone(Automerge.view(doc1, heads))
assert.deepEqual(fork, { key: "value" })
fork = Automerge.change(fork, d => (d.key = "value3"))
assert.deepEqual(fork, { key: "value3" })
})
it("handle basic set and read on root object", () => {
let doc1 = Automerge.init<any>()
let doc2 = Automerge.change(doc1, d => {
d.hello = "world"
d.big = "little"
d.zip = "zop"
d.app = "dap"
assert.deepEqual(d, {
hello: "world",
big: "little",
zip: "zop",
app: "dap",
describe('Automerge', () => {
describe('basics', () => {
it('should init clone and free', () => {
let doc1 = Automerge.init()
let doc2 = Automerge.clone(doc1);
})
})
assert.deepEqual(doc2, {
hello: "world",
big: "little",
zip: "zop",
app: "dap",
})
})
it("should be able to insert and delete a large number of properties", () => {
let doc = Automerge.init<any>()
doc = Automerge.change(doc, doc => {
doc["k1"] = true
})
for (let idx = 1; idx <= 200; idx++) {
doc = Automerge.change(doc, doc => {
delete doc["k" + idx]
doc["k" + (idx + 1)] = true
assert(Object.keys(doc).length == 1)
it('handle basic set and read on root object', () => {
let doc1 = Automerge.init()
let doc2 = Automerge.change(doc1, (d) => {
d.hello = "world"
d.big = "little"
d.zip = "zop"
d.app = "dap"
assert.deepEqual(d, { hello: "world", big: "little", zip: "zop", app: "dap" })
})
assert.deepEqual(doc2, { hello: "world", big: "little", zip: "zop", app: "dap" })
})
}
})
it("can detect an automerge doc with isAutomerge()", () => {
const doc1 = Automerge.from({ sub: { object: true } })
assert(Automerge.isAutomerge(doc1))
assert(!Automerge.isAutomerge(doc1.sub))
assert(!Automerge.isAutomerge("String"))
assert(!Automerge.isAutomerge({ sub: { object: true } }))
assert(!Automerge.isAutomerge(undefined))
const jsObj = Automerge.toJS(doc1)
assert(!Automerge.isAutomerge(jsObj))
assert.deepEqual(jsObj, doc1)
})
it('handle basic sets over many changes', () => {
let doc1 = Automerge.init()
let timestamp = new Date();
let counter = new Automerge.Counter(100);
let bytes = new Uint8Array([10,11,12]);
let doc2 = Automerge.change(doc1, (d) => {
d.hello = "world"
})
let doc3 = Automerge.change(doc2, (d) => {
d.counter1 = counter
})
let doc4 = Automerge.change(doc3, (d) => {
d.timestamp1 = timestamp
})
let doc5 = Automerge.change(doc4, (d) => {
d.app = null
})
let doc6 = Automerge.change(doc5, (d) => {
d.bytes1 = bytes
})
let doc7 = Automerge.change(doc6, (d) => {
d.uint = new Automerge.Uint(1)
d.int = new Automerge.Int(-1)
d.float64 = new Automerge.Float64(5.5)
d.number1 = 100
d.number2 = -45.67
d.true = true
d.false = false
})
it("it should recursively freeze the document if requested", () => {
let doc1 = Automerge.init<any>({ freeze: true })
let doc2 = Automerge.init<any>()
assert.deepEqual(doc7, { hello: "world", true: true, false: false, int: -1, uint: 1, float64: 5.5, number1: 100, number2: -45.67, counter1: counter, timestamp1: timestamp, bytes1: bytes, app: null })
assert(Object.isFrozen(doc1))
assert(!Object.isFrozen(doc2))
// will also freeze sub objects
doc1 = Automerge.change(
doc1,
doc => (doc.book = { title: "how to win friends" })
)
doc2 = Automerge.merge(doc2, doc1)
assert(Object.isFrozen(doc1))
assert(Object.isFrozen(doc1.book))
assert(!Object.isFrozen(doc2))
assert(!Object.isFrozen(doc2.book))
// works on from
let doc3 = Automerge.from({ sub: { obj: "inner" } }, { freeze: true })
assert(Object.isFrozen(doc3))
assert(Object.isFrozen(doc3.sub))
// works on load
let doc4 = Automerge.load<any>(Automerge.save(doc3), { freeze: true })
assert(Object.isFrozen(doc4))
assert(Object.isFrozen(doc4.sub))
// follows clone
let doc5 = Automerge.clone(doc4)
assert(Object.isFrozen(doc5))
assert(Object.isFrozen(doc5.sub))
// toJS does not freeze
let exported = Automerge.toJS(doc5)
assert(!Object.isFrozen(exported))
})
it("handle basic sets over many changes", () => {
let doc1 = Automerge.init<any>()
let timestamp = new Date()
let counter = new Automerge.Counter(100)
let bytes = new Uint8Array([10, 11, 12])
let doc2 = Automerge.change(doc1, d => {
d.hello = "world"
})
let doc3 = Automerge.change(doc2, d => {
d.counter1 = counter
})
let doc4 = Automerge.change(doc3, d => {
d.timestamp1 = timestamp
})
let doc5 = Automerge.change(doc4, d => {
d.app = null
})
let doc6 = Automerge.change(doc5, d => {
d.bytes1 = bytes
})
let doc7 = Automerge.change(doc6, d => {
d.uint = new Automerge.Uint(1)
d.int = new Automerge.Int(-1)
d.float64 = new Automerge.Float64(5.5)
d.number1 = 100
d.number2 = -45.67
d.true = true
d.false = false
})
assert.deepEqual(doc7, {
hello: "world",
true: true,
false: false,
int: -1,
uint: 1,
float64: 5.5,
number1: 100,
number2: -45.67,
counter1: counter,
timestamp1: timestamp,
bytes1: bytes,
app: null,
})
let changes = Automerge.getAllChanges(doc7)
let t1 = Automerge.init()
let [t2] = Automerge.applyChanges(t1, changes)
assert.deepEqual(doc7, t2)
})
it("handle overwrites to values", () => {
let doc1 = Automerge.init<any>()
let doc2 = Automerge.change(doc1, d => {
d.hello = "world1"
})
let doc3 = Automerge.change(doc2, d => {
d.hello = "world2"
})
let doc4 = Automerge.change(doc3, d => {
d.hello = "world3"
})
let doc5 = Automerge.change(doc4, d => {
d.hello = "world4"
})
assert.deepEqual(doc5, { hello: "world4" })
})
it("handle set with object value", () => {
let doc1 = Automerge.init<any>()
let doc2 = Automerge.change(doc1, d => {
d.subobj = { hello: "world", subsubobj: { zip: "zop" } }
})
assert.deepEqual(doc2, {
subobj: { hello: "world", subsubobj: { zip: "zop" } },
})
})
it("handle simple list creation", () => {
let doc1 = Automerge.init<any>()
let doc2 = Automerge.change(doc1, d => (d.list = []))
assert.deepEqual(doc2, { list: [] })
})
it("handle simple lists", () => {
let doc1 = Automerge.init<any>()
let doc2 = Automerge.change(doc1, d => {
d.list = [1, 2, 3]
})
assert.deepEqual(doc2.list.length, 3)
assert.deepEqual(doc2.list[0], 1)
assert.deepEqual(doc2.list[1], 2)
assert.deepEqual(doc2.list[2], 3)
assert.deepEqual(doc2, { list: [1, 2, 3] })
// assert.deepStrictEqual(Automerge.toJS(doc2), { list: [1,2,3] })
let doc3 = Automerge.change(doc2, d => {
d.list[1] = "a"
})
assert.deepEqual(doc3.list.length, 3)
assert.deepEqual(doc3.list[0], 1)
assert.deepEqual(doc3.list[1], "a")
assert.deepEqual(doc3.list[2], 3)
assert.deepEqual(doc3, { list: [1, "a", 3] })
})
it("handle simple lists", () => {
let doc1 = Automerge.init<any>()
let doc2 = Automerge.change(doc1, d => {
d.list = [1, 2, 3]
})
let changes = Automerge.getChanges(doc1, doc2)
let docB1 = Automerge.init()
let [docB2] = Automerge.applyChanges(docB1, changes)
assert.deepEqual(docB2, doc2)
})
it("handle text", () => {
let doc1 = Automerge.init<any>()
let doc2 = Automerge.change(doc1, d => {
d.list = "hello"
Automerge.splice(d, "list", 2, 0, "Z")
})
let changes = Automerge.getChanges(doc1, doc2)
let docB1 = Automerge.init()
let [docB2] = Automerge.applyChanges(docB1, changes)
assert.deepEqual(docB2, doc2)
})
it("handle non-text strings", () => {
let doc1 = WASM.create(true)
doc1.put("_root", "text", "hello world")
let doc2 = Automerge.load<any>(doc1.save())
assert.throws(() => {
Automerge.change(doc2, d => {
Automerge.splice(d, "text", 1, 0, "Z")
let changes = Automerge.getAllChanges(doc7)
let t1 = Automerge.init()
;let [t2] = Automerge.applyChanges(t1, changes)
assert.deepEqual(doc7,t2)
})
it('handle overwrites to values', () => {
let doc1 = Automerge.init()
let doc2 = Automerge.change(doc1, (d) => {
d.hello = "world1"
})
let doc3 = Automerge.change(doc2, (d) => {
d.hello = "world2"
})
let doc4 = Automerge.change(doc3, (d) => {
d.hello = "world3"
})
let doc5 = Automerge.change(doc4, (d) => {
d.hello = "world4"
})
assert.deepEqual(doc5, { hello: "world4" } )
})
it('handle set with object value', () => {
let doc1 = Automerge.init()
let doc2 = Automerge.change(doc1, (d) => {
d.subobj = { hello: "world", subsubobj: { zip: "zop" } }
})
assert.deepEqual(doc2, { subobj: { hello: "world", subsubobj: { zip: "zop" } } })
})
it('handle simple list creation', () => {
let doc1 = Automerge.init()
let doc2 = Automerge.change(doc1, (d) => d.list = [])
assert.deepEqual(doc2, { list: []})
})
it('handle simple lists', () => {
let doc1 = Automerge.init()
let doc2 = Automerge.change(doc1, (d) => {
d.list = [ 1, 2, 3 ]
})
assert.deepEqual(doc2.list.length, 3)
assert.deepEqual(doc2.list[0], 1)
assert.deepEqual(doc2.list[1], 2)
assert.deepEqual(doc2.list[2], 3)
assert.deepEqual(doc2, { list: [1,2,3] })
// assert.deepStrictEqual(Automerge.toJS(doc2), { list: [1,2,3] })
let doc3 = Automerge.change(doc2, (d) => {
d.list[1] = "a"
})
assert.deepEqual(doc3.list.length, 3)
assert.deepEqual(doc3.list[0], 1)
assert.deepEqual(doc3.list[1], "a")
assert.deepEqual(doc3.list[2], 3)
assert.deepEqual(doc3, { list: [1,"a",3] })
})
it('handle simple lists', () => {
let doc1 = Automerge.init()
let doc2 = Automerge.change(doc1, (d) => {
d.list = [ 1, 2, 3 ]
})
let changes = Automerge.getChanges(doc1, doc2)
let docB1 = Automerge.init()
;let [docB2] = Automerge.applyChanges(docB1, changes)
assert.deepEqual(docB2, doc2);
})
it('handle text', () => {
let doc1 = Automerge.init()
let tmp = new Automerge.Text("hello")
let doc2 = Automerge.change(doc1, (d) => {
d.list = new Automerge.Text("hello")
d.list.insertAt(2,"Z")
})
let changes = Automerge.getChanges(doc1, doc2)
let docB1 = Automerge.init()
;let [docB2] = Automerge.applyChanges(docB1, changes)
assert.deepEqual(docB2, doc2);
})
it('have many list methods', () => {
let doc1 = Automerge.from({ list: [1,2,3] })
assert.deepEqual(doc1, { list: [1,2,3] });
let doc2 = Automerge.change(doc1, (d) => {
d.list.splice(1,1,9,10)
})
assert.deepEqual(doc2, { list: [1,9,10,3] });
let doc3 = Automerge.change(doc2, (d) => {
d.list.push(11,12)
})
assert.deepEqual(doc3, { list: [1,9,10,3,11,12] });
let doc4 = Automerge.change(doc3, (d) => {
d.list.unshift(2,2)
})
assert.deepEqual(doc4, { list: [2,2,1,9,10,3,11,12] });
let doc5 = Automerge.change(doc4, (d) => {
d.list.shift()
})
assert.deepEqual(doc5, { list: [2,1,9,10,3,11,12] });
let doc6 = Automerge.change(doc5, (d) => {
d.list.insertAt(3,100,101)
})
assert.deepEqual(doc6, { list: [2,1,9,100,101,10,3,11,12] });
})
it('allows access to the backend', () => {
let doc = Automerge.init()
assert.deepEqual(Object.keys(Automerge.getBackend(doc)), ["ptr"])
})
it('lists and text have indexof', () => {
let doc = Automerge.from({ list: [0,1,2,3,4,5,6], text: new Automerge.Text("hello world") })
console.log(doc.list.indexOf(5))
console.log(doc.text.indexOf("world"))
})
}, /Cannot splice/)
})
it("have many list methods", () => {
let doc1 = Automerge.from({ list: [1, 2, 3] })
assert.deepEqual(doc1, { list: [1, 2, 3] })
let doc2 = Automerge.change(doc1, d => {
d.list.splice(1, 1, 9, 10)
})
assert.deepEqual(doc2, { list: [1, 9, 10, 3] })
let doc3 = Automerge.change(doc2, d => {
d.list.push(11, 12)
})
assert.deepEqual(doc3, { list: [1, 9, 10, 3, 11, 12] })
let doc4 = Automerge.change(doc3, d => {
d.list.unshift(2, 2)
})
assert.deepEqual(doc4, { list: [2, 2, 1, 9, 10, 3, 11, 12] })
let doc5 = Automerge.change(doc4, d => {
d.list.shift()
})
assert.deepEqual(doc5, { list: [2, 1, 9, 10, 3, 11, 12] })
let doc6 = Automerge.change(doc5, d => {
d.list.insertAt(3, 100, 101)
})
assert.deepEqual(doc6, { list: [2, 1, 9, 100, 101, 10, 3, 11, 12] })
describe('proxy lists', () => {
it('behave like arrays', () => {
let doc = Automerge.from({
chars: ["a","b","c"],
numbers: [20,3,100],
repeats: [20,20,3,3,3,3,100,100]
})
let r1 = []
doc = Automerge.change(doc, (d) => {
assert.deepEqual(d.chars.concat([1,2]), ["a","b","c",1,2])
assert.deepEqual(d.chars.map((n) => n + "!"), ["a!", "b!", "c!"])
assert.deepEqual(d.numbers.map((n) => n + 10), [30, 13, 110])
assert.deepEqual(d.numbers.toString(), "20,3,100")
assert.deepEqual(d.numbers.toLocaleString(), "20,3,100")
assert.deepEqual(d.numbers.forEach((n) => r1.push(n)), undefined)
assert.deepEqual(d.numbers.every((n) => n > 1), true)
assert.deepEqual(d.numbers.every((n) => n > 10), false)
assert.deepEqual(d.numbers.filter((n) => n > 10), [20,100])
assert.deepEqual(d.repeats.find((n) => n < 10), 3)
assert.deepEqual(d.repeats.toArray().find((n) => n < 10), 3)
assert.deepEqual(d.repeats.find((n) => n < 0), undefined)
assert.deepEqual(d.repeats.findIndex((n) => n < 10), 2)
assert.deepEqual(d.repeats.findIndex((n) => n < 0), -1)
assert.deepEqual(d.repeats.toArray().findIndex((n) => n < 10), 2)
assert.deepEqual(d.repeats.toArray().findIndex((n) => n < 0), -1)
assert.deepEqual(d.numbers.includes(3), true)
assert.deepEqual(d.numbers.includes(-3), false)
assert.deepEqual(d.numbers.join("|"), "20|3|100")
assert.deepEqual(d.numbers.join(), "20,3,100")
assert.deepEqual(d.numbers.some((f) => f === 3), true)
assert.deepEqual(d.numbers.some((f) => f < 0), false)
assert.deepEqual(d.numbers.reduce((sum,n) => sum + n, 100), 223)
assert.deepEqual(d.repeats.reduce((sum,n) => sum + n, 100), 352)
assert.deepEqual(d.chars.reduce((sum,n) => sum + n, "="), "=abc")
assert.deepEqual(d.chars.reduceRight((sum,n) => sum + n, "="), "=cba")
assert.deepEqual(d.numbers.reduceRight((sum,n) => sum + n, 100), 223)
assert.deepEqual(d.repeats.lastIndexOf(3), 5)
assert.deepEqual(d.repeats.lastIndexOf(3,3), 3)
})
doc = Automerge.change(doc, (d) => {
assert.deepEqual(d.numbers.fill(-1,1,2), [20,-1,100])
assert.deepEqual(d.chars.fill("z",1,100), ["a","z","z"])
})
assert.deepEqual(r1, [20,3,100])
assert.deepEqual(doc.numbers, [20,-1,100])
assert.deepEqual(doc.chars, ["a","z","z"])
})
})
it('should obtain the same conflicts, regardless of merge order', () => {
let s1 = Automerge.init()
let s2 = Automerge.init()
s1 = Automerge.change(s1, doc => { doc.x = 1; doc.y = 2 })
s2 = Automerge.change(s2, doc => { doc.x = 3; doc.y = 4 })
const m1 = Automerge.merge(Automerge.clone(s1), Automerge.clone(s2))
const m2 = Automerge.merge(Automerge.clone(s2), Automerge.clone(s1))
assert.deepStrictEqual(Automerge.getConflicts(m1, 'x'), Automerge.getConflicts(m2, 'x'))
})
it("allows access to the backend", () => {
let doc = Automerge.init()
assert.deepEqual(Object.keys(Automerge.getBackend(doc)), ["ptr"])
})
describe("getObjectId", () => {
let s1 = Automerge.from({
"string": "string",
"number": 1,
"null": null,
"date": new Date(),
"counter": new Automerge.Counter(),
"bytes": new Uint8Array(10),
"text": new Automerge.Text(),
"list": [],
"map": {}
})
it("lists and text have indexof", () => {
let doc = Automerge.from({
list: [0, 1, 2, 3, 4, 5, 6],
text: "hello world",
})
assert.deepEqual(doc.list.indexOf(5), 5)
assert.deepEqual(doc.text.indexOf("world"), 6)
})
})
it("should return null for scalar values", () => {
assert.equal(Automerge.getObjectId(s1.string), null)
assert.equal(Automerge.getObjectId(s1.number), null)
assert.equal(Automerge.getObjectId(s1.null), null)
assert.equal(Automerge.getObjectId(s1.date), null)
assert.equal(Automerge.getObjectId(s1.counter), null)
assert.equal(Automerge.getObjectId(s1.bytes), null)
})
describe("emptyChange", () => {
it("should generate a hash", () => {
let doc = Automerge.init()
doc = Automerge.change<any>(doc, d => {
d.key = "value"
})
Automerge.save(doc)
let headsBefore = Automerge.getHeads(doc)
headsBefore.sort()
doc = Automerge.emptyChange(doc, "empty change")
let headsAfter = Automerge.getHeads(doc)
headsAfter.sort()
assert.notDeepEqual(headsBefore, headsAfter)
})
})
it("should return _root for the root object", () => {
assert.equal(Automerge.getObjectId(s1), "_root")
})
describe("proxy lists", () => {
it("behave like arrays", () => {
let doc = Automerge.from({
chars: ["a", "b", "c"],
numbers: [20, 3, 100],
repeats: [20, 20, 3, 3, 3, 3, 100, 100],
})
let r1: Array<number> = []
doc = Automerge.change(doc, d => {
assert.deepEqual((d.chars as any[]).concat([1, 2]), [
"a",
"b",
"c",
1,
2,
])
assert.deepEqual(
d.chars.map(n => n + "!"),
["a!", "b!", "c!"]
)
assert.deepEqual(
d.numbers.map(n => n + 10),
[30, 13, 110]
)
assert.deepEqual(d.numbers.toString(), "20,3,100")
assert.deepEqual(d.numbers.toLocaleString(), "20,3,100")
assert.deepEqual(
d.numbers.forEach((n: number) => r1.push(n)),
undefined
)
assert.deepEqual(
d.numbers.every(n => n > 1),
true
)
assert.deepEqual(
d.numbers.every(n => n > 10),
false
)
assert.deepEqual(
d.numbers.filter(n => n > 10),
[20, 100]
)
assert.deepEqual(
d.repeats.find(n => n < 10),
3
)
assert.deepEqual(
d.repeats.find(n => n < 10),
3
)
assert.deepEqual(
d.repeats.find(n => n < 0),
undefined
)
assert.deepEqual(
d.repeats.findIndex(n => n < 10),
2
)
assert.deepEqual(
d.repeats.findIndex(n => n < 0),
-1
)
assert.deepEqual(
d.repeats.findIndex(n => n < 10),
2
)
assert.deepEqual(
d.repeats.findIndex(n => n < 0),
-1
)
assert.deepEqual(d.numbers.includes(3), true)
assert.deepEqual(d.numbers.includes(-3), false)
assert.deepEqual(d.numbers.join("|"), "20|3|100")
assert.deepEqual(d.numbers.join(), "20,3,100")
assert.deepEqual(
d.numbers.some(f => f === 3),
true
)
assert.deepEqual(
d.numbers.some(f => f < 0),
false
)
assert.deepEqual(
d.numbers.reduce((sum, n) => sum + n, 100),
223
)
assert.deepEqual(
d.repeats.reduce((sum, n) => sum + n, 100),
352
)
assert.deepEqual(
d.chars.reduce((sum, n) => sum + n, "="),
"=abc"
)
assert.deepEqual(
d.chars.reduceRight((sum, n) => sum + n, "="),
"=cba"
)
assert.deepEqual(
d.numbers.reduceRight((sum, n) => sum + n, 100),
223
)
assert.deepEqual(d.repeats.lastIndexOf(3), 5)
assert.deepEqual(d.repeats.lastIndexOf(3, 3), 3)
})
doc = Automerge.change(doc, d => {
assert.deepEqual(d.numbers.fill(-1, 1, 2), [20, -1, 100])
assert.deepEqual(d.chars.fill("z", 1, 100), ["a", "z", "z"])
})
assert.deepEqual(r1, [20, 3, 100])
assert.deepEqual(doc.numbers, [20, -1, 100])
assert.deepEqual(doc.chars, ["a", "z", "z"])
it("should return non-null for map, list, text, and objects", () => {
assert.notEqual(Automerge.getObjectId(s1.text), null)
assert.notEqual(Automerge.getObjectId(s1.list), null)
assert.notEqual(Automerge.getObjectId(s1.map), null)
})
})
})
it("should obtain the same conflicts, regardless of merge order", () => {
let s1 = Automerge.init<any>()
let s2 = Automerge.init<any>()
s1 = Automerge.change(s1, doc => {
doc.x = 1
doc.y = 2
})
s2 = Automerge.change(s2, doc => {
doc.x = 3
doc.y = 4
})
const m1 = Automerge.merge(Automerge.clone(s1), Automerge.clone(s2))
const m2 = Automerge.merge(Automerge.clone(s2), Automerge.clone(s1))
assert.deepStrictEqual(
Automerge.getConflicts(m1, "x"),
Automerge.getConflicts(m2, "x")
)
})
describe("getObjectId", () => {
let s1 = Automerge.from({
string: "string",
number: 1,
null: null,
date: new Date(),
counter: new Automerge.Counter(),
bytes: new Uint8Array(10),
text: "",
list: [],
map: {},
})
it("should return null for scalar values", () => {
assert.equal(Automerge.getObjectId(s1.string), null)
assert.equal(Automerge.getObjectId(s1.number), null)
assert.equal(Automerge.getObjectId(s1.null!), null)
assert.equal(Automerge.getObjectId(s1.date), null)
assert.equal(Automerge.getObjectId(s1.counter), null)
assert.equal(Automerge.getObjectId(s1.bytes), null)
})
it("should return _root for the root object", () => {
assert.equal(Automerge.getObjectId(s1), "_root")
})
it("should return non-null for map, list, text, and objects", () => {
assert.equal(Automerge.getObjectId(s1.text), null)
assert.notEqual(Automerge.getObjectId(s1.list), null)
assert.notEqual(Automerge.getObjectId(s1.map), null)
})
})
})

View file

@ -0,0 +1,97 @@
import * as assert from 'assert'
import { checkEncoded } from './helpers'
import * as Automerge from '../src'
import { encodeChange, decodeChange } from '../src'
describe('change encoding', () => {
it('should encode text edits', () => {
/*
const change1 = {actor: 'aaaa', seq: 1, startOp: 1, time: 9, message: '', deps: [], ops: [
{action: 'makeText', obj: '_root', key: 'text', insert: false, pred: []},
{action: 'set', obj: '1@aaaa', elemId: '_head', insert: true, value: 'h', pred: []},
{action: 'del', obj: '1@aaaa', elemId: '2@aaaa', insert: false, pred: ['2@aaaa']},
{action: 'set', obj: '1@aaaa', elemId: '_head', insert: true, value: 'H', pred: []},
{action: 'set', obj: '1@aaaa', elemId: '4@aaaa', insert: true, value: 'i', pred: []}
]}
*/
const change1 = {actor: 'aaaa', seq: 1, startOp: 1, time: 9, message: null, deps: [], ops: [
{action: 'makeText', obj: '_root', key: 'text', pred: []},
{action: 'set', obj: '1@aaaa', elemId: '_head', insert: true, value: 'h', pred: []},
{action: 'del', obj: '1@aaaa', elemId: '2@aaaa', pred: ['2@aaaa']},
{action: 'set', obj: '1@aaaa', elemId: '_head', insert: true, value: 'H', pred: []},
{action: 'set', obj: '1@aaaa', elemId: '4@aaaa', insert: true, value: 'i', pred: []}
]}
checkEncoded(encodeChange(change1), [
0x85, 0x6f, 0x4a, 0x83, // magic bytes
0xe2, 0xbd, 0xfb, 0xf5, // checksum
1, 94, 0, 2, 0xaa, 0xaa, // chunkType: change, length, deps, actor 'aaaa'
1, 1, 9, 0, 0, // seq, startOp, time, message, actor list
12, 0x01, 4, 0x02, 4, // column count, objActor, objCtr
0x11, 8, 0x13, 7, 0x15, 8, // keyActor, keyCtr, keyStr
0x34, 4, 0x42, 6, // insert, action
0x56, 6, 0x57, 3, // valLen, valRaw
0x70, 6, 0x71, 2, 0x73, 2, // predNum, predActor, predCtr
0, 1, 4, 0, // objActor column: null, 0, 0, 0, 0
0, 1, 4, 1, // objCtr column: null, 1, 1, 1, 1
0, 2, 0x7f, 0, 0, 1, 0x7f, 0, // keyActor column: null, null, 0, null, 0
0, 1, 0x7c, 0, 2, 0x7e, 4, // keyCtr column: null, 0, 2, 0, 4
0x7f, 4, 0x74, 0x65, 0x78, 0x74, 0, 4, // keyStr column: 'text', null, null, null, null
1, 1, 1, 2, // insert column: false, true, false, true, true
0x7d, 4, 1, 3, 2, 1, // action column: makeText, set, del, set, set
0x7d, 0, 0x16, 0, 2, 0x16, // valLen column: 0, 0x16, 0, 0x16, 0x16
0x68, 0x48, 0x69, // valRaw column: 'h', 'H', 'i'
2, 0, 0x7f, 1, 2, 0, // predNum column: 0, 0, 1, 0, 0
0x7f, 0, // predActor column: 0
0x7f, 2 // predCtr column: 2
])
const decoded = decodeChange(encodeChange(change1))
assert.deepStrictEqual(decoded, Object.assign({hash: decoded.hash}, change1))
})
// FIXME - skipping this b/c it was never implemented in the rust impl and isnt trivial
/*
it.skip('should require strict ordering of preds', () => {
const change = new Uint8Array([
133, 111, 74, 131, 31, 229, 112, 44, 1, 105, 1, 58, 30, 190, 100, 253, 180, 180, 66, 49, 126,
81, 142, 10, 3, 35, 140, 189, 231, 34, 145, 57, 66, 23, 224, 149, 64, 97, 88, 140, 168, 194,
229, 4, 244, 209, 58, 138, 67, 140, 1, 152, 236, 250, 2, 0, 1, 4, 55, 234, 66, 242, 8, 21, 11,
52, 1, 66, 2, 86, 3, 87, 10, 112, 2, 113, 3, 115, 4, 127, 9, 99, 111, 109, 109, 111, 110, 86,
97, 114, 1, 127, 1, 127, 166, 1, 52, 48, 57, 49, 52, 57, 52, 53, 56, 50, 127, 2, 126, 0, 1,
126, 139, 1, 0
])
assert.throws(() => { decodeChange(change) }, /operation IDs are not in ascending order/)
})
*/
describe('with trailing bytes', () => {
let change = new Uint8Array([
0x85, 0x6f, 0x4a, 0x83, // magic bytes
0xb2, 0x98, 0x9e, 0xa9, // checksum
1, 61, 0, 2, 0x12, 0x34, // chunkType: change, length, deps, actor '1234'
1, 1, 252, 250, 220, 255, 5, // seq, startOp, time
14, 73, 110, 105, 116, 105, 97, 108, 105, 122, 97, 116, 105, 111, 110, // message: 'Initialization'
0, 6, // actor list, column count
0x15, 3, 0x34, 1, 0x42, 2, // keyStr, insert, action
0x56, 2, 0x57, 1, 0x70, 2, // valLen, valRaw, predNum
0x7f, 1, 0x78, // keyStr: 'x'
1, // insert: false
0x7f, 1, // action: set
0x7f, 19, // valLen: 1 byte of type uint
1, // valRaw: 1
0x7f, 0, // predNum: 0
0, 1, 2, 3, 4, 5, 6, 7, 8, 9 // 10 trailing bytes
])
it('should allow decoding and re-encoding', () => {
// NOTE: This calls the JavaScript encoding and decoding functions, even when the WebAssembly
// backend is loaded. Should the wasm backend export its own functions for testing?
checkEncoded(change, encodeChange(decodeChange(change)))
})
it('should be preserved in document encoding', () => {
const [doc] = Automerge.applyChanges(Automerge.init(), [change])
const [reconstructed] = Automerge.getAllChanges(Automerge.load(Automerge.save(doc)))
checkEncoded(change, reconstructed)
})
})
})

View file

@ -1,28 +1,20 @@
import * as assert from "assert"
import { unstable as Automerge } from "../src"
describe("Automerge", () => {
describe("basics", () => {
it("should allow you to load incrementally", () => {
let doc1 = Automerge.from<any>({ foo: "bar" })
let doc2 = Automerge.init<any>()
doc2 = Automerge.loadIncremental(doc2, Automerge.save(doc1))
doc1 = Automerge.change(doc1, d => (d.foo2 = "bar2"))
doc2 = Automerge.loadIncremental(
doc2,
Automerge.getBackend(doc1).saveIncremental()
)
doc1 = Automerge.change(doc1, d => (d.foo = "bar2"))
doc2 = Automerge.loadIncremental(
doc2,
Automerge.getBackend(doc1).saveIncremental()
)
doc1 = Automerge.change(doc1, d => (d.x = "y"))
doc2 = Automerge.loadIncremental(
doc2,
Automerge.getBackend(doc1).saveIncremental()
)
assert.deepEqual(doc1, doc2)
import * as assert from 'assert'
import * as Automerge from '../src'
describe('Automerge', () => {
describe('basics', () => {
it('should allow you to load incrementally', () => {
let doc1 = Automerge.from({ foo: "bar" })
let doc2 = Automerge.init();
doc2 = Automerge.loadIncremental(doc2, Automerge.save(doc1))
doc1 = Automerge.change(doc1, (d) => d.foo2 = "bar2")
doc2 = Automerge.loadIncremental(doc2, Automerge.getBackend(doc1).saveIncremental() )
doc1 = Automerge.change(doc1, (d) => d.foo = "bar2")
doc2 = Automerge.loadIncremental(doc2, Automerge.getBackend(doc1).saveIncremental() )
doc1 = Automerge.change(doc1, (d) => d.x = "y")
doc2 = Automerge.loadIncremental(doc2, Automerge.getBackend(doc1).saveIncremental() )
assert.deepEqual(doc1,doc2)
})
})
})
})

View file

@ -1,21 +1,16 @@
import * as assert from "assert"
import { Encoder } from "./legacy/encoding"
import * as assert from 'assert'
import { Encoder } from './legacy/encoding'
// Assertion that succeeds if the first argument deepStrictEquals at least one of the
// subsequent arguments (but we don't care which one)
export function assertEqualsOneOf(actual, ...expected) {
function assertEqualsOneOf(actual, ...expected) {
assert(expected.length > 0)
for (let i = 0; i < expected.length; i++) {
try {
assert.deepStrictEqual(actual, expected[i])
return // if we get here without an exception, that means success
} catch (e) {
if (e instanceof assert.AssertionError) {
if (!e.name.match(/^AssertionError/) || i === expected.length - 1)
throw e
} else {
throw e
}
if (!e.name.match(/^AssertionError/) || i === expected.length - 1) throw e
}
}
}
@ -24,13 +19,14 @@ export function assertEqualsOneOf(actual, ...expected) {
* Asserts that the byte array maintained by `encoder` contains the same byte
* sequence as the array `bytes`.
*/
export function checkEncoded(encoder, bytes, detail?) {
const encoded = encoder instanceof Encoder ? encoder.buffer : encoder
function checkEncoded(encoder, bytes, detail) {
const encoded = (encoder instanceof Encoder) ? encoder.buffer : encoder
const expected = new Uint8Array(bytes)
const message =
(detail ? `${detail}: ` : "") + `${encoded} expected to equal ${expected}`
const message = (detail ? `${detail}: ` : '') + `${encoded} expected to equal ${expected}`
assert(encoded.byteLength === expected.byteLength, message)
for (let i = 0; i < encoded.byteLength; i++) {
assert(encoded[i] === expected[i], message)
}
}
module.exports = { assertEqualsOneOf, checkEncoded }

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
function isObject(obj) {
return typeof obj === "object" && obj !== null
return typeof obj === 'object' && obj !== null
}
/**
@ -20,11 +20,11 @@ function copyObject(obj) {
* with an actor ID, separated by an `@` sign) and returns an object `{counter, actorId}`.
*/
function parseOpId(opId) {
const match = /^(\d+)@(.*)$/.exec(opId || "")
const match = /^(\d+)@(.*)$/.exec(opId || '')
if (!match) {
throw new RangeError(`Not a valid opId: ${opId}`)
}
return { counter: parseInt(match[1], 10), actorId: match[2] }
return {counter: parseInt(match[1], 10), actorId: match[2]}
}
/**
@ -32,7 +32,7 @@ function parseOpId(opId) {
*/
function equalBytes(array1, array2) {
if (!(array1 instanceof Uint8Array) || !(array2 instanceof Uint8Array)) {
throw new TypeError("equalBytes can only compare Uint8Arrays")
throw new TypeError('equalBytes can only compare Uint8Arrays')
}
if (array1.byteLength !== array2.byteLength) return false
for (let i = 0; i < array1.byteLength; i++) {
@ -51,9 +51,5 @@ function createArrayOfNulls(length) {
}
module.exports = {
isObject,
copyObject,
parseOpId,
equalBytes,
createArrayOfNulls,
isObject, copyObject, parseOpId, equalBytes, createArrayOfNulls
}

View file

@ -6,7 +6,7 @@
* https://github.com/anonyco/FastestSmallestTextEncoderDecoder
*/
const utf8encoder = new TextEncoder()
const utf8decoder = new TextDecoder("utf-8")
const utf8decoder = new TextDecoder('utf-8')
function stringToUtf8(string) {
return utf8encoder.encode(string)
@ -20,48 +20,30 @@ function utf8ToString(buffer) {
* Converts a string consisting of hexadecimal digits into an Uint8Array.
*/
function hexStringToBytes(value) {
if (typeof value !== "string") {
throw new TypeError("value is not a string")
if (typeof value !== 'string') {
throw new TypeError('value is not a string')
}
if (!/^([0-9a-f][0-9a-f])*$/.test(value)) {
throw new RangeError("value is not hexadecimal")
throw new RangeError('value is not hexadecimal')
}
if (value === "") {
if (value === '') {
return new Uint8Array(0)
} else {
return new Uint8Array(value.match(/../g).map(b => parseInt(b, 16)))
}
}
const NIBBLE_TO_HEX = [
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"a",
"b",
"c",
"d",
"e",
"f",
]
const NIBBLE_TO_HEX = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']
const BYTE_TO_HEX = new Array(256)
for (let i = 0; i < 256; i++) {
BYTE_TO_HEX[i] = `${NIBBLE_TO_HEX[(i >>> 4) & 0xf]}${NIBBLE_TO_HEX[i & 0xf]}`
BYTE_TO_HEX[i] = `${NIBBLE_TO_HEX[(i >>> 4) & 0xf]}${NIBBLE_TO_HEX[i & 0xf]}`;
}
/**
* Converts a Uint8Array into the equivalent hexadecimal string.
*/
function bytesToHexString(bytes) {
let hex = "",
len = bytes.byteLength
let hex = '', len = bytes.byteLength
for (let i = 0; i < len; i++) {
hex += BYTE_TO_HEX[bytes[i]]
}
@ -113,17 +95,14 @@ class Encoder {
* appends it to the buffer. Returns the number of bytes written.
*/
appendUint32(value) {
if (!Number.isInteger(value))
throw new RangeError("value is not an integer")
if (value < 0 || value > 0xffffffff)
throw new RangeError("number out of range")
if (!Number.isInteger(value)) throw new RangeError('value is not an integer')
if (value < 0 || value > 0xffffffff) throw new RangeError('number out of range')
const numBytes = Math.max(1, Math.ceil((32 - Math.clz32(value)) / 7))
if (this.offset + numBytes > this.buf.byteLength) this.grow()
for (let i = 0; i < numBytes; i++) {
this.buf[this.offset + i] =
(value & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
this.buf[this.offset + i] = (value & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
value >>>= 7 // zero-filling right shift
}
this.offset += numBytes
@ -136,19 +115,14 @@ class Encoder {
* it to the buffer. Returns the number of bytes written.
*/
appendInt32(value) {
if (!Number.isInteger(value))
throw new RangeError("value is not an integer")
if (value < -0x80000000 || value > 0x7fffffff)
throw new RangeError("number out of range")
if (!Number.isInteger(value)) throw new RangeError('value is not an integer')
if (value < -0x80000000 || value > 0x7fffffff) throw new RangeError('number out of range')
const numBytes = Math.ceil(
(33 - Math.clz32(value >= 0 ? value : -value - 1)) / 7
)
const numBytes = Math.ceil((33 - Math.clz32(value >= 0 ? value : -value - 1)) / 7)
if (this.offset + numBytes > this.buf.byteLength) this.grow()
for (let i = 0; i < numBytes; i++) {
this.buf[this.offset + i] =
(value & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
this.buf[this.offset + i] = (value & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
value >>= 7 // sign-propagating right shift
}
this.offset += numBytes
@ -161,10 +135,9 @@ class Encoder {
* (53 bits).
*/
appendUint53(value) {
if (!Number.isInteger(value))
throw new RangeError("value is not an integer")
if (!Number.isInteger(value)) throw new RangeError('value is not an integer')
if (value < 0 || value > Number.MAX_SAFE_INTEGER) {
throw new RangeError("number out of range")
throw new RangeError('number out of range')
}
const high32 = Math.floor(value / 0x100000000)
const low32 = (value & 0xffffffff) >>> 0 // right shift to interpret as unsigned
@ -177,10 +150,9 @@ class Encoder {
* (53 bits).
*/
appendInt53(value) {
if (!Number.isInteger(value))
throw new RangeError("value is not an integer")
if (!Number.isInteger(value)) throw new RangeError('value is not an integer')
if (value < Number.MIN_SAFE_INTEGER || value > Number.MAX_SAFE_INTEGER) {
throw new RangeError("number out of range")
throw new RangeError('number out of range')
}
const high32 = Math.floor(value / 0x100000000)
const low32 = (value & 0xffffffff) >>> 0 // right shift to interpret as unsigned
@ -195,10 +167,10 @@ class Encoder {
*/
appendUint64(high32, low32) {
if (!Number.isInteger(high32) || !Number.isInteger(low32)) {
throw new RangeError("value is not an integer")
throw new RangeError('value is not an integer')
}
if (high32 < 0 || high32 > 0xffffffff || low32 < 0 || low32 > 0xffffffff) {
throw new RangeError("number out of range")
throw new RangeError('number out of range')
}
if (high32 === 0) return this.appendUint32(low32)
@ -208,12 +180,10 @@ class Encoder {
this.buf[this.offset + i] = (low32 & 0x7f) | 0x80
low32 >>>= 7 // zero-filling right shift
}
this.buf[this.offset + 4] =
(low32 & 0x0f) | ((high32 & 0x07) << 4) | (numBytes === 5 ? 0x00 : 0x80)
this.buf[this.offset + 4] = (low32 & 0x0f) | ((high32 & 0x07) << 4) | (numBytes === 5 ? 0x00 : 0x80)
high32 >>>= 3
for (let i = 5; i < numBytes; i++) {
this.buf[this.offset + i] =
(high32 & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
this.buf[this.offset + i] = (high32 & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
high32 >>>= 7
}
this.offset += numBytes
@ -230,35 +200,25 @@ class Encoder {
*/
appendInt64(high32, low32) {
if (!Number.isInteger(high32) || !Number.isInteger(low32)) {
throw new RangeError("value is not an integer")
throw new RangeError('value is not an integer')
}
if (
high32 < -0x80000000 ||
high32 > 0x7fffffff ||
low32 < -0x80000000 ||
low32 > 0xffffffff
) {
throw new RangeError("number out of range")
if (high32 < -0x80000000 || high32 > 0x7fffffff || low32 < -0x80000000 || low32 > 0xffffffff) {
throw new RangeError('number out of range')
}
low32 >>>= 0 // interpret as unsigned
if (high32 === 0 && low32 <= 0x7fffffff) return this.appendInt32(low32)
if (high32 === -1 && low32 >= 0x80000000)
return this.appendInt32(low32 - 0x100000000)
if (high32 === -1 && low32 >= 0x80000000) return this.appendInt32(low32 - 0x100000000)
const numBytes = Math.ceil(
(65 - Math.clz32(high32 >= 0 ? high32 : -high32 - 1)) / 7
)
const numBytes = Math.ceil((65 - Math.clz32(high32 >= 0 ? high32 : -high32 - 1)) / 7)
if (this.offset + numBytes > this.buf.byteLength) this.grow()
for (let i = 0; i < 4; i++) {
this.buf[this.offset + i] = (low32 & 0x7f) | 0x80
low32 >>>= 7 // zero-filling right shift
}
this.buf[this.offset + 4] =
(low32 & 0x0f) | ((high32 & 0x07) << 4) | (numBytes === 5 ? 0x00 : 0x80)
this.buf[this.offset + 4] = (low32 & 0x0f) | ((high32 & 0x07) << 4) | (numBytes === 5 ? 0x00 : 0x80)
high32 >>= 3 // sign-propagating right shift
for (let i = 5; i < numBytes; i++) {
this.buf[this.offset + i] =
(high32 & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
this.buf[this.offset + i] = (high32 & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
high32 >>= 7
}
this.offset += numBytes
@ -283,7 +243,7 @@ class Encoder {
* number of bytes appended.
*/
appendRawString(value) {
if (typeof value !== "string") throw new TypeError("value is not a string")
if (typeof value !== 'string') throw new TypeError('value is not a string')
return this.appendRawBytes(stringToUtf8(value))
}
@ -302,7 +262,7 @@ class Encoder {
* (where the length is encoded as an unsigned LEB128 integer).
*/
appendPrefixedString(value) {
if (typeof value !== "string") throw new TypeError("value is not a string")
if (typeof value !== 'string') throw new TypeError('value is not a string')
this.appendPrefixedBytes(stringToUtf8(value))
return this
}
@ -321,7 +281,8 @@ class Encoder {
* Flushes any unwritten data to the buffer. Call this before reading from
* the buffer constructed by this Encoder.
*/
finish() {}
finish() {
}
}
/**
@ -360,7 +321,7 @@ class Decoder {
*/
skip(bytes) {
if (this.offset + bytes > this.buf.byteLength) {
throw new RangeError("cannot skip beyond end of buffer")
throw new RangeError('cannot skip beyond end of buffer')
}
this.offset += bytes
}
@ -378,20 +339,18 @@ class Decoder {
* Throws an exception if the value doesn't fit in a 32-bit unsigned int.
*/
readUint32() {
let result = 0,
shift = 0
let result = 0, shift = 0
while (this.offset < this.buf.byteLength) {
const nextByte = this.buf[this.offset]
if (shift === 28 && (nextByte & 0xf0) !== 0) {
// more than 5 bytes, or value > 0xffffffff
throw new RangeError("number out of range")
if (shift === 28 && (nextByte & 0xf0) !== 0) { // more than 5 bytes, or value > 0xffffffff
throw new RangeError('number out of range')
}
result = (result | ((nextByte & 0x7f) << shift)) >>> 0 // right shift to interpret value as unsigned
result = (result | (nextByte & 0x7f) << shift) >>> 0 // right shift to interpret value as unsigned
shift += 7
this.offset++
if ((nextByte & 0x80) === 0) return result
}
throw new RangeError("buffer ended with incomplete number")
throw new RangeError('buffer ended with incomplete number')
}
/**
@ -399,17 +358,13 @@ class Decoder {
* Throws an exception if the value doesn't fit in a 32-bit signed int.
*/
readInt32() {
let result = 0,
shift = 0
let result = 0, shift = 0
while (this.offset < this.buf.byteLength) {
const nextByte = this.buf[this.offset]
if (
(shift === 28 && (nextByte & 0x80) !== 0) || // more than 5 bytes
(shift === 28 && (nextByte & 0x40) === 0 && (nextByte & 0x38) !== 0) || // positive int > 0x7fffffff
(shift === 28 && (nextByte & 0x40) !== 0 && (nextByte & 0x38) !== 0x38)
) {
// negative int < -0x80000000
throw new RangeError("number out of range")
if ((shift === 28 && (nextByte & 0x80) !== 0) || // more than 5 bytes
(shift === 28 && (nextByte & 0x40) === 0 && (nextByte & 0x38) !== 0) || // positive int > 0x7fffffff
(shift === 28 && (nextByte & 0x40) !== 0 && (nextByte & 0x38) !== 0x38)) { // negative int < -0x80000000
throw new RangeError('number out of range')
}
result |= (nextByte & 0x7f) << shift
shift += 7
@ -423,7 +378,7 @@ class Decoder {
}
}
}
throw new RangeError("buffer ended with incomplete number")
throw new RangeError('buffer ended with incomplete number')
}
/**
@ -434,7 +389,7 @@ class Decoder {
readUint53() {
const { low32, high32 } = this.readUint64()
if (high32 < 0 || high32 > 0x1fffff) {
throw new RangeError("number out of range")
throw new RangeError('number out of range')
}
return high32 * 0x100000000 + low32
}
@ -446,12 +401,8 @@ class Decoder {
*/
readInt53() {
const { low32, high32 } = this.readInt64()
if (
high32 < -0x200000 ||
(high32 === -0x200000 && low32 === 0) ||
high32 > 0x1fffff
) {
throw new RangeError("number out of range")
if (high32 < -0x200000 || (high32 === -0x200000 && low32 === 0) || high32 > 0x1fffff) {
throw new RangeError('number out of range')
}
return high32 * 0x100000000 + low32
}
@ -463,12 +414,10 @@ class Decoder {
* `{high32, low32}`.
*/
readUint64() {
let low32 = 0,
high32 = 0,
shift = 0
let low32 = 0, high32 = 0, shift = 0
while (this.offset < this.buf.byteLength && shift <= 28) {
const nextByte = this.buf[this.offset]
low32 = (low32 | ((nextByte & 0x7f) << shift)) >>> 0 // right shift to interpret value as unsigned
low32 = (low32 | (nextByte & 0x7f) << shift) >>> 0 // right shift to interpret value as unsigned
if (shift === 28) {
high32 = (nextByte & 0x70) >>> 4
}
@ -480,16 +429,15 @@ class Decoder {
shift = 3
while (this.offset < this.buf.byteLength) {
const nextByte = this.buf[this.offset]
if (shift === 31 && (nextByte & 0xfe) !== 0) {
// more than 10 bytes, or value > 2^64 - 1
throw new RangeError("number out of range")
if (shift === 31 && (nextByte & 0xfe) !== 0) { // more than 10 bytes, or value > 2^64 - 1
throw new RangeError('number out of range')
}
high32 = (high32 | ((nextByte & 0x7f) << shift)) >>> 0
high32 = (high32 | (nextByte & 0x7f) << shift) >>> 0
shift += 7
this.offset++
if ((nextByte & 0x80) === 0) return { high32, low32 }
}
throw new RangeError("buffer ended with incomplete number")
throw new RangeError('buffer ended with incomplete number')
}
/**
@ -500,20 +448,17 @@ class Decoder {
* sign of the `high32` half indicates the sign of the 64-bit number.
*/
readInt64() {
let low32 = 0,
high32 = 0,
shift = 0
let low32 = 0, high32 = 0, shift = 0
while (this.offset < this.buf.byteLength && shift <= 28) {
const nextByte = this.buf[this.offset]
low32 = (low32 | ((nextByte & 0x7f) << shift)) >>> 0 // right shift to interpret value as unsigned
low32 = (low32 | (nextByte & 0x7f) << shift) >>> 0 // right shift to interpret value as unsigned
if (shift === 28) {
high32 = (nextByte & 0x70) >>> 4
}
shift += 7
this.offset++
if ((nextByte & 0x80) === 0) {
if ((nextByte & 0x40) !== 0) {
// sign-extend negative integer
if ((nextByte & 0x40) !== 0) { // sign-extend negative integer
if (shift < 32) low32 = (low32 | (-1 << shift)) >>> 0
high32 |= -1 << Math.max(shift - 32, 0)
}
@ -527,20 +472,19 @@ class Decoder {
// On the 10th byte there are only two valid values: all 7 value bits zero
// (if the value is positive) or all 7 bits one (if the value is negative)
if (shift === 31 && nextByte !== 0 && nextByte !== 0x7f) {
throw new RangeError("number out of range")
throw new RangeError('number out of range')
}
high32 |= (nextByte & 0x7f) << shift
shift += 7
this.offset++
if ((nextByte & 0x80) === 0) {
if ((nextByte & 0x40) !== 0 && shift < 32) {
// sign-extend negative integer
if ((nextByte & 0x40) !== 0 && shift < 32) { // sign-extend negative integer
high32 |= -1 << shift
}
return { high32, low32 }
}
}
throw new RangeError("buffer ended with incomplete number")
throw new RangeError('buffer ended with incomplete number')
}
/**
@ -550,7 +494,7 @@ class Decoder {
readRawBytes(length) {
const start = this.offset
if (start + length > this.buf.byteLength) {
throw new RangeError("subarray exceeds buffer size")
throw new RangeError('subarray exceeds buffer size')
}
this.offset += length
return this.buf.subarray(start, this.offset)
@ -615,7 +559,7 @@ class RLEEncoder extends Encoder {
constructor(type) {
super()
this.type = type
this.state = "empty"
this.state = 'empty'
this.lastValue = undefined
this.count = 0
this.literal = []
@ -634,81 +578,76 @@ class RLEEncoder extends Encoder {
*/
_appendValue(value, repetitions = 1) {
if (repetitions <= 0) return
if (this.state === "empty") {
this.state =
value === null
? "nulls"
: repetitions === 1
? "loneValue"
: "repetition"
if (this.state === 'empty') {
this.state = (value === null ? 'nulls' : (repetitions === 1 ? 'loneValue' : 'repetition'))
this.lastValue = value
this.count = repetitions
} else if (this.state === "loneValue") {
} else if (this.state === 'loneValue') {
if (value === null) {
this.flush()
this.state = "nulls"
this.state = 'nulls'
this.count = repetitions
} else if (value === this.lastValue) {
this.state = "repetition"
this.state = 'repetition'
this.count = 1 + repetitions
} else if (repetitions > 1) {
this.flush()
this.state = "repetition"
this.state = 'repetition'
this.count = repetitions
this.lastValue = value
} else {
this.state = "literal"
this.state = 'literal'
this.literal = [this.lastValue]
this.lastValue = value
}
} else if (this.state === "repetition") {
} else if (this.state === 'repetition') {
if (value === null) {
this.flush()
this.state = "nulls"
this.state = 'nulls'
this.count = repetitions
} else if (value === this.lastValue) {
this.count += repetitions
} else if (repetitions > 1) {
this.flush()
this.state = "repetition"
this.state = 'repetition'
this.count = repetitions
this.lastValue = value
} else {
this.flush()
this.state = "loneValue"
this.state = 'loneValue'
this.lastValue = value
}
} else if (this.state === "literal") {
} else if (this.state === 'literal') {
if (value === null) {
this.literal.push(this.lastValue)
this.flush()
this.state = "nulls"
this.state = 'nulls'
this.count = repetitions
} else if (value === this.lastValue) {
this.flush()
this.state = "repetition"
this.state = 'repetition'
this.count = 1 + repetitions
} else if (repetitions > 1) {
this.literal.push(this.lastValue)
this.flush()
this.state = "repetition"
this.state = 'repetition'
this.count = repetitions
this.lastValue = value
} else {
this.literal.push(this.lastValue)
this.lastValue = value
}
} else if (this.state === "nulls") {
} else if (this.state === 'nulls') {
if (value === null) {
this.count += repetitions
} else if (repetitions > 1) {
this.flush()
this.state = "repetition"
this.state = 'repetition'
this.count = repetitions
this.lastValue = value
} else {
this.flush()
this.state = "loneValue"
this.state = 'loneValue'
this.lastValue = value
}
}
@ -727,16 +666,13 @@ class RLEEncoder extends Encoder {
*/
copyFrom(decoder, options = {}) {
const { count, sumValues, sumShift } = options
if (!(decoder instanceof RLEDecoder) || decoder.type !== this.type) {
throw new TypeError("incompatible type of decoder")
if (!(decoder instanceof RLEDecoder) || (decoder.type !== this.type)) {
throw new TypeError('incompatible type of decoder')
}
let remaining = typeof count === "number" ? count : Number.MAX_SAFE_INTEGER
let nonNullValues = 0,
sum = 0
if (count && remaining > 0 && decoder.done)
throw new RangeError(`cannot copy ${count} values`)
if (remaining === 0 || decoder.done)
return sumValues ? { nonNullValues, sum } : { nonNullValues }
let remaining = (typeof count === 'number' ? count : Number.MAX_SAFE_INTEGER)
let nonNullValues = 0, sum = 0
if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)
if (remaining === 0 || decoder.done) return sumValues ? {nonNullValues, sum} : {nonNullValues}
// Copy a value so that we have a well-defined starting state. NB: when super.copyFrom() is
// called by the DeltaEncoder subclass, the following calls to readValue() and appendValue()
@ -748,101 +684,87 @@ class RLEEncoder extends Encoder {
remaining -= numNulls
decoder.count -= numNulls - 1
this.appendValue(null, numNulls)
if (count && remaining > 0 && decoder.done)
throw new RangeError(`cannot copy ${count} values`)
if (remaining === 0 || decoder.done)
return sumValues ? { nonNullValues, sum } : { nonNullValues }
if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)
if (remaining === 0 || decoder.done) return sumValues ? {nonNullValues, sum} : {nonNullValues}
firstValue = decoder.readValue()
if (firstValue === null)
throw new RangeError("null run must be followed by non-null value")
if (firstValue === null) throw new RangeError('null run must be followed by non-null value')
}
this.appendValue(firstValue)
remaining--
nonNullValues++
if (sumValues) sum += sumShift ? firstValue >>> sumShift : firstValue
if (count && remaining > 0 && decoder.done)
throw new RangeError(`cannot copy ${count} values`)
if (remaining === 0 || decoder.done)
return sumValues ? { nonNullValues, sum } : { nonNullValues }
if (sumValues) sum += (sumShift ? (firstValue >>> sumShift) : firstValue)
if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)
if (remaining === 0 || decoder.done) return sumValues ? {nonNullValues, sum} : {nonNullValues}
// Copy data at the record level without expanding repetitions
let firstRun = decoder.count > 0
let firstRun = (decoder.count > 0)
while (remaining > 0 && !decoder.done) {
if (!firstRun) decoder.readRecord()
const numValues = Math.min(decoder.count, remaining)
decoder.count -= numValues
if (decoder.state === "literal") {
if (decoder.state === 'literal') {
nonNullValues += numValues
for (let i = 0; i < numValues; i++) {
if (decoder.done) throw new RangeError("incomplete literal")
if (decoder.done) throw new RangeError('incomplete literal')
const value = decoder.readRawValue()
if (value === decoder.lastValue)
throw new RangeError(
"Repetition of values is not allowed in literal"
)
if (value === decoder.lastValue) throw new RangeError('Repetition of values is not allowed in literal')
decoder.lastValue = value
this._appendValue(value)
if (sumValues) sum += sumShift ? value >>> sumShift : value
if (sumValues) sum += (sumShift ? (value >>> sumShift) : value)
}
} else if (decoder.state === "repetition") {
} else if (decoder.state === 'repetition') {
nonNullValues += numValues
if (sumValues)
sum +=
numValues *
(sumShift ? decoder.lastValue >>> sumShift : decoder.lastValue)
if (sumValues) sum += numValues * (sumShift ? (decoder.lastValue >>> sumShift) : decoder.lastValue)
const value = decoder.lastValue
this._appendValue(value)
if (numValues > 1) {
this._appendValue(value)
if (this.state !== "repetition")
throw new RangeError(`Unexpected state ${this.state}`)
if (this.state !== 'repetition') throw new RangeError(`Unexpected state ${this.state}`)
this.count += numValues - 2
}
} else if (decoder.state === "nulls") {
} else if (decoder.state === 'nulls') {
this._appendValue(null)
if (this.state !== "nulls")
throw new RangeError(`Unexpected state ${this.state}`)
if (this.state !== 'nulls') throw new RangeError(`Unexpected state ${this.state}`)
this.count += numValues - 1
}
firstRun = false
remaining -= numValues
}
if (count && remaining > 0 && decoder.done)
throw new RangeError(`cannot copy ${count} values`)
return sumValues ? { nonNullValues, sum } : { nonNullValues }
if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)
return sumValues ? {nonNullValues, sum} : {nonNullValues}
}
/**
* Private method, do not call from outside the class.
*/
flush() {
if (this.state === "loneValue") {
if (this.state === 'loneValue') {
this.appendInt32(-1)
this.appendRawValue(this.lastValue)
} else if (this.state === "repetition") {
} else if (this.state === 'repetition') {
this.appendInt53(this.count)
this.appendRawValue(this.lastValue)
} else if (this.state === "literal") {
} else if (this.state === 'literal') {
this.appendInt53(-this.literal.length)
for (let v of this.literal) this.appendRawValue(v)
} else if (this.state === "nulls") {
} else if (this.state === 'nulls') {
this.appendInt32(0)
this.appendUint53(this.count)
}
this.state = "empty"
this.state = 'empty'
}
/**
* Private method, do not call from outside the class.
*/
appendRawValue(value) {
if (this.type === "int") {
if (this.type === 'int') {
this.appendInt53(value)
} else if (this.type === "uint") {
} else if (this.type === 'uint') {
this.appendUint53(value)
} else if (this.type === "utf8") {
} else if (this.type === 'utf8') {
this.appendPrefixedString(value)
} else {
throw new RangeError(`Unknown RLEEncoder datatype: ${this.type}`)
@ -854,9 +776,9 @@ class RLEEncoder extends Encoder {
* the buffer constructed by this Encoder.
*/
finish() {
if (this.state === "literal") this.literal.push(this.lastValue)
if (this.state === 'literal') this.literal.push(this.lastValue)
// Don't write anything if the only values we have seen are nulls
if (this.state !== "nulls" || this.offset > 0) this.flush()
if (this.state !== 'nulls' || this.offset > 0) this.flush()
}
}
@ -878,7 +800,7 @@ class RLEDecoder extends Decoder {
* position, and true if we are at the end of the buffer.
*/
get done() {
return this.count === 0 && this.offset === this.buf.byteLength
return (this.count === 0) && (this.offset === this.buf.byteLength)
}
/**
@ -899,10 +821,9 @@ class RLEDecoder extends Decoder {
if (this.done) return null
if (this.count === 0) this.readRecord()
this.count -= 1
if (this.state === "literal") {
if (this.state === 'literal') {
const value = this.readRawValue()
if (value === this.lastValue)
throw new RangeError("Repetition of values is not allowed in literal")
if (value === this.lastValue) throw new RangeError('Repetition of values is not allowed in literal')
this.lastValue = value
return value
} else {
@ -918,22 +839,20 @@ class RLEDecoder extends Decoder {
if (this.count === 0) {
this.count = this.readInt53()
if (this.count > 0) {
this.lastValue =
this.count <= numSkip ? this.skipRawValues(1) : this.readRawValue()
this.state = "repetition"
this.lastValue = (this.count <= numSkip) ? this.skipRawValues(1) : this.readRawValue()
this.state = 'repetition'
} else if (this.count < 0) {
this.count = -this.count
this.state = "literal"
} else {
// this.count == 0
this.state = 'literal'
} else { // this.count == 0
this.count = this.readUint53()
this.lastValue = null
this.state = "nulls"
this.state = 'nulls'
}
}
const consume = Math.min(numSkip, this.count)
if (this.state === "literal") this.skipRawValues(consume)
if (this.state === 'literal') this.skipRawValues(consume)
numSkip -= consume
this.count -= consume
}
@ -947,34 +866,23 @@ class RLEDecoder extends Decoder {
this.count = this.readInt53()
if (this.count > 1) {
const value = this.readRawValue()
if (
(this.state === "repetition" || this.state === "literal") &&
this.lastValue === value
) {
throw new RangeError(
"Successive repetitions with the same value are not allowed"
)
if ((this.state === 'repetition' || this.state === 'literal') && this.lastValue === value) {
throw new RangeError('Successive repetitions with the same value are not allowed')
}
this.state = "repetition"
this.state = 'repetition'
this.lastValue = value
} else if (this.count === 1) {
throw new RangeError(
"Repetition count of 1 is not allowed, use a literal instead"
)
throw new RangeError('Repetition count of 1 is not allowed, use a literal instead')
} else if (this.count < 0) {
this.count = -this.count
if (this.state === "literal")
throw new RangeError("Successive literals are not allowed")
this.state = "literal"
} else {
// this.count == 0
if (this.state === "nulls")
throw new RangeError("Successive null runs are not allowed")
if (this.state === 'literal') throw new RangeError('Successive literals are not allowed')
this.state = 'literal'
} else { // this.count == 0
if (this.state === 'nulls') throw new RangeError('Successive null runs are not allowed')
this.count = this.readUint53()
if (this.count === 0)
throw new RangeError("Zero-length null runs are not allowed")
if (this.count === 0) throw new RangeError('Zero-length null runs are not allowed')
this.lastValue = null
this.state = "nulls"
this.state = 'nulls'
}
}
@ -983,11 +891,11 @@ class RLEDecoder extends Decoder {
* Reads one value of the datatype configured on construction.
*/
readRawValue() {
if (this.type === "int") {
if (this.type === 'int') {
return this.readInt53()
} else if (this.type === "uint") {
} else if (this.type === 'uint') {
return this.readUint53()
} else if (this.type === "utf8") {
} else if (this.type === 'utf8') {
return this.readPrefixedString()
} else {
throw new RangeError(`Unknown RLEDecoder datatype: ${this.type}`)
@ -999,14 +907,14 @@ class RLEDecoder extends Decoder {
* Skips over `num` values of the datatype configured on construction.
*/
skipRawValues(num) {
if (this.type === "utf8") {
if (this.type === 'utf8') {
for (let i = 0; i < num; i++) this.skip(this.readUint53())
} else {
while (num > 0 && this.offset < this.buf.byteLength) {
if ((this.buf[this.offset] & 0x80) === 0) num--
this.offset++
}
if (num > 0) throw new RangeError("cannot skip beyond end of buffer")
if (num > 0) throw new RangeError('cannot skip beyond end of buffer')
}
}
}
@ -1023,7 +931,7 @@ class RLEDecoder extends Decoder {
*/
class DeltaEncoder extends RLEEncoder {
constructor() {
super("int")
super('int')
this.absoluteValue = 0
}
@ -1033,7 +941,7 @@ class DeltaEncoder extends RLEEncoder {
*/
appendValue(value, repetitions = 1) {
if (repetitions <= 0) return
if (typeof value === "number") {
if (typeof value === 'number') {
super.appendValue(value - this.absoluteValue, 1)
this.absoluteValue = value
if (repetitions > 1) super.appendValue(0, repetitions - 1)
@ -1049,29 +957,26 @@ class DeltaEncoder extends RLEEncoder {
*/
copyFrom(decoder, options = {}) {
if (options.sumValues) {
throw new RangeError("unsupported options for DeltaEncoder.copyFrom()")
throw new RangeError('unsupported options for DeltaEncoder.copyFrom()')
}
if (!(decoder instanceof DeltaDecoder)) {
throw new TypeError("incompatible type of decoder")
throw new TypeError('incompatible type of decoder')
}
let remaining = options.count
if (remaining > 0 && decoder.done)
throw new RangeError(`cannot copy ${remaining} values`)
if (remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${remaining} values`)
if (remaining === 0 || decoder.done) return
// Copy any null values, and the first non-null value, so that appendValue() computes the
// difference between the encoder's last value and the decoder's first (absolute) value.
let value = decoder.readValue(),
nulls = 0
let value = decoder.readValue(), nulls = 0
this.appendValue(value)
if (value === null) {
nulls = decoder.count + 1
if (remaining !== undefined && remaining < nulls) nulls = remaining
decoder.count -= nulls - 1
this.count += nulls - 1
if (remaining > nulls && decoder.done)
throw new RangeError(`cannot copy ${remaining} values`)
if (remaining > nulls && decoder.done) throw new RangeError(`cannot copy ${remaining} values`)
if (remaining === nulls || decoder.done) return
// The next value read is certain to be non-null because we're not at the end of the decoder,
@ -1084,10 +989,7 @@ class DeltaEncoder extends RLEEncoder {
// value, while subsequent values are relative. Thus, the sum of all of the (non-null) copied
// values must equal the absolute value of the final element copied.
if (remaining !== undefined) remaining -= nulls + 1
const { nonNullValues, sum } = super.copyFrom(decoder, {
count: remaining,
sumValues: true,
})
const { nonNullValues, sum } = super.copyFrom(decoder, {count: remaining, sumValues: true})
if (nonNullValues > 0) {
this.absoluteValue = sum
decoder.absoluteValue = sum
@ -1101,7 +1003,7 @@ class DeltaEncoder extends RLEEncoder {
*/
class DeltaDecoder extends RLEDecoder {
constructor(buffer) {
super("int", buffer)
super('int', buffer)
this.absoluteValue = 0
}
@ -1134,12 +1036,12 @@ class DeltaDecoder extends RLEDecoder {
while (numSkip > 0 && !this.done) {
if (this.count === 0) this.readRecord()
const consume = Math.min(numSkip, this.count)
if (this.state === "literal") {
if (this.state === 'literal') {
for (let i = 0; i < consume; i++) {
this.lastValue = this.readRawValue()
this.absoluteValue += this.lastValue
}
} else if (this.state === "repetition") {
} else if (this.state === 'repetition') {
this.absoluteValue += consume * this.lastValue
}
numSkip -= consume
@ -1188,13 +1090,12 @@ class BooleanEncoder extends Encoder {
*/
copyFrom(decoder, options = {}) {
if (!(decoder instanceof BooleanDecoder)) {
throw new TypeError("incompatible type of decoder")
throw new TypeError('incompatible type of decoder')
}
const { count } = options
let remaining = typeof count === "number" ? count : Number.MAX_SAFE_INTEGER
if (count && remaining > 0 && decoder.done)
throw new RangeError(`cannot copy ${count} values`)
let remaining = (typeof count === 'number' ? count : Number.MAX_SAFE_INTEGER)
if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)
if (remaining === 0 || decoder.done) return
// Copy one value to bring decoder and encoder state into sync, then finish that value's repetitions
@ -1207,8 +1108,7 @@ class BooleanEncoder extends Encoder {
while (remaining > 0 && !decoder.done) {
decoder.count = decoder.readUint53()
if (decoder.count === 0)
throw new RangeError("Zero-length runs are not allowed")
if (decoder.count === 0) throw new RangeError('Zero-length runs are not allowed')
decoder.lastValue = !decoder.lastValue
this.appendUint53(this.count)
@ -1219,8 +1119,7 @@ class BooleanEncoder extends Encoder {
remaining -= numCopied
}
if (count && remaining > 0 && decoder.done)
throw new RangeError(`cannot copy ${count} values`)
if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)
}
/**
@ -1252,7 +1151,7 @@ class BooleanDecoder extends Decoder {
* position, and true if we are at the end of the buffer.
*/
get done() {
return this.count === 0 && this.offset === this.buf.byteLength
return (this.count === 0) && (this.offset === this.buf.byteLength)
}
/**
@ -1275,7 +1174,7 @@ class BooleanDecoder extends Decoder {
this.count = this.readUint53()
this.lastValue = !this.lastValue
if (this.count === 0 && !this.firstRun) {
throw new RangeError("Zero-length runs are not allowed")
throw new RangeError('Zero-length runs are not allowed')
}
this.firstRun = false
}
@ -1291,8 +1190,7 @@ class BooleanDecoder extends Decoder {
if (this.count === 0) {
this.count = this.readUint53()
this.lastValue = !this.lastValue
if (this.count === 0)
throw new RangeError("Zero-length runs are not allowed")
if (this.count === 0) throw new RangeError('Zero-length runs are not allowed')
}
if (this.count < numSkip) {
numSkip -= this.count
@ -1306,16 +1204,6 @@ class BooleanDecoder extends Decoder {
}
module.exports = {
stringToUtf8,
utf8ToString,
hexStringToBytes,
bytesToHexString,
Encoder,
Decoder,
RLEEncoder,
RLEDecoder,
DeltaEncoder,
DeltaDecoder,
BooleanEncoder,
BooleanDecoder,
stringToUtf8, utf8ToString, hexStringToBytes, bytesToHexString,
Encoder, Decoder, RLEEncoder, RLEDecoder, DeltaEncoder, DeltaDecoder, BooleanEncoder, BooleanDecoder
}

View file

@ -17,14 +17,9 @@
*/
const Backend = null //require('./backend')
const {
hexStringToBytes,
bytesToHexString,
Encoder,
Decoder,
} = require("./encoding")
const { decodeChangeMeta } = require("./columnar")
const { copyObject } = require("./common")
const { hexStringToBytes, bytesToHexString, Encoder, Decoder } = require('./encoding')
const { decodeChangeMeta } = require('./columnar')
const { copyObject } = require('./common')
const HASH_SIZE = 32 // 256 bits = 32 bytes
const MESSAGE_TYPE_SYNC = 0x42 // first byte of a sync message, for identification
@ -33,8 +28,7 @@ const PEER_STATE_TYPE = 0x43 // first byte of an encoded peer state, for identif
// These constants correspond to a 1% false positive rate. The values can be changed without
// breaking compatibility of the network protocol, since the parameters used for a particular
// Bloom filter are encoded in the wire format.
const BITS_PER_ENTRY = 10,
NUM_PROBES = 7
const BITS_PER_ENTRY = 10, NUM_PROBES = 7
/**
* A Bloom filter implementation that can be serialised to a byte array for transmission
@ -42,15 +36,13 @@ const BITS_PER_ENTRY = 10,
* so this implementation does not perform its own hashing.
*/
class BloomFilter {
constructor(arg) {
constructor (arg) {
if (Array.isArray(arg)) {
// arg is an array of SHA256 hashes in hexadecimal encoding
this.numEntries = arg.length
this.numBitsPerEntry = BITS_PER_ENTRY
this.numProbes = NUM_PROBES
this.bits = new Uint8Array(
Math.ceil((this.numEntries * this.numBitsPerEntry) / 8)
)
this.bits = new Uint8Array(Math.ceil(this.numEntries * this.numBitsPerEntry / 8))
for (let hash of arg) this.addHash(hash)
} else if (arg instanceof Uint8Array) {
if (arg.byteLength === 0) {
@ -63,12 +55,10 @@ class BloomFilter {
this.numEntries = decoder.readUint32()
this.numBitsPerEntry = decoder.readUint32()
this.numProbes = decoder.readUint32()
this.bits = decoder.readRawBytes(
Math.ceil((this.numEntries * this.numBitsPerEntry) / 8)
)
this.bits = decoder.readRawBytes(Math.ceil(this.numEntries * this.numBitsPerEntry / 8))
}
} else {
throw new TypeError("invalid argument")
throw new TypeError('invalid argument')
}
}
@ -96,32 +86,12 @@ class BloomFilter {
* http://www.ccis.northeastern.edu/home/pete/pub/bloom-filters-verification.pdf
*/
getProbes(hash) {
const hashBytes = hexStringToBytes(hash),
modulo = 8 * this.bits.byteLength
if (hashBytes.byteLength !== 32)
throw new RangeError(`Not a 256-bit hash: ${hash}`)
const hashBytes = hexStringToBytes(hash), modulo = 8 * this.bits.byteLength
if (hashBytes.byteLength !== 32) throw new RangeError(`Not a 256-bit hash: ${hash}`)
// on the next three lines, the right shift means interpret value as unsigned
let x =
((hashBytes[0] |
(hashBytes[1] << 8) |
(hashBytes[2] << 16) |
(hashBytes[3] << 24)) >>>
0) %
modulo
let y =
((hashBytes[4] |
(hashBytes[5] << 8) |
(hashBytes[6] << 16) |
(hashBytes[7] << 24)) >>>
0) %
modulo
let z =
((hashBytes[8] |
(hashBytes[9] << 8) |
(hashBytes[10] << 16) |
(hashBytes[11] << 24)) >>>
0) %
modulo
let x = ((hashBytes[0] | hashBytes[1] << 8 | hashBytes[2] << 16 | hashBytes[3] << 24) >>> 0) % modulo
let y = ((hashBytes[4] | hashBytes[5] << 8 | hashBytes[6] << 16 | hashBytes[7] << 24) >>> 0) % modulo
let z = ((hashBytes[8] | hashBytes[9] << 8 | hashBytes[10] << 16 | hashBytes[11] << 24) >>> 0) % modulo
const probes = [x]
for (let i = 1; i < this.numProbes; i++) {
x = (x + y) % modulo
@ -158,14 +128,12 @@ class BloomFilter {
* Encodes a sorted array of SHA-256 hashes (as hexadecimal strings) into a byte array.
*/
function encodeHashes(encoder, hashes) {
if (!Array.isArray(hashes)) throw new TypeError("hashes must be an array")
if (!Array.isArray(hashes)) throw new TypeError('hashes must be an array')
encoder.appendUint32(hashes.length)
for (let i = 0; i < hashes.length; i++) {
if (i > 0 && hashes[i - 1] >= hashes[i])
throw new RangeError("hashes must be sorted")
if (i > 0 && hashes[i - 1] >= hashes[i]) throw new RangeError('hashes must be sorted')
const bytes = hexStringToBytes(hashes[i])
if (bytes.byteLength !== HASH_SIZE)
throw new TypeError("heads hashes must be 256 bits")
if (bytes.byteLength !== HASH_SIZE) throw new TypeError('heads hashes must be 256 bits')
encoder.appendRawBytes(bytes)
}
}
@ -175,8 +143,7 @@ function encodeHashes(encoder, hashes) {
* array of hex strings.
*/
function decodeHashes(decoder) {
let length = decoder.readUint32(),
hashes = []
let length = decoder.readUint32(), hashes = []
for (let i = 0; i < length; i++) {
hashes.push(bytesToHexString(decoder.readRawBytes(HASH_SIZE)))
}
@ -216,11 +183,11 @@ function decodeSyncMessage(bytes) {
const heads = decodeHashes(decoder)
const need = decodeHashes(decoder)
const haveCount = decoder.readUint32()
let message = { heads, need, have: [], changes: [] }
let message = {heads, need, have: [], changes: []}
for (let i = 0; i < haveCount; i++) {
const lastSync = decodeHashes(decoder)
const bloom = decoder.readPrefixedBytes(decoder)
message.have.push({ lastSync, bloom })
message.have.push({lastSync, bloom})
}
const changeCount = decoder.readUint32()
for (let i = 0; i < changeCount; i++) {
@ -267,7 +234,7 @@ function decodeSyncState(bytes) {
function makeBloomFilter(backend, lastSync) {
const newChanges = Backend.getChanges(backend, lastSync)
const hashes = newChanges.map(change => decodeChangeMeta(change, true).hash)
return { lastSync, bloom: new BloomFilter(hashes).bytes }
return {lastSync, bloom: new BloomFilter(hashes).bytes}
}
/**
@ -278,26 +245,20 @@ function makeBloomFilter(backend, lastSync) {
*/
function getChangesToSend(backend, have, need) {
if (have.length === 0) {
return need
.map(hash => Backend.getChangeByHash(backend, hash))
.filter(change => change !== undefined)
return need.map(hash => Backend.getChangeByHash(backend, hash)).filter(change => change !== undefined)
}
let lastSyncHashes = {},
bloomFilters = []
let lastSyncHashes = {}, bloomFilters = []
for (let h of have) {
for (let hash of h.lastSync) lastSyncHashes[hash] = true
bloomFilters.push(new BloomFilter(h.bloom))
}
// Get all changes that were added since the last sync
const changes = Backend.getChanges(backend, Object.keys(lastSyncHashes)).map(
change => decodeChangeMeta(change, true)
)
const changes = Backend.getChanges(backend, Object.keys(lastSyncHashes))
.map(change => decodeChangeMeta(change, true))
let changeHashes = {},
dependents = {},
hashesToSend = {}
let changeHashes = {}, dependents = {}, hashesToSend = {}
for (let change of changes) {
changeHashes[change.hash] = true
@ -331,8 +292,7 @@ function getChangesToSend(backend, have, need) {
let changesToSend = []
for (let hash of need) {
hashesToSend[hash] = true
if (!changeHashes[hash]) {
// Change is not among those returned by getMissingChanges()?
if (!changeHashes[hash]) { // Change is not among those returned by getMissingChanges()?
const change = Backend.getChangeByHash(backend, hash)
if (change) changesToSend.push(change)
}
@ -357,7 +317,7 @@ function initSyncState() {
}
function compareArrays(a, b) {
return a.length === b.length && a.every((v, i) => v === b[i])
return (a.length === b.length) && a.every((v, i) => v === b[i])
}
/**
@ -369,19 +329,10 @@ function generateSyncMessage(backend, syncState) {
throw new Error("generateSyncMessage called with no Automerge document")
}
if (!syncState) {
throw new Error(
"generateSyncMessage requires a syncState, which can be created with initSyncState()"
)
throw new Error("generateSyncMessage requires a syncState, which can be created with initSyncState()")
}
let {
sharedHeads,
lastSentHeads,
theirHeads,
theirNeed,
theirHave,
sentHashes,
} = syncState
let { sharedHeads, lastSentHeads, theirHeads, theirNeed, theirHave, sentHashes } = syncState
const ourHeads = Backend.getHeads(backend)
// Hashes to explicitly request from the remote peer: any missing dependencies of unapplied
@ -405,28 +356,18 @@ function generateSyncMessage(backend, syncState) {
const lastSync = theirHave[0].lastSync
if (!lastSync.every(hash => Backend.getChangeByHash(backend, hash))) {
// we need to queue them to send us a fresh sync message, the one they sent is uninteligible so we don't know what they need
const resetMsg = {
heads: ourHeads,
need: [],
have: [{ lastSync: [], bloom: new Uint8Array(0) }],
changes: [],
}
const resetMsg = {heads: ourHeads, need: [], have: [{ lastSync: [], bloom: new Uint8Array(0) }], changes: []}
return [syncState, encodeSyncMessage(resetMsg)]
}
}
// XXX: we should limit ourselves to only sending a subset of all the messages, probably limited by a total message size
// these changes should ideally be RLE encoded but we haven't implemented that yet.
let changesToSend =
Array.isArray(theirHave) && Array.isArray(theirNeed)
? getChangesToSend(backend, theirHave, theirNeed)
: []
let changesToSend = Array.isArray(theirHave) && Array.isArray(theirNeed) ? getChangesToSend(backend, theirHave, theirNeed) : []
// If the heads are equal, we're in sync and don't need to do anything further
const headsUnchanged =
Array.isArray(lastSentHeads) && compareArrays(ourHeads, lastSentHeads)
const headsEqual =
Array.isArray(theirHeads) && compareArrays(ourHeads, theirHeads)
const headsUnchanged = Array.isArray(lastSentHeads) && compareArrays(ourHeads, lastSentHeads)
const headsEqual = Array.isArray(theirHeads) && compareArrays(ourHeads, theirHeads)
if (headsUnchanged && headsEqual && changesToSend.length === 0) {
// no need to send a sync message if we know we're synced!
return [syncState, null]
@ -434,19 +375,12 @@ function generateSyncMessage(backend, syncState) {
// TODO: this recomputes the SHA-256 hash of each change; we should restructure this to avoid the
// unnecessary recomputation
changesToSend = changesToSend.filter(
change => !sentHashes[decodeChangeMeta(change, true).hash]
)
changesToSend = changesToSend.filter(change => !sentHashes[decodeChangeMeta(change, true).hash])
// Regular response to a sync message: send any changes that the other node
// doesn't have. We leave the "have" field empty because the previous message
// generated by `syncStart` already indicated what changes we have.
const syncMessage = {
heads: ourHeads,
have: ourHave,
need: ourNeed,
changes: changesToSend,
}
const syncMessage = {heads: ourHeads, have: ourHave, need: ourNeed, changes: changesToSend}
if (changesToSend.length > 0) {
sentHashes = copyObject(sentHashes)
for (const change of changesToSend) {
@ -454,10 +388,7 @@ function generateSyncMessage(backend, syncState) {
}
}
syncState = Object.assign({}, syncState, {
lastSentHeads: ourHeads,
sentHashes,
})
syncState = Object.assign({}, syncState, {lastSentHeads: ourHeads, sentHashes})
return [syncState, encodeSyncMessage(syncMessage)]
}
@ -475,14 +406,13 @@ function generateSyncMessage(backend, syncState) {
* another peer, that means that peer had those changes, and therefore we now both know about them.
*/
function advanceHeads(myOldHeads, myNewHeads, ourOldSharedHeads) {
const newHeads = myNewHeads.filter(head => !myOldHeads.includes(head))
const commonHeads = ourOldSharedHeads.filter(head =>
myNewHeads.includes(head)
)
const newHeads = myNewHeads.filter((head) => !myOldHeads.includes(head))
const commonHeads = ourOldSharedHeads.filter((head) => myNewHeads.includes(head))
const advancedHeads = [...new Set([...newHeads, ...commonHeads])].sort()
return advancedHeads
}
/**
* Given a backend, a message message and the state of our peer, apply any changes, update what
* we believe about the peer, and (if there were applied changes) produce a patch for the frontend
@ -492,13 +422,10 @@ function receiveSyncMessage(backend, oldSyncState, binaryMessage) {
throw new Error("generateSyncMessage called with no Automerge document")
}
if (!oldSyncState) {
throw new Error(
"generateSyncMessage requires a syncState, which can be created with initSyncState()"
)
throw new Error("generateSyncMessage requires a syncState, which can be created with initSyncState()")
}
let { sharedHeads, lastSentHeads, sentHashes } = oldSyncState,
patch = null
let { sharedHeads, lastSentHeads, sentHashes } = oldSyncState, patch = null
const message = decodeSyncMessage(binaryMessage)
const beforeHeads = Backend.getHeads(backend)
@ -507,27 +434,18 @@ function receiveSyncMessage(backend, oldSyncState, binaryMessage) {
// changes without applying them. The set of changes may also be incomplete if the sender decided
// to break a large set of changes into chunks.
if (message.changes.length > 0) {
;[backend, patch] = Backend.applyChanges(backend, message.changes)
sharedHeads = advanceHeads(
beforeHeads,
Backend.getHeads(backend),
sharedHeads
)
[backend, patch] = Backend.applyChanges(backend, message.changes)
sharedHeads = advanceHeads(beforeHeads, Backend.getHeads(backend), sharedHeads)
}
// If heads are equal, indicate we don't need to send a response message
if (
message.changes.length === 0 &&
compareArrays(message.heads, beforeHeads)
) {
if (message.changes.length === 0 && compareArrays(message.heads, beforeHeads)) {
lastSentHeads = message.heads
}
// If all of the remote heads are known to us, that means either our heads are equal, or we are
// ahead of the remote peer. In this case, take the remote heads to be our shared heads.
const knownHeads = message.heads.filter(head =>
Backend.getChangeByHash(backend, head)
)
const knownHeads = message.heads.filter(head => Backend.getChangeByHash(backend, head))
if (knownHeads.length === message.heads.length) {
sharedHeads = message.heads
// If the remote peer has lost all its data, reset our state to perform a full resync
@ -549,18 +467,14 @@ function receiveSyncMessage(backend, oldSyncState, binaryMessage) {
theirHave: message.have, // the information we need to calculate the changes they need
theirHeads: message.heads,
theirNeed: message.need,
sentHashes,
sentHashes
}
return [backend, syncState, patch]
}
module.exports = {
receiveSyncMessage,
generateSyncMessage,
encodeSyncMessage,
decodeSyncMessage,
initSyncState,
encodeSyncState,
decodeSyncState,
BloomFilter, // BloomFilter is a private API, exported only for testing purposes
receiveSyncMessage, generateSyncMessage,
encodeSyncMessage, decodeSyncMessage,
initSyncState, encodeSyncState, decodeSyncState,
BloomFilter // BloomFilter is a private API, exported only for testing purposes
}

File diff suppressed because it is too large Load diff

View file

@ -1,99 +0,0 @@
import * as assert from "assert"
import * as stable from "../src"
import { unstable } from "../src"
describe("stable/unstable interop", () => {
it("should allow reading Text from stable as strings in unstable", () => {
let stableDoc = stable.from({
text: new stable.Text("abc"),
})
let unstableDoc = unstable.init<any>()
unstableDoc = unstable.merge(unstableDoc, stableDoc)
assert.deepStrictEqual(unstableDoc.text, "abc")
})
it("should allow string from stable as Text in unstable", () => {
let unstableDoc = unstable.from({
text: "abc",
})
let stableDoc = stable.init<any>()
stableDoc = unstable.merge(stableDoc, unstableDoc)
assert.deepStrictEqual(stableDoc.text, new stable.Text("abc"))
})
it("should allow reading strings from stable as RawString in unstable", () => {
let stableDoc = stable.from({
text: "abc",
})
let unstableDoc = unstable.init<any>()
unstableDoc = unstable.merge(unstableDoc, stableDoc)
assert.deepStrictEqual(unstableDoc.text, new unstable.RawString("abc"))
})
it("should allow reading RawString from unstable as string in stable", () => {
let unstableDoc = unstable.from({
text: new unstable.RawString("abc"),
})
let stableDoc = stable.init<any>()
stableDoc = unstable.merge(stableDoc, unstableDoc)
assert.deepStrictEqual(stableDoc.text, "abc")
})
it("should show conflicts on text objects", () => {
let doc1 = stable.from({ text: new stable.Text("abc") }, "bb")
let doc2 = stable.from({ text: new stable.Text("def") }, "aa")
doc1 = stable.merge(doc1, doc2)
let conflicts = stable.getConflicts(doc1, "text")!
assert.equal(conflicts["1@bb"]!.toString(), "abc")
assert.equal(conflicts["1@aa"]!.toString(), "def")
let unstableDoc = unstable.init<any>()
unstableDoc = unstable.merge(unstableDoc, doc1)
let conflicts2 = unstable.getConflicts(unstableDoc, "text")!
assert.equal(conflicts2["1@bb"]!.toString(), "abc")
assert.equal(conflicts2["1@aa"]!.toString(), "def")
})
it("should allow filling a list with text in stable", () => {
let doc = stable.from<{ list: Array<stable.Text | null> }>({
list: [null, null, null],
})
doc = stable.change(doc, doc => {
doc.list.fill(new stable.Text("abc"), 0, 3)
})
assert.deepStrictEqual(doc.list, [
new stable.Text("abc"),
new stable.Text("abc"),
new stable.Text("abc"),
])
})
it("should allow filling a list with text in unstable", () => {
let doc = unstable.from<{ list: Array<string | null> }>({
list: [null, null, null],
})
doc = stable.change(doc, doc => {
doc.list.fill("abc", 0, 3)
})
assert.deepStrictEqual(doc.list, ["abc", "abc", "abc"])
})
it("should allow splicing text into a list on stable", () => {
let doc = stable.from<{ list: Array<stable.Text> }>({ list: [] })
doc = stable.change(doc, doc => {
doc.list.splice(0, 0, new stable.Text("abc"), new stable.Text("def"))
})
assert.deepStrictEqual(doc.list, [
new stable.Text("abc"),
new stable.Text("def"),
])
})
it("should allow splicing text into a list on unstable", () => {
let doc = unstable.from<{ list: Array<string> }>({ list: [] })
doc = unstable.change(doc, doc => {
doc.list.splice(0, 0, "abc", "def")
})
assert.deepStrictEqual(doc.list, ["abc", "def"])
})
})

File diff suppressed because it is too large Load diff

View file

@ -1,111 +1,698 @@
import * as assert from "assert"
import { unstable as Automerge } from "../src"
import { assertEqualsOneOf } from "./helpers"
import * as assert from 'assert'
import * as Automerge from '../src'
import { assertEqualsOneOf } from './helpers'
type DocType = {
text: string
[key: string]: any
function attributeStateToAttributes(accumulatedAttributes) {
const attributes = {}
Object.entries(accumulatedAttributes).forEach(([key, values]) => {
if (values.length && values[0] !== null) {
attributes[key] = values[0]
}
})
return attributes
}
describe("Automerge.Text", () => {
let s1: Automerge.Doc<DocType>, s2: Automerge.Doc<DocType>
beforeEach(() => {
s1 = Automerge.change(Automerge.init<DocType>(), doc => (doc.text = ""))
s2 = Automerge.merge(Automerge.init<DocType>(), s1)
function isEquivalent(a, b) {
const aProps = Object.getOwnPropertyNames(a)
const bProps = Object.getOwnPropertyNames(b)
if (aProps.length != bProps.length) {
return false
}
for (let i = 0; i < aProps.length; i++) {
const propName = aProps[i]
if (a[propName] !== b[propName]) {
return false
}
}
return true
}
function isControlMarker(pseudoCharacter) {
return typeof pseudoCharacter === 'object' && pseudoCharacter.attributes
}
function opFrom(text, attributes) {
let op = { insert: text }
if (Object.keys(attributes).length > 0) {
op.attributes = attributes
}
return op
}
function accumulateAttributes(span, accumulatedAttributes) {
Object.entries(span).forEach(([key, value]) => {
if (!accumulatedAttributes[key]) {
accumulatedAttributes[key] = []
}
if (value === null) {
if (accumulatedAttributes[key].length === 0 || accumulatedAttributes[key] === null) {
accumulatedAttributes[key].unshift(null)
} else {
accumulatedAttributes[key].shift()
}
} else {
if (accumulatedAttributes[key][0] === null) {
accumulatedAttributes[key].shift()
} else {
accumulatedAttributes[key].unshift(value)
}
}
})
return accumulatedAttributes
}
function automergeTextToDeltaDoc(text) {
let ops = []
let controlState = {}
let currentString = ""
let attributes = {}
text.toSpans().forEach((span) => {
if (isControlMarker(span)) {
controlState = accumulateAttributes(span.attributes, controlState)
} else {
let next = attributeStateToAttributes(controlState)
// if the next span has the same calculated attributes as the current span
// don't bother outputting it as a separate span, just let it ride
if (typeof span === 'string' && isEquivalent(next, attributes)) {
currentString = currentString + span
return
}
if (currentString) {
ops.push(opFrom(currentString, attributes))
}
// If we've got a string, we might be able to concatenate it to another
// same-attributed-string, so remember it and go to the next iteration.
if (typeof span === 'string') {
currentString = span
attributes = next
} else {
// otherwise we have an embed "character" and should output it immediately.
// embeds are always one-"character" in length.
ops.push(opFrom(span, next))
currentString = ''
attributes = {}
}
}
})
it("should support insertion", () => {
s1 = Automerge.change(s1, doc => Automerge.splice(doc, "text", 0, 0, "a"))
// at the end, flush any accumulated string out
if (currentString) {
ops.push(opFrom(currentString, attributes))
}
return ops
}
function inverseAttributes(attributes) {
let invertedAttributes = {}
Object.keys(attributes).forEach((key) => {
invertedAttributes[key] = null
})
return invertedAttributes
}
function applyDeleteOp(text, offset, op) {
let length = op.delete
while (length > 0) {
if (isControlMarker(text.get(offset))) {
offset += 1
} else {
// we need to not delete control characters, but we do delete embed characters
text.deleteAt(offset, 1)
length -= 1
}
}
return [text, offset]
}
function applyRetainOp(text, offset, op) {
let length = op.retain
if (op.attributes) {
text.insertAt(offset, { attributes: op.attributes })
offset += 1
}
while (length > 0) {
const char = text.get(offset)
offset += 1
if (!isControlMarker(char)) {
length -= 1
}
}
if (op.attributes) {
text.insertAt(offset, { attributes: inverseAttributes(op.attributes) })
offset += 1
}
return [text, offset]
}
function applyInsertOp(text, offset, op) {
let originalOffset = offset
if (typeof op.insert === 'string') {
text.insertAt(offset, ...op.insert.split(''))
offset += op.insert.length
} else {
// we have an embed or something similar
text.insertAt(offset, op.insert)
offset += 1
}
if (op.attributes) {
text.insertAt(originalOffset, { attributes: op.attributes })
offset += 1
}
if (op.attributes) {
text.insertAt(offset, { attributes: inverseAttributes(op.attributes) })
offset += 1
}
return [text, offset]
}
// XXX: uhhhhh, why can't I pass in text?
function applyDeltaDocToAutomergeText(delta, doc) {
let offset = 0
delta.forEach(op => {
if (op.retain) {
[, offset] = applyRetainOp(doc.text, offset, op)
} else if (op.delete) {
[, offset] = applyDeleteOp(doc.text, offset, op)
} else if (op.insert) {
[, offset] = applyInsertOp(doc.text, offset, op)
}
})
}
describe('Automerge.Text', () => {
let s1, s2
beforeEach(() => {
s1 = Automerge.change(Automerge.init(), doc => doc.text = new Automerge.Text())
s2 = Automerge.merge(Automerge.init(), s1)
})
it('should support insertion', () => {
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, 'a'))
assert.strictEqual(s1.text.length, 1)
assert.strictEqual(s1.text[0], "a")
assert.strictEqual(s1.text, "a")
assert.strictEqual(s1.text.get(0), 'a')
assert.strictEqual(s1.text.toString(), 'a')
//assert.strictEqual(s1.text.getElemId(0), `2@${Automerge.getActorId(s1)}`)
})
it("should support deletion", () => {
s1 = Automerge.change(s1, doc => Automerge.splice(doc, "text", 0, 0, "abc"))
s1 = Automerge.change(s1, doc => Automerge.splice(doc, "text", 1, 1))
it('should support deletion', () => {
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, 'a', 'b', 'c'))
s1 = Automerge.change(s1, doc => doc.text.deleteAt(1, 1))
assert.strictEqual(s1.text.length, 2)
assert.strictEqual(s1.text[0], "a")
assert.strictEqual(s1.text[1], "c")
assert.strictEqual(s1.text, "ac")
assert.strictEqual(s1.text.get(0), 'a')
assert.strictEqual(s1.text.get(1), 'c')
assert.strictEqual(s1.text.toString(), 'ac')
})
it("should support implicit and explicit deletion", () => {
s1 = Automerge.change(s1, doc => Automerge.splice(doc, "text", 0, 0, "abc"))
s1 = Automerge.change(s1, doc => Automerge.splice(doc, "text", 1, 1))
s1 = Automerge.change(s1, doc => Automerge.splice(doc, "text", 1, 0))
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, "a", "b", "c"))
s1 = Automerge.change(s1, doc => doc.text.deleteAt(1))
s1 = Automerge.change(s1, doc => doc.text.deleteAt(1, 0))
assert.strictEqual(s1.text.length, 2)
assert.strictEqual(s1.text[0], "a")
assert.strictEqual(s1.text[1], "c")
assert.strictEqual(s1.text, "ac")
assert.strictEqual(s1.text.get(0), "a")
assert.strictEqual(s1.text.get(1), "c")
assert.strictEqual(s1.text.toString(), "ac")
})
it("should handle concurrent insertion", () => {
s1 = Automerge.change(s1, doc => Automerge.splice(doc, "text", 0, 0, "abc"))
s2 = Automerge.change(s2, doc => Automerge.splice(doc, "text", 0, 0, "xyz"))
it('should handle concurrent insertion', () => {
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, 'a', 'b', 'c'))
s2 = Automerge.change(s2, doc => doc.text.insertAt(0, 'x', 'y', 'z'))
s1 = Automerge.merge(s1, s2)
assert.strictEqual(s1.text.length, 6)
assertEqualsOneOf(s1.text, "abcxyz", "xyzabc")
assertEqualsOneOf(s1.text.toString(), 'abcxyz', 'xyzabc')
assertEqualsOneOf(s1.text.join(''), 'abcxyz', 'xyzabc')
})
it("should handle text and other ops in the same change", () => {
it('should handle text and other ops in the same change', () => {
s1 = Automerge.change(s1, doc => {
doc.foo = "bar"
Automerge.splice(doc, "text", 0, 0, "a")
doc.foo = 'bar'
doc.text.insertAt(0, 'a')
})
assert.strictEqual(s1.foo, "bar")
assert.strictEqual(s1.text, "a")
assert.strictEqual(s1.text, "a")
assert.strictEqual(s1.foo, 'bar')
assert.strictEqual(s1.text.toString(), 'a')
assert.strictEqual(s1.text.join(''), 'a')
})
it("should serialize to JSON as a simple string", () => {
s1 = Automerge.change(s1, doc => Automerge.splice(doc, "text", 0, 0, 'a"b'))
it('should serialize to JSON as a simple string', () => {
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, 'a', '"', 'b'))
assert.strictEqual(JSON.stringify(s1), '{"text":"a\\"b"}')
})
it("should allow modification after an object is assigned to a document", () => {
it('should allow modification before an object is assigned to a document', () => {
s1 = Automerge.change(Automerge.init(), doc => {
doc.text = ""
Automerge.splice(doc, "text", 0, 0, "abcd")
Automerge.splice(doc, "text", 2, 1)
assert.strictEqual(doc.text, "abd")
const text = new Automerge.Text()
text.insertAt(0, 'a', 'b', 'c', 'd')
text.deleteAt(2)
doc.text = text
assert.strictEqual(doc.text.toString(), 'abd')
assert.strictEqual(doc.text.join(''), 'abd')
})
assert.strictEqual(s1.text, "abd")
assert.strictEqual(s1.text.toString(), 'abd')
assert.strictEqual(s1.text.join(''), 'abd')
})
it("should not allow modification outside of a change callback", () => {
assert.throws(
() => Automerge.splice(s1, "text", 0, 0, "a"),
/object cannot be modified outside of a change block/
)
it('should allow modification after an object is assigned to a document', () => {
s1 = Automerge.change(Automerge.init(), doc => {
const text = new Automerge.Text()
doc.text = text
doc.text.insertAt(0, 'a', 'b', 'c', 'd')
doc.text.deleteAt(2)
assert.strictEqual(doc.text.toString(), 'abd')
assert.strictEqual(doc.text.join(''), 'abd')
})
assert.strictEqual(s1.text.join(''), 'abd')
})
describe("with initial value", () => {
it("should initialize text in Automerge.from()", () => {
let s1 = Automerge.from({ text: "init" })
it('should not allow modification outside of a change callback', () => {
assert.throws(() => s1.text.insertAt(0, 'a'), /object cannot be modified outside of a change block/)
})
describe('with initial value', () => {
it('should accept a string as initial value', () => {
let s1 = Automerge.change(Automerge.init(), doc => doc.text = new Automerge.Text('init'))
assert.strictEqual(s1.text.length, 4)
assert.strictEqual(s1.text[0], "i")
assert.strictEqual(s1.text[1], "n")
assert.strictEqual(s1.text[2], "i")
assert.strictEqual(s1.text[3], "t")
assert.strictEqual(s1.text, "init")
assert.strictEqual(s1.text.get(0), 'i')
assert.strictEqual(s1.text.get(1), 'n')
assert.strictEqual(s1.text.get(2), 'i')
assert.strictEqual(s1.text.get(3), 't')
assert.strictEqual(s1.text.toString(), 'init')
})
it("should encode the initial value as a change", () => {
const s1 = Automerge.from({ text: "init" })
it('should accept an array as initial value', () => {
let s1 = Automerge.change(Automerge.init(), doc => doc.text = new Automerge.Text(['i', 'n', 'i', 't']))
assert.strictEqual(s1.text.length, 4)
assert.strictEqual(s1.text.get(0), 'i')
assert.strictEqual(s1.text.get(1), 'n')
assert.strictEqual(s1.text.get(2), 'i')
assert.strictEqual(s1.text.get(3), 't')
assert.strictEqual(s1.text.toString(), 'init')
})
it('should initialize text in Automerge.from()', () => {
let s1 = Automerge.from({text: new Automerge.Text('init')})
assert.strictEqual(s1.text.length, 4)
assert.strictEqual(s1.text.get(0), 'i')
assert.strictEqual(s1.text.get(1), 'n')
assert.strictEqual(s1.text.get(2), 'i')
assert.strictEqual(s1.text.get(3), 't')
assert.strictEqual(s1.text.toString(), 'init')
})
it('should encode the initial value as a change', () => {
const s1 = Automerge.from({text: new Automerge.Text('init')})
const changes = Automerge.getAllChanges(s1)
assert.strictEqual(changes.length, 1)
const [s2] = Automerge.applyChanges(Automerge.init<DocType>(), changes)
assert.strictEqual(s2.text, "init")
assert.strictEqual(s2.text, "init")
const [s2] = Automerge.applyChanges(Automerge.init(), changes)
assert.strictEqual(s2.text instanceof Automerge.Text, true)
assert.strictEqual(s2.text.toString(), 'init')
assert.strictEqual(s2.text.join(''), 'init')
})
it('should allow immediate access to the value', () => {
Automerge.change(Automerge.init(), doc => {
const text = new Automerge.Text('init')
assert.strictEqual(text.length, 4)
assert.strictEqual(text.get(0), 'i')
assert.strictEqual(text.toString(), 'init')
doc.text = text
assert.strictEqual(doc.text.length, 4)
assert.strictEqual(doc.text.get(0), 'i')
assert.strictEqual(doc.text.toString(), 'init')
})
})
it('should allow pre-assignment modification of the initial value', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
const text = new Automerge.Text('init')
text.deleteAt(3)
assert.strictEqual(text.join(''), 'ini')
doc.text = text
assert.strictEqual(doc.text.join(''), 'ini')
assert.strictEqual(doc.text.toString(), 'ini')
})
assert.strictEqual(s1.text.toString(), 'ini')
assert.strictEqual(s1.text.join(''), 'ini')
})
it('should allow post-assignment modification of the initial value', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
const text = new Automerge.Text('init')
doc.text = text
doc.text.deleteAt(0)
doc.text.insertAt(0, 'I')
assert.strictEqual(doc.text.join(''), 'Init')
assert.strictEqual(doc.text.toString(), 'Init')
})
assert.strictEqual(s1.text.join(''), 'Init')
assert.strictEqual(s1.text.toString(), 'Init')
})
})
it("should support unicode when creating text", () => {
s1 = Automerge.from({
text: "🐦",
describe('non-textual control characters', () => {
let s1
beforeEach(() => {
s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text()
doc.text.insertAt(0, 'a')
doc.text.insertAt(1, { attribute: 'bold' })
})
})
assert.strictEqual(s1.text, "🐦")
it('should allow fetching non-textual characters', () => {
assert.deepEqual(s1.text.get(1), { attribute: 'bold' })
//assert.strictEqual(s1.text.getElemId(1), `3@${Automerge.getActorId(s1)}`)
})
it('should include control characters in string length', () => {
assert.strictEqual(s1.text.length, 2)
assert.strictEqual(s1.text.get(0), 'a')
})
it('should replace control characters from toString()', () => {
assert.strictEqual(s1.text.toString(), 'a\uFFFC')
})
it('should allow control characters to be updated', () => {
const s2 = Automerge.change(s1, doc => doc.text.get(1).attribute = 'italic')
const s3 = Automerge.load(Automerge.save(s2))
assert.strictEqual(s1.text.get(1).attribute, 'bold')
assert.strictEqual(s2.text.get(1).attribute, 'italic')
assert.strictEqual(s3.text.get(1).attribute, 'italic')
})
describe('spans interface to Text', () => {
it('should return a simple string as a single span', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('hello world')
})
assert.deepEqual(s1.text.toSpans(), ['hello world'])
})
it('should return an empty string as an empty array', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text()
})
assert.deepEqual(s1.text.toSpans(), [])
})
it('should split a span at a control character', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('hello world')
doc.text.insertAt(5, { attributes: { bold: true } })
})
assert.deepEqual(s1.text.toSpans(),
['hello', { attributes: { bold: true } }, ' world'])
})
it('should allow consecutive control characters', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('hello world')
doc.text.insertAt(5, { attributes: { bold: true } })
doc.text.insertAt(6, { attributes: { italic: true } })
})
assert.deepEqual(s1.text.toSpans(),
['hello',
{ attributes: { bold: true } },
{ attributes: { italic: true } },
' world'
])
})
it('should allow non-consecutive control characters', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('hello world')
doc.text.insertAt(5, { attributes: { bold: true } })
doc.text.insertAt(12, { attributes: { italic: true } })
})
assert.deepEqual(s1.text.toSpans(),
['hello',
{ attributes: { bold: true } },
' world',
{ attributes: { italic: true } }
])
})
it('should be convertable into a Quill delta', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('Gandalf the Grey')
doc.text.insertAt(0, { attributes: { bold: true } })
doc.text.insertAt(7 + 1, { attributes: { bold: null } })
doc.text.insertAt(12 + 2, { attributes: { color: '#cccccc' } })
})
let deltaDoc = automergeTextToDeltaDoc(s1.text)
// From https://quilljs.com/docs/delta/
let expectedDoc = [
{ insert: 'Gandalf', attributes: { bold: true } },
{ insert: ' the ' },
{ insert: 'Grey', attributes: { color: '#cccccc' } }
]
assert.deepEqual(deltaDoc, expectedDoc)
})
it('should support embeds', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('')
doc.text.insertAt(0, { attributes: { link: 'https://quilljs.com' } })
doc.text.insertAt(1, {
image: 'https://quilljs.com/assets/images/icon.png'
})
doc.text.insertAt(2, { attributes: { link: null } })
})
let deltaDoc = automergeTextToDeltaDoc(s1.text)
// From https://quilljs.com/docs/delta/
let expectedDoc = [{
// An image link
insert: {
image: 'https://quilljs.com/assets/images/icon.png'
},
attributes: {
link: 'https://quilljs.com'
}
}]
assert.deepEqual(deltaDoc, expectedDoc)
})
it('should handle concurrent overlapping spans', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('Gandalf the Grey')
})
let s2 = Automerge.merge(Automerge.init(), s1)
let s3 = Automerge.change(s1, doc => {
doc.text.insertAt(8, { attributes: { bold: true } })
doc.text.insertAt(16 + 1, { attributes: { bold: null } })
})
let s4 = Automerge.change(s2, doc => {
doc.text.insertAt(0, { attributes: { bold: true } })
doc.text.insertAt(11 + 1, { attributes: { bold: null } })
})
let merged = Automerge.merge(s3, s4)
let deltaDoc = automergeTextToDeltaDoc(merged.text)
// From https://quilljs.com/docs/delta/
let expectedDoc = [
{ insert: 'Gandalf the Grey', attributes: { bold: true } },
]
assert.deepEqual(deltaDoc, expectedDoc)
})
it('should handle debolding spans', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('Gandalf the Grey')
})
let s2 = Automerge.merge(Automerge.init(), s1)
let s3 = Automerge.change(s1, doc => {
doc.text.insertAt(0, { attributes: { bold: true } })
doc.text.insertAt(16 + 1, { attributes: { bold: null } })
})
let s4 = Automerge.change(s2, doc => {
doc.text.insertAt(8, { attributes: { bold: null } })
doc.text.insertAt(11 + 1, { attributes: { bold: true } })
})
let merged = Automerge.merge(s3, s4)
let deltaDoc = automergeTextToDeltaDoc(merged.text)
// From https://quilljs.com/docs/delta/
let expectedDoc = [
{ insert: 'Gandalf ', attributes: { bold: true } },
{ insert: 'the' },
{ insert: ' Grey', attributes: { bold: true } },
]
assert.deepEqual(deltaDoc, expectedDoc)
})
// xxx: how would this work for colors?
it('should handle destyling across destyled spans', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('Gandalf the Grey')
})
let s2 = Automerge.merge(Automerge.init(), s1)
let s3 = Automerge.change(s1, doc => {
doc.text.insertAt(0, { attributes: { bold: true } })
doc.text.insertAt(16 + 1, { attributes: { bold: null } })
})
let s4 = Automerge.change(s2, doc => {
doc.text.insertAt(8, { attributes: { bold: null } })
doc.text.insertAt(11 + 1, { attributes: { bold: true } })
})
let merged = Automerge.merge(s3, s4)
let final = Automerge.change(merged, doc => {
doc.text.insertAt(3 + 1, { attributes: { bold: null } })
doc.text.insertAt(doc.text.length, { attributes: { bold: true } })
})
let deltaDoc = automergeTextToDeltaDoc(final.text)
// From https://quilljs.com/docs/delta/
let expectedDoc = [
{ insert: 'Gan', attributes: { bold: true } },
{ insert: 'dalf the Grey' },
]
assert.deepEqual(deltaDoc, expectedDoc)
})
it('should apply an insert', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('Hello world')
})
const delta = [
{ retain: 6 },
{ insert: 'reader' },
{ delete: 5 }
]
let s2 = Automerge.change(s1, doc => {
applyDeltaDocToAutomergeText(delta, doc)
})
//assert.strictEqual(s2.text.join(''), 'Hello reader')
assert.strictEqual(s2.text.toString(), 'Hello reader')
})
it('should apply an insert with control characters', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('Hello world')
})
const delta = [
{ retain: 6 },
{ insert: 'reader', attributes: { bold: true } },
{ delete: 5 },
{ insert: '!' }
]
let s2 = Automerge.change(s1, doc => {
applyDeltaDocToAutomergeText(delta, doc)
})
assert.strictEqual(s2.text.toString(), 'Hello \uFFFCreader\uFFFC!')
assert.deepEqual(s2.text.toSpans(), [
"Hello ",
{ attributes: { bold: true } },
"reader",
{ attributes: { bold: null } },
"!"
])
})
it('should account for control characters in retain/delete lengths', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('Hello world')
doc.text.insertAt(4, { attributes: { color: '#ccc' } })
doc.text.insertAt(10, { attributes: { color: '#f00' } })
})
const delta = [
{ retain: 6 },
{ insert: 'reader', attributes: { bold: true } },
{ delete: 5 },
{ insert: '!' }
]
let s2 = Automerge.change(s1, doc => {
applyDeltaDocToAutomergeText(delta, doc)
})
assert.strictEqual(s2.text.toString(), 'Hell\uFFFCo \uFFFCreader\uFFFC\uFFFC!')
assert.deepEqual(s2.text.toSpans(), [
"Hell",
{ attributes: { color: '#ccc'} },
"o ",
{ attributes: { bold: true } },
"reader",
{ attributes: { bold: null } },
{ attributes: { color: '#f00'} },
"!"
])
})
it('should support embeds', () => {
let s1 = Automerge.change(Automerge.init(), doc => {
doc.text = new Automerge.Text('')
})
let deltaDoc = [{
// An image link
insert: {
image: 'https://quilljs.com/assets/images/icon.png'
},
attributes: {
link: 'https://quilljs.com'
}
}]
let s2 = Automerge.change(s1, doc => {
applyDeltaDocToAutomergeText(deltaDoc, doc)
})
assert.deepEqual(s2.text.toSpans(), [
{ attributes: { link: 'https://quilljs.com' } },
{ image: 'https://quilljs.com/assets/images/icon.png'},
{ attributes: { link: null } },
])
})
})
})
it('should support unicode when creating text', () => {
s1 = Automerge.from({
text: new Automerge.Text('🐦')
})
assert.strictEqual(s1.text.get(0), '🐦')
})
})

View file

@ -1,281 +0,0 @@
import * as assert from "assert"
import * as Automerge from "../src"
import { assertEqualsOneOf } from "./helpers"
type DocType = { text: Automerge.Text; [key: string]: any }
describe("Automerge.Text", () => {
let s1: Automerge.Doc<DocType>, s2: Automerge.Doc<DocType>
beforeEach(() => {
s1 = Automerge.change(
Automerge.init<DocType>(),
doc => (doc.text = new Automerge.Text())
)
s2 = Automerge.merge(Automerge.init(), s1)
})
it("should support insertion", () => {
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, "a"))
assert.strictEqual(s1.text.length, 1)
assert.strictEqual(s1.text.get(0), "a")
assert.strictEqual(s1.text.toString(), "a")
//assert.strictEqual(s1.text.getElemId(0), `2@${Automerge.getActorId(s1)}`)
})
it("should support deletion", () => {
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, "a", "b", "c"))
s1 = Automerge.change(s1, doc => doc.text.deleteAt(1, 1))
assert.strictEqual(s1.text.length, 2)
assert.strictEqual(s1.text.get(0), "a")
assert.strictEqual(s1.text.get(1), "c")
assert.strictEqual(s1.text.toString(), "ac")
})
it("should support implicit and explicit deletion", () => {
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, "a", "b", "c"))
s1 = Automerge.change(s1, doc => doc.text.deleteAt(1))
s1 = Automerge.change(s1, doc => doc.text.deleteAt(1, 0))
assert.strictEqual(s1.text.length, 2)
assert.strictEqual(s1.text.get(0), "a")
assert.strictEqual(s1.text.get(1), "c")
assert.strictEqual(s1.text.toString(), "ac")
})
it("should handle concurrent insertion", () => {
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, "a", "b", "c"))
s2 = Automerge.change(s2, doc => doc.text.insertAt(0, "x", "y", "z"))
s1 = Automerge.merge(s1, s2)
assert.strictEqual(s1.text.length, 6)
assertEqualsOneOf(s1.text.toString(), "abcxyz", "xyzabc")
assertEqualsOneOf(s1.text.join(""), "abcxyz", "xyzabc")
})
it("should handle text and other ops in the same change", () => {
s1 = Automerge.change(s1, doc => {
doc.foo = "bar"
doc.text.insertAt(0, "a")
})
assert.strictEqual(s1.foo, "bar")
assert.strictEqual(s1.text.toString(), "a")
assert.strictEqual(s1.text.join(""), "a")
})
it("should serialize to JSON as a simple string", () => {
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, "a", '"', "b"))
assert.strictEqual(JSON.stringify(s1), '{"text":"a\\"b"}')
})
it("should allow modification before an object is assigned to a document", () => {
s1 = Automerge.change(Automerge.init(), doc => {
const text = new Automerge.Text()
text.insertAt(0, "a", "b", "c", "d")
text.deleteAt(2)
doc.text = text
assert.strictEqual(doc.text.toString(), "abd")
assert.strictEqual(doc.text.join(""), "abd")
})
assert.strictEqual(s1.text.toString(), "abd")
assert.strictEqual(s1.text.join(""), "abd")
})
it("should allow modification after an object is assigned to a document", () => {
s1 = Automerge.change(Automerge.init(), doc => {
const text = new Automerge.Text()
doc.text = text
doc.text.insertAt(0, "a", "b", "c", "d")
doc.text.deleteAt(2)
assert.strictEqual(doc.text.toString(), "abd")
assert.strictEqual(doc.text.join(""), "abd")
})
assert.strictEqual(s1.text.join(""), "abd")
})
it("should not allow modification outside of a change callback", () => {
assert.throws(
() => s1.text.insertAt(0, "a"),
/object cannot be modified outside of a change block/
)
})
describe("with initial value", () => {
it("should accept a string as initial value", () => {
let s1 = Automerge.change(
Automerge.init<DocType>(),
doc => (doc.text = new Automerge.Text("init"))
)
assert.strictEqual(s1.text.length, 4)
assert.strictEqual(s1.text.get(0), "i")
assert.strictEqual(s1.text.get(1), "n")
assert.strictEqual(s1.text.get(2), "i")
assert.strictEqual(s1.text.get(3), "t")
assert.strictEqual(s1.text.toString(), "init")
})
it("should accept an array as initial value", () => {
let s1 = Automerge.change(
Automerge.init<DocType>(),
doc => (doc.text = new Automerge.Text(["i", "n", "i", "t"]))
)
assert.strictEqual(s1.text.length, 4)
assert.strictEqual(s1.text.get(0), "i")
assert.strictEqual(s1.text.get(1), "n")
assert.strictEqual(s1.text.get(2), "i")
assert.strictEqual(s1.text.get(3), "t")
assert.strictEqual(s1.text.toString(), "init")
})
it("should initialize text in Automerge.from()", () => {
let s1 = Automerge.from({ text: new Automerge.Text("init") })
assert.strictEqual(s1.text.length, 4)
assert.strictEqual(s1.text.get(0), "i")
assert.strictEqual(s1.text.get(1), "n")
assert.strictEqual(s1.text.get(2), "i")
assert.strictEqual(s1.text.get(3), "t")
assert.strictEqual(s1.text.toString(), "init")
})
it("should encode the initial value as a change", () => {
const s1 = Automerge.from({ text: new Automerge.Text("init") })
const changes = Automerge.getAllChanges(s1)
assert.strictEqual(changes.length, 1)
const [s2] = Automerge.applyChanges(Automerge.init<DocType>(), changes)
assert.strictEqual(s2.text instanceof Automerge.Text, true)
assert.strictEqual(s2.text.toString(), "init")
assert.strictEqual(s2.text.join(""), "init")
})
it("should allow immediate access to the value", () => {
Automerge.change(Automerge.init<DocType>(), doc => {
const text = new Automerge.Text("init")
assert.strictEqual(text.length, 4)
assert.strictEqual(text.get(0), "i")
assert.strictEqual(text.toString(), "init")
doc.text = text
assert.strictEqual(doc.text.length, 4)
assert.strictEqual(doc.text.get(0), "i")
assert.strictEqual(doc.text.toString(), "init")
})
})
it("should allow pre-assignment modification of the initial value", () => {
let s1 = Automerge.change(Automerge.init<DocType>(), doc => {
const text = new Automerge.Text("init")
text.deleteAt(3)
assert.strictEqual(text.join(""), "ini")
doc.text = text
assert.strictEqual(doc.text.join(""), "ini")
assert.strictEqual(doc.text.toString(), "ini")
})
assert.strictEqual(s1.text.toString(), "ini")
assert.strictEqual(s1.text.join(""), "ini")
})
it("should allow post-assignment modification of the initial value", () => {
let s1 = Automerge.change(Automerge.init<DocType>(), doc => {
const text = new Automerge.Text("init")
doc.text = text
doc.text.deleteAt(0)
doc.text.insertAt(0, "I")
assert.strictEqual(doc.text.join(""), "Init")
assert.strictEqual(doc.text.toString(), "Init")
})
assert.strictEqual(s1.text.join(""), "Init")
assert.strictEqual(s1.text.toString(), "Init")
})
})
describe("non-textual control characters", () => {
let s1: Automerge.Doc<DocType>
beforeEach(() => {
s1 = Automerge.change(Automerge.init<DocType>(), doc => {
doc.text = new Automerge.Text()
doc.text.insertAt(0, "a")
doc.text.insertAt(1, { attribute: "bold" })
})
})
it("should allow fetching non-textual characters", () => {
assert.deepEqual(s1.text.get(1), { attribute: "bold" })
//assert.strictEqual(s1.text.getElemId(1), `3@${Automerge.getActorId(s1)}`)
})
it("should include control characters in string length", () => {
assert.strictEqual(s1.text.length, 2)
assert.strictEqual(s1.text.get(0), "a")
})
it("should replace control characters from toString()", () => {
assert.strictEqual(s1.text.toString(), "a\uFFFC")
})
it("should allow control characters to be updated", () => {
const s2 = Automerge.change(
s1,
doc => (doc.text.get(1)!.attribute = "italic")
)
const s3 = Automerge.load<DocType>(Automerge.save(s2))
assert.strictEqual(s1.text.get(1).attribute, "bold")
assert.strictEqual(s2.text.get(1).attribute, "italic")
assert.strictEqual(s3.text.get(1).attribute, "italic")
})
describe("spans interface to Text", () => {
it("should return a simple string as a single span", () => {
let s1 = Automerge.change(Automerge.init<DocType>(), doc => {
doc.text = new Automerge.Text("hello world")
})
assert.deepEqual(s1.text.toSpans(), ["hello world"])
})
it("should return an empty string as an empty array", () => {
let s1 = Automerge.change(Automerge.init<DocType>(), doc => {
doc.text = new Automerge.Text()
})
assert.deepEqual(s1.text.toSpans(), [])
})
it("should split a span at a control character", () => {
let s1 = Automerge.change(Automerge.init<DocType>(), doc => {
doc.text = new Automerge.Text("hello world")
doc.text.insertAt(5, { attributes: { bold: true } })
})
assert.deepEqual(s1.text.toSpans(), [
"hello",
{ attributes: { bold: true } },
" world",
])
})
it("should allow consecutive control characters", () => {
let s1 = Automerge.change(Automerge.init<DocType>(), doc => {
doc.text = new Automerge.Text("hello world")
doc.text.insertAt(5, { attributes: { bold: true } })
doc.text.insertAt(6, { attributes: { italic: true } })
})
assert.deepEqual(s1.text.toSpans(), [
"hello",
{ attributes: { bold: true } },
{ attributes: { italic: true } },
" world",
])
})
it("should allow non-consecutive control characters", () => {
let s1 = Automerge.change(Automerge.init<DocType>(), doc => {
doc.text = new Automerge.Text("hello world")
doc.text.insertAt(5, { attributes: { bold: true } })
doc.text.insertAt(12, { attributes: { italic: true } })
})
assert.deepEqual(s1.text.toSpans(), [
"hello",
{ attributes: { bold: true } },
" world",
{ attributes: { italic: true } },
])
})
})
})
it("should support unicode when creating text", () => {
s1 = Automerge.from({
text: new Automerge.Text("🐦"),
})
assert.strictEqual(s1.text.get(0), "🐦")
})
})

View file

@ -1,20 +1,20 @@
import * as assert from "assert"
import * as Automerge from "../src"
import * as assert from 'assert'
import * as Automerge from '../src'
const uuid = Automerge.uuid
describe("uuid", () => {
describe('uuid', () => {
afterEach(() => {
uuid.reset()
})
describe("default implementation", () => {
it("generates unique values", () => {
describe('default implementation', () => {
it('generates unique values', () => {
assert.notEqual(uuid(), uuid())
})
})
describe("custom implementation", () => {
describe('custom implementation', () => {
let counter
function customUuid() {
@ -22,11 +22,11 @@ describe("uuid", () => {
}
before(() => uuid.setFactory(customUuid))
beforeEach(() => (counter = 0))
beforeEach(() => counter = 0)
it("invokes the custom factory", () => {
assert.equal(uuid(), "custom-uuid-0")
assert.equal(uuid(), "custom-uuid-1")
it('invokes the custom factory', () => {
assert.equal(uuid(), 'custom-uuid-0')
assert.equal(uuid(), 'custom-uuid-1')
})
})
})

View file

@ -1,19 +1,22 @@
{
"compilerOptions": {
"target": "es2016",
"sourceMap": false,
"declaration": true,
"resolveJsonModule": true,
"module": "commonjs",
"moduleResolution": "node",
"noImplicitAny": false,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"outDir": "./dist"
},
"include": ["src/**/*", "test/**/*"],
"exclude": ["./dist/**/*", "./node_modules", "./src/**/*.deno.ts"]
"compilerOptions": {
"target": "es2016",
"sourceMap": false,
"declaration": true,
"resolveJsonModule": true,
"module": "commonjs",
"moduleResolution": "node",
"noImplicitAny": false,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"outDir": "./dist"
},
"include": [ "src/**/*" ],
"exclude": [
"./dist/**/*",
"./node_modules"
]
}

View file

@ -0,0 +1,165 @@
# Automerge
This library provides the core automerge data structure and sync algorithms.
Other libraries can be built on top of this one which provide IO and
persistence.
An automerge document can be though of an immutable POJO (plain old javascript
object) which `automerge` tracks the history of, allowing it to be merged with
any other automerge document.
## Creating and modifying a document
You can create a document with {@link init} or {@link from} and then make
changes to it with {@link change}, you can merge two documents with {@link
merge}.
```javascript
import * as automerge from "@automerge/automerge"
type DocType = {ideas: Array<automerge.Text>}
let doc1 = automerge.init<DocType>()
doc1 = automerge.change(doc1, d => {
d.ideas = [new automerge.Text("an immutable document")]
})
let doc2 = automerge.init<DocType>()
doc2 = automerge.merge(doc2, automerge.clone(doc1))
doc2 = automerge.change<DocType>(doc2, d => {
d.ideas.push(new automerge.Text("which records it's history"))
})
// Note the `automerge.clone` call, see the "cloning" section of this readme for
// more detail
doc1 = automerge.merge(doc1, automerge.clone(doc2))
doc1 = automerge.change(doc1, d => {
d.ideas[0].deleteAt(13, 8)
d.ideas[0].insertAt(13, "object")
})
let doc3 = automerge.merge(doc1, doc2)
// doc3 is now {ideas: ["an immutable object", "which records it's history"]}
```
## Applying changes from another document
You can get a representation of the result of the last {@link change} you made
to a document with {@link getLastLocalChange} and you can apply that change to
another document using {@link applyChanges}.
If you need to get just the changes which are in one document but not in another
you can use {@link getHeads} to get the heads of the document without the
changes and then {@link getMissingDeps}, passing the result of {@link getHeads}
on the document with the changes.
## Saving and loading documents
You can {@link save} a document to generate a compresed binary representation of
the document which can be loaded with {@link load}. If you have a document which
you have recently made changes to you can generate recent changes with {@link
saveIncremental}, this will generate all the changes since you last called
`saveIncremental`, the changes generated can be applied to another document with
{@link loadIncremental}.
## Syncing
The sync protocol is stateful. This means that we start by creating a {@link
SyncState} for each peer we are communicating with using {@link initSyncState}.
Then we generate a message to send to the peer by calling {@link
generateSyncMessage}. When we receive a message from the peer we call {@link
receiveSyncMessage}. Here's a simple example of a loop which just keeps two
peers in sync.
```javascript
let sync1 = automerge.initSyncState()
let msg: Uint8Array | null
[sync1, msg] = automerge.generateSyncMessage(doc1, sync1)
while (true) {
if (msg != null) {
network.send(msg)
}
let resp: Uint8Array = network.receive()
[doc1, sync1, _ignore] = automerge.receiveSyncMessage(doc1, sync1, resp)
[sync1, msg] = automerge.generateSyncMessage(doc1, sync1)
}
```
## Conflicts
The only time conflicts occur in automerge documents is in concurrent
assignments to the same key in an object. In this case automerge
deterministically chooses an arbitrary value to present to the application but
you can examine the conflicts using {@link getConflicts}.
```
import * as automerge from "@automerge/automerge"
type Profile = {
pets: Array<{name: string, type: string}>
}
let doc1 = automerge.init<Profile>("aaaa")
doc1 = automerge.change(doc1, d => {
d.pets = [{name: "Lassie", type: "dog"}]
})
let doc2 = automerge.init<Profile>("bbbb")
doc2 = automerge.merge(doc2, automerge.clone(doc1))
doc2 = automerge.change(doc2, d => {
d.pets[0].name = "Beethoven"
})
doc1 = automerge.change(doc1, d => {
d.pets[0].name = "Babe"
})
const doc3 = automerge.merge(doc1, doc2)
// Note that here we pass `doc3.pets`, not `doc3`
let conflicts = automerge.getConflicts(doc3.pets[0], "name")
// The two conflicting values are the keys of the conflicts object
assert.deepEqual(Object.values(conflicts), ["Babe", Beethoven"])
```
## Actor IDs
By default automerge will generate a random actor ID for you, but most methods
for creating a document allow you to set the actor ID. You can get the actor ID
associated with the document by calling {@link getActorId}. Actor IDs must not
be used in concurrent threads of executiong - all changes by a given actor ID
are expected to be sequential.
## Listening to patches
Sometimes you want to respond to changes made to an automerge document. In this
case you can use the {@link PatchCallback} type to receive notifications when
changes have been made.
## Cloning
Currently you cannot make mutating changes (i.e. call {@link change}) to a
document which you have two pointers to. For example, in this code:
```javascript
let doc1 = automerge.init()
let doc2 = automerge.change(doc1, d => d.key = "value")
```
`doc1` and `doc2` are both pointers to the same state. Any attempt to call
mutating methods on `doc1` will now result in an error like
Attempting to change an out of date document
If you encounter this you need to clone the original document, the above sample
would work as:
```javascript
let doc1 = automerge.init()
let doc2 = automerge.change(automerge.clone(doc1), d => d.key = "value")
```

View file

@ -3,15 +3,19 @@ members = [
"automerge",
"automerge-c",
"automerge-cli",
"automerge-test",
"automerge-wasm",
"edit-trace",
]
resolver = "2"
[profile.release]
debug = true
lto = true
codegen-units = 1
opt-level = 'z'
[profile.bench]
debug = true
debug = true
[profile.release.package.automerge-wasm]
debug = false
opt-level = 'z'

View file

@ -1,250 +0,0 @@
---
Language: Cpp
# BasedOnStyle: Chromium
AccessModifierOffset: -1
AlignAfterOpenBracket: Align
AlignArrayOfStructures: None
AlignConsecutiveAssignments:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: true
AlignConsecutiveBitFields:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: false
AlignConsecutiveDeclarations:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: false
AlignConsecutiveMacros:
Enabled: false
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: false
AlignEscapedNewlines: Left
AlignOperands: Align
AlignTrailingComments: true
AllowAllArgumentsOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortEnumsOnASingleLine: true
AllowShortBlocksOnASingleLine: Never
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Inline
AllowShortLambdasOnASingleLine: All
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: true
AlwaysBreakTemplateDeclarations: Yes
AttributeMacros:
- __capability
BinPackArguments: true
BinPackParameters: false
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
AfterControlStatement: Never
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
BeforeLambdaBody: false
BeforeWhile: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakBeforeBinaryOperators: None
BreakBeforeConceptDeclarations: Always
BreakBeforeBraces: Attach
BreakBeforeInheritanceComma: false
BreakInheritanceList: BeforeColon
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: BeforeColon
BreakAfterJavaFieldAnnotations: false
BreakStringLiterals: true
ColumnLimit: 120
CommentPragmas: '^ IWYU pragma:'
QualifierAlignment: Leave
CompactNamespaces: false
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DeriveLineEnding: true
DerivePointerAlignment: false
DisableFormat: false
EmptyLineAfterAccessModifier: Never
EmptyLineBeforeAccessModifier: LogicalBlock
ExperimentalAutoDetectBinPacking: false
PackConstructorInitializers: NextLine
BasedOnStyle: ''
ConstructorInitializerAllOnOneLineOrOnePerLine: false
AllowAllConstructorInitializersOnNextLine: true
FixNamespaceComments: true
ForEachMacros:
- foreach
- Q_FOREACH
- BOOST_FOREACH
IfMacros:
- KJ_IF_MAYBE
IncludeBlocks: Preserve
IncludeCategories:
- Regex: '^<ext/.*\.h>'
Priority: 2
SortPriority: 0
CaseSensitive: false
- Regex: '^<.*\.h>'
Priority: 1
SortPriority: 0
CaseSensitive: false
- Regex: '^<.*'
Priority: 2
SortPriority: 0
CaseSensitive: false
- Regex: '.*'
Priority: 3
SortPriority: 0
CaseSensitive: false
IncludeIsMainRegex: '([-_](test|unittest))?$'
IncludeIsMainSourceRegex: ''
IndentAccessModifiers: false
IndentCaseLabels: true
IndentCaseBlocks: false
IndentGotoLabels: true
IndentPPDirectives: None
IndentExternBlock: AfterExternBlock
IndentRequiresClause: true
IndentWidth: 4
IndentWrappedFunctionNames: false
InsertBraces: false
InsertTrailingCommas: None
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: false
LambdaBodyIndentation: Signature
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Never
ObjCBlockIndentWidth: 2
ObjCBreakBeforeNestedBlockParam: true
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PenaltyBreakAssignment: 2
PenaltyBreakBeforeFirstCallParameter: 1
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakOpenParenthesis: 0
PenaltyBreakString: 1000
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 200
PenaltyIndentedWhitespace: 0
PointerAlignment: Left
PPIndentWidth: -1
RawStringFormats:
- Language: Cpp
Delimiters:
- cc
- CC
- cpp
- Cpp
- CPP
- 'c++'
- 'C++'
CanonicalDelimiter: ''
BasedOnStyle: google
- Language: TextProto
Delimiters:
- pb
- PB
- proto
- PROTO
EnclosingFunctions:
- EqualsProto
- EquivToProto
- PARSE_PARTIAL_TEXT_PROTO
- PARSE_TEST_PROTO
- PARSE_TEXT_PROTO
- ParseTextOrDie
- ParseTextProtoOrDie
- ParseTestProto
- ParsePartialTestProto
CanonicalDelimiter: pb
BasedOnStyle: google
ReferenceAlignment: Pointer
ReflowComments: true
RemoveBracesLLVM: false
RequiresClausePosition: OwnLine
SeparateDefinitionBlocks: Leave
ShortNamespaceLines: 1
SortIncludes: CaseSensitive
SortJavaStaticImport: Before
SortUsingDeclarations: true
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeCaseColon: false
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeParensOptions:
AfterControlStatements: true
AfterForeachMacros: true
AfterFunctionDefinitionName: false
AfterFunctionDeclarationName: false
AfterIfMacros: true
AfterOverloadedOperator: false
AfterRequiresInClause: false
AfterRequiresInExpression: false
BeforeNonEmptyParentheses: false
SpaceAroundPointerQualifiers: Default
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyBlock: false
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 2
SpacesInAngles: Never
SpacesInConditionalStatement: false
SpacesInContainerLiterals: true
SpacesInCStyleCastParentheses: false
SpacesInLineCommentPrefix:
Minimum: 1
Maximum: -1
SpacesInParentheses: false
SpacesInSquareBrackets: false
SpaceBeforeSquareBrackets: false
BitFieldColonSpacing: Both
Standard: Auto
StatementAttributeLikeMacros:
- Q_EMIT
StatementMacros:
- Q_UNUSED
- QT_REQUIRE_VERSION
TabWidth: 8
UseCRLF: false
UseTab: Never
WhitespaceSensitiveMacros:
- STRINGIZE
- PP_STRINGIZE
- BOOST_PP_STRINGIZE
- NS_SWIFT_NAME
- CF_SWIFT_NAME
...

View file

@ -1,10 +1,3 @@
automerge
automerge.h
automerge.o
build/
CMakeCache.txt
CMakeFiles
CMakePresets.json
Makefile
DartConfiguration.tcl
out/

View file

@ -1,297 +1,97 @@
cmake_minimum_required(VERSION 3.23 FATAL_ERROR)
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
project(automerge-c VERSION 0.1.0
LANGUAGES C
DESCRIPTION "C bindings for the Automerge Rust library.")
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
set(LIBRARY_NAME "automerge")
# Parse the library name, project name and project version out of Cargo's TOML file.
set(CARGO_LIB_SECTION OFF)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
set(LIBRARY_NAME "")
option(BUILD_SHARED_LIBS "Enable the choice of a shared or static library.")
set(CARGO_PKG_SECTION OFF)
set(CARGO_PKG_NAME "")
set(CARGO_PKG_VERSION "")
file(READ Cargo.toml TOML_STRING)
string(REPLACE ";" "\\\\;" TOML_STRING "${TOML_STRING}")
string(REPLACE "\n" ";" TOML_LINES "${TOML_STRING}")
foreach(TOML_LINE IN ITEMS ${TOML_LINES})
string(REGEX MATCH "^\\[(lib|package)\\]$" _ ${TOML_LINE})
if(CMAKE_MATCH_1 STREQUAL "lib")
set(CARGO_LIB_SECTION ON)
set(CARGO_PKG_SECTION OFF)
elseif(CMAKE_MATCH_1 STREQUAL "package")
set(CARGO_LIB_SECTION OFF)
set(CARGO_PKG_SECTION ON)
endif()
string(REGEX MATCH "^name += +\"([^\"]+)\"$" _ ${TOML_LINE})
if(CMAKE_MATCH_1 AND (CARGO_LIB_SECTION AND NOT CARGO_PKG_SECTION))
set(LIBRARY_NAME "${CMAKE_MATCH_1}")
elseif(CMAKE_MATCH_1 AND (NOT CARGO_LIB_SECTION AND CARGO_PKG_SECTION))
set(CARGO_PKG_NAME "${CMAKE_MATCH_1}")
endif()
string(REGEX MATCH "^version += +\"([^\"]+)\"$" _ ${TOML_LINE})
if(CMAKE_MATCH_1 AND CARGO_PKG_SECTION)
set(CARGO_PKG_VERSION "${CMAKE_MATCH_1}")
endif()
if(LIBRARY_NAME AND (CARGO_PKG_NAME AND CARGO_PKG_VERSION))
break()
endif()
endforeach()
project(${CARGO_PKG_NAME} VERSION ${CARGO_PKG_VERSION} LANGUAGES C DESCRIPTION "C bindings for the Automerge Rust backend.")
include(CTest)
option(BUILD_SHARED_LIBS "Enable the choice of a shared or static library.")
include(CMakePackageConfigHelpers)
include(GNUInstallDirs)
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
string(MAKE_C_IDENTIFIER ${PROJECT_NAME} SYMBOL_PREFIX)
string(TOUPPER ${SYMBOL_PREFIX} SYMBOL_PREFIX)
set(CARGO_TARGET_DIR "${CMAKE_BINARY_DIR}/Cargo/target")
set(CARGO_TARGET_DIR "${CMAKE_CURRENT_BINARY_DIR}/Cargo/target")
set(CBINDGEN_INCLUDEDIR "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_INCLUDEDIR}")
set(CBINDGEN_INCLUDEDIR "${CARGO_TARGET_DIR}/${CMAKE_INSTALL_INCLUDEDIR}")
set(CBINDGEN_TARGET_DIR "${CBINDGEN_INCLUDEDIR}/${PROJECT_NAME}")
find_program (
CARGO_CMD
"cargo"
PATHS "$ENV{CARGO_HOME}/bin"
DOC "The Cargo command"
)
add_subdirectory(src)
if(NOT CARGO_CMD)
message(FATAL_ERROR "Cargo (Rust package manager) not found! "
"Please install it and/or set the CARGO_HOME "
"environment variable to its path.")
endif()
string(TOLOWER "${CMAKE_BUILD_TYPE}" BUILD_TYPE_LOWER)
# In order to build with -Z build-std, we need to pass target explicitly.
# https://doc.rust-lang.org/cargo/reference/unstable.html#build-std
execute_process (
COMMAND rustc -vV
OUTPUT_VARIABLE RUSTC_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE
)
string(REGEX REPLACE ".*host: ([^ \n]*).*" "\\1"
CARGO_TARGET
${RUSTC_VERSION}
)
if(BUILD_TYPE_LOWER STREQUAL debug)
set(CARGO_BUILD_TYPE "debug")
set(CARGO_FLAG --target=${CARGO_TARGET})
else()
set(CARGO_BUILD_TYPE "release")
if (NOT RUSTC_VERSION MATCHES "nightly")
set(RUSTUP_TOOLCHAIN nightly)
endif()
set(RUSTFLAGS -C\ panic=abort)
set(CARGO_FLAG -Z build-std=std,panic_abort --release --target=${CARGO_TARGET})
endif()
set(CARGO_FEATURES "")
set(CARGO_BINARY_DIR "${CARGO_TARGET_DIR}/${CARGO_TARGET}/${CARGO_BUILD_TYPE}")
set(BINDINGS_NAME "${LIBRARY_NAME}_core")
configure_file(
${CMAKE_MODULE_PATH}/Cargo.toml.in
${CMAKE_SOURCE_DIR}/Cargo.toml
@ONLY
NEWLINE_STYLE LF
)
set(INCLUDE_GUARD_PREFIX "${SYMBOL_PREFIX}")
configure_file(
${CMAKE_MODULE_PATH}/cbindgen.toml.in
${CMAKE_SOURCE_DIR}/cbindgen.toml
@ONLY
NEWLINE_STYLE LF
)
set(CARGO_OUTPUT
${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
${CARGO_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${BINDINGS_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}
)
# \note cbindgen's naming behavior isn't fully configurable and it ignores
# `const fn` calls (https://github.com/eqrion/cbindgen/issues/252).
add_custom_command(
OUTPUT
${CARGO_OUTPUT}
COMMAND
# \note cbindgen won't regenerate its output header file after it's been removed but it will after its
# configuration file has been updated.
${CMAKE_COMMAND} -DCONDITION=NOT_EXISTS -P ${CMAKE_SOURCE_DIR}/cmake/file-touch.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h ${CMAKE_SOURCE_DIR}/cbindgen.toml
COMMAND
${CMAKE_COMMAND} -E env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} CBINDGEN_TARGET_DIR=${CBINDGEN_TARGET_DIR} RUSTUP_TOOLCHAIN=${RUSTUP_TOOLCHAIN} RUSTFLAGS=${RUSTFLAGS} ${CARGO_CMD} build ${CARGO_FLAG} ${CARGO_FEATURES}
COMMAND
# Compensate for cbindgen's translation of consecutive uppercase letters to "ScreamingSnakeCase".
${CMAKE_COMMAND} -DMATCH_REGEX=A_M\([^_]+\)_ -DREPLACE_EXPR=AM_\\1_ -P ${CMAKE_SOURCE_DIR}/cmake/file-regex-replace.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
COMMAND
# Compensate for cbindgen ignoring `std:mem::size_of<usize>()` calls.
${CMAKE_COMMAND} -DMATCH_REGEX=USIZE_ -DREPLACE_EXPR=\+${CMAKE_SIZEOF_VOID_P} -P ${CMAKE_SOURCE_DIR}/cmake/file-regex-replace.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
MAIN_DEPENDENCY
src/lib.rs
DEPENDS
src/actor_id.rs
src/byte_span.rs
src/change.rs
src/doc.rs
src/doc/list.rs
src/doc/map.rs
src/doc/utils.rs
src/index.rs
src/item.rs
src/items.rs
src/obj.rs
src/result.rs
src/sync.rs
src/sync/have.rs
src/sync/message.rs
src/sync/state.rs
${CMAKE_SOURCE_DIR}/build.rs
${CMAKE_MODULE_PATH}/Cargo.toml.in
${CMAKE_MODULE_PATH}/cbindgen.toml.in
WORKING_DIRECTORY
${CMAKE_SOURCE_DIR}
COMMENT
"Producing the bindings' artifacts with Cargo..."
VERBATIM
)
add_custom_target(${BINDINGS_NAME}_artifacts ALL
DEPENDS ${CARGO_OUTPUT}
)
add_library(${BINDINGS_NAME} STATIC IMPORTED GLOBAL)
target_include_directories(${BINDINGS_NAME} INTERFACE "${CBINDGEN_INCLUDEDIR}")
set_target_properties(
${BINDINGS_NAME}
PROPERTIES
# \note Cargo writes a debug build into a nested directory instead of
# decorating its name.
DEBUG_POSTFIX ""
DEFINE_SYMBOL ""
IMPORTED_IMPLIB ""
IMPORTED_LOCATION "${CARGO_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${BINDINGS_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}"
IMPORTED_NO_SONAME "TRUE"
IMPORTED_SONAME ""
LINKER_LANGUAGE C
PUBLIC_HEADER "${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h"
SOVERSION "${PROJECT_VERSION_MAJOR}"
VERSION "${PROJECT_VERSION}"
# \note Cargo exports all of the symbols automatically.
WINDOWS_EXPORT_ALL_SYMBOLS "TRUE"
)
target_compile_definitions(${BINDINGS_NAME} INTERFACE $<TARGET_PROPERTY:${BINDINGS_NAME},DEFINE_SYMBOL>)
set(UTILS_SUBDIR "utils")
add_custom_command(
OUTPUT
${CBINDGEN_TARGET_DIR}/${UTILS_SUBDIR}/enum_string.h
${CMAKE_BINARY_DIR}/src/${UTILS_SUBDIR}/enum_string.c
COMMAND
${CMAKE_COMMAND} -DPROJECT_NAME=${PROJECT_NAME} -DLIBRARY_NAME=${LIBRARY_NAME} -DSUBDIR=${UTILS_SUBDIR} -P ${CMAKE_SOURCE_DIR}/cmake/enum-string-functions-gen.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h ${CBINDGEN_TARGET_DIR}/${UTILS_SUBDIR}/enum_string.h ${CMAKE_BINARY_DIR}/src/${UTILS_SUBDIR}/enum_string.c
MAIN_DEPENDENCY
${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
DEPENDS
${CMAKE_SOURCE_DIR}/cmake/enum-string-functions-gen.cmake
WORKING_DIRECTORY
${CMAKE_SOURCE_DIR}
COMMENT
"Generating the enum string functions with CMake..."
VERBATIM
)
add_custom_target(${LIBRARY_NAME}_utilities
DEPENDS ${CBINDGEN_TARGET_DIR}/${UTILS_SUBDIR}/enum_string.h
${CMAKE_BINARY_DIR}/src/${UTILS_SUBDIR}/enum_string.c
)
add_library(${LIBRARY_NAME})
target_compile_features(${LIBRARY_NAME} PRIVATE c_std_99)
set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
find_package(Threads REQUIRED)
set(LIBRARY_DEPENDENCIES Threads::Threads ${CMAKE_DL_LIBS})
if(WIN32)
list(APPEND LIBRARY_DEPENDENCIES Bcrypt userenv ws2_32)
else()
list(APPEND LIBRARY_DEPENDENCIES m)
endif()
target_link_libraries(${LIBRARY_NAME}
PUBLIC ${BINDINGS_NAME}
${LIBRARY_DEPENDENCIES}
)
# \note An imported library's INTERFACE_INCLUDE_DIRECTORIES property can't
# contain a non-existent path so its build-time include directory
# must be specified for all of its dependent targets instead.
target_include_directories(${LIBRARY_NAME}
PUBLIC "$<BUILD_INTERFACE:${CBINDGEN_INCLUDEDIR};${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}>"
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
)
add_dependencies(${LIBRARY_NAME} ${BINDINGS_NAME}_artifacts)
# Generate the configuration header.
# Generate and install the configuration header.
math(EXPR INTEGER_PROJECT_VERSION_MAJOR "${PROJECT_VERSION_MAJOR} * 100000")
math(EXPR INTEGER_PROJECT_VERSION_MINOR "${PROJECT_VERSION_MINOR} * 100")
math(EXPR INTEGER_PROJECT_VERSION_PATCH "${PROJECT_VERSION_PATCH}")
math(EXPR INTEGER_PROJECT_VERSION "${INTEGER_PROJECT_VERSION_MAJOR} + \
${INTEGER_PROJECT_VERSION_MINOR} + \
${INTEGER_PROJECT_VERSION_PATCH}")
math(EXPR INTEGER_PROJECT_VERSION "${INTEGER_PROJECT_VERSION_MAJOR} + ${INTEGER_PROJECT_VERSION_MINOR} + ${INTEGER_PROJECT_VERSION_PATCH}")
configure_file(
${CMAKE_MODULE_PATH}/config.h.in
${CBINDGEN_TARGET_DIR}/config.h
config.h
@ONLY
NEWLINE_STYLE LF
)
target_sources(${LIBRARY_NAME}
PRIVATE
src/${UTILS_SUBDIR}/result.c
src/${UTILS_SUBDIR}/stack_callback_data.c
src/${UTILS_SUBDIR}/stack.c
src/${UTILS_SUBDIR}/string.c
${CMAKE_BINARY_DIR}/src/${UTILS_SUBDIR}/enum_string.c
PUBLIC
FILE_SET api TYPE HEADERS
BASE_DIRS
${CBINDGEN_INCLUDEDIR}
${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}
FILES
${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
${CBINDGEN_TARGET_DIR}/${UTILS_SUBDIR}/enum_string.h
${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/result.h
${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/stack_callback_data.h
${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/stack.h
${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/string.h
INTERFACE
FILE_SET config TYPE HEADERS
BASE_DIRS
${CBINDGEN_INCLUDEDIR}
FILES
${CBINDGEN_TARGET_DIR}/config.h
)
install(
TARGETS ${LIBRARY_NAME}
EXPORT ${PROJECT_NAME}-config
FILE_SET api
FILE_SET config
)
# \note Install the Cargo-built core bindings to enable direct linkage.
install(
FILES $<TARGET_PROPERTY:${BINDINGS_NAME},IMPORTED_LOCATION>
DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
install(EXPORT ${PROJECT_NAME}-config
FILE ${PROJECT_NAME}-config.cmake
NAMESPACE "${PROJECT_NAME}::"
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${LIB}
FILES ${CMAKE_BINARY_DIR}/config.h
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}
)
if(BUILD_TESTING)
@ -300,6 +100,42 @@ if(BUILD_TESTING)
enable_testing()
endif()
add_subdirectory(docs)
add_subdirectory(examples EXCLUDE_FROM_ALL)
# Generate and install .cmake files
set(PROJECT_CONFIG_NAME "${PROJECT_NAME}-config")
set(PROJECT_CONFIG_VERSION_NAME "${PROJECT_CONFIG_NAME}-version")
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_VERSION_NAME}.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY ExactVersion
)
# The namespace label starts with the title-cased library name.
string(SUBSTRING ${LIBRARY_NAME} 0 1 NS_FIRST)
string(SUBSTRING ${LIBRARY_NAME} 1 -1 NS_REST)
string(TOUPPER ${NS_FIRST} NS_FIRST)
string(TOLOWER ${NS_REST} NS_REST)
string(CONCAT NAMESPACE ${NS_FIRST} ${NS_REST} "::")
# \note CMake doesn't automate the exporting of an imported library's targets
# so the package configuration script must do it.
configure_package_config_file(
${CMAKE_MODULE_PATH}/${PROJECT_CONFIG_NAME}.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_NAME}.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
)
install(
FILES
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_NAME}.cmake
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_VERSION_NAME}.cmake
DESTINATION
${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
)

View file

@ -7,8 +7,8 @@ license = "MIT"
rust-version = "1.57.0"
[lib]
name = "automerge_core"
crate-type = ["staticlib"]
name = "automerge"
crate-type = ["cdylib", "staticlib"]
bench = false
doc = false
@ -19,4 +19,4 @@ libc = "^0.2"
smol_str = "^0.1.21"
[build-dependencies]
cbindgen = "^0.24"
cbindgen = "^0.20"

View file

@ -1,207 +1,97 @@
# Overview
automerge-c exposes a C API that can either be used directly or as the basis
for other language bindings that have good support for calling C functions.
## Methods we need to support
# Installing
### Basic management
See the main README for instructions on getting your environment set up and then
you can build the automerge-c library and install its constituent files within
a root directory of your choosing (e.g. "/usr/local") like so:
```shell
cmake -E make_directory automerge-c/build
cmake -S automerge-c -B automerge-c/build
cmake --build automerge-c/build
cmake --install automerge-c/build --prefix "/usr/local"
```
Installation is important because the name, location and structure of CMake's
out-of-source build subdirectory is subject to change based on the platform and
the release version; generated headers like `automerge-c/config.h` and
`automerge-c/utils/enum_string.h` are only sure to be found within their
installed locations.
1. `AMcreate()`
1. `AMclone(doc)`
1. `AMfree(doc)`
1. `AMconfig(doc, key, val)` // set actor
1. `actor = get_actor(doc)`
It's not obvious because they are versioned but the `Cargo.toml` and
`cbindgen.toml` configuration files are also generated in order to ensure that
the project name, project version and library name that they contain match those
specified within the top-level `CMakeLists.txt` file.
### Transactions
If you'd like to cross compile the library for different platforms you can do so
using [cross](https://github.com/cross-rs/cross). For example:
1. `AMpendingOps(doc)`
1. `AMcommit(doc, message, time)`
1. `AMrollback(doc)`
- `cross build --manifest-path rust/automerge-c/Cargo.toml -r --target aarch64-unknown-linux-gnu`
### Write
This will output a shared library in the directory `rust/target/aarch64-unknown-linux-gnu/release/`.
1. `AMset{Map|List}(doc, obj, prop, value)`
1. `AMinsert(doc, obj, index, value)`
1. `AMpush(doc, obj, value)`
1. `AMdel{Map|List}(doc, obj, prop)`
1. `AMinc{Map|List}(doc, obj, prop, value)`
1. `AMspliceText(doc, obj, start, num_del, text)`
You can replace `aarch64-unknown-linux-gnu` with any
[cross supported targets](https://github.com/cross-rs/cross#supported-targets).
The targets below are known to work, though other targets are expected to work
too:
### Read (the heads argument is optional and can be on an `at` variant)
- `x86_64-apple-darwin`
- `aarch64-apple-darwin`
- `x86_64-unknown-linux-gnu`
- `aarch64-unknown-linux-gnu`
1. `AMkeys(doc, obj, heads)`
1. `AMlength(doc, obj, heads)`
1. `AMlistRange(doc, obj, heads)`
1. `AMmapRange(doc, obj, heads)`
1. `AMvalues(doc, obj, heads)`
1. `AMtext(doc, obj, heads)`
As a caveat, CMake generates the `automerge.h` header file in terms of the
processor architecture of the computer on which it was built so, for example,
don't use a header generated for a 64-bit processor if your target is a 32-bit
processor.
### Sync
# Usage
1. `AMgenerateSyncMessage(doc, state)`
1. `AMreceiveSyncMessage(doc, state, message)`
1. `AMinitSyncState()`
You can build and view the C API's HTML reference documentation like so:
```shell
cmake -E make_directory automerge-c/build
cmake -S automerge-c -B automerge-c/build
cmake --build automerge-c/build --target automerge_docs
firefox automerge-c/build/src/html/index.html
```
### Save / Load
To get started quickly, look at the
[examples](https://github.com/automerge/automerge-rs/tree/main/rust/automerge-c/examples).
1. `AMload(data)`
1. `AMloadIncremental(doc, data)`
1. `AMsave(doc)`
1. `AMsaveIncremental(doc)`
Almost all operations in automerge-c act on an Automerge document
(`AMdoc` struct) which is structurally similar to a JSON document.
### Low Level Access
You can get a document by calling either `AMcreate()` or `AMload()`. Operations
on a given document are not thread-safe so you must use a mutex or similar to
avoid calling more than one function on the same one concurrently.
1. `AMapplyChanges(doc, changes)`
1. `AMgetChanges(doc, deps)`
1. `AMgetChangesAdded(doc1, doc2)`
1. `AMgetHeads(doc)`
1. `AMgetLastLocalChange(doc)`
1. `AMgetMissingDeps(doc, heads)`
A C API function that could succeed or fail returns a result (`AMresult` struct)
containing a status code (`AMstatus` enum) and either a sequence of at least one
item (`AMitem` struct) or a read-only view onto a UTF-8 error message string
(`AMbyteSpan` struct).
An item contains up to three components: an index within its parent object
(`AMbyteSpan` struct or `size_t`), a unique identifier (`AMobjId` struct) and a
value.
The result of a successful function call that doesn't produce any values will
contain a single item that is void (`AM_VAL_TYPE_VOID`).
A returned result **must** be passed to `AMresultFree()` once the item(s) or
error message it contains is no longer needed in order to avoid a memory leak.
```
#include <stdio.h>
#include <stdlib.h>
#include <automerge-c/automerge.h>
#include <automerge-c/utils/string.h>
### Encode/Decode
int main(int argc, char** argv) {
AMresult *docResult = AMcreate(NULL);
1. `AMencodeChange(change)`
1. `AMdecodeChange(change)`
1. `AMencodeSyncMessage(change)`
1. `AMdecodeSyncMessage(change)`
1. `AMencodeSyncState(change)`
1. `AMdecodeSyncState(change)`
if (AMresultStatus(docResult) != AM_STATUS_OK) {
char* const err_msg = AMstrdup(AMresultError(docResult), NULL);
printf("failed to create doc: %s", err_msg);
free(err_msg);
goto cleanup;
}
## Open Question - Memory management
AMdoc *doc;
AMitemToDoc(AMresultItem(docResult), &doc);
Most of these calls return one or more items of arbitrary length. Doing memory management in C is tricky. This is my proposed solution...
// useful code goes here!
###
cleanup:
AMresultFree(docResult);
}
```
```
// returns 1 or zero opids
n = automerge_set(doc, "_root", "hello", datatype, value);
if (n) {
automerge_pop(doc, &obj, len);
}
If you are writing an application in C, the `AMstackItem()`, `AMstackItems()`
and `AMstackResult()` functions enable the lifetimes of anonymous results to be
centrally managed and allow the same validation logic to be reused without
relying upon the `goto` statement (see examples/quickstart.c).
// returns n values
n = automerge_values(doc, "_root", "hello");
for (i = 0; i<n ;i ++) {
automerge_pop_value(doc, &value, &datatype, len);
}
```
If you are wrapping automerge-c in another language, particularly one that has a
garbage collector, you can call the `AMresultFree()` function within a finalizer
to ensure that memory is reclaimed when it is no longer needed.
Automerge documents consist of a mutable root which is always a map from string
keys to values. A value can be one of the following types:
- A number of type double / int64_t / uint64_t
- An explicit true / false / null
- An immutable UTF-8 string (`AMbyteSpan`).
- An immutable array of arbitrary bytes (`AMbyteSpan`).
- A mutable map from string keys to values.
- A mutable list of values.
- A mutable UTF-8 string.
If you read from a location in the document with no value, an item with type
`AM_VAL_TYPE_VOID` will be returned, but you cannot write such a value
explicitly.
Under the hood, automerge references a mutable object by its object identifier
where `AM_ROOT` signifies a document's root map object.
There are functions to put each type of value into either a map or a list, and
functions to read the current or a historical value from a map or a list. As (in general) collaborators
may edit the document at any time, you cannot guarantee that the type of the
value at a given part of the document will stay the same. As a result, reading
from the document will return an `AMitem` struct that you can inspect to
determine the type of value that it contains.
Strings in automerge-c are represented using an `AMbyteSpan` which contains a
pointer and a length. Strings must be valid UTF-8 and may contain NUL (`0`)
characters.
For your convenience, you can call `AMstr()` to get the `AMbyteSpan` struct
equivalent of a null-terminated byte string or `AMstrdup()` to get the
representation of an `AMbyteSpan` struct as a null-terminated byte string
wherein its NUL characters have been removed/replaced as you choose.
Putting all of that together, to read and write from the root of the document
you can do this:
```
#include <stdio.h>
#include <stdlib.h>
#include <automerge-c/automerge.h>
#include <automerge-c/utils/string.h>
int main(int argc, char** argv) {
// ...previous example...
AMdoc *doc;
AMitemToDoc(AMresultItem(docResult), &doc);
AMresult *putResult = AMmapPutStr(doc, AM_ROOT, AMstr("key"), AMstr("value"));
if (AMresultStatus(putResult) != AM_STATUS_OK) {
char* const err_msg = AMstrdup(AMresultError(putResult), NULL);
printf("failed to put: %s", err_msg);
free(err_msg);
goto cleanup;
}
AMresult *getResult = AMmapGet(doc, AM_ROOT, AMstr("key"), NULL);
if (AMresultStatus(getResult) != AM_STATUS_OK) {
char* const err_msg = AMstrdup(AMresultError(putResult), NULL);
printf("failed to get: %s", err_msg);
free(err_msg);
goto cleanup;
}
AMbyteSpan got;
if (AMitemToStr(AMresultItem(getResult), &got)) {
char* const c_str = AMstrdup(got, NULL);
printf("Got %zu-character string \"%s\"", got.count, c_str);
free(c_str);
} else {
printf("expected to read a string!");
goto cleanup;
}
There would be one pop method per object type. Users allocs and frees the buffers. Multiple return values would result in multiple pops. Too small buffers would error and allow retry.
cleanup:
AMresultFree(getResult);
AMresultFree(putResult);
AMresultFree(docResult);
}
```
### Formats
Functions that do not return an `AMresult` (for example `AMitemKey()`) do
not allocate memory but rather reference memory that was previously
allocated. It's therefore important to keep the original `AMresult` alive (in
this case the one returned by `AMmapRange()`) until after you are finished with
the items that it contains. However, the memory for an individual `AMitem` can
be shared with a new `AMresult` by calling `AMitemResult()` on it. In other
words, a select group of items can be filtered out of a collection and only each
one's corresponding `AMresult` must be kept alive from that point forward; the
originating collection's `AMresult` can be safely freed.
Actors - We could do (bytes,len) or a hex encoded string?.
ObjIds - We could do flat bytes of the ExId struct but lets do human readable strings for now - the struct would be faster but opque
Heads - Might as well make it a flat buffer `(n, hash, hash, ...)`
Changes - Put them all in a flat concatenated buffer
Encode/Decode - to json strings?
Beyond that, good luck!

View file

@ -10,7 +10,7 @@ fn main() {
let config = cbindgen::Config::from_file("cbindgen.toml")
.expect("Unable to find cbindgen.toml configuration file");
if let Ok(writer) = cbindgen::generate_with_config(crate_dir, config) {
if let Ok(writer) = cbindgen::generate_with_config(&crate_dir, config) {
// \note CMake sets this environment variable before invoking Cargo so
// that it can direct the generated header file into its
// out-of-source build directory for post-processing.

View file

@ -1,7 +1,7 @@
after_includes = """\n
/**
* \\defgroup enumerations Public Enumerations
* Symbolic names for integer constants.
Symbolic names for integer constants.
*/
/**
@ -12,23 +12,21 @@ after_includes = """\n
#define AM_ROOT NULL
/**
* \\memberof AMdoc
* \\memberof AMchangeHash
* \\def AM_CHANGE_HASH_SIZE
* \\brief The count of bytes in a change hash.
*/
#define AM_CHANGE_HASH_SIZE 32
"""
autogen_warning = """
/**
* \\file
* \\brief All constants, functions and types in the core Automerge C API.
*
* \\warning This file is auto-generated by cbindgen.
*/
"""
autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"
documentation = true
documentation_style = "doxy"
include_guard = "AUTOMERGE_C_H"
header = """
/** \\file
* All constants, functions and types in the Automerge library's C API.
*/
"""
include_guard = "AUTOMERGE_H"
includes = []
language = "C"
line_length = 140

View file

@ -1,22 +0,0 @@
[package]
name = "@PROJECT_NAME@"
version = "@PROJECT_VERSION@"
authors = ["Orion Henry <orion.henry@gmail.com>", "Jason Kankiewicz <jason.kankiewicz@gmail.com>"]
edition = "2021"
license = "MIT"
rust-version = "1.57.0"
[lib]
name = "@BINDINGS_NAME@"
crate-type = ["staticlib"]
bench = false
doc = false
[dependencies]
@LIBRARY_NAME@ = { path = "../@LIBRARY_NAME@" }
hex = "^0.4.3"
libc = "^0.2"
smol_str = "^0.1.21"
[build-dependencies]
cbindgen = "^0.24"

View file

@ -1,48 +0,0 @@
after_includes = """\n
/**
* \\defgroup enumerations Public Enumerations
* Symbolic names for integer constants.
*/
/**
* \\memberof AMdoc
* \\def AM_ROOT
* \\brief The root object of a document.
*/
#define AM_ROOT NULL
/**
* \\memberof AMdoc
* \\def AM_CHANGE_HASH_SIZE
* \\brief The count of bytes in a change hash.
*/
#define AM_CHANGE_HASH_SIZE 32
"""
autogen_warning = """
/**
* \\file
* \\brief All constants, functions and types in the core Automerge C API.
*
* \\warning This file is auto-generated by cbindgen.
*/
"""
documentation = true
documentation_style = "doxy"
include_guard = "@INCLUDE_GUARD_PREFIX@_H"
includes = []
language = "C"
line_length = 140
no_includes = true
style = "both"
sys_includes = ["stdbool.h", "stddef.h", "stdint.h", "time.h"]
usize_is_size_t = true
[enum]
derive_const_casts = true
enum_class = true
must_use = "MUST_USE_ENUM"
prefix_with_name = true
rename_variants = "ScreamingSnakeCase"
[export]
item_types = ["constants", "enums", "functions", "opaque", "structs", "typedefs"]

View file

@ -1,35 +1,14 @@
#ifndef @INCLUDE_GUARD_PREFIX@_CONFIG_H
#define @INCLUDE_GUARD_PREFIX@_CONFIG_H
/**
* \file
* \brief Configuration pararameters defined by the build system.
*
* \warning This file is auto-generated by CMake.
*/
#ifndef @SYMBOL_PREFIX@_CONFIG_H
#define @SYMBOL_PREFIX@_CONFIG_H
/* This header is auto-generated by CMake. */
/**
* \def @SYMBOL_PREFIX@_VERSION
* \brief Denotes a semantic version of the form {MAJOR}{MINOR}{PATCH} as three,
* two-digit decimal numbers without leading zeros (e.g. 100 is 0.1.0).
*/
#define @SYMBOL_PREFIX@_VERSION @INTEGER_PROJECT_VERSION@
/**
* \def @SYMBOL_PREFIX@_MAJOR_VERSION
* \brief Denotes a semantic major version as a decimal number.
*/
#define @SYMBOL_PREFIX@_MAJOR_VERSION (@SYMBOL_PREFIX@_VERSION / 100000)
/**
* \def @SYMBOL_PREFIX@_MINOR_VERSION
* \brief Denotes a semantic minor version as a decimal number.
*/
#define @SYMBOL_PREFIX@_MINOR_VERSION ((@SYMBOL_PREFIX@_VERSION / 100) % 1000)
/**
* \def @SYMBOL_PREFIX@_PATCH_VERSION
* \brief Denotes a semantic patch version as a decimal number.
*/
#define @SYMBOL_PREFIX@_PATCH_VERSION (@SYMBOL_PREFIX@_VERSION % 100)
#endif /* @INCLUDE_GUARD_PREFIX@_CONFIG_H */
#endif /* @SYMBOL_PREFIX@_CONFIG_H */

View file

@ -1,183 +0,0 @@
# This CMake script is used to generate a header and a source file for utility
# functions that convert the tags of generated enum types into strings and
# strings into the tags of generated enum types.
cmake_minimum_required(VERSION 3.23 FATAL_ERROR)
# Seeks the starting line of the source enum's declaration.
macro(seek_enum_mode)
if (line MATCHES "^(typedef[ \t]+)?enum ")
string(REGEX REPLACE "^enum ([0-9a-zA-Z_]+).*$" "\\1" enum_name "${line}")
set(mode "read_tags")
endif()
endmacro()
# Scans the input for the current enum's tags.
macro(read_tags_mode)
if(line MATCHES "^}")
set(mode "generate")
elseif(line MATCHES "^[A-Z0-9_]+.*$")
string(REGEX REPLACE "^([A-Za-z0-9_]+).*$" "\\1" tmp "${line}")
list(APPEND enum_tags "${tmp}")
endif()
endmacro()
macro(write_header_file)
# Generate a to-string function declaration.
list(APPEND header_body
"/**\n"
" * \\ingroup enumerations\n"
" * \\brief Gets the string representation of an `${enum_name}` enum tag.\n"
" *\n"
" * \\param[in] tag An `${enum_name}` enum tag.\n"
" * \\return A null-terminated byte string.\n"
" */\n"
"char const* ${enum_name}ToString(${enum_name} const tag)\;\n"
"\n")
# Generate a from-string function declaration.
list(APPEND header_body
"/**\n"
" * \\ingroup enumerations\n"
" * \\brief Gets an `${enum_name}` enum tag from its string representation.\n"
" *\n"
" * \\param[out] dest An `${enum_name}` enum tag pointer.\n"
" * \\param[in] src A null-terminated byte string.\n"
" * \\return `true` if \\p src matches the string representation of an\n"
" * `${enum_name}` enum tag, `false` otherwise.\n"
" */\n"
"bool ${enum_name}FromString(${enum_name}* dest, char const* const src)\;\n"
"\n")
endmacro()
macro(write_source_file)
# Generate a to-string function implementation.
list(APPEND source_body
"char const* ${enum_name}ToString(${enum_name} const tag) {\n"
" switch (tag) {\n"
" default:\n"
" return \"???\"\;\n")
foreach(label IN LISTS enum_tags)
list(APPEND source_body
" case ${label}:\n"
" return \"${label}\"\;\n")
endforeach()
list(APPEND source_body
" }\n"
"}\n"
"\n")
# Generate a from-string function implementation.
list(APPEND source_body
"bool ${enum_name}FromString(${enum_name}* dest, char const* const src) {\n")
foreach(label IN LISTS enum_tags)
list(APPEND source_body
" if (!strcmp(src, \"${label}\")) {\n"
" *dest = ${label}\;\n"
" return true\;\n"
" }\n")
endforeach()
list(APPEND source_body
" return false\;\n"
"}\n"
"\n")
endmacro()
function(main)
set(header_body "")
# File header and includes.
list(APPEND header_body
"#ifndef ${include_guard}\n"
"#define ${include_guard}\n"
"/**\n"
" * \\file\n"
" * \\brief Utility functions for converting enum tags into null-terminated\n"
" * byte strings and vice versa.\n"
" *\n"
" * \\warning This file is auto-generated by CMake.\n"
" */\n"
"\n"
"#include <stdbool.h>\n"
"\n"
"#include <${library_include}>\n"
"\n")
set(source_body "")
# File includes.
list(APPEND source_body
"/** \\warning This file is auto-generated by CMake. */\n"
"\n"
"#include \"stdio.h\"\n"
"#include \"string.h\"\n"
"\n"
"#include <${header_include}>\n"
"\n")
set(enum_name "")
set(enum_tags "")
set(mode "seek_enum")
file(STRINGS "${input_path}" lines)
foreach(line IN LISTS lines)
string(REGEX REPLACE "^(.+)(//.*)?" "\\1" line "${line}")
string(STRIP "${line}" line)
if(mode STREQUAL "seek_enum")
seek_enum_mode()
elseif(mode STREQUAL "read_tags")
read_tags_mode()
else()
# The end of the enum declaration was reached.
if(NOT enum_name)
# The end of the file was reached.
return()
endif()
if(NOT enum_tags)
message(FATAL_ERROR "No tags found for `${enum_name}`.")
endif()
string(TOLOWER "${enum_name}" output_stem_prefix)
string(CONCAT output_stem "${output_stem_prefix}" "_string")
cmake_path(REPLACE_EXTENSION output_stem "h" OUTPUT_VARIABLE output_header_basename)
write_header_file()
write_source_file()
set(enum_name "")
set(enum_tags "")
set(mode "seek_enum")
endif()
endforeach()
# File footer.
list(APPEND header_body
"#endif /* ${include_guard} */\n")
message(STATUS "Generating header file \"${output_header_path}\"...")
file(WRITE "${output_header_path}" ${header_body})
message(STATUS "Generating source file \"${output_source_path}\"...")
file(WRITE "${output_source_path}" ${source_body})
endfunction()
if(NOT DEFINED PROJECT_NAME)
message(FATAL_ERROR "Variable PROJECT_NAME is not defined.")
elseif(NOT DEFINED LIBRARY_NAME)
message(FATAL_ERROR "Variable LIBRARY_NAME is not defined.")
elseif(NOT DEFINED SUBDIR)
message(FATAL_ERROR "Variable SUBDIR is not defined.")
elseif(${CMAKE_ARGC} LESS 9)
message(FATAL_ERROR "Too few arguments.")
elseif(${CMAKE_ARGC} GREATER 10)
message(FATAL_ERROR "Too many arguments.")
elseif(NOT EXISTS ${CMAKE_ARGV5})
message(FATAL_ERROR "Input header \"${CMAKE_ARGV7}\" not found.")
endif()
cmake_path(CONVERT "${CMAKE_ARGV7}" TO_CMAKE_PATH_LIST input_path NORMALIZE)
cmake_path(CONVERT "${CMAKE_ARGV8}" TO_CMAKE_PATH_LIST output_header_path NORMALIZE)
cmake_path(CONVERT "${CMAKE_ARGV9}" TO_CMAKE_PATH_LIST output_source_path NORMALIZE)
string(TOLOWER "${PROJECT_NAME}" project_root)
cmake_path(CONVERT "${SUBDIR}" TO_CMAKE_PATH_LIST project_subdir NORMALIZE)
string(TOLOWER "${project_subdir}" project_subdir)
string(TOLOWER "${LIBRARY_NAME}" library_stem)
cmake_path(REPLACE_EXTENSION library_stem "h" OUTPUT_VARIABLE library_basename)
string(JOIN "/" library_include "${project_root}" "${library_basename}")
string(TOUPPER "${PROJECT_NAME}" project_name_upper)
string(TOUPPER "${project_subdir}" include_guard_infix)
string(REGEX REPLACE "/" "_" include_guard_infix "${include_guard_infix}")
string(REGEX REPLACE "-" "_" include_guard_prefix "${project_name_upper}")
string(JOIN "_" include_guard_prefix "${include_guard_prefix}" "${include_guard_infix}")
string(JOIN "/" output_header_prefix "${project_root}" "${project_subdir}")
cmake_path(GET output_header_path STEM output_header_stem)
string(TOUPPER "${output_header_stem}" include_guard_stem)
string(JOIN "_" include_guard "${include_guard_prefix}" "${include_guard_stem}" "H")
cmake_path(GET output_header_path FILENAME output_header_basename)
string(JOIN "/" header_include "${output_header_prefix}" "${output_header_basename}")
main()

View file

@ -1,6 +1,4 @@
# This CMake script is used to perform string substitutions within a generated
# file.
cmake_minimum_required(VERSION 3.23 FATAL_ERROR)
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
if(NOT DEFINED MATCH_REGEX)
message(FATAL_ERROR "Variable \"MATCH_REGEX\" is not defined.")

View file

@ -1,6 +1,4 @@
# This CMake script is used to force Cargo to regenerate the header file for the
# core bindings after the out-of-source build directory has been cleaned.
cmake_minimum_required(VERSION 3.23 FATAL_ERROR)
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
if(NOT DEFINED CONDITION)
message(FATAL_ERROR "Variable \"CONDITION\" is not defined.")

View file

@ -1,35 +0,0 @@
find_package(Doxygen OPTIONAL_COMPONENTS dot)
if(DOXYGEN_FOUND)
set(DOXYGEN_ALIASES "installed_headerfile=\\headerfile ${LIBRARY_NAME}.h <${PROJECT_NAME}/${LIBRARY_NAME}.h>")
set(DOXYGEN_GENERATE_LATEX YES)
set(DOXYGEN_PDF_HYPERLINKS YES)
set(DOXYGEN_PROJECT_LOGO "${CMAKE_CURRENT_SOURCE_DIR}/img/brandmark.png")
set(DOXYGEN_SORT_BRIEF_DOCS YES)
set(DOXYGEN_USE_MDFILE_AS_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md")
doxygen_add_docs(
${LIBRARY_NAME}_docs
"${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h"
"${CBINDGEN_TARGET_DIR}/config.h"
"${CBINDGEN_TARGET_DIR}/${UTILS_SUBDIR}/enum_string.h"
"${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/result.h"
"${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/stack_callback_data.h"
"${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/stack.h"
"${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/string.h"
"${CMAKE_SOURCE_DIR}/README.md"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Producing documentation with Doxygen..."
)
# \note A Doxygen input file isn't a file-level dependency so the Doxygen
# command must instead depend upon a target that either outputs the
# file or depends upon it also or it will just output an error message
# when it can't be found.
add_dependencies(${LIBRARY_NAME}_docs ${BINDINGS_NAME}_artifacts ${LIBRARY_NAME}_utilities)
endif()

View file

@ -1,39 +1,41 @@
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
add_executable(
${LIBRARY_NAME}_quickstart
example_quickstart
quickstart.c
)
set_target_properties(${LIBRARY_NAME}_quickstart PROPERTIES LINKER_LANGUAGE C)
set_target_properties(example_quickstart PROPERTIES LINKER_LANGUAGE C)
# \note An imported library's INTERFACE_INCLUDE_DIRECTORIES property can't
# contain a non-existent path so its build-time include directory
# must be specified for all of its dependent targets instead.
target_include_directories(
${LIBRARY_NAME}_quickstart
example_quickstart
PRIVATE "$<BUILD_INTERFACE:${CBINDGEN_INCLUDEDIR}>"
)
target_link_libraries(${LIBRARY_NAME}_quickstart PRIVATE ${LIBRARY_NAME})
target_link_libraries(example_quickstart PRIVATE ${LIBRARY_NAME})
add_dependencies(${LIBRARY_NAME}_quickstart ${BINDINGS_NAME}_artifacts)
add_dependencies(example_quickstart ${LIBRARY_NAME}_artifacts)
if(BUILD_SHARED_LIBS AND WIN32)
add_custom_command(
TARGET ${LIBRARY_NAME}_quickstart
TARGET example_quickstart
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_SHARED_LIBRARY_SUFFIX}
${CMAKE_BINARY_DIR}
${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Copying the DLL built by Cargo into the examples directory..."
VERBATIM
)
endif()
add_custom_command(
TARGET ${LIBRARY_NAME}_quickstart
TARGET example_quickstart
POST_BUILD
COMMAND
${LIBRARY_NAME}_quickstart
example_quickstart
COMMENT
"Running the example quickstart..."
VERBATIM

View file

@ -5,5 +5,5 @@
```shell
cmake -E make_directory automerge-c/build
cmake -S automerge-c -B automerge-c/build
cmake --build automerge-c/build --target automerge_quickstart
cmake --build automerge-c/build --target example_quickstart
```

View file

@ -3,127 +3,144 @@
#include <string.h>
#include <automerge-c/automerge.h>
#include <automerge-c/utils/enum_string.h>
#include <automerge-c/utils/stack.h>
#include <automerge-c/utils/stack_callback_data.h>
#include <automerge-c/utils/string.h>
static bool abort_cb(AMstack**, void*);
static void abort_cb(AMresultStack**, uint8_t);
/**
* \brief Based on https://automerge.github.io/docs/quickstart
*/
int main(int argc, char** argv) {
AMstack* stack = NULL;
AMdoc* doc1;
AMitemToDoc(AMstackItem(&stack, AMcreate(NULL), abort_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc1);
AMobjId const* const cards =
AMitemObjId(AMstackItem(&stack, AMmapPutObject(doc1, AM_ROOT, AMstr("cards"), AM_OBJ_TYPE_LIST), abort_cb,
AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
AMobjId const* const card1 =
AMitemObjId(AMstackItem(&stack, AMlistPutObject(doc1, cards, SIZE_MAX, true, AM_OBJ_TYPE_MAP), abort_cb,
AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
AMstackItem(NULL, AMmapPutStr(doc1, card1, AMstr("title"), AMstr("Rewrite everything in Clojure")), abort_cb,
AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMmapPutBool(doc1, card1, AMstr("done"), false), abort_cb, AMexpect(AM_VAL_TYPE_VOID));
AMobjId const* const card2 =
AMitemObjId(AMstackItem(&stack, AMlistPutObject(doc1, cards, SIZE_MAX, true, AM_OBJ_TYPE_MAP), abort_cb,
AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
AMstackItem(NULL, AMmapPutStr(doc1, card2, AMstr("title"), AMstr("Rewrite everything in Haskell")), abort_cb,
AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMmapPutBool(doc1, card2, AMstr("done"), false), abort_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMcommit(doc1, AMstr("Add card"), NULL), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
AMresultStack* stack = NULL;
AMdoc* const doc1 = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, abort_cb).doc;
AMobjId const* const cards = AMpush(&stack,
AMmapPutObject(doc1, AM_ROOT, "cards", AM_OBJ_TYPE_LIST),
AM_VALUE_OBJ_ID,
abort_cb).obj_id;
AMobjId const* const card1 = AMpush(&stack,
AMlistPutObject(doc1, cards, SIZE_MAX, true, AM_OBJ_TYPE_MAP),
AM_VALUE_OBJ_ID,
abort_cb).obj_id;
AMfree(AMmapPutStr(doc1, card1, "title", "Rewrite everything in Clojure"));
AMfree(AMmapPutBool(doc1, card1, "done", false));
AMobjId const* const card2 = AMpush(&stack,
AMlistPutObject(doc1, cards, SIZE_MAX, true, AM_OBJ_TYPE_MAP),
AM_VALUE_OBJ_ID,
abort_cb).obj_id;
AMfree(AMmapPutStr(doc1, card2, "title", "Rewrite everything in Haskell"));
AMfree(AMmapPutBool(doc1, card2, "done", false));
AMfree(AMcommit(doc1, "Add card", NULL));
AMdoc* doc2;
AMitemToDoc(AMstackItem(&stack, AMcreate(NULL), abort_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc2);
AMstackItem(NULL, AMmerge(doc2, doc1), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
AMdoc* doc2 = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, abort_cb).doc;
AMfree(AMmerge(doc2, doc1));
AMbyteSpan binary;
AMitemToBytes(AMstackItem(&stack, AMsave(doc1), abort_cb, AMexpect(AM_VAL_TYPE_BYTES)), &binary);
AMitemToDoc(AMstackItem(&stack, AMload(binary.src, binary.count), abort_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc2);
AMbyteSpan const binary = AMpush(&stack, AMsave(doc1), AM_VALUE_BYTES, abort_cb).bytes;
doc2 = AMpush(&stack, AMload(binary.src, binary.count), AM_VALUE_DOC, abort_cb).doc;
AMstackItem(NULL, AMmapPutBool(doc1, card1, AMstr("done"), true), abort_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMcommit(doc1, AMstr("Mark card as done"), NULL), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
AMfree(AMmapPutBool(doc1, card1, "done", true));
AMfree(AMcommit(doc1, "Mark card as done", NULL));
AMstackItem(NULL, AMlistDelete(doc2, cards, 0), abort_cb, AMexpect(AM_VAL_TYPE_VOID));
AMstackItem(NULL, AMcommit(doc2, AMstr("Delete card"), NULL), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
AMfree(AMlistDelete(doc2, cards, 0));
AMfree(AMcommit(doc2, "Delete card", NULL));
AMstackItem(NULL, AMmerge(doc1, doc2), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
AMfree(AMmerge(doc1, doc2));
AMitems changes = AMstackItems(&stack, AMgetChanges(doc1, NULL), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE));
AMitem* item = NULL;
while ((item = AMitemsNext(&changes, 1)) != NULL) {
AMchange const* change;
AMitemToChange(item, &change);
AMitems const heads = AMstackItems(&stack, AMitemFromChangeHash(AMchangeHash(change)), abort_cb,
AMexpect(AM_VAL_TYPE_CHANGE_HASH));
char* const c_msg = AMstrdup(AMchangeMessage(change), NULL);
printf("%s %zu\n", c_msg, AMobjSize(doc1, cards, &heads));
free(c_msg);
AMchanges changes = AMpush(&stack, AMgetChanges(doc1, NULL), AM_VALUE_CHANGES, abort_cb).changes;
AMchange const* change = NULL;
while ((change = AMchangesNext(&changes, 1)) != NULL) {
AMbyteSpan const change_hash = AMchangeHash(change);
AMchangeHashes const heads = AMpush(&stack,
AMchangeHashesInit(&change_hash, 1),
AM_VALUE_CHANGE_HASHES,
abort_cb).change_hashes;
printf("%s %ld\n", AMchangeMessage(change), AMobjSize(doc1, cards, &heads));
}
AMstackFree(&stack);
AMfreeStack(&stack);
}
static char const* discriminant_suffix(AMvalueVariant const);
/**
* \brief Examines the result at the top of the given stack and, if it's
* invalid, prints an error message to `stderr`, deallocates all results
* in the stack and exits.
* \brief Prints an error message to `stderr`, deallocates all results in the
* given stack and exits.
*
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
* \param[in] data A pointer to an owned `AMstackCallbackData` struct or `NULL`.
* \return `true` if the top `AMresult` in \p stack is valid, `false` otherwise.
* \pre \p stack `!= NULL`.
* \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
* \param[in] discriminant An `AMvalueVariant` enum tag.
* \pre \p stack` != NULL`.
* \post `*stack == NULL`.
*/
static bool abort_cb(AMstack** stack, void* data) {
static void abort_cb(AMresultStack** stack, uint8_t discriminant) {
static char buffer[512] = {0};
char const* suffix = NULL;
if (!stack) {
suffix = "Stack*";
} else if (!*stack) {
}
else if (!*stack) {
suffix = "Stack";
} else if (!(*stack)->result) {
}
else if (!(*stack)->result) {
suffix = "";
}
if (suffix) {
fprintf(stderr, "Null `AMresult%s*`.\n", suffix);
AMstackFree(stack);
fprintf(stderr, "Null `AMresult%s*`.", suffix);
AMfreeStack(stack);
exit(EXIT_FAILURE);
return false;
return;
}
AMstatus const status = AMresultStatus((*stack)->result);
switch (status) {
case AM_STATUS_ERROR:
strcpy(buffer, "Error");
break;
case AM_STATUS_INVALID_RESULT:
strcpy(buffer, "Invalid result");
break;
case AM_STATUS_OK:
break;
default:
sprintf(buffer, "Unknown `AMstatus` tag %d", status);
case AM_STATUS_ERROR: strcpy(buffer, "Error"); break;
case AM_STATUS_INVALID_RESULT: strcpy(buffer, "Invalid result"); break;
case AM_STATUS_OK: break;
default: sprintf(buffer, "Unknown `AMstatus` tag %d", status);
}
if (buffer[0]) {
char* const c_msg = AMstrdup(AMresultError((*stack)->result), NULL);
fprintf(stderr, "%s; %s.\n", buffer, c_msg);
free(c_msg);
AMstackFree(stack);
fprintf(stderr, "%s; %s.", buffer, AMerrorMessage((*stack)->result));
AMfreeStack(stack);
exit(EXIT_FAILURE);
return false;
return;
}
if (data) {
AMstackCallbackData* sc_data = (AMstackCallbackData*)data;
AMvalType const tag = AMitemValType(AMresultItem((*stack)->result));
if (tag != sc_data->bitmask) {
fprintf(stderr, "Unexpected tag `%s` (%d) instead of `%s` at %s:%d.\n", AMvalTypeToString(tag), tag,
AMvalTypeToString(sc_data->bitmask), sc_data->file, sc_data->line);
free(sc_data);
AMstackFree(stack);
exit(EXIT_FAILURE);
return false;
}
}
free(data);
return true;
AMvalue const value = AMresultValue((*stack)->result);
fprintf(stderr, "Unexpected tag `AM_VALUE_%s` (%d); expected `AM_VALUE_%s`.",
discriminant_suffix(value.tag),
value.tag,
discriminant_suffix(discriminant));
AMfreeStack(stack);
exit(EXIT_FAILURE);
}
/**
* \brief Gets the suffix for a discriminant's corresponding string
* representation.
*
* \param[in] discriminant An `AMvalueVariant` enum tag.
* \return A UTF-8 string.
*/
static char const* discriminant_suffix(AMvalueVariant const discriminant) {
char const* suffix = NULL;
switch (discriminant) {
case AM_VALUE_ACTOR_ID: suffix = "ACTOR_ID"; break;
case AM_VALUE_BOOLEAN: suffix = "BOOLEAN"; break;
case AM_VALUE_BYTES: suffix = "BYTES"; break;
case AM_VALUE_CHANGE_HASHES: suffix = "CHANGE_HASHES"; break;
case AM_VALUE_CHANGES: suffix = "CHANGES"; break;
case AM_VALUE_COUNTER: suffix = "COUNTER"; break;
case AM_VALUE_DOC: suffix = "DOC"; break;
case AM_VALUE_F64: suffix = "F64"; break;
case AM_VALUE_INT: suffix = "INT"; break;
case AM_VALUE_LIST_ITEMS: suffix = "LIST_ITEMS"; break;
case AM_VALUE_MAP_ITEMS: suffix = "MAP_ITEMS"; break;
case AM_VALUE_NULL: suffix = "NULL"; break;
case AM_VALUE_OBJ_ID: suffix = "OBJ_ID"; break;
case AM_VALUE_OBJ_ITEMS: suffix = "OBJ_ITEMS"; break;
case AM_VALUE_STR: suffix = "STR"; break;
case AM_VALUE_STRS: suffix = "STRINGS"; break;
case AM_VALUE_SYNC_MESSAGE: suffix = "SYNC_MESSAGE"; break;
case AM_VALUE_SYNC_STATE: suffix = "SYNC_STATE"; break;
case AM_VALUE_TIMESTAMP: suffix = "TIMESTAMP"; break;
case AM_VALUE_UINT: suffix = "UINT"; break;
case AM_VALUE_VOID: suffix = "VOID"; break;
default: suffix = "...";
}
return suffix;
}

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -1,30 +0,0 @@
#ifndef AUTOMERGE_C_UTILS_RESULT_H
#define AUTOMERGE_C_UTILS_RESULT_H
/**
* \file
* \brief Utility functions for use with `AMresult` structs.
*/
#include <stdarg.h>
#include <automerge-c/automerge.h>
/**
* \brief Transfers the items within an arbitrary list of results into a
* new result in their order of specification.
* \param[in] count The count of subsequent arguments.
* \param[in] ... A \p count list of arguments, each of which is a pointer to
* an `AMresult` struct whose items will be transferred out of it
* and which is subsequently freed.
* \return A pointer to an `AMresult` struct or `NULL`.
* \pre `𝑥 ` \p ... `, AMresultStatus(𝑥) == AM_STATUS_OK`
* \post `(𝑥 ` \p ... `, AMresultStatus(𝑥) != AM_STATUS_OK) -> NULL`
* \attention All `AMresult` struct pointer arguments are passed to
* `AMresultFree()` regardless of success; use `AMresultCat()`
* instead if you wish to pass them to `AMresultFree()` yourself.
* \warning The returned `AMresult` struct pointer must be passed to
* `AMresultFree()` in order to avoid a memory leak.
*/
AMresult* AMresultFrom(int count, ...);
#endif /* AUTOMERGE_C_UTILS_RESULT_H */

View file

@ -1,130 +0,0 @@
#ifndef AUTOMERGE_C_UTILS_STACK_H
#define AUTOMERGE_C_UTILS_STACK_H
/**
* \file
* \brief Utility data structures and functions for hiding `AMresult` structs,
* managing their lifetimes, and automatically applying custom
* validation logic to the `AMitem` structs that they contain.
*
* \note The `AMstack` struct and its related functions drastically reduce the
* need for boilerplate code and/or `goto` statement usage within a C
* application but a higher-level programming language offers even better
* ways to do the same things.
*/
#include <automerge-c/automerge.h>
/**
* \struct AMstack
* \brief A node in a singly-linked list of result pointers.
*/
typedef struct AMstack {
/** A result to be deallocated. */
AMresult* result;
/** The previous node in the singly-linked list or `NULL`. */
struct AMstack* prev;
} AMstack;
/**
* \memberof AMstack
* \brief The prototype of a function that examines the result at the top of
* the given stack in terms of some arbitrary data.
*
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
* \param[in] data A pointer to arbitrary data or `NULL`.
* \return `true` if the top `AMresult` struct in \p stack is valid, `false`
* otherwise.
* \pre \p stack `!= NULL`.
*/
typedef bool (*AMstackCallback)(AMstack** stack, void* data);
/**
* \memberof AMstack
* \brief Deallocates the storage for a stack of results.
*
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
* \pre \p stack `!= NULL`
* \post `*stack == NULL`
*/
void AMstackFree(AMstack** stack);
/**
* \memberof AMstack
* \brief Gets a result from the stack after removing it.
*
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
* \param[in] result A pointer to the `AMresult` to be popped or `NULL` to
* select the top result in \p stack.
* \return A pointer to an `AMresult` struct or `NULL`.
* \pre \p stack `!= NULL`
* \warning The returned `AMresult` struct pointer must be passed to
* `AMresultFree()` in order to avoid a memory leak.
*/
AMresult* AMstackPop(AMstack** stack, AMresult const* result);
/**
* \memberof AMstack
* \brief Pushes the given result onto the given stack, calls the given
* callback with the given data to validate it and then either gets the
* result if it's valid or gets `NULL` instead.
*
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
* \param[in] result A pointer to an `AMresult` struct.
* \param[in] callback A pointer to a function with the same signature as
* `AMstackCallback()` or `NULL`.
* \param[in] data A pointer to arbitrary data or `NULL` which is passed to
* \p callback.
* \return \p result or `NULL`.
* \warning If \p stack `== NULL` then \p result is deallocated in order to
* avoid a memory leak.
*/
AMresult* AMstackResult(AMstack** stack, AMresult* result, AMstackCallback callback, void* data);
/**
* \memberof AMstack
* \brief Pushes the given result onto the given stack, calls the given
* callback with the given data to validate it and then either gets the
* first item in the sequence of items within that result if it's valid
* or gets `NULL` instead.
*
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
* \param[in] result A pointer to an `AMresult` struct.
* \param[in] callback A pointer to a function with the same signature as
* `AMstackCallback()` or `NULL`.
* \param[in] data A pointer to arbitrary data or `NULL` which is passed to
* \p callback.
* \return A pointer to an `AMitem` struct or `NULL`.
* \warning If \p stack `== NULL` then \p result is deallocated in order to
* avoid a memory leak.
*/
AMitem* AMstackItem(AMstack** stack, AMresult* result, AMstackCallback callback, void* data);
/**
* \memberof AMstack
* \brief Pushes the given result onto the given stack, calls the given
* callback with the given data to validate it and then either gets an
* `AMitems` struct over the sequence of items within that result if it's
* valid or gets an empty `AMitems` instead.
*
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
* \param[in] result A pointer to an `AMresult` struct.
* \param[in] callback A pointer to a function with the same signature as
* `AMstackCallback()` or `NULL`.
* \param[in] data A pointer to arbitrary data or `NULL` which is passed to
* \p callback.
* \return An `AMitems` struct.
* \warning If \p stack `== NULL` then \p result is deallocated immediately
* in order to avoid a memory leak.
*/
AMitems AMstackItems(AMstack** stack, AMresult* result, AMstackCallback callback, void* data);
/**
* \memberof AMstack
* \brief Gets the count of results that have been pushed onto the stack.
*
* \param[in,out] stack A pointer to an `AMstack` struct.
* \return A 64-bit unsigned integer.
*/
size_t AMstackSize(AMstack const* const stack);
#endif /* AUTOMERGE_C_UTILS_STACK_H */

View file

@ -1,53 +0,0 @@
#ifndef AUTOMERGE_C_UTILS_PUSH_CALLBACK_DATA_H
#define AUTOMERGE_C_UTILS_PUSH_CALLBACK_DATA_H
/**
* \file
* \brief Utility data structures, functions and macros for supplying
* parameters to the custom validation logic applied to `AMitem`
* structs.
*/
#include <automerge-c/automerge.h>
/**
* \struct AMstackCallbackData
* \brief A data structure for passing the parameters of an item value test
* to an implementation of the `AMstackCallback` function prototype.
*/
typedef struct {
/** A bitmask of `AMvalType` tags. */
AMvalType bitmask;
/** A null-terminated file path string. */
char const* file;
/** The ordinal number of a line within a file. */
int line;
} AMstackCallbackData;
/**
* \memberof AMstackCallbackData
* \brief Allocates a new `AMstackCallbackData` struct and initializes its
* members from their corresponding arguments.
*
* \param[in] bitmask A bitmask of `AMvalType` tags.
* \param[in] file A null-terminated file path string.
* \param[in] line The ordinal number of a line within a file.
* \return A pointer to a disowned `AMstackCallbackData` struct.
* \warning The returned pointer must be passed to `free()` to avoid a memory
* leak.
*/
AMstackCallbackData* AMstackCallbackDataInit(AMvalType const bitmask, char const* const file, int const line);
/**
* \memberof AMstackCallbackData
* \def AMexpect
* \brief Allocates a new `AMstackCallbackData` struct and initializes it from
* an `AMvalueType` bitmask.
*
* \param[in] bitmask A bitmask of `AMvalType` tags.
* \return A pointer to a disowned `AMstackCallbackData` struct.
* \warning The returned pointer must be passed to `free()` to avoid a memory
* leak.
*/
#define AMexpect(bitmask) AMstackCallbackDataInit(bitmask, __FILE__, __LINE__)
#endif /* AUTOMERGE_C_UTILS_PUSH_CALLBACK_DATA_H */

View file

@ -1,29 +0,0 @@
#ifndef AUTOMERGE_C_UTILS_STRING_H
#define AUTOMERGE_C_UTILS_STRING_H
/**
* \file
* \brief Utility functions for use with `AMbyteSpan` structs that provide
* UTF-8 string views.
*/
#include <automerge-c/automerge.h>
/**
* \memberof AMbyteSpan
* \brief Returns a pointer to a null-terminated byte string which is a
* duplicate of the given UTF-8 string view except for the substitution
* of its NUL (0) characters with the specified null-terminated byte
* string.
*
* \param[in] str A UTF-8 string view as an `AMbyteSpan` struct.
* \param[in] nul A null-terminated byte string to substitute for NUL characters
* or `NULL` to substitute `"\\0"` for NUL characters.
* \return A disowned null-terminated byte string.
* \pre \p str.src `!= NULL`
* \pre \p str.count `<= sizeof(`\p str.src `)`
* \warning The returned pointer must be passed to `free()` to avoid a memory
* leak.
*/
char* AMstrdup(AMbyteSpan const str, char const* nul);
#endif /* AUTOMERGE_C_UTILS_STRING_H */

Some files were not shown because too many files have changed in this diff Show more