Compare commits
1 commit
main
...
columnar-e
Author | SHA1 | Date | |
---|---|---|---|
|
9332ed4ad9 |
521 changed files with 26596 additions and 71018 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 * * *'
|
||||
|
|
90
.github/workflows/ci.yaml
vendored
90
.github/workflows/ci.yaml
vendored
|
@ -1,11 +1,11 @@
|
|||
name: CI
|
||||
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,40 @@ 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: Install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
- 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 +102,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 +114,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
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -1,6 +1,6 @@
|
|||
/target
|
||||
/.direnv
|
||||
perf.*
|
||||
/Cargo.lock
|
||||
build/
|
||||
.vim/*
|
||||
/target
|
||||
automerge/proptest-regressions/
|
||||
.vim
|
||||
|
|
3
.vim/coc-settings.json
Normal file
3
.vim/coc-settings.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"rust-analyzer.cargo.features": ["optree-visualisation", "storage-v2"]
|
||||
}
|
|
@ -1,17 +1,16 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"automerge",
|
||||
"automerge-c",
|
||||
"automerge-cli",
|
||||
"automerge-test",
|
||||
"automerge-wasm",
|
||||
"automerge-cli",
|
||||
"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.
|
||||
|
|
32
TODO.md
Normal file
32
TODO.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
### next steps:
|
||||
1. C API
|
||||
2. port rust command line tool
|
||||
3. fast load
|
||||
|
||||
### ergonomics:
|
||||
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
|
||||
|
||||
### future:
|
||||
1. handle columns with unknown data in and out
|
||||
2. branches with different indexes
|
||||
|
||||
### Peritext
|
||||
1. add mark / remove mark -- type, start/end elemid (inclusive,exclusive)
|
||||
2. track any formatting ops that start or end on a character
|
||||
3. ops right before the character, ops right after that character
|
||||
4. query a single character - character, plus marks that start or end on that character
|
||||
what is its current formatting,
|
||||
what are the ops that include that in their span,
|
||||
None = same as last time, Set( bold, italic ),
|
||||
keep these on index
|
||||
5. op probably belongs with the start character - possible packed at the beginning or end of the list
|
||||
|
||||
### maybe:
|
||||
1. tables
|
||||
|
||||
### no:
|
||||
1. cursors
|
857
automerge-cli/Cargo.lock
generated
Normal file
857
automerge-cli/Cargo.lock
generated
Normal file
|
@ -0,0 +1,857 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "adler"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "159bb86af3a200e19a068f4224eae4c8bb2d0fa054c7e5d1cacd5cef95e684cd"
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "automerge"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"fxhash",
|
||||
"hex",
|
||||
"itertools",
|
||||
"js-sys",
|
||||
"leb128",
|
||||
"nonzero_ext",
|
||||
"rand",
|
||||
"serde",
|
||||
"sha2",
|
||||
"smol_str",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"unicode-segmentation",
|
||||
"uuid",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "automerge-cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"atty",
|
||||
"automerge",
|
||||
"clap",
|
||||
"colored_json",
|
||||
"combine",
|
||||
"duct",
|
||||
"maplit",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced1892c55c910c1219e98d6fc8d71f6bddba7905866ce740066d8bfea859312"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"bitflags",
|
||||
"clap_derive",
|
||||
"indexmap",
|
||||
"lazy_static",
|
||||
"os_str_bytes",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "3.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da95d038ede1a964ce99f49cbe27a7fb538d1da595e4b4f70b8c8f338d17bf16"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colored_json"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd32eb54d016e203b7c2600e3a7802c75843a92e38ccc4869aefeca21771a64"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"atty",
|
||||
"libc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50b727aacc797f9fc28e355d21f34709ac4fc9adecfe470ad07b8f4464f53062"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "duct"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fc6a0a59ed0888e0041cf708e66357b7ae1a82f1c67247e1f93b5e0818f7d8d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"once_cell",
|
||||
"os_pipe",
|
||||
"shared_child",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crc32fast",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fxhash"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "leb128"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "maplit"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
|
||||
dependencies = [
|
||||
"adler",
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nonzero_ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44a1290799eababa63ea60af0cbc3f03363e328e58f32fb0294798ed3e85f444"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
|
||||
|
||||
[[package]]
|
||||
name = "os_pipe"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb233f06c2307e1f5ce2ecad9f8121cffbbee2c95428f44ea85222e460d0d213"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.136"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.136"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared_child"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6be9f7d5565b1483af3e72975e2dee33879b3b86bd48c0929fccf6585d79e65a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
|
||||
|
||||
[[package]]
|
||||
name = "smol_str"
|
||||
version = "0.1.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61d15c83e300cce35b7c8cd39ff567c1ef42dde6d4a1a38dbdbf9a59902261bd"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6c650a8ef0cd2dd93736f033d21cbd1224c5a967aa0c258d00fcf7dafef9b9f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8276d9a4a3a558d7b7ad5303ad50b53d58264641b82914b7ada36bd762e7a716"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03cfcb51380632a72d3111cb8d3447a8d908e577d31beeac006f836383d29a23"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e0ab7bdc962035a87fba73f3acca9b8a8d0034c2e6f60b84aeaaddddc155dce"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.2+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2"
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
|
@ -4,7 +4,6 @@ version = "0.1.0"
|
|||
authors = ["Alex Good <alex@memoryandthought.me>"]
|
||||
edition = "2018"
|
||||
license = "MIT"
|
||||
rust-version = "1.57.0"
|
||||
|
||||
[[bin]]
|
||||
name = "automerge"
|
||||
|
@ -13,18 +12,17 @@ bench = false
|
|||
doc = false
|
||||
|
||||
[dependencies]
|
||||
clap = {version = "~4", features = ["derive"]}
|
||||
clap = {version = "~3.1", features = ["derive"]}
|
||||
serde_json = "^1.0"
|
||||
anyhow = "1.0"
|
||||
atty = "^0.2"
|
||||
thiserror = "^1.0"
|
||||
combine = "^4.5"
|
||||
maplit = "^1.0"
|
||||
colored_json = "^2.1"
|
||||
tracing-subscriber = "~0.3"
|
||||
|
||||
automerge = { path = "../automerge" }
|
||||
is-terminal = "0.4.1"
|
||||
termcolor = "1.1.3"
|
||||
serde = "1.0.150"
|
||||
|
||||
[dev-dependencies]
|
||||
duct = "^0.13"
|
|
@ -1,8 +1,6 @@
|
|||
use automerge as am;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{color_json::print_colored_json, SkipVerifyFlag};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ExamineError {
|
||||
#[error("Error reading change file: {:?}", source)]
|
||||
|
@ -22,29 +20,22 @@ pub enum ExamineError {
|
|||
},
|
||||
}
|
||||
|
||||
pub(crate) fn examine(
|
||||
pub fn examine(
|
||||
mut input: impl std::io::Read,
|
||||
mut output: impl std::io::Write,
|
||||
skip: SkipVerifyFlag,
|
||||
is_tty: bool,
|
||||
) -> Result<(), ExamineError> {
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
input
|
||||
.read_to_end(&mut buf)
|
||||
.map_err(|e| ExamineError::ReadingChanges { source: e })?;
|
||||
let doc = skip
|
||||
.load(&buf)
|
||||
let doc = am::Automerge::load(&buf)
|
||||
.map_err(|e| ExamineError::ApplyingInitialChanges { source: e })?;
|
||||
let uncompressed_changes: Vec<_> = doc
|
||||
.get_changes(&[])
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|c| c.decode())
|
||||
.collect();
|
||||
let uncompressed_changes: Vec<_> = doc.get_changes(&[]).iter().map(|c| c.decode()).collect();
|
||||
if is_tty {
|
||||
let json_changes = serde_json::to_value(uncompressed_changes).unwrap();
|
||||
print_colored_json(&json_changes).unwrap();
|
||||
writeln!(output).unwrap();
|
||||
colored_json::write_colored_json(&json_changes, &mut output).unwrap();
|
||||
writeln!(&mut output).unwrap();
|
||||
} else {
|
||||
let json_changes = serde_json::to_string_pretty(&uncompressed_changes).unwrap();
|
||||
output
|
|
@ -1,14 +1,11 @@
|
|||
use anyhow::Result;
|
||||
use automerge as am;
|
||||
use automerge::ReadDoc;
|
||||
|
||||
use crate::{color_json::print_colored_json, SkipVerifyFlag};
|
||||
|
||||
pub(crate) fn map_to_json(doc: &am::Automerge, obj: &am::ObjId) -> serde_json::Value {
|
||||
let keys = doc.keys(obj);
|
||||
let mut map = serde_json::Map::new();
|
||||
for k in keys {
|
||||
let val = doc.get(obj, &k);
|
||||
let val = doc.value(obj, &k);
|
||||
match val {
|
||||
Ok(Some((am::Value::Object(o), exid)))
|
||||
if o == am::ObjType::Map || o == am::ObjType::Table =>
|
||||
|
@ -31,7 +28,7 @@ fn list_to_json(doc: &am::Automerge, obj: &am::ObjId) -> serde_json::Value {
|
|||
let len = doc.length(obj);
|
||||
let mut array = Vec::new();
|
||||
for i in 0..len {
|
||||
let val = doc.get(obj, i);
|
||||
let val = doc.value(obj, i as usize);
|
||||
match val {
|
||||
Ok(Some((am::Value::Object(o), exid)))
|
||||
if o == am::ObjType::Map || o == am::ObjType::Table =>
|
||||
|
@ -53,13 +50,11 @@ fn list_to_json(doc: &am::Automerge, obj: &am::ObjId) -> serde_json::Value {
|
|||
fn scalar_to_json(val: &am::ScalarValue) -> serde_json::Value {
|
||||
match val {
|
||||
am::ScalarValue::Str(s) => serde_json::Value::String(s.to_string()),
|
||||
am::ScalarValue::Bytes(b) | am::ScalarValue::Unknown { bytes: b, .. } => {
|
||||
serde_json::Value::Array(
|
||||
b.iter()
|
||||
.map(|byte| serde_json::Value::Number((*byte).into()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
am::ScalarValue::Bytes(b) => serde_json::Value::Array(
|
||||
b.iter()
|
||||
.map(|byte| serde_json::Value::Number((*byte).into()))
|
||||
.collect(),
|
||||
),
|
||||
am::ScalarValue::Int(n) => serde_json::Value::Number((*n).into()),
|
||||
am::ScalarValue::Uint(n) => serde_json::Value::Number((*n).into()),
|
||||
am::ScalarValue::F64(n) => serde_json::Number::from_f64(*n)
|
||||
|
@ -72,23 +67,22 @@ fn scalar_to_json(val: &am::ScalarValue) -> serde_json::Value {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_state_json(input_data: Vec<u8>, skip: SkipVerifyFlag) -> Result<serde_json::Value> {
|
||||
let doc = skip.load(&input_data).unwrap(); // FIXME
|
||||
fn get_state_json(input_data: Vec<u8>) -> Result<serde_json::Value> {
|
||||
let doc = am::Automerge::load(&input_data).unwrap(); // FIXME
|
||||
Ok(map_to_json(&doc, &am::ObjId::Root))
|
||||
}
|
||||
|
||||
pub(crate) fn export_json(
|
||||
pub fn export_json(
|
||||
mut changes_reader: impl std::io::Read,
|
||||
mut writer: impl std::io::Write,
|
||||
skip: SkipVerifyFlag,
|
||||
is_tty: bool,
|
||||
) -> Result<()> {
|
||||
let mut input_data = vec![];
|
||||
changes_reader.read_to_end(&mut input_data)?;
|
||||
|
||||
let state_json = get_state_json(input_data, skip)?;
|
||||
let state_json = get_state_json(input_data)?;
|
||||
if is_tty {
|
||||
print_colored_json(&state_json).unwrap();
|
||||
colored_json::write_colored_json(&state_json, &mut writer).unwrap();
|
||||
writeln!(writer).unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
|
@ -107,10 +101,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn cli_export_with_empty_input() {
|
||||
assert_eq!(
|
||||
get_state_json(vec![], Default::default()).unwrap(),
|
||||
serde_json::json!({})
|
||||
)
|
||||
assert_eq!(get_state_json(vec![]).unwrap(), serde_json::json!({}))
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -124,7 +115,7 @@ mod tests {
|
|||
let mut backend = initialize_from_json(&initial_state_json).unwrap();
|
||||
let change_bytes = backend.save();
|
||||
assert_eq!(
|
||||
get_state_json(change_bytes, Default::default()).unwrap(),
|
||||
get_state_json(change_bytes).unwrap(),
|
||||
serde_json::json!({"sparrows": 15.0})
|
||||
)
|
||||
}
|
||||
|
@ -151,7 +142,7 @@ mod tests {
|
|||
*/
|
||||
let change_bytes = backend.save();
|
||||
assert_eq!(
|
||||
get_state_json(change_bytes, Default::default()).unwrap(),
|
||||
get_state_json(change_bytes).unwrap(),
|
||||
serde_json::json!({
|
||||
"birds": {
|
||||
"wrens": 3.0,
|
|
@ -3,14 +3,14 @@ use automerge::transaction::Transactable;
|
|||
|
||||
pub(crate) fn initialize_from_json(
|
||||
json_value: &serde_json::Value,
|
||||
) -> anyhow::Result<am::AutoCommit> {
|
||||
) -> Result<am::AutoCommit, am::AutomergeError> {
|
||||
let mut doc = am::AutoCommit::new();
|
||||
match json_value {
|
||||
serde_json::Value::Object(m) => {
|
||||
import_map(&mut doc, &am::ObjId::Root, m)?;
|
||||
Ok(doc)
|
||||
}
|
||||
_ => anyhow::bail!("expected an object"),
|
||||
_ => Err(am::AutomergeError::Decoding),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,35 +18,35 @@ fn import_map(
|
|||
doc: &mut am::AutoCommit,
|
||||
obj: &am::ObjId,
|
||||
map: &serde_json::Map<String, serde_json::Value>,
|
||||
) -> anyhow::Result<()> {
|
||||
) -> Result<(), am::AutomergeError> {
|
||||
for (key, value) in map {
|
||||
match value {
|
||||
serde_json::Value::Null => {
|
||||
doc.put(obj, key, ())?;
|
||||
doc.set(obj, key, ())?;
|
||||
}
|
||||
serde_json::Value::Bool(b) => {
|
||||
doc.put(obj, key, *b)?;
|
||||
doc.set(obj, key, *b)?;
|
||||
}
|
||||
serde_json::Value::String(s) => {
|
||||
doc.put(obj, key, s)?;
|
||||
doc.set(obj, key, s.as_ref())?;
|
||||
}
|
||||
serde_json::Value::Array(vec) => {
|
||||
let id = doc.put_object(obj, key, am::ObjType::List)?;
|
||||
let id = doc.set_object(obj, key, am::ObjType::List)?;
|
||||
import_list(doc, &id, vec)?;
|
||||
}
|
||||
serde_json::Value::Number(n) => {
|
||||
if let Some(m) = n.as_i64() {
|
||||
doc.put(obj, key, m)?;
|
||||
doc.set(obj, key, m)?;
|
||||
} else if let Some(m) = n.as_u64() {
|
||||
doc.put(obj, key, m)?;
|
||||
doc.set(obj, key, m)?;
|
||||
} else if let Some(m) = n.as_f64() {
|
||||
doc.put(obj, key, m)?;
|
||||
doc.set(obj, key, m)?;
|
||||
} else {
|
||||
anyhow::bail!("not a number");
|
||||
return Err(am::AutomergeError::Decoding);
|
||||
}
|
||||
}
|
||||
serde_json::Value::Object(map) => {
|
||||
let id = doc.put_object(obj, key, am::ObjType::Map)?;
|
||||
let id = doc.set_object(obj, key, am::ObjType::Map)?;
|
||||
import_map(doc, &id, map)?;
|
||||
}
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ fn import_list(
|
|||
doc: &mut am::AutoCommit,
|
||||
obj: &am::ObjId,
|
||||
list: &[serde_json::Value],
|
||||
) -> anyhow::Result<()> {
|
||||
) -> Result<(), am::AutomergeError> {
|
||||
for (i, value) in list.iter().enumerate() {
|
||||
match value {
|
||||
serde_json::Value::Null => {
|
||||
|
@ -68,7 +68,7 @@ fn import_list(
|
|||
doc.insert(obj, i, *b)?;
|
||||
}
|
||||
serde_json::Value::String(s) => {
|
||||
doc.insert(obj, i, s)?;
|
||||
doc.insert(obj, i, s.as_ref())?;
|
||||
}
|
||||
serde_json::Value::Array(vec) => {
|
||||
let id = doc.insert_object(obj, i, am::ObjType::List)?;
|
||||
|
@ -82,7 +82,7 @@ fn import_list(
|
|||
} else if let Some(m) = n.as_f64() {
|
||||
doc.insert(obj, i, m)?;
|
||||
} else {
|
||||
anyhow::bail!("not a number");
|
||||
return Err(am::AutomergeError::Decoding);
|
||||
}
|
||||
}
|
||||
serde_json::Value::Object(map) => {
|
|
@ -1,15 +1,10 @@
|
|||
use std::{fs::File, path::PathBuf, str::FromStr};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use clap::{
|
||||
builder::{BoolishValueParser, TypedValueParser, ValueParserFactory},
|
||||
Parser,
|
||||
};
|
||||
use is_terminal::IsTerminal;
|
||||
use clap::Parser;
|
||||
|
||||
mod color_json;
|
||||
//mod change;
|
||||
mod examine;
|
||||
mod examine_sync;
|
||||
mod export;
|
||||
mod import;
|
||||
mod merge;
|
||||
|
@ -21,50 +16,12 @@ struct Opts {
|
|||
cmd: Command,
|
||||
}
|
||||
|
||||
#[derive(clap::ValueEnum, Clone, Debug)]
|
||||
#[derive(Debug)]
|
||||
enum ExportFormat {
|
||||
Json,
|
||||
Toml,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug)]
|
||||
pub(crate) struct SkipVerifyFlag(bool);
|
||||
|
||||
impl SkipVerifyFlag {
|
||||
fn load(&self, buf: &[u8]) -> Result<automerge::Automerge, automerge::AutomergeError> {
|
||||
if self.0 {
|
||||
automerge::Automerge::load(buf)
|
||||
} else {
|
||||
automerge::Automerge::load_unverified_heads(buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SkipVerifyFlagParser;
|
||||
impl ValueParserFactory for SkipVerifyFlag {
|
||||
type Parser = SkipVerifyFlagParser;
|
||||
|
||||
fn value_parser() -> Self::Parser {
|
||||
SkipVerifyFlagParser
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedValueParser for SkipVerifyFlagParser {
|
||||
type Value = SkipVerifyFlag;
|
||||
|
||||
fn parse_ref(
|
||||
&self,
|
||||
cmd: &clap::Command,
|
||||
arg: Option<&clap::Arg>,
|
||||
value: &std::ffi::OsStr,
|
||||
) -> Result<Self::Value, clap::Error> {
|
||||
BoolishValueParser::new()
|
||||
.parse_ref(cmd, arg, value)
|
||||
.map(SkipVerifyFlag)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ExportFormat {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
|
@ -86,15 +43,12 @@ enum Command {
|
|||
format: ExportFormat,
|
||||
|
||||
/// Path that contains Automerge changes
|
||||
#[clap(parse(from_os_str))]
|
||||
changes_file: Option<PathBuf>,
|
||||
|
||||
/// The file to write to. If omitted assumes stdout
|
||||
#[clap(long("out"), short('o'))]
|
||||
#[clap(parse(from_os_str), long("out"), short('o'))]
|
||||
output_file: Option<PathBuf>,
|
||||
|
||||
/// Whether to verify the head hashes of a compressed document
|
||||
#[clap(long, action = clap::ArgAction::SetFalse)]
|
||||
skip_verifying_heads: SkipVerifyFlag,
|
||||
},
|
||||
|
||||
Import {
|
||||
|
@ -102,37 +56,69 @@ enum Command {
|
|||
#[clap(long, short, default_value = "json")]
|
||||
format: ExportFormat,
|
||||
|
||||
#[clap(parse(from_os_str))]
|
||||
input_file: Option<PathBuf>,
|
||||
|
||||
/// Path to write Automerge changes to
|
||||
#[clap(long("out"), short('o'))]
|
||||
#[clap(parse(from_os_str), long("out"), short('o'))]
|
||||
changes_file: Option<PathBuf>,
|
||||
},
|
||||
|
||||
/// Read an automerge document and print a JSON representation of the changes in it to stdout
|
||||
Examine {
|
||||
/// Read an automerge document from a file or stdin, perform a change on it and write a new
|
||||
/// document to stdout or the specified output file.
|
||||
Change {
|
||||
/// The change script to perform. Change scripts have the form <command> <path> [<JSON value>].
|
||||
/// The possible commands are 'set', 'insert', 'delete', and 'increment'.
|
||||
///
|
||||
/// Paths look like this: $["mapkey"][0]. They always lways start with a '$', then each
|
||||
/// subsequent segment of the path is either a string in double quotes to index a key in a
|
||||
/// map, or an integer index to address an array element.
|
||||
///
|
||||
/// Examples
|
||||
///
|
||||
/// ## set
|
||||
///
|
||||
/// > automerge change 'set $["someobject"] {"items": []}' somefile
|
||||
///
|
||||
/// ## insert
|
||||
///
|
||||
/// > automerge change 'insert $["someobject"]["items"][0] "item1"' somefile
|
||||
///
|
||||
/// ## increment
|
||||
///
|
||||
/// > automerge change 'increment $["mycounter"]'
|
||||
///
|
||||
/// ## delete
|
||||
///
|
||||
/// > automerge change 'delete $["someobject"]["items"]' somefile
|
||||
script: String,
|
||||
|
||||
/// The file to change, if omitted will assume stdin
|
||||
#[clap(parse(from_os_str))]
|
||||
input_file: Option<PathBuf>,
|
||||
skip_verifying_heads: SkipVerifyFlag,
|
||||
|
||||
/// Path to write Automerge changes to, if omitted will write to stdout
|
||||
#[clap(parse(from_os_str), long("out"), short('o'))]
|
||||
output_file: Option<PathBuf>,
|
||||
},
|
||||
|
||||
/// Read an automerge sync messaage and print a JSON representation of it
|
||||
ExamineSync { input_file: Option<PathBuf> },
|
||||
/// Read an automerge document and print a JSON representation of the changes in it to stdout
|
||||
Examine { input_file: Option<PathBuf> },
|
||||
|
||||
/// Read one or more automerge documents and output a merged, compacted version of them
|
||||
Merge {
|
||||
/// The file to write to. If omitted assumes stdout
|
||||
#[clap(long("out"), short('o'))]
|
||||
#[clap(parse(from_os_str), long("out"), short('o'))]
|
||||
output_file: Option<PathBuf>,
|
||||
|
||||
/// The file(s) to compact. If empty assumes stdin
|
||||
input: Vec<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
fn open_file_or_stdin(maybe_path: Option<PathBuf>) -> Result<Box<dyn std::io::Read>> {
|
||||
if std::io::stdin().is_terminal() {
|
||||
if atty::is(atty::Stream::Stdin) {
|
||||
if let Some(path) = maybe_path {
|
||||
Ok(Box::new(File::open(path).unwrap()))
|
||||
Ok(Box::new(File::open(&path).unwrap()))
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Must provide file path if not providing input via stdin"
|
||||
|
@ -144,9 +130,9 @@ fn open_file_or_stdin(maybe_path: Option<PathBuf>) -> Result<Box<dyn std::io::Re
|
|||
}
|
||||
|
||||
fn create_file_or_stdout(maybe_path: Option<PathBuf>) -> Result<Box<dyn std::io::Write>> {
|
||||
if std::io::stdout().is_terminal() {
|
||||
if atty::is(atty::Stream::Stdout) {
|
||||
if let Some(path) = maybe_path {
|
||||
Ok(Box::new(File::create(path).unwrap()))
|
||||
Ok(Box::new(File::create(&path).unwrap()))
|
||||
} else {
|
||||
Err(anyhow!("Must provide file path if not piping to stdout"))
|
||||
}
|
||||
|
@ -163,22 +149,16 @@ fn main() -> Result<()> {
|
|||
changes_file,
|
||||
format,
|
||||
output_file,
|
||||
skip_verifying_heads,
|
||||
} => {
|
||||
let output: Box<dyn std::io::Write> = if let Some(output_file) = output_file {
|
||||
Box::new(File::create(output_file)?)
|
||||
Box::new(File::create(&output_file)?)
|
||||
} else {
|
||||
Box::new(std::io::stdout())
|
||||
};
|
||||
match format {
|
||||
ExportFormat::Json => {
|
||||
let mut in_buffer = open_file_or_stdin(changes_file)?;
|
||||
export::export_json(
|
||||
&mut in_buffer,
|
||||
output,
|
||||
skip_verifying_heads,
|
||||
std::io::stdout().is_terminal(),
|
||||
)
|
||||
export::export_json(&mut in_buffer, output, atty::is(atty::Stream::Stdout))
|
||||
}
|
||||
ExportFormat::Toml => unimplemented!(),
|
||||
}
|
||||
|
@ -195,30 +175,23 @@ fn main() -> Result<()> {
|
|||
}
|
||||
ExportFormat::Toml => unimplemented!(),
|
||||
},
|
||||
Command::Examine {
|
||||
input_file,
|
||||
skip_verifying_heads,
|
||||
Command::Change { ..
|
||||
//input_file,
|
||||
//output_file,
|
||||
//script,
|
||||
} => {
|
||||
unimplemented!()
|
||||
/*
|
||||
let in_buffer = open_file_or_stdin(input_file)?;
|
||||
let out_buffer = std::io::stdout();
|
||||
match examine::examine(
|
||||
in_buffer,
|
||||
out_buffer,
|
||||
skip_verifying_heads,
|
||||
std::io::stdout().is_terminal(),
|
||||
) {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
eprintln!("Error: {:?}", e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
let mut out_buffer = create_file_or_stdout(output_file)?;
|
||||
change::change(in_buffer, &mut out_buffer, script.as_str())
|
||||
.map_err(|e| anyhow::format_err!("Unable to make changes: {:?}", e))
|
||||
*/
|
||||
}
|
||||
Command::ExamineSync { input_file } => {
|
||||
Command::Examine { input_file } => {
|
||||
let in_buffer = open_file_or_stdin(input_file)?;
|
||||
let out_buffer = std::io::stdout();
|
||||
match examine_sync::examine_sync(in_buffer, out_buffer, std::io::stdout().is_terminal())
|
||||
{
|
||||
match examine::examine(in_buffer, out_buffer, atty::is(atty::Stream::Stdout)) {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
eprintln!("Error: {:?}", e);
|
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) {
|
||||
if (typeof actor != 'string') {
|
||||
actor = null
|
||||
}
|
||||
const state = AutomergeWASM.create(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) {
|
||||
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.pendingOps() === 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.loadDoc(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)
|
||||
break;
|
||||
case "list":
|
||||
result[value] = listProxy(context, value, [ prop ], true)
|
||||
break;
|
||||
case "text":
|
||||
result[value] = textProxy(context, value, [ prop ], 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]
|
||||
try {
|
||||
return state.getLastLocalChange()
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
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(AutomergeWASM.importSyncState(state))
|
||||
}
|
||||
|
||||
function decodeSyncState(state) {
|
||||
return AutomergeWASM.exportSyncState(AutomergeWASM.decodeSyncState(state))
|
||||
}
|
||||
|
||||
function generateSyncMessage(doc, inState) {
|
||||
const state = doc[STATE]
|
||||
const syncState = AutomergeWASM.importSyncState(inState)
|
||||
const message = state.generateSyncMessage(syncState)
|
||||
const outState = AutomergeWASM.exportSyncState(syncState)
|
||||
return [ outState, message ]
|
||||
}
|
||||
|
||||
function receiveSyncMessage(doc, inState, message) {
|
||||
const syncState = AutomergeWASM.importSyncState(inState)
|
||||
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)
|
||||
const outState = AutomergeWASM.exportSyncState(syncState)
|
||||
doc[HEADS] = heads
|
||||
return [rootProxy(state, true), outState, null];
|
||||
}
|
||||
|
||||
function initSyncState() {
|
||||
return AutomergeWASM.exportSyncState(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 getMissingDeps(doc, heads) {
|
||||
const state = doc[STATE]
|
||||
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")
|
||||
|
||||
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 "timestamp": return val;
|
||||
case "counter": {
|
||||
if (readonly) {
|
||||
return new Counter(val);
|
||||
} else {
|
||||
return getWriteableCounter(val, context, path, objectId, prop)
|
||||
}
|
||||
}
|
||||
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, cache } = 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;
|
||||
if (!cache[key]) {
|
||||
cache[key] = valueAt(target, key)
|
||||
}
|
||||
return cache[key]
|
||||
},
|
||||
|
||||
set (target, key, val) {
|
||||
let { context, objectId, path, readonly, frozen} = target
|
||||
target.cache = {} // reset cache on set
|
||||
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_object(objectId, key, [])
|
||||
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_object(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_object(objectId, key, {})
|
||||
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
|
||||
target.cache = {} // reset cache on delete
|
||||
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_object(objectId, index, [])
|
||||
} else {
|
||||
list = context.set_object(objectId, index, [])
|
||||
}
|
||||
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_object(objectId, index, "", "text")
|
||||
} else {
|
||||
text = context.set_object(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_object(objectId, index, {})
|
||||
} else {
|
||||
map = context.set_object(objectId, index, {})
|
||||
}
|
||||
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, cache: {}}, MapHandler)
|
||||
}
|
||||
|
||||
function listProxy(context, objectId, path, readonly, heads) {
|
||||
let target = []
|
||||
Object.assign(target, {context, objectId, path, readonly: !!readonly, frozen: false, heads, cache: {}})
|
||||
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, cache: {}})
|
||||
return new Proxy(target, TextHandler)
|
||||
}
|
||||
|
||||
function rootProxy(context, readonly) {
|
||||
return mapProxy(context, "_root", [], readonly)
|
||||
}
|
||||
|
||||
function listMethods(target) {
|
||||
const {context, objectId, path, readonly, frozen, heads} = target
|
||||
const methods = {
|
||||
deleteAt(index, numDelete) {
|
||||
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_object(objectId, index, [])
|
||||
const proxyList = listProxy(context, list, [ ... path, index ], readonly);
|
||||
proxyList.splice(0,0,...value)
|
||||
break;
|
||||
case "text":
|
||||
const text = context.insert_object(objectId, index, "", "text")
|
||||
const proxyText = textProxy(context, text, [ ... path, index ], readonly);
|
||||
proxyText.splice(0,0,...value)
|
||||
break;
|
||||
case "map":
|
||||
const map = context.insert_object(objectId, index, {})
|
||||
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 }
|
|
@ -16,15 +16,11 @@
|
|||
* last sync to disk), and we fall back to sending the entire document in this case.
|
||||
*/
|
||||
|
||||
const Backend = null //require('./backend')
|
||||
const {
|
||||
hexStringToBytes,
|
||||
bytesToHexString,
|
||||
Encoder,
|
||||
Decoder,
|
||||
} = require("./encoding")
|
||||
const { decodeChangeMeta } = require("./columnar")
|
||||
const { copyObject } = require("./common")
|
||||
//const Backend = require('./backend')
|
||||
const Backend = {} //require('./backend')
|
||||
const { hexStringToBytes, bytesToHexString, Encoder, Decoder } = require('./encoding')
|
||||
const { decodeChangeMeta } = require('./columnar')
|
||||
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
|
||||
|
@ -33,8 +29,7 @@ const PEER_STATE_TYPE = 0x43 // first byte of an encoded peer state, for identif
|
|||
// These constants correspond to a 1% false positive rate. The values can be changed without
|
||||
// breaking compatibility of the network protocol, since the parameters used for a particular
|
||||
// Bloom filter are encoded in the wire format.
|
||||
const BITS_PER_ENTRY = 10,
|
||||
NUM_PROBES = 7
|
||||
const BITS_PER_ENTRY = 10, NUM_PROBES = 7
|
||||
|
||||
/**
|
||||
* A Bloom filter implementation that can be serialised to a byte array for transmission
|
||||
|
@ -42,15 +37,13 @@ const BITS_PER_ENTRY = 10,
|
|||
* so this implementation does not perform its own hashing.
|
||||
*/
|
||||
class BloomFilter {
|
||||
constructor(arg) {
|
||||
constructor (arg) {
|
||||
if (Array.isArray(arg)) {
|
||||
// arg is an array of SHA256 hashes in hexadecimal encoding
|
||||
this.numEntries = arg.length
|
||||
this.numBitsPerEntry = BITS_PER_ENTRY
|
||||
this.numProbes = NUM_PROBES
|
||||
this.bits = new Uint8Array(
|
||||
Math.ceil((this.numEntries * this.numBitsPerEntry) / 8)
|
||||
)
|
||||
this.bits = new Uint8Array(Math.ceil(this.numEntries * this.numBitsPerEntry / 8))
|
||||
for (let hash of arg) this.addHash(hash)
|
||||
} else if (arg instanceof Uint8Array) {
|
||||
if (arg.byteLength === 0) {
|
||||
|
@ -63,12 +56,10 @@ class BloomFilter {
|
|||
this.numEntries = decoder.readUint32()
|
||||
this.numBitsPerEntry = decoder.readUint32()
|
||||
this.numProbes = decoder.readUint32()
|
||||
this.bits = decoder.readRawBytes(
|
||||
Math.ceil((this.numEntries * this.numBitsPerEntry) / 8)
|
||||
)
|
||||
this.bits = decoder.readRawBytes(Math.ceil(this.numEntries * this.numBitsPerEntry / 8))
|
||||
}
|
||||
} else {
|
||||
throw new TypeError("invalid argument")
|
||||
throw new TypeError('invalid argument')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,32 +87,12 @@ class BloomFilter {
|
|||
* http://www.ccis.northeastern.edu/home/pete/pub/bloom-filters-verification.pdf
|
||||
*/
|
||||
getProbes(hash) {
|
||||
const hashBytes = hexStringToBytes(hash),
|
||||
modulo = 8 * this.bits.byteLength
|
||||
if (hashBytes.byteLength !== 32)
|
||||
throw new RangeError(`Not a 256-bit hash: ${hash}`)
|
||||
const hashBytes = hexStringToBytes(hash), modulo = 8 * this.bits.byteLength
|
||||
if (hashBytes.byteLength !== 32) throw new RangeError(`Not a 256-bit hash: ${hash}`)
|
||||
// on the next three lines, the right shift means interpret value as unsigned
|
||||
let x =
|
||||
((hashBytes[0] |
|
||||
(hashBytes[1] << 8) |
|
||||
(hashBytes[2] << 16) |
|
||||
(hashBytes[3] << 24)) >>>
|
||||
0) %
|
||||
modulo
|
||||
let y =
|
||||
((hashBytes[4] |
|
||||
(hashBytes[5] << 8) |
|
||||
(hashBytes[6] << 16) |
|
||||
(hashBytes[7] << 24)) >>>
|
||||
0) %
|
||||
modulo
|
||||
let z =
|
||||
((hashBytes[8] |
|
||||
(hashBytes[9] << 8) |
|
||||
(hashBytes[10] << 16) |
|
||||
(hashBytes[11] << 24)) >>>
|
||||
0) %
|
||||
modulo
|
||||
let x = ((hashBytes[0] | hashBytes[1] << 8 | hashBytes[2] << 16 | hashBytes[3] << 24) >>> 0) % modulo
|
||||
let y = ((hashBytes[4] | hashBytes[5] << 8 | hashBytes[6] << 16 | hashBytes[7] << 24) >>> 0) % modulo
|
||||
let z = ((hashBytes[8] | hashBytes[9] << 8 | hashBytes[10] << 16 | hashBytes[11] << 24) >>> 0) % modulo
|
||||
const probes = [x]
|
||||
for (let i = 1; i < this.numProbes; i++) {
|
||||
x = (x + y) % modulo
|
||||
|
@ -158,14 +129,12 @@ class BloomFilter {
|
|||
* Encodes a sorted array of SHA-256 hashes (as hexadecimal strings) into a byte array.
|
||||
*/
|
||||
function encodeHashes(encoder, hashes) {
|
||||
if (!Array.isArray(hashes)) throw new TypeError("hashes must be an array")
|
||||
if (!Array.isArray(hashes)) throw new TypeError('hashes must be an array')
|
||||
encoder.appendUint32(hashes.length)
|
||||
for (let i = 0; i < hashes.length; i++) {
|
||||
if (i > 0 && hashes[i - 1] >= hashes[i])
|
||||
throw new RangeError("hashes must be sorted")
|
||||
if (i > 0 && hashes[i - 1] >= hashes[i]) throw new RangeError('hashes must be sorted')
|
||||
const bytes = hexStringToBytes(hashes[i])
|
||||
if (bytes.byteLength !== HASH_SIZE)
|
||||
throw new TypeError("heads hashes must be 256 bits")
|
||||
if (bytes.byteLength !== HASH_SIZE) throw new TypeError('heads hashes must be 256 bits')
|
||||
encoder.appendRawBytes(bytes)
|
||||
}
|
||||
}
|
||||
|
@ -175,8 +144,7 @@ function encodeHashes(encoder, hashes) {
|
|||
* array of hex strings.
|
||||
*/
|
||||
function decodeHashes(decoder) {
|
||||
let length = decoder.readUint32(),
|
||||
hashes = []
|
||||
let length = decoder.readUint32(), hashes = []
|
||||
for (let i = 0; i < length; i++) {
|
||||
hashes.push(bytesToHexString(decoder.readRawBytes(HASH_SIZE)))
|
||||
}
|
||||
|
@ -216,11 +184,11 @@ function decodeSyncMessage(bytes) {
|
|||
const heads = decodeHashes(decoder)
|
||||
const need = decodeHashes(decoder)
|
||||
const haveCount = decoder.readUint32()
|
||||
let message = { heads, need, have: [], changes: [] }
|
||||
let message = {heads, need, have: [], changes: []}
|
||||
for (let i = 0; i < haveCount; i++) {
|
||||
const lastSync = decodeHashes(decoder)
|
||||
const bloom = decoder.readPrefixedBytes(decoder)
|
||||
message.have.push({ lastSync, bloom })
|
||||
message.have.push({lastSync, bloom})
|
||||
}
|
||||
const changeCount = decoder.readUint32()
|
||||
for (let i = 0; i < changeCount; i++) {
|
||||
|
@ -267,7 +235,7 @@ function decodeSyncState(bytes) {
|
|||
function makeBloomFilter(backend, lastSync) {
|
||||
const newChanges = Backend.getChanges(backend, lastSync)
|
||||
const hashes = newChanges.map(change => decodeChangeMeta(change, true).hash)
|
||||
return { lastSync, bloom: new BloomFilter(hashes).bytes }
|
||||
return {lastSync, bloom: new BloomFilter(hashes).bytes}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -278,26 +246,20 @@ function makeBloomFilter(backend, lastSync) {
|
|||
*/
|
||||
function getChangesToSend(backend, have, need) {
|
||||
if (have.length === 0) {
|
||||
return need
|
||||
.map(hash => Backend.getChangeByHash(backend, hash))
|
||||
.filter(change => change !== undefined)
|
||||
return need.map(hash => Backend.getChangeByHash(backend, hash)).filter(change => change !== undefined)
|
||||
}
|
||||
|
||||
let lastSyncHashes = {},
|
||||
bloomFilters = []
|
||||
let lastSyncHashes = {}, bloomFilters = []
|
||||
for (let h of have) {
|
||||
for (let hash of h.lastSync) lastSyncHashes[hash] = true
|
||||
bloomFilters.push(new BloomFilter(h.bloom))
|
||||
}
|
||||
|
||||
// Get all changes that were added since the last sync
|
||||
const changes = Backend.getChanges(backend, Object.keys(lastSyncHashes)).map(
|
||||
change => decodeChangeMeta(change, true)
|
||||
)
|
||||
const changes = Backend.getChanges(backend, Object.keys(lastSyncHashes))
|
||||
.map(change => decodeChangeMeta(change, true))
|
||||
|
||||
let changeHashes = {},
|
||||
dependents = {},
|
||||
hashesToSend = {}
|
||||
let changeHashes = {}, dependents = {}, hashesToSend = {}
|
||||
for (let change of changes) {
|
||||
changeHashes[change.hash] = true
|
||||
|
||||
|
@ -331,8 +293,7 @@ function getChangesToSend(backend, have, need) {
|
|||
let changesToSend = []
|
||||
for (let hash of need) {
|
||||
hashesToSend[hash] = true
|
||||
if (!changeHashes[hash]) {
|
||||
// Change is not among those returned by getMissingChanges()?
|
||||
if (!changeHashes[hash]) { // Change is not among those returned by getMissingChanges()?
|
||||
const change = Backend.getChangeByHash(backend, hash)
|
||||
if (change) changesToSend.push(change)
|
||||
}
|
||||
|
@ -357,7 +318,7 @@ function initSyncState() {
|
|||
}
|
||||
|
||||
function compareArrays(a, b) {
|
||||
return a.length === b.length && a.every((v, i) => v === b[i])
|
||||
return (a.length === b.length) && a.every((v, i) => v === b[i])
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -369,19 +330,10 @@ function generateSyncMessage(backend, syncState) {
|
|||
throw new Error("generateSyncMessage called with no Automerge document")
|
||||
}
|
||||
if (!syncState) {
|
||||
throw new Error(
|
||||
"generateSyncMessage requires a syncState, which can be created with initSyncState()"
|
||||
)
|
||||
throw new Error("generateSyncMessage requires a syncState, which can be created with initSyncState()")
|
||||
}
|
||||
|
||||
let {
|
||||
sharedHeads,
|
||||
lastSentHeads,
|
||||
theirHeads,
|
||||
theirNeed,
|
||||
theirHave,
|
||||
sentHashes,
|
||||
} = syncState
|
||||
let { sharedHeads, lastSentHeads, theirHeads, theirNeed, theirHave, sentHashes } = syncState
|
||||
const ourHeads = Backend.getHeads(backend)
|
||||
|
||||
// Hashes to explicitly request from the remote peer: any missing dependencies of unapplied
|
||||
|
@ -405,28 +357,18 @@ function generateSyncMessage(backend, syncState) {
|
|||
const lastSync = theirHave[0].lastSync
|
||||
if (!lastSync.every(hash => Backend.getChangeByHash(backend, hash))) {
|
||||
// we need to queue them to send us a fresh sync message, the one they sent is uninteligible so we don't know what they need
|
||||
const resetMsg = {
|
||||
heads: ourHeads,
|
||||
need: [],
|
||||
have: [{ lastSync: [], bloom: new Uint8Array(0) }],
|
||||
changes: [],
|
||||
}
|
||||
const resetMsg = {heads: ourHeads, need: [], have: [{ lastSync: [], bloom: new Uint8Array(0) }], changes: []}
|
||||
return [syncState, encodeSyncMessage(resetMsg)]
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: we should limit ourselves to only sending a subset of all the messages, probably limited by a total message size
|
||||
// these changes should ideally be RLE encoded but we haven't implemented that yet.
|
||||
let changesToSend =
|
||||
Array.isArray(theirHave) && Array.isArray(theirNeed)
|
||||
? getChangesToSend(backend, theirHave, theirNeed)
|
||||
: []
|
||||
let changesToSend = Array.isArray(theirHave) && Array.isArray(theirNeed) ? getChangesToSend(backend, theirHave, theirNeed) : []
|
||||
|
||||
// If the heads are equal, we're in sync and don't need to do anything further
|
||||
const headsUnchanged =
|
||||
Array.isArray(lastSentHeads) && compareArrays(ourHeads, lastSentHeads)
|
||||
const headsEqual =
|
||||
Array.isArray(theirHeads) && compareArrays(ourHeads, theirHeads)
|
||||
const headsUnchanged = Array.isArray(lastSentHeads) && compareArrays(ourHeads, lastSentHeads)
|
||||
const headsEqual = Array.isArray(theirHeads) && compareArrays(ourHeads, theirHeads)
|
||||
if (headsUnchanged && headsEqual && changesToSend.length === 0) {
|
||||
// no need to send a sync message if we know we're synced!
|
||||
return [syncState, null]
|
||||
|
@ -434,19 +376,12 @@ function generateSyncMessage(backend, syncState) {
|
|||
|
||||
// TODO: this recomputes the SHA-256 hash of each change; we should restructure this to avoid the
|
||||
// unnecessary recomputation
|
||||
changesToSend = changesToSend.filter(
|
||||
change => !sentHashes[decodeChangeMeta(change, true).hash]
|
||||
)
|
||||
changesToSend = changesToSend.filter(change => !sentHashes[decodeChangeMeta(change, true).hash])
|
||||
|
||||
// Regular response to a sync message: send any changes that the other node
|
||||
// doesn't have. We leave the "have" field empty because the previous message
|
||||
// generated by `syncStart` already indicated what changes we have.
|
||||
const syncMessage = {
|
||||
heads: ourHeads,
|
||||
have: ourHave,
|
||||
need: ourNeed,
|
||||
changes: changesToSend,
|
||||
}
|
||||
const syncMessage = {heads: ourHeads, have: ourHave, need: ourNeed, changes: changesToSend}
|
||||
if (changesToSend.length > 0) {
|
||||
sentHashes = copyObject(sentHashes)
|
||||
for (const change of changesToSend) {
|
||||
|
@ -454,10 +389,7 @@ function generateSyncMessage(backend, syncState) {
|
|||
}
|
||||
}
|
||||
|
||||
syncState = Object.assign({}, syncState, {
|
||||
lastSentHeads: ourHeads,
|
||||
sentHashes,
|
||||
})
|
||||
syncState = Object.assign({}, syncState, {lastSentHeads: ourHeads, sentHashes})
|
||||
return [syncState, encodeSyncMessage(syncMessage)]
|
||||
}
|
||||
|
||||
|
@ -475,14 +407,13 @@ function generateSyncMessage(backend, syncState) {
|
|||
* another peer, that means that peer had those changes, and therefore we now both know about them.
|
||||
*/
|
||||
function advanceHeads(myOldHeads, myNewHeads, ourOldSharedHeads) {
|
||||
const newHeads = myNewHeads.filter(head => !myOldHeads.includes(head))
|
||||
const commonHeads = ourOldSharedHeads.filter(head =>
|
||||
myNewHeads.includes(head)
|
||||
)
|
||||
const newHeads = myNewHeads.filter((head) => !myOldHeads.includes(head))
|
||||
const commonHeads = ourOldSharedHeads.filter((head) => myNewHeads.includes(head))
|
||||
const advancedHeads = [...new Set([...newHeads, ...commonHeads])].sort()
|
||||
return advancedHeads
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given a backend, a message message and the state of our peer, apply any changes, update what
|
||||
* we believe about the peer, and (if there were applied changes) produce a patch for the frontend
|
||||
|
@ -492,13 +423,10 @@ function receiveSyncMessage(backend, oldSyncState, binaryMessage) {
|
|||
throw new Error("generateSyncMessage called with no Automerge document")
|
||||
}
|
||||
if (!oldSyncState) {
|
||||
throw new Error(
|
||||
"generateSyncMessage requires a syncState, which can be created with initSyncState()"
|
||||
)
|
||||
throw new Error("generateSyncMessage requires a syncState, which can be created with initSyncState()")
|
||||
}
|
||||
|
||||
let { sharedHeads, lastSentHeads, sentHashes } = oldSyncState,
|
||||
patch = null
|
||||
let { sharedHeads, lastSentHeads, sentHashes } = oldSyncState, patch = null
|
||||
const message = decodeSyncMessage(binaryMessage)
|
||||
const beforeHeads = Backend.getHeads(backend)
|
||||
|
||||
|
@ -507,27 +435,18 @@ function receiveSyncMessage(backend, oldSyncState, binaryMessage) {
|
|||
// changes without applying them. The set of changes may also be incomplete if the sender decided
|
||||
// to break a large set of changes into chunks.
|
||||
if (message.changes.length > 0) {
|
||||
;[backend, patch] = Backend.applyChanges(backend, message.changes)
|
||||
sharedHeads = advanceHeads(
|
||||
beforeHeads,
|
||||
Backend.getHeads(backend),
|
||||
sharedHeads
|
||||
)
|
||||
[backend, patch] = Backend.applyChanges(backend, message.changes)
|
||||
sharedHeads = advanceHeads(beforeHeads, Backend.getHeads(backend), sharedHeads)
|
||||
}
|
||||
|
||||
// If heads are equal, indicate we don't need to send a response message
|
||||
if (
|
||||
message.changes.length === 0 &&
|
||||
compareArrays(message.heads, beforeHeads)
|
||||
) {
|
||||
if (message.changes.length === 0 && compareArrays(message.heads, beforeHeads)) {
|
||||
lastSentHeads = message.heads
|
||||
}
|
||||
|
||||
// If all of the remote heads are known to us, that means either our heads are equal, or we are
|
||||
// ahead of the remote peer. In this case, take the remote heads to be our shared heads.
|
||||
const knownHeads = message.heads.filter(head =>
|
||||
Backend.getChangeByHash(backend, head)
|
||||
)
|
||||
const knownHeads = message.heads.filter(head => Backend.getChangeByHash(backend, head))
|
||||
if (knownHeads.length === message.heads.length) {
|
||||
sharedHeads = message.heads
|
||||
// If the remote peer has lost all its data, reset our state to perform a full resync
|
||||
|
@ -549,18 +468,14 @@ function receiveSyncMessage(backend, oldSyncState, binaryMessage) {
|
|||
theirHave: message.have, // the information we need to calculate the changes they need
|
||||
theirHeads: message.heads,
|
||||
theirNeed: message.need,
|
||||
sentHashes,
|
||||
sentHashes
|
||||
}
|
||||
return [backend, syncState, patch]
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
receiveSyncMessage,
|
||||
generateSyncMessage,
|
||||
encodeSyncMessage,
|
||||
decodeSyncMessage,
|
||||
initSyncState,
|
||||
encodeSyncState,
|
||||
decodeSyncState,
|
||||
BloomFilter, // BloomFilter is a private API, exported only for testing purposes
|
||||
receiveSyncMessage, generateSyncMessage,
|
||||
encodeSyncMessage, decodeSyncMessage,
|
||||
initSyncState, encodeSyncState, decodeSyncState,
|
||||
BloomFilter // BloomFilter is a private API, exported only for testing purposes
|
||||
}
|
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 }
|
1419
automerge-js/test/legacy_tests.js
Normal file
1419
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"
|
||||
license = "MIT"
|
||||
rust-version = "1.57.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib","rlib"]
|
||||
|
@ -28,16 +27,15 @@ 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"]
|
||||
|
||||
|
@ -57,6 +55,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"
|
696
automerge-wasm/README.md
Normal file
696
automerge-wasm/README.md
Normal file
|
@ -0,0 +1,696 @@
|
|||
## Automerge WASM Low Level Interface
|
||||
|
||||
This is a low level automerge library written in rust exporting a javascript API via WASM. This low level api is the underpinning to the `automerge-js` library that reimplements the Automerge API via these low level functions.
|
||||
|
||||
### Static Functions
|
||||
|
||||
### Methods
|
||||
|
||||
`doc.clone(actor?: string)` : Make a complete
|
||||
|
||||
`doc.free()` : deallocate WASM memory associated with a document
|
||||
|
||||
```rust
|
||||
#[wasm_bindgen]
|
||||
pub fn free(self) {}
|
||||
|
||||
#[wasm_bindgen(js_name = pendingOps)]
|
||||
pub fn pending_ops(&self) -> JsValue {
|
||||
(self.0.pending_ops() as u32).into()
|
||||
}
|
||||
|
||||
pub fn commit(&mut self, message: Option<String>, time: Option<f64>) -> Array {
|
||||
let heads = self.0.commit(message, time.map(|n| n as i64));
|
||||
let heads: Array = heads
|
||||
.iter()
|
||||
.map(|h| JsValue::from_str(&hex::encode(&h.0)))
|
||||
.collect();
|
||||
heads
|
||||
}
|
||||
|
||||
pub fn rollback(&mut self) -> f64 {
|
||||
self.0.rollback() as f64
|
||||
}
|
||||
|
||||
pub fn keys(&mut self, obj: String, heads: Option<Array>) -> 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: String, heads: Option<Array>) -> Result<String, 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)
|
||||
}
|
||||
|
||||
pub fn splice(
|
||||
&mut self,
|
||||
obj: String,
|
||||
start: f64,
|
||||
delete_count: f64,
|
||||
text: JsValue,
|
||||
) -> Result<Option<Array>, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let start = start as usize;
|
||||
let delete_count = delete_count as usize;
|
||||
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)?;
|
||||
Ok(None)
|
||||
} else {
|
||||
if let Ok(array) = text.dyn_into::<Array>() {
|
||||
for i in array.iter() {
|
||||
if let Ok(array) = i.clone().dyn_into::<Array>() {
|
||||
let value = array.get(1);
|
||||
let datatype = array.get(2);
|
||||
let value = self.import_value(value, datatype.as_string())?;
|
||||
vals.push(value);
|
||||
} else {
|
||||
let value = self.import_value(i, None)?;
|
||||
vals.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
let result = self
|
||||
.0
|
||||
.splice(&obj, start, delete_count, vals)
|
||||
.map_err(to_js_err)?;
|
||||
if result.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
let result: Array = result
|
||||
.iter()
|
||||
.map(|r| JsValue::from(r.to_string()))
|
||||
.collect();
|
||||
Ok(result.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(
|
||||
&mut self,
|
||||
obj: String,
|
||||
value: JsValue,
|
||||
datatype: Option<String>,
|
||||
) -> Result<Option<String>, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let value = self.import_value(value, datatype)?;
|
||||
let index = self.0.length(&obj);
|
||||
let opid = self.0.insert(&obj, index, value).map_err(to_js_err)?;
|
||||
Ok(opid.map(|id| id.to_string()))
|
||||
}
|
||||
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
obj: String,
|
||||
index: f64,
|
||||
value: JsValue,
|
||||
datatype: Option<String>,
|
||||
) -> Result<Option<String>, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let index = index as f64;
|
||||
let value = self.import_value(value, datatype)?;
|
||||
let opid = self
|
||||
.0
|
||||
.insert(&obj, index as usize, value)
|
||||
.map_err(to_js_err)?;
|
||||
Ok(opid.map(|id| id.to_string()))
|
||||
}
|
||||
|
||||
pub fn set(
|
||||
&mut self,
|
||||
obj: String,
|
||||
prop: JsValue,
|
||||
value: JsValue,
|
||||
datatype: Option<String>,
|
||||
) -> Result<Option<String>, 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)?;
|
||||
Ok(opid.map(|id| id.to_string()))
|
||||
}
|
||||
|
||||
pub fn make(
|
||||
&mut self,
|
||||
obj: String,
|
||||
prop: JsValue,
|
||||
value: JsValue,
|
||||
) -> Result<String, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let prop = self.import_prop(prop)?;
|
||||
let value = self.import_value(value, None)?;
|
||||
if value.is_object() {
|
||||
let opid = self.0.set(&obj, prop, value).map_err(to_js_err)?;
|
||||
Ok(opid.unwrap().to_string())
|
||||
} else {
|
||||
Err("invalid object type".into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inc(&mut self, obj: String, 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: String,
|
||||
prop: JsValue,
|
||||
heads: Option<Array>,
|
||||
) -> 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(&obj_id.to_string().into());
|
||||
}
|
||||
Some((Value::Scalar(value), _)) => {
|
||||
result.push(&datatype(&value).into());
|
||||
result.push(&ScalarValue(value).into());
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn values(
|
||||
&mut self,
|
||||
obj: String,
|
||||
arg: JsValue,
|
||||
heads: Option<Array>,
|
||||
) -> 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(&obj_id.to_string().into());
|
||||
result.push(&sub.into());
|
||||
}
|
||||
(Value::Scalar(value), id) => {
|
||||
let sub = Array::new();
|
||||
sub.push(&datatype(&value).into());
|
||||
sub.push(&ScalarValue(value).into());
|
||||
sub.push(&id.to_string().into());
|
||||
result.push(&sub.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn length(&mut self, obj: String, heads: Option<Array>) -> Result<f64, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
if let Some(heads) = get_heads(heads) {
|
||||
Ok(self.0.length_at(&obj, &heads) as f64)
|
||||
} else {
|
||||
Ok(self.0.length(&obj) as f64)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn del(&mut self, obj: String, 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 mark(
|
||||
&mut self,
|
||||
obj: JsValue,
|
||||
range: JsValue,
|
||||
name: JsValue,
|
||||
value: JsValue,
|
||||
datatype: JsValue,
|
||||
) -> Result<(), JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let re = Regex::new(r"([\[\(])(\d+)\.\.(\d+)([\)\]])").unwrap();
|
||||
let range = range.as_string().ok_or("range must be a string")?;
|
||||
let cap = re.captures_iter(&range).next().ok_or("range must be in the form of (start..end] or [start..end) etc... () for sticky, [] for normal")?;
|
||||
let start: usize = cap[2].parse().map_err(|_| to_js_err("invalid start"))?;
|
||||
let end: usize = cap[3].parse().map_err(|_| to_js_err("invalid end"))?;
|
||||
let start_sticky = &cap[1] == "(";
|
||||
let end_sticky = &cap[4] == ")";
|
||||
let name = name
|
||||
.as_string()
|
||||
.ok_or("invalid mark name")
|
||||
.map_err(to_js_err)?;
|
||||
let value = self.import_scalar(&value, datatype.as_string())?;
|
||||
self.0
|
||||
.mark(&obj, start, start_sticky, end, end_sticky, &name, value)
|
||||
.map_err(to_js_err)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn spans(&mut self, obj: JsValue) -> Result<JsValue, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let text = self.0.text(&obj).map_err(to_js_err)?;
|
||||
let spans = self.0.spans(&obj).map_err(to_js_err)?;
|
||||
let mut last_pos = 0;
|
||||
let result = Array::new();
|
||||
for s in spans {
|
||||
let marks = Array::new();
|
||||
for m in s.marks {
|
||||
let mark = Array::new();
|
||||
mark.push(&m.0.into());
|
||||
mark.push(&datatype(&m.1).into());
|
||||
mark.push(&ScalarValue(m.1).into());
|
||||
marks.push(&mark.into());
|
||||
}
|
||||
let text_span = &text[last_pos..s.pos]; //.slice(last_pos, s.pos);
|
||||
if text_span.len() > 0 {
|
||||
result.push(&text_span.into());
|
||||
}
|
||||
result.push(&marks);
|
||||
last_pos = s.pos;
|
||||
//let obj = Object::new().into();
|
||||
//js_set(&obj, "pos", s.pos as i32)?;
|
||||
//js_set(&obj, "marks", marks)?;
|
||||
//result.push(&obj.into());
|
||||
}
|
||||
let text_span = &text[last_pos..];
|
||||
if text_span.len() > 0 {
|
||||
result.push(&text_span.into());
|
||||
}
|
||||
Ok(result.into())
|
||||
}
|
||||
|
||||
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) -> Uint8Array {
|
||||
let bytes = self.0.save_incremental();
|
||||
Uint8Array::from(bytes.as_slice())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = loadIncremental)]
|
||||
pub fn load_incremental(&mut self, data: Uint8Array) -> Result<f64, JsValue> {
|
||||
let data = data.to_vec();
|
||||
let len = self.0.load_incremental(&data).map_err(to_js_err)?;
|
||||
Ok(len as f64)
|
||||
}
|
||||
|
||||
#[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) -> Array {
|
||||
let heads = self.0.get_heads();
|
||||
let heads: Array = heads
|
||||
.iter()
|
||||
.map(|h| JsValue::from_str(&hex::encode(&h.0)))
|
||||
.collect();
|
||||
heads
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = getActorId)]
|
||||
pub fn get_actor_id(&mut self) -> String {
|
||||
let actor = self.0.get_actor();
|
||||
actor.to_string()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = getLastLocalChange)]
|
||||
pub fn get_last_local_change(&mut self) -> Result<Option<Uint8Array>, JsValue> {
|
||||
if let Some(change) = self.0.get_last_local_change() {
|
||||
Ok(Some(Uint8Array::from(change.raw_bytes())))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dump(&self) {
|
||||
self.0.dump()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = getMissingDeps)]
|
||||
pub fn get_missing_deps(&mut self, heads: Option<Array>) -> Result<Array, JsValue> {
|
||||
let heads = get_heads(heads).unwrap_or_default();
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = toJS)]
|
||||
pub fn to_js(&self) -> JsValue {
|
||||
map_to_js(&self.0, ROOT)
|
||||
}
|
||||
|
||||
fn import(&self, id: String) -> Result<ObjId, JsValue> {
|
||||
self.0.import(&id).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_scalar(
|
||||
&mut self,
|
||||
value: &JsValue,
|
||||
datatype: Option<String>,
|
||||
) -> Result<am::ScalarValue, JsValue> {
|
||||
match datatype.as_deref() {
|
||||
Some("boolean") => value
|
||||
.as_bool()
|
||||
.ok_or_else(|| "value must be a bool".into())
|
||||
.map(am::ScalarValue::Boolean),
|
||||
Some("int") => value
|
||||
.as_f64()
|
||||
.ok_or_else(|| "value must be a number".into())
|
||||
.map(|v| am::ScalarValue::Int(v as i64)),
|
||||
Some("uint") => value
|
||||
.as_f64()
|
||||
.ok_or_else(|| "value must be a number".into())
|
||||
.map(|v| am::ScalarValue::Uint(v as u64)),
|
||||
Some("f64") => value
|
||||
.as_f64()
|
||||
.ok_or_else(|| "value must be a number".into())
|
||||
.map(am::ScalarValue::F64),
|
||||
Some("bytes") => Ok(am::ScalarValue::Bytes(
|
||||
value.clone().dyn_into::<Uint8Array>().unwrap().to_vec(),
|
||||
)),
|
||||
Some("counter") => value
|
||||
.as_f64()
|
||||
.ok_or_else(|| "value must be a number".into())
|
||||
.map(|v| am::ScalarValue::counter(v as i64)),
|
||||
Some("timestamp") => value
|
||||
.as_f64()
|
||||
.ok_or_else(|| "value must be a number".into())
|
||||
.map(|v| am::ScalarValue::Timestamp(v as i64)),
|
||||
/*
|
||||
Some("bytes") => unimplemented!(),
|
||||
Some("cursor") => unimplemented!(),
|
||||
*/
|
||||
Some("null") => Ok(am::ScalarValue::Null),
|
||||
Some(_) => Err(format!("unknown datatype {:?}", datatype).into()),
|
||||
None => {
|
||||
if value.is_null() {
|
||||
Ok(am::ScalarValue::Null)
|
||||
} else if let Some(b) = value.as_bool() {
|
||||
Ok(am::ScalarValue::Boolean(b))
|
||||
} 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()))
|
||||
} else if let Some(n) = value.as_f64() {
|
||||
if (n.round() - n).abs() < f64::EPSILON {
|
||||
Ok(am::ScalarValue::Int(n as i64))
|
||||
} else {
|
||||
Ok(am::ScalarValue::F64(n))
|
||||
}
|
||||
// } else if let Some(o) = to_objtype(&value) {
|
||||
// Ok(o.into())
|
||||
} else if let Ok(d) = value.clone().dyn_into::<js_sys::Date>() {
|
||||
Ok(am::ScalarValue::Timestamp(d.get_time() as i64))
|
||||
} else if let Ok(o) = &value.clone().dyn_into::<Uint8Array>() {
|
||||
Ok(am::ScalarValue::Bytes(o.to_vec()))
|
||||
} else {
|
||||
Err("value is invalid".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn import_value(&mut self, value: JsValue, datatype: Option<String>) -> Result<Value, JsValue> {
|
||||
match self.import_scalar(&value, datatype) {
|
||||
Ok(val) => Ok(val.into()),
|
||||
Err(err) => {
|
||||
if let Some(o) = to_objtype(&value) {
|
||||
Ok(o.into())
|
||||
} else {
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
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("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(d) = value.clone().dyn_into::<js_sys::Date>() {
|
||||
Ok(am::ScalarValue::Timestamp(d.get_time() as i64).into())
|
||||
} else if let Ok(o) = &value.dyn_into::<Uint8Array>() {
|
||||
Ok(am::ScalarValue::Bytes(o.to_vec()).into())
|
||||
} else {
|
||||
Err("value is invalid".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = create)]
|
||||
pub fn init(actor: Option<String>) -> Result<Automerge, JsValue> {
|
||||
console_error_panic_hook::set_once();
|
||||
Automerge::new(actor)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = loadDoc)]
|
||||
pub fn load(data: Uint8Array, actor: Option<String>) -> 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 {
|
||||
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(am::SyncState::new())
|
||||
}
|
||||
|
||||
// this is needed to be compatible with the automerge-js api
|
||||
#[wasm_bindgen(js_name = importSyncState)]
|
||||
pub fn import_sync_state(state: JsValue) -> Result<SyncState, JsValue> {
|
||||
Ok(SyncState(JS(state).try_into()?))
|
||||
}
|
||||
|
||||
// this is needed to be compatible with the automerge-js api
|
||||
#[wasm_bindgen(js_name = exportSyncState)]
|
||||
pub fn export_sync_state(state: SyncState) -> JsValue {
|
||||
JS::from(state.0).into()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = encodeSyncMessage)]
|
||||
pub fn encode_sync_message(message: JsValue) -> Result<Uint8Array, JsValue> {
|
||||
let heads = js_get(&message, "heads")?.try_into()?;
|
||||
let need = js_get(&message, "need")?.try_into()?;
|
||||
let changes = js_get(&message, "changes")?.try_into()?;
|
||||
let have = js_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 = AR::from(msg.heads.as_slice());
|
||||
let need = AR::from(msg.need.as_slice());
|
||||
let changes = AR::from(msg.changes.as_slice());
|
||||
let have = AR::from(msg.have.as_slice());
|
||||
let obj = Object::new().into();
|
||||
js_set(&obj, "heads", heads)?;
|
||||
js_set(&obj, "need", need)?;
|
||||
js_set(&obj, "have", have)?;
|
||||
js_set(&obj, "changes", changes)?;
|
||||
Ok(obj)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = encodeSyncState)]
|
||||
pub fn encode_sync_state(state: SyncState) -> Result<Uint8Array, JsValue> {
|
||||
let state = state.0;
|
||||
Ok(Uint8Array::from(
|
||||
state.encode().map_err(to_js_err)?.as_slice(),
|
||||
))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = decodeSyncState)]
|
||||
pub fn decode_sync_state(data: Uint8Array) -> Result<SyncState, JsValue> {
|
||||
SyncState::decode(data)
|
||||
}
|
||||
|
||||
#[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 {}
|
||||
```
|
222
automerge-wasm/index.d.ts
vendored
Normal file
222
automerge-wasm/index.d.ts
vendored
Normal file
|
@ -0,0 +1,222 @@
|
|||
|
||||
export type Actor = string;
|
||||
export type ObjID = string;
|
||||
export type Change = Uint8Array;
|
||||
export type SyncMessage = Uint8Array;
|
||||
export type Prop = string | number;
|
||||
export type Hash = string;
|
||||
export type Heads = Hash[];
|
||||
export type Value = string | number | boolean | null | Date | Uint8Array
|
||||
export type ObjType = string | Array | Object
|
||||
export type FullValue =
|
||||
["str", string] |
|
||||
["int", number] |
|
||||
["uint", number] |
|
||||
["f64", number] |
|
||||
["boolean", boolean] |
|
||||
["timestamp", Date] |
|
||||
["counter", number] |
|
||||
["bytes", Uint8Array] |
|
||||
["null", Uint8Array] |
|
||||
["map", ObjID] |
|
||||
["list", ObjID] |
|
||||
["text", ObjID] |
|
||||
["table", ObjID]
|
||||
|
||||
export enum ObjTypeName {
|
||||
list = "list",
|
||||
map = "map",
|
||||
table = "table",
|
||||
text = "text",
|
||||
}
|
||||
|
||||
export type Datatype =
|
||||
"boolean" |
|
||||
"str" |
|
||||
"int" |
|
||||
"uint" |
|
||||
"f64" |
|
||||
"null" |
|
||||
"timestamp" |
|
||||
"counter" |
|
||||
"bytes" |
|
||||
"map" |
|
||||
"text" |
|
||||
"list";
|
||||
|
||||
export type DecodedSyncMessage = {
|
||||
heads: Heads,
|
||||
need: Heads,
|
||||
have: any[]
|
||||
changes: Change[]
|
||||
}
|
||||
|
||||
export type DecodedChange = {
|
||||
actor: Actor,
|
||||
seq: number
|
||||
startOp: number,
|
||||
time: number,
|
||||
message: string | null,
|
||||
deps: Heads,
|
||||
hash: Hash,
|
||||
ops: Op[]
|
||||
}
|
||||
|
||||
export type Op = {
|
||||
action: string,
|
||||
obj: ObjID,
|
||||
key: string,
|
||||
value?: string | number | boolean,
|
||||
datatype?: string,
|
||||
pred: string[],
|
||||
}
|
||||
|
||||
export function create(actor?: Actor): Automerge;
|
||||
export function loadDoc(data: Uint8Array, actor?: Actor): Automerge;
|
||||
export function encodeChange(change: DecodedChange): Change;
|
||||
export function decodeChange(change: Change): DecodedChange;
|
||||
export function initSyncState(): SyncState;
|
||||
export function encodeSyncMessage(message: DecodedSyncMessage): SyncMessage;
|
||||
export function decodeSyncMessage(msg: SyncMessage): DecodedSyncMessage;
|
||||
export function encodeSyncState(state: SyncState): Uint8Array;
|
||||
export function decodeSyncState(data: Uint8Array): SyncState;
|
||||
|
||||
export class Automerge {
|
||||
// change state
|
||||
set(obj: ObjID, prop: Prop, value: Value, datatype?: Datatype): undefined;
|
||||
set_object(obj: ObjID, prop: Prop, value: ObjType): ObjID;
|
||||
insert(obj: ObjID, index: number, value: Value, datatype?: Datatype): undefined;
|
||||
insert_object(obj: ObjID, index: number, value: ObjType): ObjID;
|
||||
push(obj: ObjID, value: Value, datatype?: Datatype): undefined;
|
||||
push_object(obj: ObjID, value: ObjType): ObjID;
|
||||
splice(obj: ObjID, start: number, delete_count: number, text?: string | Array<Value>): ObjID[] | undefined;
|
||||
inc(obj: ObjID, prop: Prop, value: number): void;
|
||||
del(obj: ObjID, prop: Prop): void;
|
||||
|
||||
// returns a single value - if there is a conflict return the winner
|
||||
value(obj: ObjID, prop: any, heads?: Heads): FullValue | null;
|
||||
// return all values in case of a conflict
|
||||
values(obj: ObjID, arg: any, heads?: Heads): FullValue[];
|
||||
keys(obj: ObjID, heads?: Heads): string[];
|
||||
text(obj: ObjID, heads?: Heads): string;
|
||||
length(obj: ObjID, heads?: Heads): number;
|
||||
materialize(obj?: ObjID): any;
|
||||
|
||||
// transactions
|
||||
commit(message?: string, time?: number): Heads;
|
||||
merge(other: Automerge): Heads;
|
||||
getActorId(): Actor;
|
||||
pendingOps(): number;
|
||||
rollback(): number;
|
||||
|
||||
// save and load to local store
|
||||
save(): Uint8Array;
|
||||
saveIncremental(): Uint8Array;
|
||||
loadIncremental(data: Uint8Array): number;
|
||||
|
||||
// sync over network
|
||||
receiveSyncMessage(state: SyncState, message: SyncMessage): void;
|
||||
generateSyncMessage(state: SyncState): SyncMessage | null;
|
||||
|
||||
// low level change functions
|
||||
applyChanges(changes: Change[]): void;
|
||||
getChanges(have_deps: Heads): Change[];
|
||||
getChangesAdded(other: Automerge): Change[];
|
||||
getHeads(): Heads;
|
||||
getLastLocalChange(): Change;
|
||||
getMissingDeps(heads?: Heads): Heads;
|
||||
|
||||
// memory management
|
||||
free(): void;
|
||||
clone(actor?: string): Automerge;
|
||||
fork(actor?: string): Automerge;
|
||||
|
||||
// dump internal state to console.log
|
||||
dump(): void;
|
||||
|
||||
// dump internal state to a JS object
|
||||
toJS(): any;
|
||||
}
|
||||
|
||||
export class SyncState {
|
||||
free(): void;
|
||||
clone(): SyncState;
|
||||
lastSentHeads: any;
|
||||
sentHashes: any;
|
||||
readonly sharedHeads: any;
|
||||
}
|
||||
|
||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
export interface InitOutput {
|
||||
readonly memory: WebAssembly.Memory;
|
||||
readonly __wbg_automerge_free: (a: number) => void;
|
||||
readonly automerge_new: (a: number, b: number, c: number) => void;
|
||||
readonly automerge_clone: (a: number, b: number, c: number, d: number) => void;
|
||||
readonly automerge_free: (a: number) => void;
|
||||
readonly automerge_pendingOps: (a: number) => number;
|
||||
readonly automerge_commit: (a: number, b: number, c: number, d: number, e: number) => number;
|
||||
readonly automerge_rollback: (a: number) => number;
|
||||
readonly automerge_keys: (a: number, b: number, c: number, d: number, e: number) => void;
|
||||
readonly automerge_text: (a: number, b: number, c: number, d: number, e: number) => void;
|
||||
readonly automerge_splice: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
|
||||
readonly automerge_push: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
|
||||
readonly automerge_insert: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void;
|
||||
readonly automerge_set: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void;
|
||||
readonly automerge_inc: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
|
||||
readonly automerge_value: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
|
||||
readonly automerge_values: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
|
||||
readonly automerge_length: (a: number, b: number, c: number, d: number, e: number) => void;
|
||||
readonly automerge_del: (a: number, b: number, c: number, d: number, e: number) => void;
|
||||
readonly automerge_save: (a: number, b: number) => void;
|
||||
readonly automerge_saveIncremental: (a: number) => number;
|
||||
readonly automerge_loadIncremental: (a: number, b: number, c: number) => void;
|
||||
readonly automerge_applyChanges: (a: number, b: number, c: number) => void;
|
||||
readonly automerge_getChanges: (a: number, b: number, c: number) => void;
|
||||
readonly automerge_getChangesAdded: (a: number, b: number, c: number) => void;
|
||||
readonly automerge_getHeads: (a: number) => number;
|
||||
readonly automerge_getActorId: (a: number, b: number) => void;
|
||||
readonly automerge_getLastLocalChange: (a: number, b: number) => void;
|
||||
readonly automerge_dump: (a: number) => void;
|
||||
readonly automerge_getMissingDeps: (a: number, b: number, c: number) => void;
|
||||
readonly automerge_receiveSyncMessage: (a: number, b: number, c: number, d: number) => void;
|
||||
readonly automerge_generateSyncMessage: (a: number, b: number, c: number) => void;
|
||||
readonly automerge_toJS: (a: number) => number;
|
||||
readonly create: (a: number, b: number, c: number) => void;
|
||||
readonly loadDoc: (a: number, b: number, c: number, d: number) => void;
|
||||
readonly encodeChange: (a: number, b: number) => void;
|
||||
readonly decodeChange: (a: number, b: number) => void;
|
||||
readonly initSyncState: () => number;
|
||||
readonly importSyncState: (a: number, b: number) => void;
|
||||
readonly exportSyncState: (a: number) => number;
|
||||
readonly encodeSyncMessage: (a: number, b: number) => void;
|
||||
readonly decodeSyncMessage: (a: number, b: number) => void;
|
||||
readonly encodeSyncState: (a: number, b: number) => void;
|
||||
readonly decodeSyncState: (a: number, b: number) => void;
|
||||
readonly __wbg_list_free: (a: number) => void;
|
||||
readonly __wbg_map_free: (a: number) => void;
|
||||
readonly __wbg_text_free: (a: number) => void;
|
||||
readonly __wbg_table_free: (a: number) => void;
|
||||
readonly __wbg_syncstate_free: (a: number) => void;
|
||||
readonly syncstate_sharedHeads: (a: number) => number;
|
||||
readonly syncstate_lastSentHeads: (a: number) => number;
|
||||
readonly syncstate_set_lastSentHeads: (a: number, b: number, c: number) => void;
|
||||
readonly syncstate_set_sentHashes: (a: number, b: number, c: number) => void;
|
||||
readonly syncstate_clone: (a: number) => number;
|
||||
readonly __wbindgen_malloc: (a: number) => number;
|
||||
readonly __wbindgen_realloc: (a: number, b: number, c: number) => number;
|
||||
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
|
||||
readonly __wbindgen_free: (a: number, b: number) => void;
|
||||
readonly __wbindgen_exn_store: (a: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
||||
* for everything else, calls `WebAssembly.instantiate` directly.
|
||||
*
|
||||
* @param {InitInput | Promise<InitInput>} module_or_path
|
||||
*
|
||||
* @returns {Promise<InitOutput>}
|
||||
*/
|
||||
|
||||
export default function init (module_or_path?: InitInput | Promise<InitInput>): Promise<InitOutput>;
|
40
automerge-wasm/package.json
Normal file
40
automerge-wasm/package.json
Normal file
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"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.0.1",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"README.md",
|
||||
"LICENSE",
|
||||
"package.json",
|
||||
"automerge_wasm_bg.wasm",
|
||||
"automerge_wasm.js"
|
||||
],
|
||||
"module": "./pkg/index.js",
|
||||
"main": "./dev/index.js",
|
||||
"scripts": {
|
||||
"build": "rimraf ./dev && wasm-pack build --target nodejs --dev --out-name index -d dev && cp index.d.ts dev",
|
||||
"release": "rimraf ./dev && wasm-pack build --target nodejs --release --out-name index -d dev && cp index.d.ts dev",
|
||||
"pkg": "rimraf ./pkg && wasm-pack build --target web --release --out-name index -d pkg && cp index.d.ts pkg && cd pkg && yarn pack && mv automerge-wasm*tgz ..",
|
||||
"prof": "rimraf ./dev && wasm-pack build --target nodejs --profiling --out-name index -d dev",
|
||||
"test": "yarn build && ts-mocha -p tsconfig.json --type-check --bail --full-trace test/*.ts"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@types/expect": "^24.3.0",
|
||||
"@types/jest": "^27.4.0",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"@types/node": "^17.0.13",
|
||||
"fast-sha256": "^1.3.0",
|
||||
"mocha": "^9.1.3",
|
||||
"pako": "^2.0.4",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-mocha": "^9.0.2",
|
||||
"typescript": "^4.5.5"
|
||||
}
|
||||
}
|
379
automerge-wasm/src/interop.rs
Normal file
379
automerge-wasm/src/interop.rs
Normal file
|
@ -0,0 +1,379 @@
|
|||
use automerge as am;
|
||||
use automerge::transaction::Transactable;
|
||||
use automerge::{Change, ChangeHash, Prop};
|
||||
use js_sys::{Array, Object, Reflect, Uint8Array};
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Display;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
use crate::{ObjId, ScalarValue, Value};
|
||||
|
||||
pub(crate) struct JS(pub JsValue);
|
||||
pub(crate) struct AR(pub Array);
|
||||
|
||||
impl From<AR> for JsValue {
|
||||
fn from(ar: AR) -> Self {
|
||||
ar.0.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JS> for JsValue {
|
||||
fn from(js: JS) -> Self {
|
||||
js.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<am::sync::State> for JS {
|
||||
fn from(state: am::sync::State) -> Self {
|
||||
let shared_heads: JS = state.shared_heads.into();
|
||||
let last_sent_heads: JS = state.last_sent_heads.into();
|
||||
let their_heads: JS = state.their_heads.into();
|
||||
let their_need: JS = state.their_need.into();
|
||||
let sent_hashes: JS = state.sent_hashes.into();
|
||||
let their_have = if let Some(have) = &state.their_have {
|
||||
JsValue::from(AR::from(have.as_slice()).0)
|
||||
} else {
|
||||
JsValue::null()
|
||||
};
|
||||
let result: JsValue = Object::new().into();
|
||||
// we can unwrap here b/c we made the object and know its not frozen
|
||||
Reflect::set(&result, &"sharedHeads".into(), &shared_heads.0).unwrap();
|
||||
Reflect::set(&result, &"lastSentHeads".into(), &last_sent_heads.0).unwrap();
|
||||
Reflect::set(&result, &"theirHeads".into(), &their_heads.0).unwrap();
|
||||
Reflect::set(&result, &"theirNeed".into(), &their_need.0).unwrap();
|
||||
Reflect::set(&result, &"theirHave".into(), &their_have).unwrap();
|
||||
Reflect::set(&result, &"sentHashes".into(), &sent_hashes.0).unwrap();
|
||||
JS(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<ChangeHash>> for JS {
|
||||
fn from(heads: Vec<ChangeHash>) -> Self {
|
||||
let heads: Array = heads
|
||||
.iter()
|
||||
.map(|h| JsValue::from_str(&h.to_string()))
|
||||
.collect();
|
||||
JS(heads.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HashSet<ChangeHash>> for JS {
|
||||
fn from(heads: HashSet<ChangeHash>) -> Self {
|
||||
let result: JsValue = Object::new().into();
|
||||
for key in &heads {
|
||||
Reflect::set(&result, &key.to_string().into(), &true.into()).unwrap();
|
||||
}
|
||||
JS(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<Vec<ChangeHash>>> for JS {
|
||||
fn from(heads: Option<Vec<ChangeHash>>) -> Self {
|
||||
if let Some(v) = heads {
|
||||
let v: Array = v
|
||||
.iter()
|
||||
.map(|h| JsValue::from_str(&h.to_string()))
|
||||
.collect();
|
||||
JS(v.into())
|
||||
} else {
|
||||
JS(JsValue::null())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<JS> for HashSet<ChangeHash> {
|
||||
type Error = JsValue;
|
||||
|
||||
fn try_from(value: JS) -> Result<Self, Self::Error> {
|
||||
let mut result = HashSet::new();
|
||||
for key in Reflect::own_keys(&value.0)?.iter() {
|
||||
if let Some(true) = Reflect::get(&value.0, &key)?.as_bool() {
|
||||
result.insert(key.into_serde().map_err(to_js_err)?);
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
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| Change::try_from(a.to_vec()))
|
||||
.collect();
|
||||
let changes = changes.map_err(to_js_err)?;
|
||||
Ok(changes)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<JS> for am::sync::State {
|
||||
type Error = JsValue;
|
||||
|
||||
fn try_from(value: JS) -> Result<Self, Self::Error> {
|
||||
let value = value.0;
|
||||
let shared_heads = js_get(&value, "sharedHeads")?.try_into()?;
|
||||
let last_sent_heads = js_get(&value, "lastSentHeads")?.try_into()?;
|
||||
let their_heads = js_get(&value, "theirHeads")?.into();
|
||||
let their_need = js_get(&value, "theirNeed")?.into();
|
||||
let their_have = js_get(&value, "theirHave")?.try_into()?;
|
||||
let sent_hashes = js_get(&value, "sentHashes")?.try_into()?;
|
||||
Ok(am::sync::State {
|
||||
shared_heads,
|
||||
last_sent_heads,
|
||||
their_heads,
|
||||
their_need,
|
||||
their_have,
|
||||
sent_hashes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<JS> for Option<Vec<am::sync::Have>> {
|
||||
type Error = JsValue;
|
||||
|
||||
fn try_from(value: JS) -> Result<Self, Self::Error> {
|
||||
if value.0.is_null() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(value.try_into()?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<JS> for Vec<am::sync::Have> {
|
||||
type Error = JsValue;
|
||||
|
||||
fn try_from(value: JS) -> Result<Self, Self::Error> {
|
||||
let value = value.0.dyn_into::<Array>()?;
|
||||
let have: Result<Vec<am::sync::Have>, JsValue> = value
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let last_sync = js_get(&s, "lastSync")?.try_into()?;
|
||||
let bloom = js_get(&s, "bloom")?.try_into()?;
|
||||
Ok(am::sync::Have { last_sync, bloom })
|
||||
})
|
||||
.collect();
|
||||
let have = have?;
|
||||
Ok(have)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<JS> for am::sync::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)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[ChangeHash]> for AR {
|
||||
fn from(value: &[ChangeHash]) -> Self {
|
||||
AR(value
|
||||
.iter()
|
||||
.map(|h| JsValue::from_str(&hex::encode(&h.0)))
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[Change]> for AR {
|
||||
fn from(value: &[Change]) -> Self {
|
||||
let changes: Array = value
|
||||
.iter()
|
||||
.map(|c| Uint8Array::from(c.raw_bytes()))
|
||||
.collect();
|
||||
AR(changes)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[am::sync::Have]> for AR {
|
||||
fn from(value: &[am::sync::Have]) -> Self {
|
||||
AR(value
|
||||
.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.to_bytes().as_slice());
|
||||
let obj: JsValue = Object::new().into();
|
||||
// we can unwrap here b/c we created the object and know its not frozen
|
||||
Reflect::set(&obj, &"lastSync".into(), &last_sync.into()).unwrap();
|
||||
Reflect::set(&obj, &"bloom".into(), &bloom.into()).unwrap();
|
||||
obj
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn to_js_err<T: Display>(err: T) -> JsValue {
|
||||
js_sys::Error::new(&std::format!("{}", err)).into()
|
||||
}
|
||||
|
||||
pub(crate) fn js_get<J: Into<JsValue>>(obj: J, prop: &str) -> Result<JS, JsValue> {
|
||||
Ok(JS(Reflect::get(&obj.into(), &prop.into())?))
|
||||
}
|
||||
|
||||
pub(crate) fn js_set<V: Into<JsValue>>(obj: &JsValue, prop: &str, val: V) -> Result<bool, JsValue> {
|
||||
Reflect::set(obj, &prop.into(), &val.into())
|
||||
}
|
||||
|
||||
pub(crate) 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(to_js_err("prop must me a string or number"))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn to_objtype(
|
||||
value: &JsValue,
|
||||
datatype: &Option<String>,
|
||||
) -> Option<(am::ObjType, Vec<(Prop, JsValue)>)> {
|
||||
match datatype.as_deref() {
|
||||
Some("map") => {
|
||||
let map = value.clone().dyn_into::<js_sys::Object>().ok()?;
|
||||
// FIXME unwrap
|
||||
let map = js_sys::Object::keys(&map)
|
||||
.iter()
|
||||
.zip(js_sys::Object::values(&map).iter())
|
||||
.map(|(key, val)| (key.as_string().unwrap().into(), val))
|
||||
.collect();
|
||||
Some((am::ObjType::Map, map))
|
||||
}
|
||||
Some("list") => {
|
||||
let list = value.clone().dyn_into::<js_sys::Array>().ok()?;
|
||||
let list = list
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, e)| (i.into(), e))
|
||||
.collect();
|
||||
Some((am::ObjType::List, list))
|
||||
}
|
||||
Some("text") => {
|
||||
let text = value.as_string()?;
|
||||
let text = text
|
||||
.graphemes(true)
|
||||
.enumerate()
|
||||
.map(|(i, ch)| (i.into(), ch.into()))
|
||||
.collect();
|
||||
Some((am::ObjType::Text, text))
|
||||
}
|
||||
Some(_) => None,
|
||||
None => {
|
||||
if let Ok(list) = value.clone().dyn_into::<js_sys::Array>() {
|
||||
let list = list
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, e)| (i.into(), e))
|
||||
.collect();
|
||||
Some((am::ObjType::List, list))
|
||||
} else if let Ok(map) = value.clone().dyn_into::<js_sys::Object>() {
|
||||
// FIXME unwrap
|
||||
let map = js_sys::Object::keys(&map)
|
||||
.iter()
|
||||
.zip(js_sys::Object::values(&map).iter())
|
||||
.map(|(key, val)| (key.as_string().unwrap().into(), val))
|
||||
.collect();
|
||||
Some((am::ObjType::Map, map))
|
||||
} else if let Some(text) = value.as_string() {
|
||||
let text = text
|
||||
.graphemes(true)
|
||||
.enumerate()
|
||||
.map(|(i, ch)| (i.into(), ch.into()))
|
||||
.collect();
|
||||
Some((am::ObjType::Text, text))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_heads(heads: Option<Array>) -> Option<Vec<ChangeHash>> {
|
||||
let heads = heads?;
|
||||
let heads: Result<Vec<ChangeHash>, _> = heads.iter().map(|j| j.into_serde()).collect();
|
||||
heads.ok()
|
||||
}
|
||||
|
||||
pub(crate) fn map_to_js(doc: &am::AutoCommit, obj: &ObjId) -> JsValue {
|
||||
let keys = doc.keys(obj);
|
||||
let map = Object::new();
|
||||
for k in keys {
|
||||
let val = doc.value(obj, &k);
|
||||
match val {
|
||||
Ok(Some((Value::Object(o), exid)))
|
||||
if o == am::ObjType::Map || o == am::ObjType::Table =>
|
||||
{
|
||||
Reflect::set(&map, &k.into(), &map_to_js(doc, &exid)).unwrap();
|
||||
}
|
||||
Ok(Some((Value::Object(o), exid))) if o == am::ObjType::List => {
|
||||
Reflect::set(&map, &k.into(), &list_to_js(doc, &exid)).unwrap();
|
||||
}
|
||||
Ok(Some((Value::Object(o), exid))) if o == am::ObjType::Text => {
|
||||
Reflect::set(&map, &k.into(), &doc.text(&exid).unwrap().into()).unwrap();
|
||||
}
|
||||
Ok(Some((Value::Scalar(v), _))) => {
|
||||
Reflect::set(&map, &k.into(), &ScalarValue(v).into()).unwrap();
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
map.into()
|
||||
}
|
||||
|
||||
pub(crate) fn list_to_js(doc: &am::AutoCommit, obj: &ObjId) -> JsValue {
|
||||
let len = doc.length(obj);
|
||||
let array = Array::new();
|
||||
for i in 0..len {
|
||||
let val = doc.value(obj, i as usize);
|
||||
match val {
|
||||
Ok(Some((Value::Object(o), exid)))
|
||||
if o == am::ObjType::Map || o == am::ObjType::Table =>
|
||||
{
|
||||
array.push(&map_to_js(doc, &exid));
|
||||
}
|
||||
Ok(Some((Value::Object(_), exid))) => {
|
||||
array.push(&list_to_js(doc, &exid));
|
||||
}
|
||||
Ok(Some((Value::Scalar(v), _))) => {
|
||||
array.push(&ScalarValue(v).into());
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
array.into()
|
||||
}
|
683
automerge-wasm/src/lib.rs
Normal file
683
automerge-wasm/src/lib.rs
Normal file
|
@ -0,0 +1,683 @@
|
|||
#![allow(clippy::unused_unit)]
|
||||
use am::transaction::CommitOptions;
|
||||
use am::transaction::Transactable;
|
||||
use automerge as am;
|
||||
use automerge::{Change, ObjId, Prop, Value, ROOT};
|
||||
use js_sys::{Array, Object, Uint8Array};
|
||||
use std::convert::TryInto;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
mod interop;
|
||||
mod sync;
|
||||
mod value;
|
||||
|
||||
use interop::{
|
||||
get_heads, js_get, js_set, list_to_js, map_to_js, to_js_err, to_objtype, to_prop, AR, JS,
|
||||
};
|
||||
use sync::SyncState;
|
||||
use value::{datatype, ScalarValue};
|
||||
|
||||
#[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;
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct Automerge(automerge::AutoCommit);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Automerge {
|
||||
pub fn new(actor: Option<String>) -> Result<Automerge, JsValue> {
|
||||
let mut automerge = automerge::AutoCommit::new();
|
||||
if let Some(a) = actor {
|
||||
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(&mut self, actor: Option<String>) -> Result<Automerge, JsValue> {
|
||||
if self.0.pending_ops() > 0 {
|
||||
self.0.commit();
|
||||
}
|
||||
let mut automerge = Automerge(self.0.clone());
|
||||
if let Some(s) = actor {
|
||||
let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec());
|
||||
automerge.0.set_actor(actor);
|
||||
}
|
||||
Ok(automerge)
|
||||
}
|
||||
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn fork(&mut self, actor: Option<String>) -> Result<Automerge, JsValue> {
|
||||
let mut automerge = Automerge(self.0.fork());
|
||||
if let Some(s) = actor {
|
||||
let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec());
|
||||
automerge.0.set_actor(actor);
|
||||
}
|
||||
Ok(automerge)
|
||||
}
|
||||
|
||||
pub fn free(self) {}
|
||||
|
||||
#[wasm_bindgen(js_name = pendingOps)]
|
||||
pub fn pending_ops(&self) -> JsValue {
|
||||
(self.0.pending_ops() as u32).into()
|
||||
}
|
||||
|
||||
pub fn commit(&mut self, message: Option<String>, time: Option<f64>) -> JsValue {
|
||||
let mut commit_opts = CommitOptions::default();
|
||||
if let Some(message) = message {
|
||||
commit_opts.set_message(message);
|
||||
}
|
||||
if let Some(time) = time {
|
||||
commit_opts.set_time(time as i64);
|
||||
}
|
||||
let hash = self.0.commit_with(commit_opts);
|
||||
let result = Array::new();
|
||||
result.push(&JsValue::from_str(&hex::encode(&hash.0)));
|
||||
result.into()
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, other: &mut Automerge) -> Result<Array, JsValue> {
|
||||
let heads = self.0.merge(&mut other.0)?;
|
||||
let heads: Array = heads
|
||||
.iter()
|
||||
.map(|h| JsValue::from_str(&hex::encode(&h.0)))
|
||||
.collect();
|
||||
Ok(heads)
|
||||
}
|
||||
|
||||
pub fn rollback(&mut self) -> f64 {
|
||||
self.0.rollback() as f64
|
||||
}
|
||||
|
||||
pub fn keys(&mut self, obj: JsValue, heads: Option<Array>) -> Result<Array, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let result = if let Some(heads) = get_heads(heads) {
|
||||
self.0
|
||||
.keys_at(&obj, &heads)
|
||||
.map(|s| JsValue::from_str(&s))
|
||||
.collect()
|
||||
} else {
|
||||
self.0.keys(&obj).map(|s| JsValue::from_str(&s)).collect()
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn text(&mut self, obj: JsValue, heads: Option<Array>) -> Result<String, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
if let Some(heads) = get_heads(heads) {
|
||||
Ok(self.0.text_at(&obj, &heads)?)
|
||||
} else {
|
||||
Ok(self.0.text(&obj)?)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn splice(
|
||||
&mut self,
|
||||
obj: JsValue,
|
||||
start: f64,
|
||||
delete_count: f64,
|
||||
text: JsValue,
|
||||
) -> Result<(), JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let start = start as usize;
|
||||
let delete_count = delete_count as usize;
|
||||
let mut vals = vec![];
|
||||
if let Some(t) = text.as_string() {
|
||||
self.0.splice_text(&obj, start, delete_count, &t)?;
|
||||
} else {
|
||||
if let Ok(array) = text.dyn_into::<Array>() {
|
||||
for i in array.iter() {
|
||||
let value = self
|
||||
.import_scalar(&i, &None)
|
||||
.ok_or_else(|| to_js_err("expected scalar"))?;
|
||||
vals.push(value);
|
||||
}
|
||||
}
|
||||
self.0.splice(&obj, start, delete_count, vals.into_iter())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn push(&mut self, obj: JsValue, value: JsValue, datatype: JsValue) -> Result<(), JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let value = self
|
||||
.import_scalar(&value, &datatype.as_string())
|
||||
.ok_or_else(|| to_js_err("invalid scalar value"))?;
|
||||
let index = self.0.length(&obj);
|
||||
self.0.insert(&obj, index, value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn push_object(&mut self, obj: JsValue, value: JsValue) -> Result<Option<String>, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let (value, subvals) =
|
||||
to_objtype(&value, &None).ok_or_else(|| to_js_err("expected object"))?;
|
||||
let index = self.0.length(&obj);
|
||||
let opid = self.0.insert_object(&obj, index, value)?;
|
||||
self.subset(&opid, subvals)?;
|
||||
Ok(opid.to_string().into())
|
||||
}
|
||||
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
obj: JsValue,
|
||||
index: f64,
|
||||
value: JsValue,
|
||||
datatype: JsValue,
|
||||
) -> Result<(), JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let index = index as f64;
|
||||
let value = self
|
||||
.import_scalar(&value, &datatype.as_string())
|
||||
.ok_or_else(|| to_js_err("expected scalar value"))?;
|
||||
self.0.insert(&obj, index as usize, value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn insert_object(
|
||||
&mut self,
|
||||
obj: JsValue,
|
||||
index: f64,
|
||||
value: JsValue,
|
||||
) -> Result<Option<String>, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let index = index as f64;
|
||||
let (value, subvals) =
|
||||
to_objtype(&value, &None).ok_or_else(|| to_js_err("expected object"))?;
|
||||
let opid = self.0.insert_object(&obj, index as usize, value)?;
|
||||
self.subset(&opid, subvals)?;
|
||||
Ok(opid.to_string().into())
|
||||
}
|
||||
|
||||
pub fn set(
|
||||
&mut self,
|
||||
obj: JsValue,
|
||||
prop: JsValue,
|
||||
value: JsValue,
|
||||
datatype: JsValue,
|
||||
) -> Result<(), JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let prop = self.import_prop(prop)?;
|
||||
let value = self
|
||||
.import_scalar(&value, &datatype.as_string())
|
||||
.ok_or_else(|| to_js_err("expected scalar value"))?;
|
||||
self.0.set(&obj, prop, value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_object(
|
||||
&mut self,
|
||||
obj: JsValue,
|
||||
prop: JsValue,
|
||||
value: JsValue,
|
||||
) -> Result<JsValue, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let prop = self.import_prop(prop)?;
|
||||
let (value, subvals) =
|
||||
to_objtype(&value, &None).ok_or_else(|| to_js_err("expected object"))?;
|
||||
let opid = self.0.set_object(&obj, prop, value)?;
|
||||
self.subset(&opid, subvals)?;
|
||||
Ok(opid.to_string().into())
|
||||
}
|
||||
|
||||
fn subset(&mut self, obj: &am::ObjId, vals: Vec<(am::Prop, JsValue)>) -> Result<(), JsValue> {
|
||||
for (p, v) in vals {
|
||||
let (value, subvals) = self.import_value(&v, None)?;
|
||||
//let opid = self.0.set(id, p, value)?;
|
||||
let opid = match (p, value) {
|
||||
(Prop::Map(s), Value::Object(objtype)) => Some(self.0.set_object(obj, s, objtype)?),
|
||||
(Prop::Map(s), Value::Scalar(scalar)) => {
|
||||
self.0.set(obj, s, scalar)?;
|
||||
None
|
||||
}
|
||||
(Prop::Seq(i), Value::Object(objtype)) => {
|
||||
Some(self.0.insert_object(obj, i, objtype)?)
|
||||
}
|
||||
(Prop::Seq(i), Value::Scalar(scalar)) => {
|
||||
self.0.insert(obj, i, scalar)?;
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(opid) = opid {
|
||||
self.subset(&opid, subvals)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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_else(|| to_js_err("inc needs a numberic value"))?;
|
||||
self.0.inc(&obj, prop, value as i64)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn value(
|
||||
&mut self,
|
||||
obj: JsValue,
|
||||
prop: JsValue,
|
||||
heads: Option<Array>,
|
||||
) -> Result<Option<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)?
|
||||
};
|
||||
match value {
|
||||
Some((Value::Object(obj_type), obj_id)) => {
|
||||
result.push(&obj_type.to_string().into());
|
||||
result.push(&obj_id.to_string().into());
|
||||
Ok(Some(result))
|
||||
}
|
||||
Some((Value::Scalar(value), _)) => {
|
||||
result.push(&datatype(&value).into());
|
||||
result.push(&ScalarValue(value).into());
|
||||
Ok(Some(result))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn values(
|
||||
&mut self,
|
||||
obj: JsValue,
|
||||
arg: JsValue,
|
||||
heads: Option<Array>,
|
||||
) -> 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(&obj_id.to_string().into());
|
||||
result.push(&sub.into());
|
||||
}
|
||||
(Value::Scalar(value), id) => {
|
||||
let sub = Array::new();
|
||||
sub.push(&datatype(&value).into());
|
||||
sub.push(&ScalarValue(value).into());
|
||||
sub.push(&id.to_string().into());
|
||||
result.push(&sub.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn length(&mut self, obj: JsValue, heads: Option<Array>) -> Result<f64, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
if let Some(heads) = get_heads(heads) {
|
||||
Ok(self.0.length_at(&obj, &heads) as f64)
|
||||
} else {
|
||||
Ok(self.0.length(&obj) as f64)
|
||||
}
|
||||
}
|
||||
|
||||
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) -> Uint8Array {
|
||||
Uint8Array::from(self.0.save().as_slice())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = saveIncremental)]
|
||||
pub fn save_incremental(&mut self) -> Uint8Array {
|
||||
let bytes = self.0.save_incremental();
|
||||
Uint8Array::from(bytes.as_slice())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = loadIncremental)]
|
||||
pub fn load_incremental(&mut self, data: Uint8Array) -> Result<f64, JsValue> {
|
||||
let data = data.to_vec();
|
||||
let len = self.0.load_incremental(&data).map_err(to_js_err)?;
|
||||
Ok(len as f64)
|
||||
}
|
||||
|
||||
#[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: &mut Automerge) -> Result<Array, JsValue> {
|
||||
let changes = self.0.get_changes_added(&mut 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) -> Array {
|
||||
let heads = self.0.get_heads();
|
||||
let heads: Array = heads
|
||||
.iter()
|
||||
.map(|h| JsValue::from_str(&hex::encode(&h.0)))
|
||||
.collect();
|
||||
heads
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = getActorId)]
|
||||
pub fn get_actor_id(&mut self) -> String {
|
||||
let actor = self.0.get_actor();
|
||||
actor.to_string()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = getLastLocalChange)]
|
||||
pub fn get_last_local_change(&mut self) -> Result<Uint8Array, JsValue> {
|
||||
if let Some(change) = self.0.get_last_local_change() {
|
||||
Ok(Uint8Array::from(change.raw_bytes()))
|
||||
} else {
|
||||
Err(to_js_err("no local changes"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dump(&self) {
|
||||
self.0.dump()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = getMissingDeps)]
|
||||
pub fn get_missing_deps(&mut self, heads: Option<Array>) -> Result<Array, JsValue> {
|
||||
let heads = get_heads(heads).unwrap_or_default();
|
||||
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::sync::Message::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().as_slice()).into())
|
||||
} else {
|
||||
Ok(JsValue::null())
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = toJS)]
|
||||
pub fn to_js(&self) -> JsValue {
|
||||
map_to_js(&self.0, &ROOT)
|
||||
}
|
||||
|
||||
pub fn materialize(&self, obj: JsValue) -> Result<JsValue, JsValue> {
|
||||
let obj = self.import(obj).unwrap_or(ROOT);
|
||||
match self.0.object_type(&obj) {
|
||||
Some(am::ObjType::Map) => Ok(map_to_js(&self.0, &obj)),
|
||||
Some(am::ObjType::List) => Ok(list_to_js(&self.0, &obj)),
|
||||
Some(am::ObjType::Text) => Ok(self.0.text(&obj)?.into()),
|
||||
Some(am::ObjType::Table) => Ok(map_to_js(&self.0, &obj)),
|
||||
None => Err(to_js_err(format!("invalid obj {}", obj))),
|
||||
}
|
||||
}
|
||||
|
||||
fn import(&self, id: JsValue) -> Result<ObjId, JsValue> {
|
||||
if let Some(s) = id.as_string() {
|
||||
if let Some(post) = s.strip_prefix('/') {
|
||||
let mut obj = ROOT;
|
||||
let mut is_map = true;
|
||||
let parts = post.split('/');
|
||||
for prop in parts {
|
||||
if prop.is_empty() {
|
||||
break;
|
||||
}
|
||||
let val = if is_map {
|
||||
self.0.value(obj, prop)?
|
||||
} else {
|
||||
self.0.value(obj, am::Prop::Seq(prop.parse().unwrap()))?
|
||||
};
|
||||
match val {
|
||||
Some((am::Value::Object(am::ObjType::Map), id)) => {
|
||||
is_map = true;
|
||||
obj = id;
|
||||
}
|
||||
Some((am::Value::Object(am::ObjType::Table), id)) => {
|
||||
is_map = true;
|
||||
obj = id;
|
||||
}
|
||||
Some((am::Value::Object(_), id)) => {
|
||||
is_map = false;
|
||||
obj = id;
|
||||
}
|
||||
None => return Err(to_js_err(format!("invalid path '{}'", s))),
|
||||
_ => return Err(to_js_err(format!("path '{}' is not an object", s))),
|
||||
};
|
||||
}
|
||||
Ok(obj)
|
||||
} else {
|
||||
Ok(self.0.import(&s)?)
|
||||
}
|
||||
} else {
|
||||
Err(to_js_err("invalid objid"))
|
||||
}
|
||||
}
|
||||
|
||||
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(to_js_err(format!("invalid prop {:?}", prop)))
|
||||
}
|
||||
}
|
||||
|
||||
fn import_scalar(
|
||||
&mut self,
|
||||
value: &JsValue,
|
||||
datatype: &Option<String>,
|
||||
) -> Option<am::ScalarValue> {
|
||||
match datatype.as_deref() {
|
||||
Some("boolean") => value.as_bool().map(am::ScalarValue::Boolean),
|
||||
Some("int") => value.as_f64().map(|v| am::ScalarValue::Int(v as i64)),
|
||||
Some("uint") => value.as_f64().map(|v| am::ScalarValue::Uint(v as u64)),
|
||||
Some("f64") => value.as_f64().map(am::ScalarValue::F64),
|
||||
Some("bytes") => Some(am::ScalarValue::Bytes(
|
||||
value.clone().dyn_into::<Uint8Array>().unwrap().to_vec(),
|
||||
)),
|
||||
Some("counter") => value.as_f64().map(|v| am::ScalarValue::counter(v as i64)),
|
||||
Some("timestamp") => value.as_f64().map(|v| am::ScalarValue::Timestamp(v as i64)),
|
||||
Some("null") => Some(am::ScalarValue::Null),
|
||||
Some(_) => None,
|
||||
None => {
|
||||
if value.is_null() {
|
||||
Some(am::ScalarValue::Null)
|
||||
} else if let Some(b) = value.as_bool() {
|
||||
Some(am::ScalarValue::Boolean(b))
|
||||
} else if let Some(s) = value.as_string() {
|
||||
Some(am::ScalarValue::Str(s.into()))
|
||||
} else if let Some(n) = value.as_f64() {
|
||||
if (n.round() - n).abs() < f64::EPSILON {
|
||||
Some(am::ScalarValue::Int(n as i64))
|
||||
} else {
|
||||
Some(am::ScalarValue::F64(n))
|
||||
}
|
||||
} else if let Ok(d) = value.clone().dyn_into::<js_sys::Date>() {
|
||||
Some(am::ScalarValue::Timestamp(d.get_time() as i64))
|
||||
} else if let Ok(o) = &value.clone().dyn_into::<Uint8Array>() {
|
||||
Some(am::ScalarValue::Bytes(o.to_vec()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn import_value(
|
||||
&mut self,
|
||||
value: &JsValue,
|
||||
datatype: Option<String>,
|
||||
) -> Result<(Value, Vec<(Prop, JsValue)>), JsValue> {
|
||||
match self.import_scalar(value, &datatype) {
|
||||
Some(val) => Ok((val.into(), vec![])),
|
||||
None => {
|
||||
if let Some((o, subvals)) = to_objtype(value, &datatype) {
|
||||
Ok((o.into(), subvals))
|
||||
} else {
|
||||
web_sys::console::log_2(&"Invalid value".into(), value);
|
||||
Err(to_js_err("invalid value"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = create)]
|
||||
pub fn init(actor: Option<String>) -> Result<Automerge, JsValue> {
|
||||
console_error_panic_hook::set_once();
|
||||
Automerge::new(actor)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = loadDoc)]
|
||||
pub fn load(data: Uint8Array, actor: Option<String>) -> Result<Automerge, JsValue> {
|
||||
let data = data.to_vec();
|
||||
let mut automerge = am::AutoCommit::load(&data).map_err(to_js_err)?;
|
||||
if let Some(s) = actor {
|
||||
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(am::sync::State::new())
|
||||
}
|
||||
|
||||
// this is needed to be compatible with the automerge-js api
|
||||
#[wasm_bindgen(js_name = importSyncState)]
|
||||
pub fn import_sync_state(state: JsValue) -> Result<SyncState, JsValue> {
|
||||
Ok(SyncState(JS(state).try_into()?))
|
||||
}
|
||||
|
||||
// this is needed to be compatible with the automerge-js api
|
||||
#[wasm_bindgen(js_name = exportSyncState)]
|
||||
pub fn export_sync_state(state: SyncState) -> JsValue {
|
||||
JS::from(state.0).into()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = encodeSyncMessage)]
|
||||
pub fn encode_sync_message(message: JsValue) -> Result<Uint8Array, JsValue> {
|
||||
let heads = js_get(&message, "heads")?.try_into()?;
|
||||
let need = js_get(&message, "need")?.try_into()?;
|
||||
let changes = js_get(&message, "changes")?.try_into()?;
|
||||
let have = js_get(&message, "have")?.try_into()?;
|
||||
Ok(Uint8Array::from(
|
||||
am::sync::Message {
|
||||
heads,
|
||||
need,
|
||||
have,
|
||||
changes,
|
||||
}
|
||||
.encode()
|
||||
.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::sync::Message::decode(&data).map_err(to_js_err)?;
|
||||
let heads = AR::from(msg.heads.as_slice());
|
||||
let need = AR::from(msg.need.as_slice());
|
||||
let changes = AR::from(msg.changes.as_slice());
|
||||
let have = AR::from(msg.have.as_slice());
|
||||
let obj = Object::new().into();
|
||||
js_set(&obj, "heads", heads)?;
|
||||
js_set(&obj, "need", need)?;
|
||||
js_set(&obj, "have", have)?;
|
||||
js_set(&obj, "changes", changes)?;
|
||||
Ok(obj)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = encodeSyncState)]
|
||||
pub fn encode_sync_state(state: SyncState) -> Result<Uint8Array, JsValue> {
|
||||
let state = state.0;
|
||||
Ok(Uint8Array::from(state.encode().as_slice()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = decodeSyncState)]
|
||||
pub fn decode_sync_state(data: Uint8Array) -> Result<SyncState, JsValue> {
|
||||
SyncState::decode(data)
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
use automerge as am;
|
||||
use automerge::ChangeHash;
|
||||
use js_sys::Uint8Array;
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::convert::TryInto;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::interop::{self, to_js_err, AR, JS};
|
||||
use crate::interop::{to_js_err, AR, JS};
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
|
@ -24,10 +24,7 @@ impl SyncState {
|
|||
}
|
||||
|
||||
#[wasm_bindgen(setter, js_name = lastSentHeads)]
|
||||
pub fn set_last_sent_heads(
|
||||
&mut self,
|
||||
heads: JsValue,
|
||||
) -> Result<(), interop::error::BadChangeHashes> {
|
||||
pub fn set_last_sent_heads(&mut self, heads: JsValue) -> Result<(), JsValue> {
|
||||
let heads: Vec<ChangeHash> = JS(heads).try_into()?;
|
||||
self.0.last_sent_heads = heads;
|
||||
Ok(())
|
||||
|
@ -35,9 +32,8 @@ impl SyncState {
|
|||
|
||||
#[wasm_bindgen(setter, js_name = sentHashes)]
|
||||
pub fn set_sent_hashes(&mut self, hashes: JsValue) -> Result<(), JsValue> {
|
||||
let hashes_map: HashMap<ChangeHash, bool> =
|
||||
serde_wasm_bindgen::from_value(hashes).map_err(to_js_err)?;
|
||||
let hashes_set: BTreeSet<ChangeHash> = hashes_map.keys().cloned().collect();
|
||||
let hashes_map: HashMap<ChangeHash, bool> = hashes.into_serde().map_err(to_js_err)?;
|
||||
let hashes_set: HashSet<ChangeHash> = hashes_map.keys().cloned().collect();
|
||||
self.0.sent_hashes = hashes_set;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -47,19 +43,10 @@ impl SyncState {
|
|||
SyncState(self.0.clone())
|
||||
}
|
||||
|
||||
pub(crate) fn decode(data: Uint8Array) -> Result<SyncState, DecodeSyncStateErr> {
|
||||
pub(crate) fn decode(data: Uint8Array) -> Result<SyncState, JsValue> {
|
||||
let data = data.to_vec();
|
||||
let s = am::sync::State::decode(&data)?;
|
||||
let s = am::sync::State::decode(&data);
|
||||
let s = s.map_err(to_js_err)?;
|
||||
Ok(SyncState(s))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error(transparent)]
|
||||
pub struct DecodeSyncStateErr(#[from] automerge::sync::DecodeStateError);
|
||||
|
||||
impl From<DecodeSyncStateErr> for JsValue {
|
||||
fn from(e: DecodeSyncStateErr) -> Self {
|
||||
JsValue::from(e.to_string())
|
||||
}
|
||||
}
|
36
automerge-wasm/src/value.rs
Normal file
36
automerge-wasm/src/value.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
use automerge as am;
|
||||
use js_sys::Uint8Array;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ScalarValue(pub(crate) 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) => (f64::from(v)).into(),
|
||||
am::ScalarValue::Timestamp(v) => js_sys::Date::new(&(*v as f64).into()).into(),
|
||||
am::ScalarValue::Boolean(v) => (*v).into(),
|
||||
am::ScalarValue::Null => JsValue::null(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) 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(),
|
||||
}
|
||||
}
|
1415
automerge-wasm/test/helpers/columnar.js
Normal file
1415
automerge-wasm/test/helpers/columnar.js
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
|||
function isObject(obj) {
|
||||
return typeof obj === "object" && obj !== null
|
||||
return typeof obj === 'object' && obj !== null
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -20,11 +20,11 @@ function copyObject(obj) {
|
|||
* with an actor ID, separated by an `@` sign) and returns an object `{counter, actorId}`.
|
||||
*/
|
||||
function parseOpId(opId) {
|
||||
const match = /^(\d+)@(.*)$/.exec(opId || "")
|
||||
const match = /^(\d+)@(.*)$/.exec(opId || '')
|
||||
if (!match) {
|
||||
throw new RangeError(`Not a valid opId: ${opId}`)
|
||||
}
|
||||
return { counter: parseInt(match[1], 10), actorId: match[2] }
|
||||
return {counter: parseInt(match[1], 10), actorId: match[2]}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -32,7 +32,7 @@ function parseOpId(opId) {
|
|||
*/
|
||||
function equalBytes(array1, array2) {
|
||||
if (!(array1 instanceof Uint8Array) || !(array2 instanceof Uint8Array)) {
|
||||
throw new TypeError("equalBytes can only compare Uint8Arrays")
|
||||
throw new TypeError('equalBytes can only compare Uint8Arrays')
|
||||
}
|
||||
if (array1.byteLength !== array2.byteLength) return false
|
||||
for (let i = 0; i < array1.byteLength; i++) {
|
||||
|
@ -41,19 +41,6 @@ function equalBytes(array1, array2) {
|
|||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an array containing the value `null` repeated `length` times.
|
||||
*/
|
||||
function createArrayOfNulls(length) {
|
||||
const array = new Array(length)
|
||||
for (let i = 0; i < length; i++) array[i] = null
|
||||
return array
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isObject,
|
||||
copyObject,
|
||||
parseOpId,
|
||||
equalBytes,
|
||||
createArrayOfNulls,
|
||||
isObject, copyObject, parseOpId, equalBytes
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
* https://github.com/anonyco/FastestSmallestTextEncoderDecoder
|
||||
*/
|
||||
const utf8encoder = new TextEncoder()
|
||||
const utf8decoder = new TextDecoder("utf-8")
|
||||
const utf8decoder = new TextDecoder('utf-8')
|
||||
|
||||
function stringToUtf8(string) {
|
||||
return utf8encoder.encode(string)
|
||||
|
@ -20,48 +20,30 @@ function utf8ToString(buffer) {
|
|||
* Converts a string consisting of hexadecimal digits into an Uint8Array.
|
||||
*/
|
||||
function hexStringToBytes(value) {
|
||||
if (typeof value !== "string") {
|
||||
throw new TypeError("value is not a string")
|
||||
if (typeof value !== 'string') {
|
||||
throw new TypeError('value is not a string')
|
||||
}
|
||||
if (!/^([0-9a-f][0-9a-f])*$/.test(value)) {
|
||||
throw new RangeError("value is not hexadecimal")
|
||||
throw new RangeError('value is not hexadecimal')
|
||||
}
|
||||
if (value === "") {
|
||||
if (value === '') {
|
||||
return new Uint8Array(0)
|
||||
} else {
|
||||
return new Uint8Array(value.match(/../g).map(b => parseInt(b, 16)))
|
||||
}
|
||||
}
|
||||
|
||||
const NIBBLE_TO_HEX = [
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"d",
|
||||
"e",
|
||||
"f",
|
||||
]
|
||||
const NIBBLE_TO_HEX = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']
|
||||
const BYTE_TO_HEX = new Array(256)
|
||||
for (let i = 0; i < 256; i++) {
|
||||
BYTE_TO_HEX[i] = `${NIBBLE_TO_HEX[(i >>> 4) & 0xf]}${NIBBLE_TO_HEX[i & 0xf]}`
|
||||
BYTE_TO_HEX[i] = `${NIBBLE_TO_HEX[(i >>> 4) & 0xf]}${NIBBLE_TO_HEX[i & 0xf]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Uint8Array into the equivalent hexadecimal string.
|
||||
*/
|
||||
function bytesToHexString(bytes) {
|
||||
let hex = "",
|
||||
len = bytes.byteLength
|
||||
let hex = '', len = bytes.byteLength
|
||||
for (let i = 0; i < len; i++) {
|
||||
hex += BYTE_TO_HEX[bytes[i]]
|
||||
}
|
||||
|
@ -113,17 +95,14 @@ class Encoder {
|
|||
* appends it to the buffer. Returns the number of bytes written.
|
||||
*/
|
||||
appendUint32(value) {
|
||||
if (!Number.isInteger(value))
|
||||
throw new RangeError("value is not an integer")
|
||||
if (value < 0 || value > 0xffffffff)
|
||||
throw new RangeError("number out of range")
|
||||
if (!Number.isInteger(value)) throw new RangeError('value is not an integer')
|
||||
if (value < 0 || value > 0xffffffff) throw new RangeError('number out of range')
|
||||
|
||||
const numBytes = Math.max(1, Math.ceil((32 - Math.clz32(value)) / 7))
|
||||
if (this.offset + numBytes > this.buf.byteLength) this.grow()
|
||||
|
||||
for (let i = 0; i < numBytes; i++) {
|
||||
this.buf[this.offset + i] =
|
||||
(value & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
|
||||
this.buf[this.offset + i] = (value & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
|
||||
value >>>= 7 // zero-filling right shift
|
||||
}
|
||||
this.offset += numBytes
|
||||
|
@ -136,19 +115,14 @@ class Encoder {
|
|||
* it to the buffer. Returns the number of bytes written.
|
||||
*/
|
||||
appendInt32(value) {
|
||||
if (!Number.isInteger(value))
|
||||
throw new RangeError("value is not an integer")
|
||||
if (value < -0x80000000 || value > 0x7fffffff)
|
||||
throw new RangeError("number out of range")
|
||||
if (!Number.isInteger(value)) throw new RangeError('value is not an integer')
|
||||
if (value < -0x80000000 || value > 0x7fffffff) throw new RangeError('number out of range')
|
||||
|
||||
const numBytes = Math.ceil(
|
||||
(33 - Math.clz32(value >= 0 ? value : -value - 1)) / 7
|
||||
)
|
||||
const numBytes = Math.ceil((33 - Math.clz32(value >= 0 ? value : -value - 1)) / 7)
|
||||
if (this.offset + numBytes > this.buf.byteLength) this.grow()
|
||||
|
||||
for (let i = 0; i < numBytes; i++) {
|
||||
this.buf[this.offset + i] =
|
||||
(value & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
|
||||
this.buf[this.offset + i] = (value & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
|
||||
value >>= 7 // sign-propagating right shift
|
||||
}
|
||||
this.offset += numBytes
|
||||
|
@ -161,10 +135,9 @@ class Encoder {
|
|||
* (53 bits).
|
||||
*/
|
||||
appendUint53(value) {
|
||||
if (!Number.isInteger(value))
|
||||
throw new RangeError("value is not an integer")
|
||||
if (!Number.isInteger(value)) throw new RangeError('value is not an integer')
|
||||
if (value < 0 || value > Number.MAX_SAFE_INTEGER) {
|
||||
throw new RangeError("number out of range")
|
||||
throw new RangeError('number out of range')
|
||||
}
|
||||
const high32 = Math.floor(value / 0x100000000)
|
||||
const low32 = (value & 0xffffffff) >>> 0 // right shift to interpret as unsigned
|
||||
|
@ -177,10 +150,9 @@ class Encoder {
|
|||
* (53 bits).
|
||||
*/
|
||||
appendInt53(value) {
|
||||
if (!Number.isInteger(value))
|
||||
throw new RangeError("value is not an integer")
|
||||
if (!Number.isInteger(value)) throw new RangeError('value is not an integer')
|
||||
if (value < Number.MIN_SAFE_INTEGER || value > Number.MAX_SAFE_INTEGER) {
|
||||
throw new RangeError("number out of range")
|
||||
throw new RangeError('number out of range')
|
||||
}
|
||||
const high32 = Math.floor(value / 0x100000000)
|
||||
const low32 = (value & 0xffffffff) >>> 0 // right shift to interpret as unsigned
|
||||
|
@ -195,10 +167,10 @@ class Encoder {
|
|||
*/
|
||||
appendUint64(high32, low32) {
|
||||
if (!Number.isInteger(high32) || !Number.isInteger(low32)) {
|
||||
throw new RangeError("value is not an integer")
|
||||
throw new RangeError('value is not an integer')
|
||||
}
|
||||
if (high32 < 0 || high32 > 0xffffffff || low32 < 0 || low32 > 0xffffffff) {
|
||||
throw new RangeError("number out of range")
|
||||
throw new RangeError('number out of range')
|
||||
}
|
||||
if (high32 === 0) return this.appendUint32(low32)
|
||||
|
||||
|
@ -208,12 +180,10 @@ class Encoder {
|
|||
this.buf[this.offset + i] = (low32 & 0x7f) | 0x80
|
||||
low32 >>>= 7 // zero-filling right shift
|
||||
}
|
||||
this.buf[this.offset + 4] =
|
||||
(low32 & 0x0f) | ((high32 & 0x07) << 4) | (numBytes === 5 ? 0x00 : 0x80)
|
||||
this.buf[this.offset + 4] = (low32 & 0x0f) | ((high32 & 0x07) << 4) | (numBytes === 5 ? 0x00 : 0x80)
|
||||
high32 >>>= 3
|
||||
for (let i = 5; i < numBytes; i++) {
|
||||
this.buf[this.offset + i] =
|
||||
(high32 & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
|
||||
this.buf[this.offset + i] = (high32 & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
|
||||
high32 >>>= 7
|
||||
}
|
||||
this.offset += numBytes
|
||||
|
@ -230,35 +200,25 @@ class Encoder {
|
|||
*/
|
||||
appendInt64(high32, low32) {
|
||||
if (!Number.isInteger(high32) || !Number.isInteger(low32)) {
|
||||
throw new RangeError("value is not an integer")
|
||||
throw new RangeError('value is not an integer')
|
||||
}
|
||||
if (
|
||||
high32 < -0x80000000 ||
|
||||
high32 > 0x7fffffff ||
|
||||
low32 < -0x80000000 ||
|
||||
low32 > 0xffffffff
|
||||
) {
|
||||
throw new RangeError("number out of range")
|
||||
if (high32 < -0x80000000 || high32 > 0x7fffffff || low32 < -0x80000000 || low32 > 0xffffffff) {
|
||||
throw new RangeError('number out of range')
|
||||
}
|
||||
low32 >>>= 0 // interpret as unsigned
|
||||
if (high32 === 0 && low32 <= 0x7fffffff) return this.appendInt32(low32)
|
||||
if (high32 === -1 && low32 >= 0x80000000)
|
||||
return this.appendInt32(low32 - 0x100000000)
|
||||
if (high32 === -1 && low32 >= 0x80000000) return this.appendInt32(low32 - 0x100000000)
|
||||
|
||||
const numBytes = Math.ceil(
|
||||
(65 - Math.clz32(high32 >= 0 ? high32 : -high32 - 1)) / 7
|
||||
)
|
||||
const numBytes = Math.ceil((65 - Math.clz32(high32 >= 0 ? high32 : -high32 - 1)) / 7)
|
||||
if (this.offset + numBytes > this.buf.byteLength) this.grow()
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.buf[this.offset + i] = (low32 & 0x7f) | 0x80
|
||||
low32 >>>= 7 // zero-filling right shift
|
||||
}
|
||||
this.buf[this.offset + 4] =
|
||||
(low32 & 0x0f) | ((high32 & 0x07) << 4) | (numBytes === 5 ? 0x00 : 0x80)
|
||||
this.buf[this.offset + 4] = (low32 & 0x0f) | ((high32 & 0x07) << 4) | (numBytes === 5 ? 0x00 : 0x80)
|
||||
high32 >>= 3 // sign-propagating right shift
|
||||
for (let i = 5; i < numBytes; i++) {
|
||||
this.buf[this.offset + i] =
|
||||
(high32 & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
|
||||
this.buf[this.offset + i] = (high32 & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
|
||||
high32 >>= 7
|
||||
}
|
||||
this.offset += numBytes
|
||||
|
@ -283,7 +243,7 @@ class Encoder {
|
|||
* number of bytes appended.
|
||||
*/
|
||||
appendRawString(value) {
|
||||
if (typeof value !== "string") throw new TypeError("value is not a string")
|
||||
if (typeof value !== 'string') throw new TypeError('value is not a string')
|
||||
return this.appendRawBytes(stringToUtf8(value))
|
||||
}
|
||||
|
||||
|
@ -302,7 +262,7 @@ class Encoder {
|
|||
* (where the length is encoded as an unsigned LEB128 integer).
|
||||
*/
|
||||
appendPrefixedString(value) {
|
||||
if (typeof value !== "string") throw new TypeError("value is not a string")
|
||||
if (typeof value !== 'string') throw new TypeError('value is not a string')
|
||||
this.appendPrefixedBytes(stringToUtf8(value))
|
||||
return this
|
||||
}
|
||||
|
@ -321,7 +281,8 @@ class Encoder {
|
|||
* Flushes any unwritten data to the buffer. Call this before reading from
|
||||
* the buffer constructed by this Encoder.
|
||||
*/
|
||||
finish() {}
|
||||
finish() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -360,7 +321,7 @@ class Decoder {
|
|||
*/
|
||||
skip(bytes) {
|
||||
if (this.offset + bytes > this.buf.byteLength) {
|
||||
throw new RangeError("cannot skip beyond end of buffer")
|
||||
throw new RangeError('cannot skip beyond end of buffer')
|
||||
}
|
||||
this.offset += bytes
|
||||
}
|
||||
|
@ -378,20 +339,18 @@ class Decoder {
|
|||
* Throws an exception if the value doesn't fit in a 32-bit unsigned int.
|
||||
*/
|
||||
readUint32() {
|
||||
let result = 0,
|
||||
shift = 0
|
||||
let result = 0, shift = 0
|
||||
while (this.offset < this.buf.byteLength) {
|
||||
const nextByte = this.buf[this.offset]
|
||||
if (shift === 28 && (nextByte & 0xf0) !== 0) {
|
||||
// more than 5 bytes, or value > 0xffffffff
|
||||
throw new RangeError("number out of range")
|
||||
if (shift === 28 && (nextByte & 0xf0) !== 0) { // more than 5 bytes, or value > 0xffffffff
|
||||
throw new RangeError('number out of range')
|
||||
}
|
||||
result = (result | ((nextByte & 0x7f) << shift)) >>> 0 // right shift to interpret value as unsigned
|
||||
result = (result | (nextByte & 0x7f) << shift) >>> 0 // right shift to interpret value as unsigned
|
||||
shift += 7
|
||||
this.offset++
|
||||
if ((nextByte & 0x80) === 0) return result
|
||||
}
|
||||
throw new RangeError("buffer ended with incomplete number")
|
||||
throw new RangeError('buffer ended with incomplete number')
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -399,17 +358,13 @@ class Decoder {
|
|||
* Throws an exception if the value doesn't fit in a 32-bit signed int.
|
||||
*/
|
||||
readInt32() {
|
||||
let result = 0,
|
||||
shift = 0
|
||||
let result = 0, shift = 0
|
||||
while (this.offset < this.buf.byteLength) {
|
||||
const nextByte = this.buf[this.offset]
|
||||
if (
|
||||
(shift === 28 && (nextByte & 0x80) !== 0) || // more than 5 bytes
|
||||
(shift === 28 && (nextByte & 0x40) === 0 && (nextByte & 0x38) !== 0) || // positive int > 0x7fffffff
|
||||
(shift === 28 && (nextByte & 0x40) !== 0 && (nextByte & 0x38) !== 0x38)
|
||||
) {
|
||||
// negative int < -0x80000000
|
||||
throw new RangeError("number out of range")
|
||||
if ((shift === 28 && (nextByte & 0x80) !== 0) || // more than 5 bytes
|
||||
(shift === 28 && (nextByte & 0x40) === 0 && (nextByte & 0x38) !== 0) || // positive int > 0x7fffffff
|
||||
(shift === 28 && (nextByte & 0x40) !== 0 && (nextByte & 0x38) !== 0x38)) { // negative int < -0x80000000
|
||||
throw new RangeError('number out of range')
|
||||
}
|
||||
result |= (nextByte & 0x7f) << shift
|
||||
shift += 7
|
||||
|
@ -423,7 +378,7 @@ class Decoder {
|
|||
}
|
||||
}
|
||||
}
|
||||
throw new RangeError("buffer ended with incomplete number")
|
||||
throw new RangeError('buffer ended with incomplete number')
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -434,7 +389,7 @@ class Decoder {
|
|||
readUint53() {
|
||||
const { low32, high32 } = this.readUint64()
|
||||
if (high32 < 0 || high32 > 0x1fffff) {
|
||||
throw new RangeError("number out of range")
|
||||
throw new RangeError('number out of range')
|
||||
}
|
||||
return high32 * 0x100000000 + low32
|
||||
}
|
||||
|
@ -446,12 +401,8 @@ class Decoder {
|
|||
*/
|
||||
readInt53() {
|
||||
const { low32, high32 } = this.readInt64()
|
||||
if (
|
||||
high32 < -0x200000 ||
|
||||
(high32 === -0x200000 && low32 === 0) ||
|
||||
high32 > 0x1fffff
|
||||
) {
|
||||
throw new RangeError("number out of range")
|
||||
if (high32 < -0x200000 || (high32 === -0x200000 && low32 === 0) || high32 > 0x1fffff) {
|
||||
throw new RangeError('number out of range')
|
||||
}
|
||||
return high32 * 0x100000000 + low32
|
||||
}
|
||||
|
@ -463,12 +414,10 @@ class Decoder {
|
|||
* `{high32, low32}`.
|
||||
*/
|
||||
readUint64() {
|
||||
let low32 = 0,
|
||||
high32 = 0,
|
||||
shift = 0
|
||||
let low32 = 0, high32 = 0, shift = 0
|
||||
while (this.offset < this.buf.byteLength && shift <= 28) {
|
||||
const nextByte = this.buf[this.offset]
|
||||
low32 = (low32 | ((nextByte & 0x7f) << shift)) >>> 0 // right shift to interpret value as unsigned
|
||||
low32 = (low32 | (nextByte & 0x7f) << shift) >>> 0 // right shift to interpret value as unsigned
|
||||
if (shift === 28) {
|
||||
high32 = (nextByte & 0x70) >>> 4
|
||||
}
|
||||
|
@ -480,16 +429,15 @@ class Decoder {
|
|||
shift = 3
|
||||
while (this.offset < this.buf.byteLength) {
|
||||
const nextByte = this.buf[this.offset]
|
||||
if (shift === 31 && (nextByte & 0xfe) !== 0) {
|
||||
// more than 10 bytes, or value > 2^64 - 1
|
||||
throw new RangeError("number out of range")
|
||||
if (shift === 31 && (nextByte & 0xfe) !== 0) { // more than 10 bytes, or value > 2^64 - 1
|
||||
throw new RangeError('number out of range')
|
||||
}
|
||||
high32 = (high32 | ((nextByte & 0x7f) << shift)) >>> 0
|
||||
high32 = (high32 | (nextByte & 0x7f) << shift) >>> 0
|
||||
shift += 7
|
||||
this.offset++
|
||||
if ((nextByte & 0x80) === 0) return { high32, low32 }
|
||||
}
|
||||
throw new RangeError("buffer ended with incomplete number")
|
||||
throw new RangeError('buffer ended with incomplete number')
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -500,20 +448,17 @@ class Decoder {
|
|||
* sign of the `high32` half indicates the sign of the 64-bit number.
|
||||
*/
|
||||
readInt64() {
|
||||
let low32 = 0,
|
||||
high32 = 0,
|
||||
shift = 0
|
||||
let low32 = 0, high32 = 0, shift = 0
|
||||
while (this.offset < this.buf.byteLength && shift <= 28) {
|
||||
const nextByte = this.buf[this.offset]
|
||||
low32 = (low32 | ((nextByte & 0x7f) << shift)) >>> 0 // right shift to interpret value as unsigned
|
||||
low32 = (low32 | (nextByte & 0x7f) << shift) >>> 0 // right shift to interpret value as unsigned
|
||||
if (shift === 28) {
|
||||
high32 = (nextByte & 0x70) >>> 4
|
||||
}
|
||||
shift += 7
|
||||
this.offset++
|
||||
if ((nextByte & 0x80) === 0) {
|
||||
if ((nextByte & 0x40) !== 0) {
|
||||
// sign-extend negative integer
|
||||
if ((nextByte & 0x40) !== 0) { // sign-extend negative integer
|
||||
if (shift < 32) low32 = (low32 | (-1 << shift)) >>> 0
|
||||
high32 |= -1 << Math.max(shift - 32, 0)
|
||||
}
|
||||
|
@ -527,20 +472,19 @@ class Decoder {
|
|||
// On the 10th byte there are only two valid values: all 7 value bits zero
|
||||
// (if the value is positive) or all 7 bits one (if the value is negative)
|
||||
if (shift === 31 && nextByte !== 0 && nextByte !== 0x7f) {
|
||||
throw new RangeError("number out of range")
|
||||
throw new RangeError('number out of range')
|
||||
}
|
||||
high32 |= (nextByte & 0x7f) << shift
|
||||
shift += 7
|
||||
this.offset++
|
||||
if ((nextByte & 0x80) === 0) {
|
||||
if ((nextByte & 0x40) !== 0 && shift < 32) {
|
||||
// sign-extend negative integer
|
||||
if ((nextByte & 0x40) !== 0 && shift < 32) { // sign-extend negative integer
|
||||
high32 |= -1 << shift
|
||||
}
|
||||
return { high32, low32 }
|
||||
}
|
||||
}
|
||||
throw new RangeError("buffer ended with incomplete number")
|
||||
throw new RangeError('buffer ended with incomplete number')
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -550,7 +494,7 @@ class Decoder {
|
|||
readRawBytes(length) {
|
||||
const start = this.offset
|
||||
if (start + length > this.buf.byteLength) {
|
||||
throw new RangeError("subarray exceeds buffer size")
|
||||
throw new RangeError('subarray exceeds buffer size')
|
||||
}
|
||||
this.offset += length
|
||||
return this.buf.subarray(start, this.offset)
|
||||
|
@ -615,7 +559,7 @@ class RLEEncoder extends Encoder {
|
|||
constructor(type) {
|
||||
super()
|
||||
this.type = type
|
||||
this.state = "empty"
|
||||
this.state = 'empty'
|
||||
this.lastValue = undefined
|
||||
this.count = 0
|
||||
this.literal = []
|
||||
|
@ -634,81 +578,76 @@ class RLEEncoder extends Encoder {
|
|||
*/
|
||||
_appendValue(value, repetitions = 1) {
|
||||
if (repetitions <= 0) return
|
||||
if (this.state === "empty") {
|
||||
this.state =
|
||||
value === null
|
||||
? "nulls"
|
||||
: repetitions === 1
|
||||
? "loneValue"
|
||||
: "repetition"
|
||||
if (this.state === 'empty') {
|
||||
this.state = (value === null ? 'nulls' : (repetitions === 1 ? 'loneValue' : 'repetition'))
|
||||
this.lastValue = value
|
||||
this.count = repetitions
|
||||
} else if (this.state === "loneValue") {
|
||||
} else if (this.state === 'loneValue') {
|
||||
if (value === null) {
|
||||
this.flush()
|
||||
this.state = "nulls"
|
||||
this.state = 'nulls'
|
||||
this.count = repetitions
|
||||
} else if (value === this.lastValue) {
|
||||
this.state = "repetition"
|
||||
this.state = 'repetition'
|
||||
this.count = 1 + repetitions
|
||||
} else if (repetitions > 1) {
|
||||
this.flush()
|
||||
this.state = "repetition"
|
||||
this.state = 'repetition'
|
||||
this.count = repetitions
|
||||
this.lastValue = value
|
||||
} else {
|
||||
this.state = "literal"
|
||||
this.state = 'literal'
|
||||
this.literal = [this.lastValue]
|
||||
this.lastValue = value
|
||||
}
|
||||
} else if (this.state === "repetition") {
|
||||
} else if (this.state === 'repetition') {
|
||||
if (value === null) {
|
||||
this.flush()
|
||||
this.state = "nulls"
|
||||
this.state = 'nulls'
|
||||
this.count = repetitions
|
||||
} else if (value === this.lastValue) {
|
||||
this.count += repetitions
|
||||
} else if (repetitions > 1) {
|
||||
this.flush()
|
||||
this.state = "repetition"
|
||||
this.state = 'repetition'
|
||||
this.count = repetitions
|
||||
this.lastValue = value
|
||||
} else {
|
||||
this.flush()
|
||||
this.state = "loneValue"
|
||||
this.state = 'loneValue'
|
||||
this.lastValue = value
|
||||
}
|
||||
} else if (this.state === "literal") {
|
||||
} else if (this.state === 'literal') {
|
||||
if (value === null) {
|
||||
this.literal.push(this.lastValue)
|
||||
this.flush()
|
||||
this.state = "nulls"
|
||||
this.state = 'nulls'
|
||||
this.count = repetitions
|
||||
} else if (value === this.lastValue) {
|
||||
this.flush()
|
||||
this.state = "repetition"
|
||||
this.state = 'repetition'
|
||||
this.count = 1 + repetitions
|
||||
} else if (repetitions > 1) {
|
||||
this.literal.push(this.lastValue)
|
||||
this.flush()
|
||||
this.state = "repetition"
|
||||
this.state = 'repetition'
|
||||
this.count = repetitions
|
||||
this.lastValue = value
|
||||
} else {
|
||||
this.literal.push(this.lastValue)
|
||||
this.lastValue = value
|
||||
}
|
||||
} else if (this.state === "nulls") {
|
||||
} else if (this.state === 'nulls') {
|
||||
if (value === null) {
|
||||
this.count += repetitions
|
||||
} else if (repetitions > 1) {
|
||||
this.flush()
|
||||
this.state = "repetition"
|
||||
this.state = 'repetition'
|
||||
this.count = repetitions
|
||||
this.lastValue = value
|
||||
} else {
|
||||
this.flush()
|
||||
this.state = "loneValue"
|
||||
this.state = 'loneValue'
|
||||
this.lastValue = value
|
||||
}
|
||||
}
|
||||
|
@ -727,16 +666,13 @@ class RLEEncoder extends Encoder {
|
|||
*/
|
||||
copyFrom(decoder, options = {}) {
|
||||
const { count, sumValues, sumShift } = options
|
||||
if (!(decoder instanceof RLEDecoder) || decoder.type !== this.type) {
|
||||
throw new TypeError("incompatible type of decoder")
|
||||
if (!(decoder instanceof RLEDecoder) || (decoder.type !== this.type)) {
|
||||
throw new TypeError('incompatible type of decoder')
|
||||
}
|
||||
let remaining = typeof count === "number" ? count : Number.MAX_SAFE_INTEGER
|
||||
let nonNullValues = 0,
|
||||
sum = 0
|
||||
if (count && remaining > 0 && decoder.done)
|
||||
throw new RangeError(`cannot copy ${count} values`)
|
||||
if (remaining === 0 || decoder.done)
|
||||
return sumValues ? { nonNullValues, sum } : { nonNullValues }
|
||||
let remaining = (typeof count === 'number' ? count : Number.MAX_SAFE_INTEGER)
|
||||
let nonNullValues = 0, sum = 0
|
||||
if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)
|
||||
if (remaining === 0 || decoder.done) return sumValues ? {nonNullValues, sum} : {nonNullValues}
|
||||
|
||||
// Copy a value so that we have a well-defined starting state. NB: when super.copyFrom() is
|
||||
// called by the DeltaEncoder subclass, the following calls to readValue() and appendValue()
|
||||
|
@ -748,101 +684,87 @@ class RLEEncoder extends Encoder {
|
|||
remaining -= numNulls
|
||||
decoder.count -= numNulls - 1
|
||||
this.appendValue(null, numNulls)
|
||||
if (count && remaining > 0 && decoder.done)
|
||||
throw new RangeError(`cannot copy ${count} values`)
|
||||
if (remaining === 0 || decoder.done)
|
||||
return sumValues ? { nonNullValues, sum } : { nonNullValues }
|
||||
if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)
|
||||
if (remaining === 0 || decoder.done) return sumValues ? {nonNullValues, sum} : {nonNullValues}
|
||||
firstValue = decoder.readValue()
|
||||
if (firstValue === null)
|
||||
throw new RangeError("null run must be followed by non-null value")
|
||||
if (firstValue === null) throw new RangeError('null run must be followed by non-null value')
|
||||
}
|
||||
this.appendValue(firstValue)
|
||||
remaining--
|
||||
nonNullValues++
|
||||
if (sumValues) sum += sumShift ? firstValue >>> sumShift : firstValue
|
||||
if (count && remaining > 0 && decoder.done)
|
||||
throw new RangeError(`cannot copy ${count} values`)
|
||||
if (remaining === 0 || decoder.done)
|
||||
return sumValues ? { nonNullValues, sum } : { nonNullValues }
|
||||
if (sumValues) sum += (sumShift ? (firstValue >>> sumShift) : firstValue)
|
||||
if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)
|
||||
if (remaining === 0 || decoder.done) return sumValues ? {nonNullValues, sum} : {nonNullValues}
|
||||
|
||||
// Copy data at the record level without expanding repetitions
|
||||
let firstRun = decoder.count > 0
|
||||
let firstRun = (decoder.count > 0)
|
||||
while (remaining > 0 && !decoder.done) {
|
||||
if (!firstRun) decoder.readRecord()
|
||||
const numValues = Math.min(decoder.count, remaining)
|
||||
decoder.count -= numValues
|
||||
|
||||
if (decoder.state === "literal") {
|
||||
if (decoder.state === 'literal') {
|
||||
nonNullValues += numValues
|
||||
for (let i = 0; i < numValues; i++) {
|
||||
if (decoder.done) throw new RangeError("incomplete literal")
|
||||
if (decoder.done) throw new RangeError('incomplete literal')
|
||||
const value = decoder.readRawValue()
|
||||
if (value === decoder.lastValue)
|
||||
throw new RangeError(
|
||||
"Repetition of values is not allowed in literal"
|
||||
)
|
||||
if (value === decoder.lastValue) throw new RangeError('Repetition of values is not allowed in literal')
|
||||
decoder.lastValue = value
|
||||
this._appendValue(value)
|
||||
if (sumValues) sum += sumShift ? value >>> sumShift : value
|
||||
if (sumValues) sum += (sumShift ? (value >>> sumShift) : value)
|
||||
}
|
||||
} else if (decoder.state === "repetition") {
|
||||
} else if (decoder.state === 'repetition') {
|
||||
nonNullValues += numValues
|
||||
if (sumValues)
|
||||
sum +=
|
||||
numValues *
|
||||
(sumShift ? decoder.lastValue >>> sumShift : decoder.lastValue)
|
||||
if (sumValues) sum += numValues * (sumShift ? (decoder.lastValue >>> sumShift) : decoder.lastValue)
|
||||
const value = decoder.lastValue
|
||||
this._appendValue(value)
|
||||
if (numValues > 1) {
|
||||
this._appendValue(value)
|
||||
if (this.state !== "repetition")
|
||||
throw new RangeError(`Unexpected state ${this.state}`)
|
||||
if (this.state !== 'repetition') throw new RangeError(`Unexpected state ${this.state}`)
|
||||
this.count += numValues - 2
|
||||
}
|
||||
} else if (decoder.state === "nulls") {
|
||||
} else if (decoder.state === 'nulls') {
|
||||
this._appendValue(null)
|
||||
if (this.state !== "nulls")
|
||||
throw new RangeError(`Unexpected state ${this.state}`)
|
||||
if (this.state !== 'nulls') throw new RangeError(`Unexpected state ${this.state}`)
|
||||
this.count += numValues - 1
|
||||
}
|
||||
|
||||
firstRun = false
|
||||
remaining -= numValues
|
||||
}
|
||||
if (count && remaining > 0 && decoder.done)
|
||||
throw new RangeError(`cannot copy ${count} values`)
|
||||
return sumValues ? { nonNullValues, sum } : { nonNullValues }
|
||||
if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)
|
||||
return sumValues ? {nonNullValues, sum} : {nonNullValues}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method, do not call from outside the class.
|
||||
*/
|
||||
flush() {
|
||||
if (this.state === "loneValue") {
|
||||
if (this.state === 'loneValue') {
|
||||
this.appendInt32(-1)
|
||||
this.appendRawValue(this.lastValue)
|
||||
} else if (this.state === "repetition") {
|
||||
} else if (this.state === 'repetition') {
|
||||
this.appendInt53(this.count)
|
||||
this.appendRawValue(this.lastValue)
|
||||
} else if (this.state === "literal") {
|
||||
} else if (this.state === 'literal') {
|
||||
this.appendInt53(-this.literal.length)
|
||||
for (let v of this.literal) this.appendRawValue(v)
|
||||
} else if (this.state === "nulls") {
|
||||
} else if (this.state === 'nulls') {
|
||||
this.appendInt32(0)
|
||||
this.appendUint53(this.count)
|
||||
}
|
||||
this.state = "empty"
|
||||
this.state = 'empty'
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method, do not call from outside the class.
|
||||
*/
|
||||
appendRawValue(value) {
|
||||
if (this.type === "int") {
|
||||
if (this.type === 'int') {
|
||||
this.appendInt53(value)
|
||||
} else if (this.type === "uint") {
|
||||
} else if (this.type === 'uint') {
|
||||
this.appendUint53(value)
|
||||
} else if (this.type === "utf8") {
|
||||
} else if (this.type === 'utf8') {
|
||||
this.appendPrefixedString(value)
|
||||
} else {
|
||||
throw new RangeError(`Unknown RLEEncoder datatype: ${this.type}`)
|
||||
|
@ -854,9 +776,9 @@ class RLEEncoder extends Encoder {
|
|||
* the buffer constructed by this Encoder.
|
||||
*/
|
||||
finish() {
|
||||
if (this.state === "literal") this.literal.push(this.lastValue)
|
||||
if (this.state === 'literal') this.literal.push(this.lastValue)
|
||||
// Don't write anything if the only values we have seen are nulls
|
||||
if (this.state !== "nulls" || this.offset > 0) this.flush()
|
||||
if (this.state !== 'nulls' || this.offset > 0) this.flush()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -878,7 +800,7 @@ class RLEDecoder extends Decoder {
|
|||
* position, and true if we are at the end of the buffer.
|
||||
*/
|
||||
get done() {
|
||||
return this.count === 0 && this.offset === this.buf.byteLength
|
||||
return (this.count === 0) && (this.offset === this.buf.byteLength)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -899,10 +821,9 @@ class RLEDecoder extends Decoder {
|
|||
if (this.done) return null
|
||||
if (this.count === 0) this.readRecord()
|
||||
this.count -= 1
|
||||
if (this.state === "literal") {
|
||||
if (this.state === 'literal') {
|
||||
const value = this.readRawValue()
|
||||
if (value === this.lastValue)
|
||||
throw new RangeError("Repetition of values is not allowed in literal")
|
||||
if (value === this.lastValue) throw new RangeError('Repetition of values is not allowed in literal')
|
||||
this.lastValue = value
|
||||
return value
|
||||
} else {
|
||||
|
@ -918,22 +839,20 @@ class RLEDecoder extends Decoder {
|
|||
if (this.count === 0) {
|
||||
this.count = this.readInt53()
|
||||
if (this.count > 0) {
|
||||
this.lastValue =
|
||||
this.count <= numSkip ? this.skipRawValues(1) : this.readRawValue()
|
||||
this.state = "repetition"
|
||||
this.lastValue = (this.count <= numSkip) ? this.skipRawValues(1) : this.readRawValue()
|
||||
this.state = 'repetition'
|
||||
} else if (this.count < 0) {
|
||||
this.count = -this.count
|
||||
this.state = "literal"
|
||||
} else {
|
||||
// this.count == 0
|
||||
this.state = 'literal'
|
||||
} else { // this.count == 0
|
||||
this.count = this.readUint53()
|
||||
this.lastValue = null
|
||||
this.state = "nulls"
|
||||
this.state = 'nulls'
|
||||
}
|
||||
}
|
||||
|
||||
const consume = Math.min(numSkip, this.count)
|
||||
if (this.state === "literal") this.skipRawValues(consume)
|
||||
if (this.state === 'literal') this.skipRawValues(consume)
|
||||
numSkip -= consume
|
||||
this.count -= consume
|
||||
}
|
||||
|
@ -947,34 +866,23 @@ class RLEDecoder extends Decoder {
|
|||
this.count = this.readInt53()
|
||||
if (this.count > 1) {
|
||||
const value = this.readRawValue()
|
||||
if (
|
||||
(this.state === "repetition" || this.state === "literal") &&
|
||||
this.lastValue === value
|
||||
) {
|
||||
throw new RangeError(
|
||||
"Successive repetitions with the same value are not allowed"
|
||||
)
|
||||
if ((this.state === 'repetition' || this.state === 'literal') && this.lastValue === value) {
|
||||
throw new RangeError('Successive repetitions with the same value are not allowed')
|
||||
}
|
||||
this.state = "repetition"
|
||||
this.state = 'repetition'
|
||||
this.lastValue = value
|
||||
} else if (this.count === 1) {
|
||||
throw new RangeError(
|
||||
"Repetition count of 1 is not allowed, use a literal instead"
|
||||
)
|
||||
throw new RangeError('Repetition count of 1 is not allowed, use a literal instead')
|
||||
} else if (this.count < 0) {
|
||||
this.count = -this.count
|
||||
if (this.state === "literal")
|
||||
throw new RangeError("Successive literals are not allowed")
|
||||
this.state = "literal"
|
||||
} else {
|
||||
// this.count == 0
|
||||
if (this.state === "nulls")
|
||||
throw new RangeError("Successive null runs are not allowed")
|
||||
if (this.state === 'literal') throw new RangeError('Successive literals are not allowed')
|
||||
this.state = 'literal'
|
||||
} else { // this.count == 0
|
||||
if (this.state === 'nulls') throw new RangeError('Successive null runs are not allowed')
|
||||
this.count = this.readUint53()
|
||||
if (this.count === 0)
|
||||
throw new RangeError("Zero-length null runs are not allowed")
|
||||
if (this.count === 0) throw new RangeError('Zero-length null runs are not allowed')
|
||||
this.lastValue = null
|
||||
this.state = "nulls"
|
||||
this.state = 'nulls'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -983,11 +891,11 @@ class RLEDecoder extends Decoder {
|
|||
* Reads one value of the datatype configured on construction.
|
||||
*/
|
||||
readRawValue() {
|
||||
if (this.type === "int") {
|
||||
if (this.type === 'int') {
|
||||
return this.readInt53()
|
||||
} else if (this.type === "uint") {
|
||||
} else if (this.type === 'uint') {
|
||||
return this.readUint53()
|
||||
} else if (this.type === "utf8") {
|
||||
} else if (this.type === 'utf8') {
|
||||
return this.readPrefixedString()
|
||||
} else {
|
||||
throw new RangeError(`Unknown RLEDecoder datatype: ${this.type}`)
|
||||
|
@ -999,14 +907,14 @@ class RLEDecoder extends Decoder {
|
|||
* Skips over `num` values of the datatype configured on construction.
|
||||
*/
|
||||
skipRawValues(num) {
|
||||
if (this.type === "utf8") {
|
||||
if (this.type === 'utf8') {
|
||||
for (let i = 0; i < num; i++) this.skip(this.readUint53())
|
||||
} else {
|
||||
while (num > 0 && this.offset < this.buf.byteLength) {
|
||||
if ((this.buf[this.offset] & 0x80) === 0) num--
|
||||
this.offset++
|
||||
}
|
||||
if (num > 0) throw new RangeError("cannot skip beyond end of buffer")
|
||||
if (num > 0) throw new RangeError('cannot skip beyond end of buffer')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1023,7 +931,7 @@ class RLEDecoder extends Decoder {
|
|||
*/
|
||||
class DeltaEncoder extends RLEEncoder {
|
||||
constructor() {
|
||||
super("int")
|
||||
super('int')
|
||||
this.absoluteValue = 0
|
||||
}
|
||||
|
||||
|
@ -1033,7 +941,7 @@ class DeltaEncoder extends RLEEncoder {
|
|||
*/
|
||||
appendValue(value, repetitions = 1) {
|
||||
if (repetitions <= 0) return
|
||||
if (typeof value === "number") {
|
||||
if (typeof value === 'number') {
|
||||
super.appendValue(value - this.absoluteValue, 1)
|
||||
this.absoluteValue = value
|
||||
if (repetitions > 1) super.appendValue(0, repetitions - 1)
|
||||
|
@ -1049,29 +957,26 @@ class DeltaEncoder extends RLEEncoder {
|
|||
*/
|
||||
copyFrom(decoder, options = {}) {
|
||||
if (options.sumValues) {
|
||||
throw new RangeError("unsupported options for DeltaEncoder.copyFrom()")
|
||||
throw new RangeError('unsupported options for DeltaEncoder.copyFrom()')
|
||||
}
|
||||
if (!(decoder instanceof DeltaDecoder)) {
|
||||
throw new TypeError("incompatible type of decoder")
|
||||
throw new TypeError('incompatible type of decoder')
|
||||
}
|
||||
|
||||
let remaining = options.count
|
||||
if (remaining > 0 && decoder.done)
|
||||
throw new RangeError(`cannot copy ${remaining} values`)
|
||||
if (remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${remaining} values`)
|
||||
if (remaining === 0 || decoder.done) return
|
||||
|
||||
// Copy any null values, and the first non-null value, so that appendValue() computes the
|
||||
// difference between the encoder's last value and the decoder's first (absolute) value.
|
||||
let value = decoder.readValue(),
|
||||
nulls = 0
|
||||
let value = decoder.readValue(), nulls = 0
|
||||
this.appendValue(value)
|
||||
if (value === null) {
|
||||
nulls = decoder.count + 1
|
||||
if (remaining !== undefined && remaining < nulls) nulls = remaining
|
||||
decoder.count -= nulls - 1
|
||||
this.count += nulls - 1
|
||||
if (remaining > nulls && decoder.done)
|
||||
throw new RangeError(`cannot copy ${remaining} values`)
|
||||
if (remaining > nulls && decoder.done) throw new RangeError(`cannot copy ${remaining} values`)
|
||||
if (remaining === nulls || decoder.done) return
|
||||
|
||||
// The next value read is certain to be non-null because we're not at the end of the decoder,
|
||||
|
@ -1084,10 +989,7 @@ class DeltaEncoder extends RLEEncoder {
|
|||
// value, while subsequent values are relative. Thus, the sum of all of the (non-null) copied
|
||||
// values must equal the absolute value of the final element copied.
|
||||
if (remaining !== undefined) remaining -= nulls + 1
|
||||
const { nonNullValues, sum } = super.copyFrom(decoder, {
|
||||
count: remaining,
|
||||
sumValues: true,
|
||||
})
|
||||
const { nonNullValues, sum } = super.copyFrom(decoder, {count: remaining, sumValues: true})
|
||||
if (nonNullValues > 0) {
|
||||
this.absoluteValue = sum
|
||||
decoder.absoluteValue = sum
|
||||
|
@ -1101,7 +1003,7 @@ class DeltaEncoder extends RLEEncoder {
|
|||
*/
|
||||
class DeltaDecoder extends RLEDecoder {
|
||||
constructor(buffer) {
|
||||
super("int", buffer)
|
||||
super('int', buffer)
|
||||
this.absoluteValue = 0
|
||||
}
|
||||
|
||||
|
@ -1134,12 +1036,12 @@ class DeltaDecoder extends RLEDecoder {
|
|||
while (numSkip > 0 && !this.done) {
|
||||
if (this.count === 0) this.readRecord()
|
||||
const consume = Math.min(numSkip, this.count)
|
||||
if (this.state === "literal") {
|
||||
if (this.state === 'literal') {
|
||||
for (let i = 0; i < consume; i++) {
|
||||
this.lastValue = this.readRawValue()
|
||||
this.absoluteValue += this.lastValue
|
||||
}
|
||||
} else if (this.state === "repetition") {
|
||||
} else if (this.state === 'repetition') {
|
||||
this.absoluteValue += consume * this.lastValue
|
||||
}
|
||||
numSkip -= consume
|
||||
|
@ -1188,13 +1090,12 @@ class BooleanEncoder extends Encoder {
|
|||
*/
|
||||
copyFrom(decoder, options = {}) {
|
||||
if (!(decoder instanceof BooleanDecoder)) {
|
||||
throw new TypeError("incompatible type of decoder")
|
||||
throw new TypeError('incompatible type of decoder')
|
||||
}
|
||||
|
||||
const { count } = options
|
||||
let remaining = typeof count === "number" ? count : Number.MAX_SAFE_INTEGER
|
||||
if (count && remaining > 0 && decoder.done)
|
||||
throw new RangeError(`cannot copy ${count} values`)
|
||||
let remaining = (typeof count === 'number' ? count : Number.MAX_SAFE_INTEGER)
|
||||
if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)
|
||||
if (remaining === 0 || decoder.done) return
|
||||
|
||||
// Copy one value to bring decoder and encoder state into sync, then finish that value's repetitions
|
||||
|
@ -1207,8 +1108,7 @@ class BooleanEncoder extends Encoder {
|
|||
|
||||
while (remaining > 0 && !decoder.done) {
|
||||
decoder.count = decoder.readUint53()
|
||||
if (decoder.count === 0)
|
||||
throw new RangeError("Zero-length runs are not allowed")
|
||||
if (decoder.count === 0) throw new RangeError('Zero-length runs are not allowed')
|
||||
decoder.lastValue = !decoder.lastValue
|
||||
this.appendUint53(this.count)
|
||||
|
||||
|
@ -1219,8 +1119,7 @@ class BooleanEncoder extends Encoder {
|
|||
remaining -= numCopied
|
||||
}
|
||||
|
||||
if (count && remaining > 0 && decoder.done)
|
||||
throw new RangeError(`cannot copy ${count} values`)
|
||||
if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1252,7 +1151,7 @@ class BooleanDecoder extends Decoder {
|
|||
* position, and true if we are at the end of the buffer.
|
||||
*/
|
||||
get done() {
|
||||
return this.count === 0 && this.offset === this.buf.byteLength
|
||||
return (this.count === 0) && (this.offset === this.buf.byteLength)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1275,7 +1174,7 @@ class BooleanDecoder extends Decoder {
|
|||
this.count = this.readUint53()
|
||||
this.lastValue = !this.lastValue
|
||||
if (this.count === 0 && !this.firstRun) {
|
||||
throw new RangeError("Zero-length runs are not allowed")
|
||||
throw new RangeError('Zero-length runs are not allowed')
|
||||
}
|
||||
this.firstRun = false
|
||||
}
|
||||
|
@ -1291,8 +1190,7 @@ class BooleanDecoder extends Decoder {
|
|||
if (this.count === 0) {
|
||||
this.count = this.readUint53()
|
||||
this.lastValue = !this.lastValue
|
||||
if (this.count === 0)
|
||||
throw new RangeError("Zero-length runs are not allowed")
|
||||
if (this.count === 0) throw new RangeError('Zero-length runs are not allowed')
|
||||
}
|
||||
if (this.count < numSkip) {
|
||||
numSkip -= this.count
|
||||
|
@ -1306,16 +1204,6 @@ class BooleanDecoder extends Decoder {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
stringToUtf8,
|
||||
utf8ToString,
|
||||
hexStringToBytes,
|
||||
bytesToHexString,
|
||||
Encoder,
|
||||
Decoder,
|
||||
RLEEncoder,
|
||||
RLEDecoder,
|
||||
DeltaEncoder,
|
||||
DeltaDecoder,
|
||||
BooleanEncoder,
|
||||
BooleanDecoder,
|
||||
stringToUtf8, utf8ToString, hexStringToBytes, bytesToHexString,
|
||||
Encoder, Decoder, RLEEncoder, RLEDecoder, DeltaEncoder, DeltaDecoder, BooleanEncoder, BooleanDecoder
|
||||
}
|
1446
automerge-wasm/test/test.ts
Normal file
1446
automerge-wasm/test/test.ts
Normal file
File diff suppressed because it is too large
Load diff
|
@ -11,9 +11,7 @@
|
|||
"paths": { "dev": ["*"]},
|
||||
"rootDir": "",
|
||||
"target": "es2016",
|
||||
"types": ["mocha", "node"],
|
||||
"typeRoots": ["./index.d.ts"]
|
||||
"typeRoots": ["./dev/index.d.ts"]
|
||||
},
|
||||
"include": ["test/**/*.ts"],
|
||||
"exclude": ["dist/**/*", "examples/**/*"]
|
||||
"exclude": ["dist/**/*"]
|
||||
}
|
43
automerge/Cargo.toml
Normal file
43
automerge/Cargo.toml
Normal file
|
@ -0,0 +1,43 @@
|
|||
[package]
|
||||
name = "automerge"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
optree-visualisation = ["dot"]
|
||||
wasm = ["js-sys", "wasm-bindgen"]
|
||||
storage-v2 = []
|
||||
|
||||
[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 }
|
||||
js-sys = { version = "^0.3", optional = true }
|
||||
wasm-bindgen = { version = "^0.2", 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" }
|
||||
decorum = "0.3.1"
|
|
@ -2,7 +2,7 @@ use automerge::transaction::CommitOptions;
|
|||
use automerge::transaction::Transactable;
|
||||
use automerge::AutomergeError;
|
||||
use automerge::ObjType;
|
||||
use automerge::{Automerge, ReadDoc, ROOT};
|
||||
use automerge::{Automerge, ROOT};
|
||||
|
||||
// Based on https://automerge.github.io/docs/quickstart
|
||||
fn main() {
|
||||
|
@ -11,13 +11,13 @@ fn main() {
|
|||
.transact_with::<_, _, AutomergeError, _>(
|
||||
|_| CommitOptions::default().with_message("Add card".to_owned()),
|
||||
|tx| {
|
||||
let cards = tx.put_object(ROOT, "cards", ObjType::List).unwrap();
|
||||
let cards = tx.set_object(ROOT, "cards", ObjType::List).unwrap();
|
||||
let card1 = tx.insert_object(&cards, 0, ObjType::Map)?;
|
||||
tx.put(&card1, "title", "Rewrite everything in Clojure")?;
|
||||
tx.put(&card1, "done", false)?;
|
||||
tx.set(&card1, "title", "Rewrite everything in Clojure")?;
|
||||
tx.set(&card1, "done", false)?;
|
||||
let card2 = tx.insert_object(&cards, 0, ObjType::Map)?;
|
||||
tx.put(&card2, "title", "Rewrite everything in Haskell")?;
|
||||
tx.put(&card2, "done", false)?;
|
||||
tx.set(&card2, "title", "Rewrite everything in Haskell")?;
|
||||
tx.set(&card2, "done", false)?;
|
||||
Ok((cards, card1))
|
||||
},
|
||||
)
|
||||
|
@ -33,7 +33,7 @@ fn main() {
|
|||
doc1.transact_with::<_, _, AutomergeError, _>(
|
||||
|_| CommitOptions::default().with_message("Mark card as done".to_owned()),
|
||||
|tx| {
|
||||
tx.put(&card1, "done", true)?;
|
||||
tx.set(&card1, "done", true)?;
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
|
@ -42,7 +42,7 @@ fn main() {
|
|||
doc2.transact_with::<_, _, AutomergeError, _>(
|
||||
|_| CommitOptions::default().with_message("Delete card".to_owned()),
|
||||
|tx| {
|
||||
tx.delete(&cards, 0)?;
|
||||
tx.del(&cards, 0)?;
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
|
@ -50,7 +50,7 @@ fn main() {
|
|||
|
||||
doc1.merge(&mut doc2).unwrap();
|
||||
|
||||
for change in doc1.get_changes(&[]).unwrap() {
|
||||
for change in doc1.get_changes(&[]) {
|
||||
let length = doc1.length_at(&cards, &[change.hash()]);
|
||||
println!("{} {}", change.message().unwrap(), length);
|
||||
}
|
446
automerge/src/autocommit.rs
Normal file
446
automerge/src/autocommit.rs
Normal file
|
@ -0,0 +1,446 @@
|
|||
use crate::exid::ExId;
|
||||
use crate::transaction::{CommitOptions, Transactable};
|
||||
use crate::{
|
||||
transaction::TransactionInner, ActorId, Automerge, AutomergeError,
|
||||
Change, ChangeHash, Prop, Value,
|
||||
};
|
||||
#[cfg(not(feature = "storage-v2"))]
|
||||
use crate::change::export_change;
|
||||
use crate::{sync, Keys, KeysAt, ObjType, ScalarValue};
|
||||
|
||||
/// An automerge document that automatically manages transactions.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AutoCommit {
|
||||
doc: Automerge,
|
||||
transaction: Option<TransactionInner>,
|
||||
}
|
||||
|
||||
impl Default for AutoCommit {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AutoCommit {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
doc: Automerge::new(),
|
||||
transaction: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the inner document.
|
||||
#[doc(hidden)]
|
||||
pub fn document(&mut self) -> &Automerge {
|
||||
self.ensure_transaction_closed();
|
||||
&self.doc
|
||||
}
|
||||
|
||||
pub fn with_actor(mut self, actor: ActorId) -> Self {
|
||||
self.ensure_transaction_closed();
|
||||
self.doc.set_actor(actor);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_actor(&mut self, actor: ActorId) -> &mut Self {
|
||||
self.ensure_transaction_closed();
|
||||
self.doc.set_actor(actor);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_actor(&self) -> &ActorId {
|
||||
self.doc.get_actor()
|
||||
}
|
||||
|
||||
fn ensure_transaction_open(&mut self) {
|
||||
if self.transaction.is_none() {
|
||||
let actor = self.doc.get_actor_index();
|
||||
|
||||
let seq = self.doc.states.entry(actor).or_default().len() as u64 + 1;
|
||||
let mut deps = self.doc.get_heads();
|
||||
if seq > 1 {
|
||||
let last_hash = self.get_hash(actor, seq - 1).unwrap();
|
||||
if !deps.contains(&last_hash) {
|
||||
deps.push(last_hash);
|
||||
}
|
||||
}
|
||||
|
||||
self.transaction = Some(TransactionInner {
|
||||
actor,
|
||||
seq,
|
||||
start_op: self.doc.max_op + 1,
|
||||
time: 0,
|
||||
message: None,
|
||||
extra_bytes: Default::default(),
|
||||
hash: None,
|
||||
operations: vec![],
|
||||
deps,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn get_hash(&mut self, actor: usize, seq: u64) -> Result<ChangeHash, AutomergeError> {
|
||||
self.doc
|
||||
.states
|
||||
.get(&actor)
|
||||
.and_then(|v| v.get(seq as usize - 1))
|
||||
.and_then(|&i| self.doc.history.get(i))
|
||||
.map(|c| c.hash())
|
||||
.ok_or(AutomergeError::InvalidSeq(seq))
|
||||
}
|
||||
|
||||
fn update_history(&mut self, change: Change) -> usize {
|
||||
self.doc.max_op = std::cmp::max(self.doc.max_op, change.start_op() + change.len() as u64 - 1);
|
||||
|
||||
self.update_deps(&change);
|
||||
|
||||
let history_index = self.doc.history.len();
|
||||
|
||||
self.doc
|
||||
.states
|
||||
.entry(self.doc.ops.m.actors.cache(change.actor_id().clone()))
|
||||
.or_default()
|
||||
.push(history_index);
|
||||
|
||||
self.doc.history_index.insert(change.hash(), history_index);
|
||||
self.doc.history.push(change);
|
||||
|
||||
history_index
|
||||
}
|
||||
|
||||
fn update_deps(&mut self, change: &Change) {
|
||||
for d in change.deps() {
|
||||
self.doc.deps.remove(d);
|
||||
}
|
||||
self.doc.deps.insert(change.hash());
|
||||
}
|
||||
|
||||
pub fn fork(&mut self) -> Self {
|
||||
self.ensure_transaction_closed();
|
||||
Self {
|
||||
doc: self.doc.fork(),
|
||||
transaction: self.transaction.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_transaction_closed(&mut self) {
|
||||
if let Some(tx) = self.transaction.take() {
|
||||
self.update_history(tx.export(
|
||||
&self.doc.ops.m.actors,
|
||||
&self.doc.ops.m.props,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(data: &[u8]) -> Result<Self, AutomergeError> {
|
||||
let doc = Automerge::load(data)?;
|
||||
Ok(Self {
|
||||
doc,
|
||||
transaction: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_incremental(&mut self, data: &[u8]) -> Result<usize, AutomergeError> {
|
||||
self.ensure_transaction_closed();
|
||||
self.doc.load_incremental(data)
|
||||
}
|
||||
|
||||
pub fn apply_changes(&mut self, changes: Vec<Change>) -> Result<(), AutomergeError> {
|
||||
self.ensure_transaction_closed();
|
||||
self.doc.apply_changes(changes)
|
||||
}
|
||||
|
||||
/// Takes all the changes in `other` which are not in `self` and applies them
|
||||
pub fn merge(&mut self, other: &mut Self) -> Result<Vec<ChangeHash>, AutomergeError> {
|
||||
self.ensure_transaction_closed();
|
||||
other.ensure_transaction_closed();
|
||||
self.doc.merge(&mut other.doc)
|
||||
}
|
||||
|
||||
pub fn save(&mut self) -> Vec<u8> {
|
||||
self.ensure_transaction_closed();
|
||||
self.doc.save()
|
||||
}
|
||||
|
||||
// should this return an empty vec instead of None?
|
||||
pub fn save_incremental(&mut self) -> Vec<u8> {
|
||||
self.ensure_transaction_closed();
|
||||
self.doc.save_incremental()
|
||||
}
|
||||
|
||||
pub fn get_missing_deps(&mut self, heads: &[ChangeHash]) -> Vec<ChangeHash> {
|
||||
self.ensure_transaction_closed();
|
||||
self.doc.get_missing_deps(heads)
|
||||
}
|
||||
|
||||
pub fn get_last_local_change(&mut self) -> Option<&Change> {
|
||||
self.ensure_transaction_closed();
|
||||
self.doc.get_last_local_change()
|
||||
}
|
||||
|
||||
pub fn get_changes(&mut self, have_deps: &[ChangeHash]) -> Vec<&Change> {
|
||||
self.ensure_transaction_closed();
|
||||
self.doc.get_changes(have_deps)
|
||||
}
|
||||
|
||||
pub fn get_change_by_hash(&mut self, hash: &ChangeHash) -> Option<&Change> {
|
||||
self.ensure_transaction_closed();
|
||||
self.doc.get_change_by_hash(hash)
|
||||
}
|
||||
|
||||
pub fn get_changes_added<'a>(&mut self, other: &'a mut Self) -> Vec<&'a Change> {
|
||||
self.ensure_transaction_closed();
|
||||
other.ensure_transaction_closed();
|
||||
self.doc.get_changes_added(&other.doc)
|
||||
}
|
||||
|
||||
pub fn import(&self, s: &str) -> Result<ExId, AutomergeError> {
|
||||
self.doc.import(s)
|
||||
}
|
||||
|
||||
pub fn dump(&self) {
|
||||
self.doc.dump()
|
||||
}
|
||||
|
||||
pub fn generate_sync_message(&mut self, sync_state: &mut sync::State) -> Option<sync::Message> {
|
||||
self.ensure_transaction_closed();
|
||||
self.doc.generate_sync_message(sync_state)
|
||||
}
|
||||
|
||||
pub fn receive_sync_message(
|
||||
&mut self,
|
||||
sync_state: &mut sync::State,
|
||||
message: sync::Message,
|
||||
) -> Result<(), AutomergeError> {
|
||||
self.ensure_transaction_closed();
|
||||
self.doc.receive_sync_message(sync_state, message)
|
||||
}
|
||||
|
||||
#[cfg(feature = "optree-visualisation")]
|
||||
pub fn visualise_optree(&self) -> String {
|
||||
self.doc.visualise_optree()
|
||||
}
|
||||
|
||||
/// Get the current heads of the document.
|
||||
///
|
||||
/// This closes the transaction first, if one is in progress.
|
||||
pub fn get_heads(&mut self) -> Vec<ChangeHash> {
|
||||
self.ensure_transaction_closed();
|
||||
self.doc.get_heads()
|
||||
}
|
||||
|
||||
pub fn commit(&mut self) -> ChangeHash {
|
||||
// ensure that even no changes triggers a change
|
||||
self.ensure_transaction_open();
|
||||
let tx = self.transaction.take().unwrap();
|
||||
tx.commit(&mut self.doc, None, None)
|
||||
}
|
||||
|
||||
/// Commit the current operations with some options.
|
||||
///
|
||||
/// ```
|
||||
/// # use automerge::transaction::CommitOptions;
|
||||
/// # use automerge::transaction::Transactable;
|
||||
/// # use automerge::ROOT;
|
||||
/// # use automerge::AutoCommit;
|
||||
/// # use automerge::ObjType;
|
||||
/// # use std::time::SystemTime;
|
||||
/// let mut doc = AutoCommit::new();
|
||||
/// doc.set_object(&ROOT, "todos", ObjType::List).unwrap();
|
||||
/// let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as
|
||||
/// i64;
|
||||
/// doc.commit_with(CommitOptions::default().with_message("Create todos list").with_time(now));
|
||||
/// ```
|
||||
pub fn commit_with(&mut self, options: CommitOptions) -> ChangeHash {
|
||||
self.ensure_transaction_open();
|
||||
let tx = self.transaction.take().unwrap();
|
||||
tx.commit(&mut self.doc, options.message, options.time)
|
||||
}
|
||||
|
||||
pub fn rollback(&mut self) -> usize {
|
||||
self.transaction
|
||||
.take()
|
||||
.map(|tx| tx.rollback(&mut self.doc))
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Transactable for AutoCommit {
|
||||
fn pending_ops(&self) -> usize {
|
||||
self.transaction
|
||||
.as_ref()
|
||||
.map(|t| t.pending_ops())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
// KeysAt::()
|
||||
// LenAt::()
|
||||
// PropAt::()
|
||||
// NthAt::()
|
||||
|
||||
fn keys<O: AsRef<ExId>>(&self, obj: O) -> Keys {
|
||||
self.doc.keys(obj)
|
||||
}
|
||||
|
||||
fn keys_at<O: AsRef<ExId>>(&self, obj: O, heads: &[ChangeHash]) -> KeysAt {
|
||||
self.doc.keys_at(obj, heads)
|
||||
}
|
||||
|
||||
fn length<O: AsRef<ExId>>(&self, obj: O) -> usize {
|
||||
self.doc.length(obj)
|
||||
}
|
||||
|
||||
fn length_at<O: AsRef<ExId>>(&self, obj: O, heads: &[ChangeHash]) -> usize {
|
||||
self.doc.length_at(obj, heads)
|
||||
}
|
||||
|
||||
fn object_type<O: AsRef<ExId>>(&self, obj: O) -> Option<ObjType> {
|
||||
self.doc.object_type(obj)
|
||||
}
|
||||
|
||||
// set(obj, prop, value) - value can be scalar or objtype
|
||||
// del(obj, prop)
|
||||
// inc(obj, prop, value)
|
||||
// insert(obj, index, value)
|
||||
|
||||
/// Set the value of property `P` to value `V` in object `obj`.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The opid of the operation which was created, or None if this operation doesn't change the
|
||||
/// document or create a new object.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This will return an error if
|
||||
/// - The object does not exist
|
||||
/// - The key is the wrong type for the object
|
||||
/// - The key does not exist in the object
|
||||
fn set<O: AsRef<ExId>, P: Into<Prop>, V: Into<ScalarValue>>(
|
||||
&mut self,
|
||||
obj: O,
|
||||
prop: P,
|
||||
value: V,
|
||||
) -> Result<(), AutomergeError> {
|
||||
self.ensure_transaction_open();
|
||||
let tx = self.transaction.as_mut().unwrap();
|
||||
tx.set(&mut self.doc, obj.as_ref(), prop, value)
|
||||
}
|
||||
|
||||
fn set_object<O: AsRef<ExId>, P: Into<Prop>>(
|
||||
&mut self,
|
||||
obj: O,
|
||||
prop: P,
|
||||
value: ObjType,
|
||||
) -> Result<ExId, AutomergeError> {
|
||||
self.ensure_transaction_open();
|
||||
let tx = self.transaction.as_mut().unwrap();
|
||||
tx.set_object(&mut self.doc, obj.as_ref(), prop, value)
|
||||
}
|
||||
|
||||
fn insert<O: AsRef<ExId>, V: Into<ScalarValue>>(
|
||||
&mut self,
|
||||
obj: O,
|
||||
index: usize,
|
||||
value: V,
|
||||
) -> Result<(), AutomergeError> {
|
||||
self.ensure_transaction_open();
|
||||
let tx = self.transaction.as_mut().unwrap();
|
||||
tx.insert(&mut self.doc, obj.as_ref(), index, value)
|
||||
}
|
||||
|
||||
fn insert_object(
|
||||
&mut self,
|
||||
obj: &ExId,
|
||||
index: usize,
|
||||
value: ObjType,
|
||||
) -> Result<ExId, AutomergeError> {
|
||||
self.ensure_transaction_open();
|
||||
let tx = self.transaction.as_mut().unwrap();
|
||||
tx.insert_object(&mut self.doc, obj, index, value)
|
||||
}
|
||||
|
||||
fn inc<O: AsRef<ExId>, P: Into<Prop>>(
|
||||
&mut self,
|
||||
obj: O,
|
||||
prop: P,
|
||||
value: i64,
|
||||
) -> Result<(), AutomergeError> {
|
||||
self.ensure_transaction_open();
|
||||
let tx = self.transaction.as_mut().unwrap();
|
||||
tx.inc(&mut self.doc, obj.as_ref(), prop, value)
|
||||
}
|
||||
|
||||
fn del<O: AsRef<ExId>, P: Into<Prop>>(
|
||||
&mut self,
|
||||
obj: O,
|
||||
prop: P,
|
||||
) -> Result<(), AutomergeError> {
|
||||
self.ensure_transaction_open();
|
||||
let tx = self.transaction.as_mut().unwrap();
|
||||
tx.del(&mut self.doc, obj.as_ref(), prop)
|
||||
}
|
||||
|
||||
/// Splice new elements into the given sequence. Returns a vector of the OpIds used to insert
|
||||
/// the new elements
|
||||
fn splice<O: AsRef<ExId>, V: IntoIterator<Item = ScalarValue>>(
|
||||
&mut self,
|
||||
obj: O,
|
||||
pos: usize,
|
||||
del: usize,
|
||||
vals: V,
|
||||
) -> Result<(), AutomergeError> {
|
||||
self.ensure_transaction_open();
|
||||
let tx = self.transaction.as_mut().unwrap();
|
||||
tx.splice(&mut self.doc, obj.as_ref(), pos, del, vals)
|
||||
}
|
||||
|
||||
fn text<O: AsRef<ExId>>(&self, obj: O) -> Result<String, AutomergeError> {
|
||||
self.doc.text(obj)
|
||||
}
|
||||
|
||||
fn text_at<O: AsRef<ExId>>(
|
||||
&self,
|
||||
obj: O,
|
||||
heads: &[ChangeHash],
|
||||
) -> Result<String, AutomergeError> {
|
||||
self.doc.text_at(obj, heads)
|
||||
}
|
||||
|
||||
// TODO - I need to return these OpId's here **only** to get
|
||||
// the legacy conflicts format of { [opid]: value }
|
||||
// Something better?
|
||||
fn value<O: AsRef<ExId>, P: Into<Prop>>(
|
||||
&self,
|
||||
obj: O,
|
||||
prop: P,
|
||||
) -> Result<Option<(Value, ExId)>, AutomergeError> {
|
||||
self.doc.value(obj, prop)
|
||||
}
|
||||
|
||||
fn value_at<O: AsRef<ExId>, P: Into<Prop>>(
|
||||
&self,
|
||||
obj: O,
|
||||
prop: P,
|
||||
heads: &[ChangeHash],
|
||||
) -> Result<Option<(Value, ExId)>, AutomergeError> {
|
||||
self.doc.value_at(obj, prop, heads)
|
||||
}
|
||||
|
||||
fn values<O: AsRef<ExId>, P: Into<Prop>>(
|
||||
&self,
|
||||
obj: O,
|
||||
prop: P,
|
||||
) -> Result<Vec<(Value, ExId)>, AutomergeError> {
|
||||
self.doc.values(obj, prop)
|
||||
}
|
||||
|
||||
fn values_at<O: AsRef<ExId>, P: Into<Prop>>(
|
||||
&self,
|
||||
obj: O,
|
||||
prop: P,
|
||||
heads: &[ChangeHash],
|
||||
) -> Result<Vec<(Value, ExId)>, AutomergeError> {
|
||||
self.doc.values_at(obj, prop, heads)
|
||||
}
|
||||
}
|
1402
automerge/src/automerge.rs
Normal file
1402
automerge/src/automerge.rs
Normal file
File diff suppressed because it is too large
Load diff
89
automerge/src/autoserde.rs
Normal file
89
automerge/src/autoserde.rs
Normal file
|
@ -0,0 +1,89 @@
|
|||
use serde::ser::{SerializeSeq, SerializeMap};
|
||||
|
||||
use crate::{ObjId, Automerge, Value, ObjType};
|
||||
|
||||
pub struct AutoSerde<'a>(&'a Automerge);
|
||||
|
||||
impl<'a> From<&'a Automerge> for AutoSerde<'a> {
|
||||
fn from(a: &'a Automerge) -> Self {
|
||||
AutoSerde(a)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> serde::Serialize for AutoSerde<'a> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer {
|
||||
AutoSerdeMap{doc: self.0, obj: ObjId::Root}.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
struct AutoSerdeMap<'a>{doc: &'a Automerge, obj: ObjId}
|
||||
|
||||
impl<'a> serde::Serialize for AutoSerdeMap<'a> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer {
|
||||
let mut map_ser = serializer.serialize_map(Some(self.doc.length(&ObjId::Root)))?;
|
||||
for key in self.doc.keys(&self.obj) {
|
||||
// SAFETY: This only errors if the object ID is unknown, but we construct this type
|
||||
// with a known real object ID
|
||||
let (val, obj) = self.doc.value(&self.obj, &key).unwrap().unwrap();
|
||||
let serdeval = AutoSerdeVal{
|
||||
doc: &self.doc,
|
||||
val,
|
||||
obj,
|
||||
};
|
||||
map_ser.serialize_entry(&key, &serdeval)?;
|
||||
}
|
||||
map_ser.end()
|
||||
}
|
||||
}
|
||||
|
||||
struct AutoSerdeSeq<'a>{doc: &'a Automerge, obj: ObjId}
|
||||
|
||||
impl<'a> serde::Serialize for AutoSerdeSeq<'a> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer {
|
||||
let mut seq_ser = serializer.serialize_seq(None)?;
|
||||
for i in 0..self.doc.length(&self.obj) {
|
||||
// SAFETY: This only errors if the object ID is unknown, but we construct this type
|
||||
// with a known real object ID
|
||||
let (val, obj) = self.doc.value(&self.obj, i).unwrap().unwrap();
|
||||
let serdeval = AutoSerdeVal{
|
||||
doc: &self.doc,
|
||||
val,
|
||||
obj,
|
||||
};
|
||||
seq_ser.serialize_element(&serdeval)?;
|
||||
}
|
||||
seq_ser.end()
|
||||
}
|
||||
}
|
||||
|
||||
struct AutoSerdeVal<'a>{
|
||||
doc: &'a Automerge,
|
||||
val: Value,
|
||||
obj: ObjId,
|
||||
}
|
||||
|
||||
impl<'a> serde::Serialize for AutoSerdeVal<'a> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer {
|
||||
match &self.val {
|
||||
Value::Object(ObjType::Map | ObjType::Table) => {
|
||||
let map = AutoSerdeMap{doc: &self.doc, obj: self.obj.clone()};
|
||||
map.serialize(serializer)
|
||||
},
|
||||
Value::Object(ObjType::List | ObjType::Text) => {
|
||||
let seq = AutoSerdeSeq{doc: &self.doc, obj: self.obj.clone()};
|
||||
seq.serialize(serializer)
|
||||
},
|
||||
Value::Scalar(v) => {
|
||||
v.serialize(serializer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1011
automerge/src/change.rs
Normal file
1011
automerge/src/change.rs
Normal file
File diff suppressed because it is too large
Load diff
179
automerge/src/change_v2.rs
Normal file
179
automerge/src/change_v2.rs
Normal file
|
@ -0,0 +1,179 @@
|
|||
use crate::{
|
||||
columnar_2::{
|
||||
rowblock::{
|
||||
change_op_columns::{ChangeOp, ChangeOpsColumns},
|
||||
RowBlock,
|
||||
},
|
||||
storage::{Change as StoredChange, Chunk, ChunkType},
|
||||
},
|
||||
types::{ActorId, ChangeHash},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Change {
|
||||
stored: StoredChange<'static>,
|
||||
hash: ChangeHash,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl Change {
|
||||
pub(crate) fn new(stored: StoredChange<'static>, hash: ChangeHash, len: usize) -> Self {
|
||||
Self{
|
||||
stored,
|
||||
hash,
|
||||
len,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn actor_id(&self) -> &ActorId {
|
||||
&self.stored.actor
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
|
||||
pub fn max_op(&self) -> u64 {
|
||||
self.stored.start_op + (self.len as u64) - 1
|
||||
}
|
||||
|
||||
pub fn start_op(&self) -> u64 {
|
||||
self.stored.start_op
|
||||
}
|
||||
|
||||
pub fn message(&self) -> Option<&String> {
|
||||
self.stored.message.as_ref()
|
||||
}
|
||||
|
||||
pub fn deps(&self) -> &[ChangeHash] {
|
||||
&self.stored.dependencies
|
||||
}
|
||||
|
||||
pub fn hash(&self) -> ChangeHash {
|
||||
self.hash
|
||||
}
|
||||
|
||||
pub fn seq(&self) -> u64 {
|
||||
self.stored.seq
|
||||
}
|
||||
|
||||
pub fn timestamp(&self) -> i64 {
|
||||
self.stored.timestamp
|
||||
}
|
||||
|
||||
pub fn compress(&mut self) {}
|
||||
|
||||
pub fn raw_bytes(&self) -> Vec<u8> {
|
||||
let vec = self.stored.write();
|
||||
let chunk = Chunk::new_change(&vec);
|
||||
chunk.write()
|
||||
}
|
||||
|
||||
pub(crate) fn iter_ops<'a>(&'a self) -> impl Iterator<Item= ChangeOp<'a>> {
|
||||
let rb = RowBlock::new(self.stored.ops_meta.iter(), self.stored.ops_data.clone()).unwrap();
|
||||
let crb: RowBlock<ChangeOpsColumns> = rb.into_change_ops().unwrap();
|
||||
let unwrapped = crb.into_iter().map(|r| r.unwrap().into_owned()).collect::<Vec<_>>();
|
||||
return OperationIterator{
|
||||
inner: unwrapped.into_iter(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extra_bytes(&self) -> &[u8] {
|
||||
self.stored.extra_bytes.as_ref()
|
||||
}
|
||||
|
||||
// TODO replace all uses of this with TryFrom<&[u8]>
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, LoadError> {
|
||||
Self::try_from(&bytes[..])
|
||||
}
|
||||
}
|
||||
|
||||
struct OperationIterator<'a> {
|
||||
inner: std::vec::IntoIter<ChangeOp<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for OperationIterator<'a> {
|
||||
type Item = ChangeOp<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<StoredChange<'static>> for Change {
|
||||
fn as_ref(&self) -> &StoredChange<'static> {
|
||||
&self.stored
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum LoadError {
|
||||
#[error("unable to parse change: {0}")]
|
||||
Parse(Box<dyn std::error::Error>),
|
||||
#[error("leftover data after parsing")]
|
||||
LeftoverData,
|
||||
#[error("wrong chunk type")]
|
||||
WrongChunkType,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a [u8]> for Change {
|
||||
type Error = LoadError;
|
||||
|
||||
fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
|
||||
use crate::columnar_2::rowblock::change_op_columns::ReadChangeOpError;
|
||||
let (remaining, chunk) = Chunk::parse(value).map_err(|e| LoadError::Parse(Box::new(e)))?;
|
||||
if remaining.len() > 0 {
|
||||
return Err(LoadError::LeftoverData);
|
||||
}
|
||||
match chunk.typ() {
|
||||
ChunkType::Change => {
|
||||
let chunkbytes = chunk.data();
|
||||
let (_, c) = StoredChange::parse(chunkbytes.as_ref())
|
||||
.map_err(|e| LoadError::Parse(Box::new(e)))?;
|
||||
let rb = RowBlock::new(c.ops_meta.iter(), c.ops_data.clone()).unwrap();
|
||||
let crb: RowBlock<ChangeOpsColumns> = rb.into_change_ops().unwrap();
|
||||
let mut iter = crb.into_iter();
|
||||
let ops_len = iter
|
||||
.try_fold::<_, _, Result<_, ReadChangeOpError>>(0, |acc, op| {
|
||||
op?;
|
||||
Ok(acc + 1)
|
||||
})
|
||||
.map_err(|e| LoadError::Parse(Box::new(e)))?;
|
||||
Ok(Self {
|
||||
stored: c.into_owned(),
|
||||
hash: chunk.hash(),
|
||||
len: ops_len,
|
||||
})
|
||||
}
|
||||
_ => Err(LoadError::WrongChunkType),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<StoredChange<'a>> for Change {
|
||||
type Error = LoadError;
|
||||
|
||||
fn try_from(c: StoredChange) -> Result<Self, Self::Error> {
|
||||
use crate::columnar_2::rowblock::change_op_columns::ReadChangeOpError;
|
||||
let rb = RowBlock::new(c.ops_meta.iter(), c.ops_data.clone()).unwrap();
|
||||
let crb: RowBlock<ChangeOpsColumns> = rb.into_change_ops().unwrap();
|
||||
let mut iter = crb.into_iter();
|
||||
let ops_len = iter
|
||||
.try_fold::<_, _, Result<_, ReadChangeOpError>>(0, |acc, op| {
|
||||
op?;
|
||||
Ok(acc + 1)
|
||||
})
|
||||
.map_err(|e| LoadError::Parse(Box::new(e)))?;
|
||||
let chunkbytes = c.write();
|
||||
let chunk = Chunk::new_change(chunkbytes.as_ref());
|
||||
Ok(Self {
|
||||
stored: c.into_owned(),
|
||||
hash: chunk.hash(),
|
||||
len: ops_len,
|
||||
})
|
||||
}
|
||||
}
|
52
automerge/src/clock.rs
Normal file
52
automerge/src/clock.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use crate::types::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)));
|
||||
}
|
||||
}
|
1314
automerge/src/columnar.rs
Normal file
1314
automerge/src/columnar.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,3 @@
|
|||
/// An implementation of column specifications as specified in [1]
|
||||
///
|
||||
/// [1]: https://alexjg.github.io/automerge-storage-docs/#column-specifications
|
||||
#[derive(Eq, PartialEq, Clone, Copy)]
|
||||
pub(crate) struct ColumnSpec(u32);
|
||||
|
||||
|
@ -28,14 +25,6 @@ impl ColumnSpec {
|
|||
self.0 & 0b00001000 > 0
|
||||
}
|
||||
|
||||
pub(crate) fn deflated(&self) -> Self {
|
||||
Self::new(self.id(), self.col_type(), true)
|
||||
}
|
||||
|
||||
pub(crate) fn inflated(&self) -> Self {
|
||||
Self::new(self.id(), self.col_type(), false)
|
||||
}
|
||||
|
||||
pub(crate) fn normalize(&self) -> Normalized {
|
||||
Normalized(self.0 & 0b11110111)
|
||||
}
|
||||
|
@ -60,14 +49,14 @@ impl std::fmt::Debug for ColumnSpec {
|
|||
pub(crate) struct ColumnId(u32);
|
||||
|
||||
impl ColumnId {
|
||||
pub(crate) const fn new(raw: u32) -> Self {
|
||||
pub const fn new(raw: u32) -> Self {
|
||||
ColumnId(raw)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for ColumnId {
|
||||
fn from(raw: u32) -> Self {
|
||||
Self(raw)
|
||||
Self(raw)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,9 +66,6 @@ impl std::fmt::Debug for ColumnId {
|
|||
}
|
||||
}
|
||||
|
||||
/// The differente possible column types, as specified in [1]
|
||||
///
|
||||
/// [1]: https://alexjg.github.io/automerge-storage-docs/#column-specifications
|
||||
#[derive(Eq, PartialEq, Clone, Copy, Debug)]
|
||||
pub(crate) enum ColumnType {
|
||||
Group,
|
162
automerge/src/columnar_2/load.rs
Normal file
162
automerge/src/columnar_2/load.rs
Normal file
|
@ -0,0 +1,162 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use tracing::instrument;
|
||||
|
||||
use super::{rowblock, storage};
|
||||
use crate::{op_set::OpSet, Change};
|
||||
|
||||
mod change_collector;
|
||||
mod loading_document;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("unable to parse chunk: {0}")]
|
||||
Parse(Box<dyn std::error::Error>),
|
||||
#[error("invalid change columns: {0}")]
|
||||
InvalidChangeColumns(Box<dyn std::error::Error>),
|
||||
#[error("invalid ops columns: {0}")]
|
||||
InvalidOpsColumns(Box<dyn std::error::Error>),
|
||||
#[error("a chunk contained leftover data")]
|
||||
LeftoverData,
|
||||
#[error("error inflating document chunk ops: {0}")]
|
||||
InflateDocument(Box<dyn std::error::Error>),
|
||||
#[error("bad checksum")]
|
||||
BadChecksum,
|
||||
}
|
||||
|
||||
/// The result of `load_opset`. See the documentation for [`load_opset`] for details on why this is
|
||||
/// necessary
|
||||
pub(crate) enum LoadOpset {
|
||||
/// The data was a "document" chunk so we loaded an op_set
|
||||
Document {
|
||||
/// The opset we loaded
|
||||
op_set: OpSet,
|
||||
/// The changes
|
||||
history: Vec<Change>,
|
||||
/// An index from history index to hash
|
||||
history_index: HashMap<crate::types::ChangeHash, usize>,
|
||||
/// An index from actor index to seq to change index
|
||||
actor_to_history: HashMap<usize, Vec<usize>>,
|
||||
},
|
||||
/// The data was a change chunk so we just loaded the change
|
||||
Change(Change),
|
||||
}
|
||||
|
||||
/// The binary storage format defines several different "chunk types". When we're loading a
|
||||
/// document for the first time we wish to distinguish between "document" chunk types, and all the
|
||||
/// others. The reason for this is that the "document" chunk type contains operations encoded in a
|
||||
/// particular order which we can take advantage of to quickly load an OpSet. For all other chunk
|
||||
/// types we must proceed as usual by loading changes in order.
|
||||
///
|
||||
/// The tuple returned by this function contains as it's first component any data which was not
|
||||
/// consumed (i.e. data which could be more chunks) and as it's second component the [`LoadOpset`]
|
||||
/// which represents the two possible alternatives described above.
|
||||
#[instrument(level = "trace", skip(data))]
|
||||
pub(crate) fn load_opset<'a>(data: &'a [u8]) -> Result<(&'a [u8], LoadOpset), Error> {
|
||||
let (remaining, chunk) = storage::Chunk::parse(data).map_err(|e| Error::Parse(Box::new(e)))?;
|
||||
if !chunk.checksum_valid() {
|
||||
return Err(Error::BadChecksum);
|
||||
}
|
||||
match chunk.typ() {
|
||||
storage::ChunkType::Document => {
|
||||
tracing::trace!("loading document chunk");
|
||||
let data = chunk.data();
|
||||
let (inner_remaining, doc) =
|
||||
storage::Document::parse(&data).map_err(|e| Error::Parse(Box::new(e)))?;
|
||||
if !inner_remaining.is_empty() {
|
||||
tracing::error!(
|
||||
remaining = inner_remaining.len(),
|
||||
"leftover data when parsing document chunk"
|
||||
);
|
||||
return Err(Error::LeftoverData);
|
||||
}
|
||||
let change_rowblock =
|
||||
rowblock::RowBlock::new(doc.change_metadata.iter(), doc.change_bytes)
|
||||
.map_err(|e| Error::InvalidChangeColumns(Box::new(e)))?
|
||||
.into_doc_change()
|
||||
.map_err(|e| Error::InvalidChangeColumns(Box::new(e)))?;
|
||||
|
||||
let ops_rowblock = rowblock::RowBlock::new(doc.op_metadata.iter(), doc.op_bytes)
|
||||
.map_err(|e| Error::InvalidOpsColumns(Box::new(e)))?
|
||||
.into_doc_ops()
|
||||
.map_err(|e| Error::InvalidOpsColumns(Box::new(e)))?;
|
||||
|
||||
let loading_document::Loaded {
|
||||
op_set,
|
||||
history,
|
||||
history_index,
|
||||
actor_to_history,
|
||||
..
|
||||
} = loading_document::load(
|
||||
doc.actors,
|
||||
doc.heads.into_iter().collect(),
|
||||
change_rowblock.into_iter(),
|
||||
ops_rowblock.into_iter(),
|
||||
)
|
||||
.map_err(|e| Error::InflateDocument(Box::new(e)))?;
|
||||
|
||||
// TODO: remove this unwrap because we already materialized all the ops
|
||||
let history = history.into_iter().map(|h| h.try_into().unwrap()).collect();
|
||||
|
||||
Ok((
|
||||
remaining,
|
||||
LoadOpset::Document {
|
||||
op_set,
|
||||
history,
|
||||
history_index,
|
||||
actor_to_history,
|
||||
},
|
||||
))
|
||||
}
|
||||
storage::ChunkType::Change => {
|
||||
tracing::trace!("loading change chunk");
|
||||
let data = chunk.data();
|
||||
let (inner_remaining, change_chunk) =
|
||||
storage::Change::parse(&data).map_err(|e| Error::Parse(Box::new(e)))?;
|
||||
if !inner_remaining.is_empty() {
|
||||
tracing::error!(
|
||||
remaining = inner_remaining.len(),
|
||||
"leftover data when parsing document chunk"
|
||||
);
|
||||
return Err(Error::LeftoverData);
|
||||
}
|
||||
let change_rowblock =
|
||||
rowblock::RowBlock::new(change_chunk.ops_meta.iter(), change_chunk.ops_data.clone())
|
||||
.map_err(|e| Error::InvalidOpsColumns(Box::new(e)))?
|
||||
.into_change_ops()
|
||||
.map_err(|e| Error::InvalidOpsColumns(Box::new(e)))?;
|
||||
let len = (&change_rowblock).into_iter().try_fold(0, |acc, c| {
|
||||
c.map_err(|e| Error::InvalidChangeColumns(Box::new(e)))?;
|
||||
Ok(acc + 1)
|
||||
})?;
|
||||
Ok((
|
||||
remaining,
|
||||
LoadOpset::Change(Change::new(change_chunk.into_owned(), chunk.hash(), len)),
|
||||
))
|
||||
}
|
||||
storage::ChunkType::Compressed => panic!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load all the chunks in `data` returning a vector of changes. Note that this will throw an error
|
||||
/// if there is data left over.
|
||||
pub(crate) fn load(data: &[u8]) -> Result<Vec<Change>, Error> {
|
||||
let mut changes = Vec::new();
|
||||
let mut data = data;
|
||||
while data.len() > 0 {
|
||||
let (remaining, load_result) = load_opset(data)?;
|
||||
match load_result {
|
||||
LoadOpset::Change(c) => changes.push(c),
|
||||
LoadOpset::Document { history, .. } => {
|
||||
for stored_change in history {
|
||||
changes.push(
|
||||
Change::try_from(stored_change)
|
||||
.map_err(|e| Error::InvalidOpsColumns(Box::new(e)))?,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
data = remaining;
|
||||
}
|
||||
Ok(changes)
|
||||
}
|
259
automerge/src/columnar_2/load/change_collector.rs
Normal file
259
automerge/src/columnar_2/load/change_collector.rs
Normal file
|
@ -0,0 +1,259 @@
|
|||
use std::{borrow::Cow, collections::{BTreeSet, HashMap}};
|
||||
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::{
|
||||
indexed_cache::IndexedCache,
|
||||
columnar_2::{
|
||||
rowblock::{
|
||||
change_op_columns::{ChangeOp, ChangeOpsColumns},
|
||||
doc_change_columns::ChangeMetadata,
|
||||
Key as StoredKey, PrimVal,
|
||||
},
|
||||
storage::Change as StoredChange,
|
||||
},
|
||||
types::{ActorId, ChangeHash, ElemId, Key, Op, ObjId},
|
||||
OpType,
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum Error {
|
||||
#[error("a change referenced an actor index we couldn't find")]
|
||||
MissingActor,
|
||||
#[error("changes out of order")]
|
||||
ChangesOutOfOrder,
|
||||
#[error("missing change")]
|
||||
MissingChange,
|
||||
#[error("some ops were missing")]
|
||||
MissingOps,
|
||||
#[error("unable to read change metadata: {0}")]
|
||||
ReadChange(Box<dyn std::error::Error>),
|
||||
}
|
||||
|
||||
pub(crate) struct ChangeCollector<'a> {
|
||||
changes_by_actor: HashMap<usize, Vec<PartialChange<'a>>>,
|
||||
}
|
||||
|
||||
pub(crate) struct CollectedChanges<'a> {
|
||||
pub(crate) history: Vec<StoredChange<'a>>,
|
||||
pub(crate) history_index: HashMap<ChangeHash, usize>,
|
||||
pub(crate) actor_to_history: HashMap<usize, Vec<usize>>,
|
||||
pub(crate) heads: BTreeSet<ChangeHash>,
|
||||
}
|
||||
|
||||
impl<'a> ChangeCollector<'a> {
|
||||
pub(crate) fn new<E: std::error::Error + 'static, I>(
|
||||
changes: I,
|
||||
) -> Result<ChangeCollector<'a>, Error>
|
||||
where
|
||||
I: IntoIterator<Item = Result<ChangeMetadata<'a>, E>>,
|
||||
{
|
||||
let mut changes_by_actor: HashMap<usize, Vec<PartialChange<'_>>> = HashMap::new();
|
||||
for (index, change) in changes.into_iter().enumerate() {
|
||||
tracing::trace!(?change, "importing change metadata");
|
||||
let change = change.map_err(|e| Error::ReadChange(Box::new(e)))?;
|
||||
let actor_changes = changes_by_actor.entry(change.actor).or_default();
|
||||
if let Some(prev) = actor_changes.last() {
|
||||
if prev.max_op >= change.max_op {
|
||||
return Err(Error::ChangesOutOfOrder);
|
||||
}
|
||||
}
|
||||
actor_changes.push(PartialChange {
|
||||
index,
|
||||
deps: change.deps,
|
||||
actor: change.actor,
|
||||
seq: change.seq,
|
||||
timestamp: change.timestamp,
|
||||
max_op: change.max_op,
|
||||
message: change.message,
|
||||
extra_bytes: change.extra,
|
||||
ops: Vec::new(),
|
||||
})
|
||||
}
|
||||
let num_changes: usize = changes_by_actor.values().map(|v| v.len()).sum();
|
||||
tracing::trace!(num_changes, ?changes_by_actor, "change collection context created");
|
||||
Ok(ChangeCollector { changes_by_actor })
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub(crate) fn collect(&mut self, obj: ObjId, op: Op) -> Result<(), Error> {
|
||||
let actor_changes = self
|
||||
.changes_by_actor
|
||||
.get_mut(&op.id.actor())
|
||||
.ok_or_else(||{
|
||||
tracing::error!(missing_actor=op.id.actor(), "missing actor for op");
|
||||
Error::MissingActor
|
||||
})?;
|
||||
let change_index = actor_changes.partition_point(|c| c.max_op < op.id.counter());
|
||||
let change = actor_changes
|
||||
.get_mut(change_index)
|
||||
.ok_or_else(||{
|
||||
tracing::error!(missing_change_index=change_index, "missing change for op");
|
||||
Error::MissingChange
|
||||
})?;
|
||||
change.ops.push((obj, op));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, actors, props))]
|
||||
pub(crate) fn finish(
|
||||
self,
|
||||
actors: &IndexedCache<ActorId>,
|
||||
props: &IndexedCache<String>,
|
||||
) -> Result<CollectedChanges<'static>, Error> {
|
||||
let mut changes_in_order =
|
||||
Vec::with_capacity(self.changes_by_actor.values().map(|c| c.len()).sum());
|
||||
for (_, changes) in self.changes_by_actor {
|
||||
let mut start_op = 0;
|
||||
let mut seq = None;
|
||||
for change in changes {
|
||||
if change.max_op != start_op + (change.ops.len() as u64) {
|
||||
tracing::error!(?change, start_op, "missing operations");
|
||||
return Err(Error::MissingOps);
|
||||
} else {
|
||||
start_op = change.max_op;
|
||||
}
|
||||
if let Some(seq) = seq {
|
||||
if seq != change.seq - 1 {
|
||||
return Err(Error::ChangesOutOfOrder);
|
||||
}
|
||||
} else if change.seq != 1 {
|
||||
return Err(Error::ChangesOutOfOrder);
|
||||
}
|
||||
seq = Some(change.seq);
|
||||
changes_in_order.push(change);
|
||||
}
|
||||
}
|
||||
changes_in_order.sort_by_key(|c| c.index);
|
||||
|
||||
let mut hashes_by_index = HashMap::new();
|
||||
let mut history = Vec::new();
|
||||
let mut actor_to_history: HashMap<usize, Vec<usize>> = HashMap::new();
|
||||
let mut heads = BTreeSet::new();
|
||||
for (index, change) in changes_in_order.into_iter().enumerate() {
|
||||
actor_to_history
|
||||
.entry(change.actor)
|
||||
.or_default()
|
||||
.push(index);
|
||||
let finished = change.finish(&hashes_by_index, actors, props)?;
|
||||
let hash = finished.hash();
|
||||
hashes_by_index.insert(index, hash);
|
||||
for dep in &finished.dependencies {
|
||||
heads.remove(dep);
|
||||
}
|
||||
tracing::trace!(?hash, "processing change hash");
|
||||
heads.insert(hash);
|
||||
history.push(finished.into_owned());
|
||||
}
|
||||
|
||||
let indices_by_hash = hashes_by_index.into_iter().map(|(k, v)| (v, k)).collect();
|
||||
Ok(CollectedChanges {
|
||||
history,
|
||||
history_index: indices_by_hash,
|
||||
actor_to_history,
|
||||
heads,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PartialChange<'a> {
|
||||
index: usize,
|
||||
deps: Vec<u64>,
|
||||
actor: usize,
|
||||
seq: u64,
|
||||
max_op: u64,
|
||||
timestamp: i64,
|
||||
message: Option<smol_str::SmolStr>,
|
||||
extra_bytes: Cow<'a, [u8]>,
|
||||
ops: Vec<(ObjId, Op)>,
|
||||
}
|
||||
|
||||
impl<'a> PartialChange<'a> {
|
||||
/// # Panics
|
||||
///
|
||||
/// If any op references a property index which is not in `props`
|
||||
#[instrument(skip(self, known_changes, actors, props))]
|
||||
fn finish(
|
||||
self,
|
||||
known_changes: &HashMap<usize, ChangeHash>,
|
||||
actors: &IndexedCache<ActorId>,
|
||||
props: &IndexedCache<String>,
|
||||
) -> Result<StoredChange<'a>, Error> {
|
||||
let deps_len = self.deps.len();
|
||||
let mut deps =
|
||||
self.deps
|
||||
.into_iter()
|
||||
.try_fold(Vec::with_capacity(deps_len), |mut acc, dep| {
|
||||
acc.push(
|
||||
known_changes
|
||||
.get(&(dep as usize))
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
tracing::error!(dependent_index=self.index, dep_index=dep, "could not find dependency");
|
||||
Error::MissingChange
|
||||
})?,
|
||||
);
|
||||
Ok(acc)
|
||||
})?;
|
||||
deps.sort();
|
||||
let other_actors =
|
||||
self.ops
|
||||
.iter()
|
||||
.try_fold(Vec::with_capacity(self.ops.len()), |mut acc, (_, op)| {
|
||||
match op.key {
|
||||
Key::Seq(ElemId(elem)) => {
|
||||
if elem.actor() != self.actor {
|
||||
acc.push(
|
||||
actors
|
||||
.safe_get(elem.actor())
|
||||
.cloned()
|
||||
.ok_or(Error::MissingActor)?,
|
||||
);
|
||||
}
|
||||
}
|
||||
Key::Map(_) => {}
|
||||
};
|
||||
Ok(acc)
|
||||
})?;
|
||||
let mut ops_data = Vec::new();
|
||||
let num_ops = self.ops.len() as u64;
|
||||
let columns = ChangeOpsColumns::empty().encode(
|
||||
self.ops.into_iter().map(|(obj, op)| {
|
||||
let action_index = op.action.action_index();
|
||||
ChangeOp {
|
||||
key: match op.key {
|
||||
// SAFETY: The caller must ensure that all props in the ops are in the propmap
|
||||
Key::Map(idx) => StoredKey::Prop(props.safe_get(idx).unwrap().into()),
|
||||
Key::Seq(elem) => StoredKey::Elem(elem),
|
||||
},
|
||||
insert: op.insert,
|
||||
val: match op.action {
|
||||
OpType::Make(_) | OpType::Del => PrimVal::Null,
|
||||
OpType::Inc(i) => PrimVal::Int(i),
|
||||
OpType::Set(v) => v.into(),
|
||||
},
|
||||
action: action_index,
|
||||
pred: op.pred,
|
||||
obj,
|
||||
}
|
||||
}),
|
||||
&mut ops_data,
|
||||
);
|
||||
Ok(StoredChange {
|
||||
dependencies: deps,
|
||||
actor: actors
|
||||
.safe_get(self.actor)
|
||||
.cloned()
|
||||
.ok_or(Error::MissingActor)?,
|
||||
other_actors,
|
||||
seq: self.seq,
|
||||
start_op: self.max_op - num_ops,
|
||||
timestamp: self.timestamp,
|
||||
message: self.message.map(|s| s.to_string()),
|
||||
ops_meta: columns.metadata(),
|
||||
ops_data: Cow::Owned(ops_data),
|
||||
extra_bytes: self.extra_bytes,
|
||||
})
|
||||
}
|
||||
}
|
241
automerge/src/columnar_2/load/loading_document.rs
Normal file
241
automerge/src/columnar_2/load/loading_document.rs
Normal file
|
@ -0,0 +1,241 @@
|
|||
use fxhash::FxBuildHasher;
|
||||
use std::collections::{HashMap, BTreeSet};
|
||||
use tracing::instrument;
|
||||
use super::change_collector::ChangeCollector;
|
||||
|
||||
use crate::{
|
||||
columnar_2::{
|
||||
storage::Change as StoredChange,
|
||||
rowblock::{
|
||||
Key as DocOpKey,
|
||||
doc_change_columns::ChangeMetadata,
|
||||
doc_op_columns::DocOp,
|
||||
PrimVal,
|
||||
}
|
||||
},
|
||||
op_set::OpSet,
|
||||
op_tree::{OpSetMetadata, OpTree},
|
||||
types::{ActorId, ChangeHash, ElemId, Key, ObjId, ObjType, Op, OpId, OpType},
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum Error {
|
||||
#[error("the document contained ops which were out of order")]
|
||||
OpsOutOfOrder,
|
||||
#[error("error reading operation: {0:?}")]
|
||||
ReadOp(Box<dyn std::error::Error>),
|
||||
#[error("an operation contained an invalid action")]
|
||||
InvalidAction,
|
||||
#[error("an operation referenced a missing actor id")]
|
||||
MissingActor,
|
||||
#[error("invalid changes: {0}")]
|
||||
InvalidChanges(#[from] super::change_collector::Error),
|
||||
#[error("mismatching heads")]
|
||||
MismatchingHeads,
|
||||
}
|
||||
|
||||
struct LoadingObject {
|
||||
id: ObjId,
|
||||
ops: Vec<Op>,
|
||||
obj_type: ObjType,
|
||||
preds: HashMap<OpId, Vec<OpId>>,
|
||||
}
|
||||
|
||||
impl LoadingObject {
|
||||
fn root() -> Self {
|
||||
LoadingObject {
|
||||
id: ObjId::root(),
|
||||
ops: Vec::new(),
|
||||
obj_type: ObjType::Map,
|
||||
preds: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn new(id: ObjId, obj_type: ObjType) -> Self {
|
||||
LoadingObject {
|
||||
id: id.into(),
|
||||
ops: Vec::new(),
|
||||
obj_type,
|
||||
preds: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn append_op(&mut self, op: Op) -> Result<(), Error> {
|
||||
if let Some(previous_op) = self.ops.last() {
|
||||
if op.key < previous_op.key {
|
||||
tracing::error!(
|
||||
?op,
|
||||
?previous_op,
|
||||
"op key was smaller than key of previous op"
|
||||
);
|
||||
return Err(Error::OpsOutOfOrder);
|
||||
}
|
||||
}
|
||||
for succ in &op.succ {
|
||||
self.preds.entry(*succ).or_default().push(op.id);
|
||||
}
|
||||
self.ops.push(op);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn finish(mut self) -> (ObjId, ObjType, OpTree) {
|
||||
let mut op_tree = OpTree::new();
|
||||
for (index, mut op) in self.ops.into_iter().enumerate() {
|
||||
if let Some(preds) = self.preds.remove(&op.id) {
|
||||
op.pred = preds;
|
||||
}
|
||||
op_tree.insert(index, op);
|
||||
}
|
||||
(self.id, self.obj_type, op_tree)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Loaded<'a> {
|
||||
pub(crate) op_set: OpSet,
|
||||
pub(crate) history: Vec<StoredChange<'a>>,
|
||||
pub(crate) history_index: HashMap<ChangeHash, usize>,
|
||||
pub(crate) actor_to_history: HashMap<usize, Vec<usize>>,
|
||||
}
|
||||
|
||||
#[instrument(skip(actors, expected_heads, changes, ops))]
|
||||
pub(crate) fn load<'a, I, C, OE, CE>(
|
||||
actors: Vec<ActorId>,
|
||||
expected_heads: BTreeSet<ChangeHash>,
|
||||
changes: C,
|
||||
ops: I,
|
||||
) -> Result<Loaded<'static>, Error>
|
||||
where
|
||||
OE: std::error::Error + 'static,
|
||||
CE: std::error::Error + 'static,
|
||||
I: Iterator<Item = Result<DocOp<'a>, OE>>,
|
||||
C: Iterator<Item = Result<ChangeMetadata<'a>, CE>>,
|
||||
{
|
||||
let mut metadata = OpSetMetadata::from_actors(actors);
|
||||
let mut completed_objects = HashMap::<_, _, FxBuildHasher>::default();
|
||||
let mut current_object = LoadingObject::root();
|
||||
let mut collector = ChangeCollector::new(changes)?;
|
||||
let mut obj_types = HashMap::new();
|
||||
obj_types.insert(ObjId::root(), ObjType::Map);
|
||||
for op_res in ops {
|
||||
let doc_op = op_res.map_err(|e| Error::ReadOp(Box::new(e)))?;
|
||||
let obj = doc_op.object;
|
||||
let op = import_op(&mut metadata, doc_op)?;
|
||||
tracing::trace!(?op, "processing op");
|
||||
collector.collect(current_object.id, op.clone())?;
|
||||
|
||||
// We have to record the object types of make operations so that when the object ID the
|
||||
// incoming operations refer to switches we can lookup the object type for the new object.
|
||||
// Ultimately we need this because the OpSet needs to know the object ID _and type_ for
|
||||
// each OpTree it tracks.
|
||||
if obj == current_object.id {
|
||||
match op.action {
|
||||
OpType::Make(obj_type) => {
|
||||
obj_types.insert(op.id.into(), obj_type.clone());
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
current_object.append_op(op)?;
|
||||
} else {
|
||||
let new_obj_type = match obj_types.get(&obj) {
|
||||
Some(t) => Ok(t.clone()),
|
||||
None => {
|
||||
tracing::error!(
|
||||
?op,
|
||||
"operation referenced an object which we haven't seen a create op for yet"
|
||||
);
|
||||
Err(Error::OpsOutOfOrder)
|
||||
}
|
||||
}?;
|
||||
if obj < current_object.id {
|
||||
tracing::error!(?op, previous_obj=?current_object.id, "op referenced an object ID which was less than the previous object ID");
|
||||
return Err(Error::OpsOutOfOrder);
|
||||
} else {
|
||||
let (id, obj_type, op_tree) = current_object.finish();
|
||||
current_object = LoadingObject::new(obj, new_obj_type);
|
||||
current_object.append_op(op)?;
|
||||
completed_objects.insert(id, (obj_type, op_tree));
|
||||
}
|
||||
}
|
||||
}
|
||||
let super::change_collector::CollectedChanges{
|
||||
history,
|
||||
history_index,
|
||||
actor_to_history,
|
||||
heads,
|
||||
} = collector.finish(
|
||||
&metadata.actors,
|
||||
&metadata.props,
|
||||
)?;
|
||||
if expected_heads != heads {
|
||||
tracing::error!(?expected_heads, ?heads, "mismatching heads");
|
||||
return Err(Error::MismatchingHeads);
|
||||
}
|
||||
let (id, obj_type, op_tree) = current_object.finish();
|
||||
completed_objects.insert(id, (obj_type, op_tree));
|
||||
let op_set = OpSet::from_parts(completed_objects, metadata);
|
||||
|
||||
Ok(Loaded {
|
||||
op_set,
|
||||
history,
|
||||
history_index,
|
||||
actor_to_history,
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(m))]
|
||||
fn import_op<'a>(m: &mut OpSetMetadata, op: DocOp<'a>) -> Result<Op, Error> {
|
||||
let key = match op.key {
|
||||
DocOpKey::Prop(s) => Key::Map(m.import_prop(s)),
|
||||
DocOpKey::Elem(ElemId(op)) => Key::Seq(ElemId(check_opid(m, op)?)),
|
||||
};
|
||||
for opid in &op.succ {
|
||||
if m.actors.safe_get(opid.actor()).is_none() {
|
||||
tracing::error!(?opid, "missing actor");
|
||||
return Err(Error::MissingActor);
|
||||
}
|
||||
}
|
||||
Ok(Op {
|
||||
id: check_opid(m, op.id)?,
|
||||
action: parse_optype(op.action, op.value)?,
|
||||
key,
|
||||
succ: op.succ,
|
||||
pred: Vec::new(),
|
||||
insert: op.insert,
|
||||
})
|
||||
}
|
||||
|
||||
/// We construct the OpSetMetadata directly from the vector of actors which are encoded in the
|
||||
/// start of the document. Therefore we need to check for each opid in the docuemnt that the actor
|
||||
/// ID which it references actually exists in the metadata.
|
||||
#[tracing::instrument(skip(m))]
|
||||
fn check_opid(m: &OpSetMetadata, opid: OpId) -> Result<OpId, Error> {
|
||||
match m.actors.safe_get(opid.actor()) {
|
||||
Some(_) => Ok(opid),
|
||||
None => {
|
||||
tracing::error!("missing actor");
|
||||
Err(Error::MissingActor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_optype<'a>(action_index: usize, value: PrimVal<'a>) -> Result<OpType, Error> {
|
||||
match action_index {
|
||||
0 => Ok(OpType::Make(ObjType::Map)),
|
||||
1 => Ok(OpType::Set(value.into())),
|
||||
2 => Ok(OpType::Make(ObjType::List)),
|
||||
3 => Ok(OpType::Del),
|
||||
4 => Ok(OpType::Make(ObjType::Text)),
|
||||
5 => match value {
|
||||
PrimVal::Int(i) => Ok(OpType::Inc(i)),
|
||||
_ => {
|
||||
tracing::error!(?value, "invalid value for counter op");
|
||||
Err(Error::InvalidAction)
|
||||
}
|
||||
},
|
||||
6 => Ok(OpType::Make(ObjType::Table)),
|
||||
other => {
|
||||
tracing::error!(action = other, "unknown action type");
|
||||
Err(Error::InvalidAction)
|
||||
}
|
||||
}
|
||||
}
|
8
automerge/src/columnar_2/mod.rs
Normal file
8
automerge/src/columnar_2/mod.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
mod column_specification;
|
||||
#[cfg(feature = "storage-v2")]
|
||||
pub(crate) mod load;
|
||||
#[cfg(feature = "storage-v2")]
|
||||
pub(crate) mod save;
|
||||
pub(crate) mod rowblock;
|
||||
pub(crate) mod storage;
|
||||
pub(crate) use column_specification::{ColumnId, ColumnSpec};
|
|
@ -0,0 +1,454 @@
|
|||
use std::{borrow::Borrow, convert::TryFrom, ops::Range};
|
||||
|
||||
use crate::{
|
||||
columnar_2::{
|
||||
column_specification::ColumnType,
|
||||
rowblock::{
|
||||
column_layout::{column::{GroupColRange, ColumnRanges}, ColumnLayout, MismatchingColumn, assert_col_type},
|
||||
column_range::{
|
||||
ActorRange, BooleanRange, DeltaIntRange, RawRange, RleIntRange, RleStringRange,
|
||||
},
|
||||
encoding::{
|
||||
BooleanDecoder, DecodeColumnError, Key, KeyDecoder, ObjDecoder,
|
||||
OpIdListDecoder, RleDecoder, ValueDecoder,
|
||||
},
|
||||
PrimVal,
|
||||
}, ColumnSpec, ColumnId, storage::ColumnMetadata
|
||||
},
|
||||
types::{ElemId, ObjId, OpId},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct ChangeOp<'a> {
|
||||
pub(crate) key: Key,
|
||||
pub(crate) insert: bool,
|
||||
pub(crate) val: PrimVal<'a>,
|
||||
pub(crate) pred: Vec<OpId>,
|
||||
pub(crate) action: u64,
|
||||
pub(crate) obj: ObjId,
|
||||
}
|
||||
|
||||
impl<'a> ChangeOp<'a> {
|
||||
pub(crate) fn into_owned(self) -> ChangeOp<'static> {
|
||||
ChangeOp {
|
||||
key: self.key,
|
||||
insert: self.insert,
|
||||
val: self.val.into_owned(),
|
||||
pred: self.pred,
|
||||
action: self.action,
|
||||
obj: self.obj,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ChangeOpsColumns {
|
||||
obj_actor: ActorRange,
|
||||
obj_counter: RleIntRange,
|
||||
key_actor: ActorRange,
|
||||
key_counter: DeltaIntRange,
|
||||
key_string: RleStringRange,
|
||||
insert: BooleanRange,
|
||||
action: RleIntRange,
|
||||
val_meta: RleIntRange,
|
||||
val_raw: RawRange,
|
||||
pred_group: RleIntRange,
|
||||
pred_actor: RleIntRange,
|
||||
pred_ctr: DeltaIntRange,
|
||||
}
|
||||
|
||||
impl ChangeOpsColumns {
|
||||
pub(crate) fn empty() -> Self {
|
||||
ChangeOpsColumns {
|
||||
obj_actor: (0..0).into(),
|
||||
obj_counter: (0..0).into(),
|
||||
key_actor: (0..0).into(),
|
||||
key_counter: (0..0).into(),
|
||||
key_string: (0..0).into(),
|
||||
insert: (0..0).into(),
|
||||
action: (0..0).into(),
|
||||
val_meta: (0..0).into(),
|
||||
val_raw: (0..0).into(),
|
||||
pred_group: (0..0).into(),
|
||||
pred_actor: (0..0).into(),
|
||||
pred_ctr: (0..0).into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn iter<'a>(&self, data: &'a [u8]) -> ChangeOpsIter<'a> {
|
||||
ChangeOpsIter {
|
||||
failed: false,
|
||||
obj: ObjDecoder::new(self.obj_actor.decoder(data), self.obj_counter.decoder(data)),
|
||||
key: KeyDecoder::new(
|
||||
self.key_actor.decoder(data),
|
||||
self.key_counter.decoder(data),
|
||||
self.key_string.decoder(data),
|
||||
),
|
||||
insert: self.insert.decoder(data),
|
||||
action: self.action.decoder(data),
|
||||
val: ValueDecoder::new(self.val_meta.decoder(data), self.val_raw.decoder(data)),
|
||||
pred: OpIdListDecoder::new(
|
||||
self.pred_group.decoder(data),
|
||||
self.pred_actor.decoder(data),
|
||||
self.pred_ctr.decoder(data),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn encode<'a, I, C: Borrow<ChangeOp<'a>>>(&self, ops: I, out: &mut Vec<u8>) -> ChangeOpsColumns
|
||||
where
|
||||
I: Iterator<Item = C> + Clone,
|
||||
{
|
||||
let obj_actor = self.obj_actor.decoder(&[]).splice(
|
||||
0..0,
|
||||
ops.clone().map(|o| Some(OpId::from(o.borrow().obj).actor() as u64)),
|
||||
out,
|
||||
);
|
||||
let obj_counter = self.obj_counter.decoder(&[]).splice(
|
||||
0..0,
|
||||
ops.clone().map(|o| Some(OpId::from(o.borrow().obj).counter())),
|
||||
out,
|
||||
);
|
||||
let key_actor = self.key_actor.decoder(&[]).splice(
|
||||
0..0,
|
||||
ops.clone().map(|o| match o.borrow().key {
|
||||
Key::Prop(_) => None,
|
||||
Key::Elem(ElemId(o)) => Some(o.actor() as u64),
|
||||
}),
|
||||
out,
|
||||
);
|
||||
let key_counter = self.key_counter.decoder(&[]).splice(
|
||||
0..0,
|
||||
ops.clone().map(|o| match o.borrow().key {
|
||||
Key::Prop(_) => None,
|
||||
Key::Elem(ElemId(o)) => Some(o.counter() as i64),
|
||||
}),
|
||||
out,
|
||||
);
|
||||
let key_string = self.key_string.decoder(&[]).splice(
|
||||
0..0,
|
||||
ops.clone().map(|o| match &o.borrow().key {
|
||||
Key::Prop(k) => Some(k.clone()),
|
||||
Key::Elem(_) => None,
|
||||
}),
|
||||
out,
|
||||
);
|
||||
let insert = self
|
||||
.insert
|
||||
.decoder(&[])
|
||||
.splice(0..0, ops.clone().map(|o| o.borrow().insert), out);
|
||||
let action =
|
||||
self.action
|
||||
.decoder(&[])
|
||||
.splice(0..0, ops.clone().map(|o| Some(o.borrow().action)), out);
|
||||
let mut val_dec = ValueDecoder::new(self.val_meta.decoder(&[]), self.val_raw.decoder(&[]));
|
||||
let (val_meta, val_raw) = val_dec.splice(0..0, ops.clone().map(|o| o.borrow().val.clone()), out);
|
||||
let mut pred_dec = OpIdListDecoder::new(
|
||||
self.pred_group.decoder(&[]),
|
||||
self.pred_actor.decoder(&[]),
|
||||
self.pred_ctr.decoder(&[]),
|
||||
);
|
||||
let (pred_group, pred_actor, pred_ctr) =
|
||||
pred_dec.splice(0..0, ops.map(|o| o.borrow().pred.clone()), out);
|
||||
Self {
|
||||
obj_actor: obj_actor.into(),
|
||||
obj_counter: obj_counter.into(),
|
||||
key_actor: key_actor.into(),
|
||||
key_counter: key_counter.into(),
|
||||
key_string: key_string.into(),
|
||||
insert: insert.into(),
|
||||
action: action.into(),
|
||||
val_meta: val_meta.into(),
|
||||
val_raw: val_raw.into(),
|
||||
pred_group: pred_group.into(),
|
||||
pred_actor: pred_actor.into(),
|
||||
pred_ctr: pred_ctr.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn metadata(&self) -> ColumnMetadata {
|
||||
const OBJ_COL_ID: ColumnId = ColumnId::new(0);
|
||||
const KEY_COL_ID: ColumnId = ColumnId::new(1);
|
||||
const INSERT_COL_ID: ColumnId = ColumnId::new(3);
|
||||
const ACTION_COL_ID: ColumnId = ColumnId::new(4);
|
||||
const VAL_COL_ID: ColumnId = ColumnId::new(5);
|
||||
const PRED_COL_ID: ColumnId = ColumnId::new(7);
|
||||
|
||||
let mut cols = vec![
|
||||
(ColumnSpec::new(OBJ_COL_ID, ColumnType::Actor, false), self.obj_actor.clone().into()),
|
||||
(ColumnSpec::new(OBJ_COL_ID, ColumnType::Integer, false), self.obj_counter.clone().into()),
|
||||
(ColumnSpec::new(KEY_COL_ID, ColumnType::Actor, false), self.key_actor.clone().into()),
|
||||
(ColumnSpec::new(KEY_COL_ID, ColumnType::DeltaInteger, false), self.key_counter.clone().into()),
|
||||
(ColumnSpec::new(KEY_COL_ID, ColumnType::String, false), self.key_string.clone().into()),
|
||||
(ColumnSpec::new(INSERT_COL_ID, ColumnType::Boolean, false), self.insert.clone().into()),
|
||||
(ColumnSpec::new(ACTION_COL_ID, ColumnType::Integer, false), self.action.clone().into()),
|
||||
(ColumnSpec::new(VAL_COL_ID, ColumnType::ValueMetadata, false), self.val_meta.clone().into()),
|
||||
];
|
||||
if self.val_raw.len() > 0 {
|
||||
cols.push((
|
||||
ColumnSpec::new(VAL_COL_ID, ColumnType::Value, false), self.val_raw.clone().into()
|
||||
));
|
||||
}
|
||||
cols.push(
|
||||
(ColumnSpec::new(PRED_COL_ID, ColumnType::Group, false), self.pred_group.clone().into()),
|
||||
);
|
||||
if self.pred_actor.len() > 0 {
|
||||
cols.extend([
|
||||
(ColumnSpec::new(PRED_COL_ID, ColumnType::Actor, false), self.pred_actor.clone().into()),
|
||||
(ColumnSpec::new(PRED_COL_ID, ColumnType::DeltaInteger, false), self.pred_ctr.clone().into()),
|
||||
]);
|
||||
}
|
||||
cols.into_iter().collect()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub(crate) enum ReadChangeOpError {
|
||||
#[error("unexpected null in column {0}")]
|
||||
UnexpectedNull(String),
|
||||
#[error("invalid value in column {column}: {description}")]
|
||||
InvalidValue { column: String, description: String },
|
||||
}
|
||||
|
||||
pub(crate) struct ChangeOpsIter<'a> {
|
||||
failed: bool,
|
||||
obj: ObjDecoder<'a>,
|
||||
key: KeyDecoder<'a>,
|
||||
insert: BooleanDecoder<'a>,
|
||||
action: RleDecoder<'a, u64>,
|
||||
val: ValueDecoder<'a>,
|
||||
pred: OpIdListDecoder<'a>,
|
||||
}
|
||||
|
||||
impl<'a> ChangeOpsIter<'a> {
|
||||
fn done(&self) -> bool {
|
||||
[
|
||||
self.obj.done(),
|
||||
self.key.done(),
|
||||
self.insert.done(),
|
||||
self.action.done(),
|
||||
self.val.done(),
|
||||
self.pred.done(),
|
||||
]
|
||||
.iter()
|
||||
.all(|e| *e)
|
||||
}
|
||||
|
||||
fn try_next(&mut self) -> Result<Option<ChangeOp<'a>>, ReadChangeOpError> {
|
||||
if self.failed {
|
||||
Ok(None)
|
||||
} else if self.done() {
|
||||
Ok(None)
|
||||
} else {
|
||||
let obj = self
|
||||
.obj
|
||||
.next()
|
||||
.transpose()
|
||||
.map_err(|e| self.handle_error("object", e))?
|
||||
.ok_or(ReadChangeOpError::UnexpectedNull("object".to_string()))?;
|
||||
let key = self
|
||||
.key
|
||||
.next()
|
||||
.transpose()
|
||||
.map_err(|e| self.handle_error("key", e))?
|
||||
.ok_or(ReadChangeOpError::UnexpectedNull("key".to_string()))?;
|
||||
let insert = self
|
||||
.insert
|
||||
.next()
|
||||
.ok_or(ReadChangeOpError::UnexpectedNull("insert".to_string()))?;
|
||||
let action = self
|
||||
.action
|
||||
.next()
|
||||
.flatten()
|
||||
.ok_or(ReadChangeOpError::UnexpectedNull("action".to_string()))?;
|
||||
let val = self
|
||||
.val
|
||||
.next()
|
||||
.transpose()
|
||||
.map_err(|e| self.handle_error("value", e))?
|
||||
.ok_or(ReadChangeOpError::UnexpectedNull("value".to_string()))?;
|
||||
let pred = self
|
||||
.pred
|
||||
.next()
|
||||
.transpose()
|
||||
.map_err(|e| self.handle_error("pred", e))?
|
||||
.ok_or(ReadChangeOpError::UnexpectedNull("pred".to_string()))?;
|
||||
Ok(Some(ChangeOp {
|
||||
obj: obj.into(),
|
||||
key,
|
||||
insert,
|
||||
action,
|
||||
val,
|
||||
pred,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_error(
|
||||
&mut self,
|
||||
outer_col: &'static str,
|
||||
err: DecodeColumnError,
|
||||
) -> ReadChangeOpError {
|
||||
match err {
|
||||
DecodeColumnError::InvalidValue {
|
||||
column,
|
||||
description,
|
||||
} => ReadChangeOpError::InvalidValue {
|
||||
column: format!("{}:{}", outer_col, column),
|
||||
description,
|
||||
},
|
||||
DecodeColumnError::UnexpectedNull(col) => {
|
||||
ReadChangeOpError::UnexpectedNull(format!("{}:{}", outer_col, col))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for ChangeOpsIter<'a> {
|
||||
type Item = Result<ChangeOp<'a>, ReadChangeOpError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.try_next() {
|
||||
Ok(v) => v.map(Ok),
|
||||
Err(e) => {
|
||||
self.failed = true;
|
||||
Some(Err(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub(crate) enum ParseChangeColumnsError {
|
||||
#[error("mismatching column at {index}.")]
|
||||
MismatchingColumn { index: usize },
|
||||
#[error("not enough columns")]
|
||||
NotEnoughColumns,
|
||||
}
|
||||
|
||||
impl From<MismatchingColumn> for ParseChangeColumnsError {
|
||||
fn from(m: MismatchingColumn) -> Self {
|
||||
Self::MismatchingColumn{index: m.index}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ColumnLayout> for ChangeOpsColumns {
|
||||
type Error = ParseChangeColumnsError;
|
||||
|
||||
fn try_from(columns: ColumnLayout) -> Result<Self, Self::Error> {
|
||||
let mut obj_actor: Option<Range<usize>> = None;
|
||||
let mut obj_ctr: Option<Range<usize>> = None;
|
||||
let mut key_actor: Option<Range<usize>> = None;
|
||||
let mut key_ctr: Option<Range<usize>> = None;
|
||||
let mut key_str: Option<Range<usize>> = None;
|
||||
let mut insert: Option<Range<usize>> = None;
|
||||
let mut action: Option<Range<usize>> = None;
|
||||
let mut val_meta: Option<Range<usize>> = None;
|
||||
let mut val_raw: Option<Range<usize>> = None;
|
||||
let mut pred_group: Option<Range<usize>> = None;
|
||||
let mut pred_actor: Option<Range<usize>> = None;
|
||||
let mut pred_ctr: Option<Range<usize>> = None;
|
||||
let mut other = ColumnLayout::empty();
|
||||
|
||||
for (index, col) in columns.into_iter().enumerate() {
|
||||
match index {
|
||||
0 => assert_col_type(index, col, ColumnType::Actor, &mut obj_actor)?,
|
||||
1 => assert_col_type(index, col, ColumnType::Integer, &mut obj_ctr)?,
|
||||
2 => assert_col_type(index, col, ColumnType::Actor, &mut key_actor)?,
|
||||
3 => assert_col_type(index, col, ColumnType::DeltaInteger, &mut key_ctr)?,
|
||||
4 => assert_col_type(index, col, ColumnType::String, &mut key_str)?,
|
||||
5 => assert_col_type(index, col, ColumnType::Boolean, &mut insert)?,
|
||||
6 => assert_col_type(index, col, ColumnType::Integer, &mut action)?,
|
||||
7 => match col.ranges() {
|
||||
ColumnRanges::Value{meta, val} => {
|
||||
val_meta = Some(meta);
|
||||
val_raw = Some(val);
|
||||
},
|
||||
_ => return Err(ParseChangeColumnsError::MismatchingColumn{ index }),
|
||||
},
|
||||
8 => match col.ranges() {
|
||||
ColumnRanges::Group{num, mut cols} => {
|
||||
pred_group = Some(num.into());
|
||||
// If there was no data in the group at all then the columns won't be
|
||||
// present
|
||||
if cols.len() == 0 {
|
||||
pred_actor = Some((0..0).into());
|
||||
pred_ctr = Some((0..0).into());
|
||||
} else {
|
||||
let first = cols.next();
|
||||
let second = cols.next();
|
||||
match (first, second) {
|
||||
(Some(GroupColRange::Single(actor_range)), Some(GroupColRange::Single(ctr_range))) =>
|
||||
{
|
||||
pred_actor = Some(actor_range.into());
|
||||
pred_ctr = Some(ctr_range.into());
|
||||
},
|
||||
_ => return Err(ParseChangeColumnsError::MismatchingColumn{ index }),
|
||||
}
|
||||
}
|
||||
if let Some(_) = cols.next() {
|
||||
return Err(ParseChangeColumnsError::MismatchingColumn{ index });
|
||||
}
|
||||
},
|
||||
_ => return Err(ParseChangeColumnsError::MismatchingColumn{ index }),
|
||||
},
|
||||
_ => {
|
||||
other.append(col);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ChangeOpsColumns {
|
||||
obj_actor: obj_actor.ok_or(ParseChangeColumnsError::NotEnoughColumns)?.into(),
|
||||
obj_counter: obj_ctr.ok_or(ParseChangeColumnsError::NotEnoughColumns)?.into(),
|
||||
key_actor: key_actor.ok_or(ParseChangeColumnsError::NotEnoughColumns)?.into(),
|
||||
key_counter: key_ctr.ok_or(ParseChangeColumnsError::NotEnoughColumns)?.into(),
|
||||
key_string: key_str.ok_or(ParseChangeColumnsError::NotEnoughColumns)?.into(),
|
||||
insert: insert.ok_or(ParseChangeColumnsError::NotEnoughColumns)?.into(),
|
||||
action: action.ok_or(ParseChangeColumnsError::NotEnoughColumns)?.into(),
|
||||
val_meta: val_meta.ok_or(ParseChangeColumnsError::NotEnoughColumns)?.into(),
|
||||
val_raw: val_raw.ok_or(ParseChangeColumnsError::NotEnoughColumns)?.into(),
|
||||
pred_group: pred_group.ok_or(ParseChangeColumnsError::NotEnoughColumns)?.into(),
|
||||
pred_actor: pred_actor.ok_or(ParseChangeColumnsError::NotEnoughColumns)?.into(),
|
||||
pred_ctr: pred_ctr.ok_or(ParseChangeColumnsError::NotEnoughColumns)?.into(),
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::columnar_2::rowblock::encoding::properties::{key, opid, value};
|
||||
use proptest::prelude::*;
|
||||
|
||||
prop_compose! {
|
||||
fn change_op()
|
||||
(key in key(),
|
||||
value in value(),
|
||||
pred in proptest::collection::vec(opid(), 0..20),
|
||||
action in 0_u64..6,
|
||||
obj in opid(),
|
||||
insert in any::<bool>()) -> ChangeOp<'static> {
|
||||
ChangeOp {
|
||||
obj: obj.into(),
|
||||
key,
|
||||
val: value,
|
||||
pred,
|
||||
action,
|
||||
insert,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_encode_decode_change_ops(ops in proptest::collection::vec(change_op(), 0..100)) {
|
||||
let cols = ChangeOpsColumns::empty();
|
||||
let mut out = Vec::new();
|
||||
let cols2 = cols.encode(ops.iter(), &mut out);
|
||||
let decoded = cols2.iter(&out[..]).collect::<Result<Vec<_>, _>>().unwrap();
|
||||
assert_eq!(ops, decoded);
|
||||
}
|
||||
}
|
||||
}
|
603
automerge/src/columnar_2/rowblock/column_layout/column.rs
Normal file
603
automerge/src/columnar_2/rowblock/column_layout/column.rs
Normal file
|
@ -0,0 +1,603 @@
|
|||
use std::{borrow::Cow, ops::Range};
|
||||
|
||||
use super::{
|
||||
super::{
|
||||
encoding::{
|
||||
generic::{GenericColDecoder, GroupDecoder, SingleLogicalColDecoder},
|
||||
RawDecoder, RleDecoder, RleEncoder, SimpleColDecoder, ValueDecoder, BooleanDecoder, DeltaDecoder,
|
||||
},
|
||||
CellValue, ColumnId, ColumnSpec,
|
||||
},
|
||||
ColumnSpliceError,
|
||||
};
|
||||
|
||||
use crate::columnar_2::column_specification::ColumnType;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Column(ColumnInner);
|
||||
|
||||
impl Column {
|
||||
pub(crate) fn range(&self) -> Range<usize> {
|
||||
self.0.range()
|
||||
}
|
||||
|
||||
pub(crate) fn ranges<'a>(&'a self) -> ColumnRanges<'a> {
|
||||
self.0.ranges()
|
||||
}
|
||||
|
||||
pub(crate) fn decoder<'a>(&self, data: &'a [u8]) -> GenericColDecoder<'a> {
|
||||
self.0.decoder(data)
|
||||
}
|
||||
|
||||
pub(crate) fn splice<'a, I>(
|
||||
&self,
|
||||
source: &[u8],
|
||||
output: &mut Vec<u8>,
|
||||
replace: Range<usize>,
|
||||
replace_with: I,
|
||||
) -> Result<Self, ColumnSpliceError>
|
||||
where
|
||||
I: Iterator<Item=CellValue<'a>> + Clone
|
||||
{
|
||||
Ok(Self(self.0.splice(source, output, replace, replace_with)?))
|
||||
}
|
||||
|
||||
pub(crate) fn col_type(&self) -> ColumnType {
|
||||
self.0.col_type()
|
||||
}
|
||||
|
||||
pub fn id(&self) -> ColumnId {
|
||||
match self.0 {
|
||||
ColumnInner::Single(SingleColumn { spec: s, .. }) => s.id(),
|
||||
ColumnInner::Composite(CompositeColumn::Value(ValueColumn { spec, .. })) => spec.id(),
|
||||
ColumnInner::Composite(CompositeColumn::Group(GroupColumn { spec, .. })) => spec.id(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn spec(&self) -> ColumnSpec {
|
||||
match &self.0 {
|
||||
ColumnInner::Single(s) => s.spec,
|
||||
ColumnInner::Composite(CompositeColumn::Value(v)) => v.spec,
|
||||
ColumnInner::Composite(CompositeColumn::Group(g)) => g.spec,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum ColumnInner {
|
||||
Single(SingleColumn),
|
||||
Composite(CompositeColumn),
|
||||
}
|
||||
|
||||
pub(crate) enum ColumnRanges<'a> {
|
||||
Single(Range<usize>),
|
||||
Group{
|
||||
num: Range<usize>,
|
||||
cols: ColRangeIter<'a>,
|
||||
},
|
||||
Value {
|
||||
meta: Range<usize>,
|
||||
val: Range<usize>,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum GroupColRange {
|
||||
Single(Range<usize>),
|
||||
Value{
|
||||
meta: Range<usize>,
|
||||
val: Range<usize>,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ColRangeIter<'a> {
|
||||
offset: usize,
|
||||
cols: &'a [GroupedColumn]
|
||||
}
|
||||
|
||||
impl<'a> Iterator for ColRangeIter<'a> {
|
||||
type Item = GroupColRange;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.cols.get(self.offset) {
|
||||
None => None,
|
||||
Some(GroupedColumn::Single(SingleColumn{range, ..})) => {
|
||||
self.offset += 1;
|
||||
Some(GroupColRange::Single(range.clone()))
|
||||
},
|
||||
Some(GroupedColumn::Value(ValueColumn{meta, value, ..})) => {
|
||||
self.offset += 1;
|
||||
Some(GroupColRange::Value{meta: meta.clone(), val: value.clone()})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ExactSizeIterator for ColRangeIter<'a> {
|
||||
fn len(&self) -> usize {
|
||||
self.cols.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [GroupedColumn]> for ColRangeIter<'a> {
|
||||
fn from(cols: &'a [GroupedColumn]) -> Self {
|
||||
ColRangeIter{
|
||||
cols,
|
||||
offset: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColumnInner {
|
||||
pub(crate) fn range(&self) -> Range<usize> {
|
||||
match self {
|
||||
Self::Single(SingleColumn { range: r, .. }) => r.clone(),
|
||||
Self::Composite(CompositeColumn::Value(ValueColumn { meta, value, .. })) => {
|
||||
meta.start..value.end
|
||||
}
|
||||
Self::Composite(CompositeColumn::Group(GroupColumn { num, values, .. })) => {
|
||||
num.start..values.last().map(|v| v.range().end).unwrap_or(num.end)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn ranges<'a>(&'a self) -> ColumnRanges<'a> {
|
||||
match self {
|
||||
Self::Single(SingleColumn{range, ..}) => ColumnRanges::Single(range.clone()),
|
||||
Self::Composite(CompositeColumn::Value(ValueColumn{ meta, value, ..})) => ColumnRanges::Value {
|
||||
meta: meta.clone(),
|
||||
val: value.clone(),
|
||||
},
|
||||
Self::Composite(CompositeColumn::Group(GroupColumn{num, values, ..})) => ColumnRanges::Group {
|
||||
num: num.clone(),
|
||||
cols: (&values[..]).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn decoder<'a>(&self, data: &'a [u8]) -> GenericColDecoder<'a> {
|
||||
match self {
|
||||
Self::Single(SingleColumn {
|
||||
range, col_type, ..
|
||||
}) => {
|
||||
let simple = col_type.decoder(&data[range.clone()]);
|
||||
GenericColDecoder::new_simple(simple)
|
||||
},
|
||||
Self::Composite(CompositeColumn::Value(ValueColumn{meta, value,..})) => GenericColDecoder::new_value(
|
||||
ValueDecoder::new(
|
||||
RleDecoder::from(Cow::Borrowed(&data[meta.clone()])),
|
||||
RawDecoder::from(Cow::Borrowed(&data[value.clone()])),
|
||||
)
|
||||
),
|
||||
Self::Composite(CompositeColumn::Group(GroupColumn{num, values, ..})) => {
|
||||
let num_coder = RleDecoder::from(Cow::from(&data[num.clone()]));
|
||||
let values = values
|
||||
.iter()
|
||||
.map(|gc| match gc {
|
||||
GroupedColumn::Single(SingleColumn{col_type, range, ..}) => SingleLogicalColDecoder::Simple(
|
||||
col_type.decoder(&data[range.clone()])
|
||||
),
|
||||
GroupedColumn::Value(ValueColumn{ meta, value, .. }) => {
|
||||
SingleLogicalColDecoder::Value(ValueDecoder::new(
|
||||
RleDecoder::from(Cow::Borrowed(&data[meta.clone()])),
|
||||
RawDecoder::from(Cow::Borrowed(&data[value.clone()])),
|
||||
))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
GenericColDecoder::new_group(GroupDecoder::new(num_coder, values))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn splice<'a, I>(
|
||||
&self,
|
||||
source: &[u8],
|
||||
output: &mut Vec<u8>,
|
||||
replace: Range<usize>,
|
||||
replace_with: I,
|
||||
) -> Result<Self, ColumnSpliceError>
|
||||
where
|
||||
I: Iterator<Item=CellValue<'a>> + Clone
|
||||
{
|
||||
match self {
|
||||
Self::Single(s) => Ok(Self::Single(s.splice(
|
||||
source,
|
||||
output,
|
||||
replace,
|
||||
replace_with,
|
||||
)?)),
|
||||
Self::Composite(s) => Ok(Self::Composite(s.splice(
|
||||
source,
|
||||
output,
|
||||
replace,
|
||||
replace_with,
|
||||
)?)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn col_type(&self) -> ColumnType {
|
||||
match self {
|
||||
Self::Single(SingleColumn{spec, ..}) => spec.col_type(),
|
||||
Self::Composite(CompositeColumn::Value(..)) => ColumnType::Value,
|
||||
Self::Composite(CompositeColumn::Group(..)) => ColumnType::Group,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct SingleColumn {
|
||||
pub(crate) spec: ColumnSpec,
|
||||
pub(crate) col_type: SimpleColType,
|
||||
pub(crate) range: Range<usize>,
|
||||
}
|
||||
|
||||
impl SingleColumn {
|
||||
fn splice<'a, I>(
|
||||
&self,
|
||||
source: &[u8],
|
||||
output: &mut Vec<u8>,
|
||||
replace: Range<usize>,
|
||||
replace_with: I,
|
||||
) -> Result<Self, ColumnSpliceError>
|
||||
where
|
||||
I: Iterator<Item=CellValue<'a>> + Clone
|
||||
{
|
||||
let output_start = output.len();
|
||||
let mut decoder = self.col_type.decoder(&source[self.range.clone()]);
|
||||
let end = decoder.splice(output, replace, replace_with)? + output_start;
|
||||
Ok(Self {
|
||||
spec: self.spec,
|
||||
col_type: self.col_type,
|
||||
range: (output_start..end).into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum CompositeColumn {
|
||||
Value(ValueColumn),
|
||||
Group(GroupColumn),
|
||||
}
|
||||
|
||||
impl CompositeColumn {
|
||||
fn splice<'a, I>(
|
||||
&self,
|
||||
source: &[u8],
|
||||
output: &mut Vec<u8>,
|
||||
replace: Range<usize>,
|
||||
replace_with: I,
|
||||
) -> Result<Self, ColumnSpliceError>
|
||||
where
|
||||
I: Iterator<Item=CellValue<'a>> + Clone
|
||||
{
|
||||
match self {
|
||||
Self::Value(value) => Ok(Self::Value(value.splice(
|
||||
source,
|
||||
replace,
|
||||
replace_with,
|
||||
output,
|
||||
)?)),
|
||||
Self::Group(group) => Ok(Self::Group(group.splice(
|
||||
source,
|
||||
output,
|
||||
replace,
|
||||
replace_with,
|
||||
)?)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ValueColumn {
|
||||
spec: ColumnSpec,
|
||||
meta: Range<usize>,
|
||||
value: Range<usize>,
|
||||
}
|
||||
|
||||
impl ValueColumn {
|
||||
fn splice<'a, I>(
|
||||
&self,
|
||||
source: &[u8],
|
||||
replace: Range<usize>,
|
||||
replace_with: I,
|
||||
output: &mut Vec<u8>,
|
||||
) -> Result<Self, ColumnSpliceError>
|
||||
where
|
||||
I: Iterator<Item=CellValue<'a>> + Clone
|
||||
{
|
||||
let mut decoder = ValueDecoder::new(
|
||||
RleDecoder::from(&source[self.meta.clone()]),
|
||||
RawDecoder::from(&source[self.value.clone()]),
|
||||
);
|
||||
let replacements = replace_with.enumerate().map(|(i, r)| match r {
|
||||
CellValue::Value(p) => Ok(p),
|
||||
_ => Err(ColumnSpliceError::InvalidValueForRow(i)),
|
||||
});
|
||||
let (new_meta, new_data) = decoder.try_splice(replace, replacements, output)?;
|
||||
Ok(ValueColumn {
|
||||
spec: self.spec,
|
||||
meta: new_meta.into(),
|
||||
value: new_data.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct GroupColumn {
|
||||
spec: ColumnSpec,
|
||||
num: Range<usize>,
|
||||
values: Vec<GroupedColumn>,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Clone, Copy, Debug)]
|
||||
enum SimpleColType {
|
||||
Actor,
|
||||
Integer,
|
||||
DeltaInteger,
|
||||
Boolean,
|
||||
String,
|
||||
}
|
||||
|
||||
impl SimpleColType {
|
||||
fn decoder<'a>(self, data: &'a [u8]) -> SimpleColDecoder<'a> {
|
||||
match self {
|
||||
SimpleColType::Actor => SimpleColDecoder::new_uint(RleDecoder::from(Cow::from(data))),
|
||||
SimpleColType::Integer => SimpleColDecoder::new_uint(RleDecoder::from(Cow::from(data))),
|
||||
SimpleColType::String => SimpleColDecoder::new_string(RleDecoder::from(Cow::from(data))),
|
||||
SimpleColType::Boolean => SimpleColDecoder::new_bool(BooleanDecoder::from(Cow::from(data))),
|
||||
SimpleColType::DeltaInteger => SimpleColDecoder::new_delta(DeltaDecoder::from(Cow::from(data))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum GroupedColumn {
|
||||
Single(SingleColumn),
|
||||
Value(ValueColumn),
|
||||
}
|
||||
|
||||
impl GroupedColumn {
|
||||
fn range(&self) -> Range<usize> {
|
||||
match self {
|
||||
Self::Single(SingleColumn{range, ..}) => range.clone(),
|
||||
Self::Value(ValueColumn { meta, value, .. }) => (meta.start..value.end),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GroupColumn {
|
||||
fn splice<'a, I>(
|
||||
&self,
|
||||
source: &[u8],
|
||||
output: &mut Vec<u8>,
|
||||
replace: Range<usize>,
|
||||
replace_with: I,
|
||||
) -> Result<Self, ColumnSpliceError>
|
||||
where
|
||||
I: Iterator<Item=CellValue<'a>> + Clone
|
||||
{
|
||||
// This is a little like ValueDecoder::splice. First we want to read off the values from `num`
|
||||
// and insert them into the output - inserting replacements lengths as we go. Then we re-read
|
||||
// num and use it to also iterate over the grouped values, inserting those into the subsidiary
|
||||
// columns as we go.
|
||||
|
||||
// First encode the lengths
|
||||
let output_start = output.len();
|
||||
let mut num_decoder =
|
||||
RleDecoder::<'_, u64>::from(Cow::from(&source[self.num.clone()]));
|
||||
let mut num_encoder = RleEncoder::from(output);
|
||||
let mut idx = 0;
|
||||
while idx < replace.start {
|
||||
match num_decoder.next() {
|
||||
Some(next_num) => {
|
||||
num_encoder.append(next_num.as_ref());
|
||||
}
|
||||
None => {
|
||||
panic!("out of bounds");
|
||||
}
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
let mut num_replace_with = replace_with.clone();
|
||||
while let Some(replacement) = num_replace_with.next() {
|
||||
let rows = match &replacement {
|
||||
CellValue::List(rows) => rows,
|
||||
_ => return Err(ColumnSpliceError::InvalidValueForRow(idx)),
|
||||
};
|
||||
for row in rows {
|
||||
if row.len() != self.values.len() {
|
||||
return Err(ColumnSpliceError::WrongNumberOfValues {
|
||||
row: idx - replace.start,
|
||||
expected: self.values.len(),
|
||||
actual: row.len(),
|
||||
});
|
||||
}
|
||||
num_encoder.append(Some(&(rows.len() as u64)));
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
while let Some(num) = num_decoder.next() {
|
||||
num_encoder.append(num.as_ref());
|
||||
idx += 1;
|
||||
}
|
||||
let _num_range = output_start..num_encoder.finish();
|
||||
|
||||
// Now encode the values
|
||||
let _num_decoder =
|
||||
RleDecoder::<'_, u64>::from(Cow::from(&source[self.num.clone()]));
|
||||
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ColumnBuilder {
|
||||
|
||||
}
|
||||
|
||||
impl ColumnBuilder {
|
||||
pub(crate) fn build_actor(spec: ColumnSpec, range: Range<usize>) -> Column {
|
||||
Column(ColumnInner::Single(SingleColumn{spec, col_type: SimpleColType::Actor, range: range.into()}))
|
||||
}
|
||||
|
||||
pub(crate) fn build_string(spec: ColumnSpec, range: Range<usize>) -> Column {
|
||||
Column(ColumnInner::Single(SingleColumn{spec, col_type: SimpleColType::String, range: range.into()}))
|
||||
}
|
||||
|
||||
pub(crate) fn build_integer(spec: ColumnSpec, range: Range<usize>) -> Column {
|
||||
Column(ColumnInner::Single(SingleColumn{spec, col_type: SimpleColType::Integer, range: range.into()}))
|
||||
}
|
||||
|
||||
pub(crate) fn build_delta_integer(spec: ColumnSpec, range: Range<usize>) -> Column {
|
||||
Column(ColumnInner::Single(SingleColumn{spec, col_type: SimpleColType::Integer, range: range.into()}))
|
||||
}
|
||||
|
||||
pub(crate) fn build_boolean(spec: ColumnSpec, range: Range<usize>) -> Column {
|
||||
Column(ColumnInner::Single(SingleColumn{spec, col_type: SimpleColType::Boolean, range: range.into()}))
|
||||
}
|
||||
|
||||
pub(crate) fn start_value(spec: ColumnSpec, meta: Range<usize>) -> AwaitingRawColumnValueBuilder {
|
||||
AwaitingRawColumnValueBuilder { spec, meta }
|
||||
}
|
||||
|
||||
pub(crate) fn start_group(spec: ColumnSpec, num: Range<usize>) -> GroupBuilder {
|
||||
GroupBuilder{spec, num_range: num, columns: Vec::new()}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct AwaitingRawColumnValueBuilder {
|
||||
spec: ColumnSpec,
|
||||
meta: Range<usize>,
|
||||
}
|
||||
|
||||
impl AwaitingRawColumnValueBuilder {
|
||||
pub(crate) fn id(&self) -> ColumnId {
|
||||
self.spec.id()
|
||||
}
|
||||
|
||||
pub(crate) fn meta_range(&self) -> &Range<usize> {
|
||||
&self.meta
|
||||
}
|
||||
|
||||
pub(crate) fn build(&mut self, raw: Range<usize>) -> Column {
|
||||
Column(ColumnInner::Composite(CompositeColumn::Value(ValueColumn{
|
||||
spec: self.spec,
|
||||
meta: self.meta.clone().into(),
|
||||
value: raw.into(),
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct GroupBuilder{
|
||||
spec: ColumnSpec,
|
||||
num_range: Range<usize>,
|
||||
columns: Vec<GroupedColumn>,
|
||||
}
|
||||
|
||||
impl GroupBuilder {
|
||||
|
||||
pub(crate) fn range(&self) -> Range<usize> {
|
||||
let start = self.num_range.start;
|
||||
let end = self.columns.last().map(|c| c.range().end).unwrap_or(self.num_range.end);
|
||||
start..end
|
||||
}
|
||||
|
||||
pub(crate) fn add_actor(&mut self, spec: ColumnSpec, range: Range<usize>) {
|
||||
self.columns.push(GroupedColumn::Single(SingleColumn{
|
||||
col_type: SimpleColType::Actor,
|
||||
range: range.into(),
|
||||
spec,
|
||||
}));
|
||||
}
|
||||
|
||||
pub(crate) fn add_string(&mut self, spec: ColumnSpec, range: Range<usize>) {
|
||||
self.columns.push(GroupedColumn::Single(SingleColumn{
|
||||
col_type: SimpleColType::String,
|
||||
range: range.into(),
|
||||
spec,
|
||||
}));
|
||||
}
|
||||
|
||||
pub(crate) fn add_integer(&mut self, spec: ColumnSpec, range: Range<usize>) {
|
||||
self.columns.push(GroupedColumn::Single(SingleColumn{
|
||||
col_type: SimpleColType::Integer,
|
||||
range: range.into(),
|
||||
spec,
|
||||
}));
|
||||
}
|
||||
|
||||
pub(crate) fn add_delta_integer(&mut self, spec: ColumnSpec, range: Range<usize>) {
|
||||
self.columns.push(GroupedColumn::Single(SingleColumn{
|
||||
col_type: SimpleColType::DeltaInteger,
|
||||
range: range.into(),
|
||||
spec,
|
||||
}));
|
||||
}
|
||||
|
||||
pub(crate) fn add_boolean(&mut self, spec: ColumnSpec, range: Range<usize>) {
|
||||
self.columns.push(GroupedColumn::Single(SingleColumn{
|
||||
col_type: SimpleColType::Boolean,
|
||||
range: range.into(),
|
||||
spec,
|
||||
}));
|
||||
}
|
||||
|
||||
pub(crate) fn start_value(&mut self, spec: ColumnSpec, meta: Range<usize>) -> GroupAwaitingValue {
|
||||
GroupAwaitingValue {
|
||||
spec,
|
||||
num_range: self.num_range.clone(),
|
||||
columns: std::mem::take(&mut self.columns),
|
||||
val_spec: spec,
|
||||
val_meta: meta,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn finish(&mut self) -> Column {
|
||||
Column(ColumnInner::Composite(CompositeColumn::Group(GroupColumn{
|
||||
spec: self.spec,
|
||||
num: self.num_range.clone(),
|
||||
values: std::mem::take(&mut self.columns),
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct GroupAwaitingValue {
|
||||
spec: ColumnSpec,
|
||||
num_range: Range<usize>,
|
||||
columns: Vec<GroupedColumn>,
|
||||
val_spec: ColumnSpec,
|
||||
val_meta: Range<usize>,
|
||||
}
|
||||
|
||||
impl GroupAwaitingValue {
|
||||
pub(crate) fn finish_empty(&mut self) -> GroupBuilder {
|
||||
self.columns.push(GroupedColumn::Value(ValueColumn{
|
||||
meta: self.val_meta.clone(),
|
||||
value: 0..0,
|
||||
spec: self.val_spec,
|
||||
}));
|
||||
GroupBuilder {
|
||||
spec: self.spec,
|
||||
num_range: self.num_range.clone(),
|
||||
columns: std::mem::take(&mut self.columns),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn finish_value(&mut self, raw: Range<usize>) -> GroupBuilder {
|
||||
self.columns.push(GroupedColumn::Value(ValueColumn{
|
||||
spec: self.val_spec,
|
||||
value: raw.into(),
|
||||
meta: self.val_meta.clone(),
|
||||
}));
|
||||
GroupBuilder {
|
||||
spec: self.spec,
|
||||
num_range: self.num_range.clone(),
|
||||
columns: std::mem::take(&mut self.columns),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn range(&self) -> Range<usize> {
|
||||
self.num_range.start..self.val_meta.end
|
||||
}
|
||||
}
|
|
@ -0,0 +1,480 @@
|
|||
use std::{borrow::{Borrow, Cow}, convert::TryFrom, ops::Range};
|
||||
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::columnar_2::{
|
||||
column_specification::ColumnType,
|
||||
rowblock::{
|
||||
column_range::{ActorRange, DeltaIntRange, RawRange, RleIntRange, RleStringRange},
|
||||
encoding::{DecodeColumnError, DeltaDecoder, RawDecoder, RleDecoder, ValueDecoder},
|
||||
PrimVal,
|
||||
},
|
||||
ColumnId, ColumnSpec,
|
||||
storage::ColumnMetadata,
|
||||
};
|
||||
|
||||
use super::{
|
||||
assert_col_type,
|
||||
column::{ColumnRanges, GroupColRange},
|
||||
ColumnLayout, MismatchingColumn,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ChangeMetadata<'a> {
|
||||
pub(crate) actor: usize,
|
||||
pub(crate) seq: u64,
|
||||
pub(crate) max_op: u64,
|
||||
pub(crate) timestamp: i64,
|
||||
pub(crate) message: Option<smol_str::SmolStr>,
|
||||
pub(crate) deps: Vec<u64>,
|
||||
pub(crate) extra: Cow<'a, [u8]>,
|
||||
}
|
||||
|
||||
pub(crate) struct DocChangeColumns {
|
||||
actor: ActorRange,
|
||||
seq: DeltaIntRange,
|
||||
max_op: DeltaIntRange,
|
||||
time: DeltaIntRange,
|
||||
message: RleStringRange,
|
||||
deps_group: RleIntRange,
|
||||
deps_index: DeltaIntRange,
|
||||
extra_meta: RleIntRange,
|
||||
extra_val: RawRange,
|
||||
other: ColumnLayout,
|
||||
}
|
||||
|
||||
impl DocChangeColumns {
|
||||
pub(crate) fn iter<'a>(&self, data: &'a [u8]) -> DocChangeColumnIter<'a> {
|
||||
DocChangeColumnIter {
|
||||
actors: self.actor.decoder(data),
|
||||
seq: self.seq.decoder(data),
|
||||
max_op: self.max_op.decoder(data),
|
||||
time: self.time.decoder(data),
|
||||
message: self.message.decoder(data),
|
||||
deps: DepsDecoder {
|
||||
group: self.deps_group.decoder(data),
|
||||
deps: self.deps_index.decoder(data),
|
||||
},
|
||||
extra: ExtraDecoder {
|
||||
val: ValueDecoder::new(self.extra_meta.decoder(data), self.extra_val.decoder(data)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn encode<'a, I, C: Borrow<ChangeMetadata<'a>>>(changes: I, out: &mut Vec<u8>) -> DocChangeColumns
|
||||
where
|
||||
I: Iterator<Item = C> + Clone,
|
||||
{
|
||||
let actor = ActorRange::from(0..0).decoder(&[]).splice(
|
||||
0..0,
|
||||
// TODO: make this fallible once iterators have a try_splice
|
||||
changes.clone().map(|c| Some(c.borrow().actor as u64)),
|
||||
out,
|
||||
);
|
||||
let seq = DeltaDecoder::from(&[] as &[u8]).splice(
|
||||
0..0,
|
||||
changes.clone().map(|c| Some(c.borrow().seq as i64)),
|
||||
out,
|
||||
);
|
||||
let max_op = DeltaDecoder::from(&[] as &[u8]).splice(
|
||||
0..0,
|
||||
changes.clone().map(|c| Some(c.borrow().max_op as i64)),
|
||||
out,
|
||||
);
|
||||
let time = DeltaDecoder::from(&[] as &[u8]).splice(
|
||||
0..0,
|
||||
changes.clone().map(|c| Some(c.borrow().timestamp)),
|
||||
out,
|
||||
);
|
||||
let message = RleDecoder::<'a, smol_str::SmolStr>::from(&[] as &[u8]).splice(
|
||||
0..0,
|
||||
changes.clone().map(|c| c.borrow().message.clone()),
|
||||
out,
|
||||
);
|
||||
let (deps_group, deps_index) = DepsDecoder {
|
||||
group: RleDecoder::from(&[] as &[u8]),
|
||||
deps: DeltaDecoder::from(&[] as &[u8]),
|
||||
}
|
||||
.splice(0..0, changes.clone().map(|c| c.borrow().deps.clone()), out);
|
||||
let (extra_meta, extra_val) = ValueDecoder::new(
|
||||
RleDecoder::from(&[] as &[u8]),
|
||||
RawDecoder::from(&[] as &[u8]),
|
||||
)
|
||||
.splice(0..0, changes.clone().map(|c| PrimVal::Bytes(c.borrow().extra.clone())), out);
|
||||
DocChangeColumns {
|
||||
actor: actor.into(),
|
||||
seq: seq.into(),
|
||||
max_op: max_op.into(),
|
||||
time: time.into(),
|
||||
message: message.into(),
|
||||
deps_group: deps_group.into(),
|
||||
deps_index: deps_index.into(),
|
||||
extra_meta: extra_meta.into(),
|
||||
extra_val: extra_val.into(),
|
||||
other: ColumnLayout::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn metadata(&self) -> ColumnMetadata {
|
||||
const ACTOR_COL_ID: ColumnId = ColumnId::new(0);
|
||||
const SEQ_COL_ID: ColumnId = ColumnId::new(0);
|
||||
const MAX_OP_COL_ID: ColumnId = ColumnId::new(1);
|
||||
const TIME_COL_ID: ColumnId = ColumnId::new(2);
|
||||
const MESSAGE_COL_ID: ColumnId = ColumnId::new(3);
|
||||
const DEPS_COL_ID: ColumnId = ColumnId::new(4);
|
||||
const EXTRA_COL_ID: ColumnId = ColumnId::new(5);
|
||||
|
||||
let mut cols = vec![
|
||||
(ColumnSpec::new(ACTOR_COL_ID, ColumnType::Actor, false), self.actor.clone().into()),
|
||||
(ColumnSpec::new(SEQ_COL_ID, ColumnType::DeltaInteger, false), self.seq.clone().into()),
|
||||
(ColumnSpec::new(MAX_OP_COL_ID, ColumnType::DeltaInteger, false), self.max_op.clone().into()),
|
||||
(ColumnSpec::new(TIME_COL_ID, ColumnType::DeltaInteger, false), self.time.clone().into()),
|
||||
(ColumnSpec::new(MESSAGE_COL_ID, ColumnType::String, false), self.message.clone().into()),
|
||||
(ColumnSpec::new(DEPS_COL_ID, ColumnType::Group, false), self.deps_group.clone().into()),
|
||||
];
|
||||
if self.deps_index.len() > 0 {
|
||||
cols.push((
|
||||
ColumnSpec::new(DEPS_COL_ID, ColumnType::DeltaInteger, false), self.deps_index.clone().into()
|
||||
))
|
||||
}
|
||||
cols.push(
|
||||
(ColumnSpec::new(EXTRA_COL_ID, ColumnType::ValueMetadata, false), self.extra_meta.clone().into()),
|
||||
);
|
||||
if self.extra_val.len() > 0 {
|
||||
cols.push((
|
||||
ColumnSpec::new(EXTRA_COL_ID, ColumnType::Value, false), self.extra_val.clone().into()
|
||||
))
|
||||
}
|
||||
cols.into_iter().collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum DecodeChangeError {
|
||||
#[error("the depenencies column was invalid")]
|
||||
InvalidDeps,
|
||||
#[error("unexpected null value for {0}")]
|
||||
UnexpectedNull(String),
|
||||
#[error("mismatching column types for column {index}")]
|
||||
MismatchingColumn { index: usize },
|
||||
#[error("not enough columns")]
|
||||
NotEnoughColumns,
|
||||
#[error("incorrect value in extra bytes column")]
|
||||
InvalidExtraBytes,
|
||||
#[error("error reading column {column}: {description}")]
|
||||
ReadColumn { column: String, description: String },
|
||||
}
|
||||
|
||||
impl DecodeChangeError {
|
||||
fn from_decode_col(col: &'static str, err: DecodeColumnError) -> Self {
|
||||
match err {
|
||||
DecodeColumnError::InvalidValue { description, .. } => Self::ReadColumn {
|
||||
column: col.to_string(),
|
||||
description,
|
||||
},
|
||||
DecodeColumnError::UnexpectedNull(inner_col) => {
|
||||
Self::UnexpectedNull(format!("{}:{}", col, inner_col))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MismatchingColumn> for DecodeChangeError {
|
||||
fn from(m: MismatchingColumn) -> Self {
|
||||
Self::MismatchingColumn { index: m.index }
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DocChangeColumnIter<'a> {
|
||||
actors: RleDecoder<'a, u64>,
|
||||
seq: DeltaDecoder<'a>,
|
||||
max_op: DeltaDecoder<'a>,
|
||||
time: DeltaDecoder<'a>,
|
||||
message: RleDecoder<'a, smol_str::SmolStr>,
|
||||
deps: DepsDecoder<'a>,
|
||||
extra: ExtraDecoder<'a>,
|
||||
}
|
||||
|
||||
macro_rules! next_or_invalid({$iter: expr, $col: literal} => {
|
||||
match $iter.next() {
|
||||
Some(Some(s)) => s,
|
||||
Some(None) => return Some(Err(DecodeChangeError::UnexpectedNull($col.to_string()))),
|
||||
None => return Some(Err(DecodeChangeError::UnexpectedNull($col.to_string()))),
|
||||
}
|
||||
});
|
||||
|
||||
impl<'a> Iterator for DocChangeColumnIter<'a> {
|
||||
type Item = Result<ChangeMetadata<'a>, DecodeChangeError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let actor = match self.actors.next() {
|
||||
Some(Some(actor)) => actor as usize,
|
||||
Some(None) => return Some(Err(DecodeChangeError::UnexpectedNull("actor".to_string()))),
|
||||
None => {
|
||||
// The actor column should always have a value so if the actor iterator returns None that
|
||||
// means we should be done, we check by asserting that all the other iterators
|
||||
// return none (which is what Self::is_done does).
|
||||
if self.is_done() {
|
||||
return None;
|
||||
} else {
|
||||
return Some(Err(DecodeChangeError::UnexpectedNull("actor".to_string())));
|
||||
}
|
||||
}
|
||||
};
|
||||
let seq = match next_or_invalid!(self.seq, "seq").try_into() {
|
||||
Ok(s) => s,
|
||||
Err(_) => {
|
||||
return Some(Err(DecodeChangeError::ReadColumn {
|
||||
column: "seq".to_string(),
|
||||
description: "negative value".to_string(),
|
||||
}))
|
||||
}
|
||||
};
|
||||
let max_op = match next_or_invalid!(self.max_op, "max_op").try_into() {
|
||||
Ok(o) => o,
|
||||
Err(_) => {
|
||||
return Some(Err(DecodeChangeError::ReadColumn {
|
||||
column: "max_op".to_string(),
|
||||
description: "negative value".to_string(),
|
||||
}))
|
||||
}
|
||||
};
|
||||
let time = next_or_invalid!(self.time, "time");
|
||||
let message = match self.message.next() {
|
||||
Some(Some(s)) => Some(s),
|
||||
Some(None) => None,
|
||||
None => return Some(Err(DecodeChangeError::UnexpectedNull("msg".to_string()))),
|
||||
};
|
||||
let deps = match self.deps.next() {
|
||||
Some(Ok(d)) => d,
|
||||
Some(Err(e)) => return Some(Err(e)),
|
||||
None => return Some(Err(DecodeChangeError::UnexpectedNull("deps".to_string()))),
|
||||
};
|
||||
let extra = match self.extra.next() {
|
||||
Some(Ok(e)) => e,
|
||||
Some(Err(e)) => return Some(Err(e)),
|
||||
None => return Some(Err(DecodeChangeError::UnexpectedNull("extra".to_string()))),
|
||||
};
|
||||
Some(Ok(ChangeMetadata {
|
||||
actor,
|
||||
seq,
|
||||
max_op,
|
||||
timestamp: time,
|
||||
message,
|
||||
deps,
|
||||
extra,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DocChangeColumnIter<'a> {
|
||||
/// Given that we have read a `None` value in the actor column, check that every other column
|
||||
/// also returns `None`.
|
||||
fn is_done(&mut self) -> bool {
|
||||
let other_cols = [
|
||||
self.seq.next().is_none(),
|
||||
self.max_op.next().is_none(),
|
||||
self.time.next().is_none(),
|
||||
self.message.next().is_none(),
|
||||
self.deps.next().is_none(),
|
||||
];
|
||||
other_cols.iter().all(|f| *f)
|
||||
}
|
||||
}
|
||||
|
||||
struct DepsDecoder<'a> {
|
||||
group: RleDecoder<'a, u64>,
|
||||
deps: DeltaDecoder<'a>,
|
||||
}
|
||||
|
||||
impl<'a> DepsDecoder<'a> {
|
||||
|
||||
fn encode<'b, I>(deps: I, out: &'a mut Vec<u8>) -> DepsDecoder<'a>
|
||||
where
|
||||
I: Iterator<Item=&'b [u64]> + Clone
|
||||
{
|
||||
let group = RleDecoder::encode(deps.clone().map(|d| d.len() as u64), out);
|
||||
let deps = DeltaDecoder::encode(deps.flat_map(|d| d.iter().map(|d| *d as i64)), out);
|
||||
DepsDecoder{
|
||||
group: RleDecoder::from(&out[group]),
|
||||
deps: DeltaDecoder::from(&out[deps]),
|
||||
}
|
||||
}
|
||||
|
||||
fn splice<'b, I>(
|
||||
&self,
|
||||
replace_range: Range<usize>,
|
||||
items: I,
|
||||
out: &mut Vec<u8>,
|
||||
) -> (Range<usize>, Range<usize>)
|
||||
where
|
||||
I: Iterator<Item = Vec<u64>> + Clone,
|
||||
{
|
||||
let mut replace_start = 0_usize;
|
||||
let mut replace_len = 0_usize;
|
||||
for (index, elems) in self.group.clone().enumerate() {
|
||||
if let Some(elems) = elems {
|
||||
if index < replace_range.start {
|
||||
replace_start += elems as usize;
|
||||
} else if index < replace_range.end {
|
||||
replace_len += elems as usize;
|
||||
}
|
||||
}
|
||||
}
|
||||
let val_replace_range = replace_start..(replace_start + replace_len);
|
||||
let group = self.group.clone().splice(
|
||||
replace_range,
|
||||
items.clone().map(|i| Some(i.len() as u64)),
|
||||
out
|
||||
);
|
||||
let items = self.deps.clone().splice(
|
||||
val_replace_range,
|
||||
items.flat_map(|elems| elems.into_iter().map(|v| Some(v as i64))),
|
||||
out,
|
||||
);
|
||||
(group, items)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for DepsDecoder<'a> {
|
||||
type Item = Result<Vec<u64>, DecodeChangeError>;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let num = match self.group.next() {
|
||||
Some(Some(n)) => n as usize,
|
||||
Some(None) => return Some(Err(DecodeChangeError::InvalidDeps)),
|
||||
None => return None,
|
||||
};
|
||||
let mut result = Vec::with_capacity(num);
|
||||
while result.len() < num {
|
||||
match self.deps.next() {
|
||||
Some(Some(elem)) => {
|
||||
let elem = match u64::try_from(elem) {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
tracing::error!(err=?e, dep=elem, "error converting dep index to u64");
|
||||
return Some(Err(DecodeChangeError::InvalidDeps));
|
||||
}
|
||||
};
|
||||
result.push(elem);
|
||||
}
|
||||
_ => return Some(Err(DecodeChangeError::InvalidDeps)),
|
||||
}
|
||||
}
|
||||
Some(Ok(result))
|
||||
}
|
||||
}
|
||||
|
||||
struct ExtraDecoder<'a> {
|
||||
val: ValueDecoder<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for ExtraDecoder<'a> {
|
||||
type Item = Result<Cow<'a, [u8]>, DecodeChangeError>;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.val.next() {
|
||||
Some(Ok(PrimVal::Bytes(b))) => Some(Ok(b)),
|
||||
Some(Ok(_)) => Some(Err(DecodeChangeError::InvalidExtraBytes)),
|
||||
Some(Err(e)) => Some(Err(DecodeChangeError::from_decode_col("value", e))),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ColumnLayout> for DocChangeColumns {
|
||||
type Error = DecodeChangeError;
|
||||
|
||||
#[instrument]
|
||||
fn try_from(columns: ColumnLayout) -> Result<Self, Self::Error> {
|
||||
let mut actor: Option<Range<usize>> = None;
|
||||
let mut seq: Option<Range<usize>> = None;
|
||||
let mut max_op: Option<Range<usize>> = None;
|
||||
let mut time: Option<Range<usize>> = None;
|
||||
let mut message: Option<Range<usize>> = None;
|
||||
let mut deps_group: Option<Range<usize>> = None;
|
||||
let mut deps_index: Option<Range<usize>> = None;
|
||||
let mut extra_meta: Option<Range<usize>> = None;
|
||||
let mut extra_val: Option<Range<usize>> = None;
|
||||
let mut other = ColumnLayout::empty();
|
||||
|
||||
for (index, col) in columns.into_iter().enumerate() {
|
||||
match index {
|
||||
0 => assert_col_type(index, col, ColumnType::Actor, &mut actor)?,
|
||||
1 => assert_col_type(index, col, ColumnType::DeltaInteger, &mut seq)?,
|
||||
2 => assert_col_type(index, col, ColumnType::DeltaInteger, &mut max_op)?,
|
||||
3 => assert_col_type(index, col, ColumnType::DeltaInteger, &mut time)?,
|
||||
4 => assert_col_type(index, col, ColumnType::String, &mut message)?,
|
||||
5 => match col.ranges() {
|
||||
ColumnRanges::Group { num, mut cols } => {
|
||||
deps_group = Some(num.into());
|
||||
let first = cols.next();
|
||||
match first {
|
||||
Some(GroupColRange::Single(index_range)) => {
|
||||
deps_index = Some(index_range.into());
|
||||
}
|
||||
Some(_) => {
|
||||
tracing::error!("deps column contained more than one grouped column");
|
||||
return Err(DecodeChangeError::MismatchingColumn{index: 5});
|
||||
}
|
||||
None => {
|
||||
deps_index = (0..0).into()
|
||||
}
|
||||
};
|
||||
if let Some(_) = cols.next() {
|
||||
return Err(DecodeChangeError::MismatchingColumn { index });
|
||||
}
|
||||
}
|
||||
_ => return Err(DecodeChangeError::MismatchingColumn { index }),
|
||||
},
|
||||
6 => match col.ranges() {
|
||||
ColumnRanges::Value { meta, val } => {
|
||||
extra_meta = Some(meta);
|
||||
extra_val = Some(val);
|
||||
}
|
||||
_ => return Err(DecodeChangeError::MismatchingColumn { index }),
|
||||
},
|
||||
_ => {
|
||||
other.append(col);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(DocChangeColumns {
|
||||
actor: actor.ok_or(DecodeChangeError::NotEnoughColumns)?.into(),
|
||||
seq: seq.ok_or(DecodeChangeError::NotEnoughColumns)?.into(),
|
||||
max_op: max_op.ok_or(DecodeChangeError::NotEnoughColumns)?.into(),
|
||||
time: time.ok_or(DecodeChangeError::NotEnoughColumns)?.into(),
|
||||
message: message.ok_or(DecodeChangeError::NotEnoughColumns)?.into(),
|
||||
deps_group: deps_group
|
||||
.ok_or(DecodeChangeError::NotEnoughColumns)?
|
||||
.into(),
|
||||
deps_index: deps_index
|
||||
.ok_or(DecodeChangeError::NotEnoughColumns)?
|
||||
.into(),
|
||||
extra_meta: extra_meta
|
||||
.ok_or(DecodeChangeError::NotEnoughColumns)?
|
||||
.into(),
|
||||
extra_val: extra_val.ok_or(DecodeChangeError::NotEnoughColumns)?.into(),
|
||||
other,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
use proptest::collection::vec as propvec;
|
||||
|
||||
fn encodable_u64() -> impl Strategy<Value = u64> + Clone {
|
||||
0_u64..((i64::MAX / 2) as u64)
|
||||
}
|
||||
|
||||
proptest!{
|
||||
#[test]
|
||||
fn encode_decode_deps(deps in propvec(propvec(encodable_u64(), 0..100), 0..100)) {
|
||||
let mut out = Vec::new();
|
||||
let decoder = DepsDecoder::encode(deps.iter().map(|d| &d[..]), &mut out);
|
||||
let decoded = decoder.collect::<Result<Vec<_>, _>>().unwrap();
|
||||
assert_eq!(deps, decoded);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,420 @@
|
|||
use std::{convert::TryFrom, ops::Range};
|
||||
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::columnar_2::storage::ColumnMetadata;
|
||||
|
||||
use super::{
|
||||
super::{
|
||||
super::column_specification::ColumnType,
|
||||
column_range::{
|
||||
ActorRange, BooleanRange, DeltaIntRange, RawRange, RleIntRange, RleStringRange,
|
||||
},
|
||||
encoding::{
|
||||
BooleanDecoder, DecodeColumnError, Key, KeyDecoder, ObjDecoder, OpIdDecoder,
|
||||
OpIdListDecoder, RleDecoder, ValueDecoder,
|
||||
},
|
||||
ColumnSpec, ColumnId,
|
||||
},
|
||||
assert_col_type,
|
||||
column::{ColumnRanges, GroupColRange},
|
||||
ColumnLayout, MismatchingColumn,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
columnar_2::rowblock::{PrimVal, encoding::{RawDecoder, DeltaDecoder}},
|
||||
types::{ObjId, OpId, ElemId},
|
||||
};
|
||||
|
||||
/// The form operations take in the compressed document format.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct DocOp<'a> {
|
||||
pub(crate) id: OpId,
|
||||
pub(crate) object: ObjId,
|
||||
pub(crate) key: Key,
|
||||
pub(crate) insert: bool,
|
||||
pub(crate) action: usize,
|
||||
pub(crate) value: PrimVal<'a>,
|
||||
pub(crate) succ: Vec<OpId>,
|
||||
}
|
||||
|
||||
pub(crate) struct DocOpColumns {
|
||||
actor: ActorRange,
|
||||
ctr: RleIntRange,
|
||||
key_actor: ActorRange,
|
||||
key_ctr: DeltaIntRange,
|
||||
key_str: RleStringRange,
|
||||
id_actor: RleIntRange,
|
||||
id_ctr: DeltaIntRange,
|
||||
insert: BooleanRange,
|
||||
action: RleIntRange,
|
||||
val_meta: RleIntRange,
|
||||
val_raw: RawRange,
|
||||
succ_group: RleIntRange,
|
||||
succ_actor: RleIntRange,
|
||||
succ_ctr: DeltaIntRange,
|
||||
other: ColumnLayout,
|
||||
}
|
||||
|
||||
impl DocOpColumns {
|
||||
pub(crate) fn empty() -> DocOpColumns {
|
||||
Self {
|
||||
actor: (0..0).into(),
|
||||
ctr: (0..0).into(),
|
||||
key_actor: (0..0).into(),
|
||||
key_ctr: (0..0).into(),
|
||||
key_str: (0..0).into(),
|
||||
id_actor: (0..0).into(),
|
||||
id_ctr: (0..0).into(),
|
||||
insert: (0..0).into(),
|
||||
action: (0..0).into(),
|
||||
val_meta: (0..0).into(),
|
||||
val_raw: (0..0).into(),
|
||||
succ_group: (0..0).into(),
|
||||
succ_actor: (0..0).into(),
|
||||
succ_ctr: (0..0).into(),
|
||||
other: ColumnLayout::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn encode<'a, I>(ops: I, out: &mut Vec<u8>) -> DocOpColumns
|
||||
where
|
||||
I: Iterator<Item = DocOp<'a>> + Clone,
|
||||
{
|
||||
let obj_actor = ActorRange::from(0..0).decoder(&[]).splice(
|
||||
0..0,
|
||||
ops.clone().map(|o| Some(o.object.opid().actor() as u64)),
|
||||
out,
|
||||
);
|
||||
let obj_ctr = RleIntRange::from(0..0).decoder(&[]).splice(
|
||||
0..0,
|
||||
ops.clone().map(|o| Some(o.object.opid().counter() as u64)),
|
||||
out,
|
||||
);
|
||||
let key_actor = ActorRange::from(0..0).decoder(&[]).splice(
|
||||
0..0,
|
||||
ops.clone().map(|o| match o.key {
|
||||
Key::Prop(_) => None,
|
||||
Key::Elem(ElemId(opid)) if opid.actor() == 0 => None,
|
||||
Key::Elem(ElemId(opid)) => Some(opid.actor() as u64),
|
||||
}),
|
||||
out,
|
||||
);
|
||||
let key_ctr = DeltaIntRange::from(0..0).decoder(&[]).splice(
|
||||
0..0,
|
||||
ops.clone().map(|o| match o.key {
|
||||
Key::Prop(_) => None,
|
||||
Key::Elem(ElemId(opid)) => Some(opid.counter() as i64),
|
||||
}),
|
||||
out,
|
||||
);
|
||||
let key_str = RleStringRange::from(0..0).decoder(&[]).splice(
|
||||
0..0,
|
||||
ops.clone().map(|o| match o.key {
|
||||
Key::Prop(s) => Some(s),
|
||||
Key::Elem(_) => None,
|
||||
}),
|
||||
out,
|
||||
);
|
||||
let id_actor = RleIntRange::from(0..0).decoder(&[]).splice(
|
||||
0..0,
|
||||
ops.clone().map(|o| Some(o.id.actor() as u64)),
|
||||
out,
|
||||
);
|
||||
let id_counter = DeltaIntRange::from(0..0).decoder(&[]).splice(
|
||||
0..0,
|
||||
ops.clone().map(|o| Some(o.id.counter() as i64)),
|
||||
out,
|
||||
);
|
||||
let insert = BooleanRange::from(0..0).decoder(&[]).splice(
|
||||
0..0,
|
||||
ops.clone().map(|o| o.insert),
|
||||
out,
|
||||
);
|
||||
let action = RleIntRange::from(0..0).decoder(&[]).splice(
|
||||
0..0,
|
||||
ops.clone().map(|o| Some(o.action as u64)),
|
||||
out,
|
||||
);
|
||||
let (val_meta, val_raw) = ValueDecoder::new(RleDecoder::from(&[] as &[u8]), RawDecoder::from(&[] as &[u8])).splice(
|
||||
0..0,
|
||||
ops.clone().map(|o| o.value),
|
||||
out,
|
||||
);
|
||||
let mut succ_dec = OpIdListDecoder::new(
|
||||
RleDecoder::from(&[] as &[u8]),
|
||||
RleDecoder::from(&[] as &[u8]),
|
||||
DeltaDecoder::from(&[] as &[u8]),
|
||||
);
|
||||
let (succ_group, succ_actor, succ_ctr) =
|
||||
succ_dec.splice(0..0, ops.map(|o| o.succ.clone()), out);
|
||||
Self {
|
||||
actor: obj_actor.into(),
|
||||
ctr: obj_ctr.into(),
|
||||
key_actor: key_actor.into(),
|
||||
key_ctr: key_ctr.into(),
|
||||
key_str: key_str.into(),
|
||||
id_actor: id_actor.into(),
|
||||
id_ctr: id_counter.into(),
|
||||
insert: insert.into(),
|
||||
action: action.into(),
|
||||
val_meta: val_meta.into(),
|
||||
val_raw: val_raw.into(),
|
||||
succ_group: succ_group.into(),
|
||||
succ_actor: succ_actor.into(),
|
||||
succ_ctr: succ_ctr.into(),
|
||||
other: ColumnLayout::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn iter<'a>(&self, data: &'a [u8]) -> DocOpColumnIter<'a> {
|
||||
DocOpColumnIter {
|
||||
id: OpIdDecoder::new(self.id_actor.decoder(data), self.id_ctr.decoder(data)),
|
||||
action: self.action.decoder(data),
|
||||
objs: ObjDecoder::new(self.actor.decoder(data), self.ctr.decoder(data)),
|
||||
keys: KeyDecoder::new(
|
||||
self.key_actor.decoder(data),
|
||||
self.key_ctr.decoder(data),
|
||||
self.key_str.decoder(data),
|
||||
),
|
||||
insert: self.insert.decoder(data),
|
||||
value: ValueDecoder::new(self.val_meta.decoder(data), self.val_raw.decoder(data)),
|
||||
succ: OpIdListDecoder::new(
|
||||
self.succ_group.decoder(data),
|
||||
self.succ_actor.decoder(data),
|
||||
self.succ_ctr.decoder(data),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn metadata(&self) -> ColumnMetadata {
|
||||
const OBJ_COL_ID: ColumnId = ColumnId::new(0);
|
||||
const KEY_COL_ID: ColumnId = ColumnId::new(1);
|
||||
const ID_COL_ID: ColumnId = ColumnId::new(2);
|
||||
const INSERT_COL_ID: ColumnId = ColumnId::new(3);
|
||||
const ACTION_COL_ID: ColumnId = ColumnId::new(4);
|
||||
const VAL_COL_ID: ColumnId = ColumnId::new(5);
|
||||
const SUCC_COL_ID: ColumnId = ColumnId::new(8);
|
||||
|
||||
let mut cols = vec![
|
||||
(ColumnSpec::new(OBJ_COL_ID, ColumnType::Actor, false), self.actor.clone().into()),
|
||||
(ColumnSpec::new(OBJ_COL_ID, ColumnType::Integer, false), self.ctr.clone().into()),
|
||||
(ColumnSpec::new(KEY_COL_ID, ColumnType::Actor, false), self.key_actor.clone().into()),
|
||||
(ColumnSpec::new(KEY_COL_ID, ColumnType::DeltaInteger, false), self.key_ctr.clone().into()),
|
||||
(ColumnSpec::new(KEY_COL_ID, ColumnType::String, false), self.key_str.clone().into()),
|
||||
(ColumnSpec::new(ID_COL_ID, ColumnType::Actor, false), self.id_actor.clone().into()),
|
||||
(ColumnSpec::new(ID_COL_ID, ColumnType::DeltaInteger, false), self.id_ctr.clone().into()),
|
||||
(ColumnSpec::new(INSERT_COL_ID, ColumnType::Boolean, false), self.insert.clone().into()),
|
||||
(ColumnSpec::new(ACTION_COL_ID, ColumnType::Integer, false), self.action.clone().into()),
|
||||
(ColumnSpec::new(VAL_COL_ID, ColumnType::ValueMetadata, false), self.val_meta.clone().into()),
|
||||
];
|
||||
if self.val_raw.len() > 0 {
|
||||
cols.push((
|
||||
ColumnSpec::new(VAL_COL_ID, ColumnType::Value, false), self.val_raw.clone().into()
|
||||
));
|
||||
}
|
||||
cols.push(
|
||||
(ColumnSpec::new(SUCC_COL_ID, ColumnType::Group, false), self.succ_group.clone().into()),
|
||||
);
|
||||
if self.succ_actor.len() > 0 {
|
||||
cols.extend([
|
||||
(ColumnSpec::new(SUCC_COL_ID, ColumnType::Actor, false), self.succ_actor.clone().into()),
|
||||
(ColumnSpec::new(SUCC_COL_ID, ColumnType::DeltaInteger, false), self.succ_ctr.clone().into()),
|
||||
]);
|
||||
}
|
||||
cols.into_iter().collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DocOpColumnIter<'a> {
|
||||
id: OpIdDecoder<'a>,
|
||||
action: RleDecoder<'a, u64>,
|
||||
objs: ObjDecoder<'a>,
|
||||
keys: KeyDecoder<'a>,
|
||||
insert: BooleanDecoder<'a>,
|
||||
value: ValueDecoder<'a>,
|
||||
succ: OpIdListDecoder<'a>,
|
||||
}
|
||||
|
||||
impl<'a> DocOpColumnIter<'a> {
|
||||
fn done(&self) -> bool {
|
||||
[
|
||||
self.id.done(),
|
||||
self.action.done(),
|
||||
self.objs.done(),
|
||||
self.keys.done(),
|
||||
self.insert.done(),
|
||||
self.value.done(),
|
||||
self.succ.done(),
|
||||
]
|
||||
.iter()
|
||||
.all(|c| *c)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum DecodeOpError {
|
||||
#[error("unexpected null in column {0}")]
|
||||
UnexpectedNull(String),
|
||||
#[error("invalid value in column {column}: {description}")]
|
||||
InvalidValue { column: String, description: String },
|
||||
}
|
||||
|
||||
macro_rules! next_or_invalid({$iter: expr, $col: literal} => {
|
||||
match $iter.next() {
|
||||
Some(Ok(id)) => id,
|
||||
Some(Err(e)) => match e {
|
||||
DecodeColumnError::UnexpectedNull(inner_col) => {
|
||||
return Some(Err(DecodeOpError::UnexpectedNull(format!(
|
||||
"{}:{}", $col, inner_col
|
||||
))));
|
||||
},
|
||||
DecodeColumnError::InvalidValue{column, description} => {
|
||||
let col = format!("{}:{}", $col, column);
|
||||
return Some(Err(DecodeOpError::InvalidValue{column: col, description}))
|
||||
}
|
||||
}
|
||||
None => return Some(Err(DecodeOpError::UnexpectedNull($col.to_string()))),
|
||||
}
|
||||
});
|
||||
|
||||
impl<'a> Iterator for DocOpColumnIter<'a> {
|
||||
type Item = Result<DocOp<'a>, DecodeOpError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.done() {
|
||||
None
|
||||
} else {
|
||||
let id = next_or_invalid!(self.id, "opid");
|
||||
let action = match self.action.next() {
|
||||
Some(Some(a)) => a,
|
||||
Some(None) | None => {
|
||||
return Some(Err(DecodeOpError::UnexpectedNull("action".to_string())))
|
||||
}
|
||||
};
|
||||
let obj = next_or_invalid!(self.objs, "obj").into();
|
||||
let key = next_or_invalid!(self.keys, "key");
|
||||
let value = next_or_invalid!(self.value, "value");
|
||||
let succ = next_or_invalid!(self.succ, "succ");
|
||||
let insert = self.insert.next().unwrap_or(false);
|
||||
Some(Ok(DocOp {
|
||||
id,
|
||||
value,
|
||||
action: action as usize,
|
||||
object: obj,
|
||||
key,
|
||||
succ,
|
||||
insert,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum Error {
|
||||
#[error("mismatching column at {index}.")]
|
||||
MismatchingColumn { index: usize },
|
||||
#[error("not enough columns")]
|
||||
NotEnoughColumns,
|
||||
}
|
||||
|
||||
impl From<MismatchingColumn> for Error {
|
||||
fn from(m: MismatchingColumn) -> Self {
|
||||
Error::MismatchingColumn { index: m.index }
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ColumnLayout> for DocOpColumns {
|
||||
type Error = Error;
|
||||
|
||||
#[instrument]
|
||||
fn try_from(columns: ColumnLayout) -> Result<Self, Self::Error> {
|
||||
let mut obj_actor: Option<Range<usize>> = None;
|
||||
let mut obj_ctr: Option<Range<usize>> = None;
|
||||
let mut key_actor: Option<Range<usize>> = None;
|
||||
let mut key_ctr: Option<Range<usize>> = None;
|
||||
let mut key_str: Option<Range<usize>> = None;
|
||||
let mut id_actor: Option<Range<usize>> = None;
|
||||
let mut id_ctr: Option<Range<usize>> = None;
|
||||
let mut insert: Option<Range<usize>> = None;
|
||||
let mut action: Option<Range<usize>> = None;
|
||||
let mut val_meta: Option<Range<usize>> = None;
|
||||
let mut val_raw: Option<Range<usize>> = None;
|
||||
let mut succ_group: Option<Range<usize>> = None;
|
||||
let mut succ_actor: Option<Range<usize>> = None;
|
||||
let mut succ_ctr: Option<Range<usize>> = None;
|
||||
let mut other = ColumnLayout::empty();
|
||||
|
||||
for (index, col) in columns.into_iter().enumerate() {
|
||||
match index {
|
||||
0 => assert_col_type(index, col, ColumnType::Actor, &mut obj_actor)?,
|
||||
1 => assert_col_type(index, col, ColumnType::Integer, &mut obj_ctr)?,
|
||||
2 => assert_col_type(index, col, ColumnType::Actor, &mut key_actor)?,
|
||||
3 => assert_col_type(index, col, ColumnType::DeltaInteger, &mut key_ctr)?,
|
||||
4 => assert_col_type(index, col, ColumnType::String, &mut key_str)?,
|
||||
5 => assert_col_type(index, col, ColumnType::Actor, &mut id_actor)?,
|
||||
6 => assert_col_type(index, col, ColumnType::DeltaInteger, &mut id_ctr)?,
|
||||
7 => assert_col_type(index, col, ColumnType::Boolean, &mut insert)?,
|
||||
8 => assert_col_type(index, col, ColumnType::Integer, &mut action)?,
|
||||
9 => match col.ranges() {
|
||||
ColumnRanges::Value { meta, val } => {
|
||||
val_meta = Some(meta);
|
||||
val_raw = Some(val);
|
||||
}
|
||||
_ => {
|
||||
tracing::error!("col 9 should be a value column");
|
||||
return Err(Error::MismatchingColumn { index });
|
||||
},
|
||||
},
|
||||
10 => match col.ranges() {
|
||||
ColumnRanges::Group { num, mut cols } => {
|
||||
let first = cols.next();
|
||||
let second = cols.next();
|
||||
succ_group = Some(num.into());
|
||||
match (first, second) {
|
||||
(
|
||||
Some(GroupColRange::Single(actor_range)),
|
||||
Some(GroupColRange::Single(ctr_range)),
|
||||
) => {
|
||||
succ_actor = Some(actor_range.into());
|
||||
succ_ctr = Some(ctr_range.into());
|
||||
},
|
||||
(None, None) => {
|
||||
succ_actor = Some((0..0).into());
|
||||
succ_ctr = Some((0..0).into());
|
||||
}
|
||||
_ => {
|
||||
tracing::error!("expected a two column group of (actor, rle int) for index 10");
|
||||
return Err(Error::MismatchingColumn { index });
|
||||
}
|
||||
};
|
||||
if let Some(_) = cols.next() {
|
||||
return Err(Error::MismatchingColumn { index });
|
||||
}
|
||||
}
|
||||
_ => return Err(Error::MismatchingColumn { index }),
|
||||
},
|
||||
_ => {
|
||||
other.append(col);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(DocOpColumns {
|
||||
actor: obj_actor.ok_or(Error::NotEnoughColumns)?.into(),
|
||||
ctr: obj_ctr.ok_or(Error::NotEnoughColumns)?.into(),
|
||||
key_actor: key_actor.ok_or(Error::NotEnoughColumns)?.into(),
|
||||
key_ctr: key_ctr.ok_or(Error::NotEnoughColumns)?.into(),
|
||||
key_str: key_str.ok_or(Error::NotEnoughColumns)?.into(),
|
||||
id_actor: id_actor.ok_or(Error::NotEnoughColumns)?.into(),
|
||||
id_ctr: id_ctr.ok_or(Error::NotEnoughColumns)?.into(),
|
||||
insert: insert.ok_or(Error::NotEnoughColumns)?.into(),
|
||||
action: action.ok_or(Error::NotEnoughColumns)?.into(),
|
||||
val_meta: val_meta.ok_or(Error::NotEnoughColumns)?.into(),
|
||||
val_raw: val_raw.ok_or(Error::NotEnoughColumns)?.into(),
|
||||
succ_group: succ_group.ok_or(Error::NotEnoughColumns)?.into(),
|
||||
succ_actor: succ_actor.ok_or(Error::NotEnoughColumns)?.into(),
|
||||
succ_ctr: succ_ctr.ok_or(Error::NotEnoughColumns)?.into(),
|
||||
other,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,92 +1,54 @@
|
|||
/// This module contains types which represent the column metadata which is encoded in the columnar
|
||||
/// storage format specified in [1]. In this format metadata about each column is packed into a 32
|
||||
/// bit integer, which is represented by the types in `column_specification`. The column data in
|
||||
/// the format is a sequence of (`ColumnSpecification`, `usize`) pairs where each pair represents
|
||||
/// the type of the column and the length of the column in the data which follows, these pairs are
|
||||
/// represented by `RawColumn` and `RawColumns`. Some columns are actually composites of several
|
||||
/// underlying columns and so not every `RawColumns` is valid. The types in `column` and
|
||||
/// `column_builder` take a `RawColumns` and produce a `Columns` - which is a valid set of possibly
|
||||
/// composite column metadata.
|
||||
///
|
||||
/// There are two typical workflows:
|
||||
///
|
||||
/// ## Reading
|
||||
/// * First parse a `RawColumns` from the underlying data using `RawColumns::parse`
|
||||
/// * Ensure that the columns are decompressed using `RawColumns::decompress` (checking first if
|
||||
/// you can avoid this using `RawColumns::uncompressed`)
|
||||
/// * Parse the `RawColumns` into a `Columns` using `Columns::parse`
|
||||
///
|
||||
/// ## Writing
|
||||
/// * Construct a `RawColumns`
|
||||
/// * Compress using `RawColumns::compress`
|
||||
/// * Write to output using `RawColumns::write`
|
||||
///
|
||||
/// [1]: https://alexjg.github.io/automerge-storage-docs/#_columnar_storage_format
|
||||
use std::ops::Range;
|
||||
|
||||
mod column_specification;
|
||||
pub(crate) use column_specification::{ColumnId, ColumnSpec, ColumnType};
|
||||
mod column;
|
||||
pub(crate) use column::Column;
|
||||
mod column_builder;
|
||||
pub(crate) use column_builder::{
|
||||
AwaitingRawColumnValueBuilder, ColumnBuilder, GroupAwaitingValue, GroupBuilder,
|
||||
use crate::columnar_2::{
|
||||
column_specification::{ColumnId, ColumnSpec, ColumnType},
|
||||
rowblock::column_layout::column::{
|
||||
AwaitingRawColumnValueBuilder, Column, ColumnBuilder, GroupAwaitingValue, GroupBuilder,
|
||||
},
|
||||
};
|
||||
|
||||
pub(crate) mod raw_column;
|
||||
pub(crate) use raw_column::{RawColumn, RawColumns};
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ColumnLayout(Vec<Column>);
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("mismatching column at {index}.")]
|
||||
pub(crate) struct MismatchingColumn {
|
||||
pub(crate) index: usize,
|
||||
}
|
||||
|
||||
pub(crate) mod compression {
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Unknown;
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Uncompressed;
|
||||
|
||||
/// A witness for what we know about whether or not a column is compressed
|
||||
pub(crate) trait ColumnCompression {}
|
||||
impl ColumnCompression for Unknown {}
|
||||
impl ColumnCompression for Uncompressed {}
|
||||
}
|
||||
|
||||
/// `Columns` represents a sequence of "logical" columns. "Logical" in this sense means that
|
||||
/// each column produces one value, but may be composed of multiple [`RawColumn`]s. For example, in a
|
||||
/// logical column containing values there are two `RawColumn`s, one for the metadata about the
|
||||
/// values, and one for the values themselves.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Columns {
|
||||
columns: Vec<Column>,
|
||||
}
|
||||
|
||||
impl Columns {
|
||||
pub(crate) fn empty() -> Self {
|
||||
Self {
|
||||
columns: Vec::new(),
|
||||
}
|
||||
impl ColumnLayout {
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = &Column> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
pub(crate) fn append(&mut self, col: Column) {
|
||||
self.columns.push(col)
|
||||
pub(crate) fn num_cols(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
pub(crate) fn parse<'a, I: Iterator<Item = &'a RawColumn<compression::Uncompressed>>>(
|
||||
pub(crate) fn parse<I: Iterator<Item = (ColumnSpec, Range<usize>)>>(
|
||||
data_size: usize,
|
||||
cols: I,
|
||||
) -> Result<Columns, BadColumnLayout> {
|
||||
) -> Result<ColumnLayout, BadColumnLayout> {
|
||||
let mut parser = ColumnLayoutParser::new(data_size, None);
|
||||
for raw_col in cols {
|
||||
parser.add_column(raw_col.spec(), raw_col.data())?;
|
||||
for (col, range) in cols {
|
||||
parser.add_column(col, range)?;
|
||||
}
|
||||
parser.build()
|
||||
}
|
||||
|
||||
pub(crate) fn as_specs(&self) -> impl Iterator<Item=(ColumnSpec, Range<usize>)> {
|
||||
panic!();
|
||||
std::iter::empty()
|
||||
}
|
||||
|
||||
pub(crate) fn empty() -> Self {
|
||||
Self(Vec::new())
|
||||
}
|
||||
|
||||
pub(crate) fn append(&mut self, col: Column) {
|
||||
self.0.push(col)
|
||||
}
|
||||
|
||||
pub(crate) fn unsafe_from_vec(v: Vec<Column>) -> Self {
|
||||
Self(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<Column> for Result<Columns, BadColumnLayout> {
|
||||
impl FromIterator<Column> for Result<ColumnLayout, BadColumnLayout> {
|
||||
fn from_iter<T: IntoIterator<Item = Column>>(iter: T) -> Self {
|
||||
let iter = iter.into_iter();
|
||||
let mut result = Vec::with_capacity(iter.size_hint().1.unwrap_or(0));
|
||||
|
@ -100,16 +62,16 @@ impl FromIterator<Column> for Result<Columns, BadColumnLayout> {
|
|||
last_column = Some(col.spec());
|
||||
result.push(col);
|
||||
}
|
||||
Ok(Columns { columns: result })
|
||||
Ok(ColumnLayout(result))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for Columns {
|
||||
impl IntoIterator for ColumnLayout {
|
||||
type Item = Column;
|
||||
type IntoIter = std::vec::IntoIter<Column>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.columns.into_iter()
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,12 +122,12 @@ impl ColumnLayoutParser {
|
|||
}
|
||||
}
|
||||
|
||||
fn build(mut self) -> Result<Columns, BadColumnLayout> {
|
||||
let columns = match self.state {
|
||||
LayoutParserState::Ready => self.columns,
|
||||
fn build(mut self) -> Result<ColumnLayout, BadColumnLayout> {
|
||||
match self.state {
|
||||
LayoutParserState::Ready => Ok(ColumnLayout(self.columns)),
|
||||
LayoutParserState::InValue(mut builder) => {
|
||||
self.columns.push(builder.build((0..0).into()));
|
||||
self.columns
|
||||
self.columns.push(builder.build(0..0));
|
||||
Ok(ColumnLayout(self.columns))
|
||||
}
|
||||
LayoutParserState::InGroup(_, groupstate) => {
|
||||
match groupstate {
|
||||
|
@ -176,13 +138,11 @@ impl ColumnLayoutParser {
|
|||
self.columns.push(builder.finish());
|
||||
}
|
||||
};
|
||||
self.columns
|
||||
Ok(ColumnLayout(self.columns))
|
||||
}
|
||||
};
|
||||
Ok(Columns { columns })
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self), err)]
|
||||
fn add_column(
|
||||
&mut self,
|
||||
column: ColumnSpec,
|
||||
|
@ -202,41 +162,38 @@ impl ColumnLayoutParser {
|
|||
ColumnType::Group => {
|
||||
self.state = LayoutParserState::InGroup(
|
||||
column.id(),
|
||||
GroupParseState::Ready(ColumnBuilder::start_group(column, range.into())),
|
||||
GroupParseState::Ready(ColumnBuilder::start_group(column, range)),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
ColumnType::ValueMetadata => {
|
||||
self.state = LayoutParserState::InValue(ColumnBuilder::start_value(
|
||||
column,
|
||||
range.into(),
|
||||
));
|
||||
self.state =
|
||||
LayoutParserState::InValue(ColumnBuilder::start_value(column, range));
|
||||
Ok(())
|
||||
}
|
||||
ColumnType::Value => Err(BadColumnLayout::LoneRawValueColumn),
|
||||
ColumnType::Actor => {
|
||||
self.columns
|
||||
.push(ColumnBuilder::build_actor(column, range.into()));
|
||||
self.columns.push(ColumnBuilder::build_actor(column, range));
|
||||
Ok(())
|
||||
}
|
||||
ColumnType::String => {
|
||||
self.columns
|
||||
.push(ColumnBuilder::build_string(column, range.into()));
|
||||
.push(ColumnBuilder::build_string(column, range));
|
||||
Ok(())
|
||||
}
|
||||
ColumnType::Integer => {
|
||||
self.columns
|
||||
.push(ColumnBuilder::build_integer(column, range.into()));
|
||||
.push(ColumnBuilder::build_integer(column, range));
|
||||
Ok(())
|
||||
}
|
||||
ColumnType::DeltaInteger => {
|
||||
self.columns
|
||||
.push(ColumnBuilder::build_delta_integer(column, range.into()));
|
||||
.push(ColumnBuilder::build_delta_integer(column, range));
|
||||
Ok(())
|
||||
}
|
||||
ColumnType::Boolean => {
|
||||
self.columns
|
||||
.push(ColumnBuilder::build_boolean(column, range.into()));
|
||||
.push(ColumnBuilder::build_boolean(column, range));
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
|
@ -245,12 +202,12 @@ impl ColumnLayoutParser {
|
|||
if builder.id() != column.id() {
|
||||
return Err(BadColumnLayout::MismatchingValueMetadataId);
|
||||
}
|
||||
self.columns.push(builder.build(range.into()));
|
||||
self.columns.push(builder.build(range));
|
||||
self.state = LayoutParserState::Ready;
|
||||
Ok(())
|
||||
}
|
||||
_ => {
|
||||
self.columns.push(builder.build((0..0).into()));
|
||||
self.columns.push(builder.build(0..0));
|
||||
self.state = LayoutParserState::Ready;
|
||||
self.add_column(column, range)
|
||||
}
|
||||
|
@ -315,7 +272,6 @@ impl ColumnLayoutParser {
|
|||
LayoutParserState::Ready => {
|
||||
if let Some(prev) = self.columns.last() {
|
||||
if prev.range().end != next_range.start {
|
||||
tracing::error!(prev=?prev.range(), next=?next_range, "it's here");
|
||||
Err(BadColumnLayout::NonContiguousColumns)
|
||||
} else {
|
||||
Ok(())
|
||||
|
@ -325,7 +281,7 @@ impl ColumnLayoutParser {
|
|||
}
|
||||
}
|
||||
LayoutParserState::InValue(builder) => {
|
||||
if builder.meta_range().end() != next_range.start {
|
||||
if builder.meta_range().end != next_range.start {
|
||||
Err(BadColumnLayout::NonContiguousColumns)
|
||||
} else {
|
||||
Ok(())
|
||||
|
@ -337,6 +293,7 @@ impl ColumnLayoutParser {
|
|||
GroupParseState::Ready(b) => b.range().end,
|
||||
};
|
||||
if end != next_range.start {
|
||||
println!("Group state: {:?}", group_state);
|
||||
Err(BadColumnLayout::NonContiguousColumns)
|
||||
} else {
|
||||
Ok(())
|
53
automerge/src/columnar_2/rowblock/column_layout/mod.rs
Normal file
53
automerge/src/columnar_2/rowblock/column_layout/mod.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
use std::ops::Range;
|
||||
|
||||
pub(crate) mod column;
|
||||
pub(crate) mod generic;
|
||||
pub(crate) mod doc_op_columns;
|
||||
pub(crate) mod doc_change_columns;
|
||||
pub(crate) mod change_op_columns;
|
||||
|
||||
pub(crate) use generic::{BadColumnLayout, ColumnLayout};
|
||||
pub(crate) use doc_op_columns::{DocOpColumns, Error as ParseDocColumnError};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum ColumnSpliceError {
|
||||
#[error("invalid value for row {0}")]
|
||||
InvalidValueForRow(usize),
|
||||
#[error("wrong number of values for row {0}, expected {expected} but got {actual}")]
|
||||
WrongNumberOfValues {
|
||||
row: usize,
|
||||
expected: usize,
|
||||
actual: usize,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("mismatching column at {index}.")]
|
||||
struct MismatchingColumn {
|
||||
index: usize,
|
||||
}
|
||||
|
||||
/// Given a `column::Column` assert that it is of the given `typ` and if so update `target` to be
|
||||
/// `Some(range)`. Otherwise return a `MismatchingColumn{index}`
|
||||
fn assert_col_type(
|
||||
index: usize,
|
||||
col: column::Column,
|
||||
typ: crate::columnar_2::column_specification::ColumnType,
|
||||
target: &mut Option<Range<usize>>,
|
||||
) -> Result<(), MismatchingColumn> {
|
||||
if col.col_type() == typ {
|
||||
match col.ranges() {
|
||||
column::ColumnRanges::Single(range) => {
|
||||
*target = Some(range);
|
||||
Ok(())
|
||||
},
|
||||
_ => {
|
||||
tracing::error!("expected a single column range");
|
||||
return Err(MismatchingColumn{ index });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::error!(index, expected=?typ, actual=?col.col_type(), "unexpected columnt type");
|
||||
Err(MismatchingColumn { index })
|
||||
}
|
||||
}
|
60
automerge/src/columnar_2/rowblock/column_range.rs
Normal file
60
automerge/src/columnar_2/rowblock/column_range.rs
Normal file
|
@ -0,0 +1,60 @@
|
|||
use std::{borrow::Cow, ops::Range};
|
||||
|
||||
use smol_str::SmolStr;
|
||||
|
||||
use super::encoding::{
|
||||
BooleanDecoder, BooleanEncoder, DeltaDecoder, DeltaEncoder, RawDecoder, RawEncoder,
|
||||
RleDecoder, RleEncoder,
|
||||
};
|
||||
|
||||
macro_rules! make_col_range({$name: ident, $decoder_name: ident$(<$($dparam: tt),+>)?, $encoder_name: ident$(<$($eparam: tt),+>)?} => {
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct $name(Range<usize>);
|
||||
|
||||
impl $name {
|
||||
pub(crate) fn decoder<'a>(&self, data: &'a[u8]) -> $decoder_name $(<$($dparam,)+>)* {
|
||||
$decoder_name::from(Cow::Borrowed(&data[self.0.clone()]))
|
||||
}
|
||||
|
||||
pub(crate) fn encoder<'a>(&self, output: &'a mut Vec<u8>) -> $encoder_name $(<$($eparam,)+>)* {
|
||||
$encoder_name::from(output)
|
||||
}
|
||||
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Range<usize>> for $name {
|
||||
fn as_ref(&self) -> &Range<usize> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Range<usize>> for $name {
|
||||
fn from(r: Range<usize>) -> $name {
|
||||
$name(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<$name> for Range<usize> {
|
||||
fn from(r: $name) -> Range<usize> {
|
||||
r.0
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
make_col_range!(ActorRange, RleDecoder<'a, u64>, RleEncoder<'a, u64>);
|
||||
make_col_range!(RleIntRange, RleDecoder<'a, u64>, RleEncoder<'a, u64>);
|
||||
make_col_range!(DeltaIntRange, DeltaDecoder<'a>, DeltaEncoder<'a>);
|
||||
make_col_range!(
|
||||
RleStringRange,
|
||||
RleDecoder<'a, SmolStr>,
|
||||
RleEncoder<'a, SmolStr>
|
||||
);
|
||||
make_col_range!(BooleanRange, BooleanDecoder<'a>, BooleanEncoder<'a>);
|
||||
make_col_range!(RawRange, RawDecoder<'a>, RawEncoder<'a>);
|
163
automerge/src/columnar_2/rowblock/encoding/boolean.rs
Normal file
163
automerge/src/columnar_2/rowblock/encoding/boolean.rs
Normal file
|
@ -0,0 +1,163 @@
|
|||
use std::{borrow::Cow, ops::Range};
|
||||
|
||||
use super::{Encodable, RawDecoder};
|
||||
|
||||
/// 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<'a> {
|
||||
written: usize,
|
||||
buf: &'a mut Vec<u8>,
|
||||
last: bool,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl<'a> BooleanEncoder<'a> {
|
||||
pub fn new(output: &'a mut Vec<u8>) -> BooleanEncoder<'a> {
|
||||
BooleanEncoder {
|
||||
written: 0,
|
||||
buf: output,
|
||||
last: false,
|
||||
count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append(&mut self, value: bool) {
|
||||
if value == self.last {
|
||||
self.count += 1;
|
||||
} else {
|
||||
self.written += self.count.encode(&mut self.buf);
|
||||
self.last = value;
|
||||
self.count = 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(mut self) -> usize {
|
||||
if self.count > 0 {
|
||||
self.written += self.count.encode(&mut self.buf);
|
||||
}
|
||||
self.written
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a mut Vec<u8>> for BooleanEncoder<'a> {
|
||||
fn from(output: &'a mut Vec<u8>) -> Self {
|
||||
BooleanEncoder::new(output)
|
||||
}
|
||||
}
|
||||
|
||||
/// See the discussion of [`BooleanEncoder`] for details on this encoding
|
||||
pub(crate) struct BooleanDecoder<'a> {
|
||||
decoder: RawDecoder<'a>,
|
||||
last_value: bool,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl<'a> BooleanDecoder<'a> {
|
||||
pub(crate) fn done(&self) -> bool {
|
||||
self.decoder.done()
|
||||
}
|
||||
|
||||
pub(crate) fn splice<I: Iterator<Item=bool>>(&mut self, replace: Range<usize>, mut replace_with: I, out: &mut Vec<u8>) -> Range<usize> {
|
||||
let start = out.len();
|
||||
let mut encoder = BooleanEncoder::new(out);
|
||||
let mut idx = 0;
|
||||
while idx < replace.start {
|
||||
match self.next() {
|
||||
Some(elem) => encoder.append(elem),
|
||||
None => panic!("out of bounds"),
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
for _ in 0..replace.len() {
|
||||
self.next();
|
||||
if let Some(next) = replace_with.next() {
|
||||
encoder.append(next);
|
||||
}
|
||||
}
|
||||
while let Some(next) = replace_with.next() {
|
||||
encoder.append(next);
|
||||
}
|
||||
while let Some(next) = self.next() {
|
||||
encoder.append(next);
|
||||
}
|
||||
start..(start + encoder.finish())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Cow<'a, [u8]>> for BooleanDecoder<'a> {
|
||||
fn from(bytes: Cow<'a, [u8]>) -> Self {
|
||||
BooleanDecoder {
|
||||
decoder: RawDecoder::from(bytes),
|
||||
last_value: true,
|
||||
count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [u8]> for BooleanDecoder<'a> {
|
||||
fn from(d: &'a [u8]) -> Self {
|
||||
Cow::Borrowed(d).into()
|
||||
}
|
||||
}
|
||||
|
||||
// this is an endless iterator that returns false after input is exhausted
|
||||
impl<'a> Iterator for BooleanDecoder<'a> {
|
||||
type Item = bool;
|
||||
|
||||
fn next(&mut self) -> Option<bool> {
|
||||
while self.count == 0 {
|
||||
if self.decoder.done() && self.count == 0 {
|
||||
return None;
|
||||
}
|
||||
self.count = self.decoder.read().unwrap_or_default();
|
||||
self.last_value = !self.last_value;
|
||||
}
|
||||
self.count -= 1;
|
||||
Some(self.last_value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::columnar_2::rowblock::encoding::properties::splice_scenario;
|
||||
|
||||
use proptest::prelude::*;
|
||||
|
||||
fn encode(vals: &[bool]) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
let mut encoder = BooleanEncoder::new(&mut buf);
|
||||
for val in vals {
|
||||
encoder.append(*val);
|
||||
}
|
||||
encoder.finish();
|
||||
buf
|
||||
}
|
||||
|
||||
fn decode(buf: &[u8]) -> Vec<bool> {
|
||||
BooleanDecoder::from(buf).collect()
|
||||
}
|
||||
|
||||
proptest!{
|
||||
#[test]
|
||||
fn encode_decode_bools(vals in proptest::collection::vec(any::<bool>(), 0..100)) {
|
||||
assert_eq!(vals, decode(&encode(&vals)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn splice_bools(scenario in splice_scenario(any::<bool>())) {
|
||||
let encoded = encode(&scenario.initial_values);
|
||||
let mut decoder = BooleanDecoder::from(&encoded[..]);
|
||||
let mut out = Vec::new();
|
||||
let r = decoder.splice(scenario.replace_range.clone(), scenario.replacements.iter().copied(), &mut out);
|
||||
let result = decode(&out);
|
||||
scenario.check(result);
|
||||
assert_eq!(r.len(), out.len());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
158
automerge/src/columnar_2/rowblock/encoding/decodable_impls.rs
Normal file
158
automerge/src/columnar_2/rowblock/encoding/decodable_impls.rs
Normal file
|
@ -0,0 +1,158 @@
|
|||
use std::{borrow::Cow, convert::TryFrom, str, io::Read};
|
||||
use smol_str::SmolStr;
|
||||
|
||||
use super::Decodable;
|
||||
use crate::ActorId;
|
||||
|
||||
impl Decodable for u8 {
|
||||
fn decode<R>(bytes: &mut R) -> Option<Self>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
let mut buffer = [0; 1];
|
||||
bytes.read_exact(&mut buffer).ok()?;
|
||||
Some(buffer[0])
|
||||
}
|
||||
}
|
||||
|
||||
impl Decodable for u32 {
|
||||
fn decode<R>(bytes: &mut R) -> Option<Self>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
u64::decode::<R>(bytes).and_then(|val| Self::try_from(val).ok())
|
||||
}
|
||||
}
|
||||
|
||||
impl Decodable for usize {
|
||||
fn decode<R>(bytes: &mut R) -> Option<Self>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
u64::decode::<R>(bytes).and_then(|val| Self::try_from(val).ok())
|
||||
}
|
||||
}
|
||||
|
||||
impl Decodable for isize {
|
||||
fn decode<R>(bytes: &mut R) -> Option<Self>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
i64::decode::<R>(bytes).and_then(|val| Self::try_from(val).ok())
|
||||
}
|
||||
}
|
||||
|
||||
impl Decodable for i32 {
|
||||
fn decode<R>(bytes: &mut R) -> Option<Self>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
i64::decode::<R>(bytes).and_then(|val| Self::try_from(val).ok())
|
||||
}
|
||||
}
|
||||
|
||||
impl Decodable for i64 {
|
||||
fn decode<R>(bytes: &mut R) -> Option<Self>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
leb128::read::signed(bytes).ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl Decodable for f64 {
|
||||
fn decode<R>(bytes: &mut R) -> Option<Self>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
let mut buffer = [0; 8];
|
||||
bytes.read_exact(&mut buffer).ok()?;
|
||||
Some(Self::from_le_bytes(buffer))
|
||||
}
|
||||
}
|
||||
|
||||
impl Decodable for f32 {
|
||||
fn decode<R>(bytes: &mut R) -> Option<Self>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
let mut buffer = [0; 4];
|
||||
bytes.read_exact(&mut buffer).ok()?;
|
||||
Some(Self::from_le_bytes(buffer))
|
||||
}
|
||||
}
|
||||
|
||||
impl Decodable for u64 {
|
||||
fn decode<R>(bytes: &mut R) -> Option<Self>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
leb128::read::unsigned(bytes).ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl Decodable for Vec<u8> {
|
||||
fn decode<R>(bytes: &mut R) -> Option<Self>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
let len = usize::decode::<R>(bytes)?;
|
||||
if len == 0 {
|
||||
return Some(vec![]);
|
||||
}
|
||||
let mut buffer = vec![0; len];
|
||||
bytes.read_exact(buffer.as_mut_slice()).ok()?;
|
||||
Some(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decodable for SmolStr {
|
||||
fn decode<R>(bytes: &mut R) -> Option<SmolStr>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
let buffer = Vec::decode(bytes)?;
|
||||
str::from_utf8(&buffer).map(|t| t.into()).ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl Decodable for Cow<'static, SmolStr> {
|
||||
fn decode<R>(bytes: &mut R) -> Option<Self>
|
||||
where
|
||||
R: std::io::Read {
|
||||
SmolStr::decode(bytes).map(|s| Cow::Owned(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl Decodable for String {
|
||||
fn decode<R>(bytes: &mut R) -> Option<String>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
let buffer = Vec::decode(bytes)?;
|
||||
str::from_utf8(&buffer).map(|t| t.into()).ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl Decodable for Option<String> {
|
||||
fn decode<R>(bytes: &mut R) -> Option<Self>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
let buffer = Vec::decode(bytes)?;
|
||||
if buffer.is_empty() {
|
||||
return Some(None);
|
||||
}
|
||||
Some(str::from_utf8(&buffer).map(|t| t.into()).ok())
|
||||
}
|
||||
}
|
||||
|
||||
impl Decodable for ActorId {
|
||||
fn decode<R>(bytes: &mut R) -> Option<Self>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
let buffer = Vec::decode(bytes)?;
|
||||
Some(buffer.into())
|
||||
}
|
||||
}
|
169
automerge/src/columnar_2/rowblock/encoding/delta.rs
Normal file
169
automerge/src/columnar_2/rowblock/encoding/delta.rs
Normal file
|
@ -0,0 +1,169 @@
|
|||
use std::{borrow::Cow, ops::Range};
|
||||
|
||||
use super::{RleEncoder, RleDecoder};
|
||||
|
||||
/// 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<'a> {
|
||||
rle: RleEncoder<'a, i64>,
|
||||
absolute_value: i64,
|
||||
}
|
||||
|
||||
impl<'a> DeltaEncoder<'a> {
|
||||
pub fn new(output: &'a mut Vec<u8>) -> DeltaEncoder<'a> {
|
||||
DeltaEncoder {
|
||||
rle: RleEncoder::new(output),
|
||||
absolute_value: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append_value(&mut self, value: i64) {
|
||||
self.rle
|
||||
.append_value(&(value.saturating_sub(self.absolute_value)));
|
||||
self.absolute_value = value;
|
||||
}
|
||||
|
||||
pub fn append_null(&mut self) {
|
||||
self.rle.append_null();
|
||||
}
|
||||
|
||||
pub fn append(&mut self, val: Option<i64>) {
|
||||
match val {
|
||||
Some(v) => self.append_value(v),
|
||||
None => self.append_null(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(self) -> usize {
|
||||
self.rle.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a mut Vec<u8>> for DeltaEncoder<'a> {
|
||||
fn from(output: &'a mut Vec<u8>) -> Self {
|
||||
DeltaEncoder::new(output)
|
||||
}
|
||||
}
|
||||
|
||||
/// See discussion on [`DeltaEncoder`] for the format data is stored in.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DeltaDecoder<'a> {
|
||||
rle: RleDecoder<'a, i64>,
|
||||
absolute_val: i64,
|
||||
}
|
||||
|
||||
impl<'a> DeltaDecoder<'a> {
|
||||
pub(crate) fn done(&self) -> bool {
|
||||
self.rle.done()
|
||||
}
|
||||
|
||||
pub(crate) fn encode<I>(items: I, out: &mut Vec<u8>) -> Range<usize>
|
||||
where
|
||||
I: Iterator<Item=i64>
|
||||
{
|
||||
let mut decoder = DeltaDecoder::from(&[] as &[u8]);
|
||||
decoder.splice(0..0, items.map(Some), out)
|
||||
}
|
||||
|
||||
pub(crate) fn splice<I: Iterator<Item=Option<i64>>>(&mut self, replace: Range<usize>, mut replace_with: I, out: &mut Vec<u8>) -> Range<usize> {
|
||||
let start = out.len();
|
||||
let mut encoder = DeltaEncoder::new(out);
|
||||
let mut idx = 0;
|
||||
while idx < replace.start {
|
||||
match self.next() {
|
||||
Some(elem) => encoder.append(elem),
|
||||
None => panic!("out of bounds"),
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
for _ in 0..replace.len() {
|
||||
self.next();
|
||||
if let Some(next) = replace_with.next() {
|
||||
encoder.append(next);
|
||||
}
|
||||
}
|
||||
while let Some(next) = replace_with.next() {
|
||||
encoder.append(next);
|
||||
}
|
||||
while let Some(next) = self.next() {
|
||||
encoder.append(next);
|
||||
}
|
||||
start..(start + encoder.finish())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Cow<'a, [u8]>> for DeltaDecoder<'a> {
|
||||
fn from(bytes: Cow<'a, [u8]>) -> Self {
|
||||
DeltaDecoder {
|
||||
rle: RleDecoder::from(bytes),
|
||||
absolute_val: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [u8]> for DeltaDecoder<'a> {
|
||||
fn from(d: &'a [u8]) -> Self {
|
||||
Cow::Borrowed(d).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for DeltaDecoder<'a> {
|
||||
type Item = Option<i64>;
|
||||
|
||||
fn next(&mut self) -> Option<Option<i64>> {
|
||||
match self.rle.next() {
|
||||
Some(Some(delta)) => {
|
||||
self.absolute_val = self.absolute_val.saturating_add(delta);
|
||||
Some(Some(self.absolute_val))
|
||||
},
|
||||
Some(None) => Some(None),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
use crate::columnar_2::rowblock::encoding::properties::splice_scenario;
|
||||
|
||||
fn encode(vals: &[Option<i64>]) -> Vec<u8> {
|
||||
let mut buf = Vec::<u8>::new();
|
||||
let mut encoder = DeltaEncoder::from(&mut buf);
|
||||
for val in vals {
|
||||
encoder.append(val.clone());
|
||||
}
|
||||
encoder.finish();
|
||||
buf
|
||||
}
|
||||
|
||||
fn decode(buf: &[u8]) -> Vec<Option<i64>> {
|
||||
DeltaDecoder::from(buf).collect()
|
||||
}
|
||||
|
||||
fn encodable_int() -> impl Strategy<Value = i64> + Clone {
|
||||
0..(i64::MAX / 2)
|
||||
}
|
||||
|
||||
proptest!{
|
||||
#[test]
|
||||
fn encode_decode_delta(vals in proptest::collection::vec(proptest::option::of(encodable_int()), 0..100)) {
|
||||
assert_eq!(vals, decode(&encode(&vals)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn splice_delta(scenario in splice_scenario(proptest::option::of(encodable_int()))) {
|
||||
let encoded = encode(&scenario.initial_values);
|
||||
let mut decoder = DeltaDecoder::from(&encoded[..]);
|
||||
let mut out = Vec::new();
|
||||
let r = decoder.splice(scenario.replace_range.clone(), scenario.replacements.iter().cloned(), &mut out);
|
||||
let decoded = decode(&out[..]);
|
||||
scenario.check(decoded);
|
||||
assert_eq!(r.len(), out.len());
|
||||
}
|
||||
}
|
||||
}
|
142
automerge/src/columnar_2/rowblock/encoding/encodable_impls.rs
Normal file
142
automerge/src/columnar_2/rowblock/encoding/encodable_impls.rs
Normal file
|
@ -0,0 +1,142 @@
|
|||
use super::Encodable;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use smol_str::SmolStr;
|
||||
use std::io::Write;
|
||||
|
||||
/// Encodes bytes without a length prefix
|
||||
pub(crate) struct RawBytes<'a>(Cow<'a, [u8]>);
|
||||
|
||||
impl<'a> From<&'a [u8]> for RawBytes<'a> {
|
||||
fn from(r: &'a [u8]) -> Self {
|
||||
RawBytes(r.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Cow<'a, [u8]>> for RawBytes<'a> {
|
||||
fn from(c: Cow<'a, [u8]>) -> Self {
|
||||
RawBytes(c)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Encodable for RawBytes<'a> {
|
||||
fn encode(&self, out: &mut Vec<u8>) -> usize {
|
||||
out.write_all(&self.0).unwrap();
|
||||
self.0.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for SmolStr {
|
||||
fn encode(&self, buf: &mut Vec<u8>) -> usize {
|
||||
let bytes = self.as_bytes();
|
||||
let len_encoded = bytes.len().encode(buf);
|
||||
let data_len = bytes.encode(buf);
|
||||
len_encoded + data_len
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Encodable for Cow<'a, SmolStr> {
|
||||
fn encode(&self, buf: &mut Vec<u8>) -> usize {
|
||||
self.as_ref().encode(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for String {
|
||||
fn encode(&self, buf: &mut Vec<u8>) ->usize {
|
||||
let bytes = self.as_bytes();
|
||||
let len_encoded = bytes.len().encode(buf);
|
||||
let data_len = bytes.encode(buf);
|
||||
len_encoded + data_len
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for Option<String> {
|
||||
fn encode(&self, buf: &mut Vec<u8>) -> usize {
|
||||
if let Some(s) = self {
|
||||
s.encode(buf)
|
||||
} else {
|
||||
0.encode(buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Encodable for Option<Cow<'a, SmolStr>> {
|
||||
fn encode(&self, out: &mut Vec<u8>) -> usize {
|
||||
if let Some(s) = self {
|
||||
SmolStr::encode(s, out)
|
||||
} else {
|
||||
0.encode(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for u64 {
|
||||
fn encode(&self, buf: &mut Vec<u8>) -> usize{
|
||||
leb128::write::unsigned(buf, *self).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for f64 {
|
||||
fn encode(&self, buf: &mut Vec<u8>) -> usize {
|
||||
let bytes = self.to_le_bytes();
|
||||
buf.write_all(&bytes).unwrap();
|
||||
bytes.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for f32 {
|
||||
fn encode(&self, buf: &mut Vec<u8>) -> usize {
|
||||
let bytes = self.to_le_bytes();
|
||||
buf.write_all(&bytes).unwrap();
|
||||
bytes.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for i64 {
|
||||
fn encode(&self, buf: &mut Vec<u8>) -> usize {
|
||||
leb128::write::signed(buf, *self).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for usize {
|
||||
fn encode(&self, buf: &mut Vec<u8>) -> usize{
|
||||
(*self as u64).encode(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for u32 {
|
||||
fn encode(&self, buf: &mut Vec<u8>) -> usize {
|
||||
u64::from(*self).encode(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for i32 {
|
||||
fn encode(&self, buf: &mut Vec<u8>) -> usize {
|
||||
i64::from(*self).encode(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for [u8] {
|
||||
fn encode(&self, out: &mut Vec<u8>) -> usize {
|
||||
out.write(self).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for &[u8] {
|
||||
fn encode(&self, out: &mut Vec<u8>) -> usize {
|
||||
out.write(self).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Encodable for Cow<'a, [u8]> {
|
||||
fn encode(&self, out: &mut Vec<u8>) -> usize {
|
||||
out.write(self).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Encodable for Vec<u8> {
|
||||
fn encode(&self, out: &mut Vec<u8>) -> usize {
|
||||
Encodable::encode(&self[..], out)
|
||||
}
|
||||
}
|
181
automerge/src/columnar_2/rowblock/encoding/generic.rs
Normal file
181
automerge/src/columnar_2/rowblock/encoding/generic.rs
Normal file
|
@ -0,0 +1,181 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
use crate::columnar_2::rowblock::{column_layout::ColumnSpliceError, value::CellValue};
|
||||
|
||||
use super::{
|
||||
BooleanDecoder, DecodeColumnError, DeltaDecoder, RleDecoder,
|
||||
ValueDecoder,
|
||||
};
|
||||
|
||||
pub(crate) enum SimpleColDecoder<'a> {
|
||||
RleUint(RleDecoder<'a, u64>),
|
||||
RleString(RleDecoder<'a, smol_str::SmolStr>),
|
||||
Delta(DeltaDecoder<'a>),
|
||||
Bool(BooleanDecoder<'a>),
|
||||
}
|
||||
|
||||
impl<'a> SimpleColDecoder<'a> {
|
||||
pub(crate) fn new_uint(d: RleDecoder<'a, u64>) -> Self {
|
||||
Self::RleUint(d)
|
||||
}
|
||||
|
||||
pub(crate) fn new_string(d: RleDecoder<'a, smol_str::SmolStr>) -> Self {
|
||||
Self::RleString(d)
|
||||
}
|
||||
|
||||
pub(crate) fn new_delta(d: DeltaDecoder<'a>) -> Self {
|
||||
Self::Delta(d)
|
||||
}
|
||||
|
||||
pub(crate) fn new_bool(d: BooleanDecoder<'a>) -> Self {
|
||||
Self::Bool(d)
|
||||
}
|
||||
|
||||
pub(crate) fn done(&self) -> bool {
|
||||
match self {
|
||||
Self::RleUint(d) => d.done(),
|
||||
Self::RleString(d) => d.done(),
|
||||
Self::Delta(d) => d.done(),
|
||||
Self::Bool(d) => d.done(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn next(&mut self) -> Option<CellValue<'a>> {
|
||||
match self {
|
||||
Self::RleUint(d) => d.next().and_then(|i| i.map(CellValue::Uint)),
|
||||
Self::RleString(d) => d
|
||||
.next()
|
||||
.and_then(|s| s.map(|s| CellValue::String(Cow::Owned(s.into())))),
|
||||
Self::Delta(d) => d.next().and_then(|i| i.map(CellValue::Int)),
|
||||
Self::Bool(d) => d.next().map(CellValue::Bool),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn splice<'b, I>(
|
||||
&mut self,
|
||||
out: &mut Vec<u8>,
|
||||
replace: Range<usize>,
|
||||
replace_with: I,
|
||||
) -> Result<usize, ColumnSpliceError>
|
||||
where
|
||||
I: Iterator<Item=CellValue<'b>> + Clone
|
||||
{
|
||||
// Requires `try_splice` methods on all the basic decoders so that we can report an error
|
||||
// if the cellvalue types don't match up
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub(crate) enum SingleLogicalColDecoder<'a> {
|
||||
Simple(SimpleColDecoder<'a>),
|
||||
Value(ValueDecoder<'a>),
|
||||
}
|
||||
|
||||
impl<'a> Iterator for SingleLogicalColDecoder<'a> {
|
||||
type Item = Result<CellValue<'a>, DecodeColumnError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self {
|
||||
Self::Simple(s) => s.next().map(Ok),
|
||||
Self::Value(v) => v.next().map(|v| v.map(|v| CellValue::Value(v))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum GenericColDecoder<'a> {
|
||||
Simple(SimpleColDecoder<'a>),
|
||||
Value(ValueDecoder<'a>),
|
||||
Group(GroupDecoder<'a>),
|
||||
}
|
||||
|
||||
impl<'a> GenericColDecoder<'a> {
|
||||
pub(crate) fn new_simple(s: SimpleColDecoder<'a>) -> Self {
|
||||
Self::Simple(s)
|
||||
}
|
||||
|
||||
pub(crate) fn new_value(v: ValueDecoder<'a>) -> Self {
|
||||
Self::Value(v)
|
||||
}
|
||||
|
||||
pub(crate) fn new_group(g: GroupDecoder<'a>) -> Self {
|
||||
Self::Group(g)
|
||||
}
|
||||
|
||||
pub(crate) fn done(&self) -> bool {
|
||||
match self {
|
||||
Self::Simple(s) => s.done(),
|
||||
Self::Group(g) => g.done(),
|
||||
Self::Value(v) => v.done(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn next(&mut self) -> Option<Result<CellValue<'a>, DecodeColumnError>> {
|
||||
match self {
|
||||
Self::Simple(s) => s.next().map(Ok),
|
||||
Self::Value(v) => v.next().map(|v| v.map(|v| CellValue::Value(v))),
|
||||
Self::Group(g) => g.next().map(|v| v.map(|v| CellValue::List(v))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for GenericColDecoder<'a> {
|
||||
type Item = Result<CellValue<'a>, DecodeColumnError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
GenericColDecoder::next(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct GroupDecoder<'a> {
|
||||
num: RleDecoder<'a, u64>,
|
||||
values: Vec<SingleLogicalColDecoder<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> GroupDecoder<'a> {
|
||||
pub(crate) fn new(
|
||||
num: RleDecoder<'a, u64>,
|
||||
values: Vec<SingleLogicalColDecoder<'a>>,
|
||||
) -> GroupDecoder<'a> {
|
||||
GroupDecoder { num, values }
|
||||
}
|
||||
|
||||
fn next(&mut self) -> Option<Result<Vec<Vec<CellValue<'a>>>, DecodeColumnError>> {
|
||||
match self.num.next() {
|
||||
Some(Some(num_rows)) => {
|
||||
let mut result = Vec::with_capacity(num_rows as usize);
|
||||
for _ in 0..num_rows {
|
||||
let mut row = Vec::with_capacity(self.values.len());
|
||||
for (index, column) in self.values.iter_mut().enumerate() {
|
||||
match column.next() {
|
||||
Some(Ok(v)) => row.push(v),
|
||||
Some(Err(e)) => {
|
||||
return Some(Err(DecodeColumnError::InvalidValue {
|
||||
column: format!("group column {0}", index + 1),
|
||||
description: e.to_string(),
|
||||
}))
|
||||
}
|
||||
None => {
|
||||
return Some(Err(DecodeColumnError::UnexpectedNull(format!(
|
||||
"grouped column {0}",
|
||||
index + 1
|
||||
))))
|
||||
}
|
||||
}
|
||||
}
|
||||
result.push(row)
|
||||
}
|
||||
Some(Ok(result))
|
||||
}
|
||||
Some(None) => Some(Err(DecodeColumnError::UnexpectedNull("num".to_string()))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn done(&self) -> bool {
|
||||
self.num.done()
|
||||
}
|
||||
}
|
41
automerge/src/columnar_2/rowblock/encoding/interned_key.rs
Normal file
41
automerge/src/columnar_2/rowblock/encoding/interned_key.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
use crate::types::{ElemId, Key, OpId};
|
||||
|
||||
use super::{DeltaDecoder, RleDecoder};
|
||||
|
||||
pub(crate) struct InternedKeyDecoder<'a> {
|
||||
actor: RleDecoder<'a, u64>,
|
||||
ctr: DeltaDecoder<'a>,
|
||||
str_idx: RleDecoder<'a, u64>,
|
||||
}
|
||||
|
||||
impl<'a> InternedKeyDecoder<'a> {
|
||||
pub(crate) fn new(
|
||||
actor: RleDecoder<'a, u64>,
|
||||
ctr: DeltaDecoder<'a>,
|
||||
str_idx: RleDecoder<'a, u64>,
|
||||
) -> Self {
|
||||
Self {
|
||||
actor,
|
||||
ctr,
|
||||
str_idx,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn done(&self) -> bool {
|
||||
self.actor.done() && self.ctr.done() && self.str_idx.done()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for InternedKeyDecoder<'a> {
|
||||
type Item = Key;
|
||||
|
||||
fn next(&mut self) -> Option<Key> {
|
||||
match (self.actor.next(), self.ctr.next(), self.str_idx.next()) {
|
||||
(None, None, Some(Some(key_idx))) => Some(Key::Map(key_idx as usize)),
|
||||
(None, Some(Some(0)), None) => Some(Key::Seq(ElemId(OpId(0, 0)))),
|
||||
(Some(Some(actor)), Some(Some(ctr)), None) => Some(Key::Seq(OpId(actor, ctr as usize).into())),
|
||||
// TODO: This should be fallible and throw here
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
119
automerge/src/columnar_2/rowblock/encoding/key.rs
Normal file
119
automerge/src/columnar_2/rowblock/encoding/key.rs
Normal file
|
@ -0,0 +1,119 @@
|
|||
use std::{borrow::Cow, ops::Range};
|
||||
|
||||
use smol_str::SmolStr;
|
||||
|
||||
use super::{DecodeColumnError, DeltaDecoder, RleDecoder};
|
||||
use crate::types::{ElemId, OpId};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) enum Key {
|
||||
Prop(smol_str::SmolStr),
|
||||
Elem(ElemId),
|
||||
}
|
||||
|
||||
pub(crate) struct KeyDecoder<'a> {
|
||||
actor: RleDecoder<'a, u64>,
|
||||
ctr: DeltaDecoder<'a>,
|
||||
str: RleDecoder<'a, SmolStr>,
|
||||
}
|
||||
|
||||
impl<'a> KeyDecoder<'a> {
|
||||
pub(crate) fn new(
|
||||
actor: RleDecoder<'a, u64>,
|
||||
ctr: DeltaDecoder<'a>,
|
||||
str: RleDecoder<'a, SmolStr>,
|
||||
) -> Self {
|
||||
Self { actor, ctr, str }
|
||||
}
|
||||
|
||||
pub(crate) fn empty() -> KeyDecoder<'static> {
|
||||
KeyDecoder {
|
||||
actor: RleDecoder::from(Cow::Owned(Vec::new())),
|
||||
ctr: DeltaDecoder::from(Cow::Owned(Vec::new())),
|
||||
str: RleDecoder::from(Cow::Owned(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn done(&self) -> bool {
|
||||
self.actor.done() && self.ctr.done() && self.str.done()
|
||||
}
|
||||
|
||||
/// Splice new keys into this set of keys, encoding the resulting actor, counter, and str
|
||||
/// columns in `out`. The result is (actor, ctr, str) where actor is the range of the output which
|
||||
/// contains the new actor column, ctr the counter column, and str the str column.
|
||||
pub(crate) fn splice<'b, I: Iterator<Item = &'b Key> + Clone>(
|
||||
&mut self,
|
||||
replace: Range<usize>,
|
||||
replace_with: I,
|
||||
out: &mut Vec<u8>,
|
||||
) -> (Range<usize>, Range<usize>, Range<usize>) {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for KeyDecoder<'a> {
|
||||
type Item = Result<Key, DecodeColumnError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match (self.actor.next(), self.ctr.next(), self.str.next()) {
|
||||
(Some(Some(_)), Some(Some(_)), Some(Some(_))) => {
|
||||
Some(Err(DecodeColumnError::InvalidValue {
|
||||
column: "key".to_string(),
|
||||
description: "too many values".to_string(),
|
||||
}))
|
||||
}
|
||||
(Some(None), Some(None), Some(Some(string))) => Some(Ok(Key::Prop(string))),
|
||||
(Some(None), Some(Some(0)), Some(None)) => Some(Ok(Key::Elem(ElemId(OpId(0, 0))))),
|
||||
(Some(Some(actor)), Some(Some(ctr)), Some(None)) => match ctr.try_into() {
|
||||
Ok(ctr) => Some(Ok(Key::Elem(ElemId(OpId(ctr, actor as usize))))),
|
||||
Err(e) => Some(Err(DecodeColumnError::InvalidValue{
|
||||
column: "counter".to_string(),
|
||||
description: "negative value for counter".to_string(),
|
||||
})),
|
||||
}
|
||||
(None, None, None) => None,
|
||||
(None | Some(None), _, _) => {
|
||||
Some(Err(DecodeColumnError::UnexpectedNull("actor".to_string())))
|
||||
}
|
||||
(_, None | Some(None), _) => {
|
||||
Some(Err(DecodeColumnError::UnexpectedNull("ctr".to_string())))
|
||||
}
|
||||
(_, _, None) => Some(Err(DecodeColumnError::UnexpectedNull("str".to_string()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
|
||||
fn encode(vals: &[Key]) -> (Vec<u8>, Range<usize>, Range<usize>, Range<usize>) {
|
||||
let mut out = Vec::new();
|
||||
let mut decoder = KeyDecoder::empty();
|
||||
let (actor, ctr, string) = decoder.splice(0..0, vals.iter(), &mut out);
|
||||
(out, actor, ctr, string)
|
||||
}
|
||||
|
||||
//proptest! {
|
||||
//#[test]
|
||||
//fn splice_key(scenario in splice_scenario(row_op_key())) {
|
||||
//let (buf, actor, ctr, string) = encode(&scenario.initial_values[..]);
|
||||
//let mut decoder = KeyDecoder::new(
|
||||
//RleDecoder::from(&buf[actor]),
|
||||
//DeltaDecoder::from(&buf[ctr]),
|
||||
//RleDecoder::from(&buf[string]),
|
||||
//);
|
||||
//let mut out = Vec::new();
|
||||
//let (actor, ctr, string) = decoder.splice(scenario.replace_range.clone(), scenario.replacements.iter(), &mut out);
|
||||
//let decoder = KeyDecoder::new(
|
||||
//RleDecoder::from(&buf[actor]),
|
||||
//DeltaDecoder::from(&buf[ctr]),
|
||||
//RleDecoder::from(&buf[string.clone()]),
|
||||
//);
|
||||
//let result = decoder.map(|c| c.unwrap()).collect();
|
||||
//scenario.check(result);
|
||||
//assert_eq!(string.end, out.len());
|
||||
//}
|
||||
//}
|
||||
}
|
52
automerge/src/columnar_2/rowblock/encoding/mod.rs
Normal file
52
automerge/src/columnar_2/rowblock/encoding/mod.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
mod raw;
|
||||
use std::borrow::Borrow;
|
||||
|
||||
pub(crate) use raw::{RawEncoder, RawDecoder};
|
||||
mod rle;
|
||||
pub(crate) use rle::{RleEncoder, RleDecoder};
|
||||
mod boolean;
|
||||
pub(crate) use boolean::{BooleanDecoder, BooleanEncoder};
|
||||
mod delta;
|
||||
pub(crate) use delta::{DeltaDecoder, DeltaEncoder};
|
||||
mod value;
|
||||
pub(crate) use value::ValueDecoder;
|
||||
pub(crate) mod generic;
|
||||
pub(crate) use generic::{GenericColDecoder, SimpleColDecoder};
|
||||
mod opid;
|
||||
pub(crate) use opid::OpIdDecoder;
|
||||
mod opid_list;
|
||||
pub(crate) use opid_list::OpIdListDecoder;
|
||||
mod obj_id;
|
||||
pub(crate) use obj_id::ObjDecoder;
|
||||
mod key;
|
||||
pub(crate) use key::{Key, KeyDecoder};
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod properties;
|
||||
|
||||
|
||||
|
||||
pub(crate) trait Encodable {
|
||||
fn encode(&self, out: &mut Vec<u8>) -> usize;
|
||||
}
|
||||
mod encodable_impls;
|
||||
pub(crate) use encodable_impls::RawBytes;
|
||||
|
||||
pub(crate) trait Decodable: Sized {
|
||||
fn decode<R>(bytes: &mut R) -> Option<Self>
|
||||
where
|
||||
R: std::io::Read;
|
||||
}
|
||||
mod decodable_impls;
|
||||
|
||||
|
||||
#[derive(Clone, thiserror::Error, Debug)]
|
||||
pub(crate) enum DecodeColumnError {
|
||||
#[error("unexpected null decoding column {0}")]
|
||||
UnexpectedNull(String),
|
||||
#[error("invalid value in column {column}: {description}")]
|
||||
InvalidValue{
|
||||
column: String,
|
||||
description: String,
|
||||
},
|
||||
}
|
35
automerge/src/columnar_2/rowblock/encoding/obj_id.rs
Normal file
35
automerge/src/columnar_2/rowblock/encoding/obj_id.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
use crate::types::{OpId, ObjId};
|
||||
|
||||
use super::{DecodeColumnError, RleDecoder};
|
||||
|
||||
pub(crate) struct ObjDecoder<'a> {
|
||||
actor: RleDecoder<'a, u64>,
|
||||
ctr: RleDecoder<'a, u64>,
|
||||
}
|
||||
|
||||
impl<'a> ObjDecoder<'a> {
|
||||
pub(crate) fn new(actor: RleDecoder<'a, u64>, ctr: RleDecoder<'a, u64>) -> Self {
|
||||
Self{
|
||||
actor,
|
||||
ctr,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn done(&self) -> bool {
|
||||
self.actor.done() || self.ctr.done()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for ObjDecoder<'a> {
|
||||
type Item = Result<ObjId, DecodeColumnError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match (self.actor.next(), self.ctr.next()) {
|
||||
(None, None) => None,
|
||||
(Some(None), Some(None)) => Some(Ok(ObjId::root())),
|
||||
(Some(Some(a)), Some(Some(c))) => Some(Ok(ObjId(OpId(c, a as usize)))),
|
||||
(Some(None), _) | (None, _) => Some(Err(DecodeColumnError::UnexpectedNull("actor".to_string()))),
|
||||
(_, Some(None)) | (_, None) => Some(Err(DecodeColumnError::UnexpectedNull("counter".to_string()))),
|
||||
}
|
||||
}
|
||||
}
|
110
automerge/src/columnar_2/rowblock/encoding/opid.rs
Normal file
110
automerge/src/columnar_2/rowblock/encoding/opid.rs
Normal file
|
@ -0,0 +1,110 @@
|
|||
use std::{borrow::Cow, ops::Range};
|
||||
|
||||
use crate::types::OpId;
|
||||
|
||||
use super::{DecodeColumnError, DeltaDecoder, RleDecoder};
|
||||
|
||||
pub(crate) struct OpIdDecoder<'a> {
|
||||
actor: RleDecoder<'a, u64>,
|
||||
ctr: DeltaDecoder<'a>,
|
||||
}
|
||||
|
||||
impl Default for OpIdDecoder<'static> {
|
||||
fn default() -> Self {
|
||||
Self::new(
|
||||
RleDecoder::from(Cow::Owned(Vec::new())),
|
||||
DeltaDecoder::from(Cow::Owned(Vec::new())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> OpIdDecoder<'a> {
|
||||
pub(crate) fn new(actor: RleDecoder<'a, u64>, ctr: DeltaDecoder<'a>) -> Self {
|
||||
Self { actor, ctr }
|
||||
}
|
||||
|
||||
pub(crate) fn done(&self) -> bool {
|
||||
self.actor.done() && self.ctr.done()
|
||||
}
|
||||
|
||||
/// Splice new operations into this set of operations, encoding the resulting actor and counter
|
||||
/// columns in `out`. The result is (actor, ctr) where actor is the range of the output which
|
||||
/// contains the new actor column and ctr the counter column.
|
||||
pub(crate) fn splice<'b, I: Iterator<Item = &'b OpId> + Clone>(
|
||||
&mut self,
|
||||
replace: Range<usize>,
|
||||
replace_with: I,
|
||||
out: &mut Vec<u8>,
|
||||
) -> (Range<usize>, Range<usize>) {
|
||||
// first splice actors, then counters
|
||||
let actor = self.actor.splice(
|
||||
replace.clone(),
|
||||
replace_with.clone().map(|i| Some(i.actor() as u64)),
|
||||
out,
|
||||
);
|
||||
let counter = self
|
||||
.ctr
|
||||
.splice(replace, replace_with.map(|i| Some(i.counter() as i64)), out);
|
||||
(actor, counter)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for OpIdDecoder<'a> {
|
||||
type Item = Result<OpId, DecodeColumnError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match (self.actor.next(), self.ctr.next()) {
|
||||
(Some(Some(a)), Some(Some(c))) => match c.try_into() {
|
||||
Ok(c) => Some(Ok(OpId(c, a as usize))),
|
||||
Err(e) => Some(Err(DecodeColumnError::InvalidValue{
|
||||
column: "counter".to_string(),
|
||||
description: "negative value encountered".to_string(),
|
||||
}))
|
||||
},
|
||||
(Some(None), _) => Some(Err(DecodeColumnError::UnexpectedNull("actor".to_string()))),
|
||||
(_, Some(None)) => Some(Err(DecodeColumnError::UnexpectedNull("actor".to_string()))),
|
||||
(Some(_), None) => Some(Err(DecodeColumnError::UnexpectedNull("ctr".to_string()))),
|
||||
(None, Some(_)) => Some(Err(DecodeColumnError::UnexpectedNull("actor".to_string()))),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::columnar_2::rowblock::encoding::properties::{opid, splice_scenario};
|
||||
use proptest::prelude::*;
|
||||
|
||||
fn encode(vals: &[OpId]) -> (Vec<u8>, Range<usize>, Range<usize>) {
|
||||
let mut out = Vec::new();
|
||||
let mut decoder = OpIdDecoder::default();
|
||||
let (actor, ctr) = decoder.splice(0..0, vals.into_iter(), &mut out);
|
||||
(out, actor, ctr)
|
||||
}
|
||||
|
||||
fn decode(buf: &[u8], actor: Range<usize>, ctr: Range<usize>) -> Vec<OpId> {
|
||||
OpIdDecoder::new(RleDecoder::from(&buf[actor]), DeltaDecoder::from(&buf[ctr]))
|
||||
.map(|c| c.unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn encode_decode_opid(opids in proptest::collection::vec(opid(), 0..100)) {
|
||||
let (encoded, actor, ctr) = encode(&opids);
|
||||
assert_eq!(opids, decode(&encoded[..], actor, ctr));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn splice_opids(scenario in splice_scenario(opid())) {
|
||||
let (encoded, actor, ctr) = encode(&scenario.initial_values);
|
||||
let mut decoder = OpIdDecoder::new(RleDecoder::from(&encoded[actor]), DeltaDecoder::from(&encoded[ctr]));
|
||||
let mut out = Vec::new();
|
||||
let (actor, ctr) = decoder.splice(scenario.replace_range.clone(), scenario.replacements.iter(), &mut out);
|
||||
let result = decode(&out[..], actor, ctr.clone());
|
||||
scenario.check(result);
|
||||
assert_eq!(ctr.end, out.len());
|
||||
}
|
||||
}
|
||||
}
|
182
automerge/src/columnar_2/rowblock/encoding/opid_list.rs
Normal file
182
automerge/src/columnar_2/rowblock/encoding/opid_list.rs
Normal file
|
@ -0,0 +1,182 @@
|
|||
use std::{borrow::Cow, ops::Range};
|
||||
|
||||
use crate::types::OpId;
|
||||
|
||||
use super::{DecodeColumnError, DeltaDecoder, RleDecoder};
|
||||
|
||||
pub(crate) struct OpIdListDecoder<'a> {
|
||||
num: RleDecoder<'a, u64>,
|
||||
actor: RleDecoder<'a, u64>,
|
||||
ctr: DeltaDecoder<'a>,
|
||||
}
|
||||
|
||||
impl<'a> OpIdListDecoder<'a> {
|
||||
pub(crate) fn new(
|
||||
num: RleDecoder<'a, u64>,
|
||||
actor: RleDecoder<'a, u64>,
|
||||
ctr: DeltaDecoder<'a>,
|
||||
) -> Self {
|
||||
Self { num, actor, ctr }
|
||||
}
|
||||
|
||||
/// A decoder which references empty arrays, therefore has no elements
|
||||
pub(crate) fn empty() -> OpIdListDecoder<'static> {
|
||||
OpIdListDecoder {
|
||||
num: RleDecoder::from(Cow::Owned(Vec::new())),
|
||||
actor: RleDecoder::from(Cow::Owned(Vec::new())),
|
||||
ctr: DeltaDecoder::from(Cow::Owned(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn done(&self) -> bool {
|
||||
self.num.done()
|
||||
}
|
||||
|
||||
/// Splice new lists of opids into this set of lists of opids, encoding the resulting num, actor and counter
|
||||
/// columns in `out`. The result is (num, actor, ctr) where num is the range of the output which
|
||||
/// contains the new num column, actor the actor column, and ctr the counter column
|
||||
pub(crate) fn splice<'b, I, II, IE>(
|
||||
&mut self,
|
||||
replace: Range<usize>,
|
||||
replace_with: I,
|
||||
out: &mut Vec<u8>,
|
||||
) -> (Range<usize>, Range<usize>, Range<usize>)
|
||||
where
|
||||
II: IntoIterator<Item = OpId, IntoIter=IE>,
|
||||
IE: Iterator<Item=OpId> + ExactSizeIterator,
|
||||
I: Iterator<Item = II> + Clone,
|
||||
{
|
||||
let group_replace = group_replace_range(replace.clone(), self.num.clone());
|
||||
// first nums
|
||||
let num = self.num.splice(
|
||||
replace.clone(),
|
||||
replace_with.clone().map(|elems| Some(elems.into_iter().len() as u64)),
|
||||
out,
|
||||
);
|
||||
let actor = self.actor.splice(
|
||||
group_replace.clone(),
|
||||
replace_with
|
||||
.clone()
|
||||
.flat_map(|elem| elem.into_iter().map(|oid| Some(oid.actor() as u64))),
|
||||
out,
|
||||
);
|
||||
let ctr = self.ctr.splice(
|
||||
group_replace,
|
||||
replace_with.flat_map(|elem| elem.into_iter().map(|oid| Some(oid.counter() as i64))),
|
||||
out,
|
||||
);
|
||||
(num, actor, ctr)
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the replace range for the grouped columns.
|
||||
fn group_replace_range(replace: Range<usize>, mut num: RleDecoder<u64>) -> Range<usize> {
|
||||
let mut idx = 0;
|
||||
let mut grouped_replace_start: usize = 0;
|
||||
let mut grouped_replace_len: usize = 0;
|
||||
while idx < replace.start {
|
||||
if let Some(Some(count)) = num.next() {
|
||||
grouped_replace_start += count as usize;
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
for _ in 0..replace.len() {
|
||||
if let Some(Some(count)) = num.next() {
|
||||
grouped_replace_len += count as usize;
|
||||
}
|
||||
}
|
||||
grouped_replace_start..(grouped_replace_start + grouped_replace_len)
|
||||
}
|
||||
|
||||
impl<'a> Iterator for OpIdListDecoder<'a> {
|
||||
type Item = Result<Vec<OpId>, DecodeColumnError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let num = match self.num.next() {
|
||||
Some(Some(n)) => n,
|
||||
Some(None) => return Some(Err(DecodeColumnError::UnexpectedNull("num".to_string()))),
|
||||
None => return None,
|
||||
};
|
||||
let mut p = Vec::with_capacity(num as usize);
|
||||
for _ in 0..num {
|
||||
match (self.actor.next(), self.ctr.next()) {
|
||||
(Some(Some(a)), Some(Some(ctr))) => match ctr.try_into() {
|
||||
Ok(ctr) => p.push(OpId(ctr, a as usize)),
|
||||
Err(e) => return Some(Err(DecodeColumnError::InvalidValue{
|
||||
column: "counter".to_string(),
|
||||
description: "negative value for counter".to_string(),
|
||||
}))
|
||||
},
|
||||
(Some(None) | None, _) => {
|
||||
return Some(Err(DecodeColumnError::UnexpectedNull("actor".to_string())))
|
||||
}
|
||||
(_, Some(None) | None) => {
|
||||
return Some(Err(DecodeColumnError::UnexpectedNull("ctr".to_string())))
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Ok(p))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use proptest::collection::vec as propvec;
|
||||
use proptest::prelude::*;
|
||||
|
||||
use crate::columnar_2::rowblock::encoding::properties::{opid, splice_scenario};
|
||||
|
||||
fn encode(opids: Vec<Vec<OpId>>) -> (Vec<u8>, Range<usize>, Range<usize>, Range<usize>) {
|
||||
let mut out = Vec::new();
|
||||
let mut decoder = OpIdListDecoder::empty();
|
||||
let (num, actor, ctr) = decoder.splice(
|
||||
0..0,
|
||||
opids.into_iter(),
|
||||
&mut out,
|
||||
);
|
||||
(out, num, actor, ctr)
|
||||
}
|
||||
|
||||
fn decode(
|
||||
buf: &[u8],
|
||||
num: Range<usize>,
|
||||
actor: Range<usize>,
|
||||
ctr: Range<usize>,
|
||||
) -> Vec<Vec<OpId>> {
|
||||
let decoder = OpIdListDecoder::new(
|
||||
RleDecoder::from(&buf[num]),
|
||||
RleDecoder::from(&buf[actor]),
|
||||
DeltaDecoder::from(&buf[ctr]),
|
||||
);
|
||||
decoder.map(|c| c.unwrap()).collect()
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn encode_decode_opid_list(opids in propvec(propvec(opid(), 0..100), 0..100)){
|
||||
let (encoded, num, actor, ctr) = encode(opids.clone());
|
||||
let result = decode(&encoded, num, actor, ctr);
|
||||
assert_eq!(opids, result)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn splice_opid_list(scenario in splice_scenario(propvec(opid(), 0..100))) {
|
||||
let (encoded, num, actor, ctr) = encode(scenario.initial_values.clone());
|
||||
let mut decoder = OpIdListDecoder::new(
|
||||
RleDecoder::from(&encoded[num]),
|
||||
RleDecoder::from(&encoded[actor]),
|
||||
DeltaDecoder::from(&encoded[ctr]),
|
||||
);
|
||||
let mut out = Vec::new();
|
||||
let (num, actor, ctr) = decoder.splice(
|
||||
scenario.replace_range.clone(),
|
||||
scenario.replacements.clone().into_iter(),
|
||||
&mut out
|
||||
);
|
||||
let result = decode(&out[..], num, actor, ctr.clone());
|
||||
scenario.check(result);
|
||||
assert_eq!(ctr.end, out.len())
|
||||
}
|
||||
}
|
||||
}
|
109
automerge/src/columnar_2/rowblock/encoding/properties.rs
Normal file
109
automerge/src/columnar_2/rowblock/encoding/properties.rs
Normal file
|
@ -0,0 +1,109 @@
|
|||
//! Helpers for property tests.
|
||||
|
||||
use std::{borrow::Cow, fmt::Debug, ops::Range};
|
||||
|
||||
use proptest::prelude::*;
|
||||
use smol_str::SmolStr;
|
||||
|
||||
use crate::{
|
||||
columnar_2::rowblock::{PrimVal, Key},
|
||||
types::{OpId, Key as InternedKey, ElemId}
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct SpliceScenario<T> {
|
||||
pub(crate) initial_values: Vec<T>,
|
||||
pub(crate) replace_range: Range<usize>,
|
||||
pub(crate) replacements: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T: Debug + PartialEq + Clone> SpliceScenario<T> {
|
||||
pub(crate) fn check(&self, results: Vec<T>) {
|
||||
let mut expected = self
|
||||
.initial_values
|
||||
.clone();
|
||||
expected.splice(self.replace_range.clone(), self.replacements.clone());
|
||||
assert_eq!(expected, results)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn splice_scenario<S: Strategy<Value = T> + Clone, T: Debug + Clone + 'static>(
|
||||
item_strat: S,
|
||||
) -> impl Strategy<Value = SpliceScenario<T>> {
|
||||
(
|
||||
proptest::collection::vec(item_strat.clone(), 0..100),
|
||||
proptest::collection::vec(item_strat, 0..10),
|
||||
)
|
||||
.prop_flat_map(move |(values, to_splice)| {
|
||||
if values.len() == 0 {
|
||||
Just(SpliceScenario {
|
||||
initial_values: values.clone(),
|
||||
replace_range: 0..0,
|
||||
replacements: to_splice.clone(),
|
||||
})
|
||||
.boxed()
|
||||
} else {
|
||||
// This is somewhat awkward to write because we have to carry the `values` and
|
||||
// `to_splice` through as `Just(..)` to please the borrow checker.
|
||||
(0..values.len(), Just(values), Just(to_splice))
|
||||
.prop_flat_map(move |(replace_range_start, values, to_splice)| {
|
||||
(
|
||||
0..(values.len() - replace_range_start),
|
||||
Just(values),
|
||||
Just(to_splice),
|
||||
)
|
||||
.prop_map(
|
||||
move |(replace_range_len, values, to_splice)| SpliceScenario {
|
||||
initial_values: values.clone(),
|
||||
replace_range: replace_range_start
|
||||
..(replace_range_start + replace_range_len),
|
||||
replacements: to_splice.clone(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn opid() -> impl Strategy<Value = OpId> + Clone {
|
||||
(0..(i64::MAX as usize), 0..(i64::MAX as u64)).prop_map(|(actor, ctr)| OpId(ctr, actor))
|
||||
}
|
||||
|
||||
pub(crate) fn elemid() -> impl Strategy<Value = ElemId> + Clone {
|
||||
opid().prop_map(ElemId)
|
||||
}
|
||||
|
||||
pub(crate) fn interned_key() -> impl Strategy<Value = InternedKey> + Clone {
|
||||
prop_oneof!{
|
||||
elemid().prop_map(InternedKey::Seq),
|
||||
(0..(i64::MAX as usize)).prop_map(InternedKey::Map),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn key() -> impl Strategy<Value = Key> + Clone {
|
||||
prop_oneof!{
|
||||
elemid().prop_map(Key::Elem),
|
||||
any::<String>().prop_map(|s| Key::Prop(s.into())),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn value() -> impl Strategy<Value = PrimVal<'static>> + Clone {
|
||||
prop_oneof! {
|
||||
Just(PrimVal::Null),
|
||||
any::<bool>().prop_map(|b| PrimVal::Bool(b)),
|
||||
any::<u64>().prop_map(|i| PrimVal::Uint(i)),
|
||||
any::<i64>().prop_map(|i| PrimVal::Int(i)),
|
||||
any::<f64>().prop_map(|f| PrimVal::Float(f)),
|
||||
any::<String>().prop_map(|s| PrimVal::String(Cow::Owned(s.into()))),
|
||||
any::<Vec<u8>>().prop_map(|b| PrimVal::Bytes(Cow::Owned(b))),
|
||||
any::<u64>().prop_map(|i| PrimVal::Counter(i)),
|
||||
any::<u64>().prop_map(|i| PrimVal::Timestamp(i)),
|
||||
(10..15_u8, any::<Vec<u8>>()).prop_map(|(c, b)| PrimVal::Unknown { type_code: c, data: b }),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn smol_str() -> impl Strategy<Value = SmolStr> + Clone {
|
||||
any::<String>().prop_map(SmolStr::from)
|
||||
}
|
|
@ -1,29 +1,26 @@
|
|||
use std::{
|
||||
borrow::{Borrow, Cow},
|
||||
fmt::Debug,
|
||||
};
|
||||
use std::{borrow::Cow, fmt::Debug};
|
||||
|
||||
use super::{Decodable, DecodeError, Encodable, Sink};
|
||||
use super::{Decodable, Encodable};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct RawDecoder<'a> {
|
||||
offset: usize,
|
||||
last_read: usize,
|
||||
pub offset: usize,
|
||||
pub last_read: usize,
|
||||
data: Cow<'a, [u8]>,
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub(crate) enum Error {
|
||||
#[error("no decoded value")]
|
||||
NoDecodedValue,
|
||||
#[error("buffer size did not change")]
|
||||
BufferSizeDidNotChange,
|
||||
#[error("trying to read past end")]
|
||||
TryingToReadPastEnd,
|
||||
#[error(transparent)]
|
||||
Decode(#[from] DecodeError),
|
||||
}
|
||||
|
||||
impl<'a> RawDecoder<'a> {
|
||||
pub(crate) fn new(data: Cow<'a, [u8]>) -> Self {
|
||||
pub fn new(data: Cow<'a, [u8]>) -> Self {
|
||||
RawDecoder {
|
||||
offset: 0,
|
||||
last_read: 0,
|
||||
|
@ -31,10 +28,10 @@ impl<'a> RawDecoder<'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)?;
|
||||
let val = T::decode::<&[u8]>(&mut buf).ok_or(Error::NoDecodedValue)?;
|
||||
let delta = init_len - buf.len();
|
||||
if delta == 0 {
|
||||
Err(Error::BufferSizeDidNotChange)
|
||||
|
@ -45,7 +42,7 @@ impl<'a> RawDecoder<'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 {
|
||||
|
@ -56,7 +53,7 @@ impl<'a> RawDecoder<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn done(&self) -> bool {
|
||||
pub fn done(&self) -> bool {
|
||||
self.offset >= self.data.len()
|
||||
}
|
||||
}
|
||||
|
@ -73,25 +70,27 @@ impl<'a> From<Cow<'a, [u8]>> for RawDecoder<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RawEncoder<S> {
|
||||
|
||||
pub(crate) struct RawEncoder<'a> {
|
||||
written: usize,
|
||||
output: S,
|
||||
output: &'a mut Vec<u8>,
|
||||
}
|
||||
|
||||
impl<S: Sink> RawEncoder<S> {
|
||||
pub(crate) fn append<B: Borrow<I>, I: Encodable>(&mut self, value: B) -> usize {
|
||||
let written = value.borrow().encode(&mut self.output);
|
||||
impl<'a> RawEncoder<'a> {
|
||||
pub(crate) fn append<I: Encodable>(&mut self, value: &I) -> usize {
|
||||
let written = value.encode(&mut self.output);
|
||||
self.written += written;
|
||||
written
|
||||
}
|
||||
|
||||
pub(crate) fn finish(self) -> (S, usize) {
|
||||
(self.output, self.written)
|
||||
fn finish(self) -> usize {
|
||||
self.written
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Sink> From<S> for RawEncoder<S> {
|
||||
fn from(output: S) -> Self {
|
||||
RawEncoder { written: 0, output }
|
||||
impl<'a> From<&'a mut Vec<u8>> for RawEncoder<'a> {
|
||||
fn from(output: &'a mut Vec<u8>) -> Self {
|
||||
RawEncoder{ written: 0, output }
|
||||
}
|
||||
}
|
||||
|
364
automerge/src/columnar_2/rowblock/encoding/rle.rs
Normal file
364
automerge/src/columnar_2/rowblock/encoding/rle.rs
Normal file
|
@ -0,0 +1,364 @@
|
|||
use std::{
|
||||
borrow::{Borrow, Cow},
|
||||
fmt::Debug,
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
use super::{Encodable, Decodable, RawDecoder};
|
||||
|
||||
pub(crate) struct RleEncoder<'a, T>
|
||||
where
|
||||
T: Encodable + PartialEq + Clone,
|
||||
{
|
||||
buf: &'a mut Vec<u8>,
|
||||
written: usize,
|
||||
state: RleState<T>,
|
||||
}
|
||||
|
||||
impl<'a, T> RleEncoder<'a, T>
|
||||
where
|
||||
T: Encodable + PartialEq + Clone,
|
||||
{
|
||||
pub fn new(output_buf: &'a mut Vec<u8>) -> RleEncoder<'a, T> {
|
||||
RleEncoder {
|
||||
buf: output_buf,
|
||||
written: 0,
|
||||
state: RleState::Empty,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(mut self) -> usize {
|
||||
match self.take_state() {
|
||||
// this covers `only_nulls`
|
||||
RleState::NullRun(size) => {
|
||||
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 => {}
|
||||
}
|
||||
self.written
|
||||
}
|
||||
|
||||
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;
|
||||
std::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.clone()),
|
||||
RleState::LoneVal(other) => {
|
||||
if &other == value {
|
||||
RleState::Run(value.clone(), 2)
|
||||
} else {
|
||||
let mut v = Vec::with_capacity(2);
|
||||
v.push(other);
|
||||
RleState::LiteralRun(value.clone(), v)
|
||||
}
|
||||
}
|
||||
RleState::Run(other, len) => {
|
||||
if &other == value {
|
||||
RleState::Run(other, len + 1)
|
||||
} else {
|
||||
self.flush_run(&other, len);
|
||||
RleState::LoneVal(value.clone())
|
||||
}
|
||||
}
|
||||
RleState::LiteralRun(last, mut run) => {
|
||||
if &last == value {
|
||||
self.flush_lit_run(run);
|
||||
RleState::Run(value.clone(), 2)
|
||||
} else {
|
||||
run.push(last);
|
||||
RleState::LiteralRun(value.clone(), run)
|
||||
}
|
||||
}
|
||||
RleState::NullRun(size) => {
|
||||
self.flush_null_run(size);
|
||||
RleState::LoneVal(value.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append(&mut self, value: Option<&T>) {
|
||||
match value {
|
||||
Some(t) => self.append_value(t),
|
||||
None => self.append_null(),
|
||||
}
|
||||
}
|
||||
|
||||
fn encode<V>(&mut self, val: &V)
|
||||
where
|
||||
V: Encodable,
|
||||
{
|
||||
self.written += val.encode(&mut self.buf);
|
||||
}
|
||||
}
|
||||
|
||||
enum RleState<T> {
|
||||
Empty,
|
||||
NullRun(usize),
|
||||
LiteralRun(T, Vec<T>),
|
||||
LoneVal(T),
|
||||
Run(T, usize),
|
||||
}
|
||||
|
||||
impl<'a, T: Clone + PartialEq + Encodable> From<&'a mut Vec<u8>> for RleEncoder<'a, T> {
|
||||
fn from(output: &'a mut Vec<u8>) -> Self {
|
||||
Self::new(output)
|
||||
}
|
||||
}
|
||||
|
||||
/// See discussion on [`RleEncoder`] for the format data is stored in.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct RleDecoder<'a, T> {
|
||||
pub decoder: RawDecoder<'a>,
|
||||
last_value: Option<T>,
|
||||
count: isize,
|
||||
literal: bool,
|
||||
}
|
||||
|
||||
impl<'a, T> RleDecoder<'a, T> {
|
||||
fn empty() -> Self {
|
||||
RleDecoder{
|
||||
decoder: RawDecoder::from(&[] as &[u8]),
|
||||
last_value: None,
|
||||
count: 0,
|
||||
literal: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn done(&self) -> bool {
|
||||
self.decoder.done() && self.count == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Clone + Debug + Encodable + Decodable + Eq> RleDecoder<'a, T> {
|
||||
|
||||
pub(crate) fn encode<I>(items: I, out: &'a mut Vec<u8>) -> Range<usize>
|
||||
where
|
||||
I: Iterator<Item=T>
|
||||
{
|
||||
let mut empty = RleDecoder::empty();
|
||||
let range = empty.splice(0..0, items.map(Some), out);
|
||||
range
|
||||
}
|
||||
|
||||
pub(crate) fn splice<I: Iterator<Item=Option<TB>>, TB: Borrow<T>>(&mut self, replace: Range<usize>, mut replace_with: I, out: &mut Vec<u8>) -> Range<usize> {
|
||||
let start = out.len();
|
||||
let mut encoder = RleEncoder::new(out);
|
||||
let mut idx = 0;
|
||||
while idx < replace.start {
|
||||
match self.next() {
|
||||
Some(elem) => encoder.append(elem.as_ref()),
|
||||
None => panic!("out of bounds"),
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
for _ in 0..replace.len() {
|
||||
self.next();
|
||||
if let Some(next) = replace_with.next() {
|
||||
encoder.append(next.as_ref().map(|n| n.borrow()));
|
||||
}
|
||||
}
|
||||
while let Some(next) = replace_with.next() {
|
||||
encoder.append(next.as_ref().map(|n| n.borrow()));
|
||||
}
|
||||
while let Some(next) = self.next() {
|
||||
encoder.append(next.as_ref());
|
||||
}
|
||||
start..(start + encoder.finish())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<Cow<'a, [u8]>> for RleDecoder<'a, T> {
|
||||
fn from(bytes: Cow<'a, [u8]>) -> Self {
|
||||
RleDecoder {
|
||||
decoder: RawDecoder::from(bytes),
|
||||
last_value: None,
|
||||
count: 0,
|
||||
literal: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<&'a [u8]> for RleDecoder<'a, T> {
|
||||
fn from(d: &'a [u8]) -> Self {
|
||||
Cow::Borrowed(d).into()
|
||||
}
|
||||
}
|
||||
|
||||
// this decoder needs to be able to send type T or 'null'
|
||||
// it is an endless iterator that will return all 'null's
|
||||
// once input is exhausted
|
||||
impl<'a, T> Iterator for RleDecoder<'a, T>
|
||||
where
|
||||
T: Clone + Debug + Decodable,
|
||||
{
|
||||
type Item = Option<T>;
|
||||
|
||||
fn next(&mut self) -> Option<Option<T>> {
|
||||
while self.count == 0 {
|
||||
if self.decoder.done() {
|
||||
return None;
|
||||
}
|
||||
match self.decoder.read::<i64>() {
|
||||
Ok(count) if count > 0 => {
|
||||
// normal run
|
||||
self.count = count as isize;
|
||||
self.last_value = self.decoder.read().ok();
|
||||
self.literal = false;
|
||||
}
|
||||
Ok(count) if count < 0 => {
|
||||
// literal run
|
||||
self.count = count.abs() as isize;
|
||||
self.literal = true;
|
||||
}
|
||||
Ok(_) => {
|
||||
// null run
|
||||
// FIXME(jeffa5): handle usize > i64 here somehow
|
||||
self.count = self.decoder.read::<usize>().unwrap() as isize;
|
||||
self.last_value = None;
|
||||
self.literal = false;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(error=?e, "error during rle decoding");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.count -= 1;
|
||||
if self.literal {
|
||||
Some(self.decoder.read().ok())
|
||||
} else {
|
||||
Some(self.last_value.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::borrow::Cow;
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
use super::super::properties::splice_scenario;
|
||||
|
||||
#[test]
|
||||
fn rle_int_round_trip() {
|
||||
let vals = [1,1,2,2,3,2,3,1,3];
|
||||
let mut buf = Vec::with_capacity(vals.len() * 3);
|
||||
let mut encoder: RleEncoder<'_, u64> = RleEncoder::new(&mut buf);
|
||||
for val in vals {
|
||||
encoder.append_value(&val)
|
||||
}
|
||||
let total_slice_len = encoder.finish();
|
||||
let mut decoder: RleDecoder<'_, u64> = RleDecoder::from(Cow::Borrowed(&buf[0..total_slice_len]));
|
||||
let mut result = Vec::new();
|
||||
while let Some(Some(val)) = decoder.next() {
|
||||
result.push(val);
|
||||
}
|
||||
assert_eq!(result, vals);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rle_int_insert() {
|
||||
let vals = [1,1,2,2,3,2,3,1,3];
|
||||
let mut buf = Vec::with_capacity(vals.len() * 3);
|
||||
let mut encoder: RleEncoder<'_, u64> = RleEncoder::new(&mut buf);
|
||||
for i in 0..4 {
|
||||
encoder.append_value(&vals[i])
|
||||
}
|
||||
encoder.append_value(&5);
|
||||
for i in 4..vals.len() {
|
||||
encoder.append_value(&vals[i]);
|
||||
}
|
||||
let total_slice_len = encoder.finish();
|
||||
let mut decoder: RleDecoder<'_, u64> = RleDecoder::from(Cow::Borrowed(&buf[0..total_slice_len]));
|
||||
let mut result = Vec::new();
|
||||
while let Some(Some(val)) = decoder.next() {
|
||||
result.push(val);
|
||||
}
|
||||
let expected = [1,1,2,2,5,3,2,3,1,3];
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
fn encode<T: Clone + Encodable + PartialEq>(vals: &[Option<T>]) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(vals.len() * 3);
|
||||
let mut encoder: RleEncoder<'_, T> = RleEncoder::new(&mut buf);
|
||||
for val in vals {
|
||||
encoder.append(val.as_ref())
|
||||
}
|
||||
encoder.finish();
|
||||
buf
|
||||
}
|
||||
|
||||
fn decode<T: Clone + Decodable + Debug>(buf: Vec<u8>) -> Vec<Option<T>> {
|
||||
let decoder = RleDecoder::<'_, T>::from(&buf[..]);
|
||||
decoder.collect()
|
||||
}
|
||||
|
||||
proptest!{
|
||||
#[test]
|
||||
fn splice_ints(scenario in splice_scenario(any::<Option<i32>>())) {
|
||||
let buf = encode(&scenario.initial_values);
|
||||
let mut decoder = RleDecoder::<'_, i32>::from(&buf[..]);
|
||||
let mut out = Vec::new();
|
||||
decoder.splice(scenario.replace_range.clone(), scenario.replacements.iter().cloned(), &mut out);
|
||||
let result = decode::<i32>(out);
|
||||
scenario.check(result)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn splice_strings(scenario in splice_scenario(any::<Option<String>>())) {
|
||||
let buf = encode(&scenario.initial_values);
|
||||
let mut decoder = RleDecoder::<'_, String>::from(&buf[..]);
|
||||
let mut out = Vec::new();
|
||||
decoder.splice(scenario.replace_range.clone(), scenario.replacements.iter().cloned(), &mut out);
|
||||
let result = decode::<String>(out);
|
||||
scenario.check(result)
|
||||
}
|
||||
}
|
||||
}
|
475
automerge/src/columnar_2/rowblock/encoding/value.rs
Normal file
475
automerge/src/columnar_2/rowblock/encoding/value.rs
Normal file
|
@ -0,0 +1,475 @@
|
|||
use crate::columnar_2::rowblock::column_layout::ColumnSpliceError;
|
||||
use std::{borrow::Cow, convert::TryInto, ops::Range};
|
||||
|
||||
use super::{DecodeColumnError, RawDecoder, RawEncoder, RleDecoder, RleEncoder, RawBytes};
|
||||
use crate::columnar_2::rowblock::value::PrimVal;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ValueDecoder<'a> {
|
||||
meta: RleDecoder<'a, u64>,
|
||||
raw: RawDecoder<'a>,
|
||||
}
|
||||
|
||||
impl<'a> ValueDecoder<'a> {
|
||||
pub(crate) fn new(meta: RleDecoder<'a, u64>, raw: RawDecoder<'a>) -> ValueDecoder<'a> {
|
||||
ValueDecoder { meta, raw }
|
||||
}
|
||||
|
||||
pub(crate) fn done(&self) -> bool {
|
||||
self.meta.done()
|
||||
}
|
||||
|
||||
pub(crate) fn next(&mut self) -> Option<Result<PrimVal<'a>, DecodeColumnError>> {
|
||||
match self.meta.next() {
|
||||
Some(Some(next)) => {
|
||||
let val_meta = ValueMeta::from(next);
|
||||
#[allow(clippy::redundant_slicing)]
|
||||
match val_meta.type_code() {
|
||||
ValueType::Null => Some(Ok(PrimVal::Null)),
|
||||
ValueType::True => Some(Ok(PrimVal::Bool(true))),
|
||||
ValueType::False => Some(Ok(PrimVal::Bool(false))),
|
||||
ValueType::Uleb => self.parse_raw(val_meta, |mut bytes| {
|
||||
let val = leb128::read::unsigned(&mut bytes).map_err(|e| {
|
||||
DecodeColumnError::InvalidValue {
|
||||
column: "value".to_string(),
|
||||
description: e.to_string(),
|
||||
}
|
||||
})?;
|
||||
Ok(PrimVal::Uint(val))
|
||||
}),
|
||||
ValueType::Leb => self.parse_raw(val_meta, |mut bytes| {
|
||||
let val = leb128::read::signed(&mut bytes).map_err(|e| {
|
||||
DecodeColumnError::InvalidValue {
|
||||
column: "value".to_string(),
|
||||
description: e.to_string(),
|
||||
}
|
||||
})?;
|
||||
Ok(PrimVal::Int(val))
|
||||
}),
|
||||
ValueType::String => self.parse_raw(val_meta, |bytes| {
|
||||
let val = Cow::Owned(
|
||||
std::str::from_utf8(bytes)
|
||||
.map_err(|e| DecodeColumnError::InvalidValue {
|
||||
column: "value".to_string(),
|
||||
description: e.to_string(),
|
||||
})?
|
||||
.into(),
|
||||
);
|
||||
Ok(PrimVal::String(val))
|
||||
}),
|
||||
ValueType::Float => self.parse_raw(val_meta, |bytes| {
|
||||
if val_meta.length() != 8 {
|
||||
return Err(DecodeColumnError::InvalidValue {
|
||||
column: "value".to_string(),
|
||||
description: format!(
|
||||
"float should have length 8, had {0}",
|
||||
val_meta.length()
|
||||
),
|
||||
});
|
||||
}
|
||||
let raw: [u8; 8] = bytes
|
||||
.try_into()
|
||||
// SAFETY: parse_raw() calls read_bytes(val_meta.length()) and we have
|
||||
// checked that val_meta.length() == 8
|
||||
.unwrap();
|
||||
let val = f64::from_le_bytes(raw);
|
||||
Ok(PrimVal::Float(val))
|
||||
}),
|
||||
ValueType::Counter => self.parse_raw(val_meta, |mut bytes| {
|
||||
let val = leb128::read::unsigned(&mut bytes).map_err(|e| {
|
||||
DecodeColumnError::InvalidValue {
|
||||
column: "value".to_string(),
|
||||
description: e.to_string(),
|
||||
}
|
||||
})?;
|
||||
Ok(PrimVal::Counter(val))
|
||||
}),
|
||||
ValueType::Timestamp => self.parse_raw(val_meta, |mut bytes| {
|
||||
let val = leb128::read::unsigned(&mut bytes).map_err(|e| {
|
||||
DecodeColumnError::InvalidValue {
|
||||
column: "value".to_string(),
|
||||
description: e.to_string(),
|
||||
}
|
||||
})?;
|
||||
Ok(PrimVal::Timestamp(val))
|
||||
}),
|
||||
ValueType::Unknown(code) => self.parse_raw(val_meta, |bytes| {
|
||||
Ok(PrimVal::Unknown {
|
||||
type_code: code,
|
||||
data: bytes.to_vec(),
|
||||
})
|
||||
}),
|
||||
ValueType::Bytes => match self.raw.read_bytes(val_meta.length()) {
|
||||
Err(e) => Some(Err(DecodeColumnError::InvalidValue {
|
||||
column: "value".to_string(),
|
||||
description: e.to_string(),
|
||||
})),
|
||||
Ok(bytes) => Some(Ok(PrimVal::Bytes(Cow::Owned(bytes.to_vec())))),
|
||||
},
|
||||
}
|
||||
}
|
||||
Some(None) => Some(Err(DecodeColumnError::UnexpectedNull("meta".to_string()))),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn splice<'b, I>(
|
||||
&'a mut self,
|
||||
replace: Range<usize>,
|
||||
replace_with: I,
|
||||
out: &mut Vec<u8>,
|
||||
) -> (Range<usize>, Range<usize>)
|
||||
where
|
||||
I: Iterator<Item = PrimVal<'a>> + Clone,
|
||||
{
|
||||
// SAFETY: try_splice only fails if the iterator fails, and this iterator is infallible
|
||||
self.try_splice(replace, replace_with.map(|i| Ok(i)), out).unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn try_splice<'b, I>(
|
||||
&'a mut self,
|
||||
replace: Range<usize>,
|
||||
mut replace_with: I,
|
||||
out: &mut Vec<u8>,
|
||||
) -> Result<(Range<usize>, Range<usize>), ColumnSpliceError>
|
||||
where
|
||||
I: Iterator<Item = Result<PrimVal<'a>, ColumnSpliceError>> + Clone,
|
||||
{
|
||||
// Our semantics here are similar to those of Vec::splice. We can describe this
|
||||
// imperatively like this:
|
||||
//
|
||||
// * First copy everything up to the start of `replace` into the output
|
||||
// * For every index in `replace` skip that index from ourselves and if `replace_with`
|
||||
// returns `Some` then copy that value to the output
|
||||
// * Once we have iterated past `replace.end` we continue to call `replace_with` until it
|
||||
// returns None, copying the results to the output
|
||||
// * Finally we copy the remainder of our data into the output
|
||||
//
|
||||
// However, things are complicated by the fact that our data is stored in two columns. This
|
||||
// means that we do this in two passes. First we execute the above logic for the metadata
|
||||
// column. Then we do it all over again for the value column.
|
||||
|
||||
// First pass - metadata
|
||||
//
|
||||
// Copy the metadata decoder so we can iterate over it again when we read the values in the
|
||||
// second pass
|
||||
let start = out.len();
|
||||
let mut meta_copy = self.meta.clone();
|
||||
let mut meta_out = RleEncoder::from(&mut *out);
|
||||
let mut idx = 0;
|
||||
// Copy everything up to replace.start to the output
|
||||
while idx < replace.start {
|
||||
let val = meta_copy.next().unwrap_or(None);
|
||||
meta_out.append(val.as_ref());
|
||||
idx += 1;
|
||||
}
|
||||
// Now step through replace, skipping our data and inserting the replacement data (if there
|
||||
// is any)
|
||||
let mut meta_replace_with = replace_with.clone();
|
||||
for _ in 0..replace.len() {
|
||||
meta_copy.next();
|
||||
if let Some(val) = meta_replace_with.next() {
|
||||
let val = val?;
|
||||
// Note that we are just constructing metadata values here.
|
||||
let meta_val = &u64::from(ValueMeta::from(&val));
|
||||
meta_out.append(Some(meta_val));
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
// Copy any remaining input from the replacments to the output
|
||||
while let Some(val) = meta_replace_with.next() {
|
||||
let val = val?;
|
||||
let meta_val = &u64::from(ValueMeta::from(&val));
|
||||
meta_out.append(Some(meta_val));
|
||||
idx += 1;
|
||||
}
|
||||
// Now copy any remaining data we have to the output
|
||||
while !meta_copy.done() {
|
||||
let val = meta_copy.next().unwrap_or(None);
|
||||
meta_out.append(val.as_ref());
|
||||
}
|
||||
let meta_len = meta_out.finish();
|
||||
let meta_range = start..(start + meta_len);
|
||||
|
||||
// Second pass, copying the values. For this pass we iterate over ourselves.
|
||||
//
|
||||
//
|
||||
let mut value_range_len = 0;
|
||||
let mut raw_encoder = RawEncoder::from(out);
|
||||
idx = 0;
|
||||
// Copy everything up to replace.start to the output
|
||||
while idx < replace.start {
|
||||
let val = self.next().unwrap().unwrap_or(PrimVal::Null);
|
||||
value_range_len += encode_primval(&mut raw_encoder, &val);
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
// Now step through replace, skipping our data and inserting the replacement data (if there
|
||||
// is any)
|
||||
for _ in 0..replace.len() {
|
||||
self.next();
|
||||
if let Some(val) = replace_with.next() {
|
||||
let val = val?;
|
||||
value_range_len += encode_primval(&mut raw_encoder, &val);
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
// Copy any remaining input from the replacments to the output
|
||||
while let Some(val) = replace_with.next() {
|
||||
let val = val?;
|
||||
value_range_len += encode_primval(&mut raw_encoder, &val);
|
||||
idx += 1;
|
||||
}
|
||||
// Now copy any remaining data we have to the output
|
||||
while !self.done() {
|
||||
let val = self.next().unwrap().unwrap_or(PrimVal::Null);
|
||||
value_range_len += encode_primval(&mut raw_encoder, &val);
|
||||
}
|
||||
|
||||
let value_range = meta_range.end..(meta_range.end + value_range_len);
|
||||
|
||||
Ok((meta_range, value_range))
|
||||
}
|
||||
|
||||
fn parse_raw<R, F: Fn(&[u8]) -> Result<R, DecodeColumnError>>(
|
||||
&mut self,
|
||||
meta: ValueMeta,
|
||||
f: F,
|
||||
) -> Option<Result<R, DecodeColumnError>> {
|
||||
let raw = match self.raw.read_bytes(meta.length()) {
|
||||
Err(e) => {
|
||||
return Some(Err(DecodeColumnError::InvalidValue {
|
||||
column: "value".to_string(),
|
||||
description: e.to_string(),
|
||||
}))
|
||||
}
|
||||
Ok(bytes) => bytes,
|
||||
};
|
||||
let val = match f(&mut &raw[..]) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return Some(Err(e)),
|
||||
};
|
||||
Some(Ok(val))
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_primval(out: &mut RawEncoder, val: &PrimVal) -> usize {
|
||||
match val {
|
||||
PrimVal::Uint(i) => out.append(i),
|
||||
PrimVal::Int(i) => out.append(i),
|
||||
PrimVal::Null => 0,
|
||||
PrimVal::Bool(_) => 0,
|
||||
PrimVal::Timestamp(i) => out.append(i),
|
||||
PrimVal::Float(f) => out.append(f),
|
||||
PrimVal::Counter(i) => out.append(i),
|
||||
PrimVal::String(s) => out.append(&RawBytes::from(s.as_bytes())),
|
||||
PrimVal::Bytes(b) => out.append(&RawBytes::from(&b[..])),
|
||||
PrimVal::Unknown { data, .. } => out.append(&RawBytes::from(&data[..])),
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for ValueDecoder<'a> {
|
||||
type Item = Result<PrimVal<'a>, DecodeColumnError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
ValueDecoder::next(self)
|
||||
}
|
||||
}
|
||||
|
||||
enum ValueType {
|
||||
Null,
|
||||
False,
|
||||
True,
|
||||
Uleb,
|
||||
Leb,
|
||||
Float,
|
||||
String,
|
||||
Bytes,
|
||||
Counter,
|
||||
Timestamp,
|
||||
Unknown(u8),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct ValueMeta(u64);
|
||||
|
||||
impl ValueMeta {
|
||||
fn type_code(&self) -> ValueType {
|
||||
let low_byte = (self.0 & 0b00001111) as u8;
|
||||
match low_byte {
|
||||
0 => ValueType::Null,
|
||||
1 => ValueType::False,
|
||||
2 => ValueType::True,
|
||||
3 => ValueType::Uleb,
|
||||
4 => ValueType::Leb,
|
||||
5 => ValueType::Float,
|
||||
6 => ValueType::String,
|
||||
7 => ValueType::Bytes,
|
||||
8 => ValueType::Counter,
|
||||
9 => ValueType::Timestamp,
|
||||
other => ValueType::Unknown(other),
|
||||
}
|
||||
}
|
||||
|
||||
fn length(&self) -> usize {
|
||||
(self.0 >> 4) as usize
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&PrimVal<'a>> for ValueMeta {
|
||||
fn from(p: &PrimVal<'a>) -> Self {
|
||||
match p {
|
||||
PrimVal::Uint(i) => Self((ulebsize(*i) << 4) | 3),
|
||||
PrimVal::Int(i) => Self((lebsize(*i) << 4) | 4),
|
||||
PrimVal::Null => Self(0),
|
||||
PrimVal::Bool(b) => Self(match b {
|
||||
false => 1,
|
||||
true => 2,
|
||||
}),
|
||||
PrimVal::Timestamp(i) => Self((ulebsize(*i) << 4) | 9),
|
||||
PrimVal::Float(_) => Self((8 << 4) | 5),
|
||||
PrimVal::Counter(i) => Self((ulebsize(*i) << 4) | 8),
|
||||
PrimVal::String(s) => Self(((s.as_bytes().len() as u64) << 4) | 6),
|
||||
PrimVal::Bytes(b) => Self(((b.len() as u64) << 4) | 7),
|
||||
PrimVal::Unknown { type_code, data } => {
|
||||
Self(((data.len() as u64) << 4) | (*type_code as u64))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for ValueMeta {
|
||||
fn from(raw: u64) -> Self {
|
||||
ValueMeta(raw)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ValueMeta> for u64 {
|
||||
fn from(v: ValueMeta) -> Self {
|
||||
v.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&PrimVal<'a>> for ValueType {
|
||||
fn from(p: &PrimVal) -> Self {
|
||||
match p {
|
||||
PrimVal::Uint(_) => ValueType::Uleb,
|
||||
PrimVal::Int(_) => ValueType::Leb,
|
||||
PrimVal::Null => ValueType::Null,
|
||||
PrimVal::Bool(b) => match b {
|
||||
true => ValueType::True,
|
||||
false => ValueType::False,
|
||||
},
|
||||
PrimVal::Timestamp(_) => ValueType::Timestamp,
|
||||
PrimVal::Float(_) => ValueType::Float,
|
||||
PrimVal::Counter(_) => ValueType::Counter,
|
||||
PrimVal::String(_) => ValueType::String,
|
||||
PrimVal::Bytes(_) => ValueType::Bytes,
|
||||
PrimVal::Unknown { type_code, .. } => ValueType::Unknown(*type_code),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ValueType> for u64 {
|
||||
fn from(v: ValueType) -> Self {
|
||||
match v {
|
||||
ValueType::Null => 0,
|
||||
ValueType::False => 1,
|
||||
ValueType::True => 2,
|
||||
ValueType::Uleb => 3,
|
||||
ValueType::Leb => 4,
|
||||
ValueType::Float => 5,
|
||||
ValueType::String => 6,
|
||||
ValueType::Bytes => 7,
|
||||
ValueType::Counter => 8,
|
||||
ValueType::Timestamp => 9,
|
||||
ValueType::Unknown(other) => other as u64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lebsize(val: i64) -> u64 {
|
||||
if val == 0 {
|
||||
return 1;
|
||||
}
|
||||
let numbits = (val as f64).abs().log2().ceil() as u64;
|
||||
let mut numblocks = (numbits as f64 / 7.0).ceil() as u64;
|
||||
// Make room for the sign bit
|
||||
if numbits % 7 == 0 {
|
||||
numblocks += 1;
|
||||
}
|
||||
return numblocks;
|
||||
}
|
||||
|
||||
fn ulebsize(val: u64) -> u64 {
|
||||
if val == 0 {
|
||||
return 1;
|
||||
}
|
||||
let numbits = (val as f64).log2().ceil() as u64;
|
||||
let numblocks = (numbits as f64 / 7.0).ceil() as u64;
|
||||
return numblocks;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::columnar_2::rowblock::encoding::{
|
||||
properties::{splice_scenario, value}, RawDecoder, RleDecoder,
|
||||
};
|
||||
use proptest::prelude::*;
|
||||
|
||||
fn encode_values(vals: &[PrimVal]) -> (Range<usize>, Range<usize>, Vec<u8>) {
|
||||
let mut decoder = ValueDecoder {
|
||||
meta: RleDecoder::from(&[] as &[u8]),
|
||||
raw: RawDecoder::from(&[] as &[u8]),
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
let (meta_range, val_range) = decoder
|
||||
.try_splice(0..0, vals.iter().map(|v| Ok(v.clone())), &mut out)
|
||||
.unwrap();
|
||||
(meta_range, val_range, out)
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_initialize_splice(values in proptest::collection::vec(value(), 0..100)) {
|
||||
let (meta_range, val_range, out) = encode_values(&values);
|
||||
let mut decoder = ValueDecoder{
|
||||
meta: RleDecoder::from(&out[meta_range]),
|
||||
raw: RawDecoder::from(&out[val_range]),
|
||||
};
|
||||
let mut testvals = Vec::new();
|
||||
while !decoder.done() {
|
||||
testvals.push(decoder.next().unwrap().unwrap());
|
||||
}
|
||||
assert_eq!(values, testvals);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_splice_values(scenario in splice_scenario(value())){
|
||||
let (meta_range, val_range, out) = encode_values(&scenario.initial_values);
|
||||
let mut decoder = ValueDecoder{
|
||||
meta: RleDecoder::from(&out[meta_range]),
|
||||
raw: RawDecoder::from(&out[val_range]),
|
||||
};
|
||||
let mut spliced = Vec::new();
|
||||
let (spliced_meta, spliced_val) = decoder
|
||||
.try_splice(
|
||||
scenario.replace_range.clone(),
|
||||
scenario.replacements.clone().into_iter().map(|i| Ok(i)),
|
||||
&mut spliced,
|
||||
).unwrap();
|
||||
let mut spliced_decoder = ValueDecoder{
|
||||
meta: RleDecoder::from(&spliced[spliced_meta]),
|
||||
raw: RawDecoder::from(&spliced[spliced_val]),
|
||||
};
|
||||
let mut result_values = Vec::new();
|
||||
while !spliced_decoder.done() {
|
||||
result_values.push(spliced_decoder.next().unwrap().unwrap());
|
||||
}
|
||||
let mut expected: Vec<_> = scenario.initial_values.clone();
|
||||
expected.splice(scenario.replace_range, scenario.replacements);
|
||||
assert_eq!(result_values, expected);
|
||||
}
|
||||
}
|
||||
}
|
143
automerge/src/columnar_2/rowblock/mod.rs
Normal file
143
automerge/src/columnar_2/rowblock/mod.rs
Normal file
|
@ -0,0 +1,143 @@
|
|||
use std::{borrow::Cow, convert::TryInto};
|
||||
|
||||
use self::column_layout::DocOpColumns;
|
||||
|
||||
use super::{ColumnId, ColumnSpec};
|
||||
|
||||
mod column_layout;
|
||||
pub(crate) use column_layout::doc_change_columns;
|
||||
pub(crate) use column_layout::doc_op_columns;
|
||||
pub(crate) use column_layout::change_op_columns;
|
||||
pub(crate) use column_layout::{BadColumnLayout, ColumnLayout};
|
||||
mod column_range;
|
||||
mod encoding;
|
||||
pub(crate) use encoding::Key;
|
||||
use encoding::{DecodeColumnError, GenericColDecoder};
|
||||
mod value;
|
||||
pub(crate) use value::{CellValue, PrimVal};
|
||||
|
||||
pub(crate) struct RowBlock<'a, C> {
|
||||
columns: C,
|
||||
data: Cow<'a, [u8]>,
|
||||
}
|
||||
|
||||
impl<'a> RowBlock<'a, ColumnLayout> {
|
||||
pub(crate) fn new<I: Iterator<Item = (ColumnSpec, std::ops::Range<usize>)>>(
|
||||
cols: I,
|
||||
data: Cow<'a, [u8]>,
|
||||
) -> Result<RowBlock<'a, ColumnLayout>, BadColumnLayout> {
|
||||
let layout = ColumnLayout::parse(data.len(), cols)?;
|
||||
Ok(RowBlock {
|
||||
columns: layout,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn into_doc_ops(
|
||||
self,
|
||||
) -> Result<RowBlock<'a, column_layout::DocOpColumns>, column_layout::ParseDocColumnError> {
|
||||
let doc_cols: column_layout::DocOpColumns = self.columns.try_into()?;
|
||||
Ok(RowBlock {
|
||||
columns: doc_cols,
|
||||
data: self.data,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn into_doc_change(
|
||||
self,
|
||||
) -> Result<
|
||||
RowBlock<'a, column_layout::doc_change_columns::DocChangeColumns>,
|
||||
column_layout::doc_change_columns::DecodeChangeError,
|
||||
> {
|
||||
let doc_cols: column_layout::doc_change_columns::DocChangeColumns =
|
||||
self.columns.try_into()?;
|
||||
Ok(RowBlock {
|
||||
columns: doc_cols,
|
||||
data: self.data,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn into_change_ops(
|
||||
self
|
||||
) -> Result<RowBlock<'a, change_op_columns::ChangeOpsColumns>, change_op_columns::ParseChangeColumnsError> {
|
||||
let change_cols: change_op_columns::ChangeOpsColumns = self.columns.try_into()?;
|
||||
Ok(RowBlock {
|
||||
columns: change_cols,
|
||||
data: self.data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> IntoIterator for &'a RowBlock<'b, ColumnLayout> {
|
||||
type Item = Result<Vec<(usize, CellValue<'a>)>, DecodeColumnError>;
|
||||
type IntoIter = RowBlockIter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
RowBlockIter {
|
||||
failed: false,
|
||||
decoders: self
|
||||
.columns
|
||||
.iter()
|
||||
.map(|c| (c.id(), c.decoder(&self.data)))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RowBlockIter<'a> {
|
||||
failed: bool,
|
||||
decoders: Vec<(ColumnId, GenericColDecoder<'a>)>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for RowBlockIter<'a> {
|
||||
type Item = Result<Vec<(usize, CellValue<'a>)>, DecodeColumnError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.failed {
|
||||
return None;
|
||||
}
|
||||
if self.decoders.iter().all(|(_, d)| d.done()) {
|
||||
None
|
||||
} else {
|
||||
let mut result = Vec::with_capacity(self.decoders.len());
|
||||
for (col_index, (_, decoder)) in self.decoders.iter_mut().enumerate() {
|
||||
match decoder.next() {
|
||||
Some(Ok(c)) => result.push((col_index, c)),
|
||||
Some(Err(e)) => {
|
||||
self.failed = true;
|
||||
return Some(Err(e));
|
||||
},
|
||||
None => {},
|
||||
}
|
||||
}
|
||||
Some(Ok(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a RowBlock<'a, DocOpColumns> {
|
||||
type Item = Result<doc_op_columns::DocOp<'a>, column_layout::doc_op_columns::DecodeOpError>;
|
||||
type IntoIter = column_layout::doc_op_columns::DocOpColumnIter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.columns.iter(&self.data)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a RowBlock<'a, doc_change_columns::DocChangeColumns> {
|
||||
type Item = Result<doc_change_columns::ChangeMetadata<'a>, doc_change_columns::DecodeChangeError>;
|
||||
type IntoIter = doc_change_columns::DocChangeColumnIter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.columns.iter(&self.data)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a RowBlock<'a, change_op_columns::ChangeOpsColumns> {
|
||||
type Item = Result<change_op_columns::ChangeOp<'a>, change_op_columns::ReadChangeOpError>;
|
||||
type IntoIter = change_op_columns::ChangeOpsIter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.columns.iter(&self.data)
|
||||
}
|
||||
}
|
105
automerge/src/columnar_2/rowblock/value.rs
Normal file
105
automerge/src/columnar_2/rowblock/value.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
use crate::ScalarValue;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use smol_str::SmolStr;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum CellValue<'a> {
|
||||
Uint(u64),
|
||||
Int(i64),
|
||||
Bool(bool),
|
||||
String(Cow<'a, SmolStr>),
|
||||
Value(PrimVal<'a>),
|
||||
List(Vec<Vec<CellValue<'a>>>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) enum PrimVal<'a> {
|
||||
Null,
|
||||
Bool(bool),
|
||||
Uint(u64),
|
||||
Int(i64),
|
||||
Float(f64),
|
||||
String(Cow<'a, SmolStr>),
|
||||
Bytes(Cow<'a, [u8]>),
|
||||
Counter(u64),
|
||||
Timestamp(u64),
|
||||
Unknown { type_code: u8, data: Vec<u8> },
|
||||
}
|
||||
|
||||
impl<'a> PrimVal<'a> {
|
||||
pub(crate) fn into_owned(self) -> PrimVal<'static> {
|
||||
match self {
|
||||
PrimVal::String(s) => PrimVal::String(Cow::Owned(s.into_owned().into())),
|
||||
PrimVal::Bytes(b) => PrimVal::Bytes(Cow::Owned(b.to_vec())),
|
||||
PrimVal::Null => PrimVal::Null,
|
||||
PrimVal::Bool(b) => PrimVal::Bool(b),
|
||||
PrimVal::Uint(u) => PrimVal::Uint(u),
|
||||
PrimVal::Int(i) => PrimVal::Int(i),
|
||||
PrimVal::Float(f) => PrimVal::Float(f),
|
||||
PrimVal::Counter(u) => PrimVal::Counter(u),
|
||||
PrimVal::Timestamp(u) => PrimVal::Timestamp(u),
|
||||
PrimVal::Unknown { type_code, data } => PrimVal::Unknown{ type_code, data},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<PrimVal<'a>> for ScalarValue {
|
||||
fn from(p: PrimVal) -> Self {
|
||||
match p {
|
||||
PrimVal::Null => Self::Null,
|
||||
PrimVal::Bool(b) => Self::Boolean(b),
|
||||
PrimVal::Uint(u) => Self::Uint(u),
|
||||
PrimVal::Int(i) => Self::Int(i),
|
||||
PrimVal::Float(f) => Self::F64(f),
|
||||
PrimVal::String(s) => Self::Str(s.into_owned()),
|
||||
PrimVal::Bytes(b) => Self::Bytes(b.to_vec()),
|
||||
PrimVal::Counter(c) => Self::Counter((c as i64).into()),
|
||||
PrimVal::Timestamp(t) => Self::Timestamp(t as i64),
|
||||
PrimVal::Unknown { data, .. } => Self::Bytes(data),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<ScalarValue> for PrimVal<'static> {
|
||||
fn from(s: ScalarValue) -> Self {
|
||||
match s {
|
||||
ScalarValue::Null => PrimVal::Null,
|
||||
ScalarValue::Boolean(b) => PrimVal::Bool(b),
|
||||
ScalarValue::Uint(u) => PrimVal::Uint(u),
|
||||
ScalarValue::Int(i) => PrimVal::Int(i),
|
||||
ScalarValue::F64(f) => PrimVal::Float(f),
|
||||
ScalarValue::Str(s) => PrimVal::String(Cow::Owned(s)),
|
||||
// This is bad, if there was an unknown type code in the primval we have lost it on the
|
||||
// round trip
|
||||
ScalarValue::Bytes(b) => PrimVal::Bytes(Cow::Owned(b)),
|
||||
ScalarValue::Counter(c) => PrimVal::Counter(c.current as u64),
|
||||
ScalarValue::Timestamp(t) => PrimVal::Timestamp(t as u64),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&ScalarValue> for PrimVal<'static> {
|
||||
fn from(s: &ScalarValue) -> Self {
|
||||
match s {
|
||||
ScalarValue::Null => PrimVal::Null,
|
||||
ScalarValue::Boolean(b) => PrimVal::Bool(*b),
|
||||
ScalarValue::Uint(u) => PrimVal::Uint(*u),
|
||||
ScalarValue::Int(i) => PrimVal::Int(*i),
|
||||
ScalarValue::F64(f) => PrimVal::Float(*f),
|
||||
ScalarValue::Str(s) => PrimVal::String(Cow::Owned(s.clone())),
|
||||
// This is bad, if there was an unknown type code in the primval we have lost it on the
|
||||
// round trip
|
||||
ScalarValue::Bytes(b) => PrimVal::Bytes(Cow::Owned(b.clone())),
|
||||
ScalarValue::Counter(c) => PrimVal::Counter(c.current as u64),
|
||||
ScalarValue::Timestamp(t) => PrimVal::Timestamp((*t) as u64),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [u8]> for PrimVal<'a> {
|
||||
fn from(d: &'a [u8]) -> Self {
|
||||
PrimVal::Bytes(Cow::Borrowed(d))
|
||||
}
|
||||
}
|
256
automerge/src/columnar_2/save.rs
Normal file
256
automerge/src/columnar_2/save.rs
Normal file
|
@ -0,0 +1,256 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
iter::Iterator,
|
||||
};
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::{
|
||||
columnar_2::{
|
||||
rowblock::{
|
||||
change_op_columns::{ChangeOp, ChangeOpsColumns},
|
||||
doc_change_columns::{ChangeMetadata, DocChangeColumns},
|
||||
doc_op_columns::{DocOp, DocOpColumns},
|
||||
Key as EncodedKey, PrimVal,
|
||||
},
|
||||
storage::{Chunk, Document},
|
||||
},
|
||||
indexed_cache::IndexedCache,
|
||||
types::{ActorId, ElemId, Key, ObjId, Op, OpId, OpType},
|
||||
Change, ChangeHash,
|
||||
};
|
||||
|
||||
/// # Panics
|
||||
///
|
||||
/// * If any of the `heads` are not in `changes`
|
||||
/// * If any of ops in `ops` reference an actor which is not in `actors`
|
||||
/// * If any of ops in `ops` reference a property which is not in `props`
|
||||
/// * If any of the changes reference a dependency index which is not in `changes`
|
||||
pub(crate) fn save_document<'a, I, O>(
|
||||
changes: I,
|
||||
ops: O,
|
||||
actors: &'a IndexedCache<ActorId>,
|
||||
props: &IndexedCache<String>,
|
||||
heads: &[ChangeHash],
|
||||
) -> Vec<u8>
|
||||
where
|
||||
I: Iterator<Item = &'a Change> + Clone + 'a,
|
||||
O: Iterator<Item = (&'a ObjId, &'a Op)> + Clone,
|
||||
{
|
||||
let actor_lookup = actors.encode_index();
|
||||
let doc_ops = ops.map(|(obj, op)| DocOp {
|
||||
id: translate_opid(&op.id, &actor_lookup),
|
||||
insert: op.insert,
|
||||
object: translate_objid(obj, &actor_lookup),
|
||||
key: translate_key(&op.key, props),
|
||||
action: op.action.action_index() as usize,
|
||||
value: match &op.action {
|
||||
OpType::Set(v) => v.into(),
|
||||
OpType::Inc(i) => PrimVal::Int(*i),
|
||||
_ => PrimVal::Null,
|
||||
},
|
||||
succ: op
|
||||
.succ
|
||||
.iter()
|
||||
.map(|o| translate_opid(o, &actor_lookup))
|
||||
.collect(),
|
||||
});
|
||||
let mut ops_out = Vec::new();
|
||||
let ops_meta = DocOpColumns::encode(doc_ops, &mut ops_out);
|
||||
|
||||
let mut change_out = Vec::new();
|
||||
let hash_graph = HashGraph::new(changes.clone(), heads);
|
||||
let cols = DocChangeColumns::encode(
|
||||
changes.map(|c| hash_graph.construct_change(c, &actor_lookup, actors)),
|
||||
&mut change_out,
|
||||
);
|
||||
|
||||
let doc = Document {
|
||||
actors: actors.sorted().cache,
|
||||
heads: heads.to_vec(),
|
||||
op_metadata: ops_meta.metadata(),
|
||||
op_bytes: Cow::Owned(ops_out),
|
||||
change_metadata: cols.metadata(),
|
||||
change_bytes: Cow::Owned(change_out),
|
||||
head_indices: hash_graph.head_indices,
|
||||
};
|
||||
|
||||
let written = doc.write();
|
||||
let chunk = Chunk::new_document(&written);
|
||||
chunk.write()
|
||||
}
|
||||
|
||||
pub(crate) fn encode_change_ops<'a, O>(
|
||||
ops: O,
|
||||
change_actor: ActorId,
|
||||
actors: &IndexedCache<ActorId>,
|
||||
props: &IndexedCache<String>,
|
||||
) -> (ChangeOpsColumns, Vec<u8>, Vec<ActorId>)
|
||||
where
|
||||
O: Iterator<Item = (&'a ObjId, &'a Op)> + Clone,
|
||||
{
|
||||
let encoded_actors = actor_ids_in_change(ops.clone(), change_actor.clone(), actors);
|
||||
let actor_lookup = actors
|
||||
.cache
|
||||
.iter()
|
||||
.map(|a| encoded_actors.iter().position(|r| r == a).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let change_ops = ops.map(|(obj, op)| ChangeOp {
|
||||
insert: op.insert,
|
||||
obj: translate_objid(obj, &actor_lookup),
|
||||
key: translate_key(&op.key, props),
|
||||
action: op.action.action_index(),
|
||||
val: match &op.action {
|
||||
OpType::Set(v) => v.into(),
|
||||
OpType::Inc(i) => PrimVal::Int(*i),
|
||||
_ => PrimVal::Null,
|
||||
},
|
||||
pred: op
|
||||
.pred
|
||||
.iter()
|
||||
.map(|o| translate_opid(o, &actor_lookup))
|
||||
.collect(),
|
||||
});
|
||||
let mut out = Vec::new();
|
||||
let cols = ChangeOpsColumns::empty().encode(change_ops, &mut out);
|
||||
let other_actors = encoded_actors.into_iter().skip(1).collect();
|
||||
(cols, out, other_actors)
|
||||
}
|
||||
|
||||
/// When encoding a change chunk we take all the actor IDs referenced by a change and place them in
|
||||
/// an array. The array has the actor who authored the change as the first element and all
|
||||
/// remaining actors (i.e. those referenced in object IDs in the target of an operation or in the
|
||||
/// `pred` of an operation) lexicographically ordered following the change author.
|
||||
fn actor_ids_in_change<'a, I>(
|
||||
ops: I,
|
||||
change_actor: ActorId,
|
||||
actors: &IndexedCache<ActorId>,
|
||||
) -> Vec<ActorId>
|
||||
where
|
||||
I: Iterator<Item = (&'a ObjId, &'a Op)> + Clone,
|
||||
{
|
||||
let mut other_ids: Vec<ActorId> = ops
|
||||
.flat_map(|(obj, o)| opids_in_operation(&obj, &o, actors))
|
||||
.filter(|a| *a != &change_actor)
|
||||
.unique()
|
||||
.cloned()
|
||||
.collect();
|
||||
other_ids.sort();
|
||||
// Now prepend the change actor
|
||||
std::iter::once(change_actor)
|
||||
.chain(other_ids.into_iter())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn opids_in_operation<'a>(
|
||||
obj: &'a ObjId,
|
||||
op: &'a Op,
|
||||
actors: &'a IndexedCache<ActorId>,
|
||||
) -> impl Iterator<Item = &'a ActorId> {
|
||||
let obj_actor_id = if obj.is_root() {
|
||||
None
|
||||
} else {
|
||||
Some(actors.get(obj.opid().actor()))
|
||||
};
|
||||
let pred_ids = op.pred.iter().filter_map(|a| {
|
||||
if a.counter() != 0 {
|
||||
Some(actors.get(a.actor()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let key_actor = match &op.key {
|
||||
Key::Seq(ElemId(op)) if !op.counter() == 0 => Some(actors.get(op.actor())),
|
||||
_ => None,
|
||||
};
|
||||
obj_actor_id
|
||||
.into_iter()
|
||||
.chain(key_actor.into_iter())
|
||||
.chain(pred_ids)
|
||||
}
|
||||
|
||||
fn translate_key(k: &Key, props: &IndexedCache<String>) -> EncodedKey {
|
||||
match k {
|
||||
Key::Seq(e) => EncodedKey::Elem(*e),
|
||||
Key::Map(idx) => EncodedKey::Prop(props.get(*idx).into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn translate_objid(obj: &ObjId, actors: &[usize]) -> ObjId {
|
||||
if obj.is_root() {
|
||||
*obj
|
||||
} else {
|
||||
ObjId(translate_opid(&obj.opid(), actors))
|
||||
}
|
||||
}
|
||||
|
||||
fn translate_opid(id: &OpId, actors: &[usize]) -> OpId {
|
||||
OpId::new(actors[id.actor()], id.counter())
|
||||
}
|
||||
|
||||
fn find_head_indices<'a, I>(changes: I, heads: &[ChangeHash]) -> Vec<u64>
|
||||
where
|
||||
I: Iterator<Item = &'a Change>,
|
||||
{
|
||||
let heads_set: BTreeSet<ChangeHash> = heads.iter().copied().collect();
|
||||
let mut head_indices = BTreeMap::new();
|
||||
for (index, change) in changes.enumerate() {
|
||||
if heads_set.contains(&change.hash()) {
|
||||
head_indices.insert(change.hash(), index as u64);
|
||||
}
|
||||
}
|
||||
heads.iter().map(|h| head_indices[h]).collect()
|
||||
}
|
||||
|
||||
struct HashGraph {
|
||||
head_indices: Vec<u64>,
|
||||
index_by_hash: BTreeMap<ChangeHash, usize>,
|
||||
}
|
||||
|
||||
impl HashGraph {
|
||||
fn new<'a, I>(changes: I, heads: &[ChangeHash]) -> Self
|
||||
where
|
||||
I: Iterator<Item = &'a Change>,
|
||||
{
|
||||
let heads_set: BTreeSet<ChangeHash> = heads.iter().copied().collect();
|
||||
let mut head_indices = BTreeMap::new();
|
||||
let mut index_by_hash = BTreeMap::new();
|
||||
for (index, change) in changes.enumerate() {
|
||||
if heads_set.contains(&change.hash()) {
|
||||
head_indices.insert(change.hash(), index as u64);
|
||||
}
|
||||
index_by_hash.insert(change.hash(), index);
|
||||
}
|
||||
let head_indices = heads.iter().map(|h| head_indices[h]).collect();
|
||||
Self {
|
||||
head_indices,
|
||||
index_by_hash,
|
||||
}
|
||||
}
|
||||
|
||||
fn change_index(&self, hash: &ChangeHash) -> usize {
|
||||
self.index_by_hash[hash]
|
||||
}
|
||||
|
||||
fn construct_change(
|
||||
&self,
|
||||
c: &Change,
|
||||
actor_lookup: &[usize],
|
||||
actors: &IndexedCache<ActorId>,
|
||||
) -> ChangeMetadata<'static> {
|
||||
ChangeMetadata {
|
||||
actor: actor_lookup[actors.lookup(c.actor_id()).unwrap()],
|
||||
seq: c.seq(),
|
||||
max_op: c.max_op(),
|
||||
timestamp: c.timestamp(),
|
||||
message: c.message().map(|s| s.into()),
|
||||
deps: c
|
||||
.deps()
|
||||
.iter()
|
||||
.map(|d| self.change_index(d) as u64)
|
||||
.collect(),
|
||||
extra: Cow::Owned(c.extra_bytes().to_vec()),
|
||||
}
|
||||
}
|
||||
}
|
114
automerge/src/columnar_2/storage/change.rs
Normal file
114
automerge/src/columnar_2/storage/change.rs
Normal file
|
@ -0,0 +1,114 @@
|
|||
use std::{borrow::Cow, io::Write};
|
||||
|
||||
use crate::{ActorId, ChangeHash};
|
||||
|
||||
use super::{parse, ColumnMetadata, Chunk};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Change<'a> {
|
||||
pub(crate) dependencies: Vec<ChangeHash>,
|
||||
pub(crate) actor: ActorId,
|
||||
pub(crate) other_actors: Vec<ActorId>,
|
||||
pub(crate) seq: u64,
|
||||
pub(crate) start_op: u64,
|
||||
pub(crate) timestamp: i64,
|
||||
pub(crate) message: Option<String>,
|
||||
pub(crate) ops_meta: ColumnMetadata,
|
||||
pub(crate) ops_data: Cow<'a, [u8]>,
|
||||
pub(crate) extra_bytes: Cow<'a, [u8]>,
|
||||
}
|
||||
|
||||
impl<'a> Change<'a> {
|
||||
pub(crate) fn parse(input: &'a [u8]) -> parse::ParseResult<Change<'a>> {
|
||||
let (i, deps) = parse::length_prefixed(parse::leb128_u64, parse::change_hash)(input)?;
|
||||
let (i, actor) = parse::actor_id(i)?;
|
||||
let (i, seq) = parse::leb128_u64(i)?;
|
||||
let (i, start_op) = parse::leb128_u64(i)?;
|
||||
let (i, timestamp) = parse::leb128_i64(i)?;
|
||||
let (i, message_len) = parse::leb128_u64(i)?;
|
||||
let (i, message) = parse::utf_8(message_len as usize, i)?;
|
||||
let (i, other_actors) = parse::length_prefixed(parse::leb128_u64, parse::actor_id)(i)?;
|
||||
let (i, ops_meta) = ColumnMetadata::parse(i)?;
|
||||
let (i, ops_data) = parse::take_n(ops_meta.total_column_len(), i)?;
|
||||
Ok((
|
||||
&[],
|
||||
Change {
|
||||
dependencies: deps,
|
||||
actor,
|
||||
other_actors,
|
||||
seq,
|
||||
start_op,
|
||||
timestamp,
|
||||
message: if message.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(message)
|
||||
},
|
||||
ops_meta,
|
||||
ops_data: Cow::Borrowed(ops_data),
|
||||
extra_bytes: Cow::Borrowed(i),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn byte_len(&self) -> usize {
|
||||
(self.dependencies.len() * 32)
|
||||
+ 8
|
||||
+ self.actor.to_bytes().len()
|
||||
+ 24 // seq, start op, timestamp
|
||||
+ 8
|
||||
+ self.message.as_ref().map(|m| m.as_bytes().len()).unwrap_or(0_usize)
|
||||
+ self.other_actors.iter().map(|a| a.to_bytes().len() + 8_usize).sum::<usize>()
|
||||
+ self.ops_meta.byte_len()
|
||||
+ self.ops_data.len()
|
||||
+ self.extra_bytes.len()
|
||||
}
|
||||
|
||||
pub(crate) fn write(&self) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(self.byte_len());
|
||||
leb128::write::unsigned(&mut out, self.dependencies.len() as u64).unwrap();
|
||||
for dep in &self.dependencies {
|
||||
out.write_all(dep.as_bytes()).unwrap();
|
||||
}
|
||||
length_prefixed_bytes(&self.actor, &mut out);
|
||||
leb128::write::unsigned(&mut out, self.seq).unwrap();
|
||||
leb128::write::unsigned(&mut out, self.start_op).unwrap();
|
||||
leb128::write::signed(&mut out, self.timestamp).unwrap();
|
||||
length_prefixed_bytes(self.message.as_ref().map(|m| m.as_bytes()).unwrap_or(&[]), &mut out);
|
||||
leb128::write::unsigned(&mut out, self.other_actors.len() as u64).unwrap();
|
||||
for actor in self.other_actors.iter() {
|
||||
length_prefixed_bytes(&actor, &mut out);
|
||||
}
|
||||
self.ops_meta.write(&mut out);
|
||||
out.write_all(self.ops_data.as_ref()).unwrap();
|
||||
out.write_all(self.extra_bytes.as_ref()).unwrap();
|
||||
out
|
||||
}
|
||||
|
||||
pub(crate) fn hash(&self) -> ChangeHash {
|
||||
let this = self.write();
|
||||
let chunk = Chunk::new_change(&this);
|
||||
chunk.hash()
|
||||
}
|
||||
|
||||
pub(crate) fn into_owned(self) -> Change<'static> {
|
||||
Change{
|
||||
dependencies: self.dependencies,
|
||||
actor: self.actor,
|
||||
other_actors: self.other_actors,
|
||||
seq: self.seq,
|
||||
start_op: self.start_op,
|
||||
timestamp: self.timestamp,
|
||||
message: self.message,
|
||||
ops_meta: self.ops_meta,
|
||||
ops_data: Cow::Owned(self.ops_data.into_owned()),
|
||||
extra_bytes: Cow::Owned(self.extra_bytes.into_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn length_prefixed_bytes<B: AsRef<[u8]>>(b: B, out: &mut Vec<u8>) -> usize {
|
||||
let prefix_len = leb128::write::unsigned(out, b.as_ref().len() as u64).unwrap();
|
||||
out.write_all(b.as_ref()).unwrap();
|
||||
prefix_len + b.as_ref().len()
|
||||
}
|
163
automerge/src/columnar_2/storage/chunk.rs
Normal file
163
automerge/src/columnar_2/storage/chunk.rs
Normal file
|
@ -0,0 +1,163 @@
|
|||
use std::{borrow::Cow, convert::{TryFrom, TryInto}};
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::ChangeHash;
|
||||
use super::parse;
|
||||
|
||||
|
||||
const MAGIC_BYTES: [u8; 4] = [0x85, 0x6f, 0x4a, 0x83];
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) enum ChunkType {
|
||||
Document,
|
||||
Change,
|
||||
Compressed,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for ChunkType {
|
||||
type Error = u8;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::Document),
|
||||
1 => Ok(Self::Change),
|
||||
2 => Ok(Self::Compressed),
|
||||
other => Err(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChunkType> for u8 {
|
||||
fn from(ct: ChunkType) -> Self {
|
||||
match ct {
|
||||
ChunkType::Document => 0,
|
||||
ChunkType::Change => 1,
|
||||
ChunkType::Compressed => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) struct CheckSum([u8; 4]);
|
||||
|
||||
impl CheckSum {
|
||||
fn bytes(&self) -> [u8; 4] {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; 4]> for CheckSum {
|
||||
fn from(raw: [u8; 4]) -> Self {
|
||||
CheckSum(raw)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChangeHash> for CheckSum {
|
||||
fn from(h: ChangeHash) -> Self {
|
||||
let bytes = h.as_bytes();
|
||||
[bytes[0], bytes[1], bytes[2], bytes[3]].into()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Chunk<'a> {
|
||||
typ: ChunkType,
|
||||
checksum: CheckSum,
|
||||
data: Cow<'a, [u8]>,
|
||||
}
|
||||
|
||||
impl<'a> Chunk<'a> {
|
||||
pub(crate) fn new_change(data: &'a [u8]) -> Chunk<'a> {
|
||||
let hash_result = hash(ChunkType::Change, data);
|
||||
Chunk{
|
||||
typ: ChunkType::Change,
|
||||
checksum: hash_result.into(),
|
||||
data: Cow::Borrowed(data),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_document(data: &'a [u8]) -> Chunk<'a> {
|
||||
let hash_result = hash(ChunkType::Document, data);
|
||||
Chunk{
|
||||
typ: ChunkType::Document,
|
||||
checksum: hash_result.into(),
|
||||
data: Cow::Borrowed(data),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse(input: &'a [u8]) -> parse::ParseResult<Chunk<'a>> {
|
||||
let (i, magic) = parse::take4(input)?;
|
||||
if magic != MAGIC_BYTES {
|
||||
return Err(parse::ParseError::Error(
|
||||
parse::ErrorKind::InvalidMagicBytes,
|
||||
));
|
||||
}
|
||||
let (i, checksum_bytes) = parse::take4(i)?;
|
||||
let (i, raw_chunk_type) = parse::take1(i)?;
|
||||
let chunk_type: ChunkType = raw_chunk_type
|
||||
.try_into()
|
||||
.map_err(|e| parse::ParseError::Error(parse::ErrorKind::UnknownChunkType(e)))?;
|
||||
let (i, chunk_len) = parse::leb128_u64(i)?;
|
||||
let (i, data) = parse::take_n(chunk_len as usize, i)?;
|
||||
Ok((
|
||||
i,
|
||||
Chunk {
|
||||
typ: chunk_type,
|
||||
checksum: checksum_bytes.into(),
|
||||
data: Cow::Borrowed(data),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn byte_len(&self) -> usize {
|
||||
MAGIC_BYTES.len()
|
||||
+ 1 // chunk type
|
||||
+ 4 // checksum
|
||||
+ 5 //length
|
||||
+ self.data.len()
|
||||
}
|
||||
|
||||
pub(crate) fn write(&self) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(self.byte_len());
|
||||
out.extend(MAGIC_BYTES);
|
||||
out.extend(self.checksum.bytes());
|
||||
out.push(u8::from(self.typ));
|
||||
leb128::write::unsigned(&mut out, self.data.len() as u64).unwrap();
|
||||
out.extend(self.data.as_ref());
|
||||
out
|
||||
}
|
||||
|
||||
pub(crate) fn checksum_valid(&self) -> bool {
|
||||
let hash = self.hash();
|
||||
let checksum = CheckSum(hash.checksum());
|
||||
checksum == self.checksum
|
||||
}
|
||||
|
||||
pub(crate) fn hash(&self) -> ChangeHash {
|
||||
hash(self.typ, self.data.as_ref())
|
||||
}
|
||||
|
||||
pub(crate) fn typ(&self) -> ChunkType {
|
||||
self.typ
|
||||
}
|
||||
|
||||
pub(crate) fn checksum(&self) -> CheckSum {
|
||||
self.checksum
|
||||
}
|
||||
|
||||
pub(crate) fn data(&self) -> Cow<'a, [u8]> {
|
||||
self.data.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn hash(typ: ChunkType, data: &[u8]) -> ChangeHash {
|
||||
let mut out = Vec::new();
|
||||
out.push(u8::from(typ));
|
||||
leb128::write::unsigned(&mut out, data.len() as u64).unwrap();
|
||||
out.extend(data.as_ref());
|
||||
let hash_result = Sha256::digest(out);
|
||||
let array: [u8; 32] = hash_result.into();
|
||||
ChangeHash(array)
|
||||
}
|
85
automerge/src/columnar_2/storage/column_metadata.rs
Normal file
85
automerge/src/columnar_2/storage/column_metadata.rs
Normal file
|
@ -0,0 +1,85 @@
|
|||
use std::ops::Range;
|
||||
|
||||
use super::{super::ColumnSpec, parse};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Column {
|
||||
spec: ColumnSpec,
|
||||
data: Range<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ColumnMetadata(Vec<Column>);
|
||||
|
||||
impl FromIterator<Column> for ColumnMetadata {
|
||||
fn from_iter<T: IntoIterator<Item = Column>>(iter: T) -> Self {
|
||||
Self(iter.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<(ColumnSpec, Range<usize>)> for ColumnMetadata {
|
||||
fn from_iter<T: IntoIterator<Item = (ColumnSpec, Range<usize>)>>(iter: T) -> Self {
|
||||
Self(iter.into_iter().map(|(spec, data)| Column{spec, data}).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl ColumnMetadata {
|
||||
pub(crate) fn parse(input: &[u8]) -> parse::ParseResult<ColumnMetadata> {
|
||||
let i = input;
|
||||
let (i, num_columns) = parse::leb128_u64(i)?;
|
||||
let (i, specs_and_lens) = parse::apply_n(
|
||||
num_columns as usize,
|
||||
parse::tuple2(
|
||||
parse::map(parse::leb128_u32, ColumnSpec::from),
|
||||
parse::leb128_u64,
|
||||
),
|
||||
)(i)?;
|
||||
let columns = specs_and_lens
|
||||
.into_iter()
|
||||
.scan(0_usize, |offset, (spec, len)| {
|
||||
let end = *offset + len as usize;
|
||||
let data = *offset..end;
|
||||
*offset = end;
|
||||
Some(Column { spec, data })
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !are_normal_sorted(&columns) {
|
||||
return Err(parse::ParseError::Error(
|
||||
parse::ErrorKind::InvalidColumnMetadataSort,
|
||||
));
|
||||
}
|
||||
Ok((i, ColumnMetadata(columns)))
|
||||
}
|
||||
|
||||
pub(crate) fn write(&self, out: &mut Vec<u8>) -> usize {
|
||||
let mut written = leb128::write::unsigned(out, self.0.len() as u64).unwrap();
|
||||
for col in &self.0 {
|
||||
written += leb128::write::unsigned(out, u32::from(col.spec) as u64).unwrap();
|
||||
written += leb128::write::unsigned(out, col.data.len() as u64).unwrap();
|
||||
}
|
||||
written
|
||||
}
|
||||
|
||||
pub(crate) fn total_column_len(&self) -> usize {
|
||||
self.0.iter().map(|c| c.data.len()).sum()
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = (ColumnSpec, Range<usize>)> + '_ {
|
||||
self.0.iter().map(|c| (c.spec, c.data.clone()))
|
||||
}
|
||||
|
||||
pub(crate) fn byte_len(&self) -> usize {
|
||||
self.0.len() * 16
|
||||
}
|
||||
}
|
||||
|
||||
fn are_normal_sorted(cols: &[Column]) -> bool {
|
||||
if cols.len() > 1 {
|
||||
for (i, col) in cols[1..].iter().enumerate() {
|
||||
if col.spec.normalize() < cols[i].spec.normalize() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue