Compare commits
5 commits
Author | SHA1 | Date | |
---|---|---|---|
|
8e83310ea3 | ||
|
149ab50b3e | ||
|
f06a3e3928 | ||
|
3bb4bd79b9 | ||
|
a09de2302b |
482 changed files with 14310 additions and 76174 deletions
2
.github/workflows/advisory-cron.yaml
vendored
2
.github/workflows/advisory-cron.yaml
vendored
|
@ -1,4 +1,4 @@
|
|||
name: Advisories
|
||||
name: ci
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 18 * * *'
|
||||
|
|
97
.github/workflows/ci.yaml
vendored
97
.github/workflows/ci.yaml
vendored
|
@ -1,11 +1,11 @@
|
|||
name: CI
|
||||
on:
|
||||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- experiment
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- experiment
|
||||
jobs:
|
||||
fmt:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -14,8 +14,7 @@ jobs:
|
|||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.67.0
|
||||
default: true
|
||||
toolchain: stable
|
||||
components: rustfmt
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- run: ./scripts/ci/fmt
|
||||
|
@ -28,8 +27,7 @@ jobs:
|
|||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.67.0
|
||||
default: true
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- run: ./scripts/ci/lint
|
||||
|
@ -42,14 +40,9 @@ jobs:
|
|||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.67.0
|
||||
default: true
|
||||
toolchain: stable
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- name: Build rust docs
|
||||
run: ./scripts/ci/rust-docs
|
||||
shell: bash
|
||||
- name: Install doxygen
|
||||
run: sudo apt-get install -y doxygen
|
||||
- run: ./scripts/ci/docs
|
||||
shell: bash
|
||||
|
||||
cargo-deny:
|
||||
|
@ -64,88 +57,31 @@ jobs:
|
|||
- uses: actions/checkout@v2
|
||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||
with:
|
||||
arguments: '--manifest-path ./rust/Cargo.toml'
|
||||
command: check ${{ matrix.checks }}
|
||||
|
||||
wasm_tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- 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/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
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- 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: Install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
- name: run tests
|
||||
run: ./scripts/ci/js_tests
|
||||
|
||||
cmake_build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly-2023-01-26
|
||||
default: true
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- name: Install CMocka
|
||||
run: sudo apt-get install -y libcmocka-dev
|
||||
- name: Install/update CMake
|
||||
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
|
||||
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
toolchain:
|
||||
- 1.67.0
|
||||
- stable
|
||||
- nightly
|
||||
continue-on-error: ${{ matrix.toolchain == 'nightly' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
default: true
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- run: ./scripts/ci/build-test
|
||||
shell: bash
|
||||
|
@ -157,8 +93,7 @@ jobs:
|
|||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.67.0
|
||||
default: true
|
||||
toolchain: stable
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- run: ./scripts/ci/build-test
|
||||
shell: bash
|
||||
|
@ -170,8 +105,8 @@ jobs:
|
|||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.67.0
|
||||
default: true
|
||||
toolchain: stable
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- run: ./scripts/ci/build-test
|
||||
shell: bash
|
||||
|
||||
|
|
52
.github/workflows/docs.yaml
vendored
52
.github/workflows/docs.yaml
vendored
|
@ -1,52 +0,0 @@
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
name: Documentation
|
||||
|
||||
jobs:
|
||||
deploy-docs:
|
||||
concurrency: deploy-docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Cache
|
||||
uses: Swatinem/rust-cache@v1
|
||||
|
||||
- name: Clean docs dir
|
||||
run: rm -rf docs
|
||||
shell: bash
|
||||
|
||||
- name: Clean Rust docs dir
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clean
|
||||
args: --manifest-path ./rust/Cargo.toml --doc
|
||||
|
||||
- name: Build Rust docs
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: doc
|
||||
args: --manifest-path ./rust/Cargo.toml --workspace --all-features --no-deps
|
||||
|
||||
- name: Move Rust docs
|
||||
run: mkdir -p docs && mv rust/target/doc/* docs/.
|
||||
shell: bash
|
||||
|
||||
- name: Configure root page
|
||||
run: echo '<meta http-equiv="refresh" content="0; url=automerge">' > docs/index.html
|
||||
|
||||
- name: Deploy docs
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./docs
|
214
.github/workflows/release.yaml
vendored
214
.github/workflows/release.yaml
vendored
|
@ -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
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,6 +1,4 @@
|
|||
/target
|
||||
/.direnv
|
||||
perf.*
|
||||
/Cargo.lock
|
||||
build/
|
||||
.vim/*
|
||||
/target
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
[workspace]
|
||||
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 = 3
|
||||
|
||||
[profile.bench]
|
||||
debug = true
|
||||
debug = true
|
13
Makefile
Normal file
13
Makefile
Normal file
|
@ -0,0 +1,13 @@
|
|||
rust:
|
||||
cd automerge && cargo test
|
||||
|
||||
wasm:
|
||||
cd automerge-wasm && yarn
|
||||
cd automerge-wasm && yarn build
|
||||
cd automerge-wasm && yarn test
|
||||
cd automerge-wasm && yarn link
|
||||
|
||||
js: wasm
|
||||
cd automerge-js && yarn
|
||||
cd automerge-js && yarn link "automerge-wasm"
|
||||
cd automerge-js && yarn test
|
188
README.md
188
README.md
|
@ -1,147 +1,81 @@
|
|||
# Automerge
|
||||
# Automerge - NEXT
|
||||
|
||||
<img src='./img/sign.svg' width='500' alt='Automerge logo' />
|
||||
This is pretty much a ground up rewrite of automerge-rs. The objective of this
|
||||
rewrite is to radically simplify the API. The end goal being to produce a library
|
||||
which is easy to work with both in Rust and from FFI.
|
||||
|
||||
[](https://automerge.org/)
|
||||
[](https://automerge.org/automerge-rs/automerge/)
|
||||
[](https://github.com/automerge/automerge-rs/actions/workflows/ci.yaml)
|
||||
[](https://github.com/automerge/automerge-rs/actions/workflows/docs.yaml)
|
||||
## How?
|
||||
|
||||
Automerge is a library which provides fast implementations of several different
|
||||
CRDTs, a compact compression format for these CRDTs, and a sync protocol for
|
||||
efficiently transmitting those changes over the network. The objective of the
|
||||
project is to support [local-first](https://www.inkandswitch.com/local-first/) applications in the same way that relational
|
||||
databases support server applications - by providing mechanisms for persistence
|
||||
which allow application developers to avoid thinking about hard distributed
|
||||
computing problems. Automerge aims to be PostgreSQL for your local-first app.
|
||||
The current iteration of automerge-rs is complicated to work with because it
|
||||
adopts the frontend/backend split architecture of the JS implementation. This
|
||||
architecture was necessary due to basic operations on the automerge opset being
|
||||
too slow to perform on the UI thread. Recently @orionz has been able to improve
|
||||
the performance to the point where the split is no longer necessary. This means
|
||||
we can adopt a much simpler mutable API.
|
||||
|
||||
If you're looking for documentation on the JavaScript implementation take a look
|
||||
at https://automerge.org/docs/hello/. There are other implementations in both
|
||||
Rust and C, but they are earlier and don't have documentation yet. You can find
|
||||
them in `rust/automerge` and `rust/automerge-c` if you are comfortable
|
||||
reading the code and tests to figure out how to use them.
|
||||
|
||||
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)
|
||||
The architecture is now built around the `OpTree`. This is a data structure
|
||||
which supports efficiently inserting new operations and realising values of
|
||||
existing operations. Most interactions with the `OpTree` are in the form of
|
||||
implementations of `TreeQuery` - a trait which can be used to traverse the
|
||||
optree and producing state of some kind. User facing operations are exposed on
|
||||
an `Automerge` object, under the covers these operations typically instantiate
|
||||
some `TreeQuery` and run it over the `OpTree`.
|
||||
|
||||
## Status
|
||||
|
||||
This project is formed of a core Rust implementation which is exposed via FFI in
|
||||
javascript+WASM, C, and soon other languages. Alex
|
||||
([@alexjg](https://github.com/alexjg/)]) is working full time on maintaining
|
||||
automerge, other members of Ink and Switch are also contributing time and there
|
||||
are several other maintainers. The focus is currently on shipping the new JS
|
||||
package. We expect to be iterating the API and adding new features over the next
|
||||
six months so there will likely be several major version bumps in all packages
|
||||
in that time.
|
||||
We have working code which passes all of the tests in the JS test suite. We're
|
||||
now working on writing a bunch more tests and cleaning up the API.
|
||||
|
||||
In general we try and respect semver.
|
||||
## Development
|
||||
|
||||
### JavaScript
|
||||
### Running CI
|
||||
|
||||
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
|
||||
The steps CI will run are all defined in `./scripts/ci`. Obviously CI will run
|
||||
everything when you submit a PR, but if you want to run everything locally
|
||||
before you push you can run `./scripts/ci/run` to run everything.
|
||||
|
||||
### Rust
|
||||
### Running the JS tests
|
||||
|
||||
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)
|
||||
You will need to have [node](https://nodejs.org/en/), [yarn](https://yarnpkg.com/getting-started/install), [rust](https://rustup.rs/) and [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/) installed.
|
||||
|
||||
## Repository Organisation
|
||||
To build and test the rust library:
|
||||
|
||||
- `./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`
|
||||
internally but presents a more idiomatic javascript interface
|
||||
- `./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
|
||||
|
||||
## Building
|
||||
|
||||
To build this codebase you will need:
|
||||
|
||||
- `rust`
|
||||
- `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
|
||||
```shell
|
||||
$ cd automerge
|
||||
$ cargo test
|
||||
```
|
||||
|
||||
If your build fails to find `cmocka.h` you may need to teach it about homebrew's
|
||||
installation location:
|
||||
To build and test the wasm library:
|
||||
|
||||
```
|
||||
export CPATH=/opt/homebrew/include
|
||||
export LIBRARY_PATH=/opt/homebrew/lib
|
||||
./scripts/ci/run
|
||||
```shell
|
||||
## setup
|
||||
$ cd automerge-wasm
|
||||
$ yarn
|
||||
|
||||
## building or testing
|
||||
$ yarn build
|
||||
$ yarn test
|
||||
|
||||
## without this the js library wont automatically use changes
|
||||
$ yarn link
|
||||
|
||||
## cutting a release or doing benchmarking
|
||||
$ yarn release
|
||||
$ yarn opt ## or set `wasm-opt = false` in Cargo.toml on supported platforms (not arm64 osx)
|
||||
```
|
||||
|
||||
## Contributing
|
||||
And finally to test the js library. This is where most of the tests reside.
|
||||
|
||||
Please try and split your changes up into relatively independent commits which
|
||||
change one subsystem at a time and add good commit messages which describe what
|
||||
the change is and why you're making it (err on the side of longer commit
|
||||
messages). `git blame` should give future maintainers a good idea of why
|
||||
something is the way it is.
|
||||
```shell
|
||||
## setup
|
||||
$ cd automerge-js
|
||||
$ yarn
|
||||
$ yarn link "automerge-wasm"
|
||||
|
||||
## testing
|
||||
$ yarn test
|
||||
```
|
||||
|
||||
## Benchmarking
|
||||
|
||||
The `edit-trace` folder has the main code for running the edit trace benchmarking.
|
||||
|
|
20
TODO.md
Normal file
20
TODO.md
Normal file
|
@ -0,0 +1,20 @@
|
|||
|
||||
### next steps:
|
||||
1. C API
|
||||
|
||||
### ergronomics:
|
||||
1. value() -> () or something that into's a value
|
||||
|
||||
### automerge:
|
||||
1. single pass (fast) load
|
||||
2. micro-patches / bare bones observation API / fully hydrated documents
|
||||
|
||||
### sync
|
||||
1. get all sync tests passing
|
||||
|
||||
### maybe:
|
||||
1. tables
|
||||
|
||||
### no:
|
||||
1. cursors
|
||||
|
2
automerge-js/.gitignore
vendored
Normal file
2
automerge-js/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/node_modules
|
||||
/yarn.lock
|
18
automerge-js/package.json
Normal file
18
automerge-js/package.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "automerge-js",
|
||||
"version": "0.1.0",
|
||||
"main": "src/index.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "mocha --bail --full-trace"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "^9.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"automerge-wasm": "file:../automerge-wasm/dev",
|
||||
"fast-sha256": "^1.3.0",
|
||||
"pako": "^2.0.4",
|
||||
"uuid": "^8.3"
|
||||
}
|
||||
}
|
18
automerge-js/src/constants.js
Normal file
18
automerge-js/src/constants.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
// 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
|
||||
const STATE = Symbol('_state') // object containing metadata about current state (e.g. sequence numbers)
|
||||
const HEADS = Symbol('_heads') // object containing metadata about current state (e.g. sequence numbers)
|
||||
const OBJECT_ID = Symbol('_objectId') // object containing metadata about current state (e.g. sequence numbers)
|
||||
const READ_ONLY = Symbol('_readOnly') // object containing metadata about current state (e.g. sequence numbers)
|
||||
const FROZEN = Symbol('_frozen') // object containing metadata about current state (e.g. sequence numbers)
|
||||
|
||||
// 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
|
||||
|
||||
module.exports = {
|
||||
STATE, HEADS, OBJECT_ID, READ_ONLY, FROZEN
|
||||
}
|
|
@ -1,16 +1,12 @@
|
|||
import { Automerge, type ObjID, type Prop } from "@automerge/automerge-wasm"
|
||||
import { COUNTER } from "./constants"
|
||||
/**
|
||||
* The most basic CRDT: an integer value that can be changed only by
|
||||
* incrementing and decrementing. Since addition of integers is commutative,
|
||||
* the value trivially converges.
|
||||
*/
|
||||
export class Counter {
|
||||
value: number
|
||||
|
||||
constructor(value?: number) {
|
||||
class Counter {
|
||||
constructor(value) {
|
||||
this.value = value || 0
|
||||
Reflect.defineProperty(this, COUNTER, { value: true })
|
||||
Object.freeze(this)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -21,7 +17,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() {
|
||||
return this.value
|
||||
}
|
||||
|
||||
|
@ -30,7 +26,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() {
|
||||
return this.valueOf().toString()
|
||||
}
|
||||
|
||||
|
@ -38,7 +34,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() {
|
||||
return this.value
|
||||
}
|
||||
}
|
||||
|
@ -48,32 +44,13 @@ export class Counter {
|
|||
* callback.
|
||||
*/
|
||||
class WriteableCounter extends Counter {
|
||||
context: Automerge
|
||||
path: Prop[]
|
||||
objectId: ObjID
|
||||
key: Prop
|
||||
|
||||
constructor(
|
||||
value: number,
|
||||
context: Automerge,
|
||||
path: Prop[],
|
||||
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
|
||||
this.context.increment(this.objectId, this.key, delta)
|
||||
increment(delta) {
|
||||
delta = typeof delta === 'number' ? delta : 1
|
||||
this.context.inc(this.objectId, this.key, delta)
|
||||
this.value += delta
|
||||
return this.value
|
||||
}
|
||||
|
@ -82,8 +59,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) {
|
||||
return this.inc(typeof delta === 'number' ? -delta : -1)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,15 +70,15 @@ 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 {
|
||||
return new WriteableCounter(value, context, path, objectId, key)
|
||||
*/
|
||||
function getWriteableCounter(value, context, path, objectId, key) {
|
||||
const instance = Object.create(WriteableCounter.prototype)
|
||||
instance.value = value
|
||||
instance.context = context
|
||||
instance.path = path
|
||||
instance.objectId = objectId
|
||||
instance.key = key
|
||||
return instance
|
||||
}
|
||||
|
||||
//module.exports = { Counter, getWriteableCounter }
|
||||
module.exports = { Counter, getWriteableCounter }
|
372
automerge-js/src/index.js
Normal file
372
automerge-js/src/index.js
Normal file
|
@ -0,0 +1,372 @@
|
|||
const AutomergeWASM = require("automerge-wasm")
|
||||
const uuid = require('./uuid')
|
||||
|
||||
let { rootProxy, listProxy, textProxy, mapProxy } = require("./proxies")
|
||||
let { Counter } = require("./counter")
|
||||
let { Text } = require("./text")
|
||||
let { Int, Uint, Float64 } = require("./numbers")
|
||||
let { STATE, HEADS, OBJECT_ID, READ_ONLY, FROZEN } = require("./constants")
|
||||
|
||||
function init(actor) {
|
||||
const state = AutomergeWASM.init(actor)
|
||||
return rootProxy(state, true);
|
||||
}
|
||||
|
||||
function clone(doc) {
|
||||
const state = doc[STATE].clone()
|
||||
return rootProxy(state, true);
|
||||
}
|
||||
|
||||
function free(doc) {
|
||||
return doc[STATE].free()
|
||||
}
|
||||
|
||||
function from(data, actor) {
|
||||
let doc1 = init(actor)
|
||||
let doc2 = change(doc1, (d) => Object.assign(d, data))
|
||||
return doc2
|
||||
}
|
||||
|
||||
function change(doc, options, callback) {
|
||||
if (callback === undefined) {
|
||||
// FIXME implement options
|
||||
callback = options
|
||||
options = {}
|
||||
}
|
||||
if (typeof options === "string") {
|
||||
options = { message: options }
|
||||
}
|
||||
if (doc === undefined || doc[STATE] === undefined || doc[OBJECT_ID] !== "_root") {
|
||||
throw new RangeError("must be the document root");
|
||||
}
|
||||
if (doc[FROZEN] === true) {
|
||||
throw new RangeError("Attempting to use an outdated Automerge document")
|
||||
}
|
||||
if (!!doc[HEADS] === true) {
|
||||
console.log("HEADS", doc[HEADS])
|
||||
throw new RangeError("Attempting to change an out of date document");
|
||||
}
|
||||
if (doc[READ_ONLY] === false) {
|
||||
throw new RangeError("Calls to Automerge.change cannot be nested")
|
||||
}
|
||||
const state = doc[STATE]
|
||||
const heads = state.getHeads()
|
||||
try {
|
||||
doc[HEADS] = heads
|
||||
doc[FROZEN] = true
|
||||
let root = rootProxy(state);
|
||||
callback(root)
|
||||
if (state.pending_ops() === 0) {
|
||||
doc[FROZEN] = false
|
||||
doc[HEADS] = undefined
|
||||
return doc
|
||||
} else {
|
||||
state.commit(options.message, options.time)
|
||||
return rootProxy(state, true);
|
||||
}
|
||||
} catch (e) {
|
||||
//console.log("ERROR: ",e)
|
||||
doc[FROZEN] = false
|
||||
doc[HEADS] = undefined
|
||||
state.rollback()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
function emptyChange(doc, options) {
|
||||
if (options === undefined) {
|
||||
options = {}
|
||||
}
|
||||
if (typeof options === "string") {
|
||||
options = { message: options }
|
||||
}
|
||||
|
||||
if (doc === undefined || doc[STATE] === undefined || doc[OBJECT_ID] !== "_root") {
|
||||
throw new RangeError("must be the document root");
|
||||
}
|
||||
if (doc[FROZEN] === true) {
|
||||
throw new RangeError("Attempting to use an outdated Automerge document")
|
||||
}
|
||||
if (doc[READ_ONLY] === false) {
|
||||
throw new RangeError("Calls to Automerge.change cannot be nested")
|
||||
}
|
||||
|
||||
const state = doc[STATE]
|
||||
state.commit(options.message, options.time)
|
||||
return rootProxy(state, true);
|
||||
}
|
||||
|
||||
function load(data, actor) {
|
||||
const state = AutomergeWASM.load(data, actor)
|
||||
return rootProxy(state, true);
|
||||
}
|
||||
|
||||
function save(doc) {
|
||||
const state = doc[STATE]
|
||||
return state.save()
|
||||
}
|
||||
|
||||
function merge(local, remote) {
|
||||
if (local[HEADS] === true) {
|
||||
throw new RangeError("Attempting to change an out of date document");
|
||||
}
|
||||
const localState = local[STATE]
|
||||
const heads = localState.getHeads()
|
||||
const remoteState = remote[STATE]
|
||||
const changes = localState.getChangesAdded(remoteState)
|
||||
localState.applyChanges(changes)
|
||||
local[HEADS] = heads
|
||||
return rootProxy(localState, true)
|
||||
}
|
||||
|
||||
function getActorId(doc) {
|
||||
const state = doc[STATE]
|
||||
return state.getActorId()
|
||||
}
|
||||
|
||||
function conflictAt(context, objectId, prop) {
|
||||
let values = context.values(objectId, prop)
|
||||
if (values.length <= 1) {
|
||||
return
|
||||
}
|
||||
let result = {}
|
||||
for (const conflict of values) {
|
||||
const datatype = conflict[0]
|
||||
const value = conflict[1]
|
||||
switch (datatype) {
|
||||
case "map":
|
||||
result[value] = mapProxy(context, value, [ prop ], true, true)
|
||||
break;
|
||||
case "list":
|
||||
result[value] = listProxy(context, value, [ prop ], true, true)
|
||||
break;
|
||||
case "text":
|
||||
result[value] = textProxy(context, value, [ prop ], true, true)
|
||||
break;
|
||||
//case "table":
|
||||
//case "cursor":
|
||||
case "str":
|
||||
case "uint":
|
||||
case "int":
|
||||
case "f64":
|
||||
case "boolean":
|
||||
case "bytes":
|
||||
case "null":
|
||||
result[conflict[2]] = value
|
||||
break;
|
||||
case "counter":
|
||||
result[conflict[2]] = new Counter(value)
|
||||
break;
|
||||
case "timestamp":
|
||||
result[conflict[2]] = new Date(value)
|
||||
break;
|
||||
default:
|
||||
throw RangeError(`datatype ${datatype} unimplemented`)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function getConflicts(doc, prop) {
|
||||
const state = doc[STATE]
|
||||
const objectId = doc[OBJECT_ID]
|
||||
return conflictAt(state, objectId, prop)
|
||||
}
|
||||
|
||||
function getLastLocalChange(doc) {
|
||||
const state = doc[STATE]
|
||||
return state.getLastLocalChange()
|
||||
}
|
||||
|
||||
function getObjectId(doc) {
|
||||
return doc[OBJECT_ID]
|
||||
}
|
||||
|
||||
function getChanges(oldState, newState) {
|
||||
const o = oldState[STATE]
|
||||
const n = newState[STATE]
|
||||
const heads = oldState[HEADS]
|
||||
return n.getChanges(heads || o.getHeads())
|
||||
}
|
||||
|
||||
function getAllChanges(doc) {
|
||||
const state = doc[STATE]
|
||||
return state.getChanges([])
|
||||
}
|
||||
|
||||
function applyChanges(doc, changes) {
|
||||
if (doc === undefined || doc[STATE] === undefined || doc[OBJECT_ID] !== "_root") {
|
||||
throw new RangeError("must be the document root");
|
||||
}
|
||||
if (doc[FROZEN] === true) {
|
||||
throw new RangeError("Attempting to use an outdated Automerge document")
|
||||
}
|
||||
if (doc[READ_ONLY] === false) {
|
||||
throw new RangeError("Calls to Automerge.change cannot be nested")
|
||||
}
|
||||
const state = doc[STATE]
|
||||
const heads = state.getHeads()
|
||||
state.applyChanges(changes)
|
||||
doc[HEADS] = heads
|
||||
return [rootProxy(state, true)];
|
||||
}
|
||||
|
||||
function getHistory(doc) {
|
||||
const actor = getActorId(doc)
|
||||
const history = getAllChanges(doc)
|
||||
return history.map((change, index) => ({
|
||||
get change () {
|
||||
return decodeChange(change)
|
||||
},
|
||||
get snapshot () {
|
||||
const [state] = applyChanges(init(), history.slice(0, index + 1))
|
||||
return state
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function equals() {
|
||||
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
|
||||
}
|
||||
|
||||
function encodeSyncMessage(msg) {
|
||||
return AutomergeWASM.encodeSyncMessage(msg)
|
||||
}
|
||||
|
||||
function decodeSyncMessage(msg) {
|
||||
return AutomergeWASM.decodeSyncMessage(msg)
|
||||
}
|
||||
|
||||
function encodeSyncState(state) {
|
||||
return AutomergeWASM.encodeSyncState(state)
|
||||
}
|
||||
|
||||
function decodeSyncState() {
|
||||
return AutomergeWASM.decodeSyncState(state)
|
||||
}
|
||||
|
||||
function generateSyncMessage(doc, syncState) {
|
||||
const state = doc[STATE]
|
||||
return [ syncState, state.generateSyncMessage(syncState) ]
|
||||
}
|
||||
|
||||
function receiveSyncMessage(doc, syncState, message) {
|
||||
if (doc === undefined || doc[STATE] === undefined || doc[OBJECT_ID] !== "_root") {
|
||||
throw new RangeError("must be the document root");
|
||||
}
|
||||
if (doc[FROZEN] === true) {
|
||||
throw new RangeError("Attempting to use an outdated Automerge document")
|
||||
}
|
||||
if (!!doc[HEADS] === true) {
|
||||
throw new RangeError("Attempting to change an out of date document");
|
||||
}
|
||||
if (doc[READ_ONLY] === false) {
|
||||
throw new RangeError("Calls to Automerge.change cannot be nested")
|
||||
}
|
||||
const state = doc[STATE]
|
||||
const heads = state.getHeads()
|
||||
state.receiveSyncMessage(syncState, message)
|
||||
doc[HEADS] = heads
|
||||
return [rootProxy(state, true), syncState, null];
|
||||
}
|
||||
|
||||
function initSyncState() {
|
||||
return AutomergeWASM.initSyncState(change)
|
||||
}
|
||||
|
||||
function encodeChange(change) {
|
||||
return AutomergeWASM.encodeChange(change)
|
||||
}
|
||||
|
||||
function decodeChange(data) {
|
||||
return AutomergeWASM.decodeChange(data)
|
||||
}
|
||||
|
||||
function encodeSyncMessage(change) {
|
||||
return AutomergeWASM.encodeSyncMessage(change)
|
||||
}
|
||||
|
||||
function decodeSyncMessage(data) {
|
||||
return AutomergeWASM.decodeSyncMessage(data)
|
||||
}
|
||||
|
||||
function encodeSyncState(change) {
|
||||
return AutomergeWASM.encodeSyncState(change)
|
||||
}
|
||||
|
||||
function decodeSyncState(data) {
|
||||
return AutomergeWASM.decodeSyncState(data)
|
||||
}
|
||||
|
||||
function getMissingDeps(doc, heads) {
|
||||
const state = doc[STATE]
|
||||
if (!heads) {
|
||||
heads = []
|
||||
}
|
||||
return state.getMissingDeps(heads)
|
||||
}
|
||||
|
||||
function getHeads(doc) {
|
||||
const state = doc[STATE]
|
||||
return doc[HEADS] || state.getHeads()
|
||||
}
|
||||
|
||||
function dump(doc) {
|
||||
const state = doc[STATE]
|
||||
state.dump()
|
||||
}
|
||||
|
||||
function toJS(doc) {
|
||||
if (typeof doc === "object") {
|
||||
if (doc instanceof Uint8Array) {
|
||||
return doc
|
||||
}
|
||||
if (doc === null) {
|
||||
return doc
|
||||
}
|
||||
if (doc instanceof Array) {
|
||||
return doc.map((a) => toJS(a))
|
||||
}
|
||||
if (doc instanceof Text) {
|
||||
return doc.map((a) => toJS(a))
|
||||
}
|
||||
let tmp = {}
|
||||
for (index in doc) {
|
||||
tmp[index] = toJS(doc[index])
|
||||
}
|
||||
return tmp
|
||||
} else {
|
||||
return doc
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init, from, change, emptyChange, clone, free,
|
||||
load, save, merge, getChanges, getAllChanges, applyChanges,
|
||||
getLastLocalChange, getObjectId, getActorId, getConflicts,
|
||||
encodeChange, decodeChange, equals, getHistory, getHeads, uuid,
|
||||
generateSyncMessage, receiveSyncMessage, initSyncState,
|
||||
decodeSyncMessage, encodeSyncMessage, decodeSyncState, encodeSyncState,
|
||||
getMissingDeps,
|
||||
dump, Text, Counter, Int, Uint, Float64, toJS,
|
||||
}
|
||||
|
||||
// depricated
|
||||
// Frontend, setDefaultBackend, Backend
|
||||
|
||||
// more...
|
||||
/*
|
||||
for (let name of ['getObjectId', 'getObjectById',
|
||||
'setActorId',
|
||||
'Text', 'Table', 'Counter', 'Observable' ]) {
|
||||
module.exports[name] = Frontend[name]
|
||||
}
|
||||
*/
|
33
automerge-js/src/numbers.js
Normal file
33
automerge-js/src/numbers.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
// Convience classes to allow users to stricly specify the number type they want
|
||||
|
||||
class Int {
|
||||
constructor(value) {
|
||||
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
|
||||
Object.freeze(this)
|
||||
}
|
||||
}
|
||||
|
||||
class Uint {
|
||||
constructor(value) {
|
||||
if (!(Number.isInteger(value) && value <= Number.MAX_SAFE_INTEGER && value >= 0)) {
|
||||
throw new RangeError(`Value ${value} cannot be a uint`)
|
||||
}
|
||||
this.value = value
|
||||
Object.freeze(this)
|
||||
}
|
||||
}
|
||||
|
||||
class Float64 {
|
||||
constructor(value) {
|
||||
if (typeof value !== 'number') {
|
||||
throw new RangeError(`Value ${value} cannot be a float64`)
|
||||
}
|
||||
this.value = value || 0.0
|
||||
Object.freeze(this)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Int, Uint, Float64 }
|
623
automerge-js/src/proxies.js
Normal file
623
automerge-js/src/proxies.js
Normal file
|
@ -0,0 +1,623 @@
|
|||
|
||||
const AutomergeWASM = require("automerge-wasm")
|
||||
const { Int, Uint, Float64 } = require("./numbers");
|
||||
const { Counter, getWriteableCounter } = require("./counter");
|
||||
const { Text } = require("./text");
|
||||
const { STATE, HEADS, FROZEN, OBJECT_ID, READ_ONLY } = require("./constants")
|
||||
const { MAP, LIST, TABLE, TEXT } = require("automerge-wasm")
|
||||
|
||||
function parseListIndex(key) {
|
||||
if (typeof key === 'string' && /^[0-9]+$/.test(key)) key = parseInt(key, 10)
|
||||
if (typeof key !== 'number') {
|
||||
// throw new TypeError('A list index must be a number, but you passed ' + JSON.stringify(key))
|
||||
return key
|
||||
}
|
||||
if (key < 0 || isNaN(key) || key === Infinity || key === -Infinity) {
|
||||
throw new RangeError('A list index must be positive, but you passed ' + key)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
function valueAt(target, prop) {
|
||||
const { context, objectId, path, readonly, heads} = target
|
||||
let value = context.value(objectId, prop, heads)
|
||||
if (value === undefined) {
|
||||
return
|
||||
}
|
||||
const datatype = value[0]
|
||||
const val = value[1]
|
||||
switch (datatype) {
|
||||
case undefined: return;
|
||||
case "map": return mapProxy(context, val, [ ... path, prop ], readonly, heads);
|
||||
case "list": return listProxy(context, val, [ ... path, prop ], readonly, heads);
|
||||
case "text": return textProxy(context, val, [ ... path, prop ], readonly, heads);
|
||||
//case "table":
|
||||
//case "cursor":
|
||||
case "str": return val;
|
||||
case "uint": return val;
|
||||
case "int": return val;
|
||||
case "f64": return val;
|
||||
case "boolean": return val;
|
||||
case "null": return null;
|
||||
case "bytes": return val;
|
||||
case "counter": {
|
||||
if (readonly) {
|
||||
return new Counter(val);
|
||||
} else {
|
||||
return getWriteableCounter(val, context, path, objectId, prop)
|
||||
}
|
||||
}
|
||||
case "timestamp": return new Date(val);
|
||||
default:
|
||||
throw RangeError(`datatype ${datatype} unimplemented`)
|
||||
}
|
||||
}
|
||||
|
||||
function import_value(value) {
|
||||
switch (typeof value) {
|
||||
case 'object':
|
||||
if (value == null) {
|
||||
return [ null, "null"]
|
||||
} else if (value instanceof Uint) {
|
||||
return [ value.value, "uint" ]
|
||||
} else if (value instanceof Int) {
|
||||
return [ value.value, "int" ]
|
||||
} else if (value instanceof Float64) {
|
||||
return [ value.value, "f64" ]
|
||||
} else if (value instanceof Counter) {
|
||||
return [ value.value, "counter" ]
|
||||
} else if (value instanceof Date) {
|
||||
return [ value.getTime(), "timestamp" ]
|
||||
} else if (value instanceof Uint8Array) {
|
||||
return [ value, "bytes" ]
|
||||
} else if (value instanceof Array) {
|
||||
return [ value, "list" ]
|
||||
} else if (value instanceof Text) {
|
||||
return [ value, "text" ]
|
||||
} else if (value[OBJECT_ID]) {
|
||||
throw new RangeError('Cannot create a reference to an existing document object')
|
||||
} else {
|
||||
return [ value, "map" ]
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
return [ value, "boolean" ]
|
||||
case 'number':
|
||||
if (Number.isInteger(value)) {
|
||||
return [ value, "int" ]
|
||||
} else {
|
||||
return [ value, "f64" ]
|
||||
}
|
||||
break;
|
||||
case 'string':
|
||||
return [ value ]
|
||||
break;
|
||||
default:
|
||||
throw new RangeError(`Unsupported type of value: ${typeof value}`)
|
||||
}
|
||||
}
|
||||
|
||||
const MapHandler = {
|
||||
get (target, key) {
|
||||
const { context, objectId, path, readonly, frozen, heads } = target
|
||||
if (key === Symbol.toStringTag) { return target[Symbol.toStringTag] }
|
||||
if (key === OBJECT_ID) return objectId
|
||||
if (key === READ_ONLY) return readonly
|
||||
if (key === FROZEN) return frozen
|
||||
if (key === HEADS) return heads
|
||||
if (key === STATE) return context;
|
||||
return valueAt(target, key)
|
||||
},
|
||||
|
||||
set (target, key, val) {
|
||||
let { context, objectId, path, readonly, frozen} = target
|
||||
if (val && val[OBJECT_ID]) {
|
||||
throw new RangeError('Cannot create a reference to an existing document object')
|
||||
}
|
||||
if (key === FROZEN) {
|
||||
target.frozen = val
|
||||
return
|
||||
}
|
||||
if (key === HEADS) {
|
||||
target.heads = val
|
||||
return
|
||||
}
|
||||
let [ value, datatype ] = import_value(val)
|
||||
if (frozen) {
|
||||
throw new RangeError("Attempting to use an outdated Automerge document")
|
||||
}
|
||||
if (readonly) {
|
||||
throw new RangeError(`Object property "${key}" cannot be modified`)
|
||||
}
|
||||
switch (datatype) {
|
||||
case "list":
|
||||
const list = context.set(objectId, key, LIST)
|
||||
const proxyList = listProxy(context, list, [ ... path, key ], readonly );
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
proxyList[i] = value[i]
|
||||
}
|
||||
break;
|
||||
case "text":
|
||||
const text = context.set(objectId, key, TEXT)
|
||||
const proxyText = textProxy(context, text, [ ... path, key ], readonly );
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
proxyText[i] = value.get(i)
|
||||
}
|
||||
break;
|
||||
case "map":
|
||||
const map = context.set(objectId, key, MAP)
|
||||
const proxyMap = mapProxy(context, map, [ ... path, key ], readonly );
|
||||
for (const key in value) {
|
||||
proxyMap[key] = value[key]
|
||||
}
|
||||
break;
|
||||
default:
|
||||
context.set(objectId, key, value, datatype)
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
deleteProperty (target, key) {
|
||||
const { context, objectId, path, readonly, frozen } = target
|
||||
if (readonly) {
|
||||
throw new RangeError(`Object property "${key}" cannot be modified`)
|
||||
}
|
||||
context.del(objectId, key)
|
||||
return true
|
||||
},
|
||||
|
||||
has (target, key) {
|
||||
const value = this.get(target, key)
|
||||
return value !== undefined
|
||||
},
|
||||
|
||||
getOwnPropertyDescriptor (target, key) {
|
||||
const { context, objectId } = target
|
||||
const value = this.get(target, key)
|
||||
if (typeof value !== 'undefined') {
|
||||
return {
|
||||
configurable: true, enumerable: true, value
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
ownKeys (target) {
|
||||
const { context, objectId, heads} = target
|
||||
return context.keys(objectId, heads)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
const ListHandler = {
|
||||
get (target, index) {
|
||||
const {context, objectId, path, readonly, frozen, heads } = target
|
||||
index = parseListIndex(index)
|
||||
if (index === Symbol.hasInstance) { return (instance) => { return [].has(instance) } }
|
||||
if (index === Symbol.toStringTag) { return target[Symbol.toStringTag] }
|
||||
if (index === OBJECT_ID) return objectId
|
||||
if (index === READ_ONLY) return readonly
|
||||
if (index === FROZEN) return frozen
|
||||
if (index === HEADS) return heads
|
||||
if (index === STATE) return context;
|
||||
if (index === 'length') return context.length(objectId, heads);
|
||||
if (index === Symbol.iterator) {
|
||||
let i = 0;
|
||||
return function *() {
|
||||
// FIXME - ugly
|
||||
let value = valueAt(target, i)
|
||||
while (value !== undefined) {
|
||||
yield value
|
||||
i += 1
|
||||
value = valueAt(target, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof index === 'number') {
|
||||
return valueAt(target, index)
|
||||
} else {
|
||||
return listMethods(target)[index]
|
||||
}
|
||||
},
|
||||
|
||||
set (target, index, val) {
|
||||
let {context, objectId, path, readonly, frozen } = target
|
||||
index = parseListIndex(index)
|
||||
if (val && val[OBJECT_ID]) {
|
||||
throw new RangeError('Cannot create a reference to an existing document object')
|
||||
}
|
||||
if (index === FROZEN) {
|
||||
target.frozen = val
|
||||
return
|
||||
}
|
||||
if (index === HEADS) {
|
||||
target.heads = val
|
||||
return
|
||||
}
|
||||
if (typeof index == "string") {
|
||||
throw new RangeError('list index must be a number')
|
||||
}
|
||||
const [ value, datatype] = import_value(val)
|
||||
if (frozen) {
|
||||
throw new RangeError("Attempting to use an outdated Automerge document")
|
||||
}
|
||||
if (readonly) {
|
||||
throw new RangeError(`Object property "${index}" cannot be modified`)
|
||||
}
|
||||
switch (datatype) {
|
||||
case "list":
|
||||
let list
|
||||
if (index >= context.length(objectId)) {
|
||||
list = context.insert(objectId, index, LIST)
|
||||
} else {
|
||||
list = context.set(objectId, index, LIST)
|
||||
}
|
||||
const proxyList = listProxy(context, list, [ ... path, index ], readonly);
|
||||
proxyList.splice(0,0,...value)
|
||||
break;
|
||||
case "text":
|
||||
let text
|
||||
if (index >= context.length(objectId)) {
|
||||
text = context.insert(objectId, index, TEXT)
|
||||
} else {
|
||||
text = context.set(objectId, index, TEXT)
|
||||
}
|
||||
const proxyText = textProxy(context, text, [ ... path, index ], readonly);
|
||||
proxyText.splice(0,0,...value)
|
||||
break;
|
||||
case "map":
|
||||
let map
|
||||
if (index >= context.length(objectId)) {
|
||||
map = context.insert(objectId, index, MAP)
|
||||
} else {
|
||||
map = context.set(objectId, index, MAP)
|
||||
}
|
||||
const proxyMap = mapProxy(context, map, [ ... path, index ], readonly);
|
||||
for (const key in value) {
|
||||
proxyMap[key] = value[key]
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (index >= context.length(objectId)) {
|
||||
context.insert(objectId, index, value, datatype)
|
||||
} else {
|
||||
context.set(objectId, index, value, datatype)
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
deleteProperty (target, index) {
|
||||
const {context, objectId} = target
|
||||
index = parseListIndex(index)
|
||||
if (context.value(objectId, index)[0] == "counter") {
|
||||
throw new TypeError('Unsupported operation: deleting a counter from a list')
|
||||
}
|
||||
context.del(objectId, index)
|
||||
return true
|
||||
},
|
||||
|
||||
has (target, index) {
|
||||
const {context, objectId, heads} = target
|
||||
index = parseListIndex(index)
|
||||
if (typeof index === 'number') {
|
||||
return index < context.length(objectId, heads)
|
||||
}
|
||||
return index === 'length'
|
||||
},
|
||||
|
||||
getOwnPropertyDescriptor (target, index) {
|
||||
const {context, objectId, path, readonly, frozen, heads} = target
|
||||
|
||||
if (index === 'length') return {writable: true, value: context.length(objectId, heads) }
|
||||
if (index === OBJECT_ID) return {configurable: false, enumerable: false, value: objectId}
|
||||
|
||||
index = parseListIndex(index)
|
||||
|
||||
let value = valueAt(target, index)
|
||||
return { configurable: true, enumerable: true, value }
|
||||
},
|
||||
|
||||
getPrototypeOf(target) { return Object.getPrototypeOf([]) },
|
||||
ownKeys (target) {
|
||||
const {context, objectId, heads } = target
|
||||
let keys = []
|
||||
// uncommenting this causes assert.deepEqual() to fail when comparing to a pojo array
|
||||
// but not uncommenting it causes for (i in list) {} to not enumerate values properly
|
||||
//for (let i = 0; i < target.context.length(objectId, heads); i++) { keys.push(i.toString()) }
|
||||
keys.push("length");
|
||||
return keys
|
||||
}
|
||||
}
|
||||
|
||||
const TextHandler = Object.assign({}, ListHandler, {
|
||||
get (target, index) {
|
||||
// FIXME this is a one line change from ListHandler.get()
|
||||
const {context, objectId, path, readonly, frozen, heads } = target
|
||||
index = parseListIndex(index)
|
||||
if (index === Symbol.toStringTag) { return target[Symbol.toStringTag] }
|
||||
if (index === Symbol.hasInstance) { return (instance) => { return [].has(instance) } }
|
||||
if (index === OBJECT_ID) return objectId
|
||||
if (index === READ_ONLY) return readonly
|
||||
if (index === FROZEN) return frozen
|
||||
if (index === HEADS) return heads
|
||||
if (index === STATE) return context;
|
||||
if (index === 'length') return context.length(objectId, heads);
|
||||
if (index === Symbol.iterator) {
|
||||
let i = 0;
|
||||
return function *() {
|
||||
let value = valueAt(target, i)
|
||||
while (value !== undefined) {
|
||||
yield value
|
||||
i += 1
|
||||
value = valueAt(target, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof index === 'number') {
|
||||
return valueAt(target, index)
|
||||
} else {
|
||||
return textMethods(target)[index] || listMethods(target)[index]
|
||||
}
|
||||
},
|
||||
getPrototypeOf(target) {
|
||||
return Object.getPrototypeOf(new Text())
|
||||
},
|
||||
})
|
||||
|
||||
function mapProxy(context, objectId, path, readonly, heads) {
|
||||
return new Proxy({context, objectId, path, readonly: !!readonly, frozen: false, heads}, MapHandler)
|
||||
}
|
||||
|
||||
function listProxy(context, objectId, path, readonly, heads) {
|
||||
let target = []
|
||||
Object.assign(target, {context, objectId, path, readonly: !!readonly, frozen: false, heads})
|
||||
return new Proxy(target, ListHandler)
|
||||
}
|
||||
|
||||
function textProxy(context, objectId, path, readonly, heads) {
|
||||
let target = []
|
||||
Object.assign(target, {context, objectId, path, readonly: !!readonly, frozen: false, heads})
|
||||
return new Proxy(target, TextHandler)
|
||||
}
|
||||
|
||||
function rootProxy(context, readonly) {
|
||||
return mapProxy(context, "_root", [], readonly, false)
|
||||
}
|
||||
|
||||
function listMethods(target) {
|
||||
const {context, objectId, path, readonly, frozen, heads} = target
|
||||
const methods = {
|
||||
deleteAt(index, numDelete) {
|
||||
// FIXME - what about many deletes?
|
||||
if (context.value(objectId, index)[0] == "counter") {
|
||||
throw new TypeError('Unsupported operation: deleting a counter from a list')
|
||||
}
|
||||
if (typeof numDelete === 'number') {
|
||||
context.splice(objectId, index, numDelete)
|
||||
} else {
|
||||
context.del(objectId, index)
|
||||
}
|
||||
return this
|
||||
},
|
||||
|
||||
fill(val, start, end) {
|
||||
// FIXME
|
||||
let list = context.getObject(objectId)
|
||||
let [value, datatype] = valueAt(target, index)
|
||||
for (let index = parseListIndex(start || 0); index < parseListIndex(end || list.length); index++) {
|
||||
context.set(objectId, index, value, datatype)
|
||||
}
|
||||
return this
|
||||
},
|
||||
|
||||
indexOf(o, start = 0) {
|
||||
// FIXME
|
||||
const id = o[OBJECT_ID]
|
||||
if (id) {
|
||||
const list = context.getObject(objectId)
|
||||
for (let index = start; index < list.length; index++) {
|
||||
if (list[index][OBJECT_ID] === id) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
} else {
|
||||
return context.indexOf(objectId, o, start)
|
||||
}
|
||||
},
|
||||
|
||||
insertAt(index, ...values) {
|
||||
this.splice(index, 0, ...values)
|
||||
return this
|
||||
},
|
||||
|
||||
pop() {
|
||||
let length = context.length(objectId)
|
||||
if (length == 0) {
|
||||
return undefined
|
||||
}
|
||||
let last = valueAt(target, length - 1)
|
||||
context.del(objectId, length - 1)
|
||||
return last
|
||||
},
|
||||
|
||||
push(...values) {
|
||||
let len = context.length(objectId)
|
||||
this.splice(len, 0, ...values)
|
||||
return context.length(objectId)
|
||||
},
|
||||
|
||||
shift() {
|
||||
if (context.length(objectId) == 0) return
|
||||
const first = valueAt(target, 0)
|
||||
context.del(objectId, 0)
|
||||
return first
|
||||
},
|
||||
|
||||
splice(index, del, ...vals) {
|
||||
index = parseListIndex(index)
|
||||
del = parseListIndex(del)
|
||||
for (let val of vals) {
|
||||
if (val && val[OBJECT_ID]) {
|
||||
throw new RangeError('Cannot create a reference to an existing document object')
|
||||
}
|
||||
}
|
||||
if (frozen) {
|
||||
throw new RangeError("Attempting to use an outdated Automerge document")
|
||||
}
|
||||
if (readonly) {
|
||||
throw new RangeError("Sequence object cannot be modified outside of a change block")
|
||||
}
|
||||
let result = []
|
||||
for (let i = 0; i < del; i++) {
|
||||
let value = valueAt(target, index)
|
||||
result.push(value)
|
||||
context.del(objectId, index)
|
||||
}
|
||||
const values = vals.map((val) => import_value(val))
|
||||
for (let [value,datatype] of values) {
|
||||
switch (datatype) {
|
||||
case "list":
|
||||
const list = context.insert(objectId, index, LIST)
|
||||
const proxyList = listProxy(context, list, [ ... path, index ], readonly);
|
||||
proxyList.splice(0,0,...value)
|
||||
break;
|
||||
case "text":
|
||||
const text = context.insert(objectId, index, TEXT)
|
||||
const proxyText = textProxy(context, text, [ ... path, index ], readonly);
|
||||
proxyText.splice(0,0,...value)
|
||||
break;
|
||||
case "map":
|
||||
const map = context.insert(objectId, index, MAP)
|
||||
const proxyMap = mapProxy(context, map, [ ... path, index ], readonly);
|
||||
for (const key in value) {
|
||||
proxyMap[key] = value[key]
|
||||
}
|
||||
break;
|
||||
default:
|
||||
context.insert(objectId, index, value, datatype)
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
return result
|
||||
},
|
||||
|
||||
unshift(...values) {
|
||||
this.splice(0, 0, ...values)
|
||||
return context.length(objectId)
|
||||
},
|
||||
|
||||
entries() {
|
||||
let i = 0;
|
||||
const iterator = {
|
||||
next: () => {
|
||||
let value = valueAt(target, i)
|
||||
if (value === undefined) {
|
||||
return { value: undefined, done: true }
|
||||
} else {
|
||||
return { value: [ i, value ], done: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
return iterator
|
||||
},
|
||||
|
||||
keys() {
|
||||
let i = 0;
|
||||
let len = context.length(objectId, heads)
|
||||
const iterator = {
|
||||
next: () => {
|
||||
let value = undefined
|
||||
if (i < len) { value = i; i++ }
|
||||
return { value, done: true }
|
||||
}
|
||||
}
|
||||
return iterator
|
||||
},
|
||||
|
||||
values() {
|
||||
let i = 0;
|
||||
const iterator = {
|
||||
next: () => {
|
||||
let value = valueAt(target, i)
|
||||
if (value === undefined) {
|
||||
return { value: undefined, done: true }
|
||||
} else {
|
||||
return { value, done: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
return iterator
|
||||
}
|
||||
}
|
||||
|
||||
// Read-only methods that can delegate to the JavaScript built-in implementations
|
||||
// FIXME - super slow
|
||||
for (let method of ['concat', 'every', 'filter', 'find', 'findIndex', 'forEach', 'includes',
|
||||
'join', 'lastIndexOf', 'map', 'reduce', 'reduceRight',
|
||||
'slice', 'some', 'toLocaleString', 'toString']) {
|
||||
methods[method] = (...args) => {
|
||||
const list = []
|
||||
while (true) {
|
||||
let value = valueAt(target, list.length)
|
||||
if (value == undefined) {
|
||||
break
|
||||
}
|
||||
list.push(value)
|
||||
}
|
||||
|
||||
return list[method](...args)
|
||||
}
|
||||
}
|
||||
|
||||
return methods
|
||||
}
|
||||
|
||||
function textMethods(target) {
|
||||
const {context, objectId, path, readonly, frozen} = target
|
||||
const methods = {
|
||||
set (index, value) {
|
||||
return this[index] = value
|
||||
},
|
||||
get (index) {
|
||||
return this[index]
|
||||
},
|
||||
toString () {
|
||||
let str = ''
|
||||
let length = this.length
|
||||
for (let i = 0; i < length; i++) {
|
||||
const value = this.get(i)
|
||||
if (typeof value === 'string') str += value
|
||||
}
|
||||
return str
|
||||
},
|
||||
toSpans () {
|
||||
let spans = []
|
||||
let chars = ''
|
||||
let length = this.length
|
||||
for (let i = 0; i < length; i++) {
|
||||
const value = this[i]
|
||||
if (typeof value === 'string') {
|
||||
chars += value
|
||||
} else {
|
||||
if (chars.length > 0) {
|
||||
spans.push(chars)
|
||||
chars = ''
|
||||
}
|
||||
spans.push(value)
|
||||
}
|
||||
}
|
||||
if (chars.length > 0) {
|
||||
spans.push(chars)
|
||||
}
|
||||
return spans
|
||||
},
|
||||
toJSON () {
|
||||
return this.toString()
|
||||
}
|
||||
}
|
||||
return methods
|
||||
}
|
||||
|
||||
|
||||
module.exports = { rootProxy, textProxy, listProxy, mapProxy, MapHandler, ListHandler, TextHandler }
|
|
@ -20,7 +20,7 @@
|
|||
const Backend = {} //require('./backend')
|
||||
const { hexStringToBytes, bytesToHexString, Encoder, Decoder } = require('./encoding')
|
||||
const { decodeChangeMeta } = require('./columnar')
|
||||
const { copyObject } = require('./common')
|
||||
const { copyObject } = require('../src/common')
|
||||
|
||||
const HASH_SIZE = 32 // 256 bits = 32 bytes
|
||||
const MESSAGE_TYPE_SYNC = 0x42 // first byte of a sync message, for identification
|
132
automerge-js/src/text.js
Normal file
132
automerge-js/src/text.js
Normal file
|
@ -0,0 +1,132 @@
|
|||
const { OBJECT_ID } = require('./constants')
|
||||
const { isObject } = require('../src/common')
|
||||
|
||||
class Text {
|
||||
constructor (text) {
|
||||
const instance = Object.create(Text.prototype)
|
||||
if (typeof text === 'string') {
|
||||
instance.elems = [...text]
|
||||
} else if (Array.isArray(text)) {
|
||||
instance.elems = text
|
||||
} else if (text === undefined) {
|
||||
instance.elems = []
|
||||
} else {
|
||||
throw new TypeError(`Unsupported initial value for Text: ${text}`)
|
||||
}
|
||||
return instance
|
||||
}
|
||||
|
||||
get length () {
|
||||
return this.elems.length
|
||||
}
|
||||
|
||||
get (index) {
|
||||
return this.elems[index]
|
||||
}
|
||||
|
||||
getElemId (index) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over the text elements character by character, including any
|
||||
* inline objects.
|
||||
*/
|
||||
[Symbol.iterator] () {
|
||||
let elems = this.elems, index = -1
|
||||
return {
|
||||
next () {
|
||||
index += 1
|
||||
if (index < elems.length) {
|
||||
return {done: false, value: elems[index]}
|
||||
} else {
|
||||
return {done: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content of the Text object as a simple string, ignoring any
|
||||
* non-character elements.
|
||||
*/
|
||||
toString() {
|
||||
// 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
|
||||
let str = ''
|
||||
for (const elem of this.elems) {
|
||||
if (typeof elem === 'string') str += elem
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* 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']
|
||||
*/
|
||||
toSpans() {
|
||||
let spans = []
|
||||
let chars = ''
|
||||
for (const elem of this.elems) {
|
||||
if (typeof elem === 'string') {
|
||||
chars += elem
|
||||
} else {
|
||||
if (chars.length > 0) {
|
||||
spans.push(chars)
|
||||
chars = ''
|
||||
}
|
||||
spans.push(elem)
|
||||
}
|
||||
}
|
||||
if (chars.length > 0) {
|
||||
spans.push(chars)
|
||||
}
|
||||
return spans
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content of the Text object as a simple string, so that the
|
||||
* JSON serialization of an Automerge document represents text nicely.
|
||||
*/
|
||||
toJSON() {
|
||||
return this.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the list item at position `index` to a new value `value`.
|
||||
*/
|
||||
set (index, value) {
|
||||
this.elems[index] = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts new list items `values` starting at position `index`.
|
||||
*/
|
||||
insertAt(index, ...values) {
|
||||
this.elems.splice(index, 0, ... values)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes `numDelete` list items starting at position `index`.
|
||||
* if `numDelete` is not given, one item is deleted.
|
||||
*/
|
||||
deleteAt(index, numDelete = 1) {
|
||||
this.elems.splice(index, numDelete)
|
||||
}
|
||||
}
|
||||
|
||||
// Read-only methods that can delegate to the JavaScript built-in array
|
||||
for (let method of ['concat', 'every', 'filter', 'find', 'findIndex', 'forEach', 'includes',
|
||||
'indexOf', 'join', 'lastIndexOf', 'map', 'reduce', 'reduceRight',
|
||||
'slice', 'some', 'toLocaleString']) {
|
||||
Text.prototype[method] = function (...args) {
|
||||
const array = [...this]
|
||||
return array[method](...args)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Text }
|
16
automerge-js/src/uuid.js
Normal file
16
automerge-js/src/uuid.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
const { v4: uuid } = require('uuid')
|
||||
|
||||
function defaultFactory() {
|
||||
return uuid().replace(/-/g, '')
|
||||
}
|
||||
|
||||
let factory = defaultFactory
|
||||
|
||||
function makeUuid() {
|
||||
return factory()
|
||||
}
|
||||
|
||||
makeUuid.setFactory = newFactory => { factory = newFactory }
|
||||
makeUuid.reset = () => { factory = defaultFactory }
|
||||
|
||||
module.exports = makeUuid
|
164
automerge-js/test/basic_test.js
Normal file
164
automerge-js/test/basic_test.js
Normal file
|
@ -0,0 +1,164 @@
|
|||
|
||||
const assert = require('assert')
|
||||
const util = require('util')
|
||||
const Automerge = require('..')
|
||||
|
||||
describe('Automerge', () => {
|
||||
describe('basics', () => {
|
||||
it('should init clone and free', () => {
|
||||
let doc1 = Automerge.init()
|
||||
let doc2 = Automerge.clone(doc1);
|
||||
})
|
||||
|
||||
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('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
|
||||
})
|
||||
|
||||
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()
|
||||
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] });
|
||||
})
|
||||
})
|
||||
})
|
97
automerge-js/test/columnar_test.js
Normal file
97
automerge-js/test/columnar_test.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
const assert = require('assert')
|
||||
const { checkEncoded } = require('./helpers')
|
||||
const Automerge = require('..')
|
||||
const { encodeChange, decodeChange } = Automerge
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,21 +1,16 @@
|
|||
import * as assert from "assert"
|
||||
import { Encoder } from "./legacy/encoding"
|
||||
const assert = require('assert')
|
||||
const { Encoder } = require('../src/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 }
|
1394
automerge-js/test/legacy_tests.js
Normal file
1394
automerge-js/test/legacy_tests.js
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
697
automerge-js/test/text_test.js
Normal file
697
automerge-js/test/text_test.js
Normal file
|
@ -0,0 +1,697 @@
|
|||
const assert = require('assert')
|
||||
const Automerge = require('..')
|
||||
const { assertEqualsOneOf } = require('./helpers')
|
||||
|
||||
function attributeStateToAttributes(accumulatedAttributes) {
|
||||
const attributes = {}
|
||||
Object.entries(accumulatedAttributes).forEach(([key, values]) => {
|
||||
if (values.length && values[0] !== null) {
|
||||
attributes[key] = values[0]
|
||||
}
|
||||
})
|
||||
return attributes
|
||||
}
|
||||
|
||||
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 = {}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 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.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(), 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(), 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(), 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')
|
||||
})
|
||||
})
|
||||
|
||||
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' })
|
||||
})
|
||||
})
|
||||
|
||||
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 exclude control characters from toString()', () => {
|
||||
assert.strictEqual(s1.text.toString(), 'a')
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
|
||||
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 reader!')
|
||||
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(), 'Hello reader!')
|
||||
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), '🐦')
|
||||
})
|
||||
})
|
32
automerge-js/test/uuid_test.js
Normal file
32
automerge-js/test/uuid_test.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
const assert = require('assert')
|
||||
const Automerge = require('..')
|
||||
|
||||
const uuid = Automerge.uuid
|
||||
|
||||
describe('uuid', () => {
|
||||
afterEach(() => {
|
||||
uuid.reset()
|
||||
})
|
||||
|
||||
describe('default implementation', () => {
|
||||
it('generates unique values', () => {
|
||||
assert.notEqual(uuid(), uuid())
|
||||
})
|
||||
})
|
||||
|
||||
describe('custom implementation', () => {
|
||||
let counter
|
||||
|
||||
function customUuid() {
|
||||
return `custom-uuid-${counter++}`
|
||||
}
|
||||
|
||||
before(() => uuid.setFactory(customUuid))
|
||||
beforeEach(() => counter = 0)
|
||||
|
||||
it('invokes the custom factory', () => {
|
||||
assert.equal(uuid(), 'custom-uuid-0')
|
||||
assert.equal(uuid(), 'custom-uuid-1')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,6 +1,5 @@
|
|||
/node_modules
|
||||
/bundler
|
||||
/nodejs
|
||||
/deno
|
||||
/dev
|
||||
/target
|
||||
Cargo.lock
|
||||
yarn.lock
|
|
@ -2,14 +2,13 @@
|
|||
[package]
|
||||
name = "automerge-wasm"
|
||||
description = "An js/wasm wrapper for the rust implementation of automerge-backend"
|
||||
repository = "https://github.com/automerge/automerge-rs"
|
||||
# repository = "https://github.com/automerge/automerge-rs"
|
||||
version = "0.1.0"
|
||||
authors = ["Alex Good <alex@memoryandthought.me>","Orion Henry <orion@inkandswitch.com>", "Martin Kleppmann"]
|
||||
categories = ["wasm"]
|
||||
readme = "README.md"
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
license = "MIT"
|
||||
rust-version = "1.57.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib","rlib"]
|
||||
|
@ -22,27 +21,25 @@ default = ["console_error_panic_hook"]
|
|||
[dependencies]
|
||||
console_error_panic_hook = { version = "^0.1", optional = true }
|
||||
# wee_alloc = { version = "^0.4", optional = true }
|
||||
automerge = { path = "../automerge", features=["wasm"] }
|
||||
automerge = { path = "../automerge" }
|
||||
js-sys = "^0.3"
|
||||
serde = "^1.0"
|
||||
serde_json = "^1.0"
|
||||
rand = { version = "^0.8.4" }
|
||||
getrandom = { version = "^0.2.2", features=["js"] }
|
||||
uuid = { version = "^1.2.1", features=["v4", "js", "serde"] }
|
||||
serde-wasm-bindgen = "0.4.3"
|
||||
uuid = { version = "^0.8.2", features=["v4", "wasm-bindgen", "serde"] }
|
||||
serde-wasm-bindgen = "0.1.3"
|
||||
serde_bytes = "0.11.5"
|
||||
unicode-segmentation = "1.7.1"
|
||||
hex = "^0.4.3"
|
||||
regex = "^1.5"
|
||||
itertools = "^0.10.3"
|
||||
thiserror = "^1.0.16"
|
||||
|
||||
[dependencies.wasm-bindgen]
|
||||
version = "^0.2.83"
|
||||
version = "^0.2"
|
||||
#features = ["std"]
|
||||
features = ["serde-serialize", "std"]
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
# wasm-opt = false
|
||||
wasm-opt = false
|
||||
|
||||
[package.metadata.wasm-pack.profile.profiling]
|
||||
wasm-opt = false
|
||||
|
@ -57,6 +54,5 @@ features = ["console"]
|
|||
|
||||
[dev-dependencies]
|
||||
futures = "^0.1"
|
||||
proptest = { version = "^1.0.0", default-features = false, features = ["std"] }
|
||||
wasm-bindgen-futures = "^0.4"
|
||||
wasm-bindgen-test = "^0.3"
|
1
automerge-wasm/README.md
Normal file
1
automerge-wasm/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
todo
|
31
automerge-wasm/package.json
Normal file
31
automerge-wasm/package.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"collaborators": [
|
||||
"Orion Henry <orion@inkandswitch.com>",
|
||||
"Alex Good <alex@memoryandthought.me>",
|
||||
"Martin Kleppmann"
|
||||
],
|
||||
"name": "automerge-wasm",
|
||||
"description": "wasm-bindgen bindings to the automerge rust implementation",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"README.md",
|
||||
"LICENSE",
|
||||
"package.json",
|
||||
"automerge_wasm_bg.wasm",
|
||||
"automerge_wasm.js"
|
||||
],
|
||||
"main": "./dev/index.js",
|
||||
"scripts": {
|
||||
"build": "rimraf ./dev && wasm-pack build --target nodejs --dev --out-name index -d dev",
|
||||
"release": "rimraf ./dev && wasm-pack build --target nodejs --release --out-name index -d dev && yarn opt",
|
||||
"prof": "rimraf ./dev && wasm-pack build --target nodejs --profiling --out-name index -d dev",
|
||||
"opt": "wasm-opt -Oz dev/index_bg.wasm -o tmp.wasm && mv tmp.wasm dev/index_bg.wasm",
|
||||
"test": "yarn build && mocha --bail --full-trace"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"mocha": "^9.1.3",
|
||||
"rimraf": "^3.0.2"
|
||||
}
|
||||
}
|
822
automerge-wasm/src/lib.rs
Normal file
822
automerge-wasm/src/lib.rs
Normal file
|
@ -0,0 +1,822 @@
|
|||
extern crate web_sys;
|
||||
use automerge as am;
|
||||
use automerge::{Change, ChangeHash, Prop, Value, ExId};
|
||||
use js_sys::{Array, Object, Reflect, Uint8Array};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::TryInto;
|
||||
use std::fmt::Display;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! log {
|
||||
( $( $t:tt )* ) => {
|
||||
web_sys::console::log_1(&format!( $( $t )* ).into());
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "wee_alloc")]
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
fn datatype(s: &am::ScalarValue) -> String {
|
||||
match s {
|
||||
am::ScalarValue::Bytes(_) => "bytes".into(),
|
||||
am::ScalarValue::Str(_) => "str".into(),
|
||||
am::ScalarValue::Int(_) => "int".into(),
|
||||
am::ScalarValue::Uint(_) => "uint".into(),
|
||||
am::ScalarValue::F64(_) => "f64".into(),
|
||||
am::ScalarValue::Counter(_) => "counter".into(),
|
||||
am::ScalarValue::Timestamp(_) => "timestamp".into(),
|
||||
am::ScalarValue::Boolean(_) => "boolean".into(),
|
||||
am::ScalarValue::Null => "null".into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ScalarValue(am::ScalarValue);
|
||||
|
||||
impl From<ScalarValue> for JsValue {
|
||||
fn from(val: ScalarValue) -> Self {
|
||||
match &val.0 {
|
||||
am::ScalarValue::Bytes(v) => Uint8Array::from(v.as_slice()).into(),
|
||||
am::ScalarValue::Str(v) => v.to_string().into(),
|
||||
am::ScalarValue::Int(v) => (*v as f64).into(),
|
||||
am::ScalarValue::Uint(v) => (*v as f64).into(),
|
||||
am::ScalarValue::F64(v) => (*v).into(),
|
||||
am::ScalarValue::Counter(v) => (*v as f64).into(),
|
||||
am::ScalarValue::Timestamp(v) => (*v as f64).into(),
|
||||
am::ScalarValue::Boolean(v) => (*v).into(),
|
||||
am::ScalarValue::Null => JsValue::null(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct Automerge(automerge::Automerge);
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct SyncState(am::SyncState);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl SyncState {
|
||||
#[wasm_bindgen(getter, js_name = sharedHeads)]
|
||||
pub fn shared_heads(&self) -> JsValue {
|
||||
rust_to_js(&self.0.shared_heads).unwrap()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter, js_name = lastSentHeads)]
|
||||
pub fn last_sent_heads(&self) -> JsValue {
|
||||
rust_to_js(self.0.last_sent_heads.as_ref()).unwrap()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(setter, js_name = lastSentHeads)]
|
||||
pub fn set_last_sent_heads(&mut self, heads: JsValue) {
|
||||
let heads: Option<Vec<ChangeHash>> = js_to_rust(&heads).unwrap();
|
||||
self.0.last_sent_heads = heads
|
||||
}
|
||||
|
||||
#[wasm_bindgen(setter, js_name = sentHashes)]
|
||||
pub fn set_sent_hashes(&mut self, hashes: JsValue) {
|
||||
let hashes_map: HashMap<ChangeHash, bool> = js_to_rust(&hashes).unwrap();
|
||||
let hashes_set: HashSet<ChangeHash> = hashes_map.keys().cloned().collect();
|
||||
self.0.sent_hashes = hashes_set
|
||||
}
|
||||
|
||||
fn decode(data: Uint8Array) -> Result<SyncState, JsValue> {
|
||||
let data = data.to_vec();
|
||||
let s = am::SyncState::decode(&data);
|
||||
let s = s.map_err(to_js_err)?;
|
||||
Ok(SyncState(s))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct JsErr(String);
|
||||
|
||||
impl From<JsErr> for JsValue {
|
||||
fn from(err: JsErr) -> Self {
|
||||
js_sys::Error::new(&std::format!("{}", err.0)).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for JsErr {
|
||||
fn from(s: &'a str) -> Self {
|
||||
JsErr(s.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Automerge {
|
||||
pub fn new(actor: JsValue) -> Result<Automerge, JsValue> {
|
||||
let mut automerge = automerge::Automerge::new();
|
||||
if let Some(a) = actor.as_string() {
|
||||
let a = automerge::ActorId::from(hex::decode(a).map_err(to_js_err)?.to_vec());
|
||||
automerge.set_actor(a);
|
||||
}
|
||||
Ok(Automerge(automerge))
|
||||
}
|
||||
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn clone(&self) -> Self {
|
||||
Automerge(self.0.clone())
|
||||
}
|
||||
|
||||
pub fn free(self) {}
|
||||
|
||||
pub fn pending_ops(&self) -> JsValue {
|
||||
(self.0.pending_ops() as u32).into()
|
||||
}
|
||||
|
||||
pub fn commit(&mut self, message: JsValue, time: JsValue) -> Array {
|
||||
let message = message.as_string();
|
||||
let time = time.as_f64().map(|v| v as i64);
|
||||
let heads = self.0.commit(message, time);
|
||||
let heads: Array = heads
|
||||
.iter()
|
||||
.map(|h| JsValue::from_str(&hex::encode(&h.0)))
|
||||
.collect();
|
||||
heads
|
||||
}
|
||||
|
||||
pub fn rollback(&mut self) -> JsValue {
|
||||
self.0.rollback().into()
|
||||
}
|
||||
|
||||
pub fn keys(&mut self, obj: JsValue, heads: JsValue) -> Result<Array, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let result = if let Some(heads) = get_heads(heads) {
|
||||
self.0.keys_at(&obj, &heads)
|
||||
} else {
|
||||
self.0.keys(&obj)
|
||||
}
|
||||
.iter()
|
||||
.map(|s| JsValue::from_str(s))
|
||||
.collect();
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn text(&mut self, obj: JsValue, heads: JsValue) -> Result<JsValue, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
if let Some(heads) = get_heads(heads) {
|
||||
self.0.text_at(&obj, &heads)
|
||||
} else {
|
||||
self.0.text(&obj)
|
||||
}
|
||||
.map_err(to_js_err)
|
||||
.map(|t| t.into())
|
||||
}
|
||||
|
||||
pub fn splice(
|
||||
&mut self,
|
||||
obj: JsValue,
|
||||
start: JsValue,
|
||||
delete_count: JsValue,
|
||||
text: JsValue,
|
||||
) -> Result<(), JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let start = to_usize(start, "start")?;
|
||||
let delete_count = to_usize(delete_count, "deleteCount")?;
|
||||
let mut vals = vec![];
|
||||
if let Some(t) = text.as_string() {
|
||||
self.0
|
||||
.splice_text(&obj, start, delete_count, &t)
|
||||
.map_err(to_js_err)?;
|
||||
} else {
|
||||
if let Ok(array) = text.dyn_into::<Array>() {
|
||||
for i in array.iter() {
|
||||
if let Some(t) = i.as_string() {
|
||||
vals.push(t.into());
|
||||
} else if let Ok(array) = i.dyn_into::<Array>() {
|
||||
let value = array.get(1);
|
||||
let datatype = array.get(2);
|
||||
let value = self.import_value(value, datatype)?;
|
||||
vals.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.0
|
||||
.splice(&obj, start, delete_count, vals)
|
||||
.map_err(to_js_err)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
obj: JsValue,
|
||||
index: JsValue,
|
||||
value: JsValue,
|
||||
datatype: JsValue,
|
||||
) -> Result<JsValue, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
//let key = self.insert_pos_for_index(&obj, prop)?;
|
||||
let index: Result<_, JsValue> = index
|
||||
.as_f64()
|
||||
.ok_or_else(|| "insert index must be a number".into());
|
||||
let index = index?;
|
||||
let value = self.import_value(value, datatype)?;
|
||||
let opid = self
|
||||
.0
|
||||
.insert(&obj, index as usize, value)
|
||||
.map_err(to_js_err)?;
|
||||
match opid {
|
||||
Some(opid) => Ok(self.export(opid)),
|
||||
None => Ok(JsValue::null()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set(
|
||||
&mut self,
|
||||
obj: JsValue,
|
||||
prop: JsValue,
|
||||
value: JsValue,
|
||||
datatype: JsValue,
|
||||
) -> Result<JsValue, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let prop = self.import_prop(prop)?;
|
||||
let value = self.import_value(value, datatype)?;
|
||||
let opid = self.0.set(&obj, prop, value).map_err(to_js_err)?;
|
||||
match opid {
|
||||
Some(opid) => Ok(self.export(opid)),
|
||||
None => Ok(JsValue::null()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inc(&mut self, obj: JsValue, prop: JsValue, value: JsValue) -> Result<(), JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let prop = self.import_prop(prop)?;
|
||||
let value: f64 = value
|
||||
.as_f64()
|
||||
.ok_or("inc needs a numberic value")
|
||||
.map_err(to_js_err)?;
|
||||
self.0.inc(&obj, prop, value as i64).map_err(to_js_err)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn value(&mut self, obj: JsValue, prop: JsValue, heads: JsValue) -> Result<Array, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let result = Array::new();
|
||||
let prop = to_prop(prop);
|
||||
let heads = get_heads(heads);
|
||||
if let Ok(prop) = prop {
|
||||
let value = if let Some(h) = heads {
|
||||
self.0.value_at(&obj, prop, &h)
|
||||
} else {
|
||||
self.0.value(&obj, prop)
|
||||
}
|
||||
.map_err(to_js_err)?;
|
||||
match value {
|
||||
Some((Value::Object(obj_type), obj_id)) => {
|
||||
result.push(&obj_type.to_string().into());
|
||||
result.push(&self.export(obj_id));
|
||||
}
|
||||
Some((Value::Scalar(value), _)) => {
|
||||
result.push(&datatype(&value).into());
|
||||
result.push(&ScalarValue(value).into());
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn values(&mut self, obj: JsValue, arg: JsValue, heads: JsValue) -> Result<Array, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let result = Array::new();
|
||||
let prop = to_prop(arg);
|
||||
if let Ok(prop) = prop {
|
||||
let values = if let Some(heads) = get_heads(heads) {
|
||||
self.0.values_at(&obj, prop, &heads)
|
||||
} else {
|
||||
self.0.values(&obj, prop)
|
||||
}
|
||||
.map_err(to_js_err)?;
|
||||
for value in values {
|
||||
match value {
|
||||
(Value::Object(obj_type), obj_id) => {
|
||||
let sub = Array::new();
|
||||
sub.push(&obj_type.to_string().into());
|
||||
sub.push(&self.export(obj_id));
|
||||
result.push(&sub.into());
|
||||
}
|
||||
(Value::Scalar(value), id) => {
|
||||
let sub = Array::new();
|
||||
sub.push(&datatype(&value).into());
|
||||
sub.push(&ScalarValue(value).into());
|
||||
sub.push(&self.export(id));
|
||||
result.push(&sub.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn length(&mut self, obj: JsValue, heads: JsValue) -> Result<JsValue, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
if let Some(heads) = get_heads(heads) {
|
||||
Ok((self.0.length_at(&obj, &heads) as f64).into())
|
||||
} else {
|
||||
Ok((self.0.length(&obj) as f64).into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn del(&mut self, obj: JsValue, prop: JsValue) -> Result<(), JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let prop = to_prop(prop)?;
|
||||
self.0.del(&obj, prop).map_err(to_js_err)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save(&mut self) -> Result<Uint8Array, JsValue> {
|
||||
self.0
|
||||
.save()
|
||||
.map(|v| Uint8Array::from(v.as_slice()))
|
||||
.map_err(to_js_err)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = saveIncremental)]
|
||||
pub fn save_incremental(&mut self) -> JsValue {
|
||||
let bytes = self.0.save_incremental();
|
||||
Uint8Array::from(bytes.as_slice()).into()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = loadIncremental)]
|
||||
pub fn load_incremental(&mut self, data: Uint8Array) -> Result<JsValue, JsValue> {
|
||||
let data = data.to_vec();
|
||||
let len = self.0.load_incremental(&data).map_err(to_js_err)?;
|
||||
Ok(len.into())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = applyChanges)]
|
||||
pub fn apply_changes(&mut self, changes: JsValue) -> Result<(), JsValue> {
|
||||
let changes: Vec<_> = JS(changes).try_into()?;
|
||||
self.0.apply_changes(&changes).map_err(to_js_err)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = getChanges)]
|
||||
pub fn get_changes(&mut self, have_deps: JsValue) -> Result<Array, JsValue> {
|
||||
let deps: Vec<_> = JS(have_deps).try_into()?;
|
||||
let changes = self.0.get_changes(&deps);
|
||||
let changes: Array = changes
|
||||
.iter()
|
||||
.map(|c| Uint8Array::from(c.raw_bytes()))
|
||||
.collect();
|
||||
Ok(changes)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = getChangesAdded)]
|
||||
pub fn get_changes_added(&mut self, other: &Automerge) -> Result<Array, JsValue> {
|
||||
let changes = self.0.get_changes_added(&other.0);
|
||||
let changes: Array = changes
|
||||
.iter()
|
||||
.map(|c| Uint8Array::from(c.raw_bytes()))
|
||||
.collect();
|
||||
Ok(changes)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = getHeads)]
|
||||
pub fn get_heads(&mut self) -> Result<Array, JsValue> {
|
||||
let heads = self.0.get_heads();
|
||||
let heads: Array = heads
|
||||
.iter()
|
||||
.map(|h| JsValue::from_str(&hex::encode(&h.0)))
|
||||
.collect();
|
||||
Ok(heads)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = getActorId)]
|
||||
pub fn get_actor_id(&mut self) -> Result<JsValue, JsValue> {
|
||||
let actor = self.0.get_actor();
|
||||
Ok(actor.to_string().into())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = getLastLocalChange)]
|
||||
pub fn get_last_local_change(&mut self) -> Result<JsValue, JsValue> {
|
||||
if let Some(change) = self.0.get_last_local_change() {
|
||||
Ok(Uint8Array::from(change.raw_bytes()).into())
|
||||
} else {
|
||||
Ok(JsValue::null())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dump(&self) {
|
||||
self.0.dump()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = getMissingDeps)]
|
||||
pub fn get_missing_deps(&mut self, heads: JsValue) -> Result<Array, JsValue> {
|
||||
let heads: Vec<_> = JS(heads).try_into()?;
|
||||
let deps = self.0.get_missing_deps(&heads);
|
||||
let deps: Array = deps
|
||||
.iter()
|
||||
.map(|h| JsValue::from_str(&hex::encode(&h.0)))
|
||||
.collect();
|
||||
Ok(deps)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = receiveSyncMessage)]
|
||||
pub fn receive_sync_message(
|
||||
&mut self,
|
||||
state: &mut SyncState,
|
||||
message: Uint8Array,
|
||||
) -> Result<(), JsValue> {
|
||||
let message = message.to_vec();
|
||||
let message = am::SyncMessage::decode(message.as_slice()).map_err(to_js_err)?;
|
||||
self.0
|
||||
.receive_sync_message(&mut state.0, message)
|
||||
.map_err(to_js_err)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = generateSyncMessage)]
|
||||
pub fn generate_sync_message(&mut self, state: &mut SyncState) -> Result<JsValue, JsValue> {
|
||||
if let Some(message) = self.0.generate_sync_message(&mut state.0) {
|
||||
Ok(Uint8Array::from(message.encode().map_err(to_js_err)?.as_slice()).into())
|
||||
} else {
|
||||
Ok(JsValue::null())
|
||||
}
|
||||
}
|
||||
|
||||
fn export(&self, val: ExId) -> JsValue {
|
||||
val.to_string().into()
|
||||
}
|
||||
|
||||
fn import(&self, id: JsValue) -> Result<ExId, JsValue> {
|
||||
let id_str = id
|
||||
.as_string()
|
||||
.ok_or("invalid opid/objid/elemid")
|
||||
.map_err(to_js_err)?;
|
||||
self.0.import(&id_str).map_err(to_js_err)
|
||||
}
|
||||
|
||||
fn import_prop(&mut self, prop: JsValue) -> Result<Prop, JsValue> {
|
||||
if let Some(s) = prop.as_string() {
|
||||
Ok(s.into())
|
||||
} else if let Some(n) = prop.as_f64() {
|
||||
Ok((n as usize).into())
|
||||
} else {
|
||||
Err(format!("invalid prop {:?}", prop).into())
|
||||
}
|
||||
}
|
||||
|
||||
fn import_value(&mut self, value: JsValue, datatype: JsValue) -> Result<Value, JsValue> {
|
||||
let datatype = datatype.as_string();
|
||||
match datatype.as_deref() {
|
||||
Some("boolean") => value
|
||||
.as_bool()
|
||||
.ok_or_else(|| "value must be a bool".into())
|
||||
.map(|v| am::ScalarValue::Boolean(v).into()),
|
||||
Some("int") => value
|
||||
.as_f64()
|
||||
.ok_or_else(|| "value must be a number".into())
|
||||
.map(|v| am::ScalarValue::Int(v as i64).into()),
|
||||
Some("uint") => value
|
||||
.as_f64()
|
||||
.ok_or_else(|| "value must be a number".into())
|
||||
.map(|v| am::ScalarValue::Uint(v as u64).into()),
|
||||
Some("f64") => value
|
||||
.as_f64()
|
||||
.ok_or_else(|| "value must be a number".into())
|
||||
.map(|n| am::ScalarValue::F64(n).into()),
|
||||
Some("bytes") => {
|
||||
Ok(am::ScalarValue::Bytes(value.dyn_into::<Uint8Array>().unwrap().to_vec()).into())
|
||||
}
|
||||
Some("counter") => value
|
||||
.as_f64()
|
||||
.ok_or_else(|| "value must be a number".into())
|
||||
.map(|v| am::ScalarValue::Counter(v as i64).into()),
|
||||
Some("timestamp") => value
|
||||
.as_f64()
|
||||
.ok_or_else(|| "value must be a number".into())
|
||||
.map(|v| am::ScalarValue::Timestamp(v as i64).into()),
|
||||
/*
|
||||
Some("bytes") => unimplemented!(),
|
||||
Some("cursor") => unimplemented!(),
|
||||
*/
|
||||
Some("null") => Ok(am::ScalarValue::Null.into()),
|
||||
Some(_) => Err(format!("unknown datatype {:?}", datatype).into()),
|
||||
None => {
|
||||
if value.is_null() {
|
||||
Ok(am::ScalarValue::Null.into())
|
||||
} else if let Some(b) = value.as_bool() {
|
||||
Ok(am::ScalarValue::Boolean(b).into())
|
||||
} else if let Some(s) = value.as_string() {
|
||||
// FIXME - we need to detect str vs int vs float vs bool here :/
|
||||
Ok(am::ScalarValue::Str(s.into()).into())
|
||||
} else if let Some(n) = value.as_f64() {
|
||||
if (n.round() - n).abs() < f64::EPSILON {
|
||||
Ok(am::ScalarValue::Int(n as i64).into())
|
||||
} else {
|
||||
Ok(am::ScalarValue::F64(n).into())
|
||||
}
|
||||
} else if let Some(o) = to_objtype(&value) {
|
||||
Ok(o.into())
|
||||
} else if let Ok(o) = &value.dyn_into::<Uint8Array>() {
|
||||
Ok(am::ScalarValue::Bytes(o.to_vec()).into())
|
||||
} else {
|
||||
Err("value is invalid".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_usize(val: JsValue, name: &str) -> Result<usize, JsValue> {
|
||||
match val.as_f64() {
|
||||
Some(n) => Ok(n as usize),
|
||||
None => Err(format!("{} must be a number", name).into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_prop(p: JsValue) -> Result<Prop, JsValue> {
|
||||
if let Some(s) = p.as_string() {
|
||||
Ok(Prop::Map(s))
|
||||
} else if let Some(n) = p.as_f64() {
|
||||
Ok(Prop::Seq(n as usize))
|
||||
} else {
|
||||
Err("prop must me a string or number".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn to_objtype(a: &JsValue) -> Option<am::ObjType> {
|
||||
if !a.is_function() {
|
||||
return None;
|
||||
}
|
||||
let f: js_sys::Function = a.clone().try_into().unwrap();
|
||||
let f = f.to_string();
|
||||
if f.starts_with("class MAP", 0) {
|
||||
Some(am::ObjType::Map)
|
||||
} else if f.starts_with("class LIST", 0) {
|
||||
Some(am::ObjType::List)
|
||||
} else if f.starts_with("class TEXT", 0) {
|
||||
Some(am::ObjType::Text)
|
||||
} else if f.starts_with("class TABLE", 0) {
|
||||
Some(am::ObjType::Table)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
struct ObjType(am::ObjType);
|
||||
|
||||
impl TryFrom<JsValue> for ObjType {
|
||||
type Error = JsValue;
|
||||
|
||||
fn try_from(val: JsValue) -> Result<Self, Self::Error> {
|
||||
match &val.as_string() {
|
||||
Some(o) if o == "map" => Ok(ObjType(am::ObjType::Map)),
|
||||
Some(o) if o == "list" => Ok(ObjType(am::ObjType::List)),
|
||||
Some(o) => Err(format!("unknown obj type {}", o).into()),
|
||||
_ => Err("obj type must be a string".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn init(actor: JsValue) -> Result<Automerge, JsValue> {
|
||||
console_error_panic_hook::set_once();
|
||||
Automerge::new(actor)
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn load(data: Uint8Array, actor: JsValue) -> Result<Automerge, JsValue> {
|
||||
let data = data.to_vec();
|
||||
let mut automerge = am::Automerge::load(&data).map_err(to_js_err)?;
|
||||
if let Some(s) = actor.as_string() {
|
||||
let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec());
|
||||
automerge.set_actor(actor)
|
||||
}
|
||||
Ok(Automerge(automerge))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = encodeChange)]
|
||||
pub fn encode_change(change: JsValue) -> Result<Uint8Array, JsValue> {
|
||||
let change: am::ExpandedChange = change.into_serde().map_err(to_js_err)?;
|
||||
let change: Change = change.into();
|
||||
Ok(Uint8Array::from(change.raw_bytes()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = decodeChange)]
|
||||
pub fn decode_change(change: Uint8Array) -> Result<JsValue, JsValue> {
|
||||
let change = Change::from_bytes(change.to_vec()).map_err(to_js_err)?;
|
||||
let change: am::ExpandedChange = change.decode();
|
||||
JsValue::from_serde(&change).map_err(to_js_err)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = initSyncState)]
|
||||
pub fn init_sync_state() -> SyncState {
|
||||
SyncState(Default::default())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = encodeSyncMessage)]
|
||||
pub fn encode_sync_message(message: JsValue) -> Result<Uint8Array, JsValue> {
|
||||
let heads = get(&message, "heads")?.try_into()?;
|
||||
let need = get(&message, "need")?.try_into()?;
|
||||
let changes = get(&message, "changes")?.try_into()?;
|
||||
let have = get(&message, "have")?.try_into()?;
|
||||
Ok(Uint8Array::from(
|
||||
am::SyncMessage {
|
||||
heads,
|
||||
need,
|
||||
have,
|
||||
changes,
|
||||
}
|
||||
.encode()
|
||||
.unwrap()
|
||||
.as_slice(),
|
||||
))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = decodeSyncMessage)]
|
||||
pub fn decode_sync_message(msg: Uint8Array) -> Result<JsValue, JsValue> {
|
||||
let data = msg.to_vec();
|
||||
let msg = am::SyncMessage::decode(&data).map_err(to_js_err)?;
|
||||
let heads: Array = VH(&msg.heads).into();
|
||||
let need: Array = VH(&msg.need).into();
|
||||
let changes: Array = VC(&msg.changes).into();
|
||||
let have: Array = VSH(&msg.have).try_into()?;
|
||||
let obj = Object::new().into();
|
||||
set(&obj, "heads", heads)?;
|
||||
set(&obj, "need", need)?;
|
||||
set(&obj, "have", have)?;
|
||||
set(&obj, "changes", changes)?;
|
||||
Ok(obj)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = encodeSyncState)]
|
||||
pub fn encode_sync_state(state: SyncState) -> Result<Uint8Array, JsValue> {
|
||||
Ok(Uint8Array::from(
|
||||
state.0.encode().map_err(to_js_err)?.as_slice(),
|
||||
))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = decodeSyncState)]
|
||||
pub fn decode_sync_state(state: Uint8Array) -> Result<SyncState, JsValue> {
|
||||
SyncState::decode(state)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = MAP)]
|
||||
pub struct Map {}
|
||||
|
||||
#[wasm_bindgen(js_name = LIST)]
|
||||
pub struct List {}
|
||||
|
||||
#[wasm_bindgen(js_name = TEXT)]
|
||||
pub struct Text {}
|
||||
|
||||
#[wasm_bindgen(js_name = TABLE)]
|
||||
pub struct Table {}
|
||||
|
||||
fn to_js_err<T: Display>(err: T) -> JsValue {
|
||||
js_sys::Error::new(&std::format!("{}", err)).into()
|
||||
}
|
||||
|
||||
fn get(obj: &JsValue, prop: &str) -> Result<JS, JsValue> {
|
||||
Ok(JS(Reflect::get(obj, &prop.into())?))
|
||||
}
|
||||
|
||||
fn set<V: Into<JsValue>>(obj: &JsValue, prop: &str, val: V) -> Result<bool, JsValue> {
|
||||
Reflect::set(obj, &prop.into(), &val.into())
|
||||
}
|
||||
|
||||
struct JS(JsValue);
|
||||
|
||||
impl TryFrom<JS> for Vec<ChangeHash> {
|
||||
type Error = JsValue;
|
||||
|
||||
fn try_from(value: JS) -> Result<Self, Self::Error> {
|
||||
let value = value.0.dyn_into::<Array>()?;
|
||||
let value: Result<Vec<ChangeHash>, _> = value.iter().map(|j| j.into_serde()).collect();
|
||||
let value = value.map_err(to_js_err)?;
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JS> for Option<Vec<ChangeHash>> {
|
||||
fn from(value: JS) -> Self {
|
||||
let value = value.0.dyn_into::<Array>().ok()?;
|
||||
let value: Result<Vec<ChangeHash>, _> = value.iter().map(|j| j.into_serde()).collect();
|
||||
let value = value.ok()?;
|
||||
Some(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<JS> for Vec<Change> {
|
||||
type Error = JsValue;
|
||||
|
||||
fn try_from(value: JS) -> Result<Self, Self::Error> {
|
||||
let value = value.0.dyn_into::<Array>()?;
|
||||
let changes: Result<Vec<Uint8Array>, _> = value.iter().map(|j| j.dyn_into()).collect();
|
||||
let changes = changes?;
|
||||
let changes: Result<Vec<Change>, _> = changes
|
||||
.iter()
|
||||
.map(|a| am::decode_change(a.to_vec()))
|
||||
.collect();
|
||||
let changes = changes.map_err(to_js_err)?;
|
||||
Ok(changes)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<JS> for Vec<am::SyncHave> {
|
||||
type Error = JsValue;
|
||||
|
||||
fn try_from(value: JS) -> Result<Self, Self::Error> {
|
||||
let value = value.0.dyn_into::<Array>()?;
|
||||
let have: Result<Vec<am::SyncHave>, JsValue> = value
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let last_sync = get(&s, "lastSync")?.try_into()?;
|
||||
let bloom = get(&s, "bloom")?.try_into()?;
|
||||
Ok(am::SyncHave { last_sync, bloom })
|
||||
})
|
||||
.collect();
|
||||
let have = have?;
|
||||
Ok(have)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<JS> for am::BloomFilter {
|
||||
type Error = JsValue;
|
||||
|
||||
fn try_from(value: JS) -> Result<Self, Self::Error> {
|
||||
let value: Uint8Array = value.0.dyn_into()?;
|
||||
let value = value.to_vec();
|
||||
let value = value.as_slice().try_into().map_err(to_js_err)?;
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
struct VH<'a>(&'a [ChangeHash]);
|
||||
|
||||
impl<'a> From<VH<'a>> for Array {
|
||||
fn from(value: VH<'a>) -> Self {
|
||||
let heads: Array = value
|
||||
.0
|
||||
.iter()
|
||||
.map(|h| JsValue::from_str(&hex::encode(&h.0)))
|
||||
.collect();
|
||||
heads
|
||||
}
|
||||
}
|
||||
|
||||
struct VC<'a>(&'a [Change]);
|
||||
|
||||
impl<'a> From<VC<'a>> for Array {
|
||||
fn from(value: VC<'a>) -> Self {
|
||||
let changes: Array = value
|
||||
.0
|
||||
.iter()
|
||||
.map(|c| Uint8Array::from(c.raw_bytes()))
|
||||
.collect();
|
||||
changes
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
struct VSH<'a>(&'a [am::SyncHave]);
|
||||
|
||||
impl<'a> TryFrom<VSH<'a>> for Array {
|
||||
type Error = JsValue;
|
||||
|
||||
fn try_from(value: VSH<'a>) -> Result<Self, Self::Error> {
|
||||
let have: Result<Array, JsValue> = value
|
||||
.0
|
||||
.iter()
|
||||
.map(|have| {
|
||||
let last_sync: Array = have
|
||||
.last_sync
|
||||
.iter()
|
||||
.map(|h| JsValue::from_str(&hex::encode(&h.0)))
|
||||
.collect();
|
||||
// FIXME - the clone and the unwrap here shouldnt be needed - look at into_bytes()
|
||||
let bloom = Uint8Array::from(have.bloom.clone().into_bytes().unwrap().as_slice());
|
||||
let obj: JsValue = Object::new().into();
|
||||
Reflect::set(&obj, &"lastSync".into(), &last_sync.into())?;
|
||||
Reflect::set(&obj, &"bloom".into(), &bloom.into())?;
|
||||
Ok(obj)
|
||||
})
|
||||
.collect();
|
||||
let have = have?;
|
||||
Ok(have)
|
||||
}
|
||||
}
|
||||
|
||||
fn rust_to_js<T: Serialize>(value: T) -> Result<JsValue, JsValue> {
|
||||
JsValue::from_serde(&value).map_err(to_js_err)
|
||||
}
|
||||
|
||||
fn js_to_rust<T: DeserializeOwned>(value: &JsValue) -> Result<T, JsValue> {
|
||||
value.into_serde().map_err(to_js_err)
|
||||
}
|
||||
|
||||
fn get_heads(heads: JsValue) -> Option<Vec<ChangeHash>> {
|
||||
JS(heads).into()
|
||||
}
|
284
automerge-wasm/test/test.js
Normal file
284
automerge-wasm/test/test.js
Normal file
|
@ -0,0 +1,284 @@
|
|||
|
||||
const assert = require('assert')
|
||||
const util = require('util')
|
||||
const Automerge = require('..')
|
||||
const { MAP, LIST, TEXT } = Automerge
|
||||
|
||||
// str to uint8array
|
||||
function en(str) {
|
||||
return new TextEncoder('utf8').encode(str)
|
||||
}
|
||||
// uint8array to str
|
||||
function de(bytes) {
|
||||
return new TextDecoder('utf8').decode(bytes);
|
||||
}
|
||||
|
||||
describe('Automerge', () => {
|
||||
describe('basics', () => {
|
||||
it('should init clone and free', () => {
|
||||
let doc1 = Automerge.init()
|
||||
let doc2 = doc1.clone()
|
||||
doc1.free()
|
||||
doc2.free()
|
||||
})
|
||||
|
||||
it('should be able to start and commit', () => {
|
||||
let doc = Automerge.init()
|
||||
doc.commit()
|
||||
})
|
||||
|
||||
it('getting a nonexistant prop does not throw an error', () => {
|
||||
let doc = Automerge.init()
|
||||
let root = "_root"
|
||||
let result = doc.value(root,"hello")
|
||||
assert.deepEqual(result,[])
|
||||
})
|
||||
|
||||
it('should be able to set and get a simple value', () => {
|
||||
let doc = Automerge.init()
|
||||
let root = "_root"
|
||||
let result
|
||||
|
||||
doc.set(root, "hello", "world")
|
||||
doc.set(root, "number1", 5, "uint")
|
||||
doc.set(root, "number2", 5)
|
||||
doc.set(root, "number3", 5.5)
|
||||
doc.set(root, "number4", 5.5, "f64")
|
||||
doc.set(root, "number5", 5.5, "int")
|
||||
doc.set(root, "bool", true)
|
||||
|
||||
result = doc.value(root,"hello")
|
||||
assert.deepEqual(result,["str","world"])
|
||||
|
||||
result = doc.value(root,"number1")
|
||||
assert.deepEqual(result,["uint",5])
|
||||
|
||||
result = doc.value(root,"number2")
|
||||
assert.deepEqual(result,["int",5])
|
||||
|
||||
result = doc.value(root,"number3")
|
||||
assert.deepEqual(result,["f64",5.5])
|
||||
|
||||
result = doc.value(root,"number4")
|
||||
assert.deepEqual(result,["f64",5.5])
|
||||
|
||||
result = doc.value(root,"number5")
|
||||
assert.deepEqual(result,["int",5])
|
||||
|
||||
result = doc.value(root,"bool")
|
||||
assert.deepEqual(result,["boolean",true])
|
||||
|
||||
doc.set(root, "bool", false, "boolean")
|
||||
|
||||
result = doc.value(root,"bool")
|
||||
assert.deepEqual(result,["boolean",false])
|
||||
})
|
||||
|
||||
it('should be able to use bytes', () => {
|
||||
let doc = Automerge.init()
|
||||
doc.set("_root","data1", new Uint8Array([10,11,12]));
|
||||
doc.set("_root","data2", new Uint8Array([13,14,15]), "bytes");
|
||||
let value1 = doc.value("_root", "data1")
|
||||
assert.deepEqual(value1, ["bytes", new Uint8Array([10,11,12])]);
|
||||
let value2 = doc.value("_root", "data2")
|
||||
assert.deepEqual(value2, ["bytes", new Uint8Array([13,14,15])]);
|
||||
})
|
||||
|
||||
it('should be able to make sub objects', () => {
|
||||
let doc = Automerge.init()
|
||||
let root = "_root"
|
||||
let result
|
||||
|
||||
let submap = doc.set(root, "submap", MAP)
|
||||
doc.set(submap, "number", 6, "uint")
|
||||
assert.strictEqual(doc.pending_ops(),2)
|
||||
|
||||
result = doc.value(root,"submap")
|
||||
assert.deepEqual(result,["map",submap])
|
||||
|
||||
result = doc.value(submap,"number")
|
||||
assert.deepEqual(result,["uint",6])
|
||||
})
|
||||
|
||||
it('should be able to make lists', () => {
|
||||
let doc = Automerge.init()
|
||||
let root = "_root"
|
||||
|
||||
let submap = doc.set(root, "numbers", LIST)
|
||||
doc.insert(submap, 0, "a");
|
||||
doc.insert(submap, 1, "b");
|
||||
doc.insert(submap, 2, "c");
|
||||
doc.insert(submap, 0, "z");
|
||||
|
||||
assert.deepEqual(doc.value(submap, 0),["str","z"])
|
||||
assert.deepEqual(doc.value(submap, 1),["str","a"])
|
||||
assert.deepEqual(doc.value(submap, 2),["str","b"])
|
||||
assert.deepEqual(doc.value(submap, 3),["str","c"])
|
||||
assert.deepEqual(doc.length(submap),4)
|
||||
|
||||
doc.set(submap, 2, "b v2");
|
||||
|
||||
assert.deepEqual(doc.value(submap, 2),["str","b v2"])
|
||||
assert.deepEqual(doc.length(submap),4)
|
||||
})
|
||||
|
||||
it('should be able delete non-existant props', () => {
|
||||
let doc = Automerge.init()
|
||||
|
||||
doc.set("_root", "foo","bar")
|
||||
doc.set("_root", "bip","bap")
|
||||
let heads1 = doc.commit()
|
||||
|
||||
assert.deepEqual(doc.keys("_root"),["bip","foo"])
|
||||
|
||||
doc.del("_root", "foo")
|
||||
doc.del("_root", "baz")
|
||||
let heads2 = doc.commit()
|
||||
|
||||
assert.deepEqual(doc.keys("_root"),["bip"])
|
||||
assert.deepEqual(doc.keys("_root", heads1),["bip", "foo"])
|
||||
assert.deepEqual(doc.keys("_root", heads2),["bip"])
|
||||
})
|
||||
|
||||
it('should be able to del', () => {
|
||||
let doc = Automerge.init()
|
||||
let root = "_root"
|
||||
|
||||
doc.set(root, "xxx", "xxx");
|
||||
assert.deepEqual(doc.value(root, "xxx"),["str","xxx"])
|
||||
doc.del(root, "xxx");
|
||||
assert.deepEqual(doc.value(root, "xxx"),[])
|
||||
})
|
||||
|
||||
it('should be able to use counters', () => {
|
||||
let doc = Automerge.init()
|
||||
let root = "_root"
|
||||
|
||||
doc.set(root, "counter", 10, "counter");
|
||||
assert.deepEqual(doc.value(root, "counter"),["counter",10])
|
||||
doc.inc(root, "counter", 10);
|
||||
assert.deepEqual(doc.value(root, "counter"),["counter",20])
|
||||
doc.inc(root, "counter", -5);
|
||||
assert.deepEqual(doc.value(root, "counter"),["counter",15])
|
||||
})
|
||||
|
||||
it('should be able to splice text', () => {
|
||||
let doc = Automerge.init()
|
||||
let root = "_root";
|
||||
|
||||
let text = doc.set(root, "text", Automerge.TEXT);
|
||||
doc.splice(text, 0, 0, "hello ")
|
||||
doc.splice(text, 6, 0, ["w","o","r","l","d"])
|
||||
doc.splice(text, 11, 0, [["str","!"],["str","?"]])
|
||||
assert.deepEqual(doc.value(text, 0),["str","h"])
|
||||
assert.deepEqual(doc.value(text, 1),["str","e"])
|
||||
assert.deepEqual(doc.value(text, 9),["str","l"])
|
||||
assert.deepEqual(doc.value(text, 10),["str","d"])
|
||||
assert.deepEqual(doc.value(text, 11),["str","!"])
|
||||
assert.deepEqual(doc.value(text, 12),["str","?"])
|
||||
})
|
||||
|
||||
it('should be able save all or incrementally', () => {
|
||||
let doc = Automerge.init()
|
||||
|
||||
doc.set("_root", "foo", 1)
|
||||
|
||||
let save1 = doc.save()
|
||||
|
||||
doc.set("_root", "bar", 2)
|
||||
|
||||
let saveMidway = doc.clone().save();
|
||||
|
||||
let save2 = doc.saveIncremental();
|
||||
|
||||
doc.set("_root", "baz", 3);
|
||||
|
||||
let save3 = doc.saveIncremental();
|
||||
|
||||
let saveA = doc.save();
|
||||
let saveB = new Uint8Array([... save1, ...save2, ...save3]);
|
||||
|
||||
assert.notDeepEqual(saveA, saveB);
|
||||
|
||||
let docA = Automerge.load(saveA);
|
||||
let docB = Automerge.load(saveB);
|
||||
let docC = Automerge.load(saveMidway)
|
||||
docC.loadIncremental(save3)
|
||||
|
||||
assert.deepEqual(docA.keys("_root"), docB.keys("_root"));
|
||||
assert.deepEqual(docA.save(), docB.save());
|
||||
assert.deepEqual(docA.save(), docC.save());
|
||||
})
|
||||
|
||||
it('should be able to splice text', () => {
|
||||
let doc = Automerge.init()
|
||||
let text = doc.set("_root", "text", TEXT);
|
||||
doc.splice(text, 0, 0, "hello world");
|
||||
let heads1 = doc.commit();
|
||||
doc.splice(text, 6, 0, "big bad ");
|
||||
let heads2 = doc.commit();
|
||||
assert.strictEqual(doc.text(text), "hello big bad world")
|
||||
assert.strictEqual(doc.length(text), 19)
|
||||
assert.strictEqual(doc.text(text, heads1), "hello world")
|
||||
assert.strictEqual(doc.length(text, heads1), 11)
|
||||
assert.strictEqual(doc.text(text, heads2), "hello big bad world")
|
||||
assert.strictEqual(doc.length(text, heads2), 19)
|
||||
})
|
||||
|
||||
it('local inc increments all visible counters in a map', () => {
|
||||
let doc1 = Automerge.init("aaaa")
|
||||
doc1.set("_root", "hello", "world")
|
||||
let doc2 = Automerge.load(doc1.save(), "bbbb");
|
||||
let doc3 = Automerge.load(doc1.save(), "cccc");
|
||||
doc1.set("_root", "cnt", 20)
|
||||
doc2.set("_root", "cnt", 0, "counter")
|
||||
doc3.set("_root", "cnt", 10, "counter")
|
||||
doc1.applyChanges(doc2.getChanges(doc1.getHeads()))
|
||||
doc1.applyChanges(doc3.getChanges(doc1.getHeads()))
|
||||
let result = doc1.values("_root", "cnt")
|
||||
assert.deepEqual(result,[
|
||||
['counter',10,'2@cccc'],
|
||||
['counter',0,'2@bbbb'],
|
||||
['int',20,'2@aaaa']
|
||||
])
|
||||
doc1.inc("_root", "cnt", 5)
|
||||
result = doc1.values("_root", "cnt")
|
||||
assert.deepEqual(result, [
|
||||
[ 'counter', 15, '2@cccc' ], [ 'counter', 5, '2@bbbb' ]
|
||||
])
|
||||
|
||||
let save1 = doc1.save()
|
||||
let doc4 = Automerge.load(save1)
|
||||
assert.deepEqual(doc4.save(), save1);
|
||||
})
|
||||
|
||||
it('local inc increments all visible counters in a sequence', () => {
|
||||
let doc1 = Automerge.init("aaaa")
|
||||
let seq = doc1.set("_root", "seq", LIST)
|
||||
doc1.insert(seq, 0, "hello")
|
||||
let doc2 = Automerge.load(doc1.save(), "bbbb");
|
||||
let doc3 = Automerge.load(doc1.save(), "cccc");
|
||||
doc1.set(seq, 0, 20)
|
||||
doc2.set(seq, 0, 0, "counter")
|
||||
doc3.set(seq, 0, 10, "counter")
|
||||
doc1.applyChanges(doc2.getChanges(doc1.getHeads()))
|
||||
doc1.applyChanges(doc3.getChanges(doc1.getHeads()))
|
||||
let result = doc1.values(seq, 0)
|
||||
assert.deepEqual(result,[
|
||||
['counter',10,'3@cccc'],
|
||||
['counter',0,'3@bbbb'],
|
||||
['int',20,'3@aaaa']
|
||||
])
|
||||
doc1.inc(seq, 0, 5)
|
||||
result = doc1.values(seq, 0)
|
||||
assert.deepEqual(result, [
|
||||
[ 'counter', 15, '3@cccc' ], [ 'counter', 5, '3@bbbb' ]
|
||||
])
|
||||
|
||||
let save = doc1.save()
|
||||
let doc4 = Automerge.load(save)
|
||||
assert.deepEqual(doc4.save(), save);
|
||||
})
|
||||
|
||||
})
|
||||
})
|
38
automerge/Cargo.toml
Normal file
38
automerge/Cargo.toml
Normal file
|
@ -0,0 +1,38 @@
|
|||
[package]
|
||||
name = "automerge"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
license = "MIT"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
optree-visualisation = ["dot"]
|
||||
|
||||
[dependencies]
|
||||
hex = "^0.4.3"
|
||||
leb128 = "^0.2.5"
|
||||
sha2 = "^0.10.0"
|
||||
rand = { version = "^0.8.4" }
|
||||
thiserror = "^1.0.16"
|
||||
itertools = "^0.10.3"
|
||||
flate2 = "^1.0.22"
|
||||
nonzero_ext = "^0.2.0"
|
||||
uuid = { version = "^0.8.2", features=["v4", "wasm-bindgen", "serde"] }
|
||||
smol_str = "^0.1.21"
|
||||
tracing = { version = "^0.1.29", features = ["log"] }
|
||||
fxhash = "^0.2.1"
|
||||
tinyvec = { version = "^1.5.1", features = ["alloc"] }
|
||||
unicode-segmentation = "1.7.1"
|
||||
serde = { version = "^1.0", features=["derive"] }
|
||||
dot = { version = "0.1.4", optional = true }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "^0.3.55"
|
||||
features = ["console"]
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.0.0"
|
||||
proptest = { version = "^1.0.0", default-features = false, features = ["std"] }
|
||||
serde_json = { version = "^1.0.73", features=["float_roundtrip"], default-features=true }
|
||||
maplit = { version = "^1.0" }
|
18
automerge/TODO.md
Normal file
18
automerge/TODO.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
|
||||
counters -> Visibility
|
||||
|
||||
fast load
|
||||
|
||||
values at clock
|
||||
length at clock
|
||||
keys at clock
|
||||
text at clock
|
||||
|
||||
extra tests
|
||||
counters in lists -> inserts with tombstones
|
||||
|
||||
ergronomics
|
||||
|
||||
set(obj, prop, val) vs mapset(obj, str, val) and seqset(obj, usize, val)
|
||||
value() -> (id, value)
|
||||
|
916
automerge/src/change.rs
Normal file
916
automerge/src/change.rs
Normal file
|
@ -0,0 +1,916 @@
|
|||
use crate::columnar::{
|
||||
ChangeEncoder, ChangeIterator, ColumnEncoder, DepsIterator, DocChange, DocOp, DocOpEncoder,
|
||||
DocOpIterator, OperationIterator, COLUMN_TYPE_DEFLATE,
|
||||
};
|
||||
use crate::decoding;
|
||||
use crate::decoding::{Decodable, InvalidChangeError};
|
||||
use crate::encoding::{Encodable, DEFLATE_MIN_SIZE};
|
||||
use crate::legacy as amp;
|
||||
use crate::{
|
||||
ActorId, AutomergeError, ElemId, IndexedCache, Key, ObjId, Op, OpId, OpType, Transaction, HEAD,
|
||||
};
|
||||
use core::ops::Range;
|
||||
use flate2::{
|
||||
bufread::{DeflateDecoder, DeflateEncoder},
|
||||
Compression,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use sha2::Digest;
|
||||
use sha2::Sha256;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::convert::TryInto;
|
||||
use std::fmt::Debug;
|
||||
use std::io::{Read, Write};
|
||||
use tracing::instrument;
|
||||
|
||||
const MAGIC_BYTES: [u8; 4] = [0x85, 0x6f, 0x4a, 0x83];
|
||||
const PREAMBLE_BYTES: usize = 8;
|
||||
const HEADER_BYTES: usize = PREAMBLE_BYTES + 1;
|
||||
|
||||
const HASH_BYTES: usize = 32;
|
||||
const BLOCK_TYPE_DOC: u8 = 0;
|
||||
const BLOCK_TYPE_CHANGE: u8 = 1;
|
||||
const BLOCK_TYPE_DEFLATE: u8 = 2;
|
||||
const CHUNK_START: usize = 8;
|
||||
const HASH_RANGE: Range<usize> = 4..8;
|
||||
|
||||
fn get_heads(changes: &[amp::Change]) -> HashSet<amp::ChangeHash> {
|
||||
changes.iter().fold(HashSet::new(), |mut acc, c| {
|
||||
if let Some(h) = c.hash {
|
||||
acc.insert(h);
|
||||
}
|
||||
for dep in &c.deps {
|
||||
acc.remove(dep);
|
||||
}
|
||||
acc
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn encode_document(
|
||||
changes: &[amp::Change],
|
||||
doc_ops: &[Op],
|
||||
actors_index: &IndexedCache<ActorId>,
|
||||
props: &[String],
|
||||
) -> Result<Vec<u8>, AutomergeError> {
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
|
||||
let heads = get_heads(changes);
|
||||
|
||||
let actors_map = actors_index.encode_index();
|
||||
let actors = actors_index.sorted();
|
||||
|
||||
/*
|
||||
// this assumes that all actor_ids referenced are seen in changes.actor_id which is true
|
||||
// so long as we have a full history
|
||||
let mut actors: Vec<_> = changes
|
||||
.iter()
|
||||
.map(|c| &c.actor)
|
||||
.unique()
|
||||
.sorted()
|
||||
.cloned()
|
||||
.collect();
|
||||
*/
|
||||
|
||||
let (change_bytes, change_info) = ChangeEncoder::encode_changes(changes, &actors);
|
||||
|
||||
//let doc_ops = group_doc_ops(changes, &actors);
|
||||
|
||||
let (ops_bytes, ops_info) = DocOpEncoder::encode_doc_ops(doc_ops, &actors_map, props);
|
||||
|
||||
bytes.extend(&MAGIC_BYTES);
|
||||
bytes.extend(vec![0, 0, 0, 0]); // we dont know the hash yet so fill in a fake
|
||||
bytes.push(BLOCK_TYPE_DOC);
|
||||
|
||||
let mut chunk = Vec::new();
|
||||
|
||||
actors.len().encode(&mut chunk)?;
|
||||
|
||||
for a in actors.into_iter() {
|
||||
a.to_bytes().encode(&mut chunk)?;
|
||||
}
|
||||
|
||||
heads.len().encode(&mut chunk)?;
|
||||
for head in heads.iter().sorted() {
|
||||
chunk.write_all(&head.0).unwrap();
|
||||
}
|
||||
|
||||
chunk.extend(change_info);
|
||||
chunk.extend(ops_info);
|
||||
|
||||
chunk.extend(change_bytes);
|
||||
chunk.extend(ops_bytes);
|
||||
|
||||
leb128::write::unsigned(&mut bytes, chunk.len() as u64).unwrap();
|
||||
|
||||
bytes.extend(&chunk);
|
||||
|
||||
let hash_result = Sha256::digest(&bytes[CHUNK_START..bytes.len()]);
|
||||
|
||||
bytes.splice(HASH_RANGE, hash_result[0..4].iter().copied());
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
impl From<amp::Change> for Change {
|
||||
fn from(value: amp::Change) -> Self {
|
||||
encode(&value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&::Change> for Change {
|
||||
fn from(value: &::Change) -> Self {
|
||||
encode(value)
|
||||
}
|
||||
}
|
||||
|
||||
fn encode(change: &::Change) -> Change {
|
||||
let mut deps = change.deps.clone();
|
||||
deps.sort_unstable();
|
||||
|
||||
let mut chunk = encode_chunk(change, &deps);
|
||||
|
||||
let mut bytes = Vec::with_capacity(MAGIC_BYTES.len() + 4 + chunk.bytes.len());
|
||||
|
||||
bytes.extend(&MAGIC_BYTES);
|
||||
|
||||
bytes.extend(vec![0, 0, 0, 0]); // we dont know the hash yet so fill in a fake
|
||||
|
||||
bytes.push(BLOCK_TYPE_CHANGE);
|
||||
|
||||
leb128::write::unsigned(&mut bytes, chunk.bytes.len() as u64).unwrap();
|
||||
|
||||
let body_start = bytes.len();
|
||||
|
||||
increment_range(&mut chunk.body, bytes.len());
|
||||
increment_range(&mut chunk.message, bytes.len());
|
||||
increment_range(&mut chunk.extra_bytes, bytes.len());
|
||||
increment_range_map(&mut chunk.ops, bytes.len());
|
||||
|
||||
bytes.extend(&chunk.bytes);
|
||||
|
||||
let hash_result = Sha256::digest(&bytes[CHUNK_START..bytes.len()]);
|
||||
let hash: amp::ChangeHash = hash_result[..].try_into().unwrap();
|
||||
|
||||
bytes.splice(HASH_RANGE, hash_result[0..4].iter().copied());
|
||||
|
||||
// any time I make changes to the encoder decoder its a good idea
|
||||
// to run it through a round trip to detect errors the tests might not
|
||||
// catch
|
||||
// let c0 = Change::from_bytes(bytes.clone()).unwrap();
|
||||
// std::assert_eq!(c1, c0);
|
||||
// perhaps we should add something like this to the test suite
|
||||
|
||||
let bytes = ChangeBytes::Uncompressed(bytes);
|
||||
|
||||
Change {
|
||||
bytes,
|
||||
body_start,
|
||||
hash,
|
||||
seq: change.seq,
|
||||
start_op: change.start_op,
|
||||
time: change.time,
|
||||
actors: chunk.actors,
|
||||
message: chunk.message,
|
||||
deps,
|
||||
ops: chunk.ops,
|
||||
extra_bytes: chunk.extra_bytes,
|
||||
}
|
||||
}
|
||||
|
||||
struct ChunkIntermediate {
|
||||
bytes: Vec<u8>,
|
||||
body: Range<usize>,
|
||||
actors: Vec<ActorId>,
|
||||
message: Range<usize>,
|
||||
ops: HashMap<u32, Range<usize>>,
|
||||
extra_bytes: Range<usize>,
|
||||
}
|
||||
|
||||
fn encode_chunk(change: &::Change, deps: &[amp::ChangeHash]) -> ChunkIntermediate {
|
||||
let mut bytes = Vec::new();
|
||||
|
||||
// All these unwraps are okay because we're writing to an in memory buffer so io erros should
|
||||
// not happen
|
||||
|
||||
// encode deps
|
||||
deps.len().encode(&mut bytes).unwrap();
|
||||
for hash in deps.iter() {
|
||||
bytes.write_all(&hash.0).unwrap();
|
||||
}
|
||||
|
||||
// encode first actor
|
||||
let mut actors = vec![change.actor_id.clone()];
|
||||
change.actor_id.to_bytes().encode(&mut bytes).unwrap();
|
||||
|
||||
// encode seq, start_op, time, message
|
||||
change.seq.encode(&mut bytes).unwrap();
|
||||
change.start_op.encode(&mut bytes).unwrap();
|
||||
change.time.encode(&mut bytes).unwrap();
|
||||
let message = bytes.len() + 1;
|
||||
change.message.encode(&mut bytes).unwrap();
|
||||
let message = message..bytes.len();
|
||||
|
||||
// encode ops into a side buffer - collect all other actors
|
||||
let (ops_buf, mut ops) = ColumnEncoder::encode_ops(&change.operations, &mut actors);
|
||||
|
||||
// encode all other actors
|
||||
actors[1..].encode(&mut bytes).unwrap();
|
||||
|
||||
// now we know how many bytes ops are offset by so we can adjust the ranges
|
||||
increment_range_map(&mut ops, bytes.len());
|
||||
|
||||
// write out the ops
|
||||
|
||||
bytes.write_all(&ops_buf).unwrap();
|
||||
|
||||
// write out the extra bytes
|
||||
let extra_bytes = bytes.len()..(bytes.len() + change.extra_bytes.len());
|
||||
bytes.write_all(&change.extra_bytes).unwrap();
|
||||
let body = 0..bytes.len();
|
||||
|
||||
ChunkIntermediate {
|
||||
bytes,
|
||||
body,
|
||||
actors,
|
||||
message,
|
||||
ops,
|
||||
extra_bytes,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
enum ChangeBytes {
|
||||
Compressed {
|
||||
compressed: Vec<u8>,
|
||||
uncompressed: Vec<u8>,
|
||||
},
|
||||
Uncompressed(Vec<u8>),
|
||||
}
|
||||
|
||||
impl ChangeBytes {
|
||||
fn uncompressed(&self) -> &[u8] {
|
||||
match self {
|
||||
ChangeBytes::Compressed { uncompressed, .. } => &uncompressed[..],
|
||||
ChangeBytes::Uncompressed(b) => &b[..],
|
||||
}
|
||||
}
|
||||
|
||||
fn compress(&mut self, body_start: usize) {
|
||||
match self {
|
||||
ChangeBytes::Compressed { .. } => {}
|
||||
ChangeBytes::Uncompressed(uncompressed) => {
|
||||
if uncompressed.len() > DEFLATE_MIN_SIZE {
|
||||
let mut result = Vec::with_capacity(uncompressed.len());
|
||||
result.extend(&uncompressed[0..8]);
|
||||
result.push(BLOCK_TYPE_DEFLATE);
|
||||
let mut deflater =
|
||||
DeflateEncoder::new(&uncompressed[body_start..], Compression::default());
|
||||
let mut deflated = Vec::new();
|
||||
let deflated_len = deflater.read_to_end(&mut deflated).unwrap();
|
||||
leb128::write::unsigned(&mut result, deflated_len as u64).unwrap();
|
||||
result.extend(&deflated[..]);
|
||||
*self = ChangeBytes::Compressed {
|
||||
compressed: result,
|
||||
uncompressed: std::mem::take(uncompressed),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn raw(&self) -> &[u8] {
|
||||
match self {
|
||||
ChangeBytes::Compressed { compressed, .. } => &compressed[..],
|
||||
ChangeBytes::Uncompressed(b) => &b[..],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub struct Change {
|
||||
bytes: ChangeBytes,
|
||||
body_start: usize,
|
||||
pub hash: amp::ChangeHash,
|
||||
pub seq: u64,
|
||||
pub start_op: u64,
|
||||
pub time: i64,
|
||||
message: Range<usize>,
|
||||
actors: Vec<ActorId>,
|
||||
pub deps: Vec<amp::ChangeHash>,
|
||||
ops: HashMap<u32, Range<usize>>,
|
||||
extra_bytes: Range<usize>,
|
||||
}
|
||||
|
||||
impl Change {
|
||||
pub fn actor_id(&self) -> &ActorId {
|
||||
&self.actors[0]
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(bytes))]
|
||||
pub fn load_document(bytes: &[u8]) -> Result<Vec<Change>, AutomergeError> {
|
||||
load_blocks(bytes)
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> Result<Change, decoding::Error> {
|
||||
decode_change(bytes)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
// TODO - this could be a lot more efficient
|
||||
self.iter_ops().count()
|
||||
}
|
||||
|
||||
pub fn max_op(&self) -> u64 {
|
||||
self.start_op + (self.len() as u64) - 1
|
||||
}
|
||||
|
||||
fn message(&self) -> Option<String> {
|
||||
let m = &self.bytes.uncompressed()[self.message.clone()];
|
||||
if m.is_empty() {
|
||||
None
|
||||
} else {
|
||||
std::str::from_utf8(m).map(ToString::to_string).ok()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode(&self) -> amp::Change {
|
||||
amp::Change {
|
||||
start_op: self.start_op,
|
||||
seq: self.seq,
|
||||
time: self.time,
|
||||
hash: Some(self.hash),
|
||||
message: self.message(),
|
||||
actor_id: self.actors[0].clone(),
|
||||
deps: self.deps.clone(),
|
||||
operations: self
|
||||
.iter_ops()
|
||||
.map(|op| amp::Op {
|
||||
action: op.action.clone(),
|
||||
obj: op.obj.clone(),
|
||||
key: op.key.clone(),
|
||||
pred: op.pred.clone(),
|
||||
insert: op.insert,
|
||||
})
|
||||
.collect(),
|
||||
extra_bytes: self.extra_bytes().into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn iter_ops(&self) -> OperationIterator {
|
||||
OperationIterator::new(self.bytes.uncompressed(), self.actors.as_slice(), &self.ops)
|
||||
}
|
||||
|
||||
pub fn extra_bytes(&self) -> &[u8] {
|
||||
&self.bytes.uncompressed()[self.extra_bytes.clone()]
|
||||
}
|
||||
|
||||
pub fn compress(&mut self) {
|
||||
self.bytes.compress(self.body_start);
|
||||
}
|
||||
|
||||
pub fn raw_bytes(&self) -> &[u8] {
|
||||
self.bytes.raw()
|
||||
}
|
||||
}
|
||||
|
||||
fn read_leb128(bytes: &mut &[u8]) -> Result<(usize, usize), decoding::Error> {
|
||||
let mut buf = &bytes[..];
|
||||
let val = leb128::read::unsigned(&mut buf)? as usize;
|
||||
let leb128_bytes = bytes.len() - buf.len();
|
||||
Ok((val, leb128_bytes))
|
||||
}
|
||||
|
||||
fn read_slice<T: Decodable + Debug>(
|
||||
bytes: &[u8],
|
||||
cursor: &mut Range<usize>,
|
||||
) -> Result<T, decoding::Error> {
|
||||
let mut view = &bytes[cursor.clone()];
|
||||
let init_len = view.len();
|
||||
let val = T::decode::<&[u8]>(&mut view).ok_or(decoding::Error::NoDecodedValue);
|
||||
let bytes_read = init_len - view.len();
|
||||
*cursor = (cursor.start + bytes_read)..cursor.end;
|
||||
val
|
||||
}
|
||||
|
||||
fn slice_bytes(bytes: &[u8], cursor: &mut Range<usize>) -> Result<Range<usize>, decoding::Error> {
|
||||
let (val, len) = read_leb128(&mut &bytes[cursor.clone()])?;
|
||||
let start = cursor.start + len;
|
||||
let end = start + val;
|
||||
*cursor = end..cursor.end;
|
||||
Ok(start..end)
|
||||
}
|
||||
|
||||
fn increment_range(range: &mut Range<usize>, len: usize) {
|
||||
range.end += len;
|
||||
range.start += len;
|
||||
}
|
||||
|
||||
fn increment_range_map(ranges: &mut HashMap<u32, Range<usize>>, len: usize) {
|
||||
for range in ranges.values_mut() {
|
||||
increment_range(range, len);
|
||||
}
|
||||
}
|
||||
|
||||
fn export_objid(id: &ObjId, actors: &IndexedCache<ActorId>) -> amp::ObjectId {
|
||||
if id == &ObjId::root() {
|
||||
amp::ObjectId::Root
|
||||
} else {
|
||||
export_opid(&id.0, actors).into()
|
||||
}
|
||||
}
|
||||
|
||||
fn export_elemid(id: &ElemId, actors: &IndexedCache<ActorId>) -> amp::ElementId {
|
||||
if id == &HEAD {
|
||||
amp::ElementId::Head
|
||||
} else {
|
||||
export_opid(&id.0, actors).into()
|
||||
}
|
||||
}
|
||||
|
||||
fn export_opid(id: &OpId, actors: &IndexedCache<ActorId>) -> amp::OpId {
|
||||
amp::OpId(id.0, actors.get(id.1).clone())
|
||||
}
|
||||
|
||||
fn export_op(op: &Op, actors: &IndexedCache<ActorId>, props: &IndexedCache<String>) -> amp::Op {
|
||||
let action = op.action.clone();
|
||||
let key = match &op.key {
|
||||
Key::Map(n) => amp::Key::Map(props.get(*n).clone().into()),
|
||||
Key::Seq(id) => amp::Key::Seq(export_elemid(id, actors)),
|
||||
};
|
||||
let obj = export_objid(&op.obj, actors);
|
||||
let pred = op.pred.iter().map(|id| export_opid(id, actors)).collect();
|
||||
amp::Op {
|
||||
action,
|
||||
obj,
|
||||
insert: op.insert,
|
||||
pred,
|
||||
key,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn export_change(
|
||||
change: &Transaction,
|
||||
actors: &IndexedCache<ActorId>,
|
||||
props: &IndexedCache<String>,
|
||||
) -> Change {
|
||||
amp::Change {
|
||||
actor_id: actors.get(change.actor).clone(),
|
||||
seq: change.seq,
|
||||
start_op: change.start_op,
|
||||
time: change.time,
|
||||
deps: change.deps.clone(),
|
||||
message: change.message.clone(),
|
||||
hash: change.hash,
|
||||
operations: change
|
||||
.operations
|
||||
.iter()
|
||||
.map(|op| export_op(op, actors, props))
|
||||
.collect(),
|
||||
extra_bytes: change.extra_bytes.clone(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn decode_change(bytes: Vec<u8>) -> Result<Change, decoding::Error> {
|
||||
let (chunktype, body) = decode_header_without_hash(&bytes)?;
|
||||
let bytes = if chunktype == BLOCK_TYPE_DEFLATE {
|
||||
decompress_chunk(0..PREAMBLE_BYTES, body, bytes)?
|
||||
} else {
|
||||
ChangeBytes::Uncompressed(bytes)
|
||||
};
|
||||
|
||||
let (chunktype, hash, body) = decode_header(bytes.uncompressed())?;
|
||||
|
||||
if chunktype != BLOCK_TYPE_CHANGE {
|
||||
return Err(decoding::Error::WrongType {
|
||||
expected_one_of: vec![BLOCK_TYPE_CHANGE],
|
||||
found: chunktype,
|
||||
});
|
||||
}
|
||||
|
||||
let body_start = body.start;
|
||||
let mut cursor = body;
|
||||
|
||||
let deps = decode_hashes(bytes.uncompressed(), &mut cursor)?;
|
||||
|
||||
let actor =
|
||||
ActorId::from(&bytes.uncompressed()[slice_bytes(bytes.uncompressed(), &mut cursor)?]);
|
||||
let seq = read_slice(bytes.uncompressed(), &mut cursor)?;
|
||||
let start_op = read_slice(bytes.uncompressed(), &mut cursor)?;
|
||||
let time = read_slice(bytes.uncompressed(), &mut cursor)?;
|
||||
let message = slice_bytes(bytes.uncompressed(), &mut cursor)?;
|
||||
|
||||
let actors = decode_actors(bytes.uncompressed(), &mut cursor, Some(actor))?;
|
||||
|
||||
let ops_info = decode_column_info(bytes.uncompressed(), &mut cursor, false)?;
|
||||
let ops = decode_columns(&mut cursor, &ops_info);
|
||||
|
||||
Ok(Change {
|
||||
bytes,
|
||||
body_start,
|
||||
hash,
|
||||
seq,
|
||||
start_op,
|
||||
time,
|
||||
actors,
|
||||
message,
|
||||
deps,
|
||||
ops,
|
||||
extra_bytes: cursor,
|
||||
})
|
||||
}
|
||||
|
||||
fn decompress_chunk(
|
||||
preamble: Range<usize>,
|
||||
body: Range<usize>,
|
||||
compressed: Vec<u8>,
|
||||
) -> Result<ChangeBytes, decoding::Error> {
|
||||
let mut decoder = DeflateDecoder::new(&compressed[body]);
|
||||
let mut decompressed = Vec::new();
|
||||
decoder.read_to_end(&mut decompressed)?;
|
||||
let mut result = Vec::with_capacity(decompressed.len() + preamble.len());
|
||||
result.extend(&compressed[preamble]);
|
||||
result.push(BLOCK_TYPE_CHANGE);
|
||||
leb128::write::unsigned::<Vec<u8>>(&mut result, decompressed.len() as u64).unwrap();
|
||||
result.extend(decompressed);
|
||||
Ok(ChangeBytes::Compressed {
|
||||
uncompressed: result,
|
||||
compressed,
|
||||
})
|
||||
}
|
||||
|
||||
fn decode_hashes(
|
||||
bytes: &[u8],
|
||||
cursor: &mut Range<usize>,
|
||||
) -> Result<Vec<amp::ChangeHash>, decoding::Error> {
|
||||
let num_hashes = read_slice(bytes, cursor)?;
|
||||
let mut hashes = Vec::with_capacity(num_hashes);
|
||||
for _ in 0..num_hashes {
|
||||
let hash = cursor.start..(cursor.start + HASH_BYTES);
|
||||
*cursor = hash.end..cursor.end;
|
||||
hashes.push(
|
||||
bytes
|
||||
.get(hash)
|
||||
.ok_or(decoding::Error::NotEnoughBytes)?
|
||||
.try_into()
|
||||
.map_err(InvalidChangeError::from)?,
|
||||
);
|
||||
}
|
||||
Ok(hashes)
|
||||
}
|
||||
|
||||
fn decode_actors(
|
||||
bytes: &[u8],
|
||||
cursor: &mut Range<usize>,
|
||||
first: Option<ActorId>,
|
||||
) -> Result<Vec<ActorId>, decoding::Error> {
|
||||
let num_actors: usize = read_slice(bytes, cursor)?;
|
||||
let mut actors = Vec::with_capacity(num_actors + 1);
|
||||
if let Some(actor) = first {
|
||||
actors.push(actor);
|
||||
}
|
||||
for _ in 0..num_actors {
|
||||
actors.push(ActorId::from(
|
||||
bytes
|
||||
.get(slice_bytes(bytes, cursor)?)
|
||||
.ok_or(decoding::Error::NotEnoughBytes)?,
|
||||
));
|
||||
}
|
||||
Ok(actors)
|
||||
}
|
||||
|
||||
fn decode_column_info(
|
||||
bytes: &[u8],
|
||||
cursor: &mut Range<usize>,
|
||||
allow_compressed_column: bool,
|
||||
) -> Result<Vec<(u32, usize)>, decoding::Error> {
|
||||
let num_columns = read_slice(bytes, cursor)?;
|
||||
let mut columns = Vec::with_capacity(num_columns);
|
||||
let mut last_id = 0;
|
||||
for _ in 0..num_columns {
|
||||
let id: u32 = read_slice(bytes, cursor)?;
|
||||
if (id & !COLUMN_TYPE_DEFLATE) <= (last_id & !COLUMN_TYPE_DEFLATE) {
|
||||
return Err(decoding::Error::ColumnsNotInAscendingOrder {
|
||||
last: last_id,
|
||||
found: id,
|
||||
});
|
||||
}
|
||||
if id & COLUMN_TYPE_DEFLATE != 0 && !allow_compressed_column {
|
||||
return Err(decoding::Error::ChangeContainedCompressedColumns);
|
||||
}
|
||||
last_id = id;
|
||||
let length = read_slice(bytes, cursor)?;
|
||||
columns.push((id, length));
|
||||
}
|
||||
Ok(columns)
|
||||
}
|
||||
|
||||
fn decode_columns(
|
||||
cursor: &mut Range<usize>,
|
||||
columns: &[(u32, usize)],
|
||||
) -> HashMap<u32, Range<usize>> {
|
||||
let mut ops = HashMap::new();
|
||||
for (id, length) in columns {
|
||||
let start = cursor.start;
|
||||
let end = start + length;
|
||||
*cursor = end..cursor.end;
|
||||
ops.insert(*id, start..end);
|
||||
}
|
||||
ops
|
||||
}
|
||||
|
||||
fn decode_header(bytes: &[u8]) -> Result<(u8, amp::ChangeHash, Range<usize>), decoding::Error> {
|
||||
let (chunktype, body) = decode_header_without_hash(bytes)?;
|
||||
|
||||
let calculated_hash = Sha256::digest(&bytes[PREAMBLE_BYTES..]);
|
||||
|
||||
let checksum = &bytes[4..8];
|
||||
if checksum != &calculated_hash[0..4] {
|
||||
return Err(decoding::Error::InvalidChecksum {
|
||||
found: checksum.try_into().unwrap(),
|
||||
calculated: calculated_hash[0..4].try_into().unwrap(),
|
||||
});
|
||||
}
|
||||
|
||||
let hash = calculated_hash[..]
|
||||
.try_into()
|
||||
.map_err(InvalidChangeError::from)?;
|
||||
|
||||
Ok((chunktype, hash, body))
|
||||
}
|
||||
|
||||
fn decode_header_without_hash(bytes: &[u8]) -> Result<(u8, Range<usize>), decoding::Error> {
|
||||
if bytes.len() <= HEADER_BYTES {
|
||||
return Err(decoding::Error::NotEnoughBytes);
|
||||
}
|
||||
|
||||
if bytes[0..4] != MAGIC_BYTES {
|
||||
return Err(decoding::Error::WrongMagicBytes);
|
||||
}
|
||||
|
||||
let (val, len) = read_leb128(&mut &bytes[HEADER_BYTES..])?;
|
||||
let body = (HEADER_BYTES + len)..(HEADER_BYTES + len + val);
|
||||
if bytes.len() != body.end {
|
||||
return Err(decoding::Error::WrongByteLength {
|
||||
expected: body.end,
|
||||
found: bytes.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let chunktype = bytes[PREAMBLE_BYTES];
|
||||
|
||||
Ok((chunktype, body))
|
||||
}
|
||||
|
||||
fn load_blocks(bytes: &[u8]) -> Result<Vec<Change>, AutomergeError> {
|
||||
let mut changes = Vec::new();
|
||||
for slice in split_blocks(bytes)? {
|
||||
decode_block(slice, &mut changes)?;
|
||||
}
|
||||
Ok(changes)
|
||||
}
|
||||
|
||||
fn split_blocks(bytes: &[u8]) -> Result<Vec<&[u8]>, decoding::Error> {
|
||||
// split off all valid blocks - ignore the rest if its corrupted or truncated
|
||||
let mut blocks = Vec::new();
|
||||
let mut cursor = bytes;
|
||||
while let Some(block) = pop_block(cursor)? {
|
||||
blocks.push(&cursor[block.clone()]);
|
||||
if cursor.len() <= block.end {
|
||||
break;
|
||||
}
|
||||
cursor = &cursor[block.end..];
|
||||
}
|
||||
Ok(blocks)
|
||||
}
|
||||
|
||||
fn pop_block(bytes: &[u8]) -> Result<Option<Range<usize>>, decoding::Error> {
|
||||
if bytes.len() < 4 || bytes[0..4] != MAGIC_BYTES {
|
||||
// not reporting error here - file got corrupted?
|
||||
return Ok(None);
|
||||
}
|
||||
let (val, len) = read_leb128(
|
||||
&mut bytes
|
||||
.get(HEADER_BYTES..)
|
||||
.ok_or(decoding::Error::NotEnoughBytes)?,
|
||||
)?;
|
||||
// val is arbitrary so it could overflow
|
||||
let end = (HEADER_BYTES + len)
|
||||
.checked_add(val)
|
||||
.ok_or(decoding::Error::Overflow)?;
|
||||
if end > bytes.len() {
|
||||
// not reporting error here - file got truncated?
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(0..end))
|
||||
}
|
||||
|
||||
fn decode_block(bytes: &[u8], changes: &mut Vec<Change>) -> Result<(), decoding::Error> {
|
||||
match bytes[PREAMBLE_BYTES] {
|
||||
BLOCK_TYPE_DOC => {
|
||||
changes.extend(decode_document(bytes)?);
|
||||
Ok(())
|
||||
}
|
||||
BLOCK_TYPE_CHANGE | BLOCK_TYPE_DEFLATE => {
|
||||
changes.push(decode_change(bytes.to_vec())?);
|
||||
Ok(())
|
||||
}
|
||||
found => Err(decoding::Error::WrongType {
|
||||
expected_one_of: vec![BLOCK_TYPE_DOC, BLOCK_TYPE_CHANGE, BLOCK_TYPE_DEFLATE],
|
||||
found,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_document(bytes: &[u8]) -> Result<Vec<Change>, decoding::Error> {
|
||||
let (chunktype, _hash, mut cursor) = decode_header(bytes)?;
|
||||
|
||||
// chunktype == 0 is a document, chunktype = 1 is a change
|
||||
if chunktype > 0 {
|
||||
return Err(decoding::Error::WrongType {
|
||||
expected_one_of: vec![0],
|
||||
found: chunktype,
|
||||
});
|
||||
}
|
||||
|
||||
let actors = decode_actors(bytes, &mut cursor, None)?;
|
||||
|
||||
let heads = decode_hashes(bytes, &mut cursor)?;
|
||||
|
||||
let changes_info = decode_column_info(bytes, &mut cursor, true)?;
|
||||
let ops_info = decode_column_info(bytes, &mut cursor, true)?;
|
||||
|
||||
let changes_data = decode_columns(&mut cursor, &changes_info);
|
||||
let mut doc_changes = ChangeIterator::new(bytes, &changes_data).collect::<Vec<_>>();
|
||||
let doc_changes_deps = DepsIterator::new(bytes, &changes_data);
|
||||
|
||||
let doc_changes_len = doc_changes.len();
|
||||
|
||||
let ops_data = decode_columns(&mut cursor, &ops_info);
|
||||
let doc_ops: Vec<_> = DocOpIterator::new(bytes, &actors, &ops_data).collect();
|
||||
|
||||
group_doc_change_and_doc_ops(&mut doc_changes, doc_ops, &actors)?;
|
||||
|
||||
let uncompressed_changes =
|
||||
doc_changes_to_uncompressed_changes(doc_changes.into_iter(), &actors);
|
||||
|
||||
let changes = compress_doc_changes(uncompressed_changes, doc_changes_deps, doc_changes_len)
|
||||
.ok_or(decoding::Error::NoDocChanges)?;
|
||||
|
||||
let mut calculated_heads = HashSet::new();
|
||||
for change in &changes {
|
||||
for dep in &change.deps {
|
||||
calculated_heads.remove(dep);
|
||||
}
|
||||
calculated_heads.insert(change.hash);
|
||||
}
|
||||
|
||||
if calculated_heads != heads.into_iter().collect::<HashSet<_>>() {
|
||||
return Err(decoding::Error::MismatchedHeads);
|
||||
}
|
||||
|
||||
Ok(changes)
|
||||
}
|
||||
|
||||
fn compress_doc_changes(
|
||||
uncompressed_changes: impl Iterator<Item = amp::Change>,
|
||||
doc_changes_deps: impl Iterator<Item = Vec<usize>>,
|
||||
num_changes: usize,
|
||||
) -> Option<Vec<Change>> {
|
||||
let mut changes: Vec<Change> = Vec::with_capacity(num_changes);
|
||||
|
||||
// fill out the hashes as we go
|
||||
for (deps, mut uncompressed_change) in doc_changes_deps.zip_eq(uncompressed_changes) {
|
||||
for idx in deps {
|
||||
uncompressed_change.deps.push(changes.get(idx)?.hash);
|
||||
}
|
||||
changes.push(uncompressed_change.into());
|
||||
}
|
||||
|
||||
Some(changes)
|
||||
}
|
||||
|
||||
fn group_doc_change_and_doc_ops(
|
||||
changes: &mut [DocChange],
|
||||
mut ops: Vec<DocOp>,
|
||||
actors: &[ActorId],
|
||||
) -> Result<(), decoding::Error> {
|
||||
let mut changes_by_actor: HashMap<usize, Vec<usize>> = HashMap::new();
|
||||
|
||||
for (i, change) in changes.iter().enumerate() {
|
||||
let actor_change_index = changes_by_actor.entry(change.actor).or_default();
|
||||
if change.seq != (actor_change_index.len() + 1) as u64 {
|
||||
return Err(decoding::Error::ChangeDecompressFailed(
|
||||
"Doc Seq Invalid".into(),
|
||||
));
|
||||
}
|
||||
if change.actor >= actors.len() {
|
||||
return Err(decoding::Error::ChangeDecompressFailed(
|
||||
"Doc Actor Invalid".into(),
|
||||
));
|
||||
}
|
||||
actor_change_index.push(i);
|
||||
}
|
||||
|
||||
let mut op_by_id = HashMap::new();
|
||||
ops.iter().enumerate().for_each(|(i, op)| {
|
||||
op_by_id.insert((op.ctr, op.actor), i);
|
||||
});
|
||||
|
||||
for i in 0..ops.len() {
|
||||
let op = ops[i].clone(); // this is safe - avoid borrow checker issues
|
||||
//let id = (op.ctr, op.actor);
|
||||
//op_by_id.insert(id, i);
|
||||
for succ in &op.succ {
|
||||
if let Some(index) = op_by_id.get(succ) {
|
||||
ops[*index].pred.push((op.ctr, op.actor));
|
||||
} else {
|
||||
let key = if op.insert {
|
||||
amp::OpId(op.ctr, actors[op.actor].clone()).into()
|
||||
} else {
|
||||
op.key.clone()
|
||||
};
|
||||
let del = DocOp {
|
||||
actor: succ.1,
|
||||
ctr: succ.0,
|
||||
action: OpType::Del,
|
||||
obj: op.obj.clone(),
|
||||
key,
|
||||
succ: Vec::new(),
|
||||
pred: vec![(op.ctr, op.actor)],
|
||||
insert: false,
|
||||
};
|
||||
op_by_id.insert(*succ, ops.len());
|
||||
ops.push(del);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for op in ops {
|
||||
// binary search for our change
|
||||
let actor_change_index = changes_by_actor.entry(op.actor).or_default();
|
||||
let mut left = 0;
|
||||
let mut right = actor_change_index.len();
|
||||
while left < right {
|
||||
let seq = (left + right) / 2;
|
||||
if changes[actor_change_index[seq]].max_op < op.ctr {
|
||||
left = seq + 1;
|
||||
} else {
|
||||
right = seq;
|
||||
}
|
||||
}
|
||||
if left >= actor_change_index.len() {
|
||||
return Err(decoding::Error::ChangeDecompressFailed(
|
||||
"Doc MaxOp Invalid".into(),
|
||||
));
|
||||
}
|
||||
changes[actor_change_index[left]].ops.push(op);
|
||||
}
|
||||
|
||||
changes
|
||||
.iter_mut()
|
||||
.for_each(|change| change.ops.sort_unstable());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn doc_changes_to_uncompressed_changes<'a>(
|
||||
changes: impl Iterator<Item = DocChange> + 'a,
|
||||
actors: &'a [ActorId],
|
||||
) -> impl Iterator<Item = amp::Change> + 'a {
|
||||
changes.map(move |change| amp::Change {
|
||||
// we've already confirmed that all change.actor's are valid
|
||||
actor_id: actors[change.actor].clone(),
|
||||
seq: change.seq,
|
||||
time: change.time,
|
||||
start_op: change.max_op - change.ops.len() as u64 + 1,
|
||||
hash: None,
|
||||
message: change.message,
|
||||
operations: change
|
||||
.ops
|
||||
.into_iter()
|
||||
.map(|op| amp::Op {
|
||||
action: op.action.clone(),
|
||||
insert: op.insert,
|
||||
key: op.key,
|
||||
obj: op.obj,
|
||||
// we've already confirmed that all op.actor's are valid
|
||||
pred: pred_into(op.pred.into_iter(), actors),
|
||||
})
|
||||
.collect(),
|
||||
deps: Vec::new(),
|
||||
extra_bytes: change.extra_bytes,
|
||||
})
|
||||
}
|
||||
|
||||
fn pred_into(
|
||||
pred: impl Iterator<Item = (u64, usize)>,
|
||||
actors: &[ActorId],
|
||||
) -> amp::SortedVec<amp::OpId> {
|
||||
pred.map(|(ctr, actor)| amp::OpId(ctr, actors[actor].clone()))
|
||||
.collect()
|
||||
}
|
52
automerge/src/clock.rs
Normal file
52
automerge/src/clock.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use crate::OpId;
|
||||
use fxhash::FxBuildHasher;
|
||||
use std::cmp;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct Clock(HashMap<usize, u64, FxBuildHasher>);
|
||||
|
||||
impl Clock {
|
||||
pub fn new() -> Self {
|
||||
Clock(Default::default())
|
||||
}
|
||||
|
||||
pub fn include(&mut self, key: usize, n: u64) {
|
||||
self.0
|
||||
.entry(key)
|
||||
.and_modify(|m| *m = cmp::max(n, *m))
|
||||
.or_insert(n);
|
||||
}
|
||||
|
||||
pub fn covers(&self, id: &OpId) -> bool {
|
||||
if let Some(val) = self.0.get(&id.1) {
|
||||
val >= &id.0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn covers() {
|
||||
let mut clock = Clock::new();
|
||||
|
||||
clock.include(1, 20);
|
||||
clock.include(2, 10);
|
||||
|
||||
assert!(clock.covers(&OpId(10, 1)));
|
||||
assert!(clock.covers(&OpId(20, 1)));
|
||||
assert!(!clock.covers(&OpId(30, 1)));
|
||||
|
||||
assert!(clock.covers(&OpId(5, 2)));
|
||||
assert!(clock.covers(&OpId(10, 2)));
|
||||
assert!(!clock.covers(&OpId(15, 2)));
|
||||
|
||||
assert!(!clock.covers(&OpId(1, 3)));
|
||||
assert!(!clock.covers(&OpId(100, 3)));
|
||||
}
|
||||
}
|
1353
automerge/src/columnar.rs
Normal file
1353
automerge/src/columnar.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,5 @@
|
|||
use core::fmt::Debug;
|
||||
use std::num::NonZeroU64;
|
||||
use std::{borrow::Cow, io, io::Read, str};
|
||||
use std::{borrow::Cow, convert::TryFrom, io, io::Read, str};
|
||||
|
||||
use crate::error;
|
||||
use crate::legacy as amp;
|
||||
|
@ -52,60 +51,7 @@ pub enum Error {
|
|||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
impl PartialEq<Error> for Error {
|
||||
fn eq(&self, other: &Error) -> bool {
|
||||
match (self, other) {
|
||||
(
|
||||
Self::WrongType {
|
||||
expected_one_of: l_expected_one_of,
|
||||
found: l_found,
|
||||
},
|
||||
Self::WrongType {
|
||||
expected_one_of: r_expected_one_of,
|
||||
found: r_found,
|
||||
},
|
||||
) => l_expected_one_of == r_expected_one_of && l_found == r_found,
|
||||
(Self::BadChangeFormat(l0), Self::BadChangeFormat(r0)) => l0 == r0,
|
||||
(
|
||||
Self::WrongByteLength {
|
||||
expected: l_expected,
|
||||
found: l_found,
|
||||
},
|
||||
Self::WrongByteLength {
|
||||
expected: r_expected,
|
||||
found: r_found,
|
||||
},
|
||||
) => l_expected == r_expected && l_found == r_found,
|
||||
(
|
||||
Self::ColumnsNotInAscendingOrder {
|
||||
last: l_last,
|
||||
found: l_found,
|
||||
},
|
||||
Self::ColumnsNotInAscendingOrder {
|
||||
last: r_last,
|
||||
found: r_found,
|
||||
},
|
||||
) => l_last == r_last && l_found == r_found,
|
||||
(
|
||||
Self::InvalidChecksum {
|
||||
found: l_found,
|
||||
calculated: l_calculated,
|
||||
},
|
||||
Self::InvalidChecksum {
|
||||
found: r_found,
|
||||
calculated: r_calculated,
|
||||
},
|
||||
) => l_found == r_found && l_calculated == r_calculated,
|
||||
(Self::InvalidChange(l0), Self::InvalidChange(r0)) => l0 == r0,
|
||||
(Self::ChangeDecompressFailed(l0), Self::ChangeDecompressFailed(r0)) => l0 == r0,
|
||||
(Self::Leb128(_l0), Self::Leb128(_r0)) => true,
|
||||
(Self::Io(l0), Self::Io(r0)) => l0.kind() == r0.kind(),
|
||||
_ => core::mem::discriminant(self) == core::mem::discriminant(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, PartialEq, Debug)]
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum InvalidChangeError {
|
||||
#[error("Change contained an operation with action 'set' which did not have a 'value'")]
|
||||
SetOpWithoutValue,
|
||||
|
@ -125,13 +71,13 @@ pub enum InvalidChangeError {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Decoder<'a> {
|
||||
pub(crate) offset: usize,
|
||||
pub(crate) last_read: usize,
|
||||
pub offset: usize,
|
||||
pub last_read: usize,
|
||||
data: Cow<'a, [u8]>,
|
||||
}
|
||||
|
||||
impl<'a> Decoder<'a> {
|
||||
pub(crate) fn new(data: Cow<'a, [u8]>) -> Self {
|
||||
pub fn new(data: Cow<'a, [u8]>) -> Self {
|
||||
Decoder {
|
||||
offset: 0,
|
||||
last_read: 0,
|
||||
|
@ -139,7 +85,7 @@ impl<'a> Decoder<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn read<T: Decodable + Debug>(&mut self) -> Result<T, Error> {
|
||||
pub fn read<T: Decodable + Debug>(&mut self) -> Result<T, Error> {
|
||||
let mut buf = &self.data[self.offset..];
|
||||
let init_len = buf.len();
|
||||
let val = T::decode::<&[u8]>(&mut buf).ok_or(Error::NoDecodedValue)?;
|
||||
|
@ -153,7 +99,7 @@ impl<'a> Decoder<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn read_bytes(&mut self, index: usize) -> Result<&[u8], Error> {
|
||||
pub fn read_bytes(&mut self, index: usize) -> Result<&[u8], Error> {
|
||||
if self.offset + index > self.data.len() {
|
||||
Err(Error::TryingToReadPastEnd)
|
||||
} else {
|
||||
|
@ -164,7 +110,7 @@ impl<'a> Decoder<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn done(&self) -> bool {
|
||||
pub fn done(&self) -> bool {
|
||||
self.offset >= self.data.len()
|
||||
}
|
||||
}
|
||||
|
@ -212,7 +158,7 @@ impl<'a> Iterator for BooleanDecoder<'a> {
|
|||
/// See discussion on [`crate::encoding::RleEncoder`] for the format data is stored in.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct RleDecoder<'a, T> {
|
||||
pub(crate) decoder: Decoder<'a>,
|
||||
pub decoder: Decoder<'a>,
|
||||
last_value: Option<T>,
|
||||
count: isize,
|
||||
literal: bool,
|
||||
|
@ -407,15 +353,6 @@ impl Decodable for u64 {
|
|||
}
|
||||
}
|
||||
|
||||
impl Decodable for NonZeroU64 {
|
||||
fn decode<R>(bytes: &mut R) -> Option<Self>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
NonZeroU64::new(leb128::read::unsigned(bytes).ok()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decodable for Vec<u8> {
|
||||
fn decode<R>(bytes: &mut R) -> Option<Self>
|
||||
where
|
376
automerge/src/encoding.rs
Normal file
376
automerge/src/encoding.rs
Normal file
|
@ -0,0 +1,376 @@
|
|||
use core::fmt::Debug;
|
||||
use std::{
|
||||
io,
|
||||
io::{Read, Write},
|
||||
mem,
|
||||
};
|
||||
|
||||
use flate2::{bufread::DeflateEncoder, Compression};
|
||||
use smol_str::SmolStr;
|
||||
|
||||
use crate::columnar::COLUMN_TYPE_DEFLATE;
|
||||
use crate::ActorId;
|
||||
|
||||
pub(crate) const DEFLATE_MIN_SIZE: usize = 256;
|
||||
|
||||
/// The error type for encoding operations.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
/// Encodes booleans by storing the count of the same value.
|
||||
///
|
||||
/// The sequence of numbers describes the count of false values on even indices (0-indexed) and the
|
||||
/// count of true values on odd indices (0-indexed).
|
||||
///
|
||||
/// Counts are encoded as usize.
|
||||
pub(crate) struct BooleanEncoder {
|
||||
buf: Vec<u8>,
|
||||
last: bool,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl BooleanEncoder {
|
||||
pub fn new() -> BooleanEncoder {
|
||||
BooleanEncoder {
|
||||
buf: Vec::new(),
|
||||
last: false,
|
||||
count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append(&mut self, value: bool) {
|
||||
if value == self.last {
|
||||
self.count += 1;
|
||||
} else {
|
||||
self.count.encode(&mut self.buf).ok();
|
||||
self.last = value;
|
||||
self.count = 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(mut self, col: u32) -> ColData {
|
||||
if self.count > 0 {
|
||||
self.count.encode(&mut self.buf).ok();
|
||||
}
|
||||
ColData::new(col, self.buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// Encodes integers as the change since the previous value.
|
||||
///
|
||||
/// The initial value is 0 encoded as u64. Deltas are encoded as i64.
|
||||
///
|
||||
/// Run length encoding is then applied to the resulting sequence.
|
||||
pub(crate) struct DeltaEncoder {
|
||||
rle: RleEncoder<i64>,
|
||||
absolute_value: u64,
|
||||
}
|
||||
|
||||
impl DeltaEncoder {
|
||||
pub fn new() -> DeltaEncoder {
|
||||
DeltaEncoder {
|
||||
rle: RleEncoder::new(),
|
||||
absolute_value: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append_value(&mut self, value: u64) {
|
||||
self.rle
|
||||
.append_value(value as i64 - self.absolute_value as i64);
|
||||
self.absolute_value = value;
|
||||
}
|
||||
|
||||
pub fn append_null(&mut self) {
|
||||
self.rle.append_null();
|
||||
}
|
||||
|
||||
pub fn finish(self, col: u32) -> ColData {
|
||||
self.rle.finish(col)
|
||||
}
|
||||
}
|
||||
|
||||
enum RleState<T> {
|
||||
Empty,
|
||||
NullRun(usize),
|
||||
LiteralRun(T, Vec<T>),
|
||||
LoneVal(T),
|
||||
Run(T, usize),
|
||||
}
|
||||
|
||||
/// Encodes data in run lengh encoding format. This is very efficient for long repeats of data
|
||||
///
|
||||
/// There are 3 types of 'run' in this encoder:
|
||||
/// - a normal run (compresses repeated values)
|
||||
/// - a null run (compresses repeated nulls)
|
||||
/// - a literal run (no compression)
|
||||
///
|
||||
/// A normal run consists of the length of the run (encoded as an i64) followed by the encoded value that this run contains.
|
||||
///
|
||||
/// A null run consists of a zero value (encoded as an i64) followed by the length of the null run (encoded as a usize).
|
||||
///
|
||||
/// A literal run consists of the **negative** length of the run (encoded as an i64) followed by the values in the run.
|
||||
///
|
||||
/// Therefore all the types start with an encoded i64, the value of which determines the type of the following data.
|
||||
pub(crate) struct RleEncoder<T>
|
||||
where
|
||||
T: Encodable + PartialEq + Clone,
|
||||
{
|
||||
buf: Vec<u8>,
|
||||
state: RleState<T>,
|
||||
}
|
||||
|
||||
impl<T> RleEncoder<T>
|
||||
where
|
||||
T: Encodable + PartialEq + Clone,
|
||||
{
|
||||
pub fn new() -> RleEncoder<T> {
|
||||
RleEncoder {
|
||||
buf: Vec::new(),
|
||||
state: RleState::Empty,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(mut self, col: u32) -> ColData {
|
||||
match self.take_state() {
|
||||
// this covers `only_nulls`
|
||||
RleState::NullRun(size) => {
|
||||
if !self.buf.is_empty() {
|
||||
self.flush_null_run(size);
|
||||
}
|
||||
}
|
||||
RleState::LoneVal(value) => self.flush_lit_run(vec![value]),
|
||||
RleState::Run(value, len) => self.flush_run(&value, len),
|
||||
RleState::LiteralRun(last, mut run) => {
|
||||
run.push(last);
|
||||
self.flush_lit_run(run);
|
||||
}
|
||||
RleState::Empty => {}
|
||||
}
|
||||
ColData::new(col, self.buf)
|
||||
}
|
||||
|
||||
fn flush_run(&mut self, val: &T, len: usize) {
|
||||
self.encode(&(len as i64));
|
||||
self.encode(val);
|
||||
}
|
||||
|
||||
fn flush_null_run(&mut self, len: usize) {
|
||||
self.encode::<i64>(&0);
|
||||
self.encode(&len);
|
||||
}
|
||||
|
||||
fn flush_lit_run(&mut self, run: Vec<T>) {
|
||||
self.encode(&-(run.len() as i64));
|
||||
for val in run {
|
||||
self.encode(&val);
|
||||
}
|
||||
}
|
||||
|
||||
fn take_state(&mut self) -> RleState<T> {
|
||||
let mut state = RleState::Empty;
|
||||
mem::swap(&mut self.state, &mut state);
|
||||
state
|
||||
}
|
||||
|
||||
pub fn append_null(&mut self) {
|
||||
self.state = match self.take_state() {
|
||||
RleState::Empty => RleState::NullRun(1),
|
||||
RleState::NullRun(size) => RleState::NullRun(size + 1),
|
||||
RleState::LoneVal(other) => {
|
||||
self.flush_lit_run(vec![other]);
|
||||
RleState::NullRun(1)
|
||||
}
|
||||
RleState::Run(other, len) => {
|
||||
self.flush_run(&other, len);
|
||||
RleState::NullRun(1)
|
||||
}
|
||||
RleState::LiteralRun(last, mut run) => {
|
||||
run.push(last);
|
||||
self.flush_lit_run(run);
|
||||
RleState::NullRun(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append_value(&mut self, value: T) {
|
||||
self.state = match self.take_state() {
|
||||
RleState::Empty => RleState::LoneVal(value),
|
||||
RleState::LoneVal(other) => {
|
||||
if other == value {
|
||||
RleState::Run(value, 2)
|
||||
} else {
|
||||
let mut v = Vec::with_capacity(2);
|
||||
v.push(other);
|
||||
RleState::LiteralRun(value, v)
|
||||
}
|
||||
}
|
||||
RleState::Run(other, len) => {
|
||||
if other == value {
|
||||
RleState::Run(other, len + 1)
|
||||
} else {
|
||||
self.flush_run(&other, len);
|
||||
RleState::LoneVal(value)
|
||||
}
|
||||
}
|
||||
RleState::LiteralRun(last, mut run) => {
|
||||
if last == value {
|
||||
self.flush_lit_run(run);
|
||||
RleState::Run(value, 2)
|
||||
} else {
|
||||
run.push(last);
|
||||
RleState::LiteralRun(value, run)
|
||||
}
|
||||
}
|
||||
RleState::NullRun(size) => {
|
||||
self.flush_null_run(size);
|
||||
RleState::LoneVal(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn encode<V>(&mut self, val: &V)
|
||||
where
|
||||
V: Encodable,
|
||||
{
|
||||
val.encode(&mut self.buf).ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait Encodable {
|
||||
fn encode_with_actors_to_vec(&self, actors: &mut Vec<ActorId>) -> io::Result<Vec<u8>> {
|
||||
let mut buf = Vec::new();
|
||||
self.encode_with_actors(&mut buf, actors)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn encode_with_actors<R: Write>(
|
||||
&self,
|
||||
buf: &mut R,
|
||||
_actors: &mut Vec<ActorId>,
|
||||
) -> io::Result<usize> {
|
||||
self.encode(buf)
|
||||
}
|
||||
|
||||
fn encode<R: Write>(&self, buf: &mut R) -> io::Result<usize>;
|
||||
}
|
||||
|
||||
impl Encodable for SmolStr {
|
||||
fn encode<R: Write>(&self, buf: &mut R) -> io::Result<usize> {
|
||||
let bytes = self.as_bytes();
|
||||
let head = bytes.len().encode(buf)?;
|
||||
buf.write_all(bytes)?;
|
||||
Ok(head + bytes.len())
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for String {
|
||||
fn encode<R: Write>(&self, buf: &mut R) -> io::Result<usize> {
|
||||
let bytes = self.as_bytes();
|
||||
let head = bytes.len().encode(buf)?;
|
||||
buf.write_all(bytes)?;
|
||||
Ok(head + bytes.len())
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for Option<String> {
|
||||
fn encode<R: Write>(&self, buf: &mut R) -> io::Result<usize> {
|
||||
if let Some(s) = self {
|
||||
s.encode(buf)
|
||||
} else {
|
||||
0.encode(buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for u64 {
|
||||
fn encode<R: Write>(&self, buf: &mut R) -> io::Result<usize> {
|
||||
leb128::write::unsigned(buf, *self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for f64 {
|
||||
fn encode<R: Write>(&self, buf: &mut R) -> io::Result<usize> {
|
||||
let bytes = self.to_le_bytes();
|
||||
buf.write_all(&bytes)?;
|
||||
Ok(bytes.len())
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for f32 {
|
||||
fn encode<R: Write>(&self, buf: &mut R) -> io::Result<usize> {
|
||||
let bytes = self.to_le_bytes();
|
||||
buf.write_all(&bytes)?;
|
||||
Ok(bytes.len())
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for i64 {
|
||||
fn encode<R: Write>(&self, buf: &mut R) -> io::Result<usize> {
|
||||
leb128::write::signed(buf, *self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for usize {
|
||||
fn encode<R: Write>(&self, buf: &mut R) -> io::Result<usize> {
|
||||
(*self as u64).encode(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for u32 {
|
||||
fn encode<R: Write>(&self, buf: &mut R) -> io::Result<usize> {
|
||||
u64::from(*self).encode(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for i32 {
|
||||
fn encode<R: Write>(&self, buf: &mut R) -> io::Result<usize> {
|
||||
i64::from(*self).encode(buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ColData {
|
||||
pub col: u32,
|
||||
pub data: Vec<u8>,
|
||||
#[cfg(debug_assertions)]
|
||||
has_been_deflated: bool,
|
||||
}
|
||||
|
||||
impl ColData {
|
||||
pub fn new(col_id: u32, data: Vec<u8>) -> ColData {
|
||||
ColData {
|
||||
col: col_id,
|
||||
data,
|
||||
#[cfg(debug_assertions)]
|
||||
has_been_deflated: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode_col_len<R: Write>(&self, buf: &mut R) -> io::Result<usize> {
|
||||
let mut len = 0;
|
||||
if !self.data.is_empty() {
|
||||
len += self.col.encode(buf)?;
|
||||
len += self.data.len().encode(buf)?;
|
||||
}
|
||||
Ok(len)
|
||||
}
|
||||
|
||||
pub fn deflate(&mut self) {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
debug_assert!(!self.has_been_deflated);
|
||||
self.has_been_deflated = true;
|
||||
}
|
||||
if self.data.len() > DEFLATE_MIN_SIZE {
|
||||
let mut deflated = Vec::new();
|
||||
let mut deflater = DeflateEncoder::new(&self.data[..], Compression::default());
|
||||
//This unwrap should be okay as we're reading and writing to in memory buffers
|
||||
deflater.read_to_end(&mut deflated).unwrap();
|
||||
self.col |= COLUMN_TYPE_DEFLATE;
|
||||
self.data = deflated;
|
||||
}
|
||||
}
|
||||
}
|
63
automerge/src/error.rs
Normal file
63
automerge/src/error.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
use crate::decoding;
|
||||
use crate::value::DataType;
|
||||
use crate::ScalarValue;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AutomergeError {
|
||||
#[error("invalid opid format `{0}`")]
|
||||
InvalidOpId(String),
|
||||
#[error("there was an ecoding problem")]
|
||||
Encoding,
|
||||
#[error("there was a decoding problem")]
|
||||
Decoding,
|
||||
#[error("key must not be an empty string")]
|
||||
EmptyStringKey,
|
||||
#[error("invalid seq {0}")]
|
||||
InvalidSeq(u64),
|
||||
#[error("index {0} is out of bounds")]
|
||||
InvalidIndex(usize),
|
||||
#[error("generic automerge error")]
|
||||
Fail,
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for AutomergeError {
|
||||
fn from(_: std::io::Error) -> Self {
|
||||
AutomergeError::Encoding
|
||||
}
|
||||
}
|
||||
|
||||
impl From<decoding::Error> for AutomergeError {
|
||||
fn from(_: decoding::Error) -> Self {
|
||||
AutomergeError::Decoding
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[error("Invalid actor ID: {0}")]
|
||||
pub struct InvalidActorId(pub String);
|
||||
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
#[error("Invalid scalar value, expected {expected} but received {unexpected}")]
|
||||
pub(crate) struct InvalidScalarValue {
|
||||
pub raw_value: ScalarValue,
|
||||
pub datatype: DataType,
|
||||
pub unexpected: String,
|
||||
pub expected: String,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
#[error("Invalid change hash slice: {0:?}")]
|
||||
pub struct InvalidChangeHashSlice(pub Vec<u8>);
|
||||
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
#[error("Invalid object ID: {0}")]
|
||||
pub struct InvalidObjectId(pub String);
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[error("Invalid element ID: {0}")]
|
||||
pub struct InvalidElementId(pub String);
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[error("Invalid OpID: {0}")]
|
||||
pub struct InvalidOpId(pub String);
|
80
automerge/src/indexed_cache.rs
Normal file
80
automerge/src/indexed_cache.rs
Normal file
|
@ -0,0 +1,80 @@
|
|||
use itertools::Itertools;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hash;
|
||||
use std::ops::Index;
|
||||
use std::rc::Rc;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct IndexedCache<T> {
|
||||
pub cache: Vec<Rc<T>>,
|
||||
lookup: HashMap<T, usize>,
|
||||
}
|
||||
|
||||
impl<T> IndexedCache<T>
|
||||
where
|
||||
T: Clone + Eq + Hash + Ord,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
IndexedCache {
|
||||
cache: Default::default(),
|
||||
lookup: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cache(&mut self, item: T) -> usize {
|
||||
if let Some(n) = self.lookup.get(&item) {
|
||||
*n
|
||||
} else {
|
||||
let n = self.cache.len();
|
||||
self.cache.push(Rc::new(item.clone()));
|
||||
self.lookup.insert(item, n);
|
||||
n
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lookup(&self, item: &T) -> Option<usize> {
|
||||
self.lookup.get(item).cloned()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.cache.len()
|
||||
}
|
||||
|
||||
pub fn get(&self, index: usize) -> &T {
|
||||
&self.cache[index]
|
||||
}
|
||||
|
||||
pub fn sorted(&self) -> IndexedCache<T> {
|
||||
let mut sorted = Self::new();
|
||||
self.cache.iter().sorted().cloned().for_each(|item| {
|
||||
let n = sorted.cache.len();
|
||||
sorted.cache.push(item.clone());
|
||||
sorted.lookup.insert(item.as_ref().clone(), n);
|
||||
});
|
||||
sorted
|
||||
}
|
||||
|
||||
pub fn encode_index(&self) -> Vec<usize> {
|
||||
let sorted: Vec<_> = self.cache.iter().sorted().cloned().collect();
|
||||
self.cache
|
||||
.iter()
|
||||
.map(|a| sorted.iter().position(|r| r == a).unwrap())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoIterator for IndexedCache<T> {
|
||||
type Item = Rc<T>;
|
||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.cache.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Index<usize> for IndexedCache<T> {
|
||||
type Output = T;
|
||||
fn index(&self, i: usize) -> &T {
|
||||
&self.cache[i]
|
||||
}
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
mod serde_impls;
|
||||
mod utility_impls;
|
||||
use std::iter::FromIterator;
|
||||
|
||||
use std::num::NonZeroU64;
|
||||
|
||||
pub(crate) use crate::types::{ActorId, ChangeHash, ObjType, OpType, ScalarValue};
|
||||
pub(crate) use crate::value::DataType;
|
||||
pub(crate) use crate::{ActorId, ChangeHash, ObjType, OpType, ScalarValue};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smol_str::SmolStr;
|
||||
|
@ -132,7 +131,7 @@ impl Key {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize)]
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct SortedVec<T>(Vec<T>);
|
||||
|
||||
|
@ -157,7 +156,7 @@ impl<T> SortedVec<T> {
|
|||
self.0.get_mut(index)
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> std::slice::Iter<'_, T> {
|
||||
pub fn iter(&self) -> impl Iterator<Item = &T> {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
@ -216,8 +215,8 @@ pub struct Op {
|
|||
impl Op {
|
||||
pub fn primitive_value(&self) -> Option<ScalarValue> {
|
||||
match &self.action {
|
||||
OpType::Put(v) => Some(v.clone()),
|
||||
OpType::Increment(i) => Some(ScalarValue::Int(*i)),
|
||||
OpType::Set(v) => Some(v.clone()),
|
||||
OpType::Inc(i) => Some(ScalarValue::Int(*i)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -234,28 +233,19 @@ impl Op {
|
|||
}
|
||||
}
|
||||
|
||||
/// A change represents a group of operations performed by an actor.
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct Change {
|
||||
/// The operations performed in this change.
|
||||
#[serde(rename = "ops")]
|
||||
pub operations: Vec<Op>,
|
||||
/// The actor that performed this change.
|
||||
#[serde(rename = "actor")]
|
||||
pub actor_id: ActorId,
|
||||
/// The hash of this change.
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub hash: Option<ChangeHash>,
|
||||
/// The index of this change in the changes from this actor.
|
||||
pub seq: u64,
|
||||
/// The start operation index. Starts at 1.
|
||||
#[serde(rename = "startOp")]
|
||||
pub start_op: NonZeroU64,
|
||||
/// The time that this change was committed.
|
||||
pub start_op: u64,
|
||||
pub time: i64,
|
||||
/// The message of this change.
|
||||
pub message: Option<String>,
|
||||
/// The dependencies of this change.
|
||||
pub deps: Vec<ChangeHash>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", default = "Default::default")]
|
||||
pub extra_bytes: Vec<u8>,
|
|
@ -9,7 +9,7 @@ impl Serialize for ChangeHash {
|
|||
where
|
||||
S: Serializer,
|
||||
{
|
||||
hex::encode(self.0).serialize(serializer)
|
||||
hex::encode(&self.0).serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ impl Serialize for Op {
|
|||
}
|
||||
|
||||
let numerical_datatype = match &self.action {
|
||||
OpType::Put(value) => value.as_numerical_datatype(),
|
||||
OpType::Set(value) => value.as_numerical_datatype(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
|
@ -47,9 +47,8 @@ impl Serialize for Op {
|
|||
op.serialize_field("datatype", &datatype)?;
|
||||
}
|
||||
match &self.action {
|
||||
OpType::Increment(n) => op.serialize_field("value", &n)?,
|
||||
OpType::Put(ScalarValue::Counter(c)) => op.serialize_field("value", &c.start)?,
|
||||
OpType::Put(value) => op.serialize_field("value", &value)?,
|
||||
OpType::Inc(n) => op.serialize_field("value", &n)?,
|
||||
OpType::Set(value) => op.serialize_field("value", &value)?,
|
||||
_ => {}
|
||||
}
|
||||
op.serialize_field("pred", &self.pred)?;
|
||||
|
@ -104,8 +103,6 @@ impl<'de> Deserialize<'de> for RawOpType {
|
|||
"del",
|
||||
"inc",
|
||||
"set",
|
||||
"mark",
|
||||
"unmark",
|
||||
];
|
||||
// TODO: Probably more efficient to deserialize to a `&str`
|
||||
let raw_type = String::deserialize(deserializer)?;
|
||||
|
@ -132,7 +129,7 @@ impl<'de> Deserialize<'de> for Op {
|
|||
impl<'de> Visitor<'de> for OperationVisitor {
|
||||
type Value = Op;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("An operation object")
|
||||
}
|
||||
|
||||
|
@ -147,8 +144,6 @@ impl<'de> Deserialize<'de> for Op {
|
|||
let mut insert: Option<bool> = None;
|
||||
let mut datatype: Option<DataType> = None;
|
||||
let mut value: Option<Option<ScalarValue>> = None;
|
||||
let mut name: Option<String> = None;
|
||||
let mut expand: Option<bool> = None;
|
||||
let mut ref_id: Option<OpId> = None;
|
||||
while let Some(field) = map.next_key::<String>()? {
|
||||
match field.as_ref() {
|
||||
|
@ -172,8 +167,6 @@ impl<'de> Deserialize<'de> for Op {
|
|||
"insert" => read_field("insert", &mut insert, &mut map)?,
|
||||
"datatype" => read_field("datatype", &mut datatype, &mut map)?,
|
||||
"value" => read_field("value", &mut value, &mut map)?,
|
||||
"name" => read_field("name", &mut name, &mut map)?,
|
||||
"expand" => read_field("expand", &mut expand, &mut map)?,
|
||||
"ref" => read_field("ref", &mut ref_id, &mut map)?,
|
||||
_ => return Err(Error::unknown_field(&field, FIELDS)),
|
||||
}
|
||||
|
@ -188,7 +181,7 @@ impl<'de> Deserialize<'de> for Op {
|
|||
RawOpType::MakeTable => OpType::Make(ObjType::Table),
|
||||
RawOpType::MakeList => OpType::Make(ObjType::List),
|
||||
RawOpType::MakeText => OpType::Make(ObjType::Text),
|
||||
RawOpType::Del => OpType::Delete,
|
||||
RawOpType::Del => OpType::Del,
|
||||
RawOpType::Set => {
|
||||
let value = if let Some(datatype) = datatype {
|
||||
let raw_value = value
|
||||
|
@ -205,20 +198,17 @@ impl<'de> Deserialize<'de> for Op {
|
|||
.ok_or_else(|| Error::missing_field("value"))?
|
||||
.unwrap_or(ScalarValue::Null)
|
||||
};
|
||||
OpType::Put(value)
|
||||
OpType::Set(value)
|
||||
}
|
||||
RawOpType::Inc => match value.flatten() {
|
||||
Some(ScalarValue::Int(n)) => Ok(OpType::Increment(n)),
|
||||
Some(ScalarValue::Uint(n)) => Ok(OpType::Increment(n as i64)),
|
||||
Some(ScalarValue::F64(n)) => Ok(OpType::Increment(n as i64)),
|
||||
Some(ScalarValue::Counter(n)) => Ok(OpType::Increment(n.into())),
|
||||
Some(ScalarValue::Timestamp(n)) => Ok(OpType::Increment(n)),
|
||||
Some(ScalarValue::Int(n)) => Ok(OpType::Inc(n)),
|
||||
Some(ScalarValue::Uint(n)) => Ok(OpType::Inc(n as i64)),
|
||||
Some(ScalarValue::F64(n)) => Ok(OpType::Inc(n as i64)),
|
||||
Some(ScalarValue::Counter(n)) => Ok(OpType::Inc(n)),
|
||||
Some(ScalarValue::Timestamp(n)) => Ok(OpType::Inc(n)),
|
||||
Some(ScalarValue::Bytes(s)) => {
|
||||
Err(Error::invalid_value(Unexpected::Bytes(&s), &"a number"))
|
||||
}
|
||||
Some(ScalarValue::Unknown { bytes, .. }) => {
|
||||
Err(Error::invalid_value(Unexpected::Bytes(&bytes), &"a number"))
|
||||
}
|
||||
Some(ScalarValue::Str(s)) => {
|
||||
Err(Error::invalid_value(Unexpected::Str(&s), &"a number"))
|
||||
}
|
||||
|
@ -270,7 +260,7 @@ mod tests {
|
|||
"pred": []
|
||||
}),
|
||||
expected: Ok(Op {
|
||||
action: OpType::Put(ScalarValue::Uint(123)),
|
||||
action: OpType::Set(ScalarValue::Uint(123)),
|
||||
obj: ObjectId::Root,
|
||||
key: "somekey".into(),
|
||||
insert: false,
|
||||
|
@ -288,7 +278,7 @@ mod tests {
|
|||
"pred": []
|
||||
}),
|
||||
expected: Ok(Op {
|
||||
action: OpType::Put(ScalarValue::Int(-123)),
|
||||
action: OpType::Set(ScalarValue::Int(-123)),
|
||||
obj: ObjectId::Root,
|
||||
key: "somekey".into(),
|
||||
insert: false,
|
||||
|
@ -306,7 +296,7 @@ mod tests {
|
|||
"pred": []
|
||||
}),
|
||||
expected: Ok(Op {
|
||||
action: OpType::Put(ScalarValue::F64(-123.0)),
|
||||
action: OpType::Set(ScalarValue::F64(-123.0)),
|
||||
obj: ObjectId::Root,
|
||||
key: "somekey".into(),
|
||||
insert: false,
|
||||
|
@ -323,7 +313,7 @@ mod tests {
|
|||
"pred": []
|
||||
}),
|
||||
expected: Ok(Op {
|
||||
action: OpType::Put(ScalarValue::Str("somestring".into())),
|
||||
action: OpType::Set(ScalarValue::Str("somestring".into())),
|
||||
obj: ObjectId::Root,
|
||||
key: "somekey".into(),
|
||||
insert: false,
|
||||
|
@ -340,7 +330,7 @@ mod tests {
|
|||
"pred": []
|
||||
}),
|
||||
expected: Ok(Op {
|
||||
action: OpType::Put(ScalarValue::F64(1.23)),
|
||||
action: OpType::Set(ScalarValue::F64(1.23)),
|
||||
obj: ObjectId::Root,
|
||||
key: "somekey".into(),
|
||||
insert: false,
|
||||
|
@ -357,7 +347,7 @@ mod tests {
|
|||
"pred": []
|
||||
}),
|
||||
expected: Ok(Op {
|
||||
action: OpType::Put(ScalarValue::Boolean(true)),
|
||||
action: OpType::Set(ScalarValue::Boolean(true)),
|
||||
obj: ObjectId::Root,
|
||||
key: "somekey".into(),
|
||||
insert: false,
|
||||
|
@ -386,7 +376,7 @@ mod tests {
|
|||
"pred": []
|
||||
}),
|
||||
expected: Ok(Op {
|
||||
action: OpType::Put(ScalarValue::Counter(123.into())),
|
||||
action: OpType::Set(ScalarValue::Counter(123)),
|
||||
obj: ObjectId::Root,
|
||||
key: "somekey".into(),
|
||||
insert: false,
|
||||
|
@ -434,7 +424,7 @@ mod tests {
|
|||
"pred": []
|
||||
}),
|
||||
expected: Ok(Op {
|
||||
action: OpType::Increment(12),
|
||||
action: OpType::Inc(12),
|
||||
obj: ObjectId::Root,
|
||||
key: "somekey".into(),
|
||||
insert: false,
|
||||
|
@ -451,7 +441,7 @@ mod tests {
|
|||
"pred": []
|
||||
}),
|
||||
expected: Ok(Op {
|
||||
action: OpType::Increment(12),
|
||||
action: OpType::Inc(12),
|
||||
obj: ObjectId::Root,
|
||||
key: "somekey".into(),
|
||||
insert: false,
|
||||
|
@ -478,7 +468,7 @@ mod tests {
|
|||
"pred": []
|
||||
}),
|
||||
expected: Ok(Op {
|
||||
action: OpType::Put(ScalarValue::Null),
|
||||
action: OpType::Set(ScalarValue::Null),
|
||||
obj: ObjectId::Root,
|
||||
key: "somekey".into(),
|
||||
insert: false,
|
||||
|
@ -556,7 +546,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_serialize_key() {
|
||||
let map_key = Op {
|
||||
action: OpType::Increment(12),
|
||||
action: OpType::Inc(12),
|
||||
obj: ObjectId::Root,
|
||||
key: "somekey".into(),
|
||||
insert: false,
|
||||
|
@ -567,7 +557,7 @@ mod tests {
|
|||
assert_eq!(json.as_object().unwrap().get("key"), Some(&expected));
|
||||
|
||||
let elemid_key = Op {
|
||||
action: OpType::Increment(12),
|
||||
action: OpType::Inc(12),
|
||||
obj: ObjectId::Root,
|
||||
key: OpId::from_str("1@7ef48769b04d47e9a88e98a134d62716")
|
||||
.unwrap()
|
||||
|
@ -584,35 +574,35 @@ mod tests {
|
|||
fn test_round_trips() {
|
||||
let testcases = vec![
|
||||
Op {
|
||||
action: OpType::Put(ScalarValue::Uint(12)),
|
||||
action: OpType::Set(ScalarValue::Uint(12)),
|
||||
obj: ObjectId::Root,
|
||||
key: "somekey".into(),
|
||||
insert: false,
|
||||
pred: SortedVec::new(),
|
||||
},
|
||||
Op {
|
||||
action: OpType::Increment(12),
|
||||
action: OpType::Inc(12),
|
||||
obj: ObjectId::from_str("1@7ef48769b04d47e9a88e98a134d62716").unwrap(),
|
||||
key: "somekey".into(),
|
||||
insert: false,
|
||||
pred: SortedVec::new(),
|
||||
},
|
||||
Op {
|
||||
action: OpType::Put(ScalarValue::Uint(12)),
|
||||
action: OpType::Set(ScalarValue::Uint(12)),
|
||||
obj: ObjectId::from_str("1@7ef48769b04d47e9a88e98a134d62716").unwrap(),
|
||||
key: "somekey".into(),
|
||||
insert: false,
|
||||
pred: vec![OpId::from_str("1@7ef48769b04d47e9a88e98a134d62716").unwrap()].into(),
|
||||
},
|
||||
Op {
|
||||
action: OpType::Increment(12),
|
||||
action: OpType::Inc(12),
|
||||
obj: ObjectId::Root,
|
||||
key: "somekey".into(),
|
||||
insert: false,
|
||||
pred: SortedVec::new(),
|
||||
},
|
||||
Op {
|
||||
action: OpType::Put("seomthing".into()),
|
||||
action: OpType::Set("seomthing".into()),
|
||||
obj: ObjectId::from_str("1@7ef48769b04d47e9a88e98a134d62716").unwrap(),
|
||||
key: OpId::from_str("1@7ef48769b04d47e9a88e98a134d62716")
|
||||
.unwrap()
|
|
@ -15,9 +15,9 @@ impl Serialize for OpType {
|
|||
OpType::Make(ObjType::Table) => RawOpType::MakeTable,
|
||||
OpType::Make(ObjType::List) => RawOpType::MakeList,
|
||||
OpType::Make(ObjType::Text) => RawOpType::MakeText,
|
||||
OpType::Delete => RawOpType::Del,
|
||||
OpType::Increment(_) => RawOpType::Inc,
|
||||
OpType::Put(_) => RawOpType::Set,
|
||||
OpType::Del => RawOpType::Del,
|
||||
OpType::Inc(_) => RawOpType::Inc,
|
||||
OpType::Set(_) => RawOpType::Set,
|
||||
};
|
||||
raw_type.serialize(serializer)
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
use serde::{de, Deserialize, Deserializer};
|
||||
use smol_str::SmolStr;
|
||||
|
||||
use crate::types::ScalarValue;
|
||||
use crate::ScalarValue;
|
||||
|
||||
impl<'de> Deserialize<'de> for ScalarValue {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
|
@ -12,7 +12,7 @@ impl<'de> Deserialize<'de> for ScalarValue {
|
|||
impl<'de> de::Visitor<'de> for ValueVisitor {
|
||||
type Value = ScalarValue;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a number, string, bool, or null")
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
use std::{
|
||||
cmp::{Ordering, PartialOrd},
|
||||
convert::TryFrom,
|
||||
str::FromStr,
|
||||
};
|
||||
|
|
@ -2,3 +2,4 @@ mod element_id;
|
|||
mod key;
|
||||
mod object_id;
|
||||
mod opid;
|
||||
mod scalar_value;
|
|
@ -1,5 +1,6 @@
|
|||
use std::{
|
||||
cmp::{Ordering, PartialOrd},
|
||||
convert::TryFrom,
|
||||
fmt,
|
||||
str::FromStr,
|
||||
};
|
|
@ -1,6 +1,7 @@
|
|||
use core::fmt;
|
||||
use std::{
|
||||
cmp::{Ordering, PartialOrd},
|
||||
convert::TryFrom,
|
||||
str::FromStr,
|
||||
};
|
||||
|
57
automerge/src/legacy/utility_impls/scalar_value.rs
Normal file
57
automerge/src/legacy/utility_impls/scalar_value.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
use std::fmt;
|
||||
|
||||
use smol_str::SmolStr;
|
||||
|
||||
use crate::legacy::ScalarValue;
|
||||
|
||||
impl From<&str> for ScalarValue {
|
||||
fn from(s: &str) -> Self {
|
||||
ScalarValue::Str(s.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for ScalarValue {
|
||||
fn from(n: i64) -> Self {
|
||||
ScalarValue::Int(n)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for ScalarValue {
|
||||
fn from(n: u64) -> Self {
|
||||
ScalarValue::Uint(n)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for ScalarValue {
|
||||
fn from(n: i32) -> Self {
|
||||
ScalarValue::Int(n as i64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for ScalarValue {
|
||||
fn from(b: bool) -> Self {
|
||||
ScalarValue::Boolean(b)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<char> for ScalarValue {
|
||||
fn from(c: char) -> Self {
|
||||
ScalarValue::Str(SmolStr::new(c.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ScalarValue {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ScalarValue::Bytes(b) => write!(f, "\"{:?}\"", b),
|
||||
ScalarValue::Str(s) => write!(f, "\"{}\"", s),
|
||||
ScalarValue::Int(i) => write!(f, "{}", i),
|
||||
ScalarValue::Uint(i) => write!(f, "{}", i),
|
||||
ScalarValue::F64(n) => write!(f, "{:.324}", n),
|
||||
ScalarValue::Counter(c) => write!(f, "Counter: {}", c),
|
||||
ScalarValue::Timestamp(i) => write!(f, "Timestamp: {}", i),
|
||||
ScalarValue::Boolean(b) => write!(f, "{}", b),
|
||||
ScalarValue::Null => write!(f, "null"),
|
||||
}
|
||||
}
|
||||
}
|
1411
automerge/src/lib.rs
Normal file
1411
automerge/src/lib.rs
Normal file
File diff suppressed because it is too large
Load diff
175
automerge/src/op_set.rs
Normal file
175
automerge/src/op_set.rs
Normal file
|
@ -0,0 +1,175 @@
|
|||
use crate::op_tree::OpTreeInternal;
|
||||
use crate::query::TreeQuery;
|
||||
use crate::{ActorId, IndexedCache, Key, ObjId, Op, OpId};
|
||||
use fxhash::FxBuildHasher;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub(crate) type OpSet = OpSetInternal<16>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct OpSetInternal<const B: usize> {
|
||||
trees: HashMap<ObjId, OpTreeInternal<B>, FxBuildHasher>,
|
||||
objs: Vec<ObjId>,
|
||||
length: usize,
|
||||
pub m: OpSetMetadata,
|
||||
}
|
||||
|
||||
impl<const B: usize> OpSetInternal<B> {
|
||||
pub fn new() -> Self {
|
||||
OpSetInternal {
|
||||
trees: Default::default(),
|
||||
objs: Default::default(),
|
||||
length: 0,
|
||||
m: OpSetMetadata {
|
||||
actors: IndexedCache::new(),
|
||||
props: IndexedCache::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> Iter<'_, B> {
|
||||
Iter {
|
||||
inner: self,
|
||||
index: 0,
|
||||
sub_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn search<Q>(&self, obj: ObjId, query: Q) -> Q
|
||||
where
|
||||
Q: TreeQuery<B>,
|
||||
{
|
||||
if let Some(tree) = self.trees.get(&obj) {
|
||||
tree.search(query, &self.m)
|
||||
} else {
|
||||
query
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replace<F>(&mut self, obj: ObjId, index: usize, f: F) -> Option<Op>
|
||||
where
|
||||
F: FnMut(&mut Op),
|
||||
{
|
||||
if let Some(tree) = self.trees.get_mut(&obj) {
|
||||
tree.replace(index, f)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, obj: ObjId, index: usize) -> Op {
|
||||
let tree = self.trees.get_mut(&obj).unwrap();
|
||||
self.length -= 1;
|
||||
let op = tree.remove(index);
|
||||
if tree.is_empty() {
|
||||
self.trees.remove(&obj);
|
||||
}
|
||||
op
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.length
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, index: usize, element: Op) {
|
||||
let Self {
|
||||
ref mut trees,
|
||||
ref mut objs,
|
||||
ref mut m,
|
||||
..
|
||||
} = self;
|
||||
trees
|
||||
.entry(element.obj)
|
||||
.or_insert_with(|| {
|
||||
let pos = objs
|
||||
.binary_search_by(|probe| m.lamport_cmp(probe.0, element.obj.0))
|
||||
.unwrap_err();
|
||||
objs.insert(pos, element.obj);
|
||||
Default::default()
|
||||
})
|
||||
.insert(index, element);
|
||||
self.length += 1;
|
||||
}
|
||||
|
||||
#[cfg(feature = "optree-visualisation")]
|
||||
pub fn visualise(&self) -> String {
|
||||
let mut out = Vec::new();
|
||||
let graph = super::visualisation::GraphVisualisation::construct(&self.trees, &self.m);
|
||||
dot::render(&graph, &mut out).unwrap();
|
||||
String::from_utf8_lossy(&out[..]).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const B: usize> Default for OpSetInternal<B> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, const B: usize> IntoIterator for &'a OpSetInternal<B> {
|
||||
type Item = &'a Op;
|
||||
|
||||
type IntoIter = Iter<'a, B>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
Iter {
|
||||
inner: self,
|
||||
index: 0,
|
||||
sub_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Iter<'a, const B: usize> {
|
||||
inner: &'a OpSetInternal<B>,
|
||||
index: usize,
|
||||
sub_index: usize,
|
||||
}
|
||||
|
||||
impl<'a, const B: usize> Iterator for Iter<'a, B> {
|
||||
type Item = &'a Op;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let obj = self.inner.objs.get(self.index)?;
|
||||
let tree = self.inner.trees.get(obj)?;
|
||||
self.sub_index += 1;
|
||||
if let Some(op) = tree.get(self.sub_index - 1) {
|
||||
Some(op)
|
||||
} else {
|
||||
self.index += 1;
|
||||
self.sub_index = 1;
|
||||
// FIXME is it possible that a rolled back transaction could break the iterator by
|
||||
// having an empty tree?
|
||||
let obj = self.inner.objs.get(self.index)?;
|
||||
let tree = self.inner.trees.get(obj)?;
|
||||
tree.get(self.sub_index - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct OpSetMetadata {
|
||||
pub actors: IndexedCache<ActorId>,
|
||||
pub props: IndexedCache<String>,
|
||||
}
|
||||
|
||||
impl OpSetMetadata {
|
||||
pub fn key_cmp(&self, left: &Key, right: &Key) -> Ordering {
|
||||
match (left, right) {
|
||||
(Key::Map(a), Key::Map(b)) => self.props[*a].cmp(&self.props[*b]),
|
||||
_ => panic!("can only compare map keys"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lamport_cmp(&self, left: OpId, right: OpId) -> Ordering {
|
||||
match (left, right) {
|
||||
(OpId(0, _), OpId(0, _)) => Ordering::Equal,
|
||||
(OpId(0, _), OpId(_, _)) => Ordering::Less,
|
||||
(OpId(_, _), OpId(0, _)) => Ordering::Greater,
|
||||
// FIXME - this one seems backwards to me - why - is values() returning in the wrong order?
|
||||
(OpId(a, x), OpId(b, y)) if a == b => self.actors[y].cmp(&self.actors[x]),
|
||||
(OpId(a, _), OpId(b, _)) => a.cmp(&b),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,20 +5,170 @@ use std::{
|
|||
};
|
||||
|
||||
pub(crate) use crate::op_set::OpSetMetadata;
|
||||
use crate::query::{ChangeVisibility, Index, QueryResult, TreeQuery};
|
||||
use crate::types::Op;
|
||||
pub(crate) const B: usize = 16;
|
||||
use crate::query::{Index, QueryResult, TreeQuery};
|
||||
use crate::{Op, OpId};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) type OpTree = OpTreeInternal<16>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct OpTreeNode {
|
||||
pub(crate) children: Vec<OpTreeNode>,
|
||||
pub(crate) elements: Vec<usize>,
|
||||
pub(crate) index: Index,
|
||||
pub(crate) length: usize,
|
||||
pub(crate) struct OpTreeInternal<const B: usize> {
|
||||
pub(crate) root_node: Option<OpTreeNode<B>>,
|
||||
}
|
||||
|
||||
impl OpTreeNode {
|
||||
pub(crate) fn new() -> Self {
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct OpTreeNode<const B: usize> {
|
||||
pub(crate) elements: Vec<Op>,
|
||||
pub(crate) children: Vec<OpTreeNode<B>>,
|
||||
pub index: Index,
|
||||
length: usize,
|
||||
}
|
||||
|
||||
impl<const B: usize> OpTreeInternal<B> {
|
||||
/// Construct a new, empty, sequence.
|
||||
pub fn new() -> Self {
|
||||
Self { root_node: None }
|
||||
}
|
||||
|
||||
/// Get the length of the sequence.
|
||||
pub fn len(&self) -> usize {
|
||||
self.root_node.as_ref().map_or(0, |n| n.len())
|
||||
}
|
||||
|
||||
pub fn search<Q>(&self, mut query: Q, m: &OpSetMetadata) -> Q
|
||||
where
|
||||
Q: TreeQuery<B>,
|
||||
{
|
||||
self.root_node
|
||||
.as_ref()
|
||||
.map(|root| match query.query_node_with_metadata(root, m) {
|
||||
QueryResult::Decend => root.search(&mut query, m),
|
||||
_ => true,
|
||||
});
|
||||
query
|
||||
}
|
||||
|
||||
/// Check if the sequence is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Create an iterator through the sequence.
|
||||
pub fn iter(&self) -> Iter<'_, B> {
|
||||
Iter {
|
||||
inner: self,
|
||||
index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert the `element` into the sequence at `index`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `index > len`.
|
||||
pub fn insert(&mut self, index: usize, element: Op) {
|
||||
let old_len = self.len();
|
||||
if let Some(root) = self.root_node.as_mut() {
|
||||
#[cfg(debug_assertions)]
|
||||
root.check();
|
||||
|
||||
if root.is_full() {
|
||||
let original_len = root.len();
|
||||
let new_root = OpTreeNode::new();
|
||||
|
||||
// move new_root to root position
|
||||
let old_root = mem::replace(root, new_root);
|
||||
|
||||
root.length += old_root.len();
|
||||
root.index = old_root.index.clone();
|
||||
root.children.push(old_root);
|
||||
root.split_child(0);
|
||||
|
||||
assert_eq!(original_len, root.len());
|
||||
|
||||
// after splitting the root has one element and two children, find which child the
|
||||
// index is in
|
||||
let first_child_len = root.children[0].len();
|
||||
let (child, insertion_index) = if first_child_len < index {
|
||||
(&mut root.children[1], index - (first_child_len + 1))
|
||||
} else {
|
||||
(&mut root.children[0], index)
|
||||
};
|
||||
root.length += 1;
|
||||
root.index.insert(&element);
|
||||
child.insert_into_non_full_node(insertion_index, element)
|
||||
} else {
|
||||
root.insert_into_non_full_node(index, element)
|
||||
}
|
||||
} else {
|
||||
let mut root = OpTreeNode::new();
|
||||
root.insert_into_non_full_node(index, element);
|
||||
self.root_node = Some(root)
|
||||
}
|
||||
assert_eq!(self.len(), old_len + 1, "{:#?}", self);
|
||||
}
|
||||
|
||||
/// Get the `element` at `index` in the sequence.
|
||||
pub fn get(&self, index: usize) -> Option<&Op> {
|
||||
self.root_node.as_ref().and_then(|n| n.get(index))
|
||||
}
|
||||
|
||||
// this replaces get_mut() because it allows the indexes to update correctly
|
||||
pub fn replace<F>(&mut self, index: usize, mut f: F) -> Option<Op>
|
||||
where
|
||||
F: FnMut(&mut Op),
|
||||
{
|
||||
if self.len() > index {
|
||||
let op = self.get(index).unwrap().clone();
|
||||
let mut new_op = op.clone();
|
||||
f(&mut new_op);
|
||||
self.set(index, new_op);
|
||||
Some(op)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the element at `index` from the sequence.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `index` is out of bounds.
|
||||
pub fn remove(&mut self, index: usize) -> Op {
|
||||
if let Some(root) = self.root_node.as_mut() {
|
||||
#[cfg(debug_assertions)]
|
||||
let len = root.check();
|
||||
let old = root.remove(index);
|
||||
|
||||
if root.elements.is_empty() {
|
||||
if root.is_leaf() {
|
||||
self.root_node = None;
|
||||
} else {
|
||||
self.root_node = Some(root.children.remove(0));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
debug_assert_eq!(len, self.root_node.as_ref().map_or(0, |r| r.check()) + 1);
|
||||
old
|
||||
} else {
|
||||
panic!("remove from empty tree")
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the `element` at `index` in the sequence, returning the old value.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `index > len`
|
||||
pub fn set(&mut self, index: usize, element: Op) -> Op {
|
||||
self.root_node.as_mut().unwrap().set(index, element)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const B: usize> OpTreeNode<B> {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
elements: Vec::new(),
|
||||
children: Vec::new(),
|
||||
|
@ -27,77 +177,31 @@ impl OpTreeNode {
|
|||
}
|
||||
}
|
||||
|
||||
fn search_element<'a, 'b: 'a, Q>(
|
||||
&'b self,
|
||||
query: &mut Q,
|
||||
m: &OpSetMetadata,
|
||||
ops: &'a [Op],
|
||||
index: usize,
|
||||
) -> bool
|
||||
pub fn search<Q>(&self, query: &mut Q, m: &OpSetMetadata) -> bool
|
||||
where
|
||||
Q: TreeQuery<'a>,
|
||||
{
|
||||
if let Some(e) = self.elements.get(index) {
|
||||
if query.query_element_with_metadata(&ops[*e], m) == QueryResult::Finish {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) fn search<'a, 'b: 'a, Q>(
|
||||
&'b self,
|
||||
query: &mut Q,
|
||||
m: &OpSetMetadata,
|
||||
ops: &'a [Op],
|
||||
mut skip: Option<usize>,
|
||||
) -> bool
|
||||
where
|
||||
Q: TreeQuery<'a>,
|
||||
Q: TreeQuery<B>,
|
||||
{
|
||||
if self.is_leaf() {
|
||||
for e in self.elements.iter().skip(skip.unwrap_or(0)) {
|
||||
if query.query_element_with_metadata(&ops[*e], m) == QueryResult::Finish {
|
||||
for e in &self.elements {
|
||||
if query.query_element_with_metadata(e, m) == QueryResult::Finish {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
} else {
|
||||
for (child_index, child) in self.children.iter().enumerate() {
|
||||
match skip {
|
||||
Some(n) if n > child.len() => {
|
||||
skip = Some(n - child.len() - 1);
|
||||
}
|
||||
Some(n) if n == child.len() => {
|
||||
skip = Some(0); // important to not be None so we never call query_node again
|
||||
if self.search_element(query, m, ops, child_index) {
|
||||
match query.query_node_with_metadata(child, m) {
|
||||
QueryResult::Decend => {
|
||||
if child.search(query, m) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Some(n) => {
|
||||
if child.search(query, m, ops, Some(n)) {
|
||||
return true;
|
||||
}
|
||||
skip = Some(0); // important to not be None so we never call query_node again
|
||||
if self.search_element(query, m, ops, child_index) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// descend and try find it
|
||||
match query.query_node_with_metadata(child, m, ops) {
|
||||
QueryResult::Descend => {
|
||||
if child.search(query, m, ops, None) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
QueryResult::Finish => return true,
|
||||
QueryResult::Next => (),
|
||||
QueryResult::Skip(_) => panic!("had skip from non-root node"),
|
||||
}
|
||||
if self.search_element(query, m, ops, child_index) {
|
||||
return true;
|
||||
}
|
||||
QueryResult::Finish => return true,
|
||||
QueryResult::Next => (),
|
||||
}
|
||||
if let Some(e) = self.elements.get(child_index) {
|
||||
if query.query_element_with_metadata(e, m) == QueryResult::Finish {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -105,26 +209,26 @@ impl OpTreeNode {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
pub fn len(&self) -> usize {
|
||||
self.length
|
||||
}
|
||||
|
||||
fn reindex(&mut self, ops: &[Op]) {
|
||||
fn reindex(&mut self) {
|
||||
let mut index = Index::new();
|
||||
for c in &self.children {
|
||||
index.merge(&c.index);
|
||||
}
|
||||
for i in &self.elements {
|
||||
index.insert(&ops[*i]);
|
||||
for e in &self.elements {
|
||||
index.insert(e);
|
||||
}
|
||||
self.index = index
|
||||
}
|
||||
|
||||
pub(crate) fn is_leaf(&self) -> bool {
|
||||
fn is_leaf(&self) -> bool {
|
||||
self.children.is_empty()
|
||||
}
|
||||
|
||||
pub(crate) fn is_full(&self) -> bool {
|
||||
fn is_full(&self) -> bool {
|
||||
self.elements.len() >= 2 * B - 1
|
||||
}
|
||||
|
||||
|
@ -139,13 +243,13 @@ impl OpTreeNode {
|
|||
cumulative_len += child.len() + 1;
|
||||
}
|
||||
}
|
||||
panic!("index {} not found in node with len {}", index, self.len())
|
||||
panic!("index not found in node")
|
||||
}
|
||||
|
||||
pub(crate) fn insert_into_non_full_node(&mut self, index: usize, element: usize, ops: &[Op]) {
|
||||
fn insert_into_non_full_node(&mut self, index: usize, element: Op) {
|
||||
assert!(!self.is_full());
|
||||
|
||||
self.index.insert(&ops[element]);
|
||||
self.index.insert(&element);
|
||||
|
||||
if self.is_leaf() {
|
||||
self.length += 1;
|
||||
|
@ -155,14 +259,14 @@ impl OpTreeNode {
|
|||
let child = &mut self.children[child_index];
|
||||
|
||||
if child.is_full() {
|
||||
self.split_child(child_index, ops);
|
||||
self.split_child(child_index);
|
||||
|
||||
// child structure has changed so we need to find the index again
|
||||
let (child_index, sub_index) = self.find_child_index(index);
|
||||
let child = &mut self.children[child_index];
|
||||
child.insert_into_non_full_node(sub_index, element, ops);
|
||||
child.insert_into_non_full_node(sub_index, element);
|
||||
} else {
|
||||
child.insert_into_non_full_node(sub_index, element, ops);
|
||||
child.insert_into_non_full_node(sub_index, element);
|
||||
}
|
||||
self.length += 1;
|
||||
}
|
||||
|
@ -170,7 +274,7 @@ impl OpTreeNode {
|
|||
|
||||
// A utility function to split the child `full_child_index` of this node
|
||||
// Note that `full_child_index` must be full when this function is called.
|
||||
pub(crate) fn split_child(&mut self, full_child_index: usize, ops: &[Op]) {
|
||||
fn split_child(&mut self, full_child_index: usize) {
|
||||
let original_len_self = self.len();
|
||||
|
||||
let full_child = &mut self.children[full_child_index];
|
||||
|
@ -204,8 +308,8 @@ impl OpTreeNode {
|
|||
|
||||
let full_child_len = full_child.len();
|
||||
|
||||
full_child.reindex(ops);
|
||||
successor_sibling.reindex(ops);
|
||||
full_child.reindex();
|
||||
successor_sibling.reindex();
|
||||
|
||||
self.children
|
||||
.insert(full_child_index + 1, successor_sibling);
|
||||
|
@ -217,37 +321,32 @@ impl OpTreeNode {
|
|||
assert_eq!(original_len_self, self.len());
|
||||
}
|
||||
|
||||
fn remove_from_leaf(&mut self, index: usize) -> usize {
|
||||
fn remove_from_leaf(&mut self, index: usize) -> Op {
|
||||
self.length -= 1;
|
||||
self.elements.remove(index)
|
||||
}
|
||||
|
||||
fn remove_element_from_non_leaf(
|
||||
&mut self,
|
||||
index: usize,
|
||||
element_index: usize,
|
||||
ops: &[Op],
|
||||
) -> usize {
|
||||
fn remove_element_from_non_leaf(&mut self, index: usize, element_index: usize) -> Op {
|
||||
self.length -= 1;
|
||||
if self.children[element_index].elements.len() >= B {
|
||||
let total_index = self.cumulative_index(element_index);
|
||||
// recursively delete index - 1 in predecessor_node
|
||||
let predecessor = self.children[element_index].remove(index - 1 - total_index, ops);
|
||||
let predecessor = self.children[element_index].remove(index - 1 - total_index);
|
||||
// replace element with that one
|
||||
mem::replace(&mut self.elements[element_index], predecessor)
|
||||
} else if self.children[element_index + 1].elements.len() >= B {
|
||||
// recursively delete index + 1 in successor_node
|
||||
let total_index = self.cumulative_index(element_index + 1);
|
||||
let successor = self.children[element_index + 1].remove(index + 1 - total_index, ops);
|
||||
let successor = self.children[element_index + 1].remove(index + 1 - total_index);
|
||||
// replace element with that one
|
||||
mem::replace(&mut self.elements[element_index], successor)
|
||||
} else {
|
||||
let middle_element = self.elements.remove(element_index);
|
||||
let successor_child = self.children.remove(element_index + 1);
|
||||
self.children[element_index].merge(middle_element, successor_child, ops);
|
||||
self.children[element_index].merge(middle_element, successor_child);
|
||||
|
||||
let total_index = self.cumulative_index(element_index);
|
||||
self.children[element_index].remove(index - total_index, ops)
|
||||
self.children[element_index].remove(index - total_index)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -258,12 +357,7 @@ impl OpTreeNode {
|
|||
.sum()
|
||||
}
|
||||
|
||||
fn remove_from_internal_child(
|
||||
&mut self,
|
||||
index: usize,
|
||||
mut child_index: usize,
|
||||
ops: &[Op],
|
||||
) -> usize {
|
||||
fn remove_from_internal_child(&mut self, index: usize, mut child_index: usize) -> Op {
|
||||
if self.children[child_index].elements.len() < B
|
||||
&& if child_index > 0 {
|
||||
self.children[child_index - 1].elements.len() < B
|
||||
|
@ -287,14 +381,14 @@ impl OpTreeNode {
|
|||
let successor = self.children.remove(child_index);
|
||||
child_index -= 1;
|
||||
|
||||
self.children[child_index].merge(middle, successor, ops);
|
||||
self.children[child_index].merge(middle, successor);
|
||||
} else {
|
||||
let middle = self.elements.remove(child_index);
|
||||
|
||||
// use the sucessor sibling
|
||||
let successor = self.children.remove(child_index + 1);
|
||||
|
||||
self.children[child_index].merge(middle, successor, ops);
|
||||
self.children[child_index].merge(middle, successor);
|
||||
}
|
||||
} else if self.children[child_index].elements.len() < B {
|
||||
if child_index > 0
|
||||
|
@ -306,16 +400,12 @@ impl OpTreeNode {
|
|||
let last_element = self.children[child_index - 1].elements.pop().unwrap();
|
||||
assert!(!self.children[child_index - 1].elements.is_empty());
|
||||
self.children[child_index - 1].length -= 1;
|
||||
self.children[child_index - 1]
|
||||
.index
|
||||
.remove(&ops[last_element]);
|
||||
self.children[child_index - 1].index.remove(&last_element);
|
||||
|
||||
let parent_element =
|
||||
mem::replace(&mut self.elements[child_index - 1], last_element);
|
||||
|
||||
self.children[child_index]
|
||||
.index
|
||||
.insert(&ops[parent_element]);
|
||||
self.children[child_index].index.insert(&parent_element);
|
||||
self.children[child_index]
|
||||
.elements
|
||||
.insert(0, parent_element);
|
||||
|
@ -323,10 +413,10 @@ impl OpTreeNode {
|
|||
|
||||
if let Some(last_child) = self.children[child_index - 1].children.pop() {
|
||||
self.children[child_index - 1].length -= last_child.len();
|
||||
self.children[child_index - 1].reindex(ops);
|
||||
self.children[child_index - 1].reindex();
|
||||
self.children[child_index].length += last_child.len();
|
||||
self.children[child_index].children.insert(0, last_child);
|
||||
self.children[child_index].reindex(ops);
|
||||
self.children[child_index].reindex();
|
||||
}
|
||||
} else if self
|
||||
.children
|
||||
|
@ -334,9 +424,7 @@ impl OpTreeNode {
|
|||
.map_or(false, |c| c.elements.len() >= B)
|
||||
{
|
||||
let first_element = self.children[child_index + 1].elements.remove(0);
|
||||
self.children[child_index + 1]
|
||||
.index
|
||||
.remove(&ops[first_element]);
|
||||
self.children[child_index + 1].index.remove(&first_element);
|
||||
self.children[child_index + 1].length -= 1;
|
||||
|
||||
assert!(!self.children[child_index + 1].elements.is_empty());
|
||||
|
@ -344,39 +432,37 @@ impl OpTreeNode {
|
|||
let parent_element = mem::replace(&mut self.elements[child_index], first_element);
|
||||
|
||||
self.children[child_index].length += 1;
|
||||
self.children[child_index]
|
||||
.index
|
||||
.insert(&ops[parent_element]);
|
||||
self.children[child_index].index.insert(&parent_element);
|
||||
self.children[child_index].elements.push(parent_element);
|
||||
|
||||
if !self.children[child_index + 1].is_leaf() {
|
||||
let first_child = self.children[child_index + 1].children.remove(0);
|
||||
self.children[child_index + 1].length -= first_child.len();
|
||||
self.children[child_index + 1].reindex(ops);
|
||||
self.children[child_index + 1].reindex();
|
||||
self.children[child_index].length += first_child.len();
|
||||
|
||||
self.children[child_index].children.push(first_child);
|
||||
self.children[child_index].reindex(ops);
|
||||
self.children[child_index].reindex();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.length -= 1;
|
||||
let total_index = self.cumulative_index(child_index);
|
||||
self.children[child_index].remove(index - total_index, ops)
|
||||
self.children[child_index].remove(index - total_index)
|
||||
}
|
||||
|
||||
pub(crate) fn check(&self) -> usize {
|
||||
fn check(&self) -> usize {
|
||||
let l = self.elements.len() + self.children.iter().map(|c| c.check()).sum::<usize>();
|
||||
assert_eq!(self.len(), l, "{:#?}", self);
|
||||
|
||||
l
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&mut self, index: usize, ops: &[Op]) -> usize {
|
||||
pub fn remove(&mut self, index: usize) -> Op {
|
||||
let original_len = self.len();
|
||||
if self.is_leaf() {
|
||||
let v = self.remove_from_leaf(index);
|
||||
self.index.remove(&ops[v]);
|
||||
self.index.remove(&v);
|
||||
assert_eq!(original_len, self.len() + 1);
|
||||
debug_assert_eq!(self.check(), self.len());
|
||||
v
|
||||
|
@ -393,16 +479,15 @@ impl OpTreeNode {
|
|||
let v = self.remove_element_from_non_leaf(
|
||||
index,
|
||||
min(child_index, self.elements.len() - 1),
|
||||
ops,
|
||||
);
|
||||
self.index.remove(&ops[v]);
|
||||
self.index.remove(&v);
|
||||
assert_eq!(original_len, self.len() + 1);
|
||||
debug_assert_eq!(self.check(), self.len());
|
||||
return v;
|
||||
}
|
||||
Ordering::Greater => {
|
||||
let v = self.remove_from_internal_child(index, child_index, ops);
|
||||
self.index.remove(&ops[v]);
|
||||
let v = self.remove_from_internal_child(index, child_index);
|
||||
self.index.remove(&v);
|
||||
assert_eq!(original_len, self.len() + 1);
|
||||
debug_assert_eq!(self.check(), self.len());
|
||||
return v;
|
||||
|
@ -419,8 +504,8 @@ impl OpTreeNode {
|
|||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, middle: usize, successor_sibling: OpTreeNode, ops: &[Op]) {
|
||||
self.index.insert(&ops[middle]);
|
||||
fn merge(&mut self, middle: Op, successor_sibling: OpTreeNode<B>) {
|
||||
self.index.insert(&middle);
|
||||
self.index.merge(&successor_sibling.index);
|
||||
self.elements.push(middle);
|
||||
self.elements.extend(successor_sibling.elements);
|
||||
|
@ -429,50 +514,47 @@ impl OpTreeNode {
|
|||
assert!(self.is_full());
|
||||
}
|
||||
|
||||
/// Update the operation at the given index using the provided function.
|
||||
///
|
||||
/// This handles updating the indices after the update.
|
||||
pub(crate) fn update<'a>(
|
||||
&mut self,
|
||||
index: usize,
|
||||
vis: ChangeVisibility<'a>,
|
||||
) -> ChangeVisibility<'a> {
|
||||
pub fn set(&mut self, index: usize, element: Op) -> Op {
|
||||
if self.is_leaf() {
|
||||
self.index.change_vis(vis)
|
||||
let old_element = self.elements.get_mut(index).unwrap();
|
||||
self.index.replace(old_element, &element);
|
||||
mem::replace(old_element, element)
|
||||
} else {
|
||||
let mut cumulative_len = 0;
|
||||
let len = self.len();
|
||||
for (_child_index, child) in self.children.iter_mut().enumerate() {
|
||||
for (child_index, child) in self.children.iter_mut().enumerate() {
|
||||
match (cumulative_len + child.len()).cmp(&index) {
|
||||
Ordering::Less => {
|
||||
cumulative_len += child.len() + 1;
|
||||
}
|
||||
Ordering::Equal => {
|
||||
return self.index.change_vis(vis);
|
||||
let old_element = self.elements.get_mut(child_index).unwrap();
|
||||
self.index.replace(old_element, &element);
|
||||
return mem::replace(old_element, element);
|
||||
}
|
||||
Ordering::Greater => {
|
||||
let vis = child.update(index - cumulative_len, vis);
|
||||
return self.index.change_vis(vis);
|
||||
let old_element = child.set(index - cumulative_len, element.clone());
|
||||
self.index.replace(&old_element, &element);
|
||||
return old_element;
|
||||
}
|
||||
}
|
||||
}
|
||||
panic!("Invalid index to set: {} but len was {}", index, len)
|
||||
panic!("Invalid index to set: {} but len was {}", index, self.len())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn last(&self) -> usize {
|
||||
pub fn last(&self) -> &Op {
|
||||
if self.is_leaf() {
|
||||
// node is never empty so this is safe
|
||||
*self.elements.last().unwrap()
|
||||
self.elements.last().unwrap()
|
||||
} else {
|
||||
// if not a leaf then there is always at least one child
|
||||
self.children.last().unwrap().last()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get(&self, index: usize) -> Option<usize> {
|
||||
pub fn get(&self, index: usize) -> Option<&Op> {
|
||||
if self.is_leaf() {
|
||||
return self.elements.get(index).copied();
|
||||
return self.elements.get(index);
|
||||
} else {
|
||||
let mut cumulative_len = 0;
|
||||
for (child_index, child) in self.children.iter().enumerate() {
|
||||
|
@ -480,7 +562,7 @@ impl OpTreeNode {
|
|||
Ordering::Less => {
|
||||
cumulative_len += child.len() + 1;
|
||||
}
|
||||
Ordering::Equal => return self.elements.get(child_index).copied(),
|
||||
Ordering::Equal => return self.elements.get(child_index),
|
||||
Ordering::Greater => {
|
||||
return child.get(index - cumulative_len);
|
||||
}
|
||||
|
@ -490,3 +572,112 @@ impl OpTreeNode {
|
|||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<const B: usize> Default for OpTreeInternal<B> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const B: usize> PartialEq for OpTreeInternal<B> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.len() == other.len() && self.iter().zip(other.iter()).all(|(a, b)| a == b)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, const B: usize> IntoIterator for &'a OpTreeInternal<B> {
|
||||
type Item = &'a Op;
|
||||
|
||||
type IntoIter = Iter<'a, B>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
Iter {
|
||||
inner: self,
|
||||
index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Iter<'a, const B: usize> {
|
||||
inner: &'a OpTreeInternal<B>,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl<'a, const B: usize> Iterator for Iter<'a, B> {
|
||||
type Item = &'a Op;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.index += 1;
|
||||
self.inner.get(self.index - 1)
|
||||
}
|
||||
|
||||
fn nth(&mut self, n: usize) -> Option<Self::Item> {
|
||||
self.index += n + 1;
|
||||
self.inner.get(self.index - 1)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct CounterData {
|
||||
pos: usize,
|
||||
val: i64,
|
||||
succ: HashSet<OpId>,
|
||||
op: Op,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::legacy as amp;
|
||||
use crate::{Op, OpId};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn op(n: usize) -> Op {
|
||||
let zero = OpId(0, 0);
|
||||
Op {
|
||||
change: n,
|
||||
id: zero,
|
||||
action: amp::OpType::Set(0.into()),
|
||||
obj: zero.into(),
|
||||
key: zero.into(),
|
||||
succ: vec![],
|
||||
pred: vec![],
|
||||
insert: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert() {
|
||||
let mut t = OpTree::new();
|
||||
|
||||
t.insert(0, op(1));
|
||||
t.insert(1, op(1));
|
||||
t.insert(0, op(1));
|
||||
t.insert(0, op(1));
|
||||
t.insert(0, op(1));
|
||||
t.insert(3, op(1));
|
||||
t.insert(4, op(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_book() {
|
||||
let mut t = OpTree::new();
|
||||
|
||||
for i in 0..100 {
|
||||
t.insert(i % 2, op(i));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_book_vec() {
|
||||
let mut t = OpTree::new();
|
||||
let mut v = Vec::new();
|
||||
|
||||
for i in 0..100 {
|
||||
t.insert(i % 3, op(i));
|
||||
v.insert(i % 3, op(i));
|
||||
|
||||
assert_eq!(v, t.iter().cloned().collect::<Vec<_>>())
|
||||
}
|
||||
}
|
||||
}
|
361
automerge/src/query.rs
Normal file
361
automerge/src/query.rs
Normal file
|
@ -0,0 +1,361 @@
|
|||
use crate::op_tree::{OpSetMetadata, OpTreeNode};
|
||||
use crate::{Clock, ElemId, Op, OpId, OpType, ScalarValue};
|
||||
use fxhash::FxBuildHasher;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::Debug;
|
||||
|
||||
mod insert;
|
||||
mod keys;
|
||||
mod keys_at;
|
||||
mod len;
|
||||
mod len_at;
|
||||
mod list_vals;
|
||||
mod list_vals_at;
|
||||
mod nth;
|
||||
mod nth_at;
|
||||
mod prop;
|
||||
mod prop_at;
|
||||
mod seek_op;
|
||||
|
||||
pub(crate) use insert::InsertNth;
|
||||
pub(crate) use keys::Keys;
|
||||
pub(crate) use keys_at::KeysAt;
|
||||
pub(crate) use len::Len;
|
||||
pub(crate) use len_at::LenAt;
|
||||
pub(crate) use list_vals::ListVals;
|
||||
pub(crate) use list_vals_at::ListValsAt;
|
||||
pub(crate) use nth::Nth;
|
||||
pub(crate) use nth_at::NthAt;
|
||||
pub(crate) use prop::Prop;
|
||||
pub(crate) use prop_at::PropAt;
|
||||
pub(crate) use seek_op::SeekOp;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct CounterData {
|
||||
pos: usize,
|
||||
val: i64,
|
||||
succ: HashSet<OpId>,
|
||||
op: Op,
|
||||
}
|
||||
|
||||
pub(crate) trait TreeQuery<const B: usize> {
|
||||
#[inline(always)]
|
||||
fn query_node_with_metadata(
|
||||
&mut self,
|
||||
child: &OpTreeNode<B>,
|
||||
_m: &OpSetMetadata,
|
||||
) -> QueryResult {
|
||||
self.query_node(child)
|
||||
}
|
||||
|
||||
fn query_node(&mut self, _child: &OpTreeNode<B>) -> QueryResult {
|
||||
QueryResult::Decend
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn query_element_with_metadata(&mut self, element: &Op, _m: &OpSetMetadata) -> QueryResult {
|
||||
self.query_element(element)
|
||||
}
|
||||
|
||||
fn query_element(&mut self, _element: &Op) -> QueryResult {
|
||||
panic!("invalid element query")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum QueryResult {
|
||||
Next,
|
||||
Decend,
|
||||
Finish,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct Index {
|
||||
pub len: usize,
|
||||
pub visible: HashMap<ElemId, usize, FxBuildHasher>,
|
||||
pub ops: HashSet<OpId, FxBuildHasher>,
|
||||
}
|
||||
|
||||
impl Index {
|
||||
pub fn new() -> Self {
|
||||
Index {
|
||||
len: 0,
|
||||
visible: Default::default(),
|
||||
ops: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has(&self, e: &Option<ElemId>) -> bool {
|
||||
if let Some(seen) = e {
|
||||
self.visible.contains_key(seen)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replace(&mut self, old: &Op, new: &Op) {
|
||||
if old.id != new.id {
|
||||
self.ops.remove(&old.id);
|
||||
self.ops.insert(new.id);
|
||||
}
|
||||
|
||||
assert!(new.key == old.key);
|
||||
|
||||
match (new.succ.is_empty(), old.succ.is_empty(), new.elemid()) {
|
||||
(false, true, Some(elem)) => match self.visible.get(&elem).copied() {
|
||||
Some(n) if n == 1 => {
|
||||
self.len -= 1;
|
||||
self.visible.remove(&elem);
|
||||
}
|
||||
Some(n) => {
|
||||
self.visible.insert(elem, n - 1);
|
||||
}
|
||||
None => panic!("remove overun in index"),
|
||||
},
|
||||
(true, false, Some(elem)) => match self.visible.get(&elem).copied() {
|
||||
Some(n) => {
|
||||
self.visible.insert(elem, n + 1);
|
||||
}
|
||||
None => {
|
||||
self.len += 1;
|
||||
self.visible.insert(elem, 1);
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, op: &Op) {
|
||||
self.ops.insert(op.id);
|
||||
if op.succ.is_empty() {
|
||||
if let Some(elem) = op.elemid() {
|
||||
match self.visible.get(&elem).copied() {
|
||||
Some(n) => {
|
||||
self.visible.insert(elem, n + 1);
|
||||
}
|
||||
None => {
|
||||
self.len += 1;
|
||||
self.visible.insert(elem, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, op: &Op) {
|
||||
self.ops.remove(&op.id);
|
||||
if op.succ.is_empty() {
|
||||
if let Some(elem) = op.elemid() {
|
||||
match self.visible.get(&elem).copied() {
|
||||
Some(n) if n == 1 => {
|
||||
self.len -= 1;
|
||||
self.visible.remove(&elem);
|
||||
}
|
||||
Some(n) => {
|
||||
self.visible.insert(elem, n - 1);
|
||||
}
|
||||
None => panic!("remove overun in index"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, other: &Index) {
|
||||
for id in &other.ops {
|
||||
self.ops.insert(*id);
|
||||
}
|
||||
for (elem, n) in other.visible.iter() {
|
||||
match self.visible.get(elem).cloned() {
|
||||
None => {
|
||||
self.visible.insert(*elem, 1);
|
||||
self.len += 1;
|
||||
}
|
||||
Some(m) => {
|
||||
self.visible.insert(*elem, m + n);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Index {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub(crate) struct VisWindow {
|
||||
counters: HashMap<OpId, CounterData>,
|
||||
}
|
||||
|
||||
impl VisWindow {
|
||||
fn visible(&mut self, op: &Op, pos: usize) -> bool {
|
||||
let mut visible = false;
|
||||
match op.action {
|
||||
OpType::Set(ScalarValue::Counter(val)) => {
|
||||
self.counters.insert(
|
||||
op.id,
|
||||
CounterData {
|
||||
pos,
|
||||
val,
|
||||
succ: op.succ.iter().cloned().collect(),
|
||||
op: op.clone(),
|
||||
},
|
||||
);
|
||||
if op.succ.is_empty() {
|
||||
visible = true;
|
||||
}
|
||||
}
|
||||
OpType::Inc(inc_val) => {
|
||||
for id in &op.pred {
|
||||
if let Some(mut entry) = self.counters.get_mut(id) {
|
||||
entry.succ.remove(&op.id);
|
||||
entry.val += inc_val;
|
||||
entry.op.action = OpType::Set(ScalarValue::Counter(entry.val));
|
||||
if entry.succ.is_empty() {
|
||||
visible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if op.succ.is_empty() {
|
||||
visible = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
visible
|
||||
}
|
||||
|
||||
fn visible_at(&mut self, op: &Op, pos: usize, clock: &Clock) -> bool {
|
||||
if !clock.covers(&op.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut visible = false;
|
||||
match op.action {
|
||||
OpType::Set(ScalarValue::Counter(val)) => {
|
||||
self.counters.insert(
|
||||
op.id,
|
||||
CounterData {
|
||||
pos,
|
||||
val,
|
||||
succ: op.succ.iter().cloned().collect(),
|
||||
op: op.clone(),
|
||||
},
|
||||
);
|
||||
if !op.succ.iter().any(|i| clock.covers(i)) {
|
||||
visible = true;
|
||||
}
|
||||
}
|
||||
OpType::Inc(inc_val) => {
|
||||
for id in &op.pred {
|
||||
// pred is always before op.id so we can see them
|
||||
if let Some(mut entry) = self.counters.get_mut(id) {
|
||||
entry.succ.remove(&op.id);
|
||||
entry.val += inc_val;
|
||||
entry.op.action = OpType::Set(ScalarValue::Counter(entry.val));
|
||||
if !entry.succ.iter().any(|i| clock.covers(i)) {
|
||||
visible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if !op.succ.iter().any(|i| clock.covers(i)) {
|
||||
visible = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
visible
|
||||
}
|
||||
|
||||
pub fn seen_op(&self, op: &Op, pos: usize) -> Vec<(usize, Op)> {
|
||||
let mut result = vec![];
|
||||
for pred in &op.pred {
|
||||
if let Some(entry) = self.counters.get(pred) {
|
||||
result.push((entry.pos, entry.op.clone()));
|
||||
}
|
||||
}
|
||||
if result.is_empty() {
|
||||
vec![(pos, op.clone())]
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_visible(op: &Op, pos: usize, counters: &mut HashMap<OpId, CounterData>) -> bool {
|
||||
let mut visible = false;
|
||||
match op.action {
|
||||
OpType::Set(ScalarValue::Counter(val)) => {
|
||||
counters.insert(
|
||||
op.id,
|
||||
CounterData {
|
||||
pos,
|
||||
val,
|
||||
succ: op.succ.iter().cloned().collect(),
|
||||
op: op.clone(),
|
||||
},
|
||||
);
|
||||
if op.succ.is_empty() {
|
||||
visible = true;
|
||||
}
|
||||
}
|
||||
OpType::Inc(inc_val) => {
|
||||
for id in &op.pred {
|
||||
if let Some(mut entry) = counters.get_mut(id) {
|
||||
entry.succ.remove(&op.id);
|
||||
entry.val += inc_val;
|
||||
entry.op.action = OpType::Set(ScalarValue::Counter(entry.val));
|
||||
if entry.succ.is_empty() {
|
||||
visible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if op.succ.is_empty() {
|
||||
visible = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
visible
|
||||
}
|
||||
|
||||
pub(crate) fn visible_op(
|
||||
op: &Op,
|
||||
pos: usize,
|
||||
counters: &HashMap<OpId, CounterData>,
|
||||
) -> Vec<(usize, Op)> {
|
||||
let mut result = vec![];
|
||||
for pred in &op.pred {
|
||||
if let Some(entry) = counters.get(pred) {
|
||||
result.push((entry.pos, entry.op.clone()));
|
||||
}
|
||||
}
|
||||
if result.is_empty() {
|
||||
vec![(pos, op.clone())]
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn binary_search_by<F, const B: usize>(node: &OpTreeNode<B>, f: F) -> usize
|
||||
where
|
||||
F: Fn(&Op) -> Ordering,
|
||||
{
|
||||
let mut right = node.len();
|
||||
let mut left = 0;
|
||||
while left < right {
|
||||
let seq = (left + right) / 2;
|
||||
if f(node.get(seq).unwrap()) == Ordering::Less {
|
||||
left = seq + 1;
|
||||
} else {
|
||||
right = seq;
|
||||
}
|
||||
}
|
||||
left
|
||||
}
|
80
automerge/src/query/insert.rs
Normal file
80
automerge/src/query/insert.rs
Normal file
|
@ -0,0 +1,80 @@
|
|||
use crate::op_tree::OpTreeNode;
|
||||
use crate::query::{QueryResult, TreeQuery, VisWindow};
|
||||
use crate::{AutomergeError, ElemId, Key, Op, HEAD};
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct InsertNth<const B: usize> {
|
||||
target: usize,
|
||||
seen: usize,
|
||||
pub pos: usize,
|
||||
last_seen: Option<ElemId>,
|
||||
last_insert: Option<ElemId>,
|
||||
window: VisWindow,
|
||||
}
|
||||
|
||||
impl<const B: usize> InsertNth<B> {
|
||||
pub fn new(target: usize) -> Self {
|
||||
InsertNth {
|
||||
target,
|
||||
seen: 0,
|
||||
pos: 0,
|
||||
last_seen: None,
|
||||
last_insert: None,
|
||||
window: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn key(&self) -> Result<Key, AutomergeError> {
|
||||
if self.target == 0 {
|
||||
Ok(HEAD.into())
|
||||
} else if self.seen == self.target && self.last_insert.is_some() {
|
||||
Ok(Key::Seq(self.last_insert.unwrap()))
|
||||
} else {
|
||||
Err(AutomergeError::InvalidIndex(self.target))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const B: usize> TreeQuery<B> for InsertNth<B> {
|
||||
fn query_node(&mut self, child: &OpTreeNode<B>) -> QueryResult {
|
||||
if self.target == 0 {
|
||||
// insert at the start of the obj all inserts are lesser b/c this is local
|
||||
self.pos = 0;
|
||||
return QueryResult::Finish;
|
||||
}
|
||||
let mut num_vis = child.index.len;
|
||||
if num_vis > 0 {
|
||||
if child.index.has(&self.last_seen) {
|
||||
num_vis -= 1;
|
||||
}
|
||||
if self.seen + num_vis >= self.target {
|
||||
QueryResult::Decend
|
||||
} else {
|
||||
self.pos += child.len();
|
||||
self.seen += num_vis;
|
||||
self.last_seen = child.last().elemid();
|
||||
QueryResult::Next
|
||||
}
|
||||
} else {
|
||||
self.pos += child.len();
|
||||
QueryResult::Next
|
||||
}
|
||||
}
|
||||
|
||||
fn query_element(&mut self, element: &Op) -> QueryResult {
|
||||
if element.insert {
|
||||
if self.seen >= self.target {
|
||||
return QueryResult::Finish;
|
||||
};
|
||||
self.last_seen = None;
|
||||
self.last_insert = element.elemid();
|
||||
}
|
||||
if self.last_seen.is_none() && self.window.visible(element, self.pos) {
|
||||
self.seen += 1;
|
||||
self.last_seen = element.elemid()
|
||||
}
|
||||
self.pos += 1;
|
||||
QueryResult::Next
|
||||
}
|
||||
}
|
34
automerge/src/query/keys.rs
Normal file
34
automerge/src/query/keys.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use crate::op_tree::OpTreeNode;
|
||||
use crate::query::{QueryResult, TreeQuery, VisWindow};
|
||||
use crate::Key;
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct Keys<const B: usize> {
|
||||
pub keys: Vec<Key>,
|
||||
window: VisWindow,
|
||||
}
|
||||
|
||||
impl<const B: usize> Keys<B> {
|
||||
pub fn new() -> Self {
|
||||
Keys {
|
||||
keys: vec![],
|
||||
window: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const B: usize> TreeQuery<B> for Keys<B> {
|
||||
fn query_node(&mut self, child: &OpTreeNode<B>) -> QueryResult {
|
||||
let mut last = None;
|
||||
for i in 0..child.len() {
|
||||
let op = child.get(i).unwrap();
|
||||
let visible = self.window.visible(op, i);
|
||||
if Some(op.key) != last && visible {
|
||||
self.keys.push(op.key);
|
||||
last = Some(op.key);
|
||||
}
|
||||
}
|
||||
QueryResult::Finish
|
||||
}
|
||||
}
|
36
automerge/src/query/keys_at.rs
Normal file
36
automerge/src/query/keys_at.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
use crate::query::{QueryResult, TreeQuery, VisWindow};
|
||||
use crate::{Clock, Key, Op};
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct KeysAt<const B: usize> {
|
||||
clock: Clock,
|
||||
pub keys: Vec<Key>,
|
||||
last: Option<Key>,
|
||||
window: VisWindow,
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<const B: usize> KeysAt<B> {
|
||||
pub fn new(clock: Clock) -> Self {
|
||||
KeysAt {
|
||||
clock,
|
||||
pos: 0,
|
||||
last: None,
|
||||
keys: vec![],
|
||||
window: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const B: usize> TreeQuery<B> for KeysAt<B> {
|
||||
fn query_element(&mut self, op: &Op) -> QueryResult {
|
||||
let visible = self.window.visible_at(op, self.pos, &self.clock);
|
||||
if Some(op.key) != self.last && visible {
|
||||
self.keys.push(op.key);
|
||||
self.last = Some(op.key);
|
||||
}
|
||||
self.pos += 1;
|
||||
QueryResult::Next
|
||||
}
|
||||
}
|
23
automerge/src/query/len.rs
Normal file
23
automerge/src/query/len.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
use crate::op_tree::OpTreeNode;
|
||||
use crate::query::{QueryResult, TreeQuery};
|
||||
use crate::ObjId;
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct Len<const B: usize> {
|
||||
obj: ObjId,
|
||||
pub len: usize,
|
||||
}
|
||||
|
||||
impl<const B: usize> Len<B> {
|
||||
pub fn new(obj: ObjId) -> Self {
|
||||
Len { obj, len: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl<const B: usize> TreeQuery<B> for Len<B> {
|
||||
fn query_node(&mut self, child: &OpTreeNode<B>) -> QueryResult {
|
||||
self.len = child.index.len;
|
||||
QueryResult::Finish
|
||||
}
|
||||
}
|
|
@ -1,39 +1,37 @@
|
|||
use crate::query::{QueryResult, TreeQuery, VisWindow};
|
||||
use crate::types::{Clock, ElemId, ListEncoding, Op};
|
||||
use crate::{Clock, ElemId, Op};
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct LenAt {
|
||||
pub(crate) len: usize,
|
||||
pub(crate) struct LenAt<const B: usize> {
|
||||
pub len: usize,
|
||||
clock: Clock,
|
||||
pos: usize,
|
||||
encoding: ListEncoding,
|
||||
last: Option<ElemId>,
|
||||
window: VisWindow,
|
||||
}
|
||||
|
||||
impl LenAt {
|
||||
pub(crate) fn new(clock: Clock, encoding: ListEncoding) -> Self {
|
||||
impl<const B: usize> LenAt<B> {
|
||||
pub fn new(clock: Clock) -> Self {
|
||||
LenAt {
|
||||
clock,
|
||||
pos: 0,
|
||||
len: 0,
|
||||
encoding,
|
||||
last: None,
|
||||
window: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TreeQuery<'a> for LenAt {
|
||||
fn query_element(&mut self, op: &'a Op) -> QueryResult {
|
||||
impl<const B: usize> TreeQuery<B> for LenAt<B> {
|
||||
fn query_element(&mut self, op: &Op) -> QueryResult {
|
||||
if op.insert {
|
||||
self.last = None;
|
||||
}
|
||||
let elem = op.elemid();
|
||||
let visible = self.window.visible_at(op, self.pos, &self.clock);
|
||||
if elem != self.last && visible {
|
||||
self.len += op.width(self.encoding);
|
||||
self.len += 1;
|
||||
self.last = elem;
|
||||
}
|
||||
self.pos += 1;
|
48
automerge/src/query/list_vals.rs
Normal file
48
automerge/src/query/list_vals.rs
Normal file
|
@ -0,0 +1,48 @@
|
|||
use crate::op_tree::{OpSetMetadata, OpTreeNode};
|
||||
use crate::query::{binary_search_by, is_visible, visible_op, QueryResult, TreeQuery};
|
||||
use crate::{ElemId, ObjId, Op};
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct ListVals {
|
||||
obj: ObjId,
|
||||
last_elem: Option<ElemId>,
|
||||
pub ops: Vec<Op>,
|
||||
}
|
||||
|
||||
impl ListVals {
|
||||
pub fn new(obj: ObjId) -> Self {
|
||||
ListVals {
|
||||
obj,
|
||||
last_elem: None,
|
||||
ops: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const B: usize> TreeQuery<B> for ListVals {
|
||||
fn query_node_with_metadata(
|
||||
&mut self,
|
||||
child: &OpTreeNode<B>,
|
||||
m: &OpSetMetadata,
|
||||
) -> QueryResult {
|
||||
let start = binary_search_by(child, |op| m.lamport_cmp(op.obj.0, self.obj.0));
|
||||
let mut counters = Default::default();
|
||||
for pos in start..child.len() {
|
||||
let op = child.get(pos).unwrap();
|
||||
if op.obj != self.obj {
|
||||
break;
|
||||
}
|
||||
if op.insert {
|
||||
self.last_elem = None;
|
||||
}
|
||||
if self.last_elem.is_none() && is_visible(op, pos, &mut counters) {
|
||||
for (_, vop) in visible_op(op, pos, &counters) {
|
||||
self.last_elem = vop.elemid();
|
||||
self.ops.push(vop);
|
||||
}
|
||||
}
|
||||
}
|
||||
QueryResult::Finish
|
||||
}
|
||||
}
|
40
automerge/src/query/list_vals_at.rs
Normal file
40
automerge/src/query/list_vals_at.rs
Normal file
|
@ -0,0 +1,40 @@
|
|||
use crate::query::{QueryResult, TreeQuery, VisWindow};
|
||||
use crate::{Clock, ElemId, Op};
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct ListValsAt {
|
||||
clock: Clock,
|
||||
last_elem: Option<ElemId>,
|
||||
pub ops: Vec<Op>,
|
||||
window: VisWindow,
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl ListValsAt {
|
||||
pub fn new(clock: Clock) -> Self {
|
||||
ListValsAt {
|
||||
clock,
|
||||
last_elem: None,
|
||||
ops: vec![],
|
||||
window: Default::default(),
|
||||
pos: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const B: usize> TreeQuery<B> for ListValsAt {
|
||||
fn query_element(&mut self, op: &Op) -> QueryResult {
|
||||
if op.insert {
|
||||
self.last_elem = None;
|
||||
}
|
||||
if self.last_elem.is_none() && self.window.visible_at(op, self.pos, &self.clock) {
|
||||
for (_, vop) in self.window.seen_op(op, self.pos) {
|
||||
self.last_elem = vop.elemid();
|
||||
self.ops.push(vop);
|
||||
}
|
||||
}
|
||||
self.pos += 1;
|
||||
QueryResult::Next
|
||||
}
|
||||
}
|
87
automerge/src/query/nth.rs
Normal file
87
automerge/src/query/nth.rs
Normal file
|
@ -0,0 +1,87 @@
|
|||
use crate::op_tree::OpTreeNode;
|
||||
use crate::query::{QueryResult, TreeQuery, VisWindow};
|
||||
use crate::{AutomergeError, ElemId, Key, Op};
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct Nth<const B: usize> {
|
||||
target: usize,
|
||||
seen: usize,
|
||||
last_seen: Option<ElemId>,
|
||||
last_elem: Option<ElemId>,
|
||||
window: VisWindow,
|
||||
pub ops: Vec<Op>,
|
||||
pub ops_pos: Vec<usize>,
|
||||
pub pos: usize,
|
||||
}
|
||||
|
||||
impl<const B: usize> Nth<B> {
|
||||
pub fn new(target: usize) -> Self {
|
||||
Nth {
|
||||
target,
|
||||
seen: 0,
|
||||
last_seen: None,
|
||||
ops: vec![],
|
||||
ops_pos: vec![],
|
||||
pos: 0,
|
||||
last_elem: None,
|
||||
window: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn key(&self) -> Result<Key, AutomergeError> {
|
||||
if let Some(e) = self.last_elem {
|
||||
Ok(Key::Seq(e))
|
||||
} else {
|
||||
Err(AutomergeError::InvalidIndex(self.target))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const B: usize> TreeQuery<B> for Nth<B> {
|
||||
fn query_node(&mut self, child: &OpTreeNode<B>) -> QueryResult {
|
||||
let mut num_vis = child.index.len;
|
||||
if num_vis > 0 {
|
||||
// num vis is the number of keys in the index
|
||||
// minus one if we're counting last_seen
|
||||
// let mut num_vis = s.keys().count();
|
||||
if child.index.has(&self.last_seen) {
|
||||
num_vis -= 1;
|
||||
}
|
||||
if self.seen + num_vis > self.target {
|
||||
QueryResult::Decend
|
||||
} else {
|
||||
self.pos += child.len();
|
||||
self.seen += num_vis;
|
||||
self.last_seen = child.last().elemid();
|
||||
QueryResult::Next
|
||||
}
|
||||
} else {
|
||||
self.pos += child.len();
|
||||
QueryResult::Next
|
||||
}
|
||||
}
|
||||
|
||||
fn query_element(&mut self, element: &Op) -> QueryResult {
|
||||
if element.insert {
|
||||
if self.seen > self.target {
|
||||
return QueryResult::Finish;
|
||||
};
|
||||
self.last_elem = element.elemid();
|
||||
self.last_seen = None
|
||||
}
|
||||
let visible = self.window.visible(element, self.pos);
|
||||
if visible && self.last_seen.is_none() {
|
||||
self.seen += 1;
|
||||
self.last_seen = element.elemid()
|
||||
}
|
||||
if self.seen == self.target + 1 && visible {
|
||||
for (vpos, vop) in self.window.seen_op(element, self.pos) {
|
||||
self.ops.push(vop);
|
||||
self.ops_pos.push(vpos);
|
||||
}
|
||||
}
|
||||
self.pos += 1;
|
||||
QueryResult::Next
|
||||
}
|
||||
}
|
57
automerge/src/query/nth_at.rs
Normal file
57
automerge/src/query/nth_at.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
use crate::query::{QueryResult, TreeQuery, VisWindow};
|
||||
use crate::{Clock, ElemId, Op};
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct NthAt<const B: usize> {
|
||||
clock: Clock,
|
||||
target: usize,
|
||||
seen: usize,
|
||||
last_seen: Option<ElemId>,
|
||||
last_elem: Option<ElemId>,
|
||||
window: VisWindow,
|
||||
pub ops: Vec<Op>,
|
||||
pub ops_pos: Vec<usize>,
|
||||
pub pos: usize,
|
||||
}
|
||||
|
||||
impl<const B: usize> NthAt<B> {
|
||||
pub fn new(target: usize, clock: Clock) -> Self {
|
||||
NthAt {
|
||||
clock,
|
||||
target,
|
||||
seen: 0,
|
||||
last_seen: None,
|
||||
ops: vec![],
|
||||
ops_pos: vec![],
|
||||
pos: 0,
|
||||
last_elem: None,
|
||||
window: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const B: usize> TreeQuery<B> for NthAt<B> {
|
||||
fn query_element(&mut self, element: &Op) -> QueryResult {
|
||||
if element.insert {
|
||||
if self.seen > self.target {
|
||||
return QueryResult::Finish;
|
||||
};
|
||||
self.last_elem = element.elemid();
|
||||
self.last_seen = None
|
||||
}
|
||||
let visible = self.window.visible_at(element, self.pos, &self.clock);
|
||||
if visible && self.last_seen.is_none() {
|
||||
self.seen += 1;
|
||||
self.last_seen = element.elemid()
|
||||
}
|
||||
if self.seen == self.target + 1 && visible {
|
||||
for (vpos, vop) in self.window.seen_op(element, self.pos) {
|
||||
self.ops.push(vop);
|
||||
self.ops_pos.push(vpos);
|
||||
}
|
||||
}
|
||||
self.pos += 1;
|
||||
QueryResult::Next
|
||||
}
|
||||
}
|
54
automerge/src/query/prop.rs
Normal file
54
automerge/src/query/prop.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
use crate::op_tree::{OpSetMetadata, OpTreeNode};
|
||||
use crate::query::{binary_search_by, is_visible, visible_op, QueryResult, TreeQuery};
|
||||
use crate::{Key, ObjId, Op};
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct Prop {
|
||||
obj: ObjId,
|
||||
key: Key,
|
||||
pub ops: Vec<Op>,
|
||||
pub ops_pos: Vec<usize>,
|
||||
pub pos: usize,
|
||||
}
|
||||
|
||||
impl Prop {
|
||||
pub fn new(obj: ObjId, prop: usize) -> Self {
|
||||
Prop {
|
||||
obj,
|
||||
key: Key::Map(prop),
|
||||
ops: vec![],
|
||||
ops_pos: vec![],
|
||||
pos: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const B: usize> TreeQuery<B> for Prop {
|
||||
fn query_node_with_metadata(
|
||||
&mut self,
|
||||
child: &OpTreeNode<B>,
|
||||
m: &OpSetMetadata,
|
||||
) -> QueryResult {
|
||||
let start = binary_search_by(child, |op| {
|
||||
m.lamport_cmp(op.obj.0, self.obj.0)
|
||||
.then_with(|| m.key_cmp(&op.key, &self.key))
|
||||
});
|
||||
let mut counters = Default::default();
|
||||
self.pos = start;
|
||||
for pos in start..child.len() {
|
||||
let op = child.get(pos).unwrap();
|
||||
if !(op.obj == self.obj && op.key == self.key) {
|
||||
break;
|
||||
}
|
||||
if is_visible(op, pos, &mut counters) {
|
||||
for (vpos, vop) in visible_op(op, pos, &counters) {
|
||||
self.ops.push(vop);
|
||||
self.ops_pos.push(vpos);
|
||||
}
|
||||
}
|
||||
self.pos += 1;
|
||||
}
|
||||
QueryResult::Finish
|
||||
}
|
||||
}
|
51
automerge/src/query/prop_at.rs
Normal file
51
automerge/src/query/prop_at.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
use crate::op_tree::{OpSetMetadata, OpTreeNode};
|
||||
use crate::query::{binary_search_by, QueryResult, TreeQuery, VisWindow};
|
||||
use crate::{Clock, Key, Op};
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct PropAt {
|
||||
clock: Clock,
|
||||
key: Key,
|
||||
pub ops: Vec<Op>,
|
||||
pub ops_pos: Vec<usize>,
|
||||
pub pos: usize,
|
||||
}
|
||||
|
||||
impl PropAt {
|
||||
pub fn new(prop: usize, clock: Clock) -> Self {
|
||||
PropAt {
|
||||
clock,
|
||||
key: Key::Map(prop),
|
||||
ops: vec![],
|
||||
ops_pos: vec![],
|
||||
pos: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const B: usize> TreeQuery<B> for PropAt {
|
||||
fn query_node_with_metadata(
|
||||
&mut self,
|
||||
child: &OpTreeNode<B>,
|
||||
m: &OpSetMetadata,
|
||||
) -> QueryResult {
|
||||
let start = binary_search_by(child, |op| m.key_cmp(&op.key, &self.key));
|
||||
let mut window: VisWindow = Default::default();
|
||||
self.pos = start;
|
||||
for pos in start..child.len() {
|
||||
let op = child.get(pos).unwrap();
|
||||
if op.key != self.key {
|
||||
break;
|
||||
}
|
||||
if window.visible_at(op, pos, &self.clock) {
|
||||
for (vpos, vop) in window.seen_op(op, pos) {
|
||||
self.ops.push(vop);
|
||||
self.ops_pos.push(vpos);
|
||||
}
|
||||
}
|
||||
self.pos += 1;
|
||||
}
|
||||
QueryResult::Finish
|
||||
}
|
||||
}
|
129
automerge/src/query/seek_op.rs
Normal file
129
automerge/src/query/seek_op.rs
Normal file
|
@ -0,0 +1,129 @@
|
|||
use crate::op_tree::{OpSetMetadata, OpTreeNode};
|
||||
use crate::query::{binary_search_by, QueryResult, TreeQuery};
|
||||
use crate::{Key, Op, HEAD};
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct SeekOp<const B: usize> {
|
||||
op: Op,
|
||||
pub pos: usize,
|
||||
pub succ: Vec<usize>,
|
||||
found: bool,
|
||||
}
|
||||
|
||||
impl<const B: usize> SeekOp<B> {
|
||||
pub fn new(op: &Op) -> Self {
|
||||
SeekOp {
|
||||
op: op.clone(),
|
||||
succ: vec![],
|
||||
pos: 0,
|
||||
found: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn different_obj(&self, op: &Op) -> bool {
|
||||
op.obj != self.op.obj
|
||||
}
|
||||
|
||||
fn lesser_insert(&self, op: &Op, m: &OpSetMetadata) -> bool {
|
||||
op.insert && m.lamport_cmp(op.id, self.op.id) == Ordering::Less
|
||||
}
|
||||
|
||||
fn greater_opid(&self, op: &Op, m: &OpSetMetadata) -> bool {
|
||||
m.lamport_cmp(op.id, self.op.id) == Ordering::Greater
|
||||
}
|
||||
|
||||
fn is_target_insert(&self, op: &Op) -> bool {
|
||||
if !op.insert {
|
||||
return false;
|
||||
}
|
||||
if self.op.insert {
|
||||
op.elemid() == self.op.key.elemid()
|
||||
} else {
|
||||
op.elemid() == self.op.elemid()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const B: usize> TreeQuery<B> for SeekOp<B> {
|
||||
fn query_node_with_metadata(
|
||||
&mut self,
|
||||
child: &OpTreeNode<B>,
|
||||
m: &OpSetMetadata,
|
||||
) -> QueryResult {
|
||||
if self.found {
|
||||
return QueryResult::Decend;
|
||||
}
|
||||
match self.op.key {
|
||||
Key::Seq(e) if e == HEAD => {
|
||||
while self.pos < child.len() {
|
||||
let op = child.get(self.pos).unwrap();
|
||||
if self.op.overwrites(op) {
|
||||
self.succ.push(self.pos);
|
||||
}
|
||||
if op.insert && m.lamport_cmp(op.id, self.op.id) == Ordering::Less {
|
||||
break;
|
||||
}
|
||||
self.pos += 1;
|
||||
}
|
||||
QueryResult::Finish
|
||||
}
|
||||
Key::Seq(e) => {
|
||||
if self.found || child.index.ops.contains(&e.0) {
|
||||
QueryResult::Decend
|
||||
} else {
|
||||
self.pos += child.len();
|
||||
QueryResult::Next
|
||||
}
|
||||
}
|
||||
Key::Map(_) => {
|
||||
self.pos = binary_search_by(child, |op| m.key_cmp(&op.key, &self.op.key));
|
||||
while self.pos < child.len() {
|
||||
let op = child.get(self.pos).unwrap();
|
||||
if op.key != self.op.key {
|
||||
break;
|
||||
}
|
||||
if self.op.overwrites(op) {
|
||||
self.succ.push(self.pos);
|
||||
}
|
||||
if m.lamport_cmp(op.id, self.op.id) == Ordering::Greater {
|
||||
break;
|
||||
}
|
||||
self.pos += 1;
|
||||
}
|
||||
QueryResult::Finish
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn query_element_with_metadata(&mut self, e: &Op, m: &OpSetMetadata) -> QueryResult {
|
||||
if !self.found {
|
||||
if self.is_target_insert(e) {
|
||||
self.found = true;
|
||||
if self.op.overwrites(e) {
|
||||
self.succ.push(self.pos);
|
||||
}
|
||||
}
|
||||
self.pos += 1;
|
||||
QueryResult::Next
|
||||
} else {
|
||||
if self.op.overwrites(e) {
|
||||
self.succ.push(self.pos);
|
||||
}
|
||||
if self.op.insert {
|
||||
if self.different_obj(e) || self.lesser_insert(e, m) {
|
||||
QueryResult::Finish
|
||||
} else {
|
||||
self.pos += 1;
|
||||
QueryResult::Next
|
||||
}
|
||||
} else if e.insert || self.different_obj(e) || self.greater_opid(e, m) {
|
||||
QueryResult::Finish
|
||||
} else {
|
||||
self.pos += 1;
|
||||
QueryResult::Next
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,37 +4,41 @@ use std::{
|
|||
mem,
|
||||
};
|
||||
|
||||
pub(crate) const B: usize = 16;
|
||||
pub(crate) type SequenceTree<T> = SequenceTreeInternal<T>;
|
||||
pub type SequenceTree<T> = SequenceTreeInternal<T, 25>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct SequenceTreeInternal<T> {
|
||||
root_node: Option<SequenceTreeNode<T>>,
|
||||
pub struct SequenceTreeInternal<T, const B: usize> {
|
||||
root_node: Option<SequenceTreeNode<T, B>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct SequenceTreeNode<T> {
|
||||
struct SequenceTreeNode<T, const B: usize> {
|
||||
elements: Vec<T>,
|
||||
children: Vec<SequenceTreeNode<T>>,
|
||||
children: Vec<SequenceTreeNode<T, B>>,
|
||||
length: usize,
|
||||
}
|
||||
|
||||
impl<T> SequenceTreeInternal<T>
|
||||
impl<T, const B: usize> SequenceTreeInternal<T, B>
|
||||
where
|
||||
T: Clone + Debug,
|
||||
{
|
||||
/// Construct a new, empty, sequence.
|
||||
pub(crate) fn new() -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self { root_node: None }
|
||||
}
|
||||
|
||||
/// Get the length of the sequence.
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
pub fn len(&self) -> usize {
|
||||
self.root_node.as_ref().map_or(0, |n| n.len())
|
||||
}
|
||||
|
||||
/// Check if the sequence is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Create an iterator through the sequence.
|
||||
pub(crate) fn iter(&self) -> Iter<'_, T> {
|
||||
pub fn iter(&self) -> Iter<'_, T, B> {
|
||||
Iter {
|
||||
inner: self,
|
||||
index: 0,
|
||||
|
@ -46,7 +50,7 @@ where
|
|||
/// # Panics
|
||||
///
|
||||
/// Panics if `index > len`.
|
||||
pub(crate) fn insert(&mut self, index: usize, element: T) {
|
||||
pub fn insert(&mut self, index: usize, element: T) {
|
||||
let old_len = self.len();
|
||||
if let Some(root) = self.root_node.as_mut() {
|
||||
#[cfg(debug_assertions)]
|
||||
|
@ -89,22 +93,27 @@ where
|
|||
}
|
||||
|
||||
/// Push the `element` onto the back of the sequence.
|
||||
pub(crate) fn push(&mut self, element: T) {
|
||||
pub fn push(&mut self, element: T) {
|
||||
let l = self.len();
|
||||
self.insert(l, element)
|
||||
}
|
||||
|
||||
/// Get the `element` at `index` in the sequence.
|
||||
pub(crate) fn get(&self, index: usize) -> Option<&T> {
|
||||
pub fn get(&self, index: usize) -> Option<&T> {
|
||||
self.root_node.as_ref().and_then(|n| n.get(index))
|
||||
}
|
||||
|
||||
/// Get the `element` at `index` in the sequence.
|
||||
pub fn get_mut(&mut self, index: usize) -> Option<&mut T> {
|
||||
self.root_node.as_mut().and_then(|n| n.get_mut(index))
|
||||
}
|
||||
|
||||
/// Removes the element at `index` from the sequence.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `index` is out of bounds.
|
||||
pub(crate) fn remove(&mut self, index: usize) -> T {
|
||||
pub fn remove(&mut self, index: usize) -> T {
|
||||
if let Some(root) = self.root_node.as_mut() {
|
||||
#[cfg(debug_assertions)]
|
||||
let len = root.check();
|
||||
|
@ -125,9 +134,18 @@ where
|
|||
panic!("remove from empty tree")
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the `element` at `index` in the sequence, returning the old value.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `index > len`
|
||||
pub fn set(&mut self, index: usize, element: T) -> T {
|
||||
self.root_node.as_mut().unwrap().set(index, element)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SequenceTreeNode<T>
|
||||
impl<T, const B: usize> SequenceTreeNode<T, B>
|
||||
where
|
||||
T: Clone + Debug,
|
||||
{
|
||||
|
@ -139,7 +157,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
pub fn len(&self) -> usize {
|
||||
self.length
|
||||
}
|
||||
|
||||
|
@ -362,7 +380,7 @@ where
|
|||
l
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&mut self, index: usize) -> T {
|
||||
pub fn remove(&mut self, index: usize) -> T {
|
||||
let original_len = self.len();
|
||||
if self.is_leaf() {
|
||||
let v = self.remove_from_leaf(index);
|
||||
|
@ -405,7 +423,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, middle: T, successor_sibling: SequenceTreeNode<T>) {
|
||||
fn merge(&mut self, middle: T, successor_sibling: SequenceTreeNode<T, B>) {
|
||||
self.elements.push(middle);
|
||||
self.elements.extend(successor_sibling.elements);
|
||||
self.children.extend(successor_sibling.children);
|
||||
|
@ -413,7 +431,31 @@ where
|
|||
assert!(self.is_full());
|
||||
}
|
||||
|
||||
pub(crate) fn get(&self, index: usize) -> Option<&T> {
|
||||
pub fn set(&mut self, index: usize, element: T) -> T {
|
||||
if self.is_leaf() {
|
||||
let old_element = self.elements.get_mut(index).unwrap();
|
||||
mem::replace(old_element, element)
|
||||
} else {
|
||||
let mut cumulative_len = 0;
|
||||
for (child_index, child) in self.children.iter_mut().enumerate() {
|
||||
match (cumulative_len + child.len()).cmp(&index) {
|
||||
Ordering::Less => {
|
||||
cumulative_len += child.len() + 1;
|
||||
}
|
||||
Ordering::Equal => {
|
||||
let old_element = self.elements.get_mut(child_index).unwrap();
|
||||
return mem::replace(old_element, element);
|
||||
}
|
||||
Ordering::Greater => {
|
||||
return child.set(index - cumulative_len, element);
|
||||
}
|
||||
}
|
||||
}
|
||||
panic!("Invalid index to set: {} but len was {}", index, self.len())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, index: usize) -> Option<&T> {
|
||||
if self.is_leaf() {
|
||||
return self.elements.get(index);
|
||||
} else {
|
||||
|
@ -432,9 +474,29 @@ where
|
|||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, index: usize) -> Option<&mut T> {
|
||||
if self.is_leaf() {
|
||||
return self.elements.get_mut(index);
|
||||
} else {
|
||||
let mut cumulative_len = 0;
|
||||
for (child_index, child) in self.children.iter_mut().enumerate() {
|
||||
match (cumulative_len + child.len()).cmp(&index) {
|
||||
Ordering::Less => {
|
||||
cumulative_len += child.len() + 1;
|
||||
}
|
||||
Ordering::Equal => return self.elements.get_mut(child_index),
|
||||
Ordering::Greater => {
|
||||
return child.get_mut(index - cumulative_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for SequenceTreeInternal<T>
|
||||
impl<T, const B: usize> Default for SequenceTreeInternal<T, B>
|
||||
where
|
||||
T: Clone + Debug,
|
||||
{
|
||||
|
@ -443,7 +505,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq for SequenceTreeInternal<T>
|
||||
impl<T, const B: usize> PartialEq for SequenceTreeInternal<T, B>
|
||||
where
|
||||
T: Clone + Debug + PartialEq,
|
||||
{
|
||||
|
@ -452,13 +514,13 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, T> IntoIterator for &'a SequenceTreeInternal<T>
|
||||
impl<'a, T, const B: usize> IntoIterator for &'a SequenceTreeInternal<T, B>
|
||||
where
|
||||
T: Clone + Debug,
|
||||
{
|
||||
type Item = &'a T;
|
||||
|
||||
type IntoIter = Iter<'a, T>;
|
||||
type IntoIter = Iter<'a, T, B>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
Iter {
|
||||
|
@ -468,13 +530,12 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Iter<'a, T> {
|
||||
inner: &'a SequenceTreeInternal<T>,
|
||||
pub struct Iter<'a, T, const B: usize> {
|
||||
inner: &'a SequenceTreeInternal<T, B>,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl<'a, T> Iterator for Iter<'a, T>
|
||||
impl<'a, T, const B: usize> Iterator for Iter<'a, T, B>
|
||||
where
|
||||
T: Clone + Debug,
|
||||
{
|
||||
|
@ -493,35 +554,37 @@ where
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use proptest::prelude::*;
|
||||
use crate::ActorId;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn push_back() {
|
||||
let mut t = SequenceTree::new();
|
||||
let actor = ActorId::random();
|
||||
|
||||
t.push(1);
|
||||
t.push(2);
|
||||
t.push(3);
|
||||
t.push(4);
|
||||
t.push(5);
|
||||
t.push(6);
|
||||
t.push(8);
|
||||
t.push(100);
|
||||
t.push(actor.op_id_at(1));
|
||||
t.push(actor.op_id_at(2));
|
||||
t.push(actor.op_id_at(3));
|
||||
t.push(actor.op_id_at(4));
|
||||
t.push(actor.op_id_at(5));
|
||||
t.push(actor.op_id_at(6));
|
||||
t.push(actor.op_id_at(8));
|
||||
t.push(actor.op_id_at(100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert() {
|
||||
let mut t = SequenceTree::new();
|
||||
let actor = ActorId::random();
|
||||
|
||||
t.insert(0, 1);
|
||||
t.insert(1, 1);
|
||||
t.insert(0, 1);
|
||||
t.insert(0, 1);
|
||||
t.insert(0, 1);
|
||||
t.insert(3, 1);
|
||||
t.insert(4, 1);
|
||||
t.insert(0, actor.op_id_at(1));
|
||||
t.insert(1, actor.op_id_at(1));
|
||||
t.insert(0, actor.op_id_at(1));
|
||||
t.insert(0, actor.op_id_at(1));
|
||||
t.insert(0, actor.op_id_at(1));
|
||||
t.insert(3, actor.op_id_at(1));
|
||||
t.insert(4, actor.op_id_at(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -546,72 +609,79 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn arb_indices() -> impl Strategy<Value = Vec<usize>> {
|
||||
proptest::collection::vec(any::<usize>(), 0..1000).prop_map(|v| {
|
||||
let mut len = 0;
|
||||
v.into_iter()
|
||||
.map(|i| {
|
||||
len += 1;
|
||||
i % len
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
|
||||
proptest! {
|
||||
|
||||
#[test]
|
||||
fn proptest_insert(indices in arb_indices()) {
|
||||
let mut t = SequenceTreeInternal::<usize>::new();
|
||||
let mut v = Vec::new();
|
||||
|
||||
for i in indices{
|
||||
if i <= v.len() {
|
||||
t.insert(i % 3, i);
|
||||
v.insert(i % 3, i);
|
||||
} else {
|
||||
return Err(proptest::test_runner::TestCaseError::reject("index out of bounds"))
|
||||
}
|
||||
|
||||
assert_eq!(v, t.iter().copied().collect::<Vec<_>>())
|
||||
}
|
||||
/*
|
||||
fn arb_indices() -> impl Strategy<Value = Vec<usize>> {
|
||||
proptest::collection::vec(any::<usize>(), 0..1000).prop_map(|v| {
|
||||
let mut len = 0;
|
||||
v.into_iter()
|
||||
.map(|i| {
|
||||
len += 1;
|
||||
i % len
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
// use proptest::prelude::*;
|
||||
|
||||
proptest! {
|
||||
/*
|
||||
proptest! {
|
||||
|
||||
// This is a really slow test due to all the copying of the Vecs (i.e. not due to the
|
||||
// sequencetree) so we only do a few runs
|
||||
#![proptest_config(ProptestConfig::with_cases(20))]
|
||||
#[test]
|
||||
fn proptest_remove(inserts in arb_indices(), removes in arb_indices()) {
|
||||
let mut t = SequenceTreeInternal::<usize>::new();
|
||||
let mut v = Vec::new();
|
||||
#[test]
|
||||
fn proptest_insert(indices in arb_indices()) {
|
||||
let mut t = SequenceTreeInternal::<usize, 3>::new();
|
||||
let actor = ActorId::random();
|
||||
let mut v = Vec::new();
|
||||
|
||||
for i in inserts {
|
||||
if i <= v.len() {
|
||||
t.insert(i , i);
|
||||
v.insert(i , i);
|
||||
} else {
|
||||
return Err(proptest::test_runner::TestCaseError::reject("index out of bounds"))
|
||||
for i in indices{
|
||||
if i <= v.len() {
|
||||
t.insert(i % 3, i);
|
||||
v.insert(i % 3, i);
|
||||
} else {
|
||||
return Err(proptest::test_runner::TestCaseError::reject("index out of bounds"))
|
||||
}
|
||||
|
||||
assert_eq!(v, t.iter().copied().collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
assert_eq!(v, t.iter().copied().collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
for i in removes {
|
||||
if i < v.len() {
|
||||
let tr = t.remove(i);
|
||||
let vr = v.remove(i);
|
||||
assert_eq!(tr, vr);
|
||||
} else {
|
||||
return Err(proptest::test_runner::TestCaseError::reject("index out of bounds"))
|
||||
}
|
||||
|
||||
assert_eq!(v, t.iter().copied().collect::<Vec<_>>())
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
/*
|
||||
proptest! {
|
||||
|
||||
#[test]
|
||||
fn proptest_remove(inserts in arb_indices(), removes in arb_indices()) {
|
||||
let mut t = SequenceTreeInternal::<usize, 3>::new();
|
||||
let actor = ActorId::random();
|
||||
let mut v = Vec::new();
|
||||
|
||||
for i in inserts {
|
||||
if i <= v.len() {
|
||||
t.insert(i , i);
|
||||
v.insert(i , i);
|
||||
} else {
|
||||
return Err(proptest::test_runner::TestCaseError::reject("index out of bounds"))
|
||||
}
|
||||
|
||||
assert_eq!(v, t.iter().copied().collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
for i in removes {
|
||||
if i < v.len() {
|
||||
let tr = t.remove(i);
|
||||
let vr = v.remove(i);
|
||||
assert_eq!(tr, vr);
|
||||
} else {
|
||||
return Err(proptest::test_runner::TestCaseError::reject("index out of bounds"))
|
||||
}
|
||||
|
||||
assert_eq!(v, t.iter().copied().collect::<Vec<_>>())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
*/
|
||||
}
|
381
automerge/src/sync.rs
Normal file
381
automerge/src/sync.rs
Normal file
|
@ -0,0 +1,381 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{HashMap, HashSet},
|
||||
convert::TryFrom,
|
||||
io,
|
||||
io::Write,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
decoding, decoding::Decoder, encoding, encoding::Encodable, Automerge, AutomergeError, Change,
|
||||
ChangeHash, Patch,
|
||||
};
|
||||
|
||||
mod bloom;
|
||||
mod state;
|
||||
|
||||
pub use bloom::BloomFilter;
|
||||
pub use state::{SyncHave, SyncState};
|
||||
|
||||
const HASH_SIZE: usize = 32; // 256 bits = 32 bytes
|
||||
const MESSAGE_TYPE_SYNC: u8 = 0x42; // first byte of a sync message, for identification
|
||||
|
||||
impl Automerge {
|
||||
pub fn generate_sync_message(&mut self, sync_state: &mut SyncState) -> Option<SyncMessage> {
|
||||
self.ensure_transaction_closed();
|
||||
self._generate_sync_message(sync_state)
|
||||
}
|
||||
|
||||
fn _generate_sync_message(&self, sync_state: &mut SyncState) -> Option<SyncMessage> {
|
||||
let our_heads = self._get_heads();
|
||||
|
||||
let our_need = self._get_missing_deps(sync_state.their_heads.as_ref().unwrap_or(&vec![]));
|
||||
|
||||
let their_heads_set = if let Some(ref heads) = sync_state.their_heads {
|
||||
heads.iter().collect::<HashSet<_>>()
|
||||
} else {
|
||||
HashSet::new()
|
||||
};
|
||||
let our_have = if our_need.iter().all(|hash| their_heads_set.contains(hash)) {
|
||||
vec![self.make_bloom_filter(sync_state.shared_heads.clone())]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
if let Some(ref their_have) = sync_state.their_have {
|
||||
if let Some(first_have) = their_have.first().as_ref() {
|
||||
if !first_have
|
||||
.last_sync
|
||||
.iter()
|
||||
.all(|hash| self._get_change_by_hash(hash).is_some())
|
||||
{
|
||||
let reset_msg = SyncMessage {
|
||||
heads: our_heads,
|
||||
need: Vec::new(),
|
||||
have: vec![SyncHave::default()],
|
||||
changes: Vec::new(),
|
||||
};
|
||||
return Some(reset_msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut changes_to_send = if let (Some(their_have), Some(their_need)) = (
|
||||
sync_state.their_have.as_ref(),
|
||||
sync_state.their_need.as_ref(),
|
||||
) {
|
||||
self.get_changes_to_send(their_have.clone(), their_need)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let heads_unchanged = if let Some(last_sent_heads) = sync_state.last_sent_heads.as_ref() {
|
||||
last_sent_heads == &our_heads
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let heads_equal = if let Some(their_heads) = sync_state.their_heads.as_ref() {
|
||||
their_heads == &our_heads
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if heads_unchanged && heads_equal && changes_to_send.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// deduplicate the changes to send with those we have already sent
|
||||
changes_to_send.retain(|change| !sync_state.sent_hashes.contains(&change.hash));
|
||||
|
||||
sync_state.last_sent_heads = Some(our_heads.clone());
|
||||
sync_state
|
||||
.sent_hashes
|
||||
.extend(changes_to_send.iter().map(|c| c.hash));
|
||||
|
||||
let sync_message = SyncMessage {
|
||||
heads: our_heads,
|
||||
have: our_have,
|
||||
need: our_need,
|
||||
changes: changes_to_send.into_iter().cloned().collect(),
|
||||
};
|
||||
|
||||
Some(sync_message)
|
||||
}
|
||||
|
||||
pub fn receive_sync_message(
|
||||
&mut self,
|
||||
sync_state: &mut SyncState,
|
||||
message: SyncMessage,
|
||||
) -> Result<Option<Patch>, AutomergeError> {
|
||||
self.ensure_transaction_closed();
|
||||
self._receive_sync_message(sync_state, message)
|
||||
}
|
||||
|
||||
fn _receive_sync_message(
|
||||
&mut self,
|
||||
sync_state: &mut SyncState,
|
||||
message: SyncMessage,
|
||||
) -> Result<Option<Patch>, AutomergeError> {
|
||||
let mut patch = None;
|
||||
|
||||
let before_heads = self.get_heads();
|
||||
|
||||
let SyncMessage {
|
||||
heads: message_heads,
|
||||
changes: message_changes,
|
||||
need: message_need,
|
||||
have: message_have,
|
||||
} = message;
|
||||
|
||||
let changes_is_empty = message_changes.is_empty();
|
||||
if !changes_is_empty {
|
||||
patch = Some(self.apply_changes(&message_changes)?);
|
||||
sync_state.shared_heads = advance_heads(
|
||||
&before_heads.iter().collect(),
|
||||
&self.get_heads().into_iter().collect(),
|
||||
&sync_state.shared_heads,
|
||||
);
|
||||
}
|
||||
|
||||
// trim down the sent hashes to those that we know they haven't seen
|
||||
self.filter_changes(&message_heads, &mut sync_state.sent_hashes);
|
||||
|
||||
if changes_is_empty && message_heads == before_heads {
|
||||
sync_state.last_sent_heads = Some(message_heads.clone());
|
||||
}
|
||||
|
||||
let known_heads = message_heads
|
||||
.iter()
|
||||
.filter(|head| self.get_change_by_hash(head).is_some())
|
||||
.collect::<Vec<_>>();
|
||||
if known_heads.len() == message_heads.len() {
|
||||
sync_state.shared_heads = message_heads.clone();
|
||||
} else {
|
||||
sync_state.shared_heads = sync_state
|
||||
.shared_heads
|
||||
.iter()
|
||||
.chain(known_heads)
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
sync_state.shared_heads.sort();
|
||||
}
|
||||
|
||||
sync_state.their_have = Some(message_have);
|
||||
sync_state.their_heads = Some(message_heads);
|
||||
sync_state.their_need = Some(message_need);
|
||||
|
||||
Ok(patch)
|
||||
}
|
||||
|
||||
fn make_bloom_filter(&self, last_sync: Vec<ChangeHash>) -> SyncHave {
|
||||
let new_changes = self._get_changes(&last_sync);
|
||||
let hashes = new_changes
|
||||
.into_iter()
|
||||
.map(|change| change.hash)
|
||||
.collect::<Vec<_>>();
|
||||
SyncHave {
|
||||
last_sync,
|
||||
bloom: BloomFilter::from(&hashes[..]),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_changes_to_send(&self, have: Vec<SyncHave>, need: &[ChangeHash]) -> Vec<&Change> {
|
||||
if have.is_empty() {
|
||||
need.iter()
|
||||
.filter_map(|hash| self._get_change_by_hash(hash))
|
||||
.collect()
|
||||
} else {
|
||||
let mut last_sync_hashes = HashSet::new();
|
||||
let mut bloom_filters = Vec::with_capacity(have.len());
|
||||
|
||||
for h in have {
|
||||
let SyncHave { last_sync, bloom } = h;
|
||||
for hash in last_sync {
|
||||
last_sync_hashes.insert(hash);
|
||||
}
|
||||
bloom_filters.push(bloom);
|
||||
}
|
||||
let last_sync_hashes = last_sync_hashes.into_iter().collect::<Vec<_>>();
|
||||
|
||||
let changes = self._get_changes(&last_sync_hashes);
|
||||
|
||||
let mut change_hashes = HashSet::with_capacity(changes.len());
|
||||
let mut dependents: HashMap<ChangeHash, Vec<ChangeHash>> = HashMap::new();
|
||||
let mut hashes_to_send = HashSet::new();
|
||||
|
||||
for change in &changes {
|
||||
change_hashes.insert(change.hash);
|
||||
|
||||
for dep in &change.deps {
|
||||
dependents.entry(*dep).or_default().push(change.hash);
|
||||
}
|
||||
|
||||
if bloom_filters
|
||||
.iter()
|
||||
.all(|bloom| !bloom.contains_hash(&change.hash))
|
||||
{
|
||||
hashes_to_send.insert(change.hash);
|
||||
}
|
||||
}
|
||||
|
||||
let mut stack = hashes_to_send.iter().copied().collect::<Vec<_>>();
|
||||
while let Some(hash) = stack.pop() {
|
||||
if let Some(deps) = dependents.get(&hash) {
|
||||
for dep in deps {
|
||||
if hashes_to_send.insert(*dep) {
|
||||
stack.push(*dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut changes_to_send = Vec::new();
|
||||
for hash in need {
|
||||
hashes_to_send.insert(*hash);
|
||||
if !change_hashes.contains(hash) {
|
||||
let change = self._get_change_by_hash(hash);
|
||||
if let Some(change) = change {
|
||||
changes_to_send.push(change);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for change in changes {
|
||||
if hashes_to_send.contains(&change.hash) {
|
||||
changes_to_send.push(change);
|
||||
}
|
||||
}
|
||||
changes_to_send
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SyncMessage {
|
||||
pub heads: Vec<ChangeHash>,
|
||||
pub need: Vec<ChangeHash>,
|
||||
pub have: Vec<SyncHave>,
|
||||
pub changes: Vec<Change>,
|
||||
}
|
||||
|
||||
impl SyncMessage {
|
||||
pub fn encode(self) -> Result<Vec<u8>, encoding::Error> {
|
||||
let mut buf = vec![MESSAGE_TYPE_SYNC];
|
||||
|
||||
encode_hashes(&mut buf, &self.heads)?;
|
||||
encode_hashes(&mut buf, &self.need)?;
|
||||
(self.have.len() as u32).encode(&mut buf)?;
|
||||
for have in self.have {
|
||||
encode_hashes(&mut buf, &have.last_sync)?;
|
||||
have.bloom.into_bytes()?.encode(&mut buf)?;
|
||||
}
|
||||
|
||||
(self.changes.len() as u32).encode(&mut buf)?;
|
||||
for mut change in self.changes {
|
||||
change.compress();
|
||||
change.raw_bytes().encode(&mut buf)?;
|
||||
}
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
pub fn decode(bytes: &[u8]) -> Result<SyncMessage, decoding::Error> {
|
||||
let mut decoder = Decoder::new(Cow::Borrowed(bytes));
|
||||
|
||||
let message_type = decoder.read::<u8>()?;
|
||||
if message_type != MESSAGE_TYPE_SYNC {
|
||||
return Err(decoding::Error::WrongType {
|
||||
expected_one_of: vec![MESSAGE_TYPE_SYNC],
|
||||
found: message_type,
|
||||
});
|
||||
}
|
||||
|
||||
let heads = decode_hashes(&mut decoder)?;
|
||||
let need = decode_hashes(&mut decoder)?;
|
||||
let have_count = decoder.read::<u32>()?;
|
||||
let mut have = Vec::with_capacity(have_count as usize);
|
||||
for _ in 0..have_count {
|
||||
let last_sync = decode_hashes(&mut decoder)?;
|
||||
let bloom_bytes: Vec<u8> = decoder.read()?;
|
||||
let bloom = BloomFilter::try_from(bloom_bytes.as_slice())?;
|
||||
have.push(SyncHave { last_sync, bloom });
|
||||
}
|
||||
|
||||
let change_count = decoder.read::<u32>()?;
|
||||
let mut changes = Vec::with_capacity(change_count as usize);
|
||||
for _ in 0..change_count {
|
||||
let change = decoder.read()?;
|
||||
changes.push(Change::from_bytes(change)?);
|
||||
}
|
||||
|
||||
Ok(SyncMessage {
|
||||
heads,
|
||||
need,
|
||||
have,
|
||||
changes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_hashes(buf: &mut Vec<u8>, hashes: &[ChangeHash]) -> Result<(), encoding::Error> {
|
||||
debug_assert!(
|
||||
hashes.windows(2).all(|h| h[0] <= h[1]),
|
||||
"hashes were not sorted"
|
||||
);
|
||||
hashes.encode(buf)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Encodable for &[ChangeHash] {
|
||||
fn encode<W: Write>(&self, buf: &mut W) -> io::Result<usize> {
|
||||
let head = self.len().encode(buf)?;
|
||||
let mut body = 0;
|
||||
for hash in self.iter() {
|
||||
buf.write_all(&hash.0)?;
|
||||
body += hash.0.len();
|
||||
}
|
||||
Ok(head + body)
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_hashes(decoder: &mut Decoder) -> Result<Vec<ChangeHash>, decoding::Error> {
|
||||
let length = decoder.read::<u32>()?;
|
||||
let mut hashes = Vec::with_capacity(length as usize);
|
||||
|
||||
for _ in 0..length {
|
||||
let hash_bytes = decoder.read_bytes(HASH_SIZE)?;
|
||||
let hash = ChangeHash::try_from(hash_bytes).map_err(decoding::Error::BadChangeFormat)?;
|
||||
hashes.push(hash);
|
||||
}
|
||||
|
||||
Ok(hashes)
|
||||
}
|
||||
|
||||
fn advance_heads(
|
||||
my_old_heads: &HashSet<&ChangeHash>,
|
||||
my_new_heads: &HashSet<ChangeHash>,
|
||||
our_old_shared_heads: &[ChangeHash],
|
||||
) -> Vec<ChangeHash> {
|
||||
let new_heads = my_new_heads
|
||||
.iter()
|
||||
.filter(|head| !my_old_heads.contains(head))
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let common_heads = our_old_shared_heads
|
||||
.iter()
|
||||
.filter(|head| my_new_heads.contains(head))
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut advanced_heads = HashSet::with_capacity(new_heads.len() + common_heads.len());
|
||||
for head in new_heads.into_iter().chain(common_heads) {
|
||||
advanced_heads.insert(head);
|
||||
}
|
||||
let mut advanced_heads = advanced_heads.into_iter().collect::<Vec<_>>();
|
||||
advanced_heads.sort();
|
||||
advanced_heads
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
use std::borrow::Borrow;
|
||||
use std::{borrow::Cow, convert::TryFrom};
|
||||
|
||||
use crate::storage::parse;
|
||||
use crate::ChangeHash;
|
||||
use crate::{decoding, decoding::Decoder, encoding, encoding::Encodable, ChangeHash};
|
||||
|
||||
// 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
|
||||
|
@ -9,7 +8,7 @@ use crate::ChangeHash;
|
|||
const BITS_PER_ENTRY: u32 = 10;
|
||||
const NUM_PROBES: u32 = 7;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct BloomFilter {
|
||||
num_entries: u32,
|
||||
num_bits_per_entry: u32,
|
||||
|
@ -17,52 +16,19 @@ pub struct BloomFilter {
|
|||
bits: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Default for BloomFilter {
|
||||
fn default() -> Self {
|
||||
BloomFilter {
|
||||
num_entries: 0,
|
||||
num_bits_per_entry: BITS_PER_ENTRY,
|
||||
num_probes: NUM_PROBES,
|
||||
bits: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum ParseError {
|
||||
#[error(transparent)]
|
||||
Leb128(#[from] parse::leb128::Error),
|
||||
}
|
||||
|
||||
impl BloomFilter {
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
if self.num_entries != 0 {
|
||||
leb128::write::unsigned(&mut buf, self.num_entries as u64).unwrap();
|
||||
leb128::write::unsigned(&mut buf, self.num_bits_per_entry as u64).unwrap();
|
||||
leb128::write::unsigned(&mut buf, self.num_probes as u64).unwrap();
|
||||
buf.extend(&self.bits);
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
pub(crate) fn parse(input: parse::Input<'_>) -> parse::ParseResult<'_, Self, ParseError> {
|
||||
if input.is_empty() {
|
||||
Ok((input, Self::default()))
|
||||
// FIXME - we can avoid a result here - why do we need to consume the bloom filter? requires
|
||||
// me to clone in places I shouldn't need to
|
||||
pub fn into_bytes(self) -> Result<Vec<u8>, encoding::Error> {
|
||||
if self.num_entries == 0 {
|
||||
Ok(Vec::new())
|
||||
} else {
|
||||
let (i, num_entries) = parse::leb128_u32(input)?;
|
||||
let (i, num_bits_per_entry) = parse::leb128_u32(i)?;
|
||||
let (i, num_probes) = parse::leb128_u32(i)?;
|
||||
let (i, bits) = parse::take_n(bits_capacity(num_entries, num_bits_per_entry), i)?;
|
||||
Ok((
|
||||
i,
|
||||
Self {
|
||||
num_entries,
|
||||
num_bits_per_entry,
|
||||
num_probes,
|
||||
bits: bits.to_vec(),
|
||||
},
|
||||
))
|
||||
let mut buf = Vec::new();
|
||||
self.num_entries.encode(&mut buf)?;
|
||||
self.num_bits_per_entry.encode(&mut buf)?;
|
||||
self.num_probes.encode(&mut buf)?;
|
||||
buf.extend(self.bits);
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,8 +45,7 @@ impl BloomFilter {
|
|||
let z = u32::from_le_bytes([hash_bytes[8], hash_bytes[9], hash_bytes[10], hash_bytes[11]])
|
||||
% modulo;
|
||||
|
||||
let mut probes = Vec::with_capacity(self.num_probes as usize);
|
||||
probes.push(x);
|
||||
let mut probes = vec![x];
|
||||
for _ in 1..self.num_probes {
|
||||
x = (x + y) % modulo;
|
||||
y = (y + z) % modulo;
|
||||
|
@ -121,23 +86,6 @@ impl BloomFilter {
|
|||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_hashes<H: Borrow<ChangeHash>>(hashes: impl ExactSizeIterator<Item = H>) -> Self {
|
||||
let num_entries = hashes.len() as u32;
|
||||
let num_bits_per_entry = BITS_PER_ENTRY;
|
||||
let num_probes = NUM_PROBES;
|
||||
let bits = vec![0; bits_capacity(num_entries, num_bits_per_entry)];
|
||||
let mut filter = Self {
|
||||
num_entries,
|
||||
num_bits_per_entry,
|
||||
num_probes,
|
||||
bits,
|
||||
};
|
||||
for hash in hashes {
|
||||
filter.add_hash(hash.borrow());
|
||||
}
|
||||
filter
|
||||
}
|
||||
}
|
||||
|
||||
fn bits_capacity(num_entries: u32, num_bits_per_entry: u32) -> usize {
|
||||
|
@ -145,16 +93,44 @@ fn bits_capacity(num_entries: u32, num_bits_per_entry: u32) -> usize {
|
|||
f as usize
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[error("{0}")]
|
||||
pub struct DecodeError(String);
|
||||
|
||||
impl TryFrom<&[u8]> for BloomFilter {
|
||||
type Error = DecodeError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
Self::parse(parse::Input::new(bytes))
|
||||
.map(|(_, b)| b)
|
||||
.map_err(|e| DecodeError(e.to_string()))
|
||||
impl From<&[ChangeHash]> for BloomFilter {
|
||||
fn from(hashes: &[ChangeHash]) -> Self {
|
||||
let num_entries = hashes.len() as u32;
|
||||
let num_bits_per_entry = BITS_PER_ENTRY;
|
||||
let num_probes = NUM_PROBES;
|
||||
let bits = vec![0; bits_capacity(num_entries, num_bits_per_entry) as usize];
|
||||
let mut filter = Self {
|
||||
num_entries,
|
||||
num_bits_per_entry,
|
||||
num_probes,
|
||||
bits,
|
||||
};
|
||||
for hash in hashes {
|
||||
filter.add_hash(hash);
|
||||
}
|
||||
filter
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for BloomFilter {
|
||||
type Error = decoding::Error;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
if bytes.is_empty() {
|
||||
Ok(Self::default())
|
||||
} else {
|
||||
let mut decoder = Decoder::new(Cow::Borrowed(bytes));
|
||||
let num_entries = decoder.read()?;
|
||||
let num_bits_per_entry = decoder.read()?;
|
||||
let num_probes = decoder.read()?;
|
||||
let bits =
|
||||
decoder.read_bytes(bits_capacity(num_entries, num_bits_per_entry) as usize)?;
|
||||
Ok(Self {
|
||||
num_entries,
|
||||
num_bits_per_entry,
|
||||
num_probes,
|
||||
bits: bits.to_vec(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
65
automerge/src/sync/state.rs
Normal file
65
automerge/src/sync/state.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
use std::{borrow::Cow, collections::HashSet};
|
||||
|
||||
use super::{decode_hashes, encode_hashes};
|
||||
use crate::{decoding, decoding::Decoder, encoding, BloomFilter, ChangeHash};
|
||||
|
||||
const SYNC_STATE_TYPE: u8 = 0x43; // first byte of an encoded sync state, for identification
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SyncState {
|
||||
pub shared_heads: Vec<ChangeHash>,
|
||||
pub last_sent_heads: Option<Vec<ChangeHash>>,
|
||||
pub their_heads: Option<Vec<ChangeHash>>,
|
||||
pub their_need: Option<Vec<ChangeHash>>,
|
||||
pub their_have: Option<Vec<SyncHave>>,
|
||||
pub sent_hashes: HashSet<ChangeHash>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SyncHave {
|
||||
pub last_sync: Vec<ChangeHash>,
|
||||
pub bloom: BloomFilter,
|
||||
}
|
||||
|
||||
impl SyncState {
|
||||
pub fn encode(&self) -> Result<Vec<u8>, encoding::Error> {
|
||||
let mut buf = vec![SYNC_STATE_TYPE];
|
||||
encode_hashes(&mut buf, &self.shared_heads)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
pub fn decode(bytes: &[u8]) -> Result<Self, decoding::Error> {
|
||||
let mut decoder = Decoder::new(Cow::Borrowed(bytes));
|
||||
|
||||
let record_type = decoder.read::<u8>()?;
|
||||
if record_type != SYNC_STATE_TYPE {
|
||||
return Err(decoding::Error::WrongType {
|
||||
expected_one_of: vec![SYNC_STATE_TYPE],
|
||||
found: record_type,
|
||||
});
|
||||
}
|
||||
|
||||
let shared_heads = decode_hashes(&mut decoder)?;
|
||||
Ok(Self {
|
||||
shared_heads,
|
||||
last_sent_heads: Some(Vec::new()),
|
||||
their_heads: None,
|
||||
their_need: None,
|
||||
their_have: Some(Vec::new()),
|
||||
sent_hashes: HashSet::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SyncState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
shared_heads: Vec::new(),
|
||||
last_sent_heads: Some(Vec::new()),
|
||||
their_heads: None,
|
||||
their_need: None,
|
||||
their_have: None,
|
||||
sent_hashes: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
425
automerge/src/types.rs
Normal file
425
automerge/src/types.rs
Normal file
|
@ -0,0 +1,425 @@
|
|||
use crate::error;
|
||||
use crate::legacy as amp;
|
||||
use crate::{ Value, ScalarValue };
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::Eq;
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::TryInto;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use tinyvec::{ArrayVec, TinyVec};
|
||||
|
||||
pub(crate) const HEAD: ElemId = ElemId(OpId(0, 0));
|
||||
pub(crate) const ROOT: OpId = OpId(0, 0);
|
||||
|
||||
const ROOT_STR: &str = "_root";
|
||||
const HEAD_STR: &str = "_head";
|
||||
|
||||
/// An actor id is a sequence of bytes. By default we use a uuid which can be nicely stack
|
||||
/// allocated.
|
||||
///
|
||||
/// In the event that users want to use their own type of identifier that is longer than a uuid
|
||||
/// then they will likely end up pushing it onto the heap which is still fine.
|
||||
///
|
||||
// Note that change encoding relies on the Ord implementation for the ActorId being implemented in
|
||||
// terms of the lexicographic ordering of the underlying bytes. Be aware of this if you are
|
||||
// changing the ActorId implementation in ways which might affect the Ord implementation
|
||||
#[derive(Eq, PartialEq, Hash, Clone, PartialOrd, Ord)]
|
||||
#[cfg_attr(feature = "derive-arbitrary", derive(arbitrary::Arbitrary))]
|
||||
pub struct ActorId(TinyVec<[u8; 16]>);
|
||||
|
||||
impl fmt::Debug for ActorId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("ActorID")
|
||||
.field(&hex::encode(&self.0))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActorId {
|
||||
pub fn random() -> ActorId {
|
||||
ActorId(TinyVec::from(*uuid::Uuid::new_v4().as_bytes()))
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn to_hex_string(&self) -> String {
|
||||
hex::encode(&self.0)
|
||||
}
|
||||
|
||||
pub fn op_id_at(&self, seq: u64) -> amp::OpId {
|
||||
amp::OpId(seq, self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ActorId {
|
||||
type Error = error::InvalidActorId;
|
||||
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
hex::decode(s)
|
||||
.map(ActorId::from)
|
||||
.map_err(|_| error::InvalidActorId(s.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<uuid::Uuid> for ActorId {
|
||||
fn from(u: uuid::Uuid) -> Self {
|
||||
ActorId(TinyVec::from(*u.as_bytes()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[u8]> for ActorId {
|
||||
fn from(b: &[u8]) -> Self {
|
||||
ActorId(TinyVec::from(b))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Vec<u8>> for ActorId {
|
||||
fn from(b: &Vec<u8>) -> Self {
|
||||
ActorId::from(b.as_slice())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for ActorId {
|
||||
fn from(b: Vec<u8>) -> Self {
|
||||
let inner = if let Ok(arr) = ArrayVec::try_from(b.as_slice()) {
|
||||
TinyVec::Inline(arr)
|
||||
} else {
|
||||
TinyVec::Heap(b)
|
||||
};
|
||||
ActorId(inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ActorId {
|
||||
type Err = error::InvalidActorId;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
ActorId::try_from(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ActorId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.to_hex_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Copy, Hash)]
|
||||
#[serde(rename_all = "camelCase", untagged)]
|
||||
pub enum ObjType {
|
||||
Map,
|
||||
Table,
|
||||
List,
|
||||
Text,
|
||||
}
|
||||
|
||||
impl ObjType {
|
||||
pub fn is_sequence(&self) -> bool {
|
||||
matches!(self, Self::List | Self::Text)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<amp::MapType> for ObjType {
|
||||
fn from(other: amp::MapType) -> Self {
|
||||
match other {
|
||||
amp::MapType::Map => Self::Map,
|
||||
amp::MapType::Table => Self::Table,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<amp::SequenceType> for ObjType {
|
||||
fn from(other: amp::SequenceType) -> Self {
|
||||
match other {
|
||||
amp::SequenceType::List => Self::List,
|
||||
amp::SequenceType::Text => Self::Text,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ObjType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ObjType::Map => write!(f, "map"),
|
||||
ObjType::Table => write!(f, "table"),
|
||||
ObjType::List => write!(f, "list"),
|
||||
ObjType::Text => write!(f, "text"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub enum OpType {
|
||||
Make(ObjType),
|
||||
/// Perform a deletion, expanding the operation to cover `n` deletions (multiOp).
|
||||
Del,
|
||||
Inc(i64),
|
||||
Set(ScalarValue),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Export {
|
||||
Id(OpId),
|
||||
Special(String),
|
||||
Prop(usize),
|
||||
}
|
||||
|
||||
pub(crate) trait Exportable {
|
||||
fn export(&self) -> Export;
|
||||
}
|
||||
|
||||
impl OpId {
|
||||
#[inline]
|
||||
pub fn counter(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
#[inline]
|
||||
pub fn actor(&self) -> usize {
|
||||
self.1
|
||||
}
|
||||
}
|
||||
|
||||
impl Exportable for ObjId {
|
||||
fn export(&self) -> Export {
|
||||
if self.0 == ROOT {
|
||||
Export::Special(ROOT_STR.to_owned())
|
||||
} else {
|
||||
Export::Id(self.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Exportable for &ObjId {
|
||||
fn export(&self) -> Export {
|
||||
if self.0 == ROOT {
|
||||
Export::Special(ROOT_STR.to_owned())
|
||||
} else {
|
||||
Export::Id(self.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Exportable for ElemId {
|
||||
fn export(&self) -> Export {
|
||||
if self == &HEAD {
|
||||
Export::Special(HEAD_STR.to_owned())
|
||||
} else {
|
||||
Export::Id(self.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Exportable for OpId {
|
||||
fn export(&self) -> Export {
|
||||
Export::Id(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Exportable for Key {
|
||||
fn export(&self) -> Export {
|
||||
match self {
|
||||
Key::Map(p) => Export::Prop(*p),
|
||||
Key::Seq(e) => e.export(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OpId> for ObjId {
|
||||
fn from(o: OpId) -> Self {
|
||||
ObjId(o)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OpId> for ElemId {
|
||||
fn from(o: OpId) -> Self {
|
||||
ElemId(o)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Prop {
|
||||
fn from(p: String) -> Self {
|
||||
Prop::Map(p)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&String> for Prop {
|
||||
fn from(p: &String) -> Self {
|
||||
Prop::Map(p.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Prop {
|
||||
fn from(p: &str) -> Self {
|
||||
Prop::Map(p.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usize> for Prop {
|
||||
fn from(index: usize) -> Self {
|
||||
Prop::Seq(index)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Prop {
|
||||
fn from(index: f64) -> Self {
|
||||
Prop::Seq(index as usize)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OpId> for Key {
|
||||
fn from(id: OpId) -> Self {
|
||||
Key::Seq(ElemId(id))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ElemId> for Key {
|
||||
fn from(e: ElemId) -> Self {
|
||||
Key::Seq(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Hash)]
|
||||
pub(crate) enum Key {
|
||||
Map(usize),
|
||||
Seq(ElemId),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Clone)]
|
||||
pub enum Prop {
|
||||
Map(String),
|
||||
Seq(usize),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Clone)]
|
||||
pub struct Patch {}
|
||||
|
||||
impl Key {
|
||||
pub fn elemid(&self) -> Option<ElemId> {
|
||||
match self {
|
||||
Key::Map(_) => None,
|
||||
Key::Seq(id) => Some(*id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialOrd, Ord, Eq, PartialEq, Copy, Hash, Default)]
|
||||
pub(crate) struct OpId(pub u64, pub usize);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialOrd, Eq, PartialEq, Ord, Hash, Default)]
|
||||
pub(crate) struct ObjId(pub OpId);
|
||||
|
||||
impl ObjId {
|
||||
pub fn root() -> Self {
|
||||
ObjId(OpId(0,0))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialOrd, Eq, PartialEq, Ord, Hash, Default)]
|
||||
pub(crate) struct ElemId(pub OpId);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct Op {
|
||||
pub change: usize,
|
||||
pub id: OpId,
|
||||
pub action: OpType,
|
||||
pub obj: ObjId,
|
||||
pub key: Key,
|
||||
pub succ: Vec<OpId>,
|
||||
pub pred: Vec<OpId>,
|
||||
pub insert: bool,
|
||||
}
|
||||
|
||||
impl Op {
|
||||
pub fn is_del(&self) -> bool {
|
||||
matches!(&self.action, OpType::Del)
|
||||
}
|
||||
|
||||
pub fn is_noop(&self, action: &OpType) -> bool {
|
||||
matches!((&self.action, action), (OpType::Set(n), OpType::Set(m)) if n == m)
|
||||
}
|
||||
|
||||
pub fn overwrites(&self, other: &Op) -> bool {
|
||||
self.pred.iter().any(|i| i == &other.id)
|
||||
}
|
||||
|
||||
pub fn elemid(&self) -> Option<ElemId> {
|
||||
if self.insert {
|
||||
Some(ElemId(self.id))
|
||||
} else {
|
||||
self.key.elemid()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn value(&self) -> Value {
|
||||
match &self.action {
|
||||
OpType::Make(obj_type) => Value::Object(*obj_type),
|
||||
OpType::Set(scalar) => Value::Scalar(scalar.clone()),
|
||||
_ => panic!("cant convert op into a value - {:?}", self),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn dump(&self) -> String {
|
||||
match &self.action {
|
||||
OpType::Set(value) if self.insert => format!("i:{}", value),
|
||||
OpType::Set(value) => format!("s:{}", value),
|
||||
OpType::Make(obj) => format!("make{}", obj),
|
||||
OpType::Inc(val) => format!("inc:{}", val),
|
||||
OpType::Del => "del".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Peer {}
|
||||
|
||||
#[derive(Eq, PartialEq, Hash, Clone, PartialOrd, Ord, Copy)]
|
||||
pub struct ChangeHash(pub [u8; 32]);
|
||||
|
||||
impl fmt::Debug for ChangeHash {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("ChangeHash")
|
||||
.field(&hex::encode(&self.0))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ParseChangeHashError {
|
||||
#[error(transparent)]
|
||||
HexDecode(#[from] hex::FromHexError),
|
||||
#[error("incorrect length, change hash should be 32 bytes, got {actual}")]
|
||||
IncorrectLength { actual: usize },
|
||||
}
|
||||
|
||||
impl FromStr for ChangeHash {
|
||||
type Err = ParseChangeHashError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let bytes = hex::decode(s)?;
|
||||
if bytes.len() == 32 {
|
||||
Ok(ChangeHash(bytes.try_into().unwrap()))
|
||||
} else {
|
||||
Err(ParseChangeHashError::IncorrectLength {
|
||||
actual: bytes.len(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for ChangeHash {
|
||||
type Error = error::InvalidChangeHashSlice;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
if bytes.len() != 32 {
|
||||
Err(error::InvalidChangeHashSlice(Vec::from(bytes)))
|
||||
} else {
|
||||
let mut array = [0; 32];
|
||||
array.copy_from_slice(bytes);
|
||||
Ok(ChangeHash(array))
|
||||
}
|
||||
}
|
||||
}
|
295
automerge/src/value.rs
Normal file
295
automerge/src/value.rs
Normal file
|
@ -0,0 +1,295 @@
|
|||
use crate::{error, ObjType, Op, OpId, OpType};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smol_str::SmolStr;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Value {
|
||||
Object(ObjType),
|
||||
Scalar(ScalarValue),
|
||||
}
|
||||
|
||||
impl Value {
|
||||
pub fn to_string(&self) -> Option<String> {
|
||||
match self {
|
||||
Value::Scalar(val) => Some(val.to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map() -> Value {
|
||||
Value::Object(ObjType::Map)
|
||||
}
|
||||
|
||||
pub fn list() -> Value {
|
||||
Value::Object(ObjType::List)
|
||||
}
|
||||
|
||||
pub fn text() -> Value {
|
||||
Value::Object(ObjType::Text)
|
||||
}
|
||||
|
||||
pub fn table() -> Value {
|
||||
Value::Object(ObjType::Table)
|
||||
}
|
||||
|
||||
pub fn str(s: &str) -> Value {
|
||||
Value::Scalar(ScalarValue::Str(s.into()))
|
||||
}
|
||||
|
||||
pub fn int(n: i64) -> Value {
|
||||
Value::Scalar(ScalarValue::Int(n))
|
||||
}
|
||||
|
||||
pub fn uint(n: u64) -> Value {
|
||||
Value::Scalar(ScalarValue::Uint(n))
|
||||
}
|
||||
|
||||
pub fn counter(n: i64) -> Value {
|
||||
Value::Scalar(ScalarValue::Counter(n))
|
||||
}
|
||||
|
||||
pub fn timestamp(n: i64) -> Value {
|
||||
Value::Scalar(ScalarValue::Timestamp(n))
|
||||
}
|
||||
|
||||
pub fn f64(n: f64) -> Value {
|
||||
Value::Scalar(ScalarValue::F64(n))
|
||||
}
|
||||
|
||||
pub fn bytes(b: Vec<u8>) -> Value {
|
||||
Value::Scalar(ScalarValue::Bytes(b))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Value {
|
||||
fn from(s: &str) -> Self {
|
||||
Value::Scalar(s.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Value {
|
||||
fn from(s: String) -> Self {
|
||||
Value::Scalar(ScalarValue::Str(s.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for Value {
|
||||
fn from(n: i64) -> Self {
|
||||
Value::Scalar(ScalarValue::Int(n))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for Value {
|
||||
fn from(n: i32) -> Self {
|
||||
Value::Scalar(ScalarValue::Int(n.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for Value {
|
||||
fn from(n: u64) -> Self {
|
||||
Value::Scalar(ScalarValue::Uint(n))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Value {
|
||||
fn from(v: bool) -> Self {
|
||||
Value::Scalar(ScalarValue::Boolean(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ObjType> for Value {
|
||||
fn from(o: ObjType) -> Self {
|
||||
Value::Object(o)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ScalarValue> for Value {
|
||||
fn from(v: ScalarValue) -> Self {
|
||||
Value::Scalar(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Op> for (Value, OpId) {
|
||||
fn from(op: &Op) -> Self {
|
||||
match &op.action {
|
||||
OpType::Make(obj_type) => (Value::Object(*obj_type), op.id),
|
||||
OpType::Set(scalar) => (Value::Scalar(scalar.clone()), op.id),
|
||||
_ => panic!("cant convert op into a value - {:?}", op),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Op> for (Value, OpId) {
|
||||
fn from(op: Op) -> Self {
|
||||
match &op.action {
|
||||
OpType::Make(obj_type) => (Value::Object(*obj_type), op.id),
|
||||
OpType::Set(scalar) => (Value::Scalar(scalar.clone()), op.id),
|
||||
_ => panic!("cant convert op into a value - {:?}", op),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Value> for OpType {
|
||||
fn from(v: Value) -> Self {
|
||||
match v {
|
||||
Value::Object(o) => OpType::Make(o),
|
||||
Value::Scalar(s) => OpType::Set(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, PartialEq, Debug, Clone, Copy)]
|
||||
pub(crate) enum DataType {
|
||||
#[serde(rename = "counter")]
|
||||
Counter,
|
||||
#[serde(rename = "timestamp")]
|
||||
Timestamp,
|
||||
#[serde(rename = "bytes")]
|
||||
Bytes,
|
||||
#[serde(rename = "uint")]
|
||||
Uint,
|
||||
#[serde(rename = "int")]
|
||||
Int,
|
||||
#[serde(rename = "float64")]
|
||||
F64,
|
||||
#[serde(rename = "undefined")]
|
||||
Undefined,
|
||||
}
|
||||
|
||||
#[derive(Serialize, PartialEq, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum ScalarValue {
|
||||
Bytes(Vec<u8>),
|
||||
Str(SmolStr),
|
||||
Int(i64),
|
||||
Uint(u64),
|
||||
F64(f64),
|
||||
Counter(i64),
|
||||
Timestamp(i64),
|
||||
Boolean(bool),
|
||||
Null,
|
||||
}
|
||||
|
||||
impl ScalarValue {
|
||||
pub(crate) fn as_datatype(
|
||||
&self,
|
||||
datatype: DataType,
|
||||
) -> Result<ScalarValue, error::InvalidScalarValue> {
|
||||
match (datatype, self) {
|
||||
(DataType::Counter, ScalarValue::Int(i)) => Ok(ScalarValue::Counter(*i)),
|
||||
(DataType::Counter, ScalarValue::Uint(u)) => match i64::try_from(*u) {
|
||||
Ok(i) => Ok(ScalarValue::Counter(i)),
|
||||
Err(_) => Err(error::InvalidScalarValue {
|
||||
raw_value: self.clone(),
|
||||
expected: "an integer".to_string(),
|
||||
unexpected: "an integer larger than i64::max_value".to_string(),
|
||||
datatype,
|
||||
}),
|
||||
},
|
||||
(DataType::Bytes, ScalarValue::Bytes(bytes)) => Ok(ScalarValue::Bytes(bytes.clone())),
|
||||
(DataType::Bytes, v) => Err(error::InvalidScalarValue {
|
||||
raw_value: self.clone(),
|
||||
expected: "a vector of bytes".to_string(),
|
||||
unexpected: v.to_string(),
|
||||
datatype,
|
||||
}),
|
||||
(DataType::Counter, v) => Err(error::InvalidScalarValue {
|
||||
raw_value: self.clone(),
|
||||
expected: "an integer".to_string(),
|
||||
unexpected: v.to_string(),
|
||||
datatype,
|
||||
}),
|
||||
(DataType::Timestamp, ScalarValue::Int(i)) => Ok(ScalarValue::Timestamp(*i)),
|
||||
(DataType::Timestamp, ScalarValue::Uint(u)) => match i64::try_from(*u) {
|
||||
Ok(i) => Ok(ScalarValue::Timestamp(i)),
|
||||
Err(_) => Err(error::InvalidScalarValue {
|
||||
raw_value: self.clone(),
|
||||
expected: "an integer".to_string(),
|
||||
unexpected: "an integer larger than i64::max_value".to_string(),
|
||||
datatype,
|
||||
}),
|
||||
},
|
||||
(DataType::Timestamp, v) => Err(error::InvalidScalarValue {
|
||||
raw_value: self.clone(),
|
||||
expected: "an integer".to_string(),
|
||||
unexpected: v.to_string(),
|
||||
datatype,
|
||||
}),
|
||||
(DataType::Int, v) => Ok(ScalarValue::Int(v.to_i64().ok_or(
|
||||
error::InvalidScalarValue {
|
||||
raw_value: self.clone(),
|
||||
expected: "an int".to_string(),
|
||||
unexpected: v.to_string(),
|
||||
datatype,
|
||||
},
|
||||
)?)),
|
||||
(DataType::Uint, v) => Ok(ScalarValue::Uint(v.to_u64().ok_or(
|
||||
error::InvalidScalarValue {
|
||||
raw_value: self.clone(),
|
||||
expected: "a uint".to_string(),
|
||||
unexpected: v.to_string(),
|
||||
datatype,
|
||||
},
|
||||
)?)),
|
||||
(DataType::F64, v) => Ok(ScalarValue::F64(v.to_f64().ok_or(
|
||||
error::InvalidScalarValue {
|
||||
raw_value: self.clone(),
|
||||
expected: "an f64".to_string(),
|
||||
unexpected: v.to_string(),
|
||||
datatype,
|
||||
},
|
||||
)?)),
|
||||
(DataType::Undefined, _) => Ok(self.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an Option containing a `DataType` if
|
||||
/// `self` represents a numerical scalar value
|
||||
/// This is necessary b/c numerical values are not self-describing
|
||||
/// (unlike strings / bytes / etc. )
|
||||
pub(crate) fn as_numerical_datatype(&self) -> Option<DataType> {
|
||||
match self {
|
||||
ScalarValue::Counter(..) => Some(DataType::Counter),
|
||||
ScalarValue::Timestamp(..) => Some(DataType::Timestamp),
|
||||
ScalarValue::Int(..) => Some(DataType::Int),
|
||||
ScalarValue::Uint(..) => Some(DataType::Uint),
|
||||
ScalarValue::F64(..) => Some(DataType::F64),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// If this value can be coerced to an i64, return the i64 value
|
||||
pub fn to_i64(&self) -> Option<i64> {
|
||||
match self {
|
||||
ScalarValue::Int(n) => Some(*n),
|
||||
ScalarValue::Uint(n) => Some(*n as i64),
|
||||
ScalarValue::F64(n) => Some(*n as i64),
|
||||
ScalarValue::Counter(n) => Some(*n),
|
||||
ScalarValue::Timestamp(n) => Some(*n),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_u64(&self) -> Option<u64> {
|
||||
match self {
|
||||
ScalarValue::Int(n) => Some(*n as u64),
|
||||
ScalarValue::Uint(n) => Some(*n),
|
||||
ScalarValue::F64(n) => Some(*n as u64),
|
||||
ScalarValue::Counter(n) => Some(*n as u64),
|
||||
ScalarValue::Timestamp(n) => Some(*n as u64),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_f64(&self) -> Option<f64> {
|
||||
match self {
|
||||
ScalarValue::Int(n) => Some(*n as f64),
|
||||
ScalarValue::Uint(n) => Some(*n as f64),
|
||||
ScalarValue::F64(n) => Some(*n),
|
||||
ScalarValue::Counter(n) => Some(*n as f64),
|
||||
ScalarValue::Timestamp(n) => Some(*n as f64),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
use crate::types::{ObjId, Op};
|
||||
use fxhash::FxHasher;
|
||||
use std::{borrow::Cow, collections::HashMap, hash::BuildHasherDefault};
|
||||
|
||||
|
@ -16,17 +15,17 @@ impl Default for NodeId {
|
|||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Node<'a> {
|
||||
pub(crate) struct Node<'a, const B: usize> {
|
||||
id: NodeId,
|
||||
children: Vec<NodeId>,
|
||||
node_type: NodeType<'a>,
|
||||
node_type: NodeType<'a, B>,
|
||||
metadata: &'a crate::op_set::OpSetMetadata,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum NodeType<'a> {
|
||||
ObjRoot(crate::types::ObjId),
|
||||
ObjTreeNode(ObjId, &'a crate::op_tree::OpTreeNode, &'a [Op]),
|
||||
pub(crate) enum NodeType<'a, const B: usize> {
|
||||
ObjRoot(crate::ObjId),
|
||||
ObjTreeNode(&'a crate::op_tree::OpTreeNode<B>),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -35,30 +34,24 @@ pub(crate) struct Edge {
|
|||
child_id: NodeId,
|
||||
}
|
||||
|
||||
pub(crate) struct GraphVisualisation<'a> {
|
||||
nodes: HashMap<NodeId, Node<'a>>,
|
||||
pub(crate) struct GraphVisualisation<'a, const B: usize> {
|
||||
nodes: HashMap<NodeId, Node<'a, B>>,
|
||||
actor_shorthands: HashMap<usize, String>,
|
||||
}
|
||||
|
||||
impl<'a> GraphVisualisation<'a> {
|
||||
impl<'a, const B: usize> GraphVisualisation<'a, B> {
|
||||
pub(super) fn construct(
|
||||
trees: &'a HashMap<
|
||||
crate::types::ObjId,
|
||||
crate::op_tree::OpTree,
|
||||
crate::op_tree::OpTreeInternal<B>,
|
||||
BuildHasherDefault<FxHasher>,
|
||||
>,
|
||||
metadata: &'a crate::op_set::OpSetMetadata,
|
||||
) -> GraphVisualisation<'a> {
|
||||
) -> GraphVisualisation<'a, B> {
|
||||
let mut nodes = HashMap::new();
|
||||
for (obj_id, tree) in trees {
|
||||
if let Some(root_node) = &tree.internal.root_node {
|
||||
let tree_id = Self::construct_nodes(
|
||||
root_node,
|
||||
&tree.internal.ops,
|
||||
obj_id,
|
||||
&mut nodes,
|
||||
metadata,
|
||||
);
|
||||
if let Some(root_node) = &tree.root_node {
|
||||
let tree_id = Self::construct_nodes(root_node, &mut nodes, metadata);
|
||||
let obj_tree_id = NodeId::default();
|
||||
nodes.insert(
|
||||
obj_tree_id,
|
||||
|
@ -76,22 +69,20 @@ impl<'a> GraphVisualisation<'a> {
|
|||
actor_shorthands.insert(actor, format!("actor{}", actor));
|
||||
}
|
||||
GraphVisualisation {
|
||||
nodes,
|
||||
actor_shorthands,
|
||||
nodes,
|
||||
}
|
||||
}
|
||||
|
||||
fn construct_nodes(
|
||||
node: &'a crate::op_tree::OpTreeNode,
|
||||
ops: &'a [Op],
|
||||
objid: &ObjId,
|
||||
nodes: &mut HashMap<NodeId, Node<'a>>,
|
||||
node: &'a crate::op_tree::OpTreeNode<B>,
|
||||
nodes: &mut HashMap<NodeId, Node<'a, B>>,
|
||||
m: &'a crate::op_set::OpSetMetadata,
|
||||
) -> NodeId {
|
||||
let node_id = NodeId::default();
|
||||
let mut child_ids = Vec::new();
|
||||
for child in &node.children {
|
||||
let child_id = Self::construct_nodes(child, ops, objid, nodes, m);
|
||||
let child_id = Self::construct_nodes(child, nodes, m);
|
||||
child_ids.push(child_id);
|
||||
}
|
||||
nodes.insert(
|
||||
|
@ -99,7 +90,7 @@ impl<'a> GraphVisualisation<'a> {
|
|||
Node {
|
||||
id: node_id,
|
||||
children: child_ids,
|
||||
node_type: NodeType::ObjTreeNode(*objid, node, ops),
|
||||
node_type: NodeType::ObjTreeNode(node),
|
||||
metadata: m,
|
||||
},
|
||||
);
|
||||
|
@ -107,8 +98,8 @@ impl<'a> GraphVisualisation<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> dot::GraphWalk<'a, &'a Node<'a>, Edge> for GraphVisualisation<'a> {
|
||||
fn nodes(&'a self) -> dot::Nodes<'a, &'a Node<'a>> {
|
||||
impl<'a, const B: usize> dot::GraphWalk<'a, &'a Node<'a, B>, Edge> for GraphVisualisation<'a, B> {
|
||||
fn nodes(&'a self) -> dot::Nodes<'a, &'a Node<'a, B>> {
|
||||
Cow::Owned(self.nodes.values().collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
|
@ -125,36 +116,36 @@ impl<'a> dot::GraphWalk<'a, &'a Node<'a>, Edge> for GraphVisualisation<'a> {
|
|||
Cow::Owned(edges)
|
||||
}
|
||||
|
||||
fn source(&'a self, edge: &Edge) -> &'a Node<'a> {
|
||||
fn source(&'a self, edge: &Edge) -> &'a Node<'a, B> {
|
||||
self.nodes.get(&edge.parent_id).unwrap()
|
||||
}
|
||||
|
||||
fn target(&'a self, edge: &Edge) -> &'a Node<'a> {
|
||||
fn target(&'a self, edge: &Edge) -> &'a Node<'a, B> {
|
||||
self.nodes.get(&edge.child_id).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> dot::Labeller<'a, &'a Node<'a>, Edge> for GraphVisualisation<'a> {
|
||||
impl<'a, const B: usize> dot::Labeller<'a, &'a Node<'a, B>, Edge> for GraphVisualisation<'a, B> {
|
||||
fn graph_id(&'a self) -> dot::Id<'a> {
|
||||
dot::Id::new("OpSet").unwrap()
|
||||
}
|
||||
|
||||
fn node_id(&'a self, n: &&Node<'a>) -> dot::Id<'a> {
|
||||
dot::Id::new(format!("node_{}", n.id.0)).unwrap()
|
||||
fn node_id(&'a self, n: &&Node<'a, B>) -> dot::Id<'a> {
|
||||
dot::Id::new(format!("node_{}", n.id.0.to_string())).unwrap()
|
||||
}
|
||||
|
||||
fn node_shape(&'a self, node: &&'a Node<'a>) -> Option<dot::LabelText<'a>> {
|
||||
fn node_shape(&'a self, node: &&'a Node<'a, B>) -> Option<dot::LabelText<'a>> {
|
||||
let shape = match node.node_type {
|
||||
NodeType::ObjTreeNode(_, _, _) => dot::LabelText::label("none"),
|
||||
NodeType::ObjTreeNode(_) => dot::LabelText::label("none"),
|
||||
NodeType::ObjRoot(_) => dot::LabelText::label("ellipse"),
|
||||
};
|
||||
Some(shape)
|
||||
}
|
||||
|
||||
fn node_label(&'a self, n: &&Node<'a>) -> dot::LabelText<'a> {
|
||||
fn node_label(&'a self, n: &&Node<'a, B>) -> dot::LabelText<'a> {
|
||||
match n.node_type {
|
||||
NodeType::ObjTreeNode(objid, tree_node, ops) => dot::LabelText::HtmlStr(
|
||||
OpTable::create(tree_node, ops, &objid, n.metadata, &self.actor_shorthands)
|
||||
NodeType::ObjTreeNode(tree_node) => dot::LabelText::HtmlStr(
|
||||
OpTable::create(tree_node, n.metadata, &self.actor_shorthands)
|
||||
.to_html()
|
||||
.into(),
|
||||
),
|
||||
|
@ -170,17 +161,15 @@ struct OpTable {
|
|||
}
|
||||
|
||||
impl OpTable {
|
||||
fn create<'a>(
|
||||
node: &'a crate::op_tree::OpTreeNode,
|
||||
ops: &'a [Op],
|
||||
obj: &ObjId,
|
||||
fn create<'a, const B: usize>(
|
||||
node: &'a crate::op_tree::OpTreeNode<B>,
|
||||
metadata: &crate::op_set::OpSetMetadata,
|
||||
actor_shorthands: &HashMap<usize, String>,
|
||||
) -> Self {
|
||||
let rows = node
|
||||
.elements
|
||||
.iter()
|
||||
.map(|e| OpTableRow::create(&ops[*e], obj, metadata, actor_shorthands))
|
||||
.map(|e| OpTableRow::create(e, metadata, actor_shorthands))
|
||||
.collect();
|
||||
OpTable { rows }
|
||||
}
|
||||
|
@ -200,7 +189,6 @@ impl OpTable {
|
|||
<td>prop</td>\
|
||||
<td>action</td>\
|
||||
<td>succ</td>\
|
||||
<td>pred</td>\
|
||||
</tr>\
|
||||
<hr/>\
|
||||
{}\
|
||||
|
@ -216,7 +204,6 @@ struct OpTableRow {
|
|||
prop: String,
|
||||
op_description: String,
|
||||
succ: String,
|
||||
pred: String,
|
||||
}
|
||||
|
||||
impl OpTableRow {
|
||||
|
@ -227,7 +214,6 @@ impl OpTableRow {
|
|||
&self.prop,
|
||||
&self.op_description,
|
||||
&self.succ,
|
||||
&self.pred,
|
||||
];
|
||||
let row = rows
|
||||
.iter()
|
||||
|
@ -239,42 +225,35 @@ impl OpTableRow {
|
|||
|
||||
impl OpTableRow {
|
||||
fn create(
|
||||
op: &super::types::Op,
|
||||
obj: &ObjId,
|
||||
op: &super::Op,
|
||||
metadata: &crate::op_set::OpSetMetadata,
|
||||
actor_shorthands: &HashMap<usize, String>,
|
||||
) -> Self {
|
||||
let op_description = match &op.action {
|
||||
crate::OpType::Delete => "del".to_string(),
|
||||
crate::OpType::Put(v) => format!("set {}", v),
|
||||
crate::OpType::Del => "del".to_string(),
|
||||
crate::OpType::Set(v) => format!("set {}", v),
|
||||
crate::OpType::Make(obj) => format!("make {}", obj),
|
||||
crate::OpType::Increment(v) => format!("inc {}", v),
|
||||
crate::OpType::Inc(v) => format!("inc {}", v),
|
||||
};
|
||||
let prop = match op.key {
|
||||
crate::types::Key::Map(k) => metadata.props[k].clone(),
|
||||
crate::types::Key::Seq(e) => print_opid(&e.0, actor_shorthands),
|
||||
crate::Key::Map(k) => metadata.props[k].clone(),
|
||||
crate::Key::Seq(e) => print_opid(&e.0, actor_shorthands),
|
||||
};
|
||||
let succ = op
|
||||
.succ
|
||||
.iter()
|
||||
.map(|s| format!(",{}", print_opid(s, actor_shorthands)))
|
||||
.collect();
|
||||
let pred = op
|
||||
.pred
|
||||
.iter()
|
||||
.map(|s| format!(",{}", print_opid(s, actor_shorthands)))
|
||||
.collect();
|
||||
OpTableRow {
|
||||
op_description,
|
||||
obj_id: print_opid(&obj.0, actor_shorthands),
|
||||
obj_id: print_opid(&op.obj.0, actor_shorthands),
|
||||
op_id: print_opid(&op.id, actor_shorthands),
|
||||
prop,
|
||||
succ,
|
||||
pred,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_opid(opid: &crate::types::OpId, actor_shorthands: &HashMap<usize, String>) -> String {
|
||||
fn print_opid(opid: &crate::OpId, actor_shorthands: &HashMap<usize, String>) -> String {
|
||||
format!("{}@{}", opid.counter(), actor_shorthands[&opid.actor()])
|
||||
}
|
504
automerge/tests/helpers/mod.rs
Normal file
504
automerge/tests/helpers/mod.rs
Normal file
|
@ -0,0 +1,504 @@
|
|||
use std::{collections::HashMap, convert::TryInto, hash::Hash};
|
||||
|
||||
use serde::ser::{SerializeMap, SerializeSeq};
|
||||
|
||||
pub fn new_doc() -> automerge::Automerge {
|
||||
automerge::Automerge::new_with_actor_id(automerge::ActorId::random())
|
||||
}
|
||||
|
||||
pub fn new_doc_with_actor(actor: automerge::ActorId) -> automerge::Automerge {
|
||||
automerge::Automerge::new_with_actor_id(actor)
|
||||
}
|
||||
|
||||
/// Returns two actor IDs, the first considered to be ordered before the second
|
||||
pub fn sorted_actors() -> (automerge::ActorId, automerge::ActorId) {
|
||||
let a = automerge::ActorId::random();
|
||||
let b = automerge::ActorId::random();
|
||||
if a > b {
|
||||
(b, a)
|
||||
} else {
|
||||
(a, b)
|
||||
}
|
||||
}
|
||||
|
||||
/// This macro makes it easy to make assertions about a document. It is called with two arguments,
|
||||
/// the first is a reference to an `automerge::Automerge`, the second is an instance of
|
||||
/// `RealizedObject<ExportableOpId>`.
|
||||
///
|
||||
/// What - I hear you ask - is a `RealizedObject`? It's a fully hydrated version of the contents of
|
||||
/// an automerge document. You don't need to think about this too much though because you can
|
||||
/// easily construct one with the `map!` and `list!` macros. Here's an example:
|
||||
///
|
||||
/// ## Constructing documents
|
||||
///
|
||||
/// ```rust
|
||||
/// let mut doc = automerge::Automerge::new();
|
||||
/// let todos = doc.set(automerge::ROOT, "todos", automerge::Value::map()).unwrap().unwrap();
|
||||
/// let todo = doc.insert(todos, 0, automerge::Value::map()).unwrap();
|
||||
/// let title = doc.set(todo, "title", "water plants").unwrap().unwrap();
|
||||
///
|
||||
/// assert_doc!(
|
||||
/// &doc,
|
||||
/// map!{
|
||||
/// "todos" => {
|
||||
/// todos => list![
|
||||
/// { todo => map!{ title = "water plants" } }
|
||||
/// ]
|
||||
/// }
|
||||
/// }
|
||||
/// );
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// This might look more complicated than you were expecting. Why are there OpIds (`todos`, `todo`,
|
||||
/// `title`) in there? Well the `RealizedObject` contains all the changes in the document tagged by
|
||||
/// OpId. This makes it easy to test for conflicts:
|
||||
///
|
||||
/// ```rust
|
||||
/// let mut doc1 = automerge::Automerge::new();
|
||||
/// let mut doc2 = automerge::Automerge::new();
|
||||
/// let op1 = doc1.set(automerge::ROOT, "field", "one").unwrap().unwrap();
|
||||
/// let op2 = doc2.set(automerge::ROOT, "field", "two").unwrap().unwrap();
|
||||
/// doc1.merge(&mut doc2);
|
||||
/// assert_doc!(
|
||||
/// &doc1,
|
||||
/// map!{
|
||||
/// "field" => {
|
||||
/// op1 => "one",
|
||||
/// op2.translate(&doc2) => "two"
|
||||
/// }
|
||||
/// }
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// ## Translating OpIds
|
||||
///
|
||||
/// One thing you may have noticed in the example above is the `op2.translate(&doc2)` call. What is
|
||||
/// that doing there? Well, the problem is that automerge OpIDs (in the current API) are specific
|
||||
/// to a document. Using an opid from one document in a different document will not work. Therefore
|
||||
/// this module defines an `OpIdExt` trait with a `translate` method on it. This method takes a
|
||||
/// document and converts the opid into something which knows how to be compared with opids from
|
||||
/// another document by using the document you pass to `translate`. Again, all you really need to
|
||||
/// know is that when constructing a document for comparison you should call `translate(fromdoc)`
|
||||
/// on opids which come from a document other than the one you pass to `assert_doc`.
|
||||
#[macro_export]
|
||||
macro_rules! assert_doc {
|
||||
($doc: expr, $expected: expr) => {{
|
||||
use $crate::helpers::{realize, ExportableOpId};
|
||||
let realized = realize($doc);
|
||||
let to_export: RealizedObject<ExportableOpId<'_>> = $expected.into();
|
||||
let exported = to_export.export($doc);
|
||||
if realized != exported {
|
||||
let serde_right = serde_json::to_string_pretty(&realized).unwrap();
|
||||
let serde_left = serde_json::to_string_pretty(&exported).unwrap();
|
||||
panic!(
|
||||
"documents didn't match\n expected\n{}\n got\n{}",
|
||||
&serde_left, &serde_right
|
||||
);
|
||||
}
|
||||
pretty_assertions::assert_eq!(realized, exported);
|
||||
}};
|
||||
}
|
||||
|
||||
/// Like `assert_doc` except that you can specify an object ID and property to select subsections
|
||||
/// of the document.
|
||||
#[macro_export]
|
||||
macro_rules! assert_obj {
|
||||
($doc: expr, $obj_id: expr, $prop: expr, $expected: expr) => {{
|
||||
use $crate::helpers::{realize_prop, ExportableOpId};
|
||||
let realized = realize_prop($doc, $obj_id, $prop);
|
||||
let to_export: RealizedObject<ExportableOpId<'_>> = $expected.into();
|
||||
let exported = to_export.export($doc);
|
||||
if realized != exported {
|
||||
let serde_right = serde_json::to_string_pretty(&realized).unwrap();
|
||||
let serde_left = serde_json::to_string_pretty(&exported).unwrap();
|
||||
panic!(
|
||||
"documents didn't match\n expected\n{}\n got\n{}",
|
||||
&serde_left, &serde_right
|
||||
);
|
||||
}
|
||||
pretty_assertions::assert_eq!(realized, exported);
|
||||
}};
|
||||
}
|
||||
|
||||
/// Construct `RealizedObject::Map`. This macro takes a nested set of curl braces. The outer set is
|
||||
/// the keys of the map, the inner set is the opid tagged values:
|
||||
///
|
||||
/// ```
|
||||
/// map!{
|
||||
/// "key" => {
|
||||
/// opid1 => "value1",
|
||||
/// opid2 => "value2",
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The map above would represent a map with a conflict on the "key" property. The values can be
|
||||
/// anything which implements `Into<RealizedObject<ExportableOpId<'_>>`. Including nested calls to
|
||||
/// `map!` or `list!`.
|
||||
#[macro_export]
|
||||
macro_rules! map {
|
||||
(@single $($x:tt)*) => (());
|
||||
(@count $($rest:expr),*) => (<[()]>::len(&[$(map!(@single $rest)),*]));
|
||||
|
||||
(@inner { $($opid:expr => $value:expr,)+ }) => { map!(@inner { $($opid => $value),+ }) };
|
||||
(@inner { $($opid:expr => $value:expr),* }) => {
|
||||
{
|
||||
use std::collections::HashMap;
|
||||
let mut inner: HashMap<ExportableOpId<'_>, RealizedObject<ExportableOpId<'_>>> = HashMap::new();
|
||||
$(
|
||||
let _ = inner.insert($opid.into(), $value.into());
|
||||
)*
|
||||
inner
|
||||
}
|
||||
};
|
||||
//(&inner $map:expr, $opid:expr => $value:expr, $($tail:tt),*) => {
|
||||
//$map.insert($opid.into(), $value.into());
|
||||
//}
|
||||
($($key:expr => $inner:tt,)+) => { map!($($key => $inner),+) };
|
||||
($($key:expr => $inner:tt),*) => {
|
||||
{
|
||||
use std::collections::HashMap;
|
||||
use crate::helpers::ExportableOpId;
|
||||
let _cap = map!(@count $($key),*);
|
||||
let mut _map: HashMap<String, HashMap<ExportableOpId<'_>, RealizedObject<ExportableOpId<'_>>>> = ::std::collections::HashMap::with_capacity(_cap);
|
||||
$(
|
||||
let inner = map!(@inner $inner);
|
||||
let _ = _map.insert($key.to_string(), inner);
|
||||
)*
|
||||
RealizedObject::Map(_map)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct `RealizedObject::Sequence`. This macro represents a sequence of opid tagged values
|
||||
///
|
||||
/// ```
|
||||
/// list![
|
||||
/// {
|
||||
/// opid1 => "value1",
|
||||
/// opid2 => "value2",
|
||||
/// }
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// The list above would represent a list with a conflict on the 0 index. The values can be
|
||||
/// anything which implements `Into<RealizedObject<ExportableOpId<'_>>` including nested calls to
|
||||
/// `map!` or `list!`.
|
||||
#[macro_export]
|
||||
macro_rules! list {
|
||||
(@single $($x:tt)*) => (());
|
||||
(@count $($rest:tt),*) => (<[()]>::len(&[$(list!(@single $rest)),*]));
|
||||
|
||||
(@inner { $($opid:expr => $value:expr,)+ }) => { list!(@inner { $($opid => $value),+ }) };
|
||||
(@inner { $($opid:expr => $value:expr),* }) => {
|
||||
{
|
||||
use std::collections::HashMap;
|
||||
let mut inner: HashMap<ExportableOpId<'_>, RealizedObject<ExportableOpId<'_>>> = HashMap::new();
|
||||
$(
|
||||
let _ = inner.insert($opid.into(), $value.into());
|
||||
)*
|
||||
inner
|
||||
}
|
||||
};
|
||||
($($inner:tt,)+) => { list!($($inner),+) };
|
||||
($($inner:tt),*) => {
|
||||
{
|
||||
use crate::helpers::ExportableOpId;
|
||||
let _cap = list!(@count $($inner),*);
|
||||
let mut _list: Vec<HashMap<ExportableOpId<'_>, RealizedObject<ExportableOpId<'_>>>> = Vec::new();
|
||||
$(
|
||||
//println!("{}", stringify!($inner));
|
||||
let inner = list!(@inner $inner);
|
||||
let _ = _list.push(inner);
|
||||
)*
|
||||
RealizedObject::Sequence(_list)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Translate an op ID produced by one document to an op ID which can be understood by
|
||||
/// another
|
||||
///
|
||||
/// The current API of automerge exposes OpIds of the form (u64, usize) where the first component
|
||||
/// is the counter of an actors lamport timestamp and the second component is the index into an
|
||||
/// array of actor IDs stored by the document where the opid was generated. Obviously this is not
|
||||
/// portable between documents as the index of the actor array is unlikely to match between two
|
||||
/// documents. This function translates between the two representations.
|
||||
///
|
||||
/// At some point we will probably change the API to not be document specific but this function
|
||||
/// allows us to write tests first.
|
||||
pub fn translate_obj_id(
|
||||
from: &automerge::Automerge,
|
||||
to: &automerge::Automerge,
|
||||
id: automerge::OpId,
|
||||
) -> automerge::OpId {
|
||||
let exported = from.export(id);
|
||||
to.import(&exported).unwrap()
|
||||
}
|
||||
|
||||
pub fn mk_counter(value: i64) -> automerge::ScalarValue {
|
||||
automerge::ScalarValue::Counter(value)
|
||||
}
|
||||
|
||||
#[derive(Eq, Hash, PartialEq, Debug)]
|
||||
pub struct ExportedOpId(String);
|
||||
|
||||
impl std::fmt::Display for ExportedOpId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A `RealizedObject` is a representation of all the current values in a document - including
|
||||
/// conflicts.
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub enum RealizedObject<Oid: PartialEq + Eq + Hash> {
|
||||
Map(HashMap<String, HashMap<Oid, RealizedObject<Oid>>>),
|
||||
Sequence(Vec<HashMap<Oid, RealizedObject<Oid>>>),
|
||||
Value(automerge::ScalarValue),
|
||||
}
|
||||
|
||||
impl serde::Serialize for RealizedObject<ExportedOpId> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
Self::Map(kvs) => {
|
||||
let mut map_ser = serializer.serialize_map(Some(kvs.len()))?;
|
||||
for (k, kvs) in kvs {
|
||||
let kvs_serded = kvs
|
||||
.iter()
|
||||
.map(|(opid, value)| (opid.to_string(), value))
|
||||
.collect::<HashMap<String, &RealizedObject<ExportedOpId>>>();
|
||||
map_ser.serialize_entry(k, &kvs_serded)?;
|
||||
}
|
||||
map_ser.end()
|
||||
}
|
||||
Self::Sequence(elems) => {
|
||||
let mut list_ser = serializer.serialize_seq(Some(elems.len()))?;
|
||||
for elem in elems {
|
||||
let kvs_serded = elem
|
||||
.iter()
|
||||
.map(|(opid, value)| (opid.to_string(), value))
|
||||
.collect::<HashMap<String, &RealizedObject<ExportedOpId>>>();
|
||||
list_ser.serialize_element(&kvs_serded)?;
|
||||
}
|
||||
list_ser.end()
|
||||
}
|
||||
Self::Value(v) => v.serialize(serializer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn realize(doc: &automerge::Automerge) -> RealizedObject<ExportedOpId> {
|
||||
realize_obj(doc, automerge::ROOT, automerge::ObjType::Map)
|
||||
}
|
||||
|
||||
pub fn realize_prop<P: Into<automerge::Prop>>(
|
||||
doc: &automerge::Automerge,
|
||||
obj_id: automerge::OpId,
|
||||
prop: P,
|
||||
) -> RealizedObject<ExportedOpId> {
|
||||
let (val, obj_id) = doc.value(obj_id, prop).unwrap().unwrap();
|
||||
match val {
|
||||
automerge::Value::Object(obj_type) => realize_obj(doc, obj_id, obj_type),
|
||||
automerge::Value::Scalar(v) => RealizedObject::Value(v),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn realize_obj(
|
||||
doc: &automerge::Automerge,
|
||||
obj_id: automerge::OpId,
|
||||
objtype: automerge::ObjType,
|
||||
) -> RealizedObject<ExportedOpId> {
|
||||
match objtype {
|
||||
automerge::ObjType::Map | automerge::ObjType::Table => {
|
||||
let mut result = HashMap::new();
|
||||
for key in doc.keys(obj_id) {
|
||||
result.insert(key.clone(), realize_values(doc, obj_id, key));
|
||||
}
|
||||
RealizedObject::Map(result)
|
||||
}
|
||||
automerge::ObjType::List | automerge::ObjType::Text => {
|
||||
let length = doc.length(obj_id);
|
||||
let mut result = Vec::with_capacity(length);
|
||||
for i in 0..length {
|
||||
result.push(realize_values(doc, obj_id, i));
|
||||
}
|
||||
RealizedObject::Sequence(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn realize_values<K: Into<automerge::Prop>>(
|
||||
doc: &automerge::Automerge,
|
||||
obj_id: automerge::OpId,
|
||||
key: K,
|
||||
) -> HashMap<ExportedOpId, RealizedObject<ExportedOpId>> {
|
||||
let mut values_by_opid = HashMap::new();
|
||||
for (value, opid) in doc.values(obj_id, key).unwrap() {
|
||||
let realized = match value {
|
||||
automerge::Value::Object(objtype) => realize_obj(doc, opid, objtype),
|
||||
automerge::Value::Scalar(v) => RealizedObject::Value(v),
|
||||
};
|
||||
let exported_opid = ExportedOpId(doc.export(opid));
|
||||
values_by_opid.insert(exported_opid, realized);
|
||||
}
|
||||
values_by_opid
|
||||
}
|
||||
|
||||
impl<'a> RealizedObject<ExportableOpId<'a>> {
|
||||
pub fn export(self, doc: &automerge::Automerge) -> RealizedObject<ExportedOpId> {
|
||||
match self {
|
||||
Self::Map(kvs) => RealizedObject::Map(
|
||||
kvs.into_iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k,
|
||||
v.into_iter()
|
||||
.map(|(k, v)| (k.export(doc), v.export(doc)))
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
Self::Sequence(values) => RealizedObject::Sequence(
|
||||
values
|
||||
.into_iter()
|
||||
.map(|v| {
|
||||
v.into_iter()
|
||||
.map(|(k, v)| (k.export(doc), v.export(doc)))
|
||||
.collect()
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
Self::Value(v) => RealizedObject::Value(v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, O: Into<ExportableOpId<'a>>, I: Into<RealizedObject<ExportableOpId<'a>>>>
|
||||
From<HashMap<&str, HashMap<O, I>>> for RealizedObject<ExportableOpId<'a>>
|
||||
{
|
||||
fn from(values: HashMap<&str, HashMap<O, I>>) -> Self {
|
||||
let intoed = values
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k.to_string(),
|
||||
v.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
RealizedObject::Map(intoed)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, O: Into<ExportableOpId<'a>>, I: Into<RealizedObject<ExportableOpId<'a>>>>
|
||||
From<Vec<HashMap<O, I>>> for RealizedObject<ExportableOpId<'a>>
|
||||
{
|
||||
fn from(values: Vec<HashMap<O, I>>) -> Self {
|
||||
RealizedObject::Sequence(
|
||||
values
|
||||
.into_iter()
|
||||
.map(|v| v.into_iter().map(|(k, v)| (k.into(), v.into())).collect())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for RealizedObject<ExportableOpId<'_>> {
|
||||
fn from(b: bool) -> Self {
|
||||
RealizedObject::Value(b.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usize> for RealizedObject<ExportableOpId<'_>> {
|
||||
fn from(u: usize) -> Self {
|
||||
let v = u.try_into().unwrap();
|
||||
RealizedObject::Value(automerge::ScalarValue::Int(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<automerge::ScalarValue> for RealizedObject<ExportableOpId<'_>> {
|
||||
fn from(s: automerge::ScalarValue) -> Self {
|
||||
RealizedObject::Value(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for RealizedObject<ExportableOpId<'_>> {
|
||||
fn from(s: &str) -> Self {
|
||||
RealizedObject::Value(automerge::ScalarValue::Str(s.into()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Hash)]
|
||||
pub enum ExportableOpId<'a> {
|
||||
Native(automerge::OpId),
|
||||
Translate(Translate<'a>),
|
||||
}
|
||||
|
||||
impl<'a> ExportableOpId<'a> {
|
||||
fn export(self, doc: &automerge::Automerge) -> ExportedOpId {
|
||||
let oid = match self {
|
||||
Self::Native(oid) => oid,
|
||||
Self::Translate(Translate { from, opid }) => translate_obj_id(from, doc, opid),
|
||||
};
|
||||
ExportedOpId(doc.export(oid))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Translate<'a> {
|
||||
from: &'a automerge::Automerge,
|
||||
opid: automerge::OpId,
|
||||
}
|
||||
|
||||
impl<'a> PartialEq for Translate<'a> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.from.maybe_get_actor().unwrap() == other.from.maybe_get_actor().unwrap()
|
||||
&& self.opid == other.opid
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Eq for Translate<'a> {}
|
||||
|
||||
impl<'a> Hash for Translate<'a> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.from.maybe_get_actor().unwrap().hash(state);
|
||||
self.opid.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait OpIdExt {
|
||||
fn native(self) -> ExportableOpId<'static>;
|
||||
fn translate(self, doc: &automerge::Automerge) -> ExportableOpId<'_>;
|
||||
}
|
||||
|
||||
impl OpIdExt for automerge::OpId {
|
||||
/// Use this opid directly when exporting
|
||||
fn native(self) -> ExportableOpId<'static> {
|
||||
ExportableOpId::Native(self)
|
||||
}
|
||||
|
||||
/// Translate this OpID from `doc` when exporting
|
||||
fn translate(self, doc: &automerge::Automerge) -> ExportableOpId<'_> {
|
||||
ExportableOpId::Translate(Translate {
|
||||
from: doc,
|
||||
opid: self,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<automerge::OpId> for ExportableOpId<'_> {
|
||||
fn from(oid: automerge::OpId) -> Self {
|
||||
ExportableOpId::Native(oid)
|
||||
}
|
||||
}
|
||||
|
||||
/// Pretty print the contents of a document
|
||||
#[allow(dead_code)]
|
||||
pub fn pretty_print(doc: &automerge::Automerge) {
|
||||
println!("{}", serde_json::to_string_pretty(&realize(doc)).unwrap())
|
||||
}
|
943
automerge/tests/test.rs
Normal file
943
automerge/tests/test.rs
Normal file
|
@ -0,0 +1,943 @@
|
|||
use automerge::Automerge;
|
||||
|
||||
mod helpers;
|
||||
#[allow(unused_imports)]
|
||||
use helpers::{
|
||||
mk_counter, new_doc, new_doc_with_actor, pretty_print, realize, realize_obj, sorted_actors,
|
||||
translate_obj_id, OpIdExt, RealizedObject,
|
||||
};
|
||||
#[test]
|
||||
fn no_conflict_on_repeated_assignment() {
|
||||
let mut doc = Automerge::new();
|
||||
doc.set(automerge::ROOT, "foo", 1).unwrap();
|
||||
let op = doc.set(automerge::ROOT, "foo", 2).unwrap().unwrap();
|
||||
assert_doc!(
|
||||
&doc,
|
||||
map! {
|
||||
"foo" => { op => 2},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_change_on_repeated_map_set() {
|
||||
let mut doc = new_doc();
|
||||
doc.set(automerge::ROOT, "foo", 1).unwrap();
|
||||
assert!(doc.set(automerge::ROOT, "foo", 1).unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_change_on_repeated_list_set() {
|
||||
let mut doc = new_doc();
|
||||
let list_id = doc
|
||||
.set(automerge::ROOT, "list", automerge::Value::list())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
doc.insert(list_id, 0, 1).unwrap();
|
||||
doc.set(list_id, 0, 1).unwrap();
|
||||
assert!(doc.set(list_id, 0, 1).unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_change_on_list_insert_followed_by_set_of_same_value() {
|
||||
let mut doc = new_doc();
|
||||
let list_id = doc
|
||||
.set(automerge::ROOT, "list", automerge::Value::list())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
doc.insert(list_id, 0, 1).unwrap();
|
||||
assert!(doc.set(list_id, 0, 1).unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_map_assignment_which_resolves_conflict_not_ignored() {
|
||||
let mut doc1 = new_doc();
|
||||
let mut doc2 = new_doc();
|
||||
doc1.set(automerge::ROOT, "field", 123).unwrap();
|
||||
doc2.merge(&mut doc1);
|
||||
doc2.set(automerge::ROOT, "field", 456).unwrap();
|
||||
doc1.set(automerge::ROOT, "field", 789).unwrap();
|
||||
doc1.merge(&mut doc2);
|
||||
assert_eq!(doc1.values(automerge::ROOT, "field").unwrap().len(), 2);
|
||||
|
||||
let op = doc1.set(automerge::ROOT, "field", 123).unwrap().unwrap();
|
||||
assert_doc!(
|
||||
&doc1,
|
||||
map! {
|
||||
"field" => {
|
||||
op => 123
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_list_assignment_which_resolves_conflict_not_ignored() {
|
||||
let mut doc1 = new_doc();
|
||||
let mut doc2 = new_doc();
|
||||
let list_id = doc1
|
||||
.set(automerge::ROOT, "list", automerge::Value::list())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
doc1.insert(list_id, 0, 123).unwrap();
|
||||
doc2.merge(&mut doc1);
|
||||
let list_id_in_doc2 = translate_obj_id(&doc1, &doc2, list_id);
|
||||
doc2.set(list_id_in_doc2, 0, 456).unwrap().unwrap();
|
||||
doc1.merge(&mut doc2);
|
||||
let doc1_op = doc1.set(list_id, 0, 789).unwrap().unwrap();
|
||||
|
||||
assert_doc!(
|
||||
&doc1,
|
||||
map! {
|
||||
"list" => {
|
||||
list_id => list![
|
||||
{ doc1_op => 789 },
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_deletion() {
|
||||
let mut doc = new_doc();
|
||||
let list_id = doc
|
||||
.set(automerge::ROOT, "list", automerge::Value::list())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let op1 = doc.insert(list_id, 0, 123).unwrap();
|
||||
doc.insert(list_id, 1, 456).unwrap();
|
||||
let op3 = doc.insert(list_id, 2, 789).unwrap();
|
||||
doc.del(list_id, 1).unwrap();
|
||||
assert_doc!(
|
||||
&doc,
|
||||
map! {
|
||||
"list" => {list_id => list![
|
||||
{ op1 => 123 },
|
||||
{ op3 => 789 },
|
||||
]}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_concurrent_map_prop_updates() {
|
||||
let mut doc1 = new_doc();
|
||||
let mut doc2 = new_doc();
|
||||
let op1 = doc1.set(automerge::ROOT, "foo", "bar").unwrap().unwrap();
|
||||
let hello = doc2
|
||||
.set(automerge::ROOT, "hello", "world")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
doc1.merge(&mut doc2);
|
||||
assert_eq!(
|
||||
doc1.value(automerge::ROOT, "foo").unwrap().unwrap().0,
|
||||
"bar".into()
|
||||
);
|
||||
assert_doc!(
|
||||
&doc1,
|
||||
map! {
|
||||
"foo" => { op1 => "bar" },
|
||||
"hello" => { hello.translate(&doc2) => "world" },
|
||||
}
|
||||
);
|
||||
doc2.merge(&mut doc1);
|
||||
assert_doc!(
|
||||
&doc2,
|
||||
map! {
|
||||
"foo" => { op1.translate(&doc1) => "bar" },
|
||||
"hello" => { hello => "world" },
|
||||
}
|
||||
);
|
||||
assert_eq!(realize(&doc1), realize(&doc2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_concurrent_increments_of_same_property() {
|
||||
let mut doc1 = new_doc();
|
||||
let mut doc2 = new_doc();
|
||||
let counter_id = doc1
|
||||
.set(automerge::ROOT, "counter", mk_counter(0))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
doc2.merge(&mut doc1);
|
||||
doc1.inc(automerge::ROOT, "counter", 1).unwrap();
|
||||
doc2.inc(automerge::ROOT, "counter", 2).unwrap();
|
||||
doc1.merge(&mut doc2);
|
||||
assert_doc!(
|
||||
&doc1,
|
||||
map! {
|
||||
"counter" => {
|
||||
counter_id => mk_counter(3)
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_increments_only_to_preceeded_values() {
|
||||
let mut doc1 = new_doc();
|
||||
let mut doc2 = new_doc();
|
||||
|
||||
// create a counter in doc1
|
||||
let doc1_counter_id = doc1
|
||||
.set(automerge::ROOT, "counter", mk_counter(0))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
doc1.inc(automerge::ROOT, "counter", 1).unwrap();
|
||||
|
||||
// create a counter in doc2
|
||||
let doc2_counter_id = doc2
|
||||
.set(automerge::ROOT, "counter", mk_counter(0))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
doc2.inc(automerge::ROOT, "counter", 3).unwrap();
|
||||
|
||||
// The two values should be conflicting rather than added
|
||||
doc1.merge(&mut doc2);
|
||||
|
||||
assert_doc!(
|
||||
&doc1,
|
||||
map! {
|
||||
"counter" => {
|
||||
doc1_counter_id.native() => mk_counter(1),
|
||||
doc2_counter_id.translate(&doc2) => mk_counter(3),
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrent_updates_of_same_field() {
|
||||
let mut doc1 = new_doc();
|
||||
let mut doc2 = new_doc();
|
||||
let set_one_opid = doc1.set(automerge::ROOT, "field", "one").unwrap().unwrap();
|
||||
let set_two_opid = doc2.set(automerge::ROOT, "field", "two").unwrap().unwrap();
|
||||
|
||||
doc1.merge(&mut doc2);
|
||||
|
||||
assert_doc!(
|
||||
&doc1,
|
||||
map! {
|
||||
"field" => {
|
||||
set_one_opid.native() => "one",
|
||||
set_two_opid.translate(&doc2) => "two",
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrent_updates_of_same_list_element() {
|
||||
let mut doc1 = new_doc();
|
||||
let mut doc2 = new_doc();
|
||||
let list_id = doc1
|
||||
.set(automerge::ROOT, "birds", automerge::Value::list())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
doc1.insert(list_id, 0, "finch").unwrap();
|
||||
doc2.merge(&mut doc1);
|
||||
let set_one_op = doc1.set(list_id, 0, "greenfinch").unwrap().unwrap();
|
||||
let list_id_in_doc2 = translate_obj_id(&doc1, &doc2, list_id);
|
||||
let set_op_two = doc2.set(list_id_in_doc2, 0, "goldfinch").unwrap().unwrap();
|
||||
|
||||
doc1.merge(&mut doc2);
|
||||
|
||||
assert_doc!(
|
||||
&doc1,
|
||||
map! {
|
||||
"birds" => {
|
||||
list_id => list![{
|
||||
set_one_op.native() => "greenfinch",
|
||||
set_op_two.translate(&doc2) => "goldfinch",
|
||||
}]
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assignment_conflicts_of_different_types() {
|
||||
let mut doc1 = new_doc();
|
||||
let mut doc2 = new_doc();
|
||||
let mut doc3 = new_doc();
|
||||
let op_one = doc1
|
||||
.set(automerge::ROOT, "field", "string")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let op_two = doc2
|
||||
.set(automerge::ROOT, "field", automerge::Value::list())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let op_three = doc3
|
||||
.set(automerge::ROOT, "field", automerge::Value::map())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
doc1.merge(&mut doc2);
|
||||
doc1.merge(&mut doc3);
|
||||
|
||||
assert_doc!(
|
||||
&doc1,
|
||||
map! {
|
||||
"field" => {
|
||||
op_one.native() => "string",
|
||||
op_two.translate(&doc2) => list!{},
|
||||
op_three.translate(&doc3) => map!{},
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changes_within_conflicting_map_field() {
|
||||
let mut doc1 = new_doc();
|
||||
let mut doc2 = new_doc();
|
||||
let op_one = doc1
|
||||
.set(automerge::ROOT, "field", "string")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let map_id = doc2
|
||||
.set(automerge::ROOT, "field", automerge::Value::map())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let set_in_doc2 = doc2.set(map_id, "innerKey", 42).unwrap().unwrap();
|
||||
doc1.merge(&mut doc2);
|
||||
|
||||
assert_doc!(
|
||||
&doc1,
|
||||
map! {
|
||||
"field" => {
|
||||
op_one.native() => "string",
|
||||
map_id.translate(&doc2) => map!{
|
||||
"innerKey" => {
|
||||
set_in_doc2.translate(&doc2) => 42,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changes_within_conflicting_list_element() {
|
||||
let (actor1, actor2) = sorted_actors();
|
||||
let mut doc1 = new_doc_with_actor(actor1);
|
||||
let mut doc2 = new_doc_with_actor(actor2);
|
||||
let list_id = doc1
|
||||
.set(automerge::ROOT, "list", automerge::Value::list())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
doc1.insert(list_id, 0, "hello").unwrap();
|
||||
doc2.merge(&mut doc1);
|
||||
|
||||
let map_in_doc1 = doc1
|
||||
.set(list_id, 0, automerge::Value::map())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let set_map1 = doc1.set(map_in_doc1, "map1", true).unwrap().unwrap();
|
||||
let set_key1 = doc1.set(map_in_doc1, "key", 1).unwrap().unwrap();
|
||||
|
||||
let list_id_in_doc2 = translate_obj_id(&doc1, &doc2, list_id);
|
||||
let map_in_doc2 = doc2
|
||||
.set(list_id_in_doc2, 0, automerge::Value::map())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
doc1.merge(&mut doc2);
|
||||
let set_map2 = doc2.set(map_in_doc2, "map2", true).unwrap().unwrap();
|
||||
let set_key2 = doc2.set(map_in_doc2, "key", 2).unwrap().unwrap();
|
||||
|
||||
doc1.merge(&mut doc2);
|
||||
|
||||
assert_doc!(
|
||||
&doc1,
|
||||
map! {
|
||||
"list" => {
|
||||
list_id => list![
|
||||
{
|
||||
map_in_doc2.translate(&doc2) => map!{
|
||||
"map2" => { set_map2.translate(&doc2) => true },
|
||||
"key" => { set_key2.translate(&doc2) => 2 },
|
||||
},
|
||||
map_in_doc1.native() => map!{
|
||||
"key" => { set_key1.native() => 1 },
|
||||
"map1" => { set_map1.native() => true },
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrently_assigned_nested_maps_should_not_merge() {
|
||||
let mut doc1 = new_doc();
|
||||
let mut doc2 = new_doc();
|
||||
|
||||
let doc1_map_id = doc1
|
||||
.set(automerge::ROOT, "config", automerge::Value::map())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let doc1_field = doc1
|
||||
.set(doc1_map_id, "background", "blue")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let doc2_map_id = doc2
|
||||
.set(automerge::ROOT, "config", automerge::Value::map())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let doc2_field = doc2
|
||||
.set(doc2_map_id, "logo_url", "logo.png")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
doc1.merge(&mut doc2);
|
||||
|
||||
assert_doc!(
|
||||
&doc1,
|
||||
map! {
|
||||
"config" => {
|
||||
doc1_map_id.native() => map!{
|
||||
"background" => {doc1_field.native() => "blue"}
|
||||
},
|
||||
doc2_map_id.translate(&doc2) => map!{
|
||||
"logo_url" => {doc2_field.translate(&doc2) => "logo.png"}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrent_insertions_at_different_list_positions() {
|
||||
let (actor1, actor2) = sorted_actors();
|
||||
let mut doc1 = new_doc_with_actor(actor1);
|
||||
let mut doc2 = new_doc_with_actor(actor2);
|
||||
assert!(doc1.maybe_get_actor().unwrap() < doc2.maybe_get_actor().unwrap());
|
||||
|
||||
let list_id = doc1
|
||||
.set(automerge::ROOT, "list", automerge::Value::list())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let one = doc1.insert(list_id, 0, "one").unwrap();
|
||||
let three = doc1.insert(list_id, 1, "three").unwrap();
|
||||
doc2.merge(&mut doc1);
|
||||
let two = doc1.splice(list_id, 1, 0, vec!["two".into()]).unwrap()[0];
|
||||
let list_id_in_doc2 = translate_obj_id(&doc1, &doc2, list_id);
|
||||
let four = doc2.insert(list_id_in_doc2, 2, "four").unwrap();
|
||||
|
||||
doc1.merge(&mut doc2);
|
||||
|
||||
assert_doc!(
|
||||
&doc1,
|
||||
map! {
|
||||
"list" => {
|
||||
list_id => list![
|
||||
{one.native() => "one"},
|
||||
{two.native() => "two"},
|
||||
{three.native() => "three"},
|
||||
{four.translate(&doc2) => "four"},
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrent_insertions_at_same_list_position() {
|
||||
let (actor1, actor2) = sorted_actors();
|
||||
let mut doc1 = new_doc_with_actor(actor1);
|
||||
let mut doc2 = new_doc_with_actor(actor2);
|
||||
assert!(doc1.maybe_get_actor().unwrap() < doc2.maybe_get_actor().unwrap());
|
||||
|
||||
let list_id = doc1
|
||||
.set(automerge::ROOT, "birds", automerge::Value::list())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let parakeet = doc1.insert(list_id, 0, "parakeet").unwrap();
|
||||
|
||||
doc2.merge(&mut doc1);
|
||||
let list_id_in_doc2 = translate_obj_id(&doc1, &doc2, list_id);
|
||||
let starling = doc1.insert(list_id, 1, "starling").unwrap();
|
||||
let chaffinch = doc2.insert(list_id_in_doc2, 1, "chaffinch").unwrap();
|
||||
doc1.merge(&mut doc2);
|
||||
|
||||
assert_doc!(
|
||||
&doc1,
|
||||
map! {
|
||||
"birds" => {
|
||||
list_id => list![
|
||||
{
|
||||
parakeet.native() => "parakeet",
|
||||
},
|
||||
{
|
||||
starling.native() => "starling",
|
||||
},
|
||||
{
|
||||
chaffinch.translate(&doc2) => "chaffinch",
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrent_assignment_and_deletion_of_a_map_entry() {
|
||||
let mut doc1 = new_doc();
|
||||
let mut doc2 = new_doc();
|
||||
doc1.set(automerge::ROOT, "bestBird", "robin").unwrap();
|
||||
doc2.merge(&mut doc1);
|
||||
doc1.del(automerge::ROOT, "bestBird").unwrap();
|
||||
let set_two = doc2
|
||||
.set(automerge::ROOT, "bestBird", "magpie")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
doc1.merge(&mut doc2);
|
||||
|
||||
assert_doc!(
|
||||
&doc1,
|
||||
map! {
|
||||
"bestBird" => {
|
||||
set_two.translate(&doc2) => "magpie",
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrent_assignment_and_deletion_of_list_entry() {
|
||||
let mut doc1 = new_doc();
|
||||
let mut doc2 = new_doc();
|
||||
let list_id = doc1
|
||||
.set(automerge::ROOT, "birds", automerge::Value::list())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let blackbird = doc1.insert(list_id, 0, "blackbird").unwrap();
|
||||
doc1.insert(list_id, 1, "thrush").unwrap();
|
||||
let goldfinch = doc1.insert(list_id, 2, "goldfinch").unwrap();
|
||||
doc2.merge(&mut doc1);
|
||||
|
||||
let starling = doc1.set(list_id, 1, "starling").unwrap().unwrap();
|
||||
|
||||
let list_id_in_doc2 = translate_obj_id(&doc1, &doc2, list_id);
|
||||
doc2.del(list_id_in_doc2, 1).unwrap();
|
||||
|
||||
assert_doc!(
|
||||
&doc2,
|
||||
map! {
|
||||
"birds" => {list_id.translate(&doc1) => list![
|
||||
{ blackbird.translate(&doc1) => "blackbird"},
|
||||
{ goldfinch.translate(&doc1) => "goldfinch"},
|
||||
]}
|
||||
}
|
||||
);
|
||||
|
||||
assert_doc!(
|
||||
&doc1,
|
||||
map! {
|
||||
"birds" => {list_id => list![
|
||||
{ blackbird => "blackbird" },
|
||||
{ starling => "starling" },
|
||||
{ goldfinch => "goldfinch" },
|
||||
]}
|
||||
}
|
||||
);
|
||||
|
||||
doc1.merge(&mut doc2);
|
||||
|
||||
assert_doc!(
|
||||
&doc1,
|
||||
map! {
|
||||
"birds" => {list_id => list![
|
||||
{ blackbird => "blackbird" },
|
||||
{ starling => "starling" },
|
||||
{ goldfinch => "goldfinch" },
|
||||
]}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insertion_after_a_deleted_list_element() {
|
||||
let mut doc1 = new_doc();
|
||||
let mut doc2 = new_doc();
|
||||
let list_id = doc1
|
||||
.set(automerge::ROOT, "birds", automerge::Value::list())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let blackbird = doc1.insert(list_id, 0, "blackbird").unwrap();
|
||||
doc1.insert(list_id, 1, "thrush").unwrap();
|
||||
doc1.insert(list_id, 2, "goldfinch").unwrap();
|
||||
|
||||
doc2.merge(&mut doc1);
|
||||
|
||||
doc1.splice(list_id, 1, 2, Vec::new()).unwrap();
|
||||
|
||||
let list_id_in_doc2 = translate_obj_id(&doc1, &doc2, list_id);
|
||||
let starling = doc2
|
||||
.splice(list_id_in_doc2, 2, 0, vec!["starling".into()])
|
||||
.unwrap()[0];
|
||||
|
||||
doc1.merge(&mut doc2);
|
||||
|
||||
assert_doc!(
|
||||
&doc1,
|
||||
map! {
|
||||
"birds" => {list_id => list![
|
||||
{ blackbird.native() => "blackbird" },
|
||||
{ starling.translate(&doc2) => "starling" }
|
||||
]}
|
||||
}
|
||||
);
|
||||
|
||||
doc2.merge(&mut doc1);
|
||||
assert_doc!(
|
||||
&doc2,
|
||||
map! {
|
||||
"birds" => {list_id.translate(&doc1) => list![
|
||||
{ blackbird.translate(&doc1) => "blackbird" },
|
||||
{ starling.native() => "starling" }
|
||||
]}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrent_deletion_of_same_list_element() {
|
||||
let mut doc1 = new_doc();
|
||||
let mut doc2 = new_doc();
|
||||
let list_id = doc1
|
||||
.set(automerge::ROOT, "birds", automerge::Value::list())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let albatross = doc1.insert(list_id, 0, "albatross").unwrap();
|
||||
doc1.insert(list_id, 1, "buzzard").unwrap();
|
||||
let cormorant = doc1.insert(list_id, 2, "cormorant").unwrap();
|
||||
|
||||
doc2.merge(&mut doc1);
|
||||
|
||||
doc1.del(list_id, 1).unwrap();
|
||||
|
||||
let list_id_in_doc2 = translate_obj_id(&doc1, &doc2, list_id);
|
||||
doc2.del(list_id_in_doc2, 1).unwrap();
|
||||
|
||||
doc1.merge(&mut doc2);
|
||||
|
||||
assert_doc!(
|
||||
&doc1,
|
||||
map! {
|
||||
"birds" => {list_id => list![
|
||||
{ albatross => "albatross" },
|
||||
{ cormorant => "cormorant" }
|
||||
]}
|
||||
}
|
||||
);
|
||||
|
||||
doc2.merge(&mut doc1);
|
||||
assert_doc!(
|
||||
&doc2,
|
||||
map! {
|
||||
"birds" => {list_id.translate(&doc1) => list![
|
||||
{ albatross.translate(&doc1) => "albatross" },
|
||||
{ cormorant.translate(&doc1) => "cormorant" }
|
||||
]}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrent_updates_at_different_levels() {
|
||||
let mut doc1 = new_doc();
|
||||
let mut doc2 = new_doc();
|
||||
|
||||
let animals = doc1
|
||||
.set(automerge::ROOT, "animals", automerge::Value::map())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let birds = doc1
|
||||
.set(animals, "birds", automerge::Value::map())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
doc1.set(birds, "pink", "flamingo").unwrap().unwrap();
|
||||
doc1.set(birds, "black", "starling").unwrap().unwrap();
|
||||
|
||||
let mammals = doc1
|
||||
.set(animals, "mammals", automerge::Value::list())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let badger = doc1.insert(mammals, 0, "badger").unwrap();
|
||||
|
||||
doc2.merge(&mut doc1);
|
||||
|
||||
doc1.set(birds, "brown", "sparrow").unwrap().unwrap();
|
||||
|
||||
let animals_in_doc2 = translate_obj_id(&doc1, &doc2, animals);
|
||||
doc2.del(animals_in_doc2, "birds").unwrap();
|
||||
doc1.merge(&mut doc2);
|
||||
|
||||
assert_obj!(
|
||||
&doc1,
|
||||
automerge::ROOT,
|
||||
"animals",
|
||||
map! {
|
||||
"mammals" => {
|
||||
mammals => list![{ badger => "badger" }],
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
assert_obj!(
|
||||
&doc2,
|
||||
automerge::ROOT,
|
||||
"animals",
|
||||
map! {
|
||||
"mammals" => {
|
||||
mammals.translate(&doc1) => list![{ badger.translate(&doc1) => "badger" }],
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrent_updates_of_concurrently_deleted_objects() {
|
||||
let mut doc1 = new_doc();
|
||||
let mut doc2 = new_doc();
|
||||
|
||||
let birds = doc1
|
||||
.set(automerge::ROOT, "birds", automerge::Value::map())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let blackbird = doc1
|
||||
.set(birds, "blackbird", automerge::Value::map())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
doc1.set(blackbird, "feathers", "black").unwrap().unwrap();
|
||||
|
||||
doc2.merge(&mut doc1);
|
||||
|
||||
doc1.del(birds, "blackbird").unwrap();
|
||||
|
||||
translate_obj_id(&doc1, &doc2, blackbird);
|
||||
doc2.set(blackbird, "beak", "orange").unwrap();
|
||||
|
||||
doc1.merge(&mut doc2);
|
||||
|
||||
assert_doc!(
|
||||
&doc1,
|
||||
map! {
|
||||
"birds" => {
|
||||
birds => map!{},
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_interleave_sequence_insertions_at_same_position() {
|
||||
let (actor1, actor2) = sorted_actors();
|
||||
let mut doc1 = new_doc_with_actor(actor1);
|
||||
let mut doc2 = new_doc_with_actor(actor2);
|
||||
|
||||
let wisdom = doc1
|
||||
.set(automerge::ROOT, "wisdom", automerge::Value::list())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
doc2.merge(&mut doc1);
|
||||
|
||||
let doc1elems = doc1
|
||||
.splice(
|
||||
wisdom,
|
||||
0,
|
||||
0,
|
||||
vec![
|
||||
"to".into(),
|
||||
"be".into(),
|
||||
"is".into(),
|
||||
"to".into(),
|
||||
"do".into(),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let wisdom_in_doc2 = translate_obj_id(&doc1, &doc2, wisdom);
|
||||
let doc2elems = doc2
|
||||
.splice(
|
||||
wisdom_in_doc2,
|
||||
0,
|
||||
0,
|
||||
vec![
|
||||
"to".into(),
|
||||
"do".into(),
|
||||
"is".into(),
|
||||
"to".into(),
|
||||
"be".into(),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
doc1.merge(&mut doc2);
|
||||
|
||||
assert_doc!(
|
||||
&doc1,
|
||||
map! {
|
||||
"wisdom" => {wisdom => list![
|
||||
{doc1elems[0].native() => "to"},
|
||||
{doc1elems[1].native() => "be"},
|
||||
{doc1elems[2].native() => "is"},
|
||||
{doc1elems[3].native() => "to"},
|
||||
{doc1elems[4].native() => "do"},
|
||||
{doc2elems[0].translate(&doc2) => "to"},
|
||||
{doc2elems[1].translate(&doc2) => "do"},
|
||||
{doc2elems[2].translate(&doc2) => "is"},
|
||||
{doc2elems[3].translate(&doc2) => "to"},
|
||||
{doc2elems[4].translate(&doc2) => "be"},
|
||||
]}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mutliple_insertions_at_same_list_position_with_insertion_by_greater_actor_id() {
|
||||
let (actor1, actor2) = sorted_actors();
|
||||
assert!(actor2 > actor1);
|
||||
let mut doc1 = new_doc_with_actor(actor1);
|
||||
let mut doc2 = new_doc_with_actor(actor2);
|
||||
|
||||
let list = doc1
|
||||
.set(automerge::ROOT, "list", automerge::Value::list())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let two = doc1.insert(list, 0, "two").unwrap();
|
||||
doc2.merge(&mut doc1);
|
||||
|
||||
let list_in_doc2 = translate_obj_id(&doc1, &doc2, list);
|
||||
let one = doc2.insert(list_in_doc2, 0, "one").unwrap();
|
||||
assert_doc!(
|
||||
&doc2,
|
||||
map! {
|
||||
"list" => { list.translate(&doc1) => list![
|
||||
{ one.native() => "one" },
|
||||
{ two.translate(&doc1) => "two" },
|
||||
]}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mutliple_insertions_at_same_list_position_with_insertion_by_lesser_actor_id() {
|
||||
let (actor2, actor1) = sorted_actors();
|
||||
assert!(actor2 < actor1);
|
||||
let mut doc1 = new_doc_with_actor(actor1);
|
||||
let mut doc2 = new_doc_with_actor(actor2);
|
||||
|
||||
let list = doc1
|
||||
.set(automerge::ROOT, "list", automerge::Value::list())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let two = doc1.insert(list, 0, "two").unwrap();
|
||||
doc2.merge(&mut doc1);
|
||||
|
||||
let list_in_doc2 = translate_obj_id(&doc1, &doc2, list);
|
||||
let one = doc2.insert(list_in_doc2, 0, "one").unwrap();
|
||||
assert_doc!(
|
||||
&doc2,
|
||||
map! {
|
||||
"list" => { list.translate(&doc1) => list![
|
||||
{ one.native() => "one" },
|
||||
{ two.translate(&doc1) => "two" },
|
||||
]}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insertion_consistent_with_causality() {
|
||||
let mut doc1 = new_doc();
|
||||
let mut doc2 = new_doc();
|
||||
|
||||
let list = doc1
|
||||
.set(automerge::ROOT, "list", automerge::Value::list())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let four = doc1.insert(list, 0, "four").unwrap();
|
||||
doc2.merge(&mut doc1);
|
||||
let list_in_doc2 = translate_obj_id(&doc1, &doc2, list);
|
||||
let three = doc2.insert(list_in_doc2, 0, "three").unwrap();
|
||||
doc1.merge(&mut doc2);
|
||||
let two = doc1.insert(list, 0, "two").unwrap();
|
||||
doc2.merge(&mut doc1);
|
||||
let one = doc2.insert(list_in_doc2, 0, "one").unwrap();
|
||||
|
||||
assert_doc!(
|
||||
&doc2,
|
||||
map! {
|
||||
"list" => {list.translate(&doc1) => list![
|
||||
{one.native() => "one"},
|
||||
{two.translate(&doc1) => "two"},
|
||||
{three.native() => "three" },
|
||||
{four.translate(&doc1) => "four"},
|
||||
]}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_and_restore_empty() {
|
||||
let mut doc = new_doc();
|
||||
let loaded = Automerge::load(&doc.save().unwrap()).unwrap();
|
||||
|
||||
assert_doc!(&loaded, map! {});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_restore_complex() {
|
||||
let mut doc1 = new_doc();
|
||||
let todos = doc1
|
||||
.set(automerge::ROOT, "todos", automerge::Value::list())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let first_todo = doc1.insert(todos, 0, automerge::Value::map()).unwrap();
|
||||
doc1.set(first_todo, "title", "water plants")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let first_done = doc1.set(first_todo, "done", false).unwrap().unwrap();
|
||||
|
||||
let mut doc2 = new_doc();
|
||||
doc2.merge(&mut doc1);
|
||||
let first_todo_in_doc2 = translate_obj_id(&doc1, &doc2, first_todo);
|
||||
let weed_title = doc2
|
||||
.set(first_todo_in_doc2, "title", "weed plants")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let kill_title = doc1
|
||||
.set(first_todo, "title", "kill plants")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
doc1.merge(&mut doc2);
|
||||
|
||||
let reloaded = Automerge::load(&doc1.save().unwrap()).unwrap();
|
||||
|
||||
assert_doc!(
|
||||
&reloaded,
|
||||
map! {
|
||||
"todos" => {todos.translate(&doc1) => list![
|
||||
{first_todo.translate(&doc1) => map!{
|
||||
"title" => {
|
||||
weed_title.translate(&doc2) => "weed plants",
|
||||
kill_title.translate(&doc1) => "kill plants",
|
||||
},
|
||||
"done" => {first_done.translate(&doc1) => false},
|
||||
}}
|
||||
]}
|
||||
}
|
||||
);
|
||||
}
|
|
@ -46,6 +46,7 @@ notice = "warn"
|
|||
# output a note when they are encountered.
|
||||
ignore = [
|
||||
#"RUSTSEC-0000-0000",
|
||||
"RUSTSEC-2021-0127", # serde_cbor is unmaintained, but we only use it in criterion for benchmarks
|
||||
]
|
||||
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
|
||||
# lower than the range specified will be ignored. Note that ignored advisories
|
||||
|
@ -99,20 +100,9 @@ confidence-threshold = 0.8
|
|||
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
|
||||
# aren't accepted for every possible crate as with the normal allow list
|
||||
exceptions = [
|
||||
# The Unicode-DFS--2016 license is necessary for unicode-ident because they
|
||||
# use data from the unicode tables to generate the tables which are
|
||||
# included in the application. We do not distribute those data files so
|
||||
# this is not a problem for us. See https://github.com/dtolnay/unicode-ident/pull/9/files
|
||||
# for more details.
|
||||
{ allow = ["MIT", "Apache-2.0", "Unicode-DFS-2016"], name = "unicode-ident" },
|
||||
|
||||
# these are needed by cbindgen and its dependancies
|
||||
# should be revied more fully before release
|
||||
{ allow = ["MPL-2.0"], name = "cbindgen" },
|
||||
{ allow = ["BSD-3-Clause"], name = "instant" },
|
||||
|
||||
# we only use prettytable in tests
|
||||
{ allow = ["BSD-3-Clause"], name = "prettytable" },
|
||||
# Each entry is the crate and version constraint, and its specific allow
|
||||
# list
|
||||
#{ allow = ["Zlib"], name = "adler32", version = "*" },
|
||||
]
|
||||
|
||||
# Some crates don't have (easily) machine readable licensing information,
|
||||
|
@ -175,20 +165,15 @@ deny = [
|
|||
]
|
||||
# Certain crates/versions that will be skipped when doing duplicate detection.
|
||||
skip = [
|
||||
# duct, which we only depend on for integration tests in automerge-cli,
|
||||
# pulls in a version of os_pipe which in turn pulls in a version of
|
||||
# windows-sys which is different to the version in pulled in by is-terminal.
|
||||
# This is fine to ignore for now because it doesn't end up in downstream
|
||||
# dependencies.
|
||||
{ name = "windows-sys", version = "0.42.0" }
|
||||
# This is a transitive depdendency of criterion, which is only included for benchmarking anyway
|
||||
{ name = "itoa", version = "0.4.8" },
|
||||
]
|
||||
# Similarly to `skip` allows you to skip certain crates during duplicate
|
||||
# detection. Unlike skip, it also includes the entire tree of transitive
|
||||
# dependencies starting at the specified crate, up to a certain depth, which is
|
||||
# by default infinite
|
||||
skip-tree = [
|
||||
# // We only ever use criterion in benchmarks
|
||||
{ name = "criterion", version = "0.4.0", depth=10},
|
||||
#{ name = "ansi_term", version = "=0.11.0", depth = 20 },
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check sources`.
|
|
@ -3,4 +3,3 @@ Cargo.lock
|
|||
node_modules
|
||||
yarn.lock
|
||||
flamegraph.svg
|
||||
/prof
|
|
@ -1,23 +1,17 @@
|
|||
[package]
|
||||
name = "edit-trace"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = "2018"
|
||||
license = "MIT"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
automerge = { path = "../automerge" }
|
||||
criterion = "0.4.0"
|
||||
criterion = "0.3.5"
|
||||
json = "0.12.4"
|
||||
rand = "^0.8"
|
||||
|
||||
|
||||
[[bin]]
|
||||
name = "edit-trace"
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bench]]
|
||||
debug = true
|
||||
name = "main"
|
||||
harness = false
|
||||
|
52
edit-trace/README.md
Normal file
52
edit-trace/README.md
Normal file
|
@ -0,0 +1,52 @@
|
|||
Try the different editing traces on different automerge implementations
|
||||
|
||||
### Automerge Experiement - pure rust
|
||||
|
||||
```code
|
||||
# cargo --release run
|
||||
```
|
||||
|
||||
#### Benchmarks
|
||||
|
||||
There are some criterion benchmarks in the `benches` folder which can be run with `cargo bench` or `cargo criterion`.
|
||||
For flamegraphing, `cargo flamegraph --bench main -- --bench "save" # or "load" or "replay" or nothing` can be useful.
|
||||
|
||||
### Automerge Experiement - wasm api
|
||||
|
||||
```code
|
||||
# node automerge-wasm.js
|
||||
```
|
||||
|
||||
### Automerge Experiment - JS wrapper
|
||||
|
||||
```code
|
||||
# node automerge-js.js
|
||||
```
|
||||
|
||||
### Automerge 1.0 pure javascript - new fast backend
|
||||
|
||||
This assume automerge has been checked out in a directory along side this repo
|
||||
|
||||
```code
|
||||
# node automerge-1.0.js
|
||||
```
|
||||
|
||||
### Automerge 1.0 with rust backend
|
||||
|
||||
This assume automerge has been checked out in a directory along side this repo
|
||||
|
||||
```code
|
||||
# node automerge-rs.js
|
||||
```
|
||||
|
||||
### Automerge Experiment - JS wrapper
|
||||
|
||||
```code
|
||||
# node automerge-js.js
|
||||
```
|
||||
|
||||
### Baseline Test. Javascript Array with no CRDT info
|
||||
|
||||
```code
|
||||
# node baseline.js
|
||||
```
|
23
edit-trace/automerge-js.js
Normal file
23
edit-trace/automerge-js.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
// Apply the paper editing trace to an Automerge.Text object, one char at a time
|
||||
const { edits, finalText } = require('./editing-trace')
|
||||
const Automerge = require('../automerge-js')
|
||||
|
||||
const start = new Date()
|
||||
let state = Automerge.from({text: new Automerge.Text()})
|
||||
|
||||
state = Automerge.change(state, doc => {
|
||||
for (let i = 0; i < edits.length; i++) {
|
||||
if (i % 1000 === 0) {
|
||||
console.log(`Processed ${i} edits in ${new Date() - start} ms`)
|
||||
}
|
||||
if (edits[i][1] > 0) doc.text.deleteAt(edits[i][0], edits[i][1])
|
||||
if (edits[i].length > 2) doc.text.insertAt(edits[i][0], ...edits[i].slice(2))
|
||||
}
|
||||
})
|
||||
|
||||
let _ = Automerge.save(state)
|
||||
console.log(`Done in ${new Date() - start} ms`)
|
||||
|
||||
if (state.text.join('') !== finalText) {
|
||||
throw new RangeError('ERROR: final text did not match expectation')
|
||||
}
|
31
edit-trace/automerge-rs.js
Normal file
31
edit-trace/automerge-rs.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
|
||||
// this assumes that the automerge-rs folder is checked out along side this repo
|
||||
// and someone has run
|
||||
|
||||
// # cd automerge-rs/automerge-backend-wasm
|
||||
// # yarn release
|
||||
|
||||
const { edits, finalText } = require('./editing-trace')
|
||||
const Automerge = require('../../automerge')
|
||||
const path = require('path')
|
||||
const wasmBackend = require(path.resolve("../../automerge-rs/automerge-backend-wasm"))
|
||||
Automerge.setDefaultBackend(wasmBackend)
|
||||
|
||||
const start = new Date()
|
||||
let state = Automerge.from({text: new Automerge.Text()})
|
||||
|
||||
state = Automerge.change(state, doc => {
|
||||
for (let i = 0; i < edits.length; i++) {
|
||||
if (i % 1000 === 0) {
|
||||
console.log(`Processed ${i} edits in ${new Date() - start} ms`)
|
||||
}
|
||||
if (edits[i][1] > 0) doc.text.deleteAt(edits[i][0], edits[i][1])
|
||||
if (edits[i].length > 2) doc.text.insertAt(edits[i][0], ...edits[i].slice(2))
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`Done in ${new Date() - start} ms`)
|
||||
|
||||
if (state.text.join('') !== finalText) {
|
||||
throw new RangeError('ERROR: final text did not match expectation')
|
||||
}
|
30
edit-trace/automerge-wasm.js
Normal file
30
edit-trace/automerge-wasm.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
|
||||
// make sure to
|
||||
|
||||
// # cd ../automerge-wasm
|
||||
// # yarn release
|
||||
// # yarn opt
|
||||
|
||||
const { edits, finalText } = require('./editing-trace')
|
||||
const Automerge = require('../automerge-wasm')
|
||||
|
||||
const start = new Date()
|
||||
|
||||
let doc = Automerge.init();
|
||||
let text = doc.set("_root", "text", Automerge.TEXT)
|
||||
|
||||
for (let i = 0; i < edits.length; i++) {
|
||||
let edit = edits[i]
|
||||
if (i % 1000 === 0) {
|
||||
console.log(`Processed ${i} edits in ${new Date() - start} ms`)
|
||||
}
|
||||
doc.splice(text, ...edit)
|
||||
}
|
||||
|
||||
let _ = doc.save()
|
||||
|
||||
console.log(`Done in ${new Date() - start} ms`)
|
||||
|
||||
if (doc.text(text) !== finalText) {
|
||||
throw new RangeError('ERROR: final text did not match expectation')
|
||||
}
|
|
@ -5,7 +5,7 @@ const start = new Date()
|
|||
let chars = []
|
||||
for (let i = 0; i < edits.length; i++) {
|
||||
let edit = edits[i]
|
||||
if (i % 10000 === 0) {
|
||||
if (i % 1000 === 0) {
|
||||
console.log(`Processed ${i} edits in ${new Date() - start} ms`)
|
||||
}
|
||||
chars.splice(...edit)
|
71
edit-trace/benches/main.rs
Normal file
71
edit-trace/benches/main.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
use automerge::{Automerge, Value, ROOT};
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
use std::fs;
|
||||
|
||||
fn replay_trace(commands: Vec<(usize, usize, Vec<Value>)>) -> Automerge {
|
||||
let mut doc = Automerge::new();
|
||||
|
||||
let text = doc.set(&ROOT, "text", Value::text()).unwrap().unwrap();
|
||||
for (pos, del, vals) in commands {
|
||||
doc.splice(&text, pos, del, vals).unwrap();
|
||||
}
|
||||
doc.commit(None, None);
|
||||
doc
|
||||
}
|
||||
|
||||
fn save_trace(mut doc: Automerge) {
|
||||
doc.save().unwrap();
|
||||
}
|
||||
|
||||
fn load_trace(bytes: &[u8]) {
|
||||
Automerge::load(bytes).unwrap();
|
||||
}
|
||||
|
||||
fn bench(c: &mut Criterion) {
|
||||
let contents = fs::read_to_string("edits.json").expect("cannot read edits file");
|
||||
let edits = json::parse(&contents).expect("cant parse edits");
|
||||
let mut commands = vec![];
|
||||
for i in 0..edits.len() {
|
||||
let pos: usize = edits[i][0].as_usize().unwrap();
|
||||
let del: usize = edits[i][1].as_usize().unwrap();
|
||||
let mut vals = vec![];
|
||||
for j in 2..edits[i].len() {
|
||||
let v = edits[i][j].as_str().unwrap();
|
||||
vals.push(Value::str(v));
|
||||
}
|
||||
commands.push((pos, del, vals));
|
||||
}
|
||||
|
||||
let mut group = c.benchmark_group("edit trace");
|
||||
group.throughput(Throughput::Elements(commands.len() as u64));
|
||||
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("replay", commands.len()),
|
||||
&commands,
|
||||
|b, commands| {
|
||||
b.iter_batched(
|
||||
|| commands.clone(),
|
||||
replay_trace,
|
||||
criterion::BatchSize::LargeInput,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
let commands_len = commands.len();
|
||||
let mut doc = replay_trace(commands);
|
||||
group.bench_with_input(BenchmarkId::new("save", commands_len), &doc, |b, doc| {
|
||||
b.iter_batched(|| doc.clone(), save_trace, criterion::BatchSize::LargeInput)
|
||||
});
|
||||
|
||||
let bytes = doc.save().unwrap();
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("load", commands_len),
|
||||
&bytes,
|
||||
|b, bytes| b.iter(|| load_trace(bytes)),
|
||||
);
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench);
|
||||
criterion_main!(benches);
|
|
@ -4,9 +4,9 @@
|
|||
"main": "wasm-text.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"wasm": "0x -D prof automerge-wasm.js"
|
||||
"wasm": "0x -D prof wasm-text.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"0x": "^5.4.1"
|
||||
"0x": "^4.11.0"
|
||||
}
|
||||
}
|
32
edit-trace/src/main.rs
Normal file
32
edit-trace/src/main.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use automerge::{Automerge, AutomergeError, Value, ROOT};
|
||||
use std::fs;
|
||||
use std::time::Instant;
|
||||
|
||||
fn main() -> Result<(), AutomergeError> {
|
||||
let contents = fs::read_to_string("edits.json").expect("cannot read edits file");
|
||||
let edits = json::parse(&contents).expect("cant parse edits");
|
||||
let mut commands = vec![];
|
||||
for i in 0..edits.len() {
|
||||
let pos: usize = edits[i][0].as_usize().unwrap();
|
||||
let del: usize = edits[i][1].as_usize().unwrap();
|
||||
let mut vals = vec![];
|
||||
for j in 2..edits[i].len() {
|
||||
let v = edits[i][j].as_str().unwrap();
|
||||
vals.push(Value::str(v));
|
||||
}
|
||||
commands.push((pos, del, vals));
|
||||
}
|
||||
let mut doc = Automerge::new();
|
||||
|
||||
let now = Instant::now();
|
||||
let text = doc.set( &ROOT, "text", Value::text()).unwrap().unwrap();
|
||||
for (i, (pos, del, vals)) in commands.into_iter().enumerate() {
|
||||
if i % 1000 == 0 {
|
||||
println!("Processed {} edits in {} ms", i, now.elapsed().as_millis());
|
||||
}
|
||||
doc.splice(&text, pos, del, vals)?;
|
||||
}
|
||||
let _ = doc.save();
|
||||
println!("Done in {} ms", now.elapsed().as_millis());
|
||||
Ok(())
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue