Compare commits
145 commits
Author | SHA1 | Date | |
---|---|---|---|
|
cb409b6ffe | ||
|
b34b46fa16 | ||
|
7b747b8341 | ||
|
2c1970f664 | ||
|
63b761c0d1 | ||
|
44fa7ac416 | ||
|
8de2fa9bd4 | ||
|
407faefa6e | ||
|
1425af43cd | ||
|
c92d042c87 | ||
|
9271b20cf5 | ||
|
5e82dbc3c8 | ||
|
2cd7427f35 | ||
|
11f063cbfe | ||
|
a24d536d16 | ||
|
c5fde2802f | ||
|
13a775ed9a | ||
|
1e33c9d9e0 | ||
|
c3c04128f5 | ||
|
da55dfac7a | ||
|
9195e9cb76 | ||
|
f8d5a8ea98 | ||
|
2a9652e642 | ||
|
a6959e70e8 | ||
|
de5af2fffa | ||
|
08801ab580 | ||
|
89a0866272 | ||
|
9b6a3c8691 | ||
|
58a7a06b75 | ||
|
f428fe0169 | ||
|
931ee7e77b | ||
|
819767cc33 | ||
|
78adbc4ff9 | ||
|
1f7b109dcd | ||
|
98e755106f | ||
|
6b0ee6da2e | ||
|
9b44a75f69 | ||
|
d8baa116e7 | ||
|
5629a7bec4 | ||
|
964ae2bd81 | ||
|
d8df1707d9 | ||
|
681a3f1f3f | ||
|
22e9915fac | ||
|
2d8df12522 | ||
|
f073dbf701 | ||
|
5c02445bee | ||
|
3ef60747f4 | ||
|
d12bd3bb06 | ||
|
a0d698dc8e | ||
|
93a257896e | ||
|
9c3d0976c8 | ||
|
1ca1cc38ef | ||
|
0e7fb6cc10 | ||
|
d1220b9dd0 | ||
|
6c0d102032 | ||
|
5763210b07 | ||
|
18a3f61704 | ||
|
0306ade939 | ||
|
1e7dcdedec | ||
|
8a645bb193 | ||
|
4de0756bb4 | ||
|
d678280b57 | ||
|
f682db3039 | ||
|
6da93b6adc | ||
|
0f90fe4d02 | ||
|
8aff1296b9 | ||
|
6dad2b7df1 | ||
|
e75ca2a834 | ||
|
3229548fc7 | ||
|
a96f77c96b | ||
|
b78211ca65 | ||
|
1222fc0df1 | ||
|
2db9e78f2a | ||
|
b05c9e83a4 | ||
|
c3932e6267 | ||
|
becc301877 | ||
|
0ab6a770d8 | ||
|
2826f4f08c | ||
|
de16adbcc5 | ||
|
ea5688e418 | ||
|
149f870102 | ||
|
e0b2bc995a | ||
|
aaddb3c9ea | ||
|
2400d67755 | ||
|
d3885a3443 | ||
|
f8428896bd | ||
|
fb0c69cc52 | ||
|
edbb33522d | ||
|
625f48f33a | ||
|
7c9f927136 | ||
|
b60c310f5c | ||
|
3dd954d5b7 | ||
|
3e2e697504 | ||
|
a324b02005 | ||
|
d26cb0c0cb | ||
|
ed108ba6fc | ||
|
484a5bac4f | ||
|
01350c2b3f | ||
|
22d60987f6 | ||
|
bbf729e1d6 | ||
|
ca25ed0ca0 | ||
|
03b3da203d | ||
|
e713c35d21 | ||
|
92c044eadb | ||
|
a7656b999b | ||
|
05093071ce | ||
|
bcab3b6e47 | ||
|
b53584bec0 | ||
|
91f313bb83 | ||
|
6bbed76f0f | ||
|
bba4fe2c36 | ||
|
61aaa52718 | ||
|
20d543d28d | ||
|
5adb6952e9 | ||
|
3705212747 | ||
|
d7d2916acb | ||
|
3482e06b15 | ||
|
59289f67b1 | ||
|
a4a3dd9ed3 | ||
|
ac6eeb8711 | ||
|
20adff0071 | ||
|
6bb611e4b3 | ||
|
e8309495ce | ||
|
4755c5bf5e | ||
|
38205fbcc2 | ||
|
a2704bac4b | ||
|
ac90f8f028 | ||
|
c602e9e7ed | ||
|
1c6da6f9a3 | ||
|
24dcf8270a | ||
|
e189ec9ca8 | ||
|
96f15c6e00 | ||
|
8e131922e7 | ||
|
dd3c6d1303 | ||
|
5ce3a556a9 | ||
|
dd5edafa9d | ||
|
cd2997e63f | ||
|
f0f036eb89 | ||
|
ee0c3ef3ac | ||
|
e6d1828c12 | ||
|
4c17fd9c00 | ||
|
660678d038 | ||
|
a7a4bd42f1 | ||
|
352a0127c7 | ||
|
ed0da24020 |
507 changed files with 35272 additions and 25236 deletions
56
.github/workflows/ci.yaml
vendored
56
.github/workflows/ci.yaml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
|||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.60.0
|
||||
toolchain: 1.67.0
|
||||
default: true
|
||||
components: rustfmt
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
|
@ -28,7 +28,7 @@ jobs:
|
|||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.60.0
|
||||
toolchain: 1.67.0
|
||||
default: true
|
||||
components: clippy
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
|
@ -42,7 +42,7 @@ jobs:
|
|||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.60.0
|
||||
toolchain: 1.67.0
|
||||
default: true
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- name: Build rust docs
|
||||
|
@ -51,9 +51,6 @@ jobs:
|
|||
- name: Install doxygen
|
||||
run: sudo apt-get install -y doxygen
|
||||
shell: bash
|
||||
- name: Build C docs
|
||||
run: ./scripts/ci/cmake-docs
|
||||
shell: bash
|
||||
|
||||
cargo-deny:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -67,23 +64,50 @@ 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-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
- name: Install wasm-bindgen-cli
|
||||
run: cargo install wasm-bindgen-cli wasm-opt
|
||||
- name: Install wasm32 target
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- name: run tests
|
||||
run: ./scripts/ci/wasm_tests
|
||||
deno_tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: v1.x
|
||||
- name: Install wasm-bindgen-cli
|
||||
run: cargo install wasm-bindgen-cli wasm-opt
|
||||
- name: Install wasm32 target
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- name: run tests
|
||||
run: ./scripts/ci/deno_tests
|
||||
|
||||
js_fmt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: install
|
||||
run: yarn global add prettier
|
||||
- name: format
|
||||
run: prettier -c javascript/.prettierrc javascript
|
||||
|
||||
js_tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
- 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/js_tests
|
||||
|
||||
|
@ -94,7 +118,7 @@ jobs:
|
|||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.60.0
|
||||
toolchain: nightly-2023-01-26
|
||||
default: true
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- name: Install CMocka
|
||||
|
@ -103,6 +127,8 @@ jobs:
|
|||
uses: jwlawson/actions-setup-cmake@v1.12
|
||||
with:
|
||||
cmake-version: latest
|
||||
- name: Install rust-src
|
||||
run: rustup component add rust-src
|
||||
- name: Build and test C bindings
|
||||
run: ./scripts/ci/cmake-build Release Static
|
||||
shell: bash
|
||||
|
@ -112,9 +138,7 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
toolchain:
|
||||
- 1.60.0
|
||||
- nightly
|
||||
continue-on-error: ${{ matrix.toolchain == 'nightly' }}
|
||||
- 1.67.0
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
|
@ -133,7 +157,7 @@ jobs:
|
|||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.60.0
|
||||
toolchain: 1.67.0
|
||||
default: true
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- run: ./scripts/ci/build-test
|
||||
|
@ -146,7 +170,7 @@ jobs:
|
|||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.60.0
|
||||
toolchain: 1.67.0
|
||||
default: true
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- run: ./scripts/ci/build-test
|
||||
|
|
18
.github/workflows/docs.yaml
vendored
18
.github/workflows/docs.yaml
vendored
|
@ -30,28 +30,16 @@ jobs:
|
|||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clean
|
||||
args: --doc
|
||||
args: --manifest-path ./rust/Cargo.toml --doc
|
||||
|
||||
- name: Build Rust docs
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: doc
|
||||
args: --workspace --all-features --no-deps
|
||||
args: --manifest-path ./rust/Cargo.toml --workspace --all-features --no-deps
|
||||
|
||||
- name: Move Rust docs
|
||||
run: mkdir -p docs && mv target/doc/* docs/.
|
||||
shell: bash
|
||||
|
||||
- name: Install doxygen
|
||||
run: sudo apt-get install -y doxygen
|
||||
shell: bash
|
||||
|
||||
- name: Build C docs
|
||||
run: ./scripts/ci/cmake-docs
|
||||
shell: bash
|
||||
|
||||
- name: Move C docs
|
||||
run: mkdir -p docs/automerge-c && mv automerge-c/build/src/html/* docs/automerge-c/.
|
||||
run: mkdir -p docs && mv rust/target/doc/* docs/.
|
||||
shell: bash
|
||||
|
||||
- name: Configure root page
|
||||
|
|
214
.github/workflows/release.yaml
vendored
Normal file
214
.github/workflows/release.yaml
vendored
Normal file
|
@ -0,0 +1,214 @@
|
|||
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
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,7 +1,6 @@
|
|||
/target
|
||||
/.direnv
|
||||
perf.*
|
||||
/Cargo.lock
|
||||
build/
|
||||
automerge/proptest-regressions/
|
||||
.vim/*
|
||||
/target
|
||||
|
|
20
Makefile
20
Makefile
|
@ -1,20 +0,0 @@
|
|||
.PHONY: rust
|
||||
rust:
|
||||
cd automerge && cargo test
|
||||
|
||||
.PHONY: wasm
|
||||
wasm:
|
||||
cd automerge-wasm && yarn
|
||||
cd automerge-wasm && yarn build
|
||||
cd automerge-wasm && yarn test
|
||||
cd automerge-wasm && yarn link
|
||||
|
||||
.PHONY: js
|
||||
js: wasm
|
||||
cd automerge-js && yarn
|
||||
cd automerge-js && yarn link "automerge-wasm"
|
||||
cd automerge-js && yarn test
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
git clean -x -d -f
|
198
README.md
198
README.md
|
@ -1,4 +1,4 @@
|
|||
# Automerge RS
|
||||
# Automerge
|
||||
|
||||
<img src='./img/sign.svg' width='500' alt='Automerge logo' />
|
||||
|
||||
|
@ -7,103 +7,141 @@
|
|||
[![ci](https://github.com/automerge/automerge-rs/actions/workflows/ci.yaml/badge.svg)](https://github.com/automerge/automerge-rs/actions/workflows/ci.yaml)
|
||||
[![docs](https://github.com/automerge/automerge-rs/actions/workflows/docs.yaml/badge.svg)](https://github.com/automerge/automerge-rs/actions/workflows/docs.yaml)
|
||||
|
||||
This is a Rust library implementation of the [Automerge](https://github.com/automerge/automerge) file format and network protocol. Its focus is to support the creation of Automerge implementations in other languages, currently; WASM, JS and C. A `libautomerge` if you will.
|
||||
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 original [Automerge](https://github.com/automerge/automerge) project (written in JS from the ground up) is still very much maintained and recommended. Indeed it is because of the success of that project that the next stage of Automerge is being explored here. Hopefully Rust can offer a more performant and scalable Automerge, opening up even more use cases.
|
||||
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)
|
||||
|
||||
## Status
|
||||
|
||||
The project has 5 components:
|
||||
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.
|
||||
|
||||
1. [_automerge_](automerge) - The main Rust implementation of the library.
|
||||
2. [_automerge-wasm_](automerge-wasm) - A JS/WASM interface to the underlying Rust library. This API is generally mature and in use in a handful of projects.
|
||||
3. [_automerge-js_](automerge-js) - This is a Javascript library using the WASM interface to export the same public API of the primary Automerge project. Currently this project passes all of Automerge's tests but has not been used in any real project or packaged as an NPM. Alpha testers welcome.
|
||||
4. [_automerge-c_](automerge-c) - This is a C library intended to be an FFI integration point for all other languages. It is currently a work in progress and not yet ready for any testing.
|
||||
5. [_automerge-cli_](automerge-cli) - An experimental CLI wrapper around the Rust library. Currently not functional.
|
||||
In general we try and respect semver.
|
||||
|
||||
## How?
|
||||
### JavaScript
|
||||
|
||||
The magic of the architecture is 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`.
|
||||
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
|
||||
|
||||
## Development
|
||||
### Rust
|
||||
|
||||
Please feel free to open issues and pull requests.
|
||||
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)
|
||||
|
||||
### Running CI
|
||||
## Repository Organisation
|
||||
|
||||
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` - 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
|
||||
|
||||
### Running the JS tests
|
||||
## Building
|
||||
|
||||
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.
|
||||
To build this codebase you will need:
|
||||
|
||||
To build and test the rust library:
|
||||
- `rust`
|
||||
- `node`
|
||||
- `yarn`
|
||||
- `cmake`
|
||||
- `cmocka`
|
||||
|
||||
```shell
|
||||
$ cd automerge
|
||||
$ cargo test
|
||||
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
|
||||
```
|
||||
|
||||
To build and test the wasm library:
|
||||
If your build fails to find `cmocka.h` you may need to teach it about homebrew's
|
||||
installation location:
|
||||
|
||||
```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
|
||||
```
|
||||
export CPATH=/opt/homebrew/include
|
||||
export LIBRARY_PATH=/opt/homebrew/lib
|
||||
./scripts/ci/run
|
||||
```
|
||||
|
||||
To test the js library. This is where most of the tests reside.
|
||||
## Contributing
|
||||
|
||||
```shell
|
||||
## setup
|
||||
$ cd automerge-js
|
||||
$ yarn
|
||||
$ yarn link "automerge-wasm"
|
||||
|
||||
## testing
|
||||
$ yarn test
|
||||
```
|
||||
|
||||
And finally, to build and test the C bindings with CMake:
|
||||
|
||||
```shell
|
||||
## setup
|
||||
$ cd automerge-c
|
||||
$ mkdir -p build
|
||||
$ cd build
|
||||
$ cmake -S .. -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF
|
||||
## building and testing
|
||||
$ cmake --build . --target test_automerge
|
||||
```
|
||||
|
||||
To add debugging symbols, replace `Release` with `Debug`.
|
||||
To build a shared library instead of a static one, replace `OFF` with `ON`.
|
||||
|
||||
The C bindings can be built and tested on any platform for which CMake is
|
||||
available but the steps for doing so vary across platforms and are too numerous
|
||||
to list here.
|
||||
|
||||
## Benchmarking
|
||||
|
||||
The [`edit-trace`](edit-trace) folder has the main code for running the edit trace benchmarking.
|
||||
|
||||
## The old Rust project
|
||||
If you are looking for the origional `automerge-rs` project that can be used as a wasm backend to the javascript implementation, it can be found [here](https://github.com/automerge/automerge-rs/tree/automerge-1.0).
|
||||
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.
|
||||
|
|
32
TODO.md
32
TODO.md
|
@ -1,32 +0,0 @@
|
|||
### 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
|
3
automerge-c/.gitignore
vendored
3
automerge-c/.gitignore
vendored
|
@ -1,3 +0,0 @@
|
|||
automerge
|
||||
automerge.h
|
||||
automerge.o
|
|
@ -1,141 +0,0 @@
|
|||
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
|
||||
|
||||
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
|
||||
|
||||
# Parse the library name, project name and project version out of Cargo's TOML file.
|
||||
set(CARGO_LIB_SECTION OFF)
|
||||
|
||||
set(LIBRARY_NAME "")
|
||||
|
||||
set(CARGO_PKG_SECTION OFF)
|
||||
|
||||
set(CARGO_PKG_NAME "")
|
||||
|
||||
set(CARGO_PKG_VERSION "")
|
||||
|
||||
file(READ Cargo.toml TOML_STRING)
|
||||
|
||||
string(REPLACE ";" "\\\\;" TOML_STRING "${TOML_STRING}")
|
||||
|
||||
string(REPLACE "\n" ";" TOML_LINES "${TOML_STRING}")
|
||||
|
||||
foreach(TOML_LINE IN ITEMS ${TOML_LINES})
|
||||
string(REGEX MATCH "^\\[(lib|package)\\]$" _ ${TOML_LINE})
|
||||
|
||||
if(CMAKE_MATCH_1 STREQUAL "lib")
|
||||
set(CARGO_LIB_SECTION ON)
|
||||
|
||||
set(CARGO_PKG_SECTION OFF)
|
||||
elseif(CMAKE_MATCH_1 STREQUAL "package")
|
||||
set(CARGO_LIB_SECTION OFF)
|
||||
|
||||
set(CARGO_PKG_SECTION ON)
|
||||
endif()
|
||||
|
||||
string(REGEX MATCH "^name += +\"([^\"]+)\"$" _ ${TOML_LINE})
|
||||
|
||||
if(CMAKE_MATCH_1 AND (CARGO_LIB_SECTION AND NOT CARGO_PKG_SECTION))
|
||||
set(LIBRARY_NAME "${CMAKE_MATCH_1}")
|
||||
elseif(CMAKE_MATCH_1 AND (NOT CARGO_LIB_SECTION AND CARGO_PKG_SECTION))
|
||||
set(CARGO_PKG_NAME "${CMAKE_MATCH_1}")
|
||||
endif()
|
||||
|
||||
string(REGEX MATCH "^version += +\"([^\"]+)\"$" _ ${TOML_LINE})
|
||||
|
||||
if(CMAKE_MATCH_1 AND CARGO_PKG_SECTION)
|
||||
set(CARGO_PKG_VERSION "${CMAKE_MATCH_1}")
|
||||
endif()
|
||||
|
||||
if(LIBRARY_NAME AND (CARGO_PKG_NAME AND CARGO_PKG_VERSION))
|
||||
break()
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
project(${CARGO_PKG_NAME} VERSION ${CARGO_PKG_VERSION} LANGUAGES C DESCRIPTION "C bindings for the Automerge Rust backend.")
|
||||
|
||||
include(CTest)
|
||||
|
||||
option(BUILD_SHARED_LIBS "Enable the choice of a shared or static library.")
|
||||
|
||||
include(CMakePackageConfigHelpers)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
|
||||
string(MAKE_C_IDENTIFIER ${PROJECT_NAME} SYMBOL_PREFIX)
|
||||
|
||||
string(TOUPPER ${SYMBOL_PREFIX} SYMBOL_PREFIX)
|
||||
|
||||
set(CARGO_TARGET_DIR "${CMAKE_CURRENT_BINARY_DIR}/Cargo/target")
|
||||
|
||||
set(CBINDGEN_INCLUDEDIR "${CARGO_TARGET_DIR}/${CMAKE_INSTALL_INCLUDEDIR}")
|
||||
|
||||
set(CBINDGEN_TARGET_DIR "${CBINDGEN_INCLUDEDIR}/${PROJECT_NAME}")
|
||||
|
||||
add_subdirectory(src)
|
||||
|
||||
# Generate and install the configuration header.
|
||||
math(EXPR INTEGER_PROJECT_VERSION_MAJOR "${PROJECT_VERSION_MAJOR} * 100000")
|
||||
|
||||
math(EXPR INTEGER_PROJECT_VERSION_MINOR "${PROJECT_VERSION_MINOR} * 100")
|
||||
|
||||
math(EXPR INTEGER_PROJECT_VERSION_PATCH "${PROJECT_VERSION_PATCH}")
|
||||
|
||||
math(EXPR INTEGER_PROJECT_VERSION "${INTEGER_PROJECT_VERSION_MAJOR} + ${INTEGER_PROJECT_VERSION_MINOR} + ${INTEGER_PROJECT_VERSION_PATCH}")
|
||||
|
||||
configure_file(
|
||||
${CMAKE_MODULE_PATH}/config.h.in
|
||||
config.h
|
||||
@ONLY
|
||||
NEWLINE_STYLE LF
|
||||
)
|
||||
|
||||
install(
|
||||
FILES ${CMAKE_BINARY_DIR}/config.h
|
||||
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}
|
||||
)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_subdirectory(test EXCLUDE_FROM_ALL)
|
||||
|
||||
enable_testing()
|
||||
endif()
|
||||
|
||||
add_subdirectory(examples EXCLUDE_FROM_ALL)
|
||||
|
||||
# Generate and install .cmake files
|
||||
set(PROJECT_CONFIG_NAME "${PROJECT_NAME}-config")
|
||||
|
||||
set(PROJECT_CONFIG_VERSION_NAME "${PROJECT_CONFIG_NAME}-version")
|
||||
|
||||
write_basic_package_version_file(
|
||||
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_VERSION_NAME}.cmake
|
||||
VERSION ${PROJECT_VERSION}
|
||||
COMPATIBILITY ExactVersion
|
||||
)
|
||||
|
||||
# The namespace label starts with the title-cased library name.
|
||||
string(SUBSTRING ${LIBRARY_NAME} 0 1 NS_FIRST)
|
||||
|
||||
string(SUBSTRING ${LIBRARY_NAME} 1 -1 NS_REST)
|
||||
|
||||
string(TOUPPER ${NS_FIRST} NS_FIRST)
|
||||
|
||||
string(TOLOWER ${NS_REST} NS_REST)
|
||||
|
||||
string(CONCAT NAMESPACE ${NS_FIRST} ${NS_REST} "::")
|
||||
|
||||
# \note CMake doesn't automate the exporting of an imported library's targets
|
||||
# so the package configuration script must do it.
|
||||
configure_package_config_file(
|
||||
${CMAKE_MODULE_PATH}/${PROJECT_CONFIG_NAME}.cmake.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_NAME}.cmake
|
||||
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
|
||||
)
|
||||
|
||||
install(
|
||||
FILES
|
||||
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_NAME}.cmake
|
||||
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_VERSION_NAME}.cmake
|
||||
DESTINATION
|
||||
${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
|
||||
)
|
|
@ -1,97 +0,0 @@
|
|||
|
||||
## Methods we need to support
|
||||
|
||||
### Basic management
|
||||
|
||||
1. `AMcreate()`
|
||||
1. `AMclone(doc)`
|
||||
1. `AMfree(doc)`
|
||||
1. `AMconfig(doc, key, val)` // set actor
|
||||
1. `actor = get_actor(doc)`
|
||||
|
||||
### Transactions
|
||||
|
||||
1. `AMpendingOps(doc)`
|
||||
1. `AMcommit(doc, message, time)`
|
||||
1. `AMrollback(doc)`
|
||||
|
||||
### Write
|
||||
|
||||
1. `AMset{Map|List}(doc, obj, prop, value)`
|
||||
1. `AMinsert(doc, obj, index, value)`
|
||||
1. `AMpush(doc, obj, value)`
|
||||
1. `AMdel{Map|List}(doc, obj, prop)`
|
||||
1. `AMinc{Map|List}(doc, obj, prop, value)`
|
||||
1. `AMspliceText(doc, obj, start, num_del, text)`
|
||||
|
||||
### Read (the heads argument is optional and can be on an `at` variant)
|
||||
|
||||
1. `AMkeys(doc, obj, heads)`
|
||||
1. `AMlength(doc, obj, heads)`
|
||||
1. `AMlistRange(doc, obj, heads)`
|
||||
1. `AMmapRange(doc, obj, heads)`
|
||||
1. `AMvalues(doc, obj, heads)`
|
||||
1. `AMtext(doc, obj, heads)`
|
||||
|
||||
### Sync
|
||||
|
||||
1. `AMgenerateSyncMessage(doc, state)`
|
||||
1. `AMreceiveSyncMessage(doc, state, message)`
|
||||
1. `AMinitSyncState()`
|
||||
|
||||
### Save / Load
|
||||
|
||||
1. `AMload(data)`
|
||||
1. `AMloadIncremental(doc, data)`
|
||||
1. `AMsave(doc)`
|
||||
1. `AMsaveIncremental(doc)`
|
||||
|
||||
### Low Level Access
|
||||
|
||||
1. `AMapplyChanges(doc, changes)`
|
||||
1. `AMgetChanges(doc, deps)`
|
||||
1. `AMgetChangesAdded(doc1, doc2)`
|
||||
1. `AMgetHeads(doc)`
|
||||
1. `AMgetLastLocalChange(doc)`
|
||||
1. `AMgetMissingDeps(doc, heads)`
|
||||
|
||||
### Encode/Decode
|
||||
|
||||
1. `AMencodeChange(change)`
|
||||
1. `AMdecodeChange(change)`
|
||||
1. `AMencodeSyncMessage(change)`
|
||||
1. `AMdecodeSyncMessage(change)`
|
||||
1. `AMencodeSyncState(change)`
|
||||
1. `AMdecodeSyncState(change)`
|
||||
|
||||
## Open Question - Memory management
|
||||
|
||||
Most of these calls return one or more items of arbitrary length. Doing memory management in C is tricky. This is my proposed solution...
|
||||
|
||||
###
|
||||
|
||||
```
|
||||
// returns 1 or zero opids
|
||||
n = automerge_set(doc, "_root", "hello", datatype, value);
|
||||
if (n) {
|
||||
automerge_pop(doc, &obj, len);
|
||||
}
|
||||
|
||||
// returns n values
|
||||
n = automerge_values(doc, "_root", "hello");
|
||||
for (i = 0; i<n ;i ++) {
|
||||
automerge_pop_value(doc, &value, &datatype, len);
|
||||
}
|
||||
```
|
||||
|
||||
There would be one pop method per object type. Users allocs and frees the buffers. Multiple return values would result in multiple pops. Too small buffers would error and allow retry.
|
||||
|
||||
|
||||
### Formats
|
||||
|
||||
Actors - We could do (bytes,len) or a hex encoded string?.
|
||||
ObjIds - We could do flat bytes of the ExId struct but lets do human readable strings for now - the struct would be faster but opque
|
||||
Heads - Might as well make it a flat buffer `(n, hash, hash, ...)`
|
||||
Changes - Put them all in a flat concatenated buffer
|
||||
Encode/Decode - to json strings?
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
#ifndef @SYMBOL_PREFIX@_CONFIG_H
|
||||
#define @SYMBOL_PREFIX@_CONFIG_H
|
||||
|
||||
/* This header is auto-generated by CMake. */
|
||||
|
||||
#define @SYMBOL_PREFIX@_VERSION @INTEGER_PROJECT_VERSION@
|
||||
|
||||
#define @SYMBOL_PREFIX@_MAJOR_VERSION (@SYMBOL_PREFIX@_VERSION / 100000)
|
||||
|
||||
#define @SYMBOL_PREFIX@_MINOR_VERSION ((@SYMBOL_PREFIX@_VERSION / 100) % 1000)
|
||||
|
||||
#define @SYMBOL_PREFIX@_PATCH_VERSION (@SYMBOL_PREFIX@_VERSION % 100)
|
||||
|
||||
#endif /* @SYMBOL_PREFIX@_CONFIG_H */
|
|
@ -1,146 +0,0 @@
|
|||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <automerge-c/automerge.h>
|
||||
|
||||
static void abort_cb(AMresultStack**, uint8_t);
|
||||
|
||||
/**
|
||||
* \brief Based on https://automerge.github.io/docs/quickstart
|
||||
*/
|
||||
int main(int argc, char** argv) {
|
||||
AMresultStack* stack = NULL;
|
||||
AMdoc* const doc1 = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, abort_cb).doc;
|
||||
AMobjId const* const cards = AMpush(&stack,
|
||||
AMmapPutObject(doc1, AM_ROOT, "cards", AM_OBJ_TYPE_LIST),
|
||||
AM_VALUE_OBJ_ID,
|
||||
abort_cb).obj_id;
|
||||
AMobjId const* const card1 = AMpush(&stack,
|
||||
AMlistPutObject(doc1, cards, SIZE_MAX, true, AM_OBJ_TYPE_MAP),
|
||||
AM_VALUE_OBJ_ID,
|
||||
abort_cb).obj_id;
|
||||
AMfree(AMmapPutStr(doc1, card1, "title", "Rewrite everything in Clojure"));
|
||||
AMfree(AMmapPutBool(doc1, card1, "done", false));
|
||||
AMobjId const* const card2 = AMpush(&stack,
|
||||
AMlistPutObject(doc1, cards, SIZE_MAX, true, AM_OBJ_TYPE_MAP),
|
||||
AM_VALUE_OBJ_ID,
|
||||
abort_cb).obj_id;
|
||||
AMfree(AMmapPutStr(doc1, card2, "title", "Rewrite everything in Haskell"));
|
||||
AMfree(AMmapPutBool(doc1, card2, "done", false));
|
||||
AMfree(AMcommit(doc1, "Add card", NULL));
|
||||
|
||||
AMdoc* doc2 = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, abort_cb).doc;
|
||||
AMfree(AMmerge(doc2, doc1));
|
||||
|
||||
AMbyteSpan const binary = AMpush(&stack, AMsave(doc1), AM_VALUE_BYTES, abort_cb).bytes;
|
||||
doc2 = AMpush(&stack, AMload(binary.src, binary.count), AM_VALUE_DOC, abort_cb).doc;
|
||||
|
||||
AMfree(AMmapPutBool(doc1, card1, "done", true));
|
||||
AMfree(AMcommit(doc1, "Mark card as done", NULL));
|
||||
|
||||
AMfree(AMlistDelete(doc2, cards, 0));
|
||||
AMfree(AMcommit(doc2, "Delete card", NULL));
|
||||
|
||||
AMfree(AMmerge(doc1, doc2));
|
||||
|
||||
AMchanges changes = AMpush(&stack, AMgetChanges(doc1, NULL), AM_VALUE_CHANGES, abort_cb).changes;
|
||||
AMchange const* change = NULL;
|
||||
while ((change = AMchangesNext(&changes, 1)) != NULL) {
|
||||
AMbyteSpan const change_hash = AMchangeHash(change);
|
||||
AMchangeHashes const heads = AMpush(&stack,
|
||||
AMchangeHashesInit(&change_hash, 1),
|
||||
AM_VALUE_CHANGE_HASHES,
|
||||
abort_cb).change_hashes;
|
||||
printf("%s %ld\n", AMchangeMessage(change), AMobjSize(doc1, cards, &heads));
|
||||
}
|
||||
AMfreeStack(&stack);
|
||||
}
|
||||
|
||||
static char const* discriminant_suffix(AMvalueVariant const);
|
||||
|
||||
/**
|
||||
* \brief Prints an error message to `stderr`, deallocates all results in the
|
||||
* given stack and exits.
|
||||
*
|
||||
* \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
|
||||
* \param[in] discriminant An `AMvalueVariant` enum tag.
|
||||
* \pre \p stack` != NULL`.
|
||||
* \post `*stack == NULL`.
|
||||
*/
|
||||
static void abort_cb(AMresultStack** stack, uint8_t discriminant) {
|
||||
static char buffer[512] = {0};
|
||||
|
||||
char const* suffix = NULL;
|
||||
if (!stack) {
|
||||
suffix = "Stack*";
|
||||
}
|
||||
else if (!*stack) {
|
||||
suffix = "Stack";
|
||||
}
|
||||
else if (!(*stack)->result) {
|
||||
suffix = "";
|
||||
}
|
||||
if (suffix) {
|
||||
fprintf(stderr, "Null `AMresult%s*`.", suffix);
|
||||
AMfreeStack(stack);
|
||||
exit(EXIT_FAILURE);
|
||||
return;
|
||||
}
|
||||
AMstatus const status = AMresultStatus((*stack)->result);
|
||||
switch (status) {
|
||||
case AM_STATUS_ERROR: strcpy(buffer, "Error"); break;
|
||||
case AM_STATUS_INVALID_RESULT: strcpy(buffer, "Invalid result"); break;
|
||||
case AM_STATUS_OK: break;
|
||||
default: sprintf(buffer, "Unknown `AMstatus` tag %d", status);
|
||||
}
|
||||
if (buffer[0]) {
|
||||
fprintf(stderr, "%s; %s.", buffer, AMerrorMessage((*stack)->result));
|
||||
AMfreeStack(stack);
|
||||
exit(EXIT_FAILURE);
|
||||
return;
|
||||
}
|
||||
AMvalue const value = AMresultValue((*stack)->result);
|
||||
fprintf(stderr, "Unexpected tag `AM_VALUE_%s` (%d); expected `AM_VALUE_%s`.",
|
||||
discriminant_suffix(value.tag),
|
||||
value.tag,
|
||||
discriminant_suffix(discriminant));
|
||||
AMfreeStack(stack);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Gets the suffix for a discriminant's corresponding string
|
||||
* representation.
|
||||
*
|
||||
* \param[in] discriminant An `AMvalueVariant` enum tag.
|
||||
* \return A UTF-8 string.
|
||||
*/
|
||||
static char const* discriminant_suffix(AMvalueVariant const discriminant) {
|
||||
char const* suffix = NULL;
|
||||
switch (discriminant) {
|
||||
case AM_VALUE_ACTOR_ID: suffix = "ACTOR_ID"; break;
|
||||
case AM_VALUE_BOOLEAN: suffix = "BOOLEAN"; break;
|
||||
case AM_VALUE_BYTES: suffix = "BYTES"; break;
|
||||
case AM_VALUE_CHANGE_HASHES: suffix = "CHANGE_HASHES"; break;
|
||||
case AM_VALUE_CHANGES: suffix = "CHANGES"; break;
|
||||
case AM_VALUE_COUNTER: suffix = "COUNTER"; break;
|
||||
case AM_VALUE_DOC: suffix = "DOC"; break;
|
||||
case AM_VALUE_F64: suffix = "F64"; break;
|
||||
case AM_VALUE_INT: suffix = "INT"; break;
|
||||
case AM_VALUE_LIST_ITEMS: suffix = "LIST_ITEMS"; break;
|
||||
case AM_VALUE_MAP_ITEMS: suffix = "MAP_ITEMS"; break;
|
||||
case AM_VALUE_NULL: suffix = "NULL"; break;
|
||||
case AM_VALUE_OBJ_ID: suffix = "OBJ_ID"; break;
|
||||
case AM_VALUE_OBJ_ITEMS: suffix = "OBJ_ITEMS"; break;
|
||||
case AM_VALUE_STR: suffix = "STR"; break;
|
||||
case AM_VALUE_STRS: suffix = "STRINGS"; break;
|
||||
case AM_VALUE_SYNC_MESSAGE: suffix = "SYNC_MESSAGE"; break;
|
||||
case AM_VALUE_SYNC_STATE: suffix = "SYNC_STATE"; break;
|
||||
case AM_VALUE_TIMESTAMP: suffix = "TIMESTAMP"; break;
|
||||
case AM_VALUE_UINT: suffix = "UINT"; break;
|
||||
case AM_VALUE_VOID: suffix = "VOID"; break;
|
||||
default: suffix = "...";
|
||||
}
|
||||
return suffix;
|
||||
}
|
|
@ -1,250 +0,0 @@
|
|||
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
|
||||
|
||||
find_program (
|
||||
CARGO_CMD
|
||||
"cargo"
|
||||
PATHS "$ENV{CARGO_HOME}/bin"
|
||||
DOC "The Cargo command"
|
||||
)
|
||||
|
||||
if(NOT CARGO_CMD)
|
||||
message(FATAL_ERROR "Cargo (Rust package manager) not found! Install it and/or set the CARGO_HOME environment variable.")
|
||||
endif()
|
||||
|
||||
string(TOLOWER "${CMAKE_BUILD_TYPE}" BUILD_TYPE_LOWER)
|
||||
|
||||
if(BUILD_TYPE_LOWER STREQUAL debug)
|
||||
set(CARGO_BUILD_TYPE "debug")
|
||||
|
||||
set(CARGO_FLAG "")
|
||||
else()
|
||||
set(CARGO_BUILD_TYPE "release")
|
||||
|
||||
set(CARGO_FLAG "--release")
|
||||
endif()
|
||||
|
||||
set(CARGO_FEATURES "")
|
||||
|
||||
set(CARGO_CURRENT_BINARY_DIR "${CARGO_TARGET_DIR}/${CARGO_BUILD_TYPE}")
|
||||
|
||||
set(
|
||||
CARGO_OUTPUT
|
||||
${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
|
||||
${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}
|
||||
${CARGO_CURRENT_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
# \note The basename of an import library output by Cargo is the filename
|
||||
# of its corresponding shared library.
|
||||
list(APPEND CARGO_OUTPUT ${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}${CMAKE_STATIC_LIBRARY_SUFFIX})
|
||||
endif()
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT
|
||||
${CARGO_OUTPUT}
|
||||
COMMAND
|
||||
# \note cbindgen won't regenerate its output header file after it's
|
||||
# been removed but it will after its configuration file has been
|
||||
# updated.
|
||||
${CMAKE_COMMAND} -DCONDITION=NOT_EXISTS -P ${CMAKE_SOURCE_DIR}/cmake/file_touch.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h ${CMAKE_SOURCE_DIR}/cbindgen.toml
|
||||
COMMAND
|
||||
${CMAKE_COMMAND} -E env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} CBINDGEN_TARGET_DIR=${CBINDGEN_TARGET_DIR} ${CARGO_CMD} build ${CARGO_FLAG} ${CARGO_FEATURES}
|
||||
MAIN_DEPENDENCY
|
||||
lib.rs
|
||||
DEPENDS
|
||||
actor_id.rs
|
||||
byte_span.rs
|
||||
change_hashes.rs
|
||||
change.rs
|
||||
changes.rs
|
||||
doc.rs
|
||||
doc/list.rs
|
||||
doc/list/item.rs
|
||||
doc/list/items.rs
|
||||
doc/map.rs
|
||||
doc/map/item.rs
|
||||
doc/map/items.rs
|
||||
doc/utils.rs
|
||||
obj.rs
|
||||
obj/item.rs
|
||||
obj/items.rs
|
||||
result.rs
|
||||
result_stack.rs
|
||||
strs.rs
|
||||
sync.rs
|
||||
sync/have.rs
|
||||
sync/haves.rs
|
||||
sync/message.rs
|
||||
sync/state.rs
|
||||
${CMAKE_SOURCE_DIR}/build.rs
|
||||
${CMAKE_SOURCE_DIR}/Cargo.toml
|
||||
${CMAKE_SOURCE_DIR}/cbindgen.toml
|
||||
WORKING_DIRECTORY
|
||||
${CMAKE_SOURCE_DIR}
|
||||
COMMENT
|
||||
"Producing the library artifacts with Cargo..."
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_custom_target(
|
||||
${LIBRARY_NAME}_artifacts ALL
|
||||
DEPENDS ${CARGO_OUTPUT}
|
||||
)
|
||||
|
||||
# \note cbindgen's naming behavior isn't fully configurable and it ignores
|
||||
# `const fn` calls (https://github.com/eqrion/cbindgen/issues/252).
|
||||
add_custom_command(
|
||||
TARGET ${LIBRARY_NAME}_artifacts
|
||||
POST_BUILD
|
||||
COMMAND
|
||||
# Compensate for cbindgen's variant struct naming.
|
||||
${CMAKE_COMMAND} -DMATCH_REGEX=AM\([^_]+_[^_]+\)_Body -DREPLACE_EXPR=AM\\1 -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
|
||||
COMMAND
|
||||
# Compensate for cbindgen's union tag enum type naming.
|
||||
${CMAKE_COMMAND} -DMATCH_REGEX=AM\([^_]+\)_Tag -DREPLACE_EXPR=AM\\1Variant -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
|
||||
COMMAND
|
||||
# Compensate for cbindgen's translation of consecutive uppercase letters to "ScreamingSnakeCase".
|
||||
${CMAKE_COMMAND} -DMATCH_REGEX=A_M\([^_]+\)_ -DREPLACE_EXPR=AM_\\1_ -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
|
||||
COMMAND
|
||||
# Compensate for cbindgen ignoring `std:mem::size_of<usize>()` calls.
|
||||
${CMAKE_COMMAND} -DMATCH_REGEX=USIZE_ -DREPLACE_EXPR=\+${CMAKE_SIZEOF_VOID_P} -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
|
||||
WORKING_DIRECTORY
|
||||
${CMAKE_SOURCE_DIR}
|
||||
COMMENT
|
||||
"Compensating for cbindgen deficits..."
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
if(BUILD_SHARED_LIBS)
|
||||
if(WIN32)
|
||||
set(LIBRARY_DESTINATION "${CMAKE_INSTALL_BINDIR}")
|
||||
else()
|
||||
set(LIBRARY_DESTINATION "${CMAKE_INSTALL_LIBDIR}")
|
||||
endif()
|
||||
|
||||
set(LIBRARY_DEFINE_SYMBOL "${SYMBOL_PREFIX}_EXPORTS")
|
||||
|
||||
# \note The basename of an import library output by Cargo is the filename
|
||||
# of its corresponding shared library.
|
||||
set(LIBRARY_IMPLIB "${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}${CMAKE_STATIC_LIBRARY_SUFFIX}")
|
||||
|
||||
set(LIBRARY_LOCATION "${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}")
|
||||
|
||||
set(LIBRARY_NO_SONAME "${WIN32}")
|
||||
|
||||
set(LIBRARY_SONAME "${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_SHARED_LIBRARY_SUFFIX}")
|
||||
|
||||
set(LIBRARY_TYPE "SHARED")
|
||||
else()
|
||||
set(LIBRARY_DEFINE_SYMBOL "")
|
||||
|
||||
set(LIBRARY_DESTINATION "${CMAKE_INSTALL_LIBDIR}")
|
||||
|
||||
set(LIBRARY_IMPLIB "")
|
||||
|
||||
set(LIBRARY_LOCATION "${CARGO_CURRENT_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}")
|
||||
|
||||
set(LIBRARY_NO_SONAME "TRUE")
|
||||
|
||||
set(LIBRARY_SONAME "")
|
||||
|
||||
set(LIBRARY_TYPE "STATIC")
|
||||
endif()
|
||||
|
||||
add_library(${LIBRARY_NAME} ${LIBRARY_TYPE} IMPORTED GLOBAL)
|
||||
|
||||
set_target_properties(
|
||||
${LIBRARY_NAME}
|
||||
PROPERTIES
|
||||
# \note Cargo writes a debug build into a nested directory instead of
|
||||
# decorating its name.
|
||||
DEBUG_POSTFIX ""
|
||||
DEFINE_SYMBOL "${LIBRARY_DEFINE_SYMBOL}"
|
||||
IMPORTED_IMPLIB "${LIBRARY_IMPLIB}"
|
||||
IMPORTED_LOCATION "${LIBRARY_LOCATION}"
|
||||
IMPORTED_NO_SONAME "${LIBRARY_NO_SONAME}"
|
||||
IMPORTED_SONAME "${LIBRARY_SONAME}"
|
||||
LINKER_LANGUAGE C
|
||||
PUBLIC_HEADER "${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h"
|
||||
SOVERSION "${PROJECT_VERSION_MAJOR}"
|
||||
VERSION "${PROJECT_VERSION}"
|
||||
# \note Cargo exports all of the symbols automatically.
|
||||
WINDOWS_EXPORT_ALL_SYMBOLS "TRUE"
|
||||
)
|
||||
|
||||
target_compile_definitions(${LIBRARY_NAME} INTERFACE $<TARGET_PROPERTY:${LIBRARY_NAME},DEFINE_SYMBOL>)
|
||||
|
||||
target_include_directories(
|
||||
${LIBRARY_NAME}
|
||||
INTERFACE
|
||||
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}>"
|
||||
)
|
||||
|
||||
set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
|
||||
|
||||
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
set(LIBRARY_DEPENDENCIES Threads::Threads ${CMAKE_DL_LIBS})
|
||||
|
||||
if(WIN32)
|
||||
list(APPEND LIBRARY_DEPENDENCIES Bcrypt userenv ws2_32)
|
||||
else()
|
||||
list(APPEND LIBRARY_DEPENDENCIES m)
|
||||
endif()
|
||||
|
||||
target_link_libraries(${LIBRARY_NAME} INTERFACE ${LIBRARY_DEPENDENCIES})
|
||||
|
||||
install(
|
||||
FILES $<TARGET_PROPERTY:${LIBRARY_NAME},IMPORTED_IMPLIB>
|
||||
TYPE LIB
|
||||
# \note The basename of an import library output by Cargo is the filename
|
||||
# of its corresponding shared library.
|
||||
RENAME "${CMAKE_STATIC_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_STATIC_LIBRARY_SUFFIX}"
|
||||
OPTIONAL
|
||||
)
|
||||
|
||||
set(LIBRARY_FILE_NAME "${CMAKE_${LIBRARY_TYPE}_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_${LIBRARY_TYPE}_LIBRARY_SUFFIX}")
|
||||
|
||||
install(
|
||||
FILES $<TARGET_PROPERTY:${LIBRARY_NAME},IMPORTED_LOCATION>
|
||||
RENAME "${LIBRARY_FILE_NAME}"
|
||||
DESTINATION ${LIBRARY_DESTINATION}
|
||||
)
|
||||
|
||||
install(
|
||||
FILES $<TARGET_PROPERTY:${LIBRARY_NAME},PUBLIC_HEADER>
|
||||
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}
|
||||
)
|
||||
|
||||
find_package(Doxygen OPTIONAL_COMPONENTS dot)
|
||||
|
||||
if(DOXYGEN_FOUND)
|
||||
set(DOXYGEN_ALIASES "installed_headerfile=\\headerfile ${LIBRARY_NAME}.h <${PROJECT_NAME}/${LIBRARY_NAME}.h>")
|
||||
|
||||
set(DOXYGEN_GENERATE_LATEX YES)
|
||||
|
||||
set(DOXYGEN_PDF_HYPERLINKS YES)
|
||||
|
||||
set(DOXYGEN_PROJECT_LOGO "${CMAKE_SOURCE_DIR}/img/brandmark.png")
|
||||
|
||||
set(DOXYGEN_SORT_BRIEF_DOCS YES)
|
||||
|
||||
set(DOXYGEN_USE_MDFILE_AS_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md")
|
||||
|
||||
doxygen_add_docs(
|
||||
${LIBRARY_NAME}_docs
|
||||
"${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h"
|
||||
"${CMAKE_SOURCE_DIR}/README.md"
|
||||
USE_STAMP_FILE
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
COMMENT "Producing documentation with Doxygen..."
|
||||
)
|
||||
|
||||
# \note A Doxygen input file isn't a file-level dependency so the Doxygen
|
||||
# command must instead depend upon a target that outputs the file or
|
||||
# it will just output an error message when it can't be found.
|
||||
add_dependencies(${LIBRARY_NAME}_docs ${LIBRARY_NAME}_artifacts)
|
||||
endif()
|
|
@ -1,166 +0,0 @@
|
|||
use automerge as am;
|
||||
use std::cell::RefCell;
|
||||
use std::cmp::Ordering;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::byte_span::AMbyteSpan;
|
||||
use crate::result::{to_result, AMresult};
|
||||
|
||||
/// \struct AMactorId
|
||||
/// \installed_headerfile
|
||||
/// \brief An actor's unique identifier.
|
||||
#[derive(Eq, PartialEq)]
|
||||
pub struct AMactorId {
|
||||
body: *const am::ActorId,
|
||||
c_str: RefCell<Option<CString>>,
|
||||
}
|
||||
|
||||
impl AMactorId {
|
||||
pub fn new(actor_id: &am::ActorId) -> Self {
|
||||
Self {
|
||||
body: actor_id,
|
||||
c_str: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_c_str(&self) -> *const c_char {
|
||||
let mut c_str = self.c_str.borrow_mut();
|
||||
match c_str.as_mut() {
|
||||
None => {
|
||||
let hex_str = unsafe { (*self.body).to_hex_string() };
|
||||
c_str.insert(CString::new(hex_str).unwrap()).as_ptr()
|
||||
}
|
||||
Some(hex_str) => hex_str.as_ptr(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<am::ActorId> for AMactorId {
|
||||
fn as_ref(&self) -> &am::ActorId {
|
||||
unsafe { &*self.body }
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMactorId
|
||||
/// \brief Gets the value of an actor identifier as a sequence of bytes.
|
||||
///
|
||||
/// \param[in] actor_id A pointer to an `AMactorId` struct.
|
||||
/// \pre \p actor_id `!= NULL`.
|
||||
/// \return An `AMbyteSpan` struct.
|
||||
/// \internal
|
||||
///
|
||||
/// # Safety
|
||||
/// actor_id must be a valid pointer to an AMactorId
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMactorIdBytes(actor_id: *const AMactorId) -> AMbyteSpan {
|
||||
match actor_id.as_ref() {
|
||||
Some(actor_id) => actor_id.as_ref().into(),
|
||||
None => AMbyteSpan::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMactorId
|
||||
/// \brief Compares two actor identifiers.
|
||||
///
|
||||
/// \param[in] actor_id1 A pointer to an `AMactorId` struct.
|
||||
/// \param[in] actor_id2 A pointer to an `AMactorId` struct.
|
||||
/// \return `-1` if \p actor_id1 `<` \p actor_id2, `0` if
|
||||
/// \p actor_id1 `==` \p actor_id2 and `1` if
|
||||
/// \p actor_id1 `>` \p actor_id2.
|
||||
/// \pre \p actor_id1 `!= NULL`.
|
||||
/// \pre \p actor_id2 `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// actor_id1 must be a valid pointer to an AMactorId
|
||||
/// actor_id2 must be a valid pointer to an AMactorId
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMactorIdCmp(
|
||||
actor_id1: *const AMactorId,
|
||||
actor_id2: *const AMactorId,
|
||||
) -> isize {
|
||||
match (actor_id1.as_ref(), actor_id2.as_ref()) {
|
||||
(Some(actor_id1), Some(actor_id2)) => match actor_id1.as_ref().cmp(actor_id2.as_ref()) {
|
||||
Ordering::Less => -1,
|
||||
Ordering::Equal => 0,
|
||||
Ordering::Greater => 1,
|
||||
},
|
||||
(None, Some(_)) => -1,
|
||||
(Some(_), None) => 1,
|
||||
(None, None) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMactorId
|
||||
/// \brief Allocates a new actor identifier and initializes it with a random
|
||||
/// UUID.
|
||||
///
|
||||
/// \return A pointer to an `AMresult` struct containing a pointer to an
|
||||
/// `AMactorId` struct.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMactorIdInit() -> *mut AMresult {
|
||||
to_result(Ok::<am::ActorId, am::AutomergeError>(am::ActorId::random()))
|
||||
}
|
||||
|
||||
/// \memberof AMactorId
|
||||
/// \brief Allocates a new actor identifier and initializes it from a sequence
|
||||
/// of bytes.
|
||||
///
|
||||
/// \param[in] src A pointer to a contiguous sequence of bytes.
|
||||
/// \param[in] count The number of bytes to copy from \p src.
|
||||
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
|
||||
/// \return A pointer to an `AMresult` struct containing a pointer to an
|
||||
/// `AMactorId` struct.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// src must be a byte array of size `>= count`
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMactorIdInitBytes(src: *const u8, count: usize) -> *mut AMresult {
|
||||
let slice = std::slice::from_raw_parts(src, count);
|
||||
to_result(Ok::<am::ActorId, am::InvalidActorId>(am::ActorId::from(
|
||||
slice,
|
||||
)))
|
||||
}
|
||||
|
||||
/// \memberof AMactorId
|
||||
/// \brief Allocates a new actor identifier and initializes it from a
|
||||
/// hexadecimal string.
|
||||
///
|
||||
/// \param[in] hex_str A UTF-8 string.
|
||||
/// \return A pointer to an `AMresult` struct containing a pointer to an
|
||||
/// `AMactorId` struct.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// hex_str must be a null-terminated array of `c_char`
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMactorIdInitStr(hex_str: *const c_char) -> *mut AMresult {
|
||||
to_result(am::ActorId::from_str(
|
||||
CStr::from_ptr(hex_str).to_str().unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
/// \memberof AMactorId
|
||||
/// \brief Gets the value of an actor identifier as a hexadecimal string.
|
||||
///
|
||||
/// \param[in] actor_id A pointer to an `AMactorId` struct.
|
||||
/// \pre \p actor_id `!= NULL`.
|
||||
/// \return A UTF-8 string.
|
||||
/// \internal
|
||||
///
|
||||
/// # Safety
|
||||
/// actor_id must be a valid pointer to an AMactorId
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMactorIdStr(actor_id: *const AMactorId) -> *const c_char {
|
||||
match actor_id.as_ref() {
|
||||
Some(actor_id) => actor_id.as_c_str(),
|
||||
None => std::ptr::null::<c_char>(),
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
use automerge as am;
|
||||
|
||||
/// \struct AMbyteSpan
|
||||
/// \installed_headerfile
|
||||
/// \brief A view onto a contiguous sequence of bytes.
|
||||
#[repr(C)]
|
||||
#[derive(Eq, PartialEq)]
|
||||
pub struct AMbyteSpan {
|
||||
/// A pointer to an array of bytes.
|
||||
/// \attention <b>NEVER CALL `free()` ON \p src!</b>
|
||||
/// \warning \p src is only valid until the `AMfree()` function is called
|
||||
/// on the `AMresult` struct that stores the array of bytes to
|
||||
/// which it points.
|
||||
pub src: *const u8,
|
||||
/// The number of bytes in the array.
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
impl Default for AMbyteSpan {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
src: std::ptr::null(),
|
||||
count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&am::ActorId> for AMbyteSpan {
|
||||
fn from(actor: &am::ActorId) -> Self {
|
||||
let slice = actor.to_bytes();
|
||||
Self {
|
||||
src: slice.as_ptr(),
|
||||
count: slice.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&mut am::ActorId> for AMbyteSpan {
|
||||
fn from(actor: &mut am::ActorId) -> Self {
|
||||
let slice = actor.to_bytes();
|
||||
Self {
|
||||
src: slice.as_ptr(),
|
||||
count: slice.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&am::ChangeHash> for AMbyteSpan {
|
||||
fn from(change_hash: &am::ChangeHash) -> Self {
|
||||
Self {
|
||||
src: change_hash.0.as_ptr(),
|
||||
count: change_hash.0.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[u8]> for AMbyteSpan {
|
||||
fn from(slice: &[u8]) -> Self {
|
||||
Self {
|
||||
src: slice.as_ptr(),
|
||||
count: slice.len(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,399 +0,0 @@
|
|||
use automerge as am;
|
||||
use std::cmp::Ordering;
|
||||
use std::ffi::c_void;
|
||||
use std::mem::size_of;
|
||||
|
||||
use crate::byte_span::AMbyteSpan;
|
||||
use crate::result::{to_result, AMresult};
|
||||
|
||||
#[repr(C)]
|
||||
struct Detail {
|
||||
len: usize,
|
||||
offset: isize,
|
||||
ptr: *const c_void,
|
||||
}
|
||||
|
||||
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
|
||||
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
|
||||
/// propagate the name of a constant initialized from it so if the
|
||||
/// constant's name is a symbolic representation of the value it can be
|
||||
/// converted into a number by post-processing the header it generated.
|
||||
pub const USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
|
||||
|
||||
impl Detail {
|
||||
fn new(change_hashes: &[am::ChangeHash], offset: isize) -> Self {
|
||||
Self {
|
||||
len: change_hashes.len(),
|
||||
offset,
|
||||
ptr: change_hashes.as_ptr() as *const c_void,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance(&mut self, n: isize) {
|
||||
if n == 0 {
|
||||
return;
|
||||
}
|
||||
let len = self.len as isize;
|
||||
self.offset = if self.offset < 0 {
|
||||
// It's reversed.
|
||||
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
|
||||
if unclipped >= 0 {
|
||||
// Clip it to the forward stop.
|
||||
len
|
||||
} else {
|
||||
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
|
||||
}
|
||||
} else {
|
||||
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
|
||||
if unclipped < 0 {
|
||||
// Clip it to the reverse stop.
|
||||
-(len + 1)
|
||||
} else {
|
||||
std::cmp::max(0, std::cmp::min(unclipped, len))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_index(&self) -> usize {
|
||||
(self.offset
|
||||
+ if self.offset < 0 {
|
||||
self.len as isize
|
||||
} else {
|
||||
0
|
||||
}) as usize
|
||||
}
|
||||
|
||||
pub fn next(&mut self, n: isize) -> Option<&am::ChangeHash> {
|
||||
if self.is_stopped() {
|
||||
return None;
|
||||
}
|
||||
let slice: &[am::ChangeHash] =
|
||||
unsafe { std::slice::from_raw_parts(self.ptr as *const am::ChangeHash, self.len) };
|
||||
let value = &slice[self.get_index()];
|
||||
self.advance(n);
|
||||
Some(value)
|
||||
}
|
||||
|
||||
pub fn is_stopped(&self) -> bool {
|
||||
let len = self.len as isize;
|
||||
self.offset < -len || self.offset == len
|
||||
}
|
||||
|
||||
pub fn prev(&mut self, n: isize) -> Option<&am::ChangeHash> {
|
||||
self.advance(-n);
|
||||
if self.is_stopped() {
|
||||
return None;
|
||||
}
|
||||
let slice: &[am::ChangeHash] =
|
||||
unsafe { std::slice::from_raw_parts(self.ptr as *const am::ChangeHash, self.len) };
|
||||
Some(&slice[self.get_index()])
|
||||
}
|
||||
|
||||
pub fn reversed(&self) -> Self {
|
||||
Self {
|
||||
len: self.len,
|
||||
offset: -(self.offset + 1),
|
||||
ptr: self.ptr,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rewound(&self) -> Self {
|
||||
Self {
|
||||
len: self.len,
|
||||
offset: if self.offset < 0 { -1 } else { 0 },
|
||||
ptr: self.ptr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Detail> for [u8; USIZE_USIZE_USIZE_] {
|
||||
fn from(detail: Detail) -> Self {
|
||||
unsafe {
|
||||
std::slice::from_raw_parts((&detail as *const Detail) as *const u8, USIZE_USIZE_USIZE_)
|
||||
.try_into()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// \struct AMchangeHashes
|
||||
/// \installed_headerfile
|
||||
/// \brief A random-access iterator over a sequence of change hashes.
|
||||
#[repr(C)]
|
||||
#[derive(Eq, PartialEq)]
|
||||
pub struct AMchangeHashes {
|
||||
/// An implementation detail that is intentionally opaque.
|
||||
/// \warning Modifying \p detail will cause undefined behavior.
|
||||
/// \note The actual size of \p detail will vary by platform, this is just
|
||||
/// the one for the platform this documentation was built on.
|
||||
detail: [u8; USIZE_USIZE_USIZE_],
|
||||
}
|
||||
|
||||
impl AMchangeHashes {
|
||||
pub fn new(change_hashes: &[am::ChangeHash]) -> Self {
|
||||
Self {
|
||||
detail: Detail::new(change_hashes, 0).into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance(&mut self, n: isize) {
|
||||
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
|
||||
detail.advance(n);
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
detail.len
|
||||
}
|
||||
|
||||
pub fn next(&mut self, n: isize) -> Option<&am::ChangeHash> {
|
||||
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
|
||||
detail.next(n)
|
||||
}
|
||||
|
||||
pub fn prev(&mut self, n: isize) -> Option<&am::ChangeHash> {
|
||||
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
|
||||
detail.prev(n)
|
||||
}
|
||||
|
||||
pub fn reversed(&self) -> Self {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
Self {
|
||||
detail: detail.reversed().into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rewound(&self) -> Self {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
Self {
|
||||
detail: detail.rewound().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[am::ChangeHash]> for AMchangeHashes {
|
||||
fn as_ref(&self) -> &[am::ChangeHash] {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
unsafe { std::slice::from_raw_parts(detail.ptr as *const am::ChangeHash, detail.len) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AMchangeHashes {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
detail: [0; USIZE_USIZE_USIZE_],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMchangeHashes
|
||||
/// \brief Advances an iterator over a sequence of change hashes by at most
|
||||
/// \p |n| positions where the sign of \p n is relative to the
|
||||
/// iterator's direction.
|
||||
///
|
||||
/// \param[in,out] change_hashes A pointer to an `AMchangeHashes` struct.
|
||||
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
|
||||
/// number of positions to advance.
|
||||
/// \pre \p change_hashes `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// change_hashes must be a valid pointer to an AMchangeHashes
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMchangeHashesAdvance(change_hashes: *mut AMchangeHashes, n: isize) {
|
||||
if let Some(change_hashes) = change_hashes.as_mut() {
|
||||
change_hashes.advance(n);
|
||||
};
|
||||
}
|
||||
|
||||
/// \memberof AMchangeHashes
|
||||
/// \brief Compares the sequences of change hashes underlying a pair of
|
||||
/// iterators.
|
||||
///
|
||||
/// \param[in] change_hashes1 A pointer to an `AMchangeHashes` struct.
|
||||
/// \param[in] change_hashes2 A pointer to an `AMchangeHashes` struct.
|
||||
/// \return `-1` if \p change_hashes1 `<` \p change_hashes2, `0` if
|
||||
/// \p change_hashes1 `==` \p change_hashes2 and `1` if
|
||||
/// \p change_hashes1 `>` \p change_hashes2.
|
||||
/// \pre \p change_hashes1 `!= NULL`.
|
||||
/// \pre \p change_hashes2 `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// change_hashes1 must be a valid pointer to an AMchangeHashes
|
||||
/// change_hashes2 must be a valid pointer to an AMchangeHashes
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMchangeHashesCmp(
|
||||
change_hashes1: *const AMchangeHashes,
|
||||
change_hashes2: *const AMchangeHashes,
|
||||
) -> isize {
|
||||
match (change_hashes1.as_ref(), change_hashes2.as_ref()) {
|
||||
(Some(change_hashes1), Some(change_hashes2)) => {
|
||||
match change_hashes1.as_ref().cmp(change_hashes2.as_ref()) {
|
||||
Ordering::Less => -1,
|
||||
Ordering::Equal => 0,
|
||||
Ordering::Greater => 1,
|
||||
}
|
||||
}
|
||||
(None, Some(_)) => -1,
|
||||
(Some(_), None) => 1,
|
||||
(None, None) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMchangeHashes
|
||||
/// \brief Allocates an iterator over a sequence of change hashes and
|
||||
/// initializes it from a sequence of byte spans.
|
||||
///
|
||||
/// \param[in] src A pointer to an array of `AMbyteSpan` structs.
|
||||
/// \param[in] count The number of `AMbyteSpan` structs to copy from \p src.
|
||||
/// \return A pointer to an `AMresult` struct containing an `AMchangeHashes`
|
||||
/// struct.
|
||||
/// \pre \p src `!= NULL`.
|
||||
/// \pre `0 <` \p count `<= sizeof(`\p src`) / sizeof(AMbyteSpan)`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// src must be an AMbyteSpan array of size `>= count`
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMchangeHashesInit(src: *const AMbyteSpan, count: usize) -> *mut AMresult {
|
||||
let mut change_hashes = Vec::<am::ChangeHash>::new();
|
||||
for n in 0..count {
|
||||
let byte_span = &*src.add(n);
|
||||
let slice = std::slice::from_raw_parts(byte_span.src, byte_span.count);
|
||||
match slice.try_into() {
|
||||
Ok(change_hash) => {
|
||||
change_hashes.push(change_hash);
|
||||
}
|
||||
Err(e) => {
|
||||
return to_result(Err(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
to_result(Ok::<Vec<am::ChangeHash>, am::InvalidChangeHashSlice>(
|
||||
change_hashes,
|
||||
))
|
||||
}
|
||||
|
||||
/// \memberof AMchangeHashes
|
||||
/// \brief Gets the change hash at the current position of an iterator over a
|
||||
/// sequence of change hashes and then advances it by at most \p |n|
|
||||
/// positions where the sign of \p n is relative to the iterator's
|
||||
/// direction.
|
||||
///
|
||||
/// \param[in,out] change_hashes A pointer to an `AMchangeHashes` struct.
|
||||
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
|
||||
/// number of positions to advance.
|
||||
/// \return An `AMbyteSpan` struct with `.src == NULL` when \p change_hashes
|
||||
/// was previously advanced past its forward/reverse limit.
|
||||
/// \pre \p change_hashes `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// change_hashes must be a valid pointer to an AMchangeHashes
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMchangeHashesNext(
|
||||
change_hashes: *mut AMchangeHashes,
|
||||
n: isize,
|
||||
) -> AMbyteSpan {
|
||||
if let Some(change_hashes) = change_hashes.as_mut() {
|
||||
if let Some(change_hash) = change_hashes.next(n) {
|
||||
return change_hash.into();
|
||||
}
|
||||
}
|
||||
AMbyteSpan::default()
|
||||
}
|
||||
|
||||
/// \memberof AMchangeHashes
|
||||
/// \brief Advances an iterator over a sequence of change hashes by at most
|
||||
/// \p |n| positions where the sign of \p n is relative to the
|
||||
/// iterator's direction and then gets the change hash at its new
|
||||
/// position.
|
||||
///
|
||||
/// \param[in,out] change_hashes A pointer to an `AMchangeHashes` struct.
|
||||
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
|
||||
/// number of positions to advance.
|
||||
/// \return An `AMbyteSpan` struct with `.src == NULL` when \p change_hashes is
|
||||
/// presently advanced past its forward/reverse limit.
|
||||
/// \pre \p change_hashes `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// change_hashes must be a valid pointer to an AMchangeHashes
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMchangeHashesPrev(
|
||||
change_hashes: *mut AMchangeHashes,
|
||||
n: isize,
|
||||
) -> AMbyteSpan {
|
||||
if let Some(change_hashes) = change_hashes.as_mut() {
|
||||
if let Some(change_hash) = change_hashes.prev(n) {
|
||||
return change_hash.into();
|
||||
}
|
||||
}
|
||||
AMbyteSpan::default()
|
||||
}
|
||||
|
||||
/// \memberof AMchangeHashes
|
||||
/// \brief Gets the size of the sequence of change hashes underlying an
|
||||
/// iterator.
|
||||
///
|
||||
/// \param[in] change_hashes A pointer to an `AMchangeHashes` struct.
|
||||
/// \return The count of values in \p change_hashes.
|
||||
/// \pre \p change_hashes `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// change_hashes must be a valid pointer to an AMchangeHashes
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMchangeHashesSize(change_hashes: *const AMchangeHashes) -> usize {
|
||||
if let Some(change_hashes) = change_hashes.as_ref() {
|
||||
change_hashes.len()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMchangeHashes
|
||||
/// \brief Creates an iterator over the same sequence of change hashes as the
|
||||
/// given one but with the opposite position and direction.
|
||||
///
|
||||
/// \param[in] change_hashes A pointer to an `AMchangeHashes` struct.
|
||||
/// \return An `AMchangeHashes` struct
|
||||
/// \pre \p change_hashes `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// change_hashes must be a valid pointer to an AMchangeHashes
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMchangeHashesReversed(
|
||||
change_hashes: *const AMchangeHashes,
|
||||
) -> AMchangeHashes {
|
||||
if let Some(change_hashes) = change_hashes.as_ref() {
|
||||
change_hashes.reversed()
|
||||
} else {
|
||||
AMchangeHashes::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMchangeHashes
|
||||
/// \brief Creates an iterator at the starting position over the same sequence
|
||||
/// of change hashes as the given one.
|
||||
///
|
||||
/// \param[in] change_hashes A pointer to an `AMchangeHashes` struct.
|
||||
/// \return An `AMchangeHashes` struct
|
||||
/// \pre \p change_hashes `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// change_hashes must be a valid pointer to an AMchangeHashes
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMchangeHashesRewound(
|
||||
change_hashes: *const AMchangeHashes,
|
||||
) -> AMchangeHashes {
|
||||
if let Some(change_hashes) = change_hashes.as_ref() {
|
||||
change_hashes.rewound()
|
||||
} else {
|
||||
AMchangeHashes::default()
|
||||
}
|
||||
}
|
|
@ -1,398 +0,0 @@
|
|||
use automerge as am;
|
||||
use std::collections::BTreeMap;
|
||||
use std::ffi::c_void;
|
||||
use std::mem::size_of;
|
||||
|
||||
use crate::byte_span::AMbyteSpan;
|
||||
use crate::change::AMchange;
|
||||
use crate::result::{to_result, AMresult};
|
||||
|
||||
#[repr(C)]
|
||||
struct Detail {
|
||||
len: usize,
|
||||
offset: isize,
|
||||
ptr: *const c_void,
|
||||
storage: *mut c_void,
|
||||
}
|
||||
|
||||
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
|
||||
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
|
||||
/// propagate the name of a constant initialized from it so if the
|
||||
/// constant's name is a symbolic representation of the value it can be
|
||||
/// converted into a number by post-processing the header it generated.
|
||||
pub const USIZE_USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
|
||||
|
||||
impl Detail {
|
||||
fn new(changes: &[am::Change], offset: isize, storage: &mut BTreeMap<usize, AMchange>) -> Self {
|
||||
let storage: *mut BTreeMap<usize, AMchange> = storage;
|
||||
Self {
|
||||
len: changes.len(),
|
||||
offset,
|
||||
ptr: changes.as_ptr() as *const c_void,
|
||||
storage: storage as *mut c_void,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance(&mut self, n: isize) {
|
||||
if n == 0 {
|
||||
return;
|
||||
}
|
||||
let len = self.len as isize;
|
||||
self.offset = if self.offset < 0 {
|
||||
// It's reversed.
|
||||
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
|
||||
if unclipped >= 0 {
|
||||
// Clip it to the forward stop.
|
||||
len
|
||||
} else {
|
||||
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
|
||||
}
|
||||
} else {
|
||||
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
|
||||
if unclipped < 0 {
|
||||
// Clip it to the reverse stop.
|
||||
-(len + 1)
|
||||
} else {
|
||||
std::cmp::max(0, std::cmp::min(unclipped, len))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_index(&self) -> usize {
|
||||
(self.offset
|
||||
+ if self.offset < 0 {
|
||||
self.len as isize
|
||||
} else {
|
||||
0
|
||||
}) as usize
|
||||
}
|
||||
|
||||
pub fn next(&mut self, n: isize) -> Option<*const AMchange> {
|
||||
if self.is_stopped() {
|
||||
return None;
|
||||
}
|
||||
let slice: &mut [am::Change] =
|
||||
unsafe { std::slice::from_raw_parts_mut(self.ptr as *mut am::Change, self.len) };
|
||||
let storage = unsafe { &mut *(self.storage as *mut BTreeMap<usize, AMchange>) };
|
||||
let index = self.get_index();
|
||||
let value = match storage.get_mut(&index) {
|
||||
Some(value) => value,
|
||||
None => {
|
||||
storage.insert(index, AMchange::new(&mut slice[index]));
|
||||
storage.get_mut(&index).unwrap()
|
||||
}
|
||||
};
|
||||
self.advance(n);
|
||||
Some(value)
|
||||
}
|
||||
|
||||
pub fn is_stopped(&self) -> bool {
|
||||
let len = self.len as isize;
|
||||
self.offset < -len || self.offset == len
|
||||
}
|
||||
|
||||
pub fn prev(&mut self, n: isize) -> Option<*const AMchange> {
|
||||
self.advance(-n);
|
||||
if self.is_stopped() {
|
||||
return None;
|
||||
}
|
||||
let slice: &mut [am::Change] =
|
||||
unsafe { std::slice::from_raw_parts_mut(self.ptr as *mut am::Change, self.len) };
|
||||
let storage = unsafe { &mut *(self.storage as *mut BTreeMap<usize, AMchange>) };
|
||||
let index = self.get_index();
|
||||
Some(match storage.get_mut(&index) {
|
||||
Some(value) => value,
|
||||
None => {
|
||||
storage.insert(index, AMchange::new(&mut slice[index]));
|
||||
storage.get_mut(&index).unwrap()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reversed(&self) -> Self {
|
||||
Self {
|
||||
len: self.len,
|
||||
offset: -(self.offset + 1),
|
||||
ptr: self.ptr,
|
||||
storage: self.storage,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rewound(&self) -> Self {
|
||||
Self {
|
||||
len: self.len,
|
||||
offset: if self.offset < 0 { -1 } else { 0 },
|
||||
ptr: self.ptr,
|
||||
storage: self.storage,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Detail> for [u8; USIZE_USIZE_USIZE_USIZE_] {
|
||||
fn from(detail: Detail) -> Self {
|
||||
unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
(&detail as *const Detail) as *const u8,
|
||||
USIZE_USIZE_USIZE_USIZE_,
|
||||
)
|
||||
.try_into()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// \struct AMchanges
|
||||
/// \installed_headerfile
|
||||
/// \brief A random-access iterator over a sequence of changes.
|
||||
#[repr(C)]
|
||||
#[derive(Eq, PartialEq)]
|
||||
pub struct AMchanges {
|
||||
/// An implementation detail that is intentionally opaque.
|
||||
/// \warning Modifying \p detail will cause undefined behavior.
|
||||
/// \note The actual size of \p detail will vary by platform, this is just
|
||||
/// the one for the platform this documentation was built on.
|
||||
detail: [u8; USIZE_USIZE_USIZE_USIZE_],
|
||||
}
|
||||
|
||||
impl AMchanges {
|
||||
pub fn new(changes: &[am::Change], storage: &mut BTreeMap<usize, AMchange>) -> Self {
|
||||
Self {
|
||||
detail: Detail::new(changes, 0, &mut *storage).into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance(&mut self, n: isize) {
|
||||
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
|
||||
detail.advance(n);
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
detail.len
|
||||
}
|
||||
|
||||
pub fn next(&mut self, n: isize) -> Option<*const AMchange> {
|
||||
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
|
||||
detail.next(n)
|
||||
}
|
||||
|
||||
pub fn prev(&mut self, n: isize) -> Option<*const AMchange> {
|
||||
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
|
||||
detail.prev(n)
|
||||
}
|
||||
|
||||
pub fn reversed(&self) -> Self {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
Self {
|
||||
detail: detail.reversed().into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rewound(&self) -> Self {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
Self {
|
||||
detail: detail.rewound().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[am::Change]> for AMchanges {
|
||||
fn as_ref(&self) -> &[am::Change] {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
unsafe { std::slice::from_raw_parts(detail.ptr as *const am::Change, detail.len) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AMchanges {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
detail: [0; USIZE_USIZE_USIZE_USIZE_],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMchanges
|
||||
/// \brief Advances an iterator over a sequence of changes by at most \p |n|
|
||||
/// positions where the sign of \p n is relative to the iterator's
|
||||
/// direction.
|
||||
///
|
||||
/// \param[in,out] changes A pointer to an `AMchanges` struct.
|
||||
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
|
||||
/// number of positions to advance.
|
||||
/// \pre \p changes `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// changes must be a valid pointer to an AMchanges
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMchangesAdvance(changes: *mut AMchanges, n: isize) {
|
||||
if let Some(changes) = changes.as_mut() {
|
||||
changes.advance(n);
|
||||
};
|
||||
}
|
||||
|
||||
/// \memberof AMchanges
|
||||
/// \brief Tests the equality of two sequences of changes underlying a pair of
|
||||
/// iterators.
|
||||
///
|
||||
/// \param[in] changes1 A pointer to an `AMchanges` struct.
|
||||
/// \param[in] changes2 A pointer to an `AMchanges` struct.
|
||||
/// \return `true` if \p changes1 `==` \p changes2 and `false` otherwise.
|
||||
/// \pre \p changes1 `!= NULL`.
|
||||
/// \pre \p changes2 `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// changes1 must be a valid pointer to an AMchanges
|
||||
/// changes2 must be a valid pointer to an AMchanges
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMchangesEqual(
|
||||
changes1: *const AMchanges,
|
||||
changes2: *const AMchanges,
|
||||
) -> bool {
|
||||
match (changes1.as_ref(), changes2.as_ref()) {
|
||||
(Some(changes1), Some(changes2)) => changes1.as_ref() == changes2.as_ref(),
|
||||
(None, Some(_)) | (Some(_), None) | (None, None) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMchanges
|
||||
/// \brief Allocates an iterator over a sequence of changes and initializes it
|
||||
/// from a sequence of byte spans.
|
||||
///
|
||||
/// \param[in] src A pointer to an array of `AMbyteSpan` structs.
|
||||
/// \param[in] count The number of `AMbyteSpan` structs to copy from \p src.
|
||||
/// \return A pointer to an `AMresult` struct containing an `AMchanges` struct.
|
||||
/// \pre \p src `!= NULL`.
|
||||
/// \pre `0 <` \p count `<= sizeof(`\p src`) / sizeof(AMbyteSpan)`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// src must be an AMbyteSpan array of size `>= count`
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMchangesInit(src: *const AMbyteSpan, count: usize) -> *mut AMresult {
|
||||
let mut changes = Vec::<am::Change>::new();
|
||||
for n in 0..count {
|
||||
let byte_span = &*src.add(n);
|
||||
let slice = std::slice::from_raw_parts(byte_span.src, byte_span.count);
|
||||
match slice.try_into() {
|
||||
Ok(change) => {
|
||||
changes.push(change);
|
||||
}
|
||||
Err(e) => {
|
||||
return to_result(Err::<Vec<am::Change>, am::LoadChangeError>(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
to_result(Ok::<Vec<am::Change>, am::LoadChangeError>(changes))
|
||||
}
|
||||
|
||||
/// \memberof AMchanges
|
||||
/// \brief Gets the change at the current position of an iterator over a
|
||||
/// sequence of changes and then advances it by at most \p |n| positions
|
||||
/// where the sign of \p n is relative to the iterator's direction.
|
||||
///
|
||||
/// \param[in,out] changes A pointer to an `AMchanges` struct.
|
||||
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
|
||||
/// number of positions to advance.
|
||||
/// \return A pointer to an `AMchange` struct that's `NULL` when \p changes was
|
||||
/// previously advanced past its forward/reverse limit.
|
||||
/// \pre \p changes `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// changes must be a valid pointer to an AMchanges
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMchangesNext(changes: *mut AMchanges, n: isize) -> *const AMchange {
|
||||
if let Some(changes) = changes.as_mut() {
|
||||
if let Some(change) = changes.next(n) {
|
||||
return change;
|
||||
}
|
||||
}
|
||||
std::ptr::null()
|
||||
}
|
||||
|
||||
/// \memberof AMchanges
|
||||
/// \brief Advances an iterator over a sequence of changes by at most \p |n|
|
||||
/// positions where the sign of \p n is relative to the iterator's
|
||||
/// direction and then gets the change at its new position.
|
||||
///
|
||||
/// \param[in,out] changes A pointer to an `AMchanges` struct.
|
||||
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
|
||||
/// number of positions to advance.
|
||||
/// \return A pointer to an `AMchange` struct that's `NULL` when \p changes is
|
||||
/// presently advanced past its forward/reverse limit.
|
||||
/// \pre \p changes `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// changes must be a valid pointer to an AMchanges
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMchangesPrev(changes: *mut AMchanges, n: isize) -> *const AMchange {
|
||||
if let Some(changes) = changes.as_mut() {
|
||||
if let Some(change) = changes.prev(n) {
|
||||
return change;
|
||||
}
|
||||
}
|
||||
std::ptr::null()
|
||||
}
|
||||
|
||||
/// \memberof AMchanges
|
||||
/// \brief Gets the size of the sequence of changes underlying an iterator.
|
||||
///
|
||||
/// \param[in] changes A pointer to an `AMchanges` struct.
|
||||
/// \return The count of values in \p changes.
|
||||
/// \pre \p changes `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// changes must be a valid pointer to an AMchanges
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMchangesSize(changes: *const AMchanges) -> usize {
|
||||
if let Some(changes) = changes.as_ref() {
|
||||
changes.len()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMchanges
|
||||
/// \brief Creates an iterator over the same sequence of changes as the given
|
||||
/// one but with the opposite position and direction.
|
||||
///
|
||||
/// \param[in] changes A pointer to an `AMchanges` struct.
|
||||
/// \return An `AMchanges` struct.
|
||||
/// \pre \p changes `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// changes must be a valid pointer to an AMchanges
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMchangesReversed(changes: *const AMchanges) -> AMchanges {
|
||||
if let Some(changes) = changes.as_ref() {
|
||||
changes.reversed()
|
||||
} else {
|
||||
AMchanges::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMchanges
|
||||
/// \brief Creates an iterator at the starting position over the same sequence
|
||||
/// of changes as the given one.
|
||||
///
|
||||
/// \param[in] changes A pointer to an `AMchanges` struct.
|
||||
/// \return An `AMchanges` struct
|
||||
/// \pre \p changes `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// changes must be a valid pointer to an AMchanges
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMchangesRewound(changes: *const AMchanges) -> AMchanges {
|
||||
if let Some(changes) = changes.as_ref() {
|
||||
changes.rewound()
|
||||
} else {
|
||||
AMchanges::default()
|
||||
}
|
||||
}
|
|
@ -1,833 +0,0 @@
|
|||
use automerge as am;
|
||||
use automerge::transaction::{CommitOptions, Transactable};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
use crate::actor_id::AMactorId;
|
||||
use crate::change_hashes::AMchangeHashes;
|
||||
use crate::obj::AMobjId;
|
||||
use crate::result::{to_result, AMresult, AMvalue};
|
||||
use crate::sync::{to_sync_message, AMsyncMessage, AMsyncState};
|
||||
|
||||
pub mod list;
|
||||
pub mod map;
|
||||
pub mod utils;
|
||||
|
||||
use crate::changes::AMchanges;
|
||||
use crate::doc::utils::to_str;
|
||||
use crate::doc::utils::{to_actor_id, to_doc, to_doc_mut, to_obj_id};
|
||||
|
||||
macro_rules! to_changes {
|
||||
($handle:expr) => {{
|
||||
let handle = $handle.as_ref();
|
||||
match handle {
|
||||
Some(b) => b,
|
||||
None => return AMresult::err("Invalid AMchanges pointer").into(),
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! to_index {
|
||||
($index:expr, $len:expr, $param_name:expr) => {{
|
||||
if $index > $len && $index != usize::MAX {
|
||||
return AMresult::err(&format!("Invalid {} {}", $param_name, $index)).into();
|
||||
}
|
||||
std::cmp::min($index, $len)
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! to_sync_state_mut {
|
||||
($handle:expr) => {{
|
||||
let handle = $handle.as_mut();
|
||||
match handle {
|
||||
Some(b) => b,
|
||||
None => return AMresult::err("Invalid AMsyncState pointer").into(),
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
/// \struct AMdoc
|
||||
/// \installed_headerfile
|
||||
/// \brief A JSON-like CRDT.
|
||||
#[derive(Clone)]
|
||||
pub struct AMdoc(am::AutoCommit);
|
||||
|
||||
impl AMdoc {
|
||||
pub fn new(auto_commit: am::AutoCommit) -> Self {
|
||||
Self(auto_commit)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<am::AutoCommit> for AMdoc {
|
||||
fn as_ref(&self) -> &am::AutoCommit {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for AMdoc {
|
||||
type Target = am::AutoCommit;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for AMdoc {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Applies a sequence of changes to a document.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] changes A pointer to an `AMchanges` struct.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre \p changes `!= NULL`.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// changes must be a valid pointer to an AMchanges.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMapplyChanges(
|
||||
doc: *mut AMdoc,
|
||||
changes: *const AMchanges,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let changes = to_changes!(changes);
|
||||
to_result(doc.apply_changes(changes.as_ref().to_vec()))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Allocates storage for a document and initializes it by duplicating
|
||||
/// the given document.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \return A pointer to an `AMresult` struct containing a pointer to an
|
||||
/// `AMdoc` struct.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMclone(doc: *const AMdoc) -> *mut AMresult {
|
||||
let doc = to_doc!(doc);
|
||||
to_result(doc.as_ref().clone())
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Allocates a new document and initializes it with defaults.
|
||||
///
|
||||
/// \param[in] actor_id A pointer to an `AMactorId` struct or `NULL` for a
|
||||
/// random one.
|
||||
/// \return A pointer to an `AMresult` struct containing a pointer to an
|
||||
/// `AMdoc` struct.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
///
|
||||
/// # Safety
|
||||
/// actor_id must be a valid pointer to an AMactorId or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMcreate(actor_id: *const AMactorId) -> *mut AMresult {
|
||||
to_result(match actor_id.as_ref() {
|
||||
Some(actor_id) => am::AutoCommit::new().with_actor(actor_id.as_ref().clone()),
|
||||
None => am::AutoCommit::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Commits the current operations on a document with an optional
|
||||
/// message and/or time override as seconds since the epoch.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] message A UTF-8 string or `NULL`.
|
||||
/// \param[in] time A pointer to a `time_t` value or `NULL`.
|
||||
/// \return A pointer to an `AMresult` struct containing an `AMchangeHashes`
|
||||
/// with one element.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMcommit(
|
||||
doc: *mut AMdoc,
|
||||
message: *const c_char,
|
||||
time: *const libc::time_t,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let mut options = CommitOptions::default();
|
||||
if !message.is_null() {
|
||||
options.set_message(to_str(message));
|
||||
}
|
||||
if let Some(time) = time.as_ref() {
|
||||
options.set_time(*time);
|
||||
}
|
||||
to_result(doc.commit_with(options))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Tests the equality of two documents after closing their respective
|
||||
/// transactions.
|
||||
///
|
||||
/// \param[in,out] doc1 An `AMdoc` struct.
|
||||
/// \param[in,out] doc2 An `AMdoc` struct.
|
||||
/// \return `true` if \p doc1 `==` \p doc2 and `false` otherwise.
|
||||
/// \pre \p doc1 `!= NULL`.
|
||||
/// \pre \p doc2 `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// doc1 must be a valid pointer to an AMdoc
|
||||
/// doc2 must be a valid pointer to an AMdoc
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMequal(doc1: *mut AMdoc, doc2: *mut AMdoc) -> bool {
|
||||
match (doc1.as_mut(), doc2.as_mut()) {
|
||||
(Some(doc1), Some(doc2)) => doc1.document().get_heads() == doc2.document().get_heads(),
|
||||
(None, Some(_)) | (Some(_), None) | (None, None) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Forks this document at the current or a historical point for use by
|
||||
/// a different actor.
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] heads A pointer to an `AMchangeHashes` struct for a historical
|
||||
/// point or `NULL` for the current point.
|
||||
/// \return A pointer to an `AMresult` struct containing a pointer to an
|
||||
/// `AMdoc` struct.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMfork(doc: *mut AMdoc, heads: *const AMchangeHashes) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
match heads.as_ref() {
|
||||
None => to_result(doc.fork()),
|
||||
Some(heads) => to_result(doc.fork_at(heads.as_ref())),
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Generates a synchronization message for a peer based upon the given
|
||||
/// synchronization state.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in,out] sync_state A pointer to an `AMsyncState` struct.
|
||||
/// \return A pointer to an `AMresult` struct containing either a pointer to an
|
||||
/// `AMsyncMessage` struct or a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre \p sync_state `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// sync_state must be a valid pointer to an AMsyncState
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMgenerateSyncMessage(
|
||||
doc: *mut AMdoc,
|
||||
sync_state: *mut AMsyncState,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let sync_state = to_sync_state_mut!(sync_state);
|
||||
to_result(doc.generate_sync_message(sync_state.as_mut()))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Gets a document's actor identifier.
|
||||
///
|
||||
/// \param[in] doc A pointer to an `AMdoc` struct.
|
||||
/// \return A pointer to an `AMresult` struct containing a pointer to an
|
||||
/// `AMactorId` struct.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
///
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMgetActorId(doc: *const AMdoc) -> *mut AMresult {
|
||||
let doc = to_doc!(doc);
|
||||
to_result(Ok::<am::ActorId, am::AutomergeError>(
|
||||
doc.get_actor().clone(),
|
||||
))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Gets the change added to a document by its respective hash.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] src A pointer to an array of bytes.
|
||||
/// \param[in] count The number of bytes in \p src.
|
||||
/// \return A pointer to an `AMresult` struct containing an `AMchanges` struct.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre \p src `!= NULL`.
|
||||
/// \pre \p count `>= AM_CHANGE_HASH_SIZE`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// src must be a byte array of size `>= automerge::types::HASH_SIZE`
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMgetChangeByHash(
|
||||
doc: *mut AMdoc,
|
||||
src: *const u8,
|
||||
count: usize,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let slice = std::slice::from_raw_parts(src, count);
|
||||
match slice.try_into() {
|
||||
Ok(change_hash) => to_result(doc.get_change_by_hash(&change_hash)),
|
||||
Err(e) => AMresult::err(&e.to_string()).into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Gets the changes added to a document by their respective hashes.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] have_deps A pointer to an `AMchangeHashes` struct or `NULL`.
|
||||
/// \return A pointer to an `AMresult` struct containing an `AMchanges` struct.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMgetChanges(
|
||||
doc: *mut AMdoc,
|
||||
have_deps: *const AMchangeHashes,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let empty_deps = Vec::<am::ChangeHash>::new();
|
||||
let have_deps = match have_deps.as_ref() {
|
||||
Some(have_deps) => have_deps.as_ref(),
|
||||
None => &empty_deps,
|
||||
};
|
||||
to_result(doc.get_changes(have_deps))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Gets the changes added to a second document that weren't added to
|
||||
/// a first document.
|
||||
///
|
||||
/// \param[in,out] doc1 An `AMdoc` struct.
|
||||
/// \param[in,out] doc2 An `AMdoc` struct.
|
||||
/// \return A pointer to an `AMresult` struct containing an `AMchanges` struct.
|
||||
/// \pre \p doc1 `!= NULL`.
|
||||
/// \pre \p doc2 `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc1 must be a valid pointer to an AMdoc
|
||||
/// doc2 must be a valid pointer to an AMdoc
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMgetChangesAdded(doc1: *mut AMdoc, doc2: *mut AMdoc) -> *mut AMresult {
|
||||
let doc1 = to_doc_mut!(doc1);
|
||||
let doc2 = to_doc_mut!(doc2);
|
||||
to_result(doc1.get_changes_added(doc2))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Gets the current heads of a document.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \return A pointer to an `AMresult` struct containing an `AMchangeHashes`
|
||||
/// struct.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMgetHeads(doc: *mut AMdoc) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
to_result(Ok::<Vec<am::ChangeHash>, am::AutomergeError>(
|
||||
doc.get_heads(),
|
||||
))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Gets the hashes of the changes in a document that aren't transitive
|
||||
/// dependencies of the given hashes of changes.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] heads A pointer to an `AMchangeHashes` struct or `NULL`.
|
||||
/// \return A pointer to an `AMresult` struct containing an `AMchangeHashes`
|
||||
/// struct.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMgetMissingDeps(
|
||||
doc: *mut AMdoc,
|
||||
heads: *const AMchangeHashes,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let empty_heads = Vec::<am::ChangeHash>::new();
|
||||
let heads = match heads.as_ref() {
|
||||
Some(heads) => heads.as_ref(),
|
||||
None => &empty_heads,
|
||||
};
|
||||
to_result(doc.get_missing_deps(heads))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Gets the last change made to a document.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \return A pointer to an `AMresult` struct containing either an `AMchange`
|
||||
/// struct or a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMgetLastLocalChange(doc: *mut AMdoc) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
to_result(doc.get_last_local_change())
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Gets the current or historical keys of a map object.
|
||||
///
|
||||
/// \param[in] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
|
||||
/// keys or `NULL` for current keys.
|
||||
/// \return A pointer to an `AMresult` struct containing an `AMstrs` struct.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMkeys(
|
||||
doc: *const AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
heads: *const AMchangeHashes,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc!(doc);
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
match heads.as_ref() {
|
||||
None => to_result(doc.keys(obj_id)),
|
||||
Some(heads) => to_result(doc.keys_at(obj_id, heads.as_ref())),
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Allocates storage for a document and initializes it with the compact
|
||||
/// form of an incremental save.
|
||||
///
|
||||
/// \param[in] src A pointer to an array of bytes.
|
||||
/// \param[in] count The number of bytes in \p src to load.
|
||||
/// \return A pointer to an `AMresult` struct containing a pointer to an
|
||||
/// `AMdoc` struct.
|
||||
/// \pre \p src `!= NULL`.
|
||||
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// src must be a byte array of size `>= count`
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMload(src: *const u8, count: usize) -> *mut AMresult {
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(std::slice::from_raw_parts(src, count));
|
||||
to_result(am::AutoCommit::load(&data))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Loads the compact form of an incremental save into a document.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] src A pointer to an array of bytes.
|
||||
/// \param[in] count The number of bytes in \p src to load.
|
||||
/// \return A pointer to an `AMresult` struct containing the number of
|
||||
/// operations loaded from \p src.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre \p src `!= NULL`.
|
||||
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// src must be a byte array of size `>= count`
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMloadIncremental(
|
||||
doc: *mut AMdoc,
|
||||
src: *const u8,
|
||||
count: usize,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(std::slice::from_raw_parts(src, count));
|
||||
to_result(doc.load_incremental(&data))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Applies all of the changes in \p src which are not in \p dest to
|
||||
/// \p dest.
|
||||
///
|
||||
/// \param[in,out] dest A pointer to an `AMdoc` struct.
|
||||
/// \param[in,out] src A pointer to an `AMdoc` struct.
|
||||
/// \return A pointer to an `AMresult` struct containing an `AMchangeHashes`
|
||||
/// struct.
|
||||
/// \pre \p dest `!= NULL`.
|
||||
/// \pre \p src `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// dest must be a valid pointer to an AMdoc
|
||||
/// src must be a valid pointer to an AMdoc
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmerge(dest: *mut AMdoc, src: *mut AMdoc) -> *mut AMresult {
|
||||
let dest = to_doc_mut!(dest);
|
||||
to_result(dest.merge(to_doc_mut!(src)))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Gets the current or historical size of an object.
|
||||
///
|
||||
/// \param[in] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
|
||||
/// size or `NULL` for current size.
|
||||
/// \return A 64-bit unsigned integer.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMobjSize(
|
||||
doc: *const AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
heads: *const AMchangeHashes,
|
||||
) -> usize {
|
||||
if let Some(doc) = doc.as_ref() {
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
match heads.as_ref() {
|
||||
None => doc.length(obj_id),
|
||||
Some(heads) => doc.length_at(obj_id, heads.as_ref()),
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Gets the current or historical values of an object within its entire
|
||||
/// range.
|
||||
///
|
||||
/// \param[in] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
|
||||
/// items or `NULL` for current items.
|
||||
/// \return A pointer to an `AMresult` struct containing an `AMobjItems` struct.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMobjValues(
|
||||
doc: *const AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
heads: *const AMchangeHashes,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc!(doc);
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
match heads.as_ref() {
|
||||
None => to_result(doc.values(obj_id)),
|
||||
Some(heads) => to_result(doc.values_at(obj_id, heads.as_ref())),
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Gets the number of pending operations added during a document's
|
||||
/// current transaction.
|
||||
///
|
||||
/// \param[in] doc A pointer to an `AMdoc` struct.
|
||||
/// \return The count of pending operations for \p doc.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMpendingOps(doc: *const AMdoc) -> usize {
|
||||
if let Some(doc) = doc.as_ref() {
|
||||
doc.pending_ops()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Receives a synchronization message from a peer based upon a given
|
||||
/// synchronization state.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in,out] sync_state A pointer to an `AMsyncState` struct.
|
||||
/// \param[in] sync_message A pointer to an `AMsyncMessage` struct.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre \p sync_state `!= NULL`.
|
||||
/// \pre \p sync_message `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// sync_state must be a valid pointer to an AMsyncState
|
||||
/// sync_message must be a valid pointer to an AMsyncMessage
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMreceiveSyncMessage(
|
||||
doc: *mut AMdoc,
|
||||
sync_state: *mut AMsyncState,
|
||||
sync_message: *const AMsyncMessage,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let sync_state = to_sync_state_mut!(sync_state);
|
||||
let sync_message = to_sync_message!(sync_message);
|
||||
to_result(doc.receive_sync_message(sync_state.as_mut(), sync_message.as_ref().clone()))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Cancels the pending operations added during a document's current
|
||||
/// transaction and gets the number of cancellations.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \return The count of pending operations for \p doc that were cancelled.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMrollback(doc: *mut AMdoc) -> usize {
|
||||
if let Some(doc) = doc.as_mut() {
|
||||
doc.rollback()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Saves the entirety of a document into a compact form.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \return A pointer to an `AMresult` struct containing an array of bytes as
|
||||
/// an `AMbyteSpan` struct.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMsave(doc: *mut AMdoc) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
to_result(Ok(doc.save()))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Saves the changes to a document since its last save into a compact
|
||||
/// form.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \return A pointer to an `AMresult` struct containing an array of bytes as
|
||||
/// an `AMbyteSpan` struct.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMsaveIncremental(doc: *mut AMdoc) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
to_result(Ok(doc.save_incremental()))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Puts the actor identifier of a document.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] actor_id A pointer to an `AMactorId` struct.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre \p actor_id `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// actor_id must be a valid pointer to an AMactorId
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMsetActorId(
|
||||
doc: *mut AMdoc,
|
||||
actor_id: *const AMactorId,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let actor_id = to_actor_id!(actor_id);
|
||||
doc.set_actor(actor_id.as_ref().clone());
|
||||
to_result(Ok(()))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Splices values into and/or removes values from the identified object
|
||||
/// at a given position within it.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] pos A position in the object identified by \p obj_id or
|
||||
/// `SIZE_MAX` to indicate one past its end.
|
||||
/// \param[in] del The number of characters to delete or `SIZE_MAX` to indicate
|
||||
/// all of them.
|
||||
/// \param[in] src A pointer to an array of `AMvalue` structs.
|
||||
/// \param[in] count The number of `AMvalue` structs in \p src to load.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id`)` or \p pos `== SIZE_MAX`.
|
||||
/// \pre `0 <=` \p del `<= AMobjSize(`\p obj_id`)` or \p del `== SIZE_MAX`.
|
||||
/// \pre `(`\p src `!= NULL and 1 <=` \p count `<= sizeof(`\p src`)/
|
||||
/// sizeof(AMvalue)) or `\p src `== NULL or `\p count `== 0`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// src must be an AMvalue array of size `>= count` or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMsplice(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
pos: usize,
|
||||
del: usize,
|
||||
src: *const AMvalue,
|
||||
count: usize,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
let len = doc.length(obj_id);
|
||||
let pos = to_index!(pos, len, "pos");
|
||||
let del = to_index!(del, len, "del");
|
||||
let mut vals: Vec<am::ScalarValue> = vec![];
|
||||
if !(src.is_null() || count == 0) {
|
||||
let c_vals = std::slice::from_raw_parts(src, count);
|
||||
for c_val in c_vals {
|
||||
match c_val.try_into() {
|
||||
Ok(s) => {
|
||||
vals.push(s);
|
||||
}
|
||||
Err(e) => {
|
||||
return AMresult::err(&e.to_string()).into();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
to_result(doc.splice(obj_id, pos, del, vals))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Splices characters into and/or removes characters from the
|
||||
/// identified object at a given position within it.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] pos A position in the text object identified by \p obj_id or
|
||||
/// `SIZE_MAX` to indicate one past its end.
|
||||
/// \param[in] del The number of characters to delete or `SIZE_MAX` to indicate
|
||||
/// all of them.
|
||||
/// \param[in] text A UTF-8 string.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id`)` or \p pos `== SIZE_MAX`.
|
||||
/// \pre `0 <=` \p del `<= AMobjSize(`\p obj_id`)` or \p del `== SIZE_MAX`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// text must be a null-terminated array of `c_char` or NULL.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMspliceText(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
pos: usize,
|
||||
del: usize,
|
||||
text: *const c_char,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
let len = doc.length(obj_id);
|
||||
let pos = to_index!(pos, len, "pos");
|
||||
let del = to_index!(del, len, "del");
|
||||
to_result(doc.splice_text(obj_id, pos, del, &to_str(text)))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Gets the current or historical string represented by a text object.
|
||||
///
|
||||
/// \param[in] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
|
||||
/// keys or `NULL` for current keys.
|
||||
/// \return A pointer to an `AMresult` struct containing a UTF-8 string.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMtext(
|
||||
doc: *const AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
heads: *const AMchangeHashes,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc!(doc);
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
match heads.as_ref() {
|
||||
None => to_result(doc.text(obj_id)),
|
||||
Some(heads) => to_result(doc.text_at(obj_id, heads.as_ref())),
|
||||
}
|
||||
}
|
|
@ -1,604 +0,0 @@
|
|||
use automerge as am;
|
||||
use automerge::transaction::Transactable;
|
||||
use std::os::raw::c_char;
|
||||
|
||||
use crate::change_hashes::AMchangeHashes;
|
||||
use crate::doc::{to_doc, to_doc_mut, to_obj_id, to_str, AMdoc};
|
||||
use crate::obj::{AMobjId, AMobjType};
|
||||
use crate::result::{to_result, AMresult};
|
||||
|
||||
pub mod item;
|
||||
pub mod items;
|
||||
|
||||
macro_rules! adjust {
|
||||
($index:expr, $insert:expr, $len:expr) => {{
|
||||
// An empty object can only be inserted into.
|
||||
let insert = $insert || $len == 0;
|
||||
let end = if insert { $len } else { $len - 1 };
|
||||
if $index > end && $index != usize::MAX {
|
||||
return AMresult::err(&format!("Invalid index {}", $index)).into();
|
||||
}
|
||||
(std::cmp::min($index, end), insert)
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! to_range {
|
||||
($begin:expr, $end:expr) => {{
|
||||
if $begin > $end {
|
||||
return AMresult::err(&format!("Invalid range [{}-{})", $begin, $end)).into();
|
||||
};
|
||||
($begin..$end)
|
||||
}};
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Deletes an index in a list object.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] index An index in the list object identified by \p obj_id or
|
||||
/// `SIZE_MAX` to indicate its last index.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistDelete(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
index: usize,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
let (index, _) = adjust!(index, false, doc.length(obj_id));
|
||||
to_result(doc.delete(obj_id, index))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Gets the current or historical value at an index in a list object.
|
||||
///
|
||||
/// \param[in] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] index An index in the list object identified by \p obj_id or
|
||||
/// `SIZE_MAX` to indicate its last index.
|
||||
/// \param[in] heads A pointer to an `AMchangeHashes` struct for a historical
|
||||
/// value or `NULL` for the current value.
|
||||
/// \return A pointer to an `AMresult` struct that doesn't contain a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistGet(
|
||||
doc: *const AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
index: usize,
|
||||
heads: *const AMchangeHashes,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc!(doc);
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
let (index, _) = adjust!(index, false, doc.length(obj_id));
|
||||
match heads.as_ref() {
|
||||
None => to_result(doc.get(obj_id, index)),
|
||||
Some(heads) => to_result(doc.get_at(obj_id, index, heads.as_ref())),
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Gets all of the historical values at an index in a list object until
|
||||
/// its current one or a specific one.
|
||||
///
|
||||
/// \param[in] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] index An index in the list object identified by \p obj_id or
|
||||
/// `SIZE_MAX` to indicate its last index.
|
||||
/// \param[in] heads A pointer to an `AMchangeHashes` struct for a historical
|
||||
/// last value or `NULL` for the current last value.
|
||||
/// \return A pointer to an `AMresult` struct containing an `AMobjItems` struct.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistGetAll(
|
||||
doc: *const AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
index: usize,
|
||||
heads: *const AMchangeHashes,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc!(doc);
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
let (index, _) = adjust!(index, false, doc.length(obj_id));
|
||||
match heads.as_ref() {
|
||||
None => to_result(doc.get_all(obj_id, index)),
|
||||
Some(heads) => to_result(doc.get_all_at(obj_id, index, heads.as_ref())),
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Increments a counter at an index in a list object by the given
|
||||
/// value.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] index An index in the list object identified by \p obj_id or
|
||||
/// `SIZE_MAX` to indicate its last index.
|
||||
/// \param[in] value A 64-bit signed integer.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistIncrement(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
index: usize,
|
||||
value: i64,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
let (index, _) = adjust!(index, false, doc.length(obj_id));
|
||||
to_result(doc.increment(obj_id, index, value))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Puts a boolean as the value at an index in a list object.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] index An index in the list object identified by \p obj_id or
|
||||
/// `SIZE_MAX` to indicate its last index if \p insert
|
||||
/// `== false` or one past its last index if \p insert
|
||||
/// `== true`.
|
||||
/// \param[in] insert A flag to insert \p value before \p index instead of
|
||||
/// writing \p value over \p index.
|
||||
/// \param[in] value A boolean.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistPutBool(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
index: usize,
|
||||
insert: bool,
|
||||
value: bool,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
|
||||
let value = am::ScalarValue::Boolean(value);
|
||||
to_result(if insert {
|
||||
doc.insert(obj_id, index, value)
|
||||
} else {
|
||||
doc.put(obj_id, index, value)
|
||||
})
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Puts a sequence of bytes as the value at an index in a list object.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] index An index in the list object identified by \p obj_id or
|
||||
/// `SIZE_MAX` to indicate its last index if \p insert
|
||||
/// `== false` or one past its last index if \p insert
|
||||
/// `== true`.
|
||||
/// \param[in] insert A flag to insert \p src before \p index instead of
|
||||
/// writing \p src over \p index.
|
||||
/// \param[in] src A pointer to an array of bytes.
|
||||
/// \param[in] count The number of bytes to copy from \p src.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
|
||||
/// \pre \p src `!= NULL`.
|
||||
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// src must be a byte array of size `>= count`
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistPutBytes(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
index: usize,
|
||||
insert: bool,
|
||||
src: *const u8,
|
||||
count: usize,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
|
||||
let mut value = Vec::new();
|
||||
value.extend_from_slice(std::slice::from_raw_parts(src, count));
|
||||
to_result(if insert {
|
||||
doc.insert(obj_id, index, value)
|
||||
} else {
|
||||
doc.put(obj_id, index, value)
|
||||
})
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Puts a CRDT counter as the value at an index in a list object.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] index An index in the list object identified by \p obj_id or
|
||||
/// `SIZE_MAX` to indicate its last index if \p insert
|
||||
/// `== false` or one past its last index if \p insert
|
||||
/// `== true`.
|
||||
/// \param[in] insert A flag to insert \p value before \p index instead of
|
||||
/// writing \p value over \p index.
|
||||
/// \param[in] value A 64-bit signed integer.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistPutCounter(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
index: usize,
|
||||
insert: bool,
|
||||
value: i64,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
|
||||
let value = am::ScalarValue::Counter(value.into());
|
||||
to_result(if insert {
|
||||
doc.insert(obj_id, index, value)
|
||||
} else {
|
||||
doc.put(obj_id, index, value)
|
||||
})
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Puts a float as the value at an index in a list object.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] index An index in the list object identified by \p obj_id or
|
||||
/// `SIZE_MAX` to indicate its last index if \p insert
|
||||
/// `== false` or one past its last index if \p insert
|
||||
/// `== true`.
|
||||
/// \param[in] insert A flag to insert \p value before \p index instead of
|
||||
/// writing \p value over \p index.
|
||||
/// \param[in] value A 64-bit float.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistPutF64(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
index: usize,
|
||||
insert: bool,
|
||||
value: f64,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
|
||||
to_result(if insert {
|
||||
doc.insert(obj_id, index, value)
|
||||
} else {
|
||||
doc.put(obj_id, index, value)
|
||||
})
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Puts a signed integer as the value at an index in a list object.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] index An index in the list object identified by \p obj_id or
|
||||
/// `SIZE_MAX` to indicate its last index if \p insert
|
||||
/// `== false` or one past its last index if \p insert
|
||||
/// `== true`.
|
||||
/// \param[in] insert A flag to insert \p value before \p index instead of
|
||||
/// writing \p value over \p index.
|
||||
/// \param[in] value A 64-bit signed integer.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistPutInt(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
index: usize,
|
||||
insert: bool,
|
||||
value: i64,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
|
||||
to_result(if insert {
|
||||
doc.insert(obj_id, index, value)
|
||||
} else {
|
||||
doc.put(obj_id, index, value)
|
||||
})
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Puts null as the value at an index in a list object.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] index An index in the list object identified by \p obj_id or
|
||||
/// `SIZE_MAX` to indicate its last index if \p insert
|
||||
/// `== false` or one past its last index if \p insert
|
||||
/// `== true`.
|
||||
/// \param[in] insert A flag to insert \p value before \p index instead of
|
||||
/// writing \p value over \p index.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistPutNull(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
index: usize,
|
||||
insert: bool,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
|
||||
to_result(if insert {
|
||||
doc.insert(obj_id, index, ())
|
||||
} else {
|
||||
doc.put(obj_id, index, ())
|
||||
})
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Puts an empty object as the value at an index in a list object.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] index An index in the list object identified by \p obj_id or
|
||||
/// `SIZE_MAX` to indicate its last index if \p insert
|
||||
/// `== false` or one past its last index if \p insert
|
||||
/// `== true`.
|
||||
/// \param[in] insert A flag to insert \p value before \p index instead of
|
||||
/// writing \p value over \p index.
|
||||
/// \param[in] obj_type An `AMobjIdType` enum tag.
|
||||
/// \return A pointer to an `AMresult` struct containing a pointer to an
|
||||
/// `AMobjId` struct.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistPutObject(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
index: usize,
|
||||
insert: bool,
|
||||
obj_type: AMobjType,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
|
||||
let object = obj_type.into();
|
||||
to_result(if insert {
|
||||
doc.insert_object(obj_id, index, object)
|
||||
} else {
|
||||
doc.put_object(obj_id, index, object)
|
||||
})
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Puts a UTF-8 string as the value at an index in a list object.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] index An index in the list object identified by \p obj_id or
|
||||
/// `SIZE_MAX` to indicate its last index if \p insert
|
||||
/// `== false` or one past its last index if \p insert
|
||||
/// `== true`.
|
||||
/// \param[in] insert A flag to insert \p value before \p index instead of
|
||||
/// writing \p value over \p index.
|
||||
/// \param[in] value A UTF-8 string.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
|
||||
/// \pre \p value `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// value must be a null-terminated array of `c_char`
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistPutStr(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
index: usize,
|
||||
insert: bool,
|
||||
value: *const c_char,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
|
||||
let value = to_str(value);
|
||||
to_result(if insert {
|
||||
doc.insert(obj_id, index, value)
|
||||
} else {
|
||||
doc.put(obj_id, index, value)
|
||||
})
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Puts a Lamport timestamp as the value at an index in a list object.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] index An index in the list object identified by \p obj_id or
|
||||
/// `SIZE_MAX` to indicate its last index if \p insert
|
||||
/// `== false` or one past its last index if \p insert
|
||||
/// `== true`.
|
||||
/// \param[in] insert A flag to insert \p value before \p index instead of
|
||||
/// writing \p value over \p index.
|
||||
/// \param[in] value A 64-bit signed integer.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistPutTimestamp(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
index: usize,
|
||||
insert: bool,
|
||||
value: i64,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
|
||||
let value = am::ScalarValue::Timestamp(value);
|
||||
to_result(if insert {
|
||||
doc.insert(obj_id, index, value)
|
||||
} else {
|
||||
doc.put(obj_id, index, value)
|
||||
})
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Puts an unsigned integer as the value at an index in a list object.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] index An index in the list object identified by \p obj_id or
|
||||
/// `SIZE_MAX` to indicate its last index if \p insert
|
||||
/// `== false` or one past its last index if \p insert
|
||||
/// `== true`.
|
||||
/// \param[in] insert A flag to insert \p value before \p index instead of
|
||||
/// writing \p value over \p index.
|
||||
/// \param[in] value A 64-bit unsigned integer.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistPutUint(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
index: usize,
|
||||
insert: bool,
|
||||
value: u64,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
|
||||
to_result(if insert {
|
||||
doc.insert(obj_id, index, value)
|
||||
} else {
|
||||
doc.put(obj_id, index, value)
|
||||
})
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Gets the current or historical indices and values of the list object
|
||||
/// within the given range.
|
||||
///
|
||||
/// \param[in] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] begin The first index in a range of indices.
|
||||
/// \param[in] end At least one past the last index in a range of indices.
|
||||
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
|
||||
/// indices and values or `NULL` for current indices and
|
||||
/// values.
|
||||
/// \return A pointer to an `AMresult` struct containing an `AMlistItems`
|
||||
/// struct.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre \p begin `<=` \p end `<= SIZE_MAX`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistRange(
|
||||
doc: *const AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
begin: usize,
|
||||
end: usize,
|
||||
heads: *const AMchangeHashes,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc!(doc);
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
let range = to_range!(begin, end);
|
||||
match heads.as_ref() {
|
||||
None => to_result(doc.list_range(obj_id, range)),
|
||||
Some(heads) => to_result(doc.list_range_at(obj_id, range, heads.as_ref())),
|
||||
}
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
use automerge as am;
|
||||
use std::cell::RefCell;
|
||||
use std::ffi::CString;
|
||||
|
||||
use crate::obj::AMobjId;
|
||||
use crate::result::AMvalue;
|
||||
|
||||
/// \struct AMlistItem
|
||||
/// \installed_headerfile
|
||||
/// \brief An item in a list object.
|
||||
#[repr(C)]
|
||||
pub struct AMlistItem {
|
||||
/// The index of an item in a list object.
|
||||
index: usize,
|
||||
/// The object identifier of an item in a list object.
|
||||
obj_id: AMobjId,
|
||||
/// The value of an item in a list object.
|
||||
value: (am::Value<'static>, RefCell<Option<CString>>),
|
||||
}
|
||||
|
||||
impl AMlistItem {
|
||||
pub fn new(index: usize, value: am::Value<'static>, obj_id: am::ObjId) -> Self {
|
||||
Self {
|
||||
index,
|
||||
obj_id: AMobjId::new(obj_id),
|
||||
value: (value, Default::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for AMlistItem {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.index == other.index && self.obj_id == other.obj_id && self.value.0 == other.value.0
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
impl From<&AMlistItem> for (usize, am::Value<'static>, am::ObjId) {
|
||||
fn from(list_item: &AMlistItem) -> Self {
|
||||
(list_item.index, list_item.value.0.clone(), list_item.obj_id.as_ref().clone())
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/// \memberof AMlistItem
|
||||
/// \brief Gets the index of an item in a list object.
|
||||
///
|
||||
/// \param[in] list_item A pointer to an `AMlistItem` struct.
|
||||
/// \return A 64-bit unsigned integer.
|
||||
/// \pre \p list_item `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// # Safety
|
||||
/// list_item must be a valid pointer to an AMlistItem
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistItemIndex(list_item: *const AMlistItem) -> usize {
|
||||
if let Some(list_item) = list_item.as_ref() {
|
||||
list_item.index
|
||||
} else {
|
||||
usize::MAX
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMlistItem
|
||||
/// \brief Gets the object identifier of an item in a list object.
|
||||
///
|
||||
/// \param[in] list_item A pointer to an `AMlistItem` struct.
|
||||
/// \return A pointer to an `AMobjId` struct.
|
||||
/// \pre \p list_item `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// # Safety
|
||||
/// list_item must be a valid pointer to an AMlistItem
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistItemObjId(list_item: *const AMlistItem) -> *const AMobjId {
|
||||
if let Some(list_item) = list_item.as_ref() {
|
||||
&list_item.obj_id
|
||||
} else {
|
||||
std::ptr::null()
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMlistItem
|
||||
/// \brief Gets the value of an item in a list object.
|
||||
///
|
||||
/// \param[in] list_item A pointer to an `AMlistItem` struct.
|
||||
/// \return An `AMvalue` struct.
|
||||
/// \pre \p list_item `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// # Safety
|
||||
/// list_item must be a valid pointer to an AMlistItem
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistItemValue<'a>(list_item: *const AMlistItem) -> AMvalue<'a> {
|
||||
if let Some(list_item) = list_item.as_ref() {
|
||||
(&list_item.value.0, &list_item.value.1).into()
|
||||
} else {
|
||||
AMvalue::Void
|
||||
}
|
||||
}
|
|
@ -1,348 +0,0 @@
|
|||
use std::ffi::c_void;
|
||||
use std::mem::size_of;
|
||||
|
||||
use crate::doc::list::item::AMlistItem;
|
||||
|
||||
#[repr(C)]
|
||||
struct Detail {
|
||||
len: usize,
|
||||
offset: isize,
|
||||
ptr: *const c_void,
|
||||
}
|
||||
|
||||
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
|
||||
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
|
||||
/// propagate the name of a constant initialized from it so if the
|
||||
/// constant's name is a symbolic representation of the value it can be
|
||||
/// converted into a number by post-processing the header it generated.
|
||||
pub const USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
|
||||
|
||||
impl Detail {
|
||||
fn new(list_items: &[AMlistItem], offset: isize) -> Self {
|
||||
Self {
|
||||
len: list_items.len(),
|
||||
offset,
|
||||
ptr: list_items.as_ptr() as *const c_void,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance(&mut self, n: isize) {
|
||||
if n == 0 {
|
||||
return;
|
||||
}
|
||||
let len = self.len as isize;
|
||||
self.offset = if self.offset < 0 {
|
||||
// It's reversed.
|
||||
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
|
||||
if unclipped >= 0 {
|
||||
// Clip it to the forward stop.
|
||||
len
|
||||
} else {
|
||||
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
|
||||
}
|
||||
} else {
|
||||
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
|
||||
if unclipped < 0 {
|
||||
// Clip it to the reverse stop.
|
||||
-(len + 1)
|
||||
} else {
|
||||
std::cmp::max(0, std::cmp::min(unclipped, len))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_index(&self) -> usize {
|
||||
(self.offset
|
||||
+ if self.offset < 0 {
|
||||
self.len as isize
|
||||
} else {
|
||||
0
|
||||
}) as usize
|
||||
}
|
||||
|
||||
pub fn next(&mut self, n: isize) -> Option<&AMlistItem> {
|
||||
if self.is_stopped() {
|
||||
return None;
|
||||
}
|
||||
let slice: &[AMlistItem] =
|
||||
unsafe { std::slice::from_raw_parts(self.ptr as *const AMlistItem, self.len) };
|
||||
let value = &slice[self.get_index()];
|
||||
self.advance(n);
|
||||
Some(value)
|
||||
}
|
||||
|
||||
pub fn is_stopped(&self) -> bool {
|
||||
let len = self.len as isize;
|
||||
self.offset < -len || self.offset == len
|
||||
}
|
||||
|
||||
pub fn prev(&mut self, n: isize) -> Option<&AMlistItem> {
|
||||
self.advance(-n);
|
||||
if self.is_stopped() {
|
||||
return None;
|
||||
}
|
||||
let slice: &[AMlistItem] =
|
||||
unsafe { std::slice::from_raw_parts(self.ptr as *const AMlistItem, self.len) };
|
||||
Some(&slice[self.get_index()])
|
||||
}
|
||||
|
||||
pub fn reversed(&self) -> Self {
|
||||
Self {
|
||||
len: self.len,
|
||||
offset: -(self.offset + 1),
|
||||
ptr: self.ptr,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rewound(&self) -> Self {
|
||||
Self {
|
||||
len: self.len,
|
||||
offset: if self.offset < 0 { -1 } else { 0 },
|
||||
ptr: self.ptr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Detail> for [u8; USIZE_USIZE_USIZE_] {
|
||||
fn from(detail: Detail) -> Self {
|
||||
unsafe {
|
||||
std::slice::from_raw_parts((&detail as *const Detail) as *const u8, USIZE_USIZE_USIZE_)
|
||||
.try_into()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// \struct AMlistItems
|
||||
/// \installed_headerfile
|
||||
/// \brief A random-access iterator over a sequence of list object items.
|
||||
#[repr(C)]
|
||||
#[derive(Eq, PartialEq)]
|
||||
pub struct AMlistItems {
|
||||
/// An implementation detail that is intentionally opaque.
|
||||
/// \warning Modifying \p detail will cause undefined behavior.
|
||||
/// \note The actual size of \p detail will vary by platform, this is just
|
||||
/// the one for the platform this documentation was built on.
|
||||
detail: [u8; USIZE_USIZE_USIZE_],
|
||||
}
|
||||
|
||||
impl AMlistItems {
|
||||
pub fn new(list_items: &[AMlistItem]) -> Self {
|
||||
Self {
|
||||
detail: Detail::new(list_items, 0).into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance(&mut self, n: isize) {
|
||||
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
|
||||
detail.advance(n);
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
detail.len
|
||||
}
|
||||
|
||||
pub fn next(&mut self, n: isize) -> Option<&AMlistItem> {
|
||||
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
|
||||
detail.next(n)
|
||||
}
|
||||
|
||||
pub fn prev(&mut self, n: isize) -> Option<&AMlistItem> {
|
||||
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
|
||||
detail.prev(n)
|
||||
}
|
||||
|
||||
pub fn reversed(&self) -> Self {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
Self {
|
||||
detail: detail.reversed().into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rewound(&self) -> Self {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
Self {
|
||||
detail: detail.rewound().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[AMlistItem]> for AMlistItems {
|
||||
fn as_ref(&self) -> &[AMlistItem] {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
unsafe { std::slice::from_raw_parts(detail.ptr as *const AMlistItem, detail.len) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AMlistItems {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
detail: [0; USIZE_USIZE_USIZE_],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMlistItems
|
||||
/// \brief Advances an iterator over a sequence of list object items by at most
|
||||
/// \p |n| positions where the sign of \p n is relative to the
|
||||
/// iterator's direction.
|
||||
///
|
||||
/// \param[in,out] list_items A pointer to an `AMlistItems` struct.
|
||||
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
|
||||
/// number of positions to advance.
|
||||
/// \pre \p list_items `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// list_items must be a valid pointer to an AMlistItems
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistItemsAdvance(list_items: *mut AMlistItems, n: isize) {
|
||||
if let Some(list_items) = list_items.as_mut() {
|
||||
list_items.advance(n);
|
||||
};
|
||||
}
|
||||
|
||||
/// \memberof AMlistItems
|
||||
/// \brief Tests the equality of two sequences of list object items underlying
|
||||
/// a pair of iterators.
|
||||
///
|
||||
/// \param[in] list_items1 A pointer to an `AMlistItems` struct.
|
||||
/// \param[in] list_items2 A pointer to an `AMlistItems` struct.
|
||||
/// \return `true` if \p list_items1 `==` \p list_items2 and `false` otherwise.
|
||||
/// \pre \p list_items1 `!= NULL`.
|
||||
/// \pre \p list_items2 `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// list_items1 must be a valid pointer to an AMlistItems
|
||||
/// list_items2 must be a valid pointer to an AMlistItems
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistItemsEqual(
|
||||
list_items1: *const AMlistItems,
|
||||
list_items2: *const AMlistItems,
|
||||
) -> bool {
|
||||
match (list_items1.as_ref(), list_items2.as_ref()) {
|
||||
(Some(list_items1), Some(list_items2)) => list_items1.as_ref() == list_items2.as_ref(),
|
||||
(None, Some(_)) | (Some(_), None) | (None, None) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMlistItems
|
||||
/// \brief Gets the list object item at the current position of an iterator
|
||||
/// over a sequence of list object items and then advances it by at most
|
||||
/// \p |n| positions where the sign of \p n is relative to the
|
||||
/// iterator's direction.
|
||||
///
|
||||
/// \param[in,out] list_items A pointer to an `AMlistItems` struct.
|
||||
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
|
||||
/// number of positions to advance.
|
||||
/// \return A pointer to an `AMlistItem` struct that's `NULL` when
|
||||
/// \p list_items was previously advanced past its forward/reverse
|
||||
/// limit.
|
||||
/// \pre \p list_items `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// list_items must be a valid pointer to an AMlistItems
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistItemsNext(
|
||||
list_items: *mut AMlistItems,
|
||||
n: isize,
|
||||
) -> *const AMlistItem {
|
||||
if let Some(list_items) = list_items.as_mut() {
|
||||
if let Some(list_item) = list_items.next(n) {
|
||||
return list_item;
|
||||
}
|
||||
}
|
||||
std::ptr::null()
|
||||
}
|
||||
|
||||
/// \memberof AMlistItems
|
||||
/// \brief Advances an iterator over a sequence of list object items by at most
|
||||
/// \p |n| positions where the sign of \p n is relative to the
|
||||
/// iterator's direction and then gets the list object item at its new
|
||||
/// position.
|
||||
///
|
||||
/// \param[in,out] list_items A pointer to an `AMlistItems` struct.
|
||||
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
|
||||
/// number of positions to advance.
|
||||
/// \return A pointer to an `AMlistItem` struct that's `NULL` when
|
||||
/// \p list_items is presently advanced past its forward/reverse limit.
|
||||
/// \pre \p list_items `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// list_items must be a valid pointer to an AMlistItems
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistItemsPrev(
|
||||
list_items: *mut AMlistItems,
|
||||
n: isize,
|
||||
) -> *const AMlistItem {
|
||||
if let Some(list_items) = list_items.as_mut() {
|
||||
if let Some(list_item) = list_items.prev(n) {
|
||||
return list_item;
|
||||
}
|
||||
}
|
||||
std::ptr::null()
|
||||
}
|
||||
|
||||
/// \memberof AMlistItems
|
||||
/// \brief Gets the size of the sequence of list object items underlying an
|
||||
/// iterator.
|
||||
///
|
||||
/// \param[in] list_items A pointer to an `AMlistItems` struct.
|
||||
/// \return The count of values in \p list_items.
|
||||
/// \pre \p list_items `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// list_items must be a valid pointer to an AMlistItems
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistItemsSize(list_items: *const AMlistItems) -> usize {
|
||||
if let Some(list_items) = list_items.as_ref() {
|
||||
list_items.len()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMlistItems
|
||||
/// \brief Creates an iterator over the same sequence of list object items as
|
||||
/// the given one but with the opposite position and direction.
|
||||
///
|
||||
/// \param[in] list_items A pointer to an `AMlistItems` struct.
|
||||
/// \return An `AMlistItems` struct
|
||||
/// \pre \p list_items `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// list_items must be a valid pointer to an AMlistItems
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistItemsReversed(list_items: *const AMlistItems) -> AMlistItems {
|
||||
if let Some(list_items) = list_items.as_ref() {
|
||||
list_items.reversed()
|
||||
} else {
|
||||
AMlistItems::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMlistItems
|
||||
/// \brief Creates an iterator at the starting position over the same sequence
|
||||
/// of list object items as the given one.
|
||||
///
|
||||
/// \param[in] list_items A pointer to an `AMlistItems` struct.
|
||||
/// \return An `AMlistItems` struct
|
||||
/// \pre \p list_items `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// list_items must be a valid pointer to an AMlistItems
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMlistItemsRewound(list_items: *const AMlistItems) -> AMlistItems {
|
||||
if let Some(list_items) = list_items.as_ref() {
|
||||
list_items.rewound()
|
||||
} else {
|
||||
AMlistItems::default()
|
||||
}
|
||||
}
|
|
@ -1,506 +0,0 @@
|
|||
use automerge as am;
|
||||
use automerge::transaction::Transactable;
|
||||
use std::os::raw::c_char;
|
||||
|
||||
use crate::change_hashes::AMchangeHashes;
|
||||
use crate::doc::utils::to_str;
|
||||
use crate::doc::{to_doc, to_doc_mut, to_obj_id, AMdoc};
|
||||
use crate::obj::{AMobjId, AMobjType};
|
||||
use crate::result::{to_result, AMresult};
|
||||
|
||||
pub mod item;
|
||||
pub mod items;
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Deletes a key in a map object.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre \p key `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// key must be a c string of the map key to be used
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapDelete(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
key: *const c_char,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
to_result(doc.delete(to_obj_id!(obj_id), to_str(key)))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Gets the current or historical value for a key in a map object.
|
||||
///
|
||||
/// \param[in] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] key A UTF-8 string key for the map object identified by
|
||||
/// \p obj_id.
|
||||
/// \param[in] heads A pointer to an `AMchangeHashes` struct for a historical
|
||||
/// value or `NULL` for the current value.
|
||||
/// \return A pointer to an `AMresult` struct that doesn't contain a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre \p key `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// key must be a c string of the map key to be used
|
||||
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapGet(
|
||||
doc: *const AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
key: *const c_char,
|
||||
heads: *const AMchangeHashes,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc!(doc);
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
match heads.as_ref() {
|
||||
None => to_result(doc.get(obj_id, to_str(key))),
|
||||
Some(heads) => to_result(doc.get_at(obj_id, to_str(key), heads.as_ref())),
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Gets all of the historical values for a key in a map object until
|
||||
/// its current one or a specific one.
|
||||
///
|
||||
/// \param[in] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] key A UTF-8 string key for the map object identified by
|
||||
/// \p obj_id.
|
||||
/// \param[in] heads A pointer to an `AMchangeHashes` struct for a historical
|
||||
/// last value or `NULL` for the current last value.
|
||||
/// \return A pointer to an `AMresult` struct containing an `AMobjItems` struct.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre \p key `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// key must be a c string of the map key to be used
|
||||
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapGetAll(
|
||||
doc: *const AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
key: *const c_char,
|
||||
heads: *const AMchangeHashes,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc!(doc);
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
match heads.as_ref() {
|
||||
None => to_result(doc.get_all(obj_id, to_str(key))),
|
||||
Some(heads) => to_result(doc.get_all_at(obj_id, to_str(key), heads.as_ref())),
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Increments a counter for a key in a map object by the given value.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
|
||||
/// \param[in] value A 64-bit signed integer.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre \p key `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// key must be a c string of the map key to be used
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapIncrement(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
key: *const c_char,
|
||||
value: i64,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
to_result(doc.increment(to_obj_id!(obj_id), to_str(key), value))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Puts a boolean as the value of a key in a map object.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
|
||||
/// \param[in] value A boolean.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre \p key `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// key must be a c string of the map key to be used
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapPutBool(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
key: *const c_char,
|
||||
value: bool,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
to_result(doc.put(to_obj_id!(obj_id), to_str(key), value))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Puts a sequence of bytes as the value of a key in a map object.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
|
||||
/// \param[in] src A pointer to an array of bytes.
|
||||
/// \param[in] count The number of bytes to copy from \p src.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre \p key `!= NULL`.
|
||||
/// \pre \p src `!= NULL`.
|
||||
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// key must be a c string of the map key to be used
|
||||
/// src must be a byte array of size `>= count`
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapPutBytes(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
key: *const c_char,
|
||||
src: *const u8,
|
||||
count: usize,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
let mut vec = Vec::new();
|
||||
vec.extend_from_slice(std::slice::from_raw_parts(src, count));
|
||||
to_result(doc.put(to_obj_id!(obj_id), to_str(key), vec))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Puts a CRDT counter as the value of a key in a map object.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
|
||||
/// \param[in] value A 64-bit signed integer.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre \p key `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// key must be a c string of the map key to be used
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapPutCounter(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
key: *const c_char,
|
||||
value: i64,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
to_result(doc.put(
|
||||
to_obj_id!(obj_id),
|
||||
to_str(key),
|
||||
am::ScalarValue::Counter(value.into()),
|
||||
))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Puts null as the value of a key in a map object.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre \p key `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// key must be a c string of the map key to be used
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapPutNull(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
key: *const c_char,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
to_result(doc.put(to_obj_id!(obj_id), to_str(key), ()))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Puts an empty object as the value of a key in a map object.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
|
||||
/// \param[in] obj_type An `AMobjIdType` enum tag.
|
||||
/// \return A pointer to an `AMresult` struct containing a pointer to an
|
||||
/// `AMobjId` struct.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre \p key `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// key must be a c string of the map key to be used
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapPutObject(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
key: *const c_char,
|
||||
obj_type: AMobjType,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
to_result(doc.put_object(to_obj_id!(obj_id), to_str(key), obj_type.into()))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Puts a float as the value of a key in a map object.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
|
||||
/// \param[in] value A 64-bit float.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre \p key `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// key must be a c string of the map key to be used
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapPutF64(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
key: *const c_char,
|
||||
value: f64,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
to_result(doc.put(to_obj_id!(obj_id), to_str(key), value))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Puts a signed integer as the value of a key in a map object.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
|
||||
/// \param[in] value A 64-bit signed integer.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre \p key `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// key must be a c string of the map key to be used
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapPutInt(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
key: *const c_char,
|
||||
value: i64,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
to_result(doc.put(to_obj_id!(obj_id), to_str(key), value))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Puts a UTF-8 string as the value of a key in a map object.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
|
||||
/// \param[in] value A UTF-8 string.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre \p key `!= NULL`.
|
||||
/// \pre \p value `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// key must be a c string of the map key to be used
|
||||
/// value must be a null-terminated array of `c_char`
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapPutStr(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
key: *const c_char,
|
||||
value: *const c_char,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
to_result(doc.put(to_obj_id!(obj_id), to_str(key), to_str(value)))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Puts a Lamport timestamp as the value of a key in a map object.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
|
||||
/// \param[in] value A 64-bit signed integer.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre \p key `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// key must be a c string of the map key to be used
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapPutTimestamp(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
key: *const c_char,
|
||||
value: i64,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
to_result(doc.put(
|
||||
to_obj_id!(obj_id),
|
||||
to_str(key),
|
||||
am::ScalarValue::Timestamp(value),
|
||||
))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Puts an unsigned integer as the value of a key in a map object.
|
||||
///
|
||||
/// \param[in,out] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
|
||||
/// \param[in] value A 64-bit unsigned integer.
|
||||
/// \return A pointer to an `AMresult` struct containing a void.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre \p key `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// key must be a c string of the map key to be used
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapPutUint(
|
||||
doc: *mut AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
key: *const c_char,
|
||||
value: u64,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc_mut!(doc);
|
||||
to_result(doc.put(to_obj_id!(obj_id), to_str(key), value))
|
||||
}
|
||||
|
||||
/// \memberof AMdoc
|
||||
/// \brief Gets the current or historical keys and values of the map object
|
||||
/// within the given range.
|
||||
///
|
||||
/// \param[in] doc A pointer to an `AMdoc` struct.
|
||||
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
|
||||
/// \param[in] begin The first key in a subrange or `NULL` to indicate the
|
||||
/// absolute first key.
|
||||
/// \param[in] end The key one past the last key in a subrange or `NULL` to
|
||||
/// indicate one past the absolute last key.
|
||||
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
|
||||
/// keys and values or `NULL` for current keys and values.
|
||||
/// \return A pointer to an `AMresult` struct containing an `AMmapItems`
|
||||
/// struct.
|
||||
/// \pre \p doc `!= NULL`.
|
||||
/// \pre `strcmp(`\p begin, \p end`) != 1` if \p begin `!= NULL` and \p end `!= NULL`.
|
||||
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
|
||||
/// in order to prevent a memory leak.
|
||||
/// \internal
|
||||
/// # Safety
|
||||
/// doc must be a valid pointer to an AMdoc
|
||||
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
|
||||
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapRange(
|
||||
doc: *const AMdoc,
|
||||
obj_id: *const AMobjId,
|
||||
begin: *const c_char,
|
||||
end: *const c_char,
|
||||
heads: *const AMchangeHashes,
|
||||
) -> *mut AMresult {
|
||||
let doc = to_doc!(doc);
|
||||
let obj_id = to_obj_id!(obj_id);
|
||||
match (begin.as_ref(), end.as_ref()) {
|
||||
(Some(_), Some(_)) => {
|
||||
let (begin, end) = (to_str(begin), to_str(end));
|
||||
if begin > end {
|
||||
return AMresult::err(&format!("Invalid range [{}-{})", begin, end)).into();
|
||||
};
|
||||
let bounds = begin..end;
|
||||
if let Some(heads) = heads.as_ref() {
|
||||
to_result(doc.map_range_at(obj_id, bounds, heads.as_ref()))
|
||||
} else {
|
||||
to_result(doc.map_range(obj_id, bounds))
|
||||
}
|
||||
}
|
||||
(Some(_), None) => {
|
||||
let bounds = to_str(begin)..;
|
||||
if let Some(heads) = heads.as_ref() {
|
||||
to_result(doc.map_range_at(obj_id, bounds, heads.as_ref()))
|
||||
} else {
|
||||
to_result(doc.map_range(obj_id, bounds))
|
||||
}
|
||||
}
|
||||
(None, Some(_)) => {
|
||||
let bounds = ..to_str(end);
|
||||
if let Some(heads) = heads.as_ref() {
|
||||
to_result(doc.map_range_at(obj_id, bounds, heads.as_ref()))
|
||||
} else {
|
||||
to_result(doc.map_range(obj_id, bounds))
|
||||
}
|
||||
}
|
||||
(None, None) => {
|
||||
let bounds = ..;
|
||||
if let Some(heads) = heads.as_ref() {
|
||||
to_result(doc.map_range_at(obj_id, bounds, heads.as_ref()))
|
||||
} else {
|
||||
to_result(doc.map_range(obj_id, bounds))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
use automerge as am;
|
||||
use std::cell::RefCell;
|
||||
use std::ffi::CString;
|
||||
use std::os::raw::c_char;
|
||||
|
||||
use crate::obj::AMobjId;
|
||||
use crate::result::AMvalue;
|
||||
|
||||
/// \struct AMmapItem
|
||||
/// \installed_headerfile
|
||||
/// \brief An item in a map object.
|
||||
#[repr(C)]
|
||||
pub struct AMmapItem {
|
||||
/// The key of an item in a map object.
|
||||
key: CString,
|
||||
/// The object identifier of an item in a map object.
|
||||
obj_id: AMobjId,
|
||||
/// The value of an item in a map object.
|
||||
value: (am::Value<'static>, RefCell<Option<CString>>),
|
||||
}
|
||||
|
||||
impl AMmapItem {
|
||||
pub fn new(key: &'static str, value: am::Value<'static>, obj_id: am::ObjId) -> Self {
|
||||
Self {
|
||||
key: CString::new(key).unwrap(),
|
||||
obj_id: AMobjId::new(obj_id),
|
||||
value: (value, Default::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for AMmapItem {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.key == other.key && self.obj_id == other.obj_id && self.value.0 == other.value.0
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
impl From<&AMmapItem> for (String, am::Value<'static>, am::ObjId) {
|
||||
fn from(map_item: &AMmapItem) -> Self {
|
||||
(map_item.key.into_string().unwrap(), map_item.value.0.clone(), map_item.obj_id.as_ref().clone())
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/// \memberof AMmapItem
|
||||
/// \brief Gets the key of an item in a map object.
|
||||
///
|
||||
/// \param[in] map_item A pointer to an `AMmapItem` struct.
|
||||
/// \return A 64-bit unsigned integer.
|
||||
/// \pre \p map_item `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// # Safety
|
||||
/// map_item must be a valid pointer to an AMmapItem
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapItemKey(map_item: *const AMmapItem) -> *const c_char {
|
||||
if let Some(map_item) = map_item.as_ref() {
|
||||
map_item.key.as_ptr()
|
||||
} else {
|
||||
std::ptr::null()
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMmapItem
|
||||
/// \brief Gets the object identifier of an item in a map object.
|
||||
///
|
||||
/// \param[in] map_item A pointer to an `AMmapItem` struct.
|
||||
/// \return A pointer to an `AMobjId` struct.
|
||||
/// \pre \p map_item `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// # Safety
|
||||
/// map_item must be a valid pointer to an AMmapItem
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapItemObjId(map_item: *const AMmapItem) -> *const AMobjId {
|
||||
if let Some(map_item) = map_item.as_ref() {
|
||||
&map_item.obj_id
|
||||
} else {
|
||||
std::ptr::null()
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMmapItem
|
||||
/// \brief Gets the value of an item in a map object.
|
||||
///
|
||||
/// \param[in] map_item A pointer to an `AMmapItem` struct.
|
||||
/// \return An `AMvalue` struct.
|
||||
/// \pre \p map_item `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// # Safety
|
||||
/// map_item must be a valid pointer to an AMmapItem
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapItemValue<'a>(map_item: *const AMmapItem) -> AMvalue<'a> {
|
||||
if let Some(map_item) = map_item.as_ref() {
|
||||
(&map_item.value.0, &map_item.value.1).into()
|
||||
} else {
|
||||
AMvalue::Void
|
||||
}
|
||||
}
|
|
@ -1,340 +0,0 @@
|
|||
use std::ffi::c_void;
|
||||
use std::mem::size_of;
|
||||
|
||||
use crate::doc::map::item::AMmapItem;
|
||||
|
||||
#[repr(C)]
|
||||
struct Detail {
|
||||
len: usize,
|
||||
offset: isize,
|
||||
ptr: *const c_void,
|
||||
}
|
||||
|
||||
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
|
||||
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
|
||||
/// propagate the name of a constant initialized from it so if the
|
||||
/// constant's name is a symbolic representation of the value it can be
|
||||
/// converted into a number by post-processing the header it generated.
|
||||
pub const USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
|
||||
|
||||
impl Detail {
|
||||
fn new(map_items: &[AMmapItem], offset: isize) -> Self {
|
||||
Self {
|
||||
len: map_items.len(),
|
||||
offset,
|
||||
ptr: map_items.as_ptr() as *const c_void,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance(&mut self, n: isize) {
|
||||
if n == 0 {
|
||||
return;
|
||||
}
|
||||
let len = self.len as isize;
|
||||
self.offset = if self.offset < 0 {
|
||||
// It's reversed.
|
||||
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
|
||||
if unclipped >= 0 {
|
||||
// Clip it to the forward stop.
|
||||
len
|
||||
} else {
|
||||
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
|
||||
}
|
||||
} else {
|
||||
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
|
||||
if unclipped < 0 {
|
||||
// Clip it to the reverse stop.
|
||||
-(len + 1)
|
||||
} else {
|
||||
std::cmp::max(0, std::cmp::min(unclipped, len))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_index(&self) -> usize {
|
||||
(self.offset
|
||||
+ if self.offset < 0 {
|
||||
self.len as isize
|
||||
} else {
|
||||
0
|
||||
}) as usize
|
||||
}
|
||||
|
||||
pub fn next(&mut self, n: isize) -> Option<&AMmapItem> {
|
||||
if self.is_stopped() {
|
||||
return None;
|
||||
}
|
||||
let slice: &[AMmapItem] =
|
||||
unsafe { std::slice::from_raw_parts(self.ptr as *const AMmapItem, self.len) };
|
||||
let value = &slice[self.get_index()];
|
||||
self.advance(n);
|
||||
Some(value)
|
||||
}
|
||||
|
||||
pub fn is_stopped(&self) -> bool {
|
||||
let len = self.len as isize;
|
||||
self.offset < -len || self.offset == len
|
||||
}
|
||||
|
||||
pub fn prev(&mut self, n: isize) -> Option<&AMmapItem> {
|
||||
self.advance(-n);
|
||||
if self.is_stopped() {
|
||||
return None;
|
||||
}
|
||||
let slice: &[AMmapItem] =
|
||||
unsafe { std::slice::from_raw_parts(self.ptr as *const AMmapItem, self.len) };
|
||||
Some(&slice[self.get_index()])
|
||||
}
|
||||
|
||||
pub fn reversed(&self) -> Self {
|
||||
Self {
|
||||
len: self.len,
|
||||
offset: -(self.offset + 1),
|
||||
ptr: self.ptr,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rewound(&self) -> Self {
|
||||
Self {
|
||||
len: self.len,
|
||||
offset: if self.offset < 0 { -1 } else { 0 },
|
||||
ptr: self.ptr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Detail> for [u8; USIZE_USIZE_USIZE_] {
|
||||
fn from(detail: Detail) -> Self {
|
||||
unsafe {
|
||||
std::slice::from_raw_parts((&detail as *const Detail) as *const u8, USIZE_USIZE_USIZE_)
|
||||
.try_into()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// \struct AMmapItems
|
||||
/// \installed_headerfile
|
||||
/// \brief A random-access iterator over a sequence of map object items.
|
||||
#[repr(C)]
|
||||
#[derive(Eq, PartialEq)]
|
||||
pub struct AMmapItems {
|
||||
/// An implementation detail that is intentionally opaque.
|
||||
/// \warning Modifying \p detail will cause undefined behavior.
|
||||
/// \note The actual size of \p detail will vary by platform, this is just
|
||||
/// the one for the platform this documentation was built on.
|
||||
detail: [u8; USIZE_USIZE_USIZE_],
|
||||
}
|
||||
|
||||
impl AMmapItems {
|
||||
pub fn new(map_items: &[AMmapItem]) -> Self {
|
||||
Self {
|
||||
detail: Detail::new(map_items, 0).into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance(&mut self, n: isize) {
|
||||
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
|
||||
detail.advance(n);
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
detail.len
|
||||
}
|
||||
|
||||
pub fn next(&mut self, n: isize) -> Option<&AMmapItem> {
|
||||
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
|
||||
detail.next(n)
|
||||
}
|
||||
|
||||
pub fn prev(&mut self, n: isize) -> Option<&AMmapItem> {
|
||||
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
|
||||
detail.prev(n)
|
||||
}
|
||||
|
||||
pub fn reversed(&self) -> Self {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
Self {
|
||||
detail: detail.reversed().into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rewound(&self) -> Self {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
Self {
|
||||
detail: detail.rewound().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[AMmapItem]> for AMmapItems {
|
||||
fn as_ref(&self) -> &[AMmapItem] {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
unsafe { std::slice::from_raw_parts(detail.ptr as *const AMmapItem, detail.len) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AMmapItems {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
detail: [0; USIZE_USIZE_USIZE_],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMmapItems
|
||||
/// \brief Advances an iterator over a sequence of map object items by at most
|
||||
/// \p |n| positions where the sign of \p n is relative to the
|
||||
/// iterator's direction.
|
||||
///
|
||||
/// \param[in,out] map_items A pointer to an `AMmapItems` struct.
|
||||
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
|
||||
/// number of positions to advance.
|
||||
/// \pre \p map_items `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// map_items must be a valid pointer to an AMmapItems
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapItemsAdvance(map_items: *mut AMmapItems, n: isize) {
|
||||
if let Some(map_items) = map_items.as_mut() {
|
||||
map_items.advance(n);
|
||||
};
|
||||
}
|
||||
|
||||
/// \memberof AMmapItems
|
||||
/// \brief Tests the equality of two sequences of map object items underlying
|
||||
/// a pair of iterators.
|
||||
///
|
||||
/// \param[in] map_items1 A pointer to an `AMmapItems` struct.
|
||||
/// \param[in] map_items2 A pointer to an `AMmapItems` struct.
|
||||
/// \return `true` if \p map_items1 `==` \p map_items2 and `false` otherwise.
|
||||
/// \pre \p map_items1 `!= NULL`.
|
||||
/// \pre \p map_items2 `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// map_items1 must be a valid pointer to an AMmapItems
|
||||
/// map_items2 must be a valid pointer to an AMmapItems
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapItemsEqual(
|
||||
map_items1: *const AMmapItems,
|
||||
map_items2: *const AMmapItems,
|
||||
) -> bool {
|
||||
match (map_items1.as_ref(), map_items2.as_ref()) {
|
||||
(Some(map_items1), Some(map_items2)) => map_items1.as_ref() == map_items2.as_ref(),
|
||||
(None, Some(_)) | (Some(_), None) | (None, None) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMmapItems
|
||||
/// \brief Gets the map object item at the current position of an iterator
|
||||
/// over a sequence of map object items and then advances it by at most
|
||||
/// \p |n| positions where the sign of \p n is relative to the
|
||||
/// iterator's direction.
|
||||
///
|
||||
/// \param[in,out] map_items A pointer to an `AMmapItems` struct.
|
||||
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
|
||||
/// number of positions to advance.
|
||||
/// \return A pointer to an `AMmapItem` struct that's `NULL` when \p map_items
|
||||
/// was previously advanced past its forward/reverse limit.
|
||||
/// \pre \p map_items `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// map_items must be a valid pointer to an AMmapItems
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapItemsNext(map_items: *mut AMmapItems, n: isize) -> *const AMmapItem {
|
||||
if let Some(map_items) = map_items.as_mut() {
|
||||
if let Some(map_item) = map_items.next(n) {
|
||||
return map_item;
|
||||
}
|
||||
}
|
||||
std::ptr::null()
|
||||
}
|
||||
|
||||
/// \memberof AMmapItems
|
||||
/// \brief Advances an iterator over a sequence of map object items by at most
|
||||
/// \p |n| positions where the sign of \p n is relative to the
|
||||
/// iterator's direction and then gets the map object item at its new
|
||||
/// position.
|
||||
///
|
||||
/// \param[in,out] map_items A pointer to an `AMmapItems` struct.
|
||||
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
|
||||
/// number of positions to advance.
|
||||
/// \return A pointer to an `AMmapItem` struct that's `NULL` when \p map_items
|
||||
/// is presently advanced past its forward/reverse limit.
|
||||
/// \pre \p map_items `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// map_items must be a valid pointer to an AMmapItems
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapItemsPrev(map_items: *mut AMmapItems, n: isize) -> *const AMmapItem {
|
||||
if let Some(map_items) = map_items.as_mut() {
|
||||
if let Some(map_item) = map_items.prev(n) {
|
||||
return map_item;
|
||||
}
|
||||
}
|
||||
std::ptr::null()
|
||||
}
|
||||
|
||||
/// \memberof AMmapItems
|
||||
/// \brief Gets the size of the sequence of map object items underlying an
|
||||
/// iterator.
|
||||
///
|
||||
/// \param[in] map_items A pointer to an `AMmapItems` struct.
|
||||
/// \return The count of values in \p map_items.
|
||||
/// \pre \p map_items `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// map_items must be a valid pointer to an AMmapItems
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapItemsSize(map_items: *const AMmapItems) -> usize {
|
||||
if let Some(map_items) = map_items.as_ref() {
|
||||
map_items.len()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMmapItems
|
||||
/// \brief Creates an iterator over the same sequence of map object items as
|
||||
/// the given one but with the opposite position and direction.
|
||||
///
|
||||
/// \param[in] map_items A pointer to an `AMmapItems` struct.
|
||||
/// \return An `AMmapItems` struct
|
||||
/// \pre \p map_items `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// map_items must be a valid pointer to an AMmapItems
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapItemsReversed(map_items: *const AMmapItems) -> AMmapItems {
|
||||
if let Some(map_items) = map_items.as_ref() {
|
||||
map_items.reversed()
|
||||
} else {
|
||||
AMmapItems::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMmapItems
|
||||
/// \brief Creates an iterator at the starting position over the same sequence of map object items as the given one.
|
||||
///
|
||||
/// \param[in] map_items A pointer to an `AMmapItems` struct.
|
||||
/// \return An `AMmapItems` struct
|
||||
/// \pre \p map_items `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// map_items must be a valid pointer to an AMmapItems
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMmapItemsRewound(map_items: *const AMmapItems) -> AMmapItems {
|
||||
if let Some(map_items) = map_items.as_ref() {
|
||||
map_items.rewound()
|
||||
} else {
|
||||
AMmapItems::default()
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
use std::ffi::CStr;
|
||||
use std::os::raw::c_char;
|
||||
|
||||
macro_rules! to_actor_id {
|
||||
($handle:expr) => {{
|
||||
let handle = $handle.as_ref();
|
||||
match handle {
|
||||
Some(b) => b,
|
||||
None => return AMresult::err("Invalid AMactorId pointer").into(),
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
pub(crate) use to_actor_id;
|
||||
|
||||
macro_rules! to_doc {
|
||||
($handle:expr) => {{
|
||||
let handle = $handle.as_ref();
|
||||
match handle {
|
||||
Some(b) => b,
|
||||
None => return AMresult::err("Invalid AMdoc pointer").into(),
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
pub(crate) use to_doc;
|
||||
|
||||
macro_rules! to_doc_mut {
|
||||
($handle:expr) => {{
|
||||
let handle = $handle.as_mut();
|
||||
match handle {
|
||||
Some(b) => b,
|
||||
None => return AMresult::err("Invalid AMdoc pointer").into(),
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
pub(crate) use to_doc_mut;
|
||||
|
||||
macro_rules! to_obj_id {
|
||||
($handle:expr) => {{
|
||||
match $handle.as_ref() {
|
||||
Some(obj_id) => obj_id,
|
||||
None => &automerge::ROOT,
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
pub(crate) use to_obj_id;
|
||||
|
||||
pub(crate) unsafe fn to_str(c: *const c_char) -> String {
|
||||
if !c.is_null() {
|
||||
CStr::from_ptr(c).to_string_lossy().to_string()
|
||||
} else {
|
||||
String::default()
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
mod actor_id;
|
||||
mod byte_span;
|
||||
mod change;
|
||||
mod change_hashes;
|
||||
mod changes;
|
||||
mod doc;
|
||||
mod obj;
|
||||
mod result;
|
||||
mod result_stack;
|
||||
mod strs;
|
||||
mod sync;
|
|
@ -1,76 +0,0 @@
|
|||
use automerge as am;
|
||||
use std::cell::RefCell;
|
||||
use std::ffi::CString;
|
||||
|
||||
use crate::obj::AMobjId;
|
||||
use crate::result::AMvalue;
|
||||
|
||||
/// \struct AMobjItem
|
||||
/// \installed_headerfile
|
||||
/// \brief An item in an object.
|
||||
#[repr(C)]
|
||||
pub struct AMobjItem {
|
||||
/// The object identifier of an item in an object.
|
||||
obj_id: AMobjId,
|
||||
/// The value of an item in an object.
|
||||
value: (am::Value<'static>, RefCell<Option<CString>>),
|
||||
}
|
||||
|
||||
impl AMobjItem {
|
||||
pub fn new(value: am::Value<'static>, obj_id: am::ObjId) -> Self {
|
||||
Self {
|
||||
obj_id: AMobjId::new(obj_id),
|
||||
value: (value, Default::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for AMobjItem {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.obj_id == other.obj_id && self.value.0 == other.value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&AMobjItem> for (am::Value<'static>, am::ObjId) {
|
||||
fn from(obj_item: &AMobjItem) -> Self {
|
||||
(obj_item.value.0.clone(), obj_item.obj_id.as_ref().clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMobjItem
|
||||
/// \brief Gets the object identifier of an item in an object.
|
||||
///
|
||||
/// \param[in] obj_item A pointer to an `AMobjItem` struct.
|
||||
/// \return A pointer to an `AMobjId` struct.
|
||||
/// \pre \p obj_item `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// # Safety
|
||||
/// obj_item must be a valid pointer to an AMobjItem
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMobjItemObjId(obj_item: *const AMobjItem) -> *const AMobjId {
|
||||
if let Some(obj_item) = obj_item.as_ref() {
|
||||
&obj_item.obj_id
|
||||
} else {
|
||||
std::ptr::null()
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMobjItem
|
||||
/// \brief Gets the value of an item in an object.
|
||||
///
|
||||
/// \param[in] obj_item A pointer to an `AMobjItem` struct.
|
||||
/// \return An `AMvalue` struct.
|
||||
/// \pre \p obj_item `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// # Safety
|
||||
/// obj_item must be a valid pointer to an AMobjItem
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMobjItemValue<'a>(obj_item: *const AMobjItem) -> AMvalue<'a> {
|
||||
if let Some(obj_item) = obj_item.as_ref() {
|
||||
(&obj_item.value.0, &obj_item.value.1).into()
|
||||
} else {
|
||||
AMvalue::Void
|
||||
}
|
||||
}
|
|
@ -1,341 +0,0 @@
|
|||
use std::ffi::c_void;
|
||||
use std::mem::size_of;
|
||||
|
||||
use crate::obj::item::AMobjItem;
|
||||
|
||||
#[repr(C)]
|
||||
struct Detail {
|
||||
len: usize,
|
||||
offset: isize,
|
||||
ptr: *const c_void,
|
||||
}
|
||||
|
||||
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
|
||||
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
|
||||
/// propagate the name of a constant initialized from it so if the
|
||||
/// constant's name is a symbolic representation of the value it can be
|
||||
/// converted into a number by post-processing the header it generated.
|
||||
pub const USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
|
||||
|
||||
impl Detail {
|
||||
fn new(obj_items: &[AMobjItem], offset: isize) -> Self {
|
||||
Self {
|
||||
len: obj_items.len(),
|
||||
offset,
|
||||
ptr: obj_items.as_ptr() as *const c_void,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance(&mut self, n: isize) {
|
||||
if n == 0 {
|
||||
return;
|
||||
}
|
||||
let len = self.len as isize;
|
||||
self.offset = if self.offset < 0 {
|
||||
// It's reversed.
|
||||
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
|
||||
if unclipped >= 0 {
|
||||
// Clip it to the forward stop.
|
||||
len
|
||||
} else {
|
||||
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
|
||||
}
|
||||
} else {
|
||||
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
|
||||
if unclipped < 0 {
|
||||
// Clip it to the reverse stop.
|
||||
-(len + 1)
|
||||
} else {
|
||||
std::cmp::max(0, std::cmp::min(unclipped, len))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_index(&self) -> usize {
|
||||
(self.offset
|
||||
+ if self.offset < 0 {
|
||||
self.len as isize
|
||||
} else {
|
||||
0
|
||||
}) as usize
|
||||
}
|
||||
|
||||
pub fn next(&mut self, n: isize) -> Option<&AMobjItem> {
|
||||
if self.is_stopped() {
|
||||
return None;
|
||||
}
|
||||
let slice: &[AMobjItem] =
|
||||
unsafe { std::slice::from_raw_parts(self.ptr as *const AMobjItem, self.len) };
|
||||
let value = &slice[self.get_index()];
|
||||
self.advance(n);
|
||||
Some(value)
|
||||
}
|
||||
|
||||
pub fn is_stopped(&self) -> bool {
|
||||
let len = self.len as isize;
|
||||
self.offset < -len || self.offset == len
|
||||
}
|
||||
|
||||
pub fn prev(&mut self, n: isize) -> Option<&AMobjItem> {
|
||||
self.advance(-n);
|
||||
if self.is_stopped() {
|
||||
return None;
|
||||
}
|
||||
let slice: &[AMobjItem] =
|
||||
unsafe { std::slice::from_raw_parts(self.ptr as *const AMobjItem, self.len) };
|
||||
Some(&slice[self.get_index()])
|
||||
}
|
||||
|
||||
pub fn reversed(&self) -> Self {
|
||||
Self {
|
||||
len: self.len,
|
||||
offset: -(self.offset + 1),
|
||||
ptr: self.ptr,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rewound(&self) -> Self {
|
||||
Self {
|
||||
len: self.len,
|
||||
offset: if self.offset < 0 { -1 } else { 0 },
|
||||
ptr: self.ptr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Detail> for [u8; USIZE_USIZE_USIZE_] {
|
||||
fn from(detail: Detail) -> Self {
|
||||
unsafe {
|
||||
std::slice::from_raw_parts((&detail as *const Detail) as *const u8, USIZE_USIZE_USIZE_)
|
||||
.try_into()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// \struct AMobjItems
|
||||
/// \installed_headerfile
|
||||
/// \brief A random-access iterator over a sequence of object items.
|
||||
#[repr(C)]
|
||||
#[derive(Eq, PartialEq)]
|
||||
pub struct AMobjItems {
|
||||
/// An implementation detail that is intentionally opaque.
|
||||
/// \warning Modifying \p detail will cause undefined behavior.
|
||||
/// \note The actual size of \p detail will vary by platform, this is just
|
||||
/// the one for the platform this documentation was built on.
|
||||
detail: [u8; USIZE_USIZE_USIZE_],
|
||||
}
|
||||
|
||||
impl AMobjItems {
|
||||
pub fn new(obj_items: &[AMobjItem]) -> Self {
|
||||
Self {
|
||||
detail: Detail::new(obj_items, 0).into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance(&mut self, n: isize) {
|
||||
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
|
||||
detail.advance(n);
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
detail.len
|
||||
}
|
||||
|
||||
pub fn next(&mut self, n: isize) -> Option<&AMobjItem> {
|
||||
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
|
||||
detail.next(n)
|
||||
}
|
||||
|
||||
pub fn prev(&mut self, n: isize) -> Option<&AMobjItem> {
|
||||
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
|
||||
detail.prev(n)
|
||||
}
|
||||
|
||||
pub fn reversed(&self) -> Self {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
Self {
|
||||
detail: detail.reversed().into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rewound(&self) -> Self {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
Self {
|
||||
detail: detail.rewound().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[AMobjItem]> for AMobjItems {
|
||||
fn as_ref(&self) -> &[AMobjItem] {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
unsafe { std::slice::from_raw_parts(detail.ptr as *const AMobjItem, detail.len) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AMobjItems {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
detail: [0; USIZE_USIZE_USIZE_],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMobjItems
|
||||
/// \brief Advances an iterator over a sequence of object items by at most
|
||||
/// \p |n| positions where the sign of \p n is relative to the
|
||||
/// iterator's direction.
|
||||
///
|
||||
/// \param[in,out] obj_items A pointer to an `AMobjItems` struct.
|
||||
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
|
||||
/// number of positions to advance.
|
||||
/// \pre \p obj_items `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// obj_items must be a valid pointer to an AMobjItems
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMobjItemsAdvance(obj_items: *mut AMobjItems, n: isize) {
|
||||
if let Some(obj_items) = obj_items.as_mut() {
|
||||
obj_items.advance(n);
|
||||
};
|
||||
}
|
||||
|
||||
/// \memberof AMobjItems
|
||||
/// \brief Tests the equality of two sequences of object items underlying a
|
||||
/// pair of iterators.
|
||||
///
|
||||
/// \param[in] obj_items1 A pointer to an `AMobjItems` struct.
|
||||
/// \param[in] obj_items2 A pointer to an `AMobjItems` struct.
|
||||
/// \return `true` if \p obj_items1 `==` \p obj_items2 and `false` otherwise.
|
||||
/// \pre \p obj_items1 `!= NULL`.
|
||||
/// \pre \p obj_items2 `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// obj_items1 must be a valid pointer to an AMobjItems
|
||||
/// obj_items2 must be a valid pointer to an AMobjItems
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMobjItemsEqual(
|
||||
obj_items1: *const AMobjItems,
|
||||
obj_items2: *const AMobjItems,
|
||||
) -> bool {
|
||||
match (obj_items1.as_ref(), obj_items2.as_ref()) {
|
||||
(Some(obj_items1), Some(obj_items2)) => obj_items1.as_ref() == obj_items2.as_ref(),
|
||||
(None, Some(_)) | (Some(_), None) | (None, None) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMobjItems
|
||||
/// \brief Gets the object item at the current position of an iterator over a
|
||||
/// sequence of object items and then advances it by at most \p |n|
|
||||
/// positions where the sign of \p n is relative to the iterator's
|
||||
/// direction.
|
||||
///
|
||||
/// \param[in,out] obj_items A pointer to an `AMobjItems` struct.
|
||||
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
|
||||
/// number of positions to advance.
|
||||
/// \return A pointer to an `AMobjItem` struct that's `NULL` when \p obj_items
|
||||
/// was previously advanced past its forward/reverse limit.
|
||||
/// \pre \p obj_items `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// obj_items must be a valid pointer to an AMobjItems
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMobjItemsNext(obj_items: *mut AMobjItems, n: isize) -> *const AMobjItem {
|
||||
if let Some(obj_items) = obj_items.as_mut() {
|
||||
if let Some(obj_item) = obj_items.next(n) {
|
||||
return obj_item;
|
||||
}
|
||||
}
|
||||
std::ptr::null()
|
||||
}
|
||||
|
||||
/// \memberof AMobjItems
|
||||
/// \brief Advances an iterator over a sequence of object items by at most
|
||||
/// \p |n| positions where the sign of \p n is relative to the
|
||||
/// iterator's direction and then gets the object item at its new
|
||||
/// position.
|
||||
///
|
||||
/// \param[in,out] obj_items A pointer to an `AMobjItems` struct.
|
||||
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
|
||||
/// number of positions to advance.
|
||||
/// \return A pointer to an `AMobjItem` struct that's `NULL` when \p obj_items
|
||||
/// is presently advanced past its forward/reverse limit.
|
||||
/// \pre \p obj_items `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// obj_items must be a valid pointer to an AMobjItems
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMobjItemsPrev(obj_items: *mut AMobjItems, n: isize) -> *const AMobjItem {
|
||||
if let Some(obj_items) = obj_items.as_mut() {
|
||||
if let Some(obj_item) = obj_items.prev(n) {
|
||||
return obj_item;
|
||||
}
|
||||
}
|
||||
std::ptr::null()
|
||||
}
|
||||
|
||||
/// \memberof AMobjItems
|
||||
/// \brief Gets the size of the sequence of object items underlying an
|
||||
/// iterator.
|
||||
///
|
||||
/// \param[in] obj_items A pointer to an `AMobjItems` struct.
|
||||
/// \return The count of values in \p obj_items.
|
||||
/// \pre \p obj_items `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// obj_items must be a valid pointer to an AMobjItems
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMobjItemsSize(obj_items: *const AMobjItems) -> usize {
|
||||
if let Some(obj_items) = obj_items.as_ref() {
|
||||
obj_items.len()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMobjItems
|
||||
/// \brief Creates an iterator over the same sequence of object items as the
|
||||
/// given one but with the opposite position and direction.
|
||||
///
|
||||
/// \param[in] obj_items A pointer to an `AMobjItems` struct.
|
||||
/// \return An `AMobjItems` struct
|
||||
/// \pre \p obj_items `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// obj_items must be a valid pointer to an AMobjItems
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMobjItemsReversed(obj_items: *const AMobjItems) -> AMobjItems {
|
||||
if let Some(obj_items) = obj_items.as_ref() {
|
||||
obj_items.reversed()
|
||||
} else {
|
||||
AMobjItems::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMobjItems
|
||||
/// \brief Creates an iterator at the starting position over the same sequence
|
||||
/// of object items as the given one.
|
||||
///
|
||||
/// \param[in] obj_items A pointer to an `AMobjItems` struct.
|
||||
/// \return An `AMobjItems` struct
|
||||
/// \pre \p obj_items `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// obj_items must be a valid pointer to an AMobjItems
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMobjItemsRewound(obj_items: *const AMobjItems) -> AMobjItems {
|
||||
if let Some(obj_items) = obj_items.as_ref() {
|
||||
obj_items.rewound()
|
||||
} else {
|
||||
AMobjItems::default()
|
||||
}
|
||||
}
|
|
@ -1,914 +0,0 @@
|
|||
use automerge as am;
|
||||
use libc::strcmp;
|
||||
use smol_str::SmolStr;
|
||||
use std::any::type_name;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
use std::ffi::CString;
|
||||
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
use crate::actor_id::AMactorId;
|
||||
use crate::byte_span::AMbyteSpan;
|
||||
use crate::change::AMchange;
|
||||
use crate::change_hashes::AMchangeHashes;
|
||||
use crate::changes::AMchanges;
|
||||
use crate::doc::list::{item::AMlistItem, items::AMlistItems};
|
||||
use crate::doc::map::{item::AMmapItem, items::AMmapItems};
|
||||
use crate::doc::utils::to_str;
|
||||
use crate::doc::AMdoc;
|
||||
use crate::obj::item::AMobjItem;
|
||||
use crate::obj::items::AMobjItems;
|
||||
use crate::obj::AMobjId;
|
||||
use crate::strs::AMstrs;
|
||||
use crate::sync::{AMsyncMessage, AMsyncState};
|
||||
|
||||
/// \struct AMvalue
|
||||
/// \installed_headerfile
|
||||
/// \brief A discriminated union of value type variants for a result.
|
||||
///
|
||||
/// \enum AMvalueVariant
|
||||
/// \brief A value type discriminant.
|
||||
///
|
||||
/// \var AMvalue::actor_id
|
||||
/// An actor identifier as a pointer to an `AMactorId` struct.
|
||||
///
|
||||
/// \var AMvalue::boolean
|
||||
/// A boolean.
|
||||
///
|
||||
/// \var AMvalue::bytes
|
||||
/// A sequence of bytes as an `AMbyteSpan` struct.
|
||||
///
|
||||
/// \var AMvalue::change_hashes
|
||||
/// A sequence of change hashes as an `AMchangeHashes` struct.
|
||||
///
|
||||
/// \var AMvalue::changes
|
||||
/// A sequence of changes as an `AMchanges` struct.
|
||||
///
|
||||
/// \var AMvalue::counter
|
||||
/// A CRDT counter.
|
||||
///
|
||||
/// \var AMvalue::doc
|
||||
/// A document as a pointer to an `AMdoc` struct.
|
||||
///
|
||||
/// \var AMvalue::f64
|
||||
/// A 64-bit float.
|
||||
///
|
||||
/// \var AMvalue::int_
|
||||
/// A 64-bit signed integer.
|
||||
///
|
||||
/// \var AMvalue::list_items
|
||||
/// A sequence of list object items as an `AMlistItems` struct.
|
||||
///
|
||||
/// \var AMvalue::map_items
|
||||
/// A sequence of map object items as an `AMmapItems` struct.
|
||||
///
|
||||
/// \var AMvalue::obj_id
|
||||
/// An object identifier as a pointer to an `AMobjId` struct.
|
||||
///
|
||||
/// \var AMvalue::obj_items
|
||||
/// A sequence of object items as an `AMobjItems` struct.
|
||||
///
|
||||
/// \var AMvalue::str
|
||||
/// A UTF-8 string.
|
||||
///
|
||||
/// \var AMvalue::strs
|
||||
/// A sequence of UTF-8 strings as an `AMstrs` struct.
|
||||
///
|
||||
/// \var AMvalue::sync_message
|
||||
/// A synchronization message as a pointer to an `AMsyncMessage` struct.
|
||||
///
|
||||
/// \var AMvalue::sync_state
|
||||
/// A synchronization state as a pointer to an `AMsyncState` struct.
|
||||
///
|
||||
/// \var AMvalue::tag
|
||||
/// The variant discriminator.
|
||||
///
|
||||
/// \var AMvalue::timestamp
|
||||
/// A Lamport timestamp.
|
||||
///
|
||||
/// \var AMvalue::uint
|
||||
/// A 64-bit unsigned integer.
|
||||
///
|
||||
/// \var AMvalue::unknown
|
||||
/// A value of unknown type as an `AMunknownValue` struct.
|
||||
#[repr(u8)]
|
||||
pub enum AMvalue<'a> {
|
||||
/// A void variant.
|
||||
/// \note This tag is unalphabetized so that a zeroed struct will have it.
|
||||
Void,
|
||||
/// An actor identifier variant.
|
||||
ActorId(&'a AMactorId),
|
||||
/// A boolean variant.
|
||||
Boolean(bool),
|
||||
/// A byte array variant.
|
||||
Bytes(AMbyteSpan),
|
||||
/// A change hashes variant.
|
||||
ChangeHashes(AMchangeHashes),
|
||||
/// A changes variant.
|
||||
Changes(AMchanges),
|
||||
/// A CRDT counter variant.
|
||||
Counter(i64),
|
||||
/// A document variant.
|
||||
Doc(*mut AMdoc),
|
||||
/// A 64-bit float variant.
|
||||
F64(f64),
|
||||
/// A 64-bit signed integer variant.
|
||||
Int(i64),
|
||||
/// A list items variant.
|
||||
ListItems(AMlistItems),
|
||||
/// A map items variant.
|
||||
MapItems(AMmapItems),
|
||||
/// A null variant.
|
||||
Null,
|
||||
/// An object identifier variant.
|
||||
ObjId(&'a AMobjId),
|
||||
/// An object items variant.
|
||||
ObjItems(AMobjItems),
|
||||
/// A UTF-8 string variant.
|
||||
Str(*const libc::c_char),
|
||||
/// A UTF-8 strings variant.
|
||||
Strs(AMstrs),
|
||||
/// A synchronization message variant.
|
||||
SyncMessage(&'a AMsyncMessage),
|
||||
/// A synchronization state variant.
|
||||
SyncState(&'a mut AMsyncState),
|
||||
/// A Lamport timestamp variant.
|
||||
Timestamp(i64),
|
||||
/// A 64-bit unsigned integer variant.
|
||||
Uint(u64),
|
||||
/// An unknown type of scalar value variant.
|
||||
Unknown(AMunknownValue),
|
||||
}
|
||||
|
||||
impl<'a> PartialEq for AMvalue<'a> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
use AMvalue::*;
|
||||
|
||||
match (self, other) {
|
||||
(ActorId(lhs), ActorId(rhs)) => *lhs == *rhs,
|
||||
(Boolean(lhs), Boolean(rhs)) => lhs == rhs,
|
||||
(Bytes(lhs), Bytes(rhs)) => lhs == rhs,
|
||||
(ChangeHashes(lhs), ChangeHashes(rhs)) => lhs == rhs,
|
||||
(Changes(lhs), Changes(rhs)) => lhs == rhs,
|
||||
(Counter(lhs), Counter(rhs)) => lhs == rhs,
|
||||
(Doc(lhs), Doc(rhs)) => *lhs == *rhs,
|
||||
(F64(lhs), F64(rhs)) => lhs == rhs,
|
||||
(Int(lhs), Int(rhs)) => lhs == rhs,
|
||||
(ListItems(lhs), ListItems(rhs)) => lhs == rhs,
|
||||
(MapItems(lhs), MapItems(rhs)) => lhs == rhs,
|
||||
(ObjId(lhs), ObjId(rhs)) => *lhs == *rhs,
|
||||
(ObjItems(lhs), ObjItems(rhs)) => lhs == rhs,
|
||||
(Str(lhs), Str(rhs)) => unsafe { strcmp(*lhs, *rhs) == 0 },
|
||||
(Strs(lhs), Strs(rhs)) => lhs == rhs,
|
||||
(SyncMessage(lhs), SyncMessage(rhs)) => *lhs == *rhs,
|
||||
(SyncState(lhs), SyncState(rhs)) => *lhs == *rhs,
|
||||
(Timestamp(lhs), Timestamp(rhs)) => lhs == rhs,
|
||||
(Uint(lhs), Uint(rhs)) => lhs == rhs,
|
||||
(Unknown(lhs), Unknown(rhs)) => lhs == rhs,
|
||||
(Null, Null) | (Void, Void) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&am::Value<'_>, &RefCell<Option<CString>>)> for AMvalue<'_> {
|
||||
fn from((value, c_str): (&am::Value<'_>, &RefCell<Option<CString>>)) -> Self {
|
||||
match value {
|
||||
am::Value::Scalar(scalar) => match scalar.as_ref() {
|
||||
am::ScalarValue::Boolean(flag) => AMvalue::Boolean(*flag),
|
||||
am::ScalarValue::Bytes(bytes) => AMvalue::Bytes(bytes.as_slice().into()),
|
||||
am::ScalarValue::Counter(counter) => AMvalue::Counter(counter.into()),
|
||||
am::ScalarValue::F64(float) => AMvalue::F64(*float),
|
||||
am::ScalarValue::Int(int) => AMvalue::Int(*int),
|
||||
am::ScalarValue::Null => AMvalue::Null,
|
||||
am::ScalarValue::Str(smol_str) => {
|
||||
let mut c_str = c_str.borrow_mut();
|
||||
AMvalue::Str(match c_str.as_mut() {
|
||||
None => {
|
||||
let value_str = CString::new(smol_str.to_string()).unwrap();
|
||||
c_str.insert(value_str).as_ptr()
|
||||
}
|
||||
Some(value_str) => value_str.as_ptr(),
|
||||
})
|
||||
}
|
||||
am::ScalarValue::Timestamp(timestamp) => AMvalue::Timestamp(*timestamp),
|
||||
am::ScalarValue::Uint(uint) => AMvalue::Uint(*uint),
|
||||
am::ScalarValue::Unknown { bytes, type_code } => AMvalue::Unknown(AMunknownValue {
|
||||
bytes: bytes.as_slice().into(),
|
||||
type_code: *type_code,
|
||||
}),
|
||||
},
|
||||
// \todo Confirm that an object variant should be ignored
|
||||
// when there's no object ID variant.
|
||||
am::Value::Object(_) => AMvalue::Void,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&AMvalue<'_>> for u8 {
|
||||
fn from(value: &AMvalue) -> Self {
|
||||
use AMvalue::*;
|
||||
|
||||
// \warning These numbers must correspond to the order in which the
|
||||
// variants of an AMvalue are declared within it.
|
||||
match value {
|
||||
ActorId(_) => 1,
|
||||
Boolean(_) => 2,
|
||||
Bytes(_) => 3,
|
||||
ChangeHashes(_) => 4,
|
||||
Changes(_) => 5,
|
||||
Counter(_) => 6,
|
||||
Doc(_) => 7,
|
||||
F64(_) => 8,
|
||||
Int(_) => 9,
|
||||
ListItems(_) => 10,
|
||||
MapItems(_) => 11,
|
||||
Null => 12,
|
||||
ObjId(_) => 13,
|
||||
ObjItems(_) => 14,
|
||||
Str(_) => 15,
|
||||
Strs(_) => 16,
|
||||
SyncMessage(_) => 17,
|
||||
SyncState(_) => 18,
|
||||
Timestamp(_) => 19,
|
||||
Uint(_) => 20,
|
||||
Unknown(..) => 21,
|
||||
Void => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&AMvalue<'_>> for am::ScalarValue {
|
||||
type Error = am::AutomergeError;
|
||||
|
||||
fn try_from(c_value: &AMvalue) -> Result<Self, Self::Error> {
|
||||
use am::AutomergeError::InvalidValueType;
|
||||
use AMvalue::*;
|
||||
|
||||
let expected = type_name::<am::ScalarValue>().to_string();
|
||||
match c_value {
|
||||
Boolean(b) => Ok(am::ScalarValue::Boolean(*b)),
|
||||
Bytes(span) => {
|
||||
let slice = unsafe { std::slice::from_raw_parts(span.src, span.count) };
|
||||
Ok(am::ScalarValue::Bytes(slice.to_vec()))
|
||||
}
|
||||
Counter(c) => Ok(am::ScalarValue::Counter(c.into())),
|
||||
F64(f) => Ok(am::ScalarValue::F64(*f)),
|
||||
Int(i) => Ok(am::ScalarValue::Int(*i)),
|
||||
Str(c_str) => {
|
||||
let smol_str = unsafe { SmolStr::new(to_str(*c_str)) };
|
||||
Ok(am::ScalarValue::Str(smol_str))
|
||||
}
|
||||
Timestamp(t) => Ok(am::ScalarValue::Timestamp(*t)),
|
||||
Uint(u) => Ok(am::ScalarValue::Uint(*u)),
|
||||
Null => Ok(am::ScalarValue::Null),
|
||||
Unknown(AMunknownValue { bytes, type_code }) => {
|
||||
let slice = unsafe { std::slice::from_raw_parts(bytes.src, bytes.count) };
|
||||
Ok(am::ScalarValue::Unknown {
|
||||
bytes: slice.to_vec(),
|
||||
type_code: *type_code,
|
||||
})
|
||||
}
|
||||
ActorId(_) => Err(InvalidValueType {
|
||||
expected,
|
||||
unexpected: type_name::<AMactorId>().to_string(),
|
||||
}),
|
||||
ChangeHashes(_) => Err(InvalidValueType {
|
||||
expected,
|
||||
unexpected: type_name::<AMchangeHashes>().to_string(),
|
||||
}),
|
||||
Changes(_) => Err(InvalidValueType {
|
||||
expected,
|
||||
unexpected: type_name::<AMchanges>().to_string(),
|
||||
}),
|
||||
Doc(_) => Err(InvalidValueType {
|
||||
expected,
|
||||
unexpected: type_name::<AMdoc>().to_string(),
|
||||
}),
|
||||
ListItems(_) => Err(InvalidValueType {
|
||||
expected,
|
||||
unexpected: type_name::<AMlistItems>().to_string(),
|
||||
}),
|
||||
MapItems(_) => Err(InvalidValueType {
|
||||
expected,
|
||||
unexpected: type_name::<AMmapItems>().to_string(),
|
||||
}),
|
||||
ObjId(_) => Err(InvalidValueType {
|
||||
expected,
|
||||
unexpected: type_name::<AMobjId>().to_string(),
|
||||
}),
|
||||
ObjItems(_) => Err(InvalidValueType {
|
||||
expected,
|
||||
unexpected: type_name::<AMobjItems>().to_string(),
|
||||
}),
|
||||
Strs(_) => Err(InvalidValueType {
|
||||
expected,
|
||||
unexpected: type_name::<AMstrs>().to_string(),
|
||||
}),
|
||||
SyncMessage(_) => Err(InvalidValueType {
|
||||
expected,
|
||||
unexpected: type_name::<AMsyncMessage>().to_string(),
|
||||
}),
|
||||
SyncState(_) => Err(InvalidValueType {
|
||||
expected,
|
||||
unexpected: type_name::<AMsyncState>().to_string(),
|
||||
}),
|
||||
Void => Err(InvalidValueType {
|
||||
expected,
|
||||
unexpected: type_name::<()>().to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMvalue
|
||||
/// \brief Tests the equality of two values.
|
||||
///
|
||||
/// \param[in] value1 A pointer to an `AMvalue` struct.
|
||||
/// \param[in] value2 A pointer to an `AMvalue` struct.
|
||||
/// \return `true` if \p value1 `==` \p value2 and `false` otherwise.
|
||||
/// \pre \p value1 `!= NULL`.
|
||||
/// \pre \p value2 `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// value1 must be a valid AMvalue pointer
|
||||
/// value2 must be a valid AMvalue pointer
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMvalueEqual(value1: *const AMvalue, value2: *const AMvalue) -> bool {
|
||||
match (value1.as_ref(), value2.as_ref()) {
|
||||
(Some(value1), Some(value2)) => *value1 == *value2,
|
||||
(None, Some(_)) | (Some(_), None) | (None, None) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// \struct AMresult
|
||||
/// \installed_headerfile
|
||||
/// \brief A discriminated union of result variants.
|
||||
pub enum AMresult {
|
||||
ActorId(am::ActorId, Option<AMactorId>),
|
||||
ChangeHashes(Vec<am::ChangeHash>),
|
||||
Changes(Vec<am::Change>, Option<BTreeMap<usize, AMchange>>),
|
||||
Doc(Box<AMdoc>),
|
||||
Error(CString),
|
||||
ListItems(Vec<AMlistItem>),
|
||||
MapItems(Vec<AMmapItem>),
|
||||
ObjId(AMobjId),
|
||||
ObjItems(Vec<AMobjItem>),
|
||||
String(CString),
|
||||
Strings(Vec<CString>),
|
||||
SyncMessage(AMsyncMessage),
|
||||
SyncState(Box<AMsyncState>),
|
||||
Value(am::Value<'static>, RefCell<Option<CString>>),
|
||||
Void,
|
||||
}
|
||||
|
||||
impl AMresult {
|
||||
pub(crate) fn err(s: &str) -> Self {
|
||||
AMresult::Error(CString::new(s).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<am::AutoCommit> for AMresult {
|
||||
fn from(auto_commit: am::AutoCommit) -> Self {
|
||||
AMresult::Doc(Box::new(AMdoc::new(auto_commit)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<am::ChangeHash> for AMresult {
|
||||
fn from(change_hash: am::ChangeHash) -> Self {
|
||||
AMresult::ChangeHashes(vec![change_hash])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<am::Keys<'_, '_>> for AMresult {
|
||||
fn from(keys: am::Keys<'_, '_>) -> Self {
|
||||
let cstrings: Vec<CString> = keys.map(|s| CString::new(s).unwrap()).collect();
|
||||
AMresult::Strings(cstrings)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<am::KeysAt<'_, '_>> for AMresult {
|
||||
fn from(keys: am::KeysAt<'_, '_>) -> Self {
|
||||
let cstrings: Vec<CString> = keys.map(|s| CString::new(s).unwrap()).collect();
|
||||
AMresult::Strings(cstrings)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<am::ListRange<'static, Range<usize>>> for AMresult {
|
||||
fn from(list_range: am::ListRange<'static, Range<usize>>) -> Self {
|
||||
AMresult::ListItems(
|
||||
list_range
|
||||
.map(|(i, v, o)| AMlistItem::new(i, v.clone(), o))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<am::ListRangeAt<'static, Range<usize>>> for AMresult {
|
||||
fn from(list_range: am::ListRangeAt<'static, Range<usize>>) -> Self {
|
||||
AMresult::ListItems(
|
||||
list_range
|
||||
.map(|(i, v, o)| AMlistItem::new(i, v.clone(), o))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<am::MapRange<'static, Range<String>>> for AMresult {
|
||||
fn from(map_range: am::MapRange<'static, Range<String>>) -> Self {
|
||||
let map_items: Vec<AMmapItem> = map_range
|
||||
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
|
||||
.collect();
|
||||
AMresult::MapItems(map_items)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<am::MapRangeAt<'static, Range<String>>> for AMresult {
|
||||
fn from(map_range: am::MapRangeAt<'static, Range<String>>) -> Self {
|
||||
let map_items: Vec<AMmapItem> = map_range
|
||||
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
|
||||
.collect();
|
||||
AMresult::MapItems(map_items)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<am::MapRange<'static, RangeFrom<String>>> for AMresult {
|
||||
fn from(map_range: am::MapRange<'static, RangeFrom<String>>) -> Self {
|
||||
let map_items: Vec<AMmapItem> = map_range
|
||||
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
|
||||
.collect();
|
||||
AMresult::MapItems(map_items)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<am::MapRangeAt<'static, RangeFrom<String>>> for AMresult {
|
||||
fn from(map_range: am::MapRangeAt<'static, RangeFrom<String>>) -> Self {
|
||||
let map_items: Vec<AMmapItem> = map_range
|
||||
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
|
||||
.collect();
|
||||
AMresult::MapItems(map_items)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<am::MapRange<'static, RangeFull>> for AMresult {
|
||||
fn from(map_range: am::MapRange<'static, RangeFull>) -> Self {
|
||||
let map_items: Vec<AMmapItem> = map_range
|
||||
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
|
||||
.collect();
|
||||
AMresult::MapItems(map_items)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<am::MapRangeAt<'static, RangeFull>> for AMresult {
|
||||
fn from(map_range: am::MapRangeAt<'static, RangeFull>) -> Self {
|
||||
let map_items: Vec<AMmapItem> = map_range
|
||||
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
|
||||
.collect();
|
||||
AMresult::MapItems(map_items)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<am::MapRange<'static, RangeTo<String>>> for AMresult {
|
||||
fn from(map_range: am::MapRange<'static, RangeTo<String>>) -> Self {
|
||||
let map_items: Vec<AMmapItem> = map_range
|
||||
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
|
||||
.collect();
|
||||
AMresult::MapItems(map_items)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<am::MapRangeAt<'static, RangeTo<String>>> for AMresult {
|
||||
fn from(map_range: am::MapRangeAt<'static, RangeTo<String>>) -> Self {
|
||||
let map_items: Vec<AMmapItem> = map_range
|
||||
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
|
||||
.collect();
|
||||
AMresult::MapItems(map_items)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<am::sync::State> for AMresult {
|
||||
fn from(state: am::sync::State) -> Self {
|
||||
AMresult::SyncState(Box::new(AMsyncState::new(state)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<am::Values<'static>> for AMresult {
|
||||
fn from(pairs: am::Values<'static>) -> Self {
|
||||
AMresult::ObjItems(pairs.map(|(v, o)| AMobjItem::new(v.clone(), o)).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Result<Vec<(am::Value<'static>, am::ObjId)>, am::AutomergeError>> for AMresult {
|
||||
fn from(maybe: Result<Vec<(am::Value<'static>, am::ObjId)>, am::AutomergeError>) -> Self {
|
||||
match maybe {
|
||||
Ok(pairs) => AMresult::ObjItems(
|
||||
pairs
|
||||
.into_iter()
|
||||
.map(|(v, o)| AMobjItem::new(v, o))
|
||||
.collect(),
|
||||
),
|
||||
Err(e) => AMresult::err(&e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AMresult> for *mut AMresult {
|
||||
fn from(b: AMresult) -> Self {
|
||||
Box::into_raw(Box::new(b))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<&am::Change>> for AMresult {
|
||||
fn from(maybe: Option<&am::Change>) -> Self {
|
||||
match maybe {
|
||||
Some(change) => AMresult::Changes(vec![change.clone()], None),
|
||||
None => AMresult::Void,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<am::sync::Message>> for AMresult {
|
||||
fn from(maybe: Option<am::sync::Message>) -> Self {
|
||||
match maybe {
|
||||
Some(message) => AMresult::SyncMessage(AMsyncMessage::new(message)),
|
||||
None => AMresult::Void,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Result<(), am::AutomergeError>> for AMresult {
|
||||
fn from(maybe: Result<(), am::AutomergeError>) -> Self {
|
||||
match maybe {
|
||||
Ok(()) => AMresult::Void,
|
||||
Err(e) => AMresult::err(&e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<Result<am::ActorId, am::AutomergeError>> for AMresult {
|
||||
fn from(maybe: Result<am::ActorId, am::AutomergeError>) -> Self {
|
||||
match maybe {
|
||||
Ok(actor_id) => AMresult::ActorId(actor_id, None),
|
||||
Err(e) => AMresult::err(&e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Result<am::ActorId, am::InvalidActorId>> for AMresult {
|
||||
fn from(maybe: Result<am::ActorId, am::InvalidActorId>) -> Self {
|
||||
match maybe {
|
||||
Ok(actor_id) => AMresult::ActorId(actor_id, None),
|
||||
Err(e) => AMresult::err(&e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Result<am::AutoCommit, am::AutomergeError>> for AMresult {
|
||||
fn from(maybe: Result<am::AutoCommit, am::AutomergeError>) -> Self {
|
||||
match maybe {
|
||||
Ok(auto_commit) => AMresult::Doc(Box::new(AMdoc::new(auto_commit))),
|
||||
Err(e) => AMresult::err(&e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Result<am::Change, am::LoadChangeError>> for AMresult {
|
||||
fn from(maybe: Result<am::Change, am::LoadChangeError>) -> Self {
|
||||
match maybe {
|
||||
Ok(change) => AMresult::Changes(vec![change], None),
|
||||
Err(e) => AMresult::err(&e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Result<am::ObjId, am::AutomergeError>> for AMresult {
|
||||
fn from(maybe: Result<am::ObjId, am::AutomergeError>) -> Self {
|
||||
match maybe {
|
||||
Ok(obj_id) => AMresult::ObjId(AMobjId::new(obj_id)),
|
||||
Err(e) => AMresult::err(&e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Result<am::sync::Message, am::sync::ReadMessageError>> for AMresult {
|
||||
fn from(maybe: Result<am::sync::Message, am::sync::ReadMessageError>) -> Self {
|
||||
match maybe {
|
||||
Ok(message) => AMresult::SyncMessage(AMsyncMessage::new(message)),
|
||||
Err(e) => AMresult::err(&e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Result<am::sync::State, am::sync::DecodeStateError>> for AMresult {
|
||||
fn from(maybe: Result<am::sync::State, am::sync::DecodeStateError>) -> Self {
|
||||
match maybe {
|
||||
Ok(state) => AMresult::SyncState(Box::new(AMsyncState::new(state))),
|
||||
Err(e) => AMresult::err(&e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Result<am::Value<'static>, am::AutomergeError>> for AMresult {
|
||||
fn from(maybe: Result<am::Value<'static>, am::AutomergeError>) -> Self {
|
||||
match maybe {
|
||||
Ok(value) => AMresult::Value(value, Default::default()),
|
||||
Err(e) => AMresult::err(&e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Result<Option<(am::Value<'static>, am::ObjId)>, am::AutomergeError>> for AMresult {
|
||||
fn from(maybe: Result<Option<(am::Value<'static>, am::ObjId)>, am::AutomergeError>) -> Self {
|
||||
match maybe {
|
||||
Ok(Some((value, obj_id))) => match value {
|
||||
am::Value::Object(_) => AMresult::ObjId(AMobjId::new(obj_id)),
|
||||
_ => AMresult::Value(value, Default::default()),
|
||||
},
|
||||
Ok(None) => AMresult::Void,
|
||||
Err(e) => AMresult::err(&e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Result<String, am::AutomergeError>> for AMresult {
|
||||
fn from(maybe: Result<String, am::AutomergeError>) -> Self {
|
||||
match maybe {
|
||||
Ok(string) => AMresult::String(CString::new(string).unwrap()),
|
||||
Err(e) => AMresult::err(&e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Result<usize, am::AutomergeError>> for AMresult {
|
||||
fn from(maybe: Result<usize, am::AutomergeError>) -> Self {
|
||||
match maybe {
|
||||
Ok(size) => AMresult::Value(am::Value::uint(size as u64), Default::default()),
|
||||
Err(e) => AMresult::err(&e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Result<Vec<am::Change>, am::AutomergeError>> for AMresult {
|
||||
fn from(maybe: Result<Vec<am::Change>, am::AutomergeError>) -> Self {
|
||||
match maybe {
|
||||
Ok(changes) => AMresult::Changes(changes, None),
|
||||
Err(e) => AMresult::err(&e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Result<Vec<am::Change>, am::LoadChangeError>> for AMresult {
|
||||
fn from(maybe: Result<Vec<am::Change>, am::LoadChangeError>) -> Self {
|
||||
match maybe {
|
||||
Ok(changes) => AMresult::Changes(changes, None),
|
||||
Err(e) => AMresult::err(&e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Result<Vec<&am::Change>, am::AutomergeError>> for AMresult {
|
||||
fn from(maybe: Result<Vec<&am::Change>, am::AutomergeError>) -> Self {
|
||||
match maybe {
|
||||
Ok(changes) => {
|
||||
let changes: Vec<am::Change> =
|
||||
changes.iter().map(|&change| change.clone()).collect();
|
||||
AMresult::Changes(changes, None)
|
||||
}
|
||||
Err(e) => AMresult::err(&e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Result<Vec<am::ChangeHash>, am::AutomergeError>> for AMresult {
|
||||
fn from(maybe: Result<Vec<am::ChangeHash>, am::AutomergeError>) -> Self {
|
||||
match maybe {
|
||||
Ok(change_hashes) => AMresult::ChangeHashes(change_hashes),
|
||||
Err(e) => AMresult::err(&e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Result<Vec<am::ChangeHash>, am::InvalidChangeHashSlice>> for AMresult {
|
||||
fn from(maybe: Result<Vec<am::ChangeHash>, am::InvalidChangeHashSlice>) -> Self {
|
||||
match maybe {
|
||||
Ok(change_hashes) => AMresult::ChangeHashes(change_hashes),
|
||||
Err(e) => AMresult::err(&e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Result<Vec<u8>, am::AutomergeError>> for AMresult {
|
||||
fn from(maybe: Result<Vec<u8>, am::AutomergeError>) -> Self {
|
||||
match maybe {
|
||||
Ok(bytes) => AMresult::Value(am::Value::bytes(bytes), Default::default()),
|
||||
Err(e) => AMresult::err(&e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<&am::Change>> for AMresult {
|
||||
fn from(changes: Vec<&am::Change>) -> Self {
|
||||
let changes: Vec<am::Change> = changes.iter().map(|&change| change.clone()).collect();
|
||||
AMresult::Changes(changes, None)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<am::ChangeHash>> for AMresult {
|
||||
fn from(change_hashes: Vec<am::ChangeHash>) -> Self {
|
||||
AMresult::ChangeHashes(change_hashes)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for AMresult {
|
||||
fn from(bytes: Vec<u8>) -> Self {
|
||||
AMresult::Value(am::Value::bytes(bytes), Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_result<R: Into<AMresult>>(r: R) -> *mut AMresult {
|
||||
(r.into()).into()
|
||||
}
|
||||
|
||||
/// \ingroup enumerations
|
||||
/// \enum AMstatus
|
||||
/// \brief The status of an API call.
|
||||
#[derive(Debug)]
|
||||
#[repr(u8)]
|
||||
pub enum AMstatus {
|
||||
/// Success.
|
||||
/// \note This tag is unalphabetized so that `0` indicates success.
|
||||
Ok,
|
||||
/// Failure due to an error.
|
||||
Error,
|
||||
/// Failure due to an invalid result.
|
||||
InvalidResult,
|
||||
}
|
||||
|
||||
/// \memberof AMresult
|
||||
/// \brief Gets a result's error message string.
|
||||
///
|
||||
/// \param[in] result A pointer to an `AMresult` struct.
|
||||
/// \return A UTF-8 string value or `NULL`.
|
||||
/// \pre \p result `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// # Safety
|
||||
/// result must be a valid pointer to an AMresult
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMerrorMessage(result: *const AMresult) -> *const c_char {
|
||||
match result.as_ref() {
|
||||
Some(AMresult::Error(s)) => s.as_ptr(),
|
||||
_ => std::ptr::null::<c_char>(),
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMresult
|
||||
/// \brief Deallocates the storage for a result.
|
||||
///
|
||||
/// \param[in,out] result A pointer to an `AMresult` struct.
|
||||
/// \pre \p result `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// # Safety
|
||||
/// result must be a valid pointer to an AMresult
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMfree(result: *mut AMresult) {
|
||||
if !result.is_null() {
|
||||
let result: AMresult = *Box::from_raw(result);
|
||||
drop(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMresult
|
||||
/// \brief Gets the size of a result's value.
|
||||
///
|
||||
/// \param[in] result A pointer to an `AMresult` struct.
|
||||
/// \return The count of values in \p result.
|
||||
/// \pre \p result `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// # Safety
|
||||
/// result must be a valid pointer to an AMresult
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMresultSize(result: *const AMresult) -> usize {
|
||||
if let Some(result) = result.as_ref() {
|
||||
use AMresult::*;
|
||||
|
||||
match result {
|
||||
Error(_) | Void => 0,
|
||||
ActorId(_, _)
|
||||
| Doc(_)
|
||||
| ObjId(_)
|
||||
| String(_)
|
||||
| SyncMessage(_)
|
||||
| SyncState(_)
|
||||
| Value(_, _) => 1,
|
||||
ChangeHashes(change_hashes) => change_hashes.len(),
|
||||
Changes(changes, _) => changes.len(),
|
||||
ListItems(list_items) => list_items.len(),
|
||||
MapItems(map_items) => map_items.len(),
|
||||
ObjItems(obj_items) => obj_items.len(),
|
||||
Strings(cstrings) => cstrings.len(),
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMresult
|
||||
/// \brief Gets the status code of a result.
|
||||
///
|
||||
/// \param[in] result A pointer to an `AMresult` struct.
|
||||
/// \return An `AMstatus` enum tag.
|
||||
/// \pre \p result `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// # Safety
|
||||
/// result must be a valid pointer to an AMresult
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMresultStatus(result: *const AMresult) -> AMstatus {
|
||||
match result.as_ref() {
|
||||
Some(AMresult::Error(_)) => AMstatus::Error,
|
||||
None => AMstatus::InvalidResult,
|
||||
_ => AMstatus::Ok,
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMresult
|
||||
/// \brief Gets a result's value.
|
||||
///
|
||||
/// \param[in] result A pointer to an `AMresult` struct.
|
||||
/// \return An `AMvalue` struct.
|
||||
/// \pre \p result `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// # Safety
|
||||
/// result must be a valid pointer to an AMresult
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMresultValue<'a>(result: *mut AMresult) -> AMvalue<'a> {
|
||||
let mut content = AMvalue::Void;
|
||||
if let Some(result) = result.as_mut() {
|
||||
match result {
|
||||
AMresult::ActorId(actor_id, c_actor_id) => match c_actor_id {
|
||||
None => {
|
||||
content = AMvalue::ActorId(&*c_actor_id.insert(AMactorId::new(&*actor_id)));
|
||||
}
|
||||
Some(c_actor_id) => {
|
||||
content = AMvalue::ActorId(&*c_actor_id);
|
||||
}
|
||||
},
|
||||
AMresult::ChangeHashes(change_hashes) => {
|
||||
content = AMvalue::ChangeHashes(AMchangeHashes::new(change_hashes));
|
||||
}
|
||||
AMresult::Changes(changes, storage) => {
|
||||
content = AMvalue::Changes(AMchanges::new(
|
||||
changes,
|
||||
storage.get_or_insert(BTreeMap::new()),
|
||||
));
|
||||
}
|
||||
AMresult::Doc(doc) => content = AMvalue::Doc(&mut **doc),
|
||||
AMresult::Error(_) => {}
|
||||
AMresult::ListItems(list_items) => {
|
||||
content = AMvalue::ListItems(AMlistItems::new(list_items));
|
||||
}
|
||||
AMresult::MapItems(map_items) => {
|
||||
content = AMvalue::MapItems(AMmapItems::new(map_items));
|
||||
}
|
||||
AMresult::ObjId(obj_id) => {
|
||||
content = AMvalue::ObjId(obj_id);
|
||||
}
|
||||
AMresult::ObjItems(obj_items) => {
|
||||
content = AMvalue::ObjItems(AMobjItems::new(obj_items));
|
||||
}
|
||||
AMresult::String(cstring) => content = AMvalue::Str(cstring.as_ptr()),
|
||||
AMresult::Strings(cstrings) => {
|
||||
content = AMvalue::Strs(AMstrs::new(cstrings));
|
||||
}
|
||||
AMresult::SyncMessage(sync_message) => {
|
||||
content = AMvalue::SyncMessage(sync_message);
|
||||
}
|
||||
AMresult::SyncState(sync_state) => {
|
||||
content = AMvalue::SyncState(&mut *sync_state);
|
||||
}
|
||||
AMresult::Value(value, value_str) => {
|
||||
content = (&*value, &*value_str).into();
|
||||
}
|
||||
AMresult::Void => {}
|
||||
}
|
||||
};
|
||||
content
|
||||
}
|
||||
|
||||
/// \struct AMunknownValue
|
||||
/// \installed_headerfile
|
||||
/// \brief A value (typically for a `set` operation) whose type is unknown.
|
||||
///
|
||||
#[derive(Eq, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub struct AMunknownValue {
|
||||
/// The value's raw bytes.
|
||||
bytes: AMbyteSpan,
|
||||
/// The value's encoded type identifier.
|
||||
type_code: u8,
|
||||
}
|
|
@ -1,156 +0,0 @@
|
|||
use crate::result::{AMfree, AMresult, AMresultStatus, AMresultValue, AMstatus, AMvalue};
|
||||
|
||||
/// \struct AMresultStack
|
||||
/// \installed_headerfile
|
||||
/// \brief A node in a singly-linked list of result pointers.
|
||||
///
|
||||
/// \note Using this data structure is purely optional because its only purpose
|
||||
/// is to make memory management tolerable for direct usage of this API
|
||||
/// in C, C++ and Objective-C.
|
||||
#[repr(C)]
|
||||
pub struct AMresultStack {
|
||||
/// A result to be deallocated.
|
||||
pub result: *mut AMresult,
|
||||
/// The next node in the singly-linked list or `NULL`.
|
||||
pub next: *mut AMresultStack,
|
||||
}
|
||||
|
||||
impl AMresultStack {
|
||||
pub fn new(result: *mut AMresult, next: *mut AMresultStack) -> Self {
|
||||
Self { result, next }
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMresultStack
|
||||
/// \brief Deallocates the storage for a stack of results.
|
||||
///
|
||||
/// \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
|
||||
/// \return The number of `AMresult` structs freed.
|
||||
/// \pre \p stack `!= NULL`.
|
||||
/// \post `*stack == NULL`.
|
||||
/// \note Calling this function is purely optional because its only purpose is
|
||||
/// to make memory management tolerable for direct usage of this API in
|
||||
/// C, C++ and Objective-C.
|
||||
/// \internal
|
||||
///
|
||||
/// # Safety
|
||||
/// stack must be a valid AMresultStack pointer pointer
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMfreeStack(stack: *mut *mut AMresultStack) -> usize {
|
||||
if stack.is_null() {
|
||||
return 0;
|
||||
}
|
||||
let mut count: usize = 0;
|
||||
while !(*stack).is_null() {
|
||||
AMfree(AMpop(stack));
|
||||
count += 1;
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
/// \memberof AMresultStack
|
||||
/// \brief Gets the topmost result from the stack after removing it.
|
||||
///
|
||||
/// \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
|
||||
/// \return A pointer to an `AMresult` struct or `NULL`.
|
||||
/// \pre \p stack `!= NULL`.
|
||||
/// \post `*stack == NULL`.
|
||||
/// \note Calling this function is purely optional because its only purpose is
|
||||
/// to make memory management tolerable for direct usage of this API in
|
||||
/// C, C++ and Objective-C.
|
||||
/// \internal
|
||||
///
|
||||
/// # Safety
|
||||
/// stack must be a valid AMresultStack pointer pointer
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMpop(stack: *mut *mut AMresultStack) -> *mut AMresult {
|
||||
if stack.is_null() || (*stack).is_null() {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
let top = Box::from_raw(*stack);
|
||||
*stack = top.next;
|
||||
let result = top.result;
|
||||
drop(top);
|
||||
result
|
||||
}
|
||||
|
||||
/// \memberof AMresultStack
|
||||
/// \brief The prototype of a function to be called when a value matching the
|
||||
/// given discriminant cannot be extracted from the result at the top of
|
||||
/// the given stack.
|
||||
///
|
||||
/// \note Implementing this function is purely optional because its only purpose
|
||||
/// is to make memory management tolerable for direct usage of this API
|
||||
/// in C, C++ and Objective-C.
|
||||
pub type AMpushCallback =
|
||||
Option<extern "C" fn(stack: *mut *mut AMresultStack, discriminant: u8) -> ()>;
|
||||
|
||||
/// \memberof AMresultStack
|
||||
/// \brief Pushes the given result onto the given stack and then either extracts
|
||||
/// a value matching the given discriminant from that result or,
|
||||
/// failing that, calls the given function and gets a void value instead.
|
||||
///
|
||||
/// \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
|
||||
/// \param[in] result A pointer to an `AMresult` struct.
|
||||
/// \param[in] discriminant An `AMvalue` variant's corresponding enum tag.
|
||||
/// \param[in] callback A pointer to a function with the same signature as
|
||||
/// `AMpushCallback()` or `NULL`.
|
||||
/// \return An `AMvalue` struct.
|
||||
/// \pre \p stack `!= NULL`.
|
||||
/// \pre \p result `!= NULL`.
|
||||
/// \warning If \p stack `== NULL` then \p result is deallocated in order to
|
||||
/// prevent a memory leak.
|
||||
/// \note Calling this function is purely optional because its only purpose is
|
||||
/// to make memory management tolerable for direct usage of this API in
|
||||
/// C, C++ and Objective-C.
|
||||
/// \internal
|
||||
///
|
||||
/// # Safety
|
||||
/// stack must be a valid AMresultStack pointer pointer
|
||||
/// result must be a valid AMresult pointer
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMpush<'a>(
|
||||
stack: *mut *mut AMresultStack,
|
||||
result: *mut AMresult,
|
||||
discriminant: u8,
|
||||
callback: AMpushCallback,
|
||||
) -> AMvalue<'a> {
|
||||
if stack.is_null() {
|
||||
// There's no stack to push the result onto so it has to be freed in
|
||||
// order to prevent a memory leak.
|
||||
AMfree(result);
|
||||
if let Some(callback) = callback {
|
||||
callback(stack, discriminant);
|
||||
}
|
||||
return AMvalue::Void;
|
||||
} else if result.is_null() {
|
||||
if let Some(callback) = callback {
|
||||
callback(stack, discriminant);
|
||||
}
|
||||
return AMvalue::Void;
|
||||
}
|
||||
// Always push the result onto the stack, even if it's wrong, so that the
|
||||
// given callback can retrieve it.
|
||||
let node = Box::new(AMresultStack::new(result, *stack));
|
||||
let top = Box::into_raw(node);
|
||||
*stack = top;
|
||||
// Test that the result contains a value.
|
||||
match AMresultStatus(result) {
|
||||
AMstatus::Ok => {}
|
||||
_ => {
|
||||
if let Some(callback) = callback {
|
||||
callback(stack, discriminant);
|
||||
}
|
||||
return AMvalue::Void;
|
||||
}
|
||||
}
|
||||
// Test that the result's value matches the given discriminant.
|
||||
let value = AMresultValue(result);
|
||||
if discriminant != u8::from(&value) {
|
||||
if let Some(callback) = callback {
|
||||
callback(stack, discriminant);
|
||||
}
|
||||
return AMvalue::Void;
|
||||
}
|
||||
value
|
||||
}
|
|
@ -1,344 +0,0 @@
|
|||
use std::cmp::Ordering;
|
||||
use std::ffi::{c_void, CString};
|
||||
use std::mem::size_of;
|
||||
use std::os::raw::c_char;
|
||||
|
||||
#[repr(C)]
|
||||
struct Detail {
|
||||
len: usize,
|
||||
offset: isize,
|
||||
ptr: *const c_void,
|
||||
}
|
||||
|
||||
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
|
||||
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
|
||||
/// propagate the name of a constant initialized from it so if the
|
||||
/// constant's name is a symbolic representation of the value it can be
|
||||
/// converted into a number by post-processing the header it generated.
|
||||
pub const USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
|
||||
|
||||
impl Detail {
|
||||
fn new(c_strings: &[CString], offset: isize) -> Self {
|
||||
Self {
|
||||
len: c_strings.len(),
|
||||
offset,
|
||||
ptr: c_strings.as_ptr() as *const c_void,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance(&mut self, n: isize) {
|
||||
if n == 0 {
|
||||
return;
|
||||
}
|
||||
let len = self.len as isize;
|
||||
self.offset = if self.offset < 0 {
|
||||
// It's reversed.
|
||||
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
|
||||
if unclipped >= 0 {
|
||||
// Clip it to the forward stop.
|
||||
len
|
||||
} else {
|
||||
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
|
||||
}
|
||||
} else {
|
||||
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
|
||||
if unclipped < 0 {
|
||||
// Clip it to the reverse stop.
|
||||
-(len + 1)
|
||||
} else {
|
||||
std::cmp::max(0, std::cmp::min(unclipped, len))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_index(&self) -> usize {
|
||||
(self.offset
|
||||
+ if self.offset < 0 {
|
||||
self.len as isize
|
||||
} else {
|
||||
0
|
||||
}) as usize
|
||||
}
|
||||
|
||||
pub fn next(&mut self, n: isize) -> Option<*const c_char> {
|
||||
if self.is_stopped() {
|
||||
return None;
|
||||
}
|
||||
let slice: &[CString] =
|
||||
unsafe { std::slice::from_raw_parts(self.ptr as *const CString, self.len) };
|
||||
let value = slice[self.get_index()].as_ptr();
|
||||
self.advance(n);
|
||||
Some(value)
|
||||
}
|
||||
|
||||
pub fn is_stopped(&self) -> bool {
|
||||
let len = self.len as isize;
|
||||
self.offset < -len || self.offset == len
|
||||
}
|
||||
|
||||
pub fn prev(&mut self, n: isize) -> Option<*const c_char> {
|
||||
self.advance(-n);
|
||||
if self.is_stopped() {
|
||||
return None;
|
||||
}
|
||||
let slice: &[CString] =
|
||||
unsafe { std::slice::from_raw_parts(self.ptr as *const CString, self.len) };
|
||||
Some(slice[self.get_index()].as_ptr())
|
||||
}
|
||||
|
||||
pub fn reversed(&self) -> Self {
|
||||
Self {
|
||||
len: self.len,
|
||||
offset: -(self.offset + 1),
|
||||
ptr: self.ptr,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rewound(&self) -> Self {
|
||||
Self {
|
||||
len: self.len,
|
||||
offset: if self.offset < 0 { -1 } else { 0 },
|
||||
ptr: self.ptr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Detail> for [u8; USIZE_USIZE_USIZE_] {
|
||||
fn from(detail: Detail) -> Self {
|
||||
unsafe {
|
||||
std::slice::from_raw_parts((&detail as *const Detail) as *const u8, USIZE_USIZE_USIZE_)
|
||||
.try_into()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// \struct AMstrs
|
||||
/// \installed_headerfile
|
||||
/// \brief A random-access iterator over a sequence of UTF-8 strings.
|
||||
#[repr(C)]
|
||||
#[derive(Eq, PartialEq)]
|
||||
pub struct AMstrs {
|
||||
/// An implementation detail that is intentionally opaque.
|
||||
/// \warning Modifying \p detail will cause undefined behavior.
|
||||
/// \note The actual size of \p detail will vary by platform, this is just
|
||||
/// the one for the platform this documentation was built on.
|
||||
detail: [u8; USIZE_USIZE_USIZE_],
|
||||
}
|
||||
|
||||
impl AMstrs {
|
||||
pub fn new(c_strings: &[CString]) -> Self {
|
||||
Self {
|
||||
detail: Detail::new(c_strings, 0).into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance(&mut self, n: isize) {
|
||||
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
|
||||
detail.advance(n);
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
detail.len
|
||||
}
|
||||
|
||||
pub fn next(&mut self, n: isize) -> Option<*const c_char> {
|
||||
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
|
||||
detail.next(n)
|
||||
}
|
||||
|
||||
pub fn prev(&mut self, n: isize) -> Option<*const c_char> {
|
||||
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
|
||||
detail.prev(n)
|
||||
}
|
||||
|
||||
pub fn reversed(&self) -> Self {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
Self {
|
||||
detail: detail.reversed().into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rewound(&self) -> Self {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
Self {
|
||||
detail: detail.rewound().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[CString]> for AMstrs {
|
||||
fn as_ref(&self) -> &[CString] {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
unsafe { std::slice::from_raw_parts(detail.ptr as *const CString, detail.len) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AMstrs {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
detail: [0; USIZE_USIZE_USIZE_],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMstrs
|
||||
/// \brief Advances an iterator over a sequence of UTF-8 strings by at most
|
||||
/// \p |n| positions where the sign of \p n is relative to the
|
||||
/// iterator's direction.
|
||||
///
|
||||
/// \param[in,out] strs A pointer to an `AMstrs` struct.
|
||||
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
|
||||
/// number of positions to advance.
|
||||
/// \pre \p strs `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// strs must be a valid pointer to an AMstrs
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMstrsAdvance(strs: *mut AMstrs, n: isize) {
|
||||
if let Some(strs) = strs.as_mut() {
|
||||
strs.advance(n);
|
||||
};
|
||||
}
|
||||
|
||||
/// \memberof AMstrs
|
||||
/// \brief Compares the sequences of UTF-8 strings underlying a pair of
|
||||
/// iterators.
|
||||
///
|
||||
/// \param[in] strs1 A pointer to an `AMstrs` struct.
|
||||
/// \param[in] strs2 A pointer to an `AMstrs` struct.
|
||||
/// \return `-1` if \p strs1 `<` \p strs2, `0` if
|
||||
/// \p strs1 `==` \p strs2 and `1` if
|
||||
/// \p strs1 `>` \p strs2.
|
||||
/// \pre \p strs1 `!= NULL`.
|
||||
/// \pre \p strs2 `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// strs1 must be a valid pointer to an AMstrs
|
||||
/// strs2 must be a valid pointer to an AMstrs
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMstrsCmp(strs1: *const AMstrs, strs2: *const AMstrs) -> isize {
|
||||
match (strs1.as_ref(), strs2.as_ref()) {
|
||||
(Some(strs1), Some(strs2)) => match strs1.as_ref().cmp(strs2.as_ref()) {
|
||||
Ordering::Less => -1,
|
||||
Ordering::Equal => 0,
|
||||
Ordering::Greater => 1,
|
||||
},
|
||||
(None, Some(_)) => -1,
|
||||
(Some(_), None) => 1,
|
||||
(None, None) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMstrs
|
||||
/// \brief Gets the key at the current position of an iterator over a sequence
|
||||
/// of UTF-8 strings and then advances it by at most \p |n| positions
|
||||
/// where the sign of \p n is relative to the iterator's direction.
|
||||
///
|
||||
/// \param[in,out] strs A pointer to an `AMstrs` struct.
|
||||
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
|
||||
/// number of positions to advance.
|
||||
/// \return A UTF-8 string that's `NULL` when \p strs was previously advanced
|
||||
/// past its forward/reverse limit.
|
||||
/// \pre \p strs `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// strs must be a valid pointer to an AMstrs
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMstrsNext(strs: *mut AMstrs, n: isize) -> *const c_char {
|
||||
if let Some(strs) = strs.as_mut() {
|
||||
if let Some(key) = strs.next(n) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
std::ptr::null()
|
||||
}
|
||||
|
||||
/// \memberof AMstrs
|
||||
/// \brief Advances an iterator over a sequence of UTF-8 strings by at most
|
||||
/// \p |n| positions where the sign of \p n is relative to the
|
||||
/// iterator's direction and then gets the key at its new position.
|
||||
///
|
||||
/// \param[in,out] strs A pointer to an `AMstrs` struct.
|
||||
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
|
||||
/// number of positions to advance.
|
||||
/// \return A UTF-8 string that's `NULL` when \p strs is presently advanced
|
||||
/// past its forward/reverse limit.
|
||||
/// \pre \p strs `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// strs must be a valid pointer to an AMstrs
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMstrsPrev(strs: *mut AMstrs, n: isize) -> *const c_char {
|
||||
if let Some(strs) = strs.as_mut() {
|
||||
if let Some(key) = strs.prev(n) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
std::ptr::null()
|
||||
}
|
||||
|
||||
/// \memberof AMstrs
|
||||
/// \brief Gets the size of the sequence of UTF-8 strings underlying an
|
||||
/// iterator.
|
||||
///
|
||||
/// \param[in] strs A pointer to an `AMstrs` struct.
|
||||
/// \return The count of values in \p strs.
|
||||
/// \pre \p strs `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// strs must be a valid pointer to an AMstrs
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMstrsSize(strs: *const AMstrs) -> usize {
|
||||
if let Some(strs) = strs.as_ref() {
|
||||
strs.len()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMstrs
|
||||
/// \brief Creates an iterator over the same sequence of UTF-8 strings as the
|
||||
/// given one but with the opposite position and direction.
|
||||
///
|
||||
/// \param[in] strs A pointer to an `AMstrs` struct.
|
||||
/// \return An `AMstrs` struct.
|
||||
/// \pre \p strs `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// strs must be a valid pointer to an AMstrs
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMstrsReversed(strs: *const AMstrs) -> AMstrs {
|
||||
if let Some(strs) = strs.as_ref() {
|
||||
strs.reversed()
|
||||
} else {
|
||||
AMstrs::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMstrs
|
||||
/// \brief Creates an iterator at the starting position over the same sequence
|
||||
/// of UTF-8 strings as the given one.
|
||||
///
|
||||
/// \param[in] strs A pointer to an `AMstrs` struct.
|
||||
/// \return An `AMstrs` struct
|
||||
/// \pre \p strs `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// strs must be a valid pointer to an AMstrs
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMstrsRewound(strs: *const AMstrs) -> AMstrs {
|
||||
if let Some(strs) = strs.as_ref() {
|
||||
strs.rewound()
|
||||
} else {
|
||||
AMstrs::default()
|
||||
}
|
||||
}
|
|
@ -1,378 +0,0 @@
|
|||
use automerge as am;
|
||||
use std::collections::BTreeMap;
|
||||
use std::ffi::c_void;
|
||||
use std::mem::size_of;
|
||||
|
||||
use crate::sync::have::AMsyncHave;
|
||||
|
||||
#[repr(C)]
|
||||
struct Detail {
|
||||
len: usize,
|
||||
offset: isize,
|
||||
ptr: *const c_void,
|
||||
storage: *mut c_void,
|
||||
}
|
||||
|
||||
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
|
||||
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
|
||||
/// propagate the name of a constant initialized from it so if the
|
||||
/// constant's name is a symbolic representation of the value it can be
|
||||
/// converted into a number by post-processing the header it generated.
|
||||
pub const USIZE_USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
|
||||
|
||||
impl Detail {
|
||||
fn new(
|
||||
haves: &[am::sync::Have],
|
||||
offset: isize,
|
||||
storage: &mut BTreeMap<usize, AMsyncHave>,
|
||||
) -> Self {
|
||||
let storage: *mut BTreeMap<usize, AMsyncHave> = storage;
|
||||
Self {
|
||||
len: haves.len(),
|
||||
offset,
|
||||
ptr: haves.as_ptr() as *const c_void,
|
||||
storage: storage as *mut c_void,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance(&mut self, n: isize) {
|
||||
if n == 0 {
|
||||
return;
|
||||
}
|
||||
let len = self.len as isize;
|
||||
self.offset = if self.offset < 0 {
|
||||
// It's reversed.
|
||||
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
|
||||
if unclipped >= 0 {
|
||||
// Clip it to the forward stop.
|
||||
len
|
||||
} else {
|
||||
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
|
||||
}
|
||||
} else {
|
||||
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
|
||||
if unclipped < 0 {
|
||||
// Clip it to the reverse stop.
|
||||
-(len + 1)
|
||||
} else {
|
||||
std::cmp::max(0, std::cmp::min(unclipped, len))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_index(&self) -> usize {
|
||||
(self.offset
|
||||
+ if self.offset < 0 {
|
||||
self.len as isize
|
||||
} else {
|
||||
0
|
||||
}) as usize
|
||||
}
|
||||
|
||||
pub fn next(&mut self, n: isize) -> Option<*const AMsyncHave> {
|
||||
if self.is_stopped() {
|
||||
return None;
|
||||
}
|
||||
let slice: &[am::sync::Have] =
|
||||
unsafe { std::slice::from_raw_parts(self.ptr as *const am::sync::Have, self.len) };
|
||||
let storage = unsafe { &mut *(self.storage as *mut BTreeMap<usize, AMsyncHave>) };
|
||||
let index = self.get_index();
|
||||
let value = match storage.get_mut(&index) {
|
||||
Some(value) => value,
|
||||
None => {
|
||||
storage.insert(index, AMsyncHave::new(&slice[index]));
|
||||
storage.get_mut(&index).unwrap()
|
||||
}
|
||||
};
|
||||
self.advance(n);
|
||||
Some(value)
|
||||
}
|
||||
|
||||
pub fn is_stopped(&self) -> bool {
|
||||
let len = self.len as isize;
|
||||
self.offset < -len || self.offset == len
|
||||
}
|
||||
|
||||
pub fn prev(&mut self, n: isize) -> Option<*const AMsyncHave> {
|
||||
self.advance(-n);
|
||||
if self.is_stopped() {
|
||||
return None;
|
||||
}
|
||||
let slice: &[am::sync::Have] =
|
||||
unsafe { std::slice::from_raw_parts(self.ptr as *const am::sync::Have, self.len) };
|
||||
let storage = unsafe { &mut *(self.storage as *mut BTreeMap<usize, AMsyncHave>) };
|
||||
let index = self.get_index();
|
||||
Some(match storage.get_mut(&index) {
|
||||
Some(value) => value,
|
||||
None => {
|
||||
storage.insert(index, AMsyncHave::new(&slice[index]));
|
||||
storage.get_mut(&index).unwrap()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reversed(&self) -> Self {
|
||||
Self {
|
||||
len: self.len,
|
||||
offset: -(self.offset + 1),
|
||||
ptr: self.ptr,
|
||||
storage: self.storage,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rewound(&self) -> Self {
|
||||
Self {
|
||||
len: self.len,
|
||||
offset: if self.offset < 0 { -1 } else { 0 },
|
||||
ptr: self.ptr,
|
||||
storage: self.storage,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Detail> for [u8; USIZE_USIZE_USIZE_USIZE_] {
|
||||
fn from(detail: Detail) -> Self {
|
||||
unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
(&detail as *const Detail) as *const u8,
|
||||
USIZE_USIZE_USIZE_USIZE_,
|
||||
)
|
||||
.try_into()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// \struct AMsyncHaves
|
||||
/// \installed_headerfile
|
||||
/// \brief A random-access iterator over a sequence of synchronization haves.
|
||||
#[repr(C)]
|
||||
#[derive(Eq, PartialEq)]
|
||||
pub struct AMsyncHaves {
|
||||
/// An implementation detail that is intentionally opaque.
|
||||
/// \warning Modifying \p detail will cause undefined behavior.
|
||||
/// \note The actual size of \p detail will vary by platform, this is just
|
||||
/// the one for the platform this documentation was built on.
|
||||
detail: [u8; USIZE_USIZE_USIZE_USIZE_],
|
||||
}
|
||||
|
||||
impl AMsyncHaves {
|
||||
pub fn new(haves: &[am::sync::Have], storage: &mut BTreeMap<usize, AMsyncHave>) -> Self {
|
||||
Self {
|
||||
detail: Detail::new(haves, 0, storage).into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance(&mut self, n: isize) {
|
||||
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
|
||||
detail.advance(n);
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
detail.len
|
||||
}
|
||||
|
||||
pub fn next(&mut self, n: isize) -> Option<*const AMsyncHave> {
|
||||
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
|
||||
detail.next(n)
|
||||
}
|
||||
|
||||
pub fn prev(&mut self, n: isize) -> Option<*const AMsyncHave> {
|
||||
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
|
||||
detail.prev(n)
|
||||
}
|
||||
|
||||
pub fn reversed(&self) -> Self {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
Self {
|
||||
detail: detail.reversed().into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rewound(&self) -> Self {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
Self {
|
||||
detail: detail.rewound().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[am::sync::Have]> for AMsyncHaves {
|
||||
fn as_ref(&self) -> &[am::sync::Have] {
|
||||
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
|
||||
unsafe { std::slice::from_raw_parts(detail.ptr as *const am::sync::Have, detail.len) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AMsyncHaves {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
detail: [0; USIZE_USIZE_USIZE_USIZE_],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMsyncHaves
|
||||
/// \brief Advances an iterator over a sequence of synchronization haves by at
|
||||
/// most \p |n| positions where the sign of \p n is relative to the
|
||||
/// iterator's direction.
|
||||
///
|
||||
/// \param[in,out] sync_haves A pointer to an `AMsyncHaves` struct.
|
||||
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
|
||||
/// number of positions to advance.
|
||||
/// \pre \p sync_haves `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// sync_haves must be a valid pointer to an AMsyncHaves
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMsyncHavesAdvance(sync_haves: *mut AMsyncHaves, n: isize) {
|
||||
if let Some(sync_haves) = sync_haves.as_mut() {
|
||||
sync_haves.advance(n);
|
||||
};
|
||||
}
|
||||
|
||||
/// \memberof AMsyncHaves
|
||||
/// \brief Tests the equality of two sequences of synchronization haves
|
||||
/// underlying a pair of iterators.
|
||||
///
|
||||
/// \param[in] sync_haves1 A pointer to an `AMsyncHaves` struct.
|
||||
/// \param[in] sync_haves2 A pointer to an `AMsyncHaves` struct.
|
||||
/// \return `true` if \p sync_haves1 `==` \p sync_haves2 and `false` otherwise.
|
||||
/// \pre \p sync_haves1 `!= NULL`.
|
||||
/// \pre \p sync_haves2 `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// sync_haves1 must be a valid pointer to an AMsyncHaves
|
||||
/// sync_haves2 must be a valid pointer to an AMsyncHaves
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMsyncHavesEqual(
|
||||
sync_haves1: *const AMsyncHaves,
|
||||
sync_haves2: *const AMsyncHaves,
|
||||
) -> bool {
|
||||
match (sync_haves1.as_ref(), sync_haves2.as_ref()) {
|
||||
(Some(sync_haves1), Some(sync_haves2)) => sync_haves1.as_ref() == sync_haves2.as_ref(),
|
||||
(None, Some(_)) | (Some(_), None) | (None, None) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMsyncHaves
|
||||
/// \brief Gets the synchronization have at the current position of an iterator
|
||||
/// over a sequence of synchronization haves and then advances it by at
|
||||
/// most \p |n| positions where the sign of \p n is relative to the
|
||||
/// iterator's direction.
|
||||
///
|
||||
/// \param[in,out] sync_haves A pointer to an `AMsyncHaves` struct.
|
||||
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
|
||||
/// number of positions to advance.
|
||||
/// \return A pointer to an `AMsyncHave` struct that's `NULL` when
|
||||
/// \p sync_haves was previously advanced past its forward/reverse
|
||||
/// limit.
|
||||
/// \pre \p sync_haves `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// sync_haves must be a valid pointer to an AMsyncHaves
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMsyncHavesNext(
|
||||
sync_haves: *mut AMsyncHaves,
|
||||
n: isize,
|
||||
) -> *const AMsyncHave {
|
||||
if let Some(sync_haves) = sync_haves.as_mut() {
|
||||
if let Some(sync_have) = sync_haves.next(n) {
|
||||
return sync_have;
|
||||
}
|
||||
}
|
||||
std::ptr::null()
|
||||
}
|
||||
|
||||
/// \memberof AMsyncHaves
|
||||
/// \brief Advances an iterator over a sequence of synchronization haves by at
|
||||
/// most \p |n| positions where the sign of \p n is relative to the
|
||||
/// iterator's direction and then gets the synchronization have at its
|
||||
/// new position.
|
||||
///
|
||||
/// \param[in,out] sync_haves A pointer to an `AMsyncHaves` struct.
|
||||
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
|
||||
/// number of positions to advance.
|
||||
/// \return A pointer to an `AMsyncHave` struct that's `NULL` when
|
||||
/// \p sync_haves is presently advanced past its forward/reverse limit.
|
||||
/// \pre \p sync_haves `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// sync_haves must be a valid pointer to an AMsyncHaves
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMsyncHavesPrev(
|
||||
sync_haves: *mut AMsyncHaves,
|
||||
n: isize,
|
||||
) -> *const AMsyncHave {
|
||||
if let Some(sync_haves) = sync_haves.as_mut() {
|
||||
if let Some(sync_have) = sync_haves.prev(n) {
|
||||
return sync_have;
|
||||
}
|
||||
}
|
||||
std::ptr::null()
|
||||
}
|
||||
|
||||
/// \memberof AMsyncHaves
|
||||
/// \brief Gets the size of the sequence of synchronization haves underlying an
|
||||
/// iterator.
|
||||
///
|
||||
/// \param[in] sync_haves A pointer to an `AMsyncHaves` struct.
|
||||
/// \return The count of values in \p sync_haves.
|
||||
/// \pre \p sync_haves `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// sync_haves must be a valid pointer to an AMsyncHaves
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMsyncHavesSize(sync_haves: *const AMsyncHaves) -> usize {
|
||||
if let Some(sync_haves) = sync_haves.as_ref() {
|
||||
sync_haves.len()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMsyncHaves
|
||||
/// \brief Creates an iterator over the same sequence of synchronization haves
|
||||
/// as the given one but with the opposite position and direction.
|
||||
///
|
||||
/// \param[in] sync_haves A pointer to an `AMsyncHaves` struct.
|
||||
/// \return An `AMsyncHaves` struct
|
||||
/// \pre \p sync_haves `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// sync_haves must be a valid pointer to an AMsyncHaves
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMsyncHavesReversed(sync_haves: *const AMsyncHaves) -> AMsyncHaves {
|
||||
if let Some(sync_haves) = sync_haves.as_ref() {
|
||||
sync_haves.reversed()
|
||||
} else {
|
||||
AMsyncHaves::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// \memberof AMsyncHaves
|
||||
/// \brief Creates an iterator at the starting position over the same sequence
|
||||
/// of synchronization haves as the given one.
|
||||
///
|
||||
/// \param[in] sync_haves A pointer to an `AMsyncHaves` struct.
|
||||
/// \return An `AMsyncHaves` struct
|
||||
/// \pre \p sync_haves `!= NULL`.
|
||||
/// \internal
|
||||
///
|
||||
/// #Safety
|
||||
/// sync_haves must be a valid pointer to an AMsyncHaves
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn AMsyncHavesRewound(sync_haves: *const AMsyncHaves) -> AMsyncHaves {
|
||||
if let Some(sync_haves) = sync_haves.as_ref() {
|
||||
sync_haves.rewound()
|
||||
} else {
|
||||
AMsyncHaves::default()
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
|
||||
|
||||
find_package(cmocka REQUIRED)
|
||||
|
||||
add_executable(
|
||||
test_${LIBRARY_NAME}
|
||||
actor_id_tests.c
|
||||
doc_tests.c
|
||||
group_state.c
|
||||
list_tests.c
|
||||
macro_utils.c
|
||||
main.c
|
||||
map_tests.c
|
||||
stack_utils.c
|
||||
str_utils.c
|
||||
ported_wasm/basic_tests.c
|
||||
ported_wasm/suite.c
|
||||
ported_wasm/sync_tests.c
|
||||
)
|
||||
|
||||
set_target_properties(test_${LIBRARY_NAME} PROPERTIES LINKER_LANGUAGE C)
|
||||
|
||||
# \note An imported library's INTERFACE_INCLUDE_DIRECTORIES property can't
|
||||
# contain a non-existent path so its build-time include directory
|
||||
# must be specified for all of its dependent targets instead.
|
||||
target_include_directories(
|
||||
test_${LIBRARY_NAME}
|
||||
PRIVATE "$<BUILD_INTERFACE:${CBINDGEN_INCLUDEDIR}>"
|
||||
)
|
||||
|
||||
target_link_libraries(test_${LIBRARY_NAME} PRIVATE cmocka ${LIBRARY_NAME})
|
||||
|
||||
add_dependencies(test_${LIBRARY_NAME} ${LIBRARY_NAME}_artifacts)
|
||||
|
||||
if(BUILD_SHARED_LIBS AND WIN32)
|
||||
add_custom_command(
|
||||
TARGET test_${LIBRARY_NAME}
|
||||
POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_SHARED_LIBRARY_SUFFIX}
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
COMMENT "Copying the DLL built by Cargo into the test directory..."
|
||||
VERBATIM
|
||||
)
|
||||
endif()
|
||||
|
||||
add_test(NAME test_${LIBRARY_NAME} COMMAND test_${LIBRARY_NAME})
|
||||
|
||||
add_custom_command(
|
||||
TARGET test_${LIBRARY_NAME}
|
||||
POST_BUILD
|
||||
COMMAND
|
||||
${CMAKE_CTEST_COMMAND} --config $<CONFIG> --output-on-failure
|
||||
COMMENT
|
||||
"Running the test(s)..."
|
||||
VERBATIM
|
||||
)
|
|
@ -1,105 +0,0 @@
|
|||
#include <math.h>
|
||||
#include <setjmp.h>
|
||||
#include <stdarg.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* third-party */
|
||||
#include <cmocka.h>
|
||||
|
||||
/* local */
|
||||
#include <automerge-c/automerge.h>
|
||||
#include "str_utils.h"
|
||||
|
||||
typedef struct {
|
||||
uint8_t* src;
|
||||
char const* str;
|
||||
size_t count;
|
||||
} GroupState;
|
||||
|
||||
static int group_setup(void** state) {
|
||||
GroupState* group_state = test_calloc(1, sizeof(GroupState));
|
||||
group_state->str = "000102030405060708090a0b0c0d0e0f";
|
||||
group_state->count = strlen(group_state->str) / 2;
|
||||
group_state->src = test_malloc(group_state->count);
|
||||
hex_to_bytes(group_state->str, group_state->src, group_state->count);
|
||||
*state = group_state;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int group_teardown(void** state) {
|
||||
GroupState* group_state = *state;
|
||||
test_free(group_state->src);
|
||||
test_free(group_state);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void test_AMactorIdInit() {
|
||||
AMresult* prior_result = NULL;
|
||||
AMbyteSpan prior_bytes;
|
||||
char const* prior_str = NULL;
|
||||
AMresult* result = NULL;
|
||||
for (size_t i = 0; i != 11; ++i) {
|
||||
result = AMactorIdInit();
|
||||
if (AMresultStatus(result) != AM_STATUS_OK) {
|
||||
fail_msg("%s", AMerrorMessage(result));
|
||||
}
|
||||
assert_int_equal(AMresultSize(result), 1);
|
||||
AMvalue const value = AMresultValue(result);
|
||||
assert_int_equal(value.tag, AM_VALUE_ACTOR_ID);
|
||||
AMbyteSpan const bytes = AMactorIdBytes(value.actor_id);
|
||||
char const* const str = AMactorIdStr(value.actor_id);
|
||||
if (prior_result) {
|
||||
size_t const min_count = fmax(bytes.count, prior_bytes.count);
|
||||
assert_memory_not_equal(bytes.src, prior_bytes.src, min_count);
|
||||
assert_string_not_equal(str, prior_str);
|
||||
AMfree(prior_result);
|
||||
}
|
||||
prior_result = result;
|
||||
prior_bytes = bytes;
|
||||
prior_str = str;
|
||||
}
|
||||
AMfree(result);
|
||||
}
|
||||
|
||||
static void test_AMactorIdInitBytes(void **state) {
|
||||
GroupState* group_state = *state;
|
||||
AMresult* const result = AMactorIdInitBytes(group_state->src, group_state->count);
|
||||
if (AMresultStatus(result) != AM_STATUS_OK) {
|
||||
fail_msg("%s", AMerrorMessage(result));
|
||||
}
|
||||
assert_int_equal(AMresultSize(result), 1);
|
||||
AMvalue const value = AMresultValue(result);
|
||||
assert_int_equal(value.tag, AM_VALUE_ACTOR_ID);
|
||||
AMbyteSpan const bytes = AMactorIdBytes(value.actor_id);
|
||||
assert_int_equal(bytes.count, group_state->count);
|
||||
assert_memory_equal(bytes.src, group_state->src, bytes.count);
|
||||
AMfree(result);
|
||||
}
|
||||
|
||||
static void test_AMactorIdInitStr(void **state) {
|
||||
GroupState* group_state = *state;
|
||||
AMresult* const result = AMactorIdInitStr(group_state->str);
|
||||
if (AMresultStatus(result) != AM_STATUS_OK) {
|
||||
fail_msg("%s", AMerrorMessage(result));
|
||||
}
|
||||
assert_int_equal(AMresultSize(result), 1);
|
||||
AMvalue const value = AMresultValue(result);
|
||||
assert_int_equal(value.tag, AM_VALUE_ACTOR_ID);
|
||||
char const* const str = AMactorIdStr(value.actor_id);
|
||||
assert_int_equal(strlen(str), group_state->count * 2);
|
||||
assert_string_equal(str, group_state->str);
|
||||
AMfree(result);
|
||||
}
|
||||
|
||||
int run_actor_id_tests(void) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_AMactorIdInit),
|
||||
cmocka_unit_test(test_AMactorIdInitBytes),
|
||||
cmocka_unit_test(test_AMactorIdInitStr),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, group_setup, group_teardown);
|
||||
}
|
|
@ -1,202 +0,0 @@
|
|||
#include <setjmp.h>
|
||||
#include <stdarg.h>
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* third-party */
|
||||
#include <cmocka.h>
|
||||
|
||||
/* local */
|
||||
#include <automerge-c/automerge.h>
|
||||
#include "group_state.h"
|
||||
#include "stack_utils.h"
|
||||
#include "str_utils.h"
|
||||
|
||||
typedef struct {
|
||||
GroupState* group_state;
|
||||
char const* actor_id_str;
|
||||
uint8_t* actor_id_bytes;
|
||||
size_t actor_id_size;
|
||||
} TestState;
|
||||
|
||||
static int setup(void** state) {
|
||||
TestState* test_state = test_calloc(1, sizeof(TestState));
|
||||
group_setup((void**)&test_state->group_state);
|
||||
test_state->actor_id_str = "000102030405060708090a0b0c0d0e0f";
|
||||
test_state->actor_id_size = strlen(test_state->actor_id_str) / 2;
|
||||
test_state->actor_id_bytes = test_malloc(test_state->actor_id_size);
|
||||
hex_to_bytes(test_state->actor_id_str, test_state->actor_id_bytes, test_state->actor_id_size);
|
||||
*state = test_state;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int teardown(void** state) {
|
||||
TestState* test_state = *state;
|
||||
group_teardown((void**)&test_state->group_state);
|
||||
test_free(test_state->actor_id_bytes);
|
||||
test_free(test_state);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void test_AMkeys_empty() {
|
||||
AMresultStack* stack = NULL;
|
||||
AMdoc* const doc = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
|
||||
AMstrs forward = AMpush(&stack,
|
||||
AMkeys(doc, AM_ROOT, NULL),
|
||||
AM_VALUE_STRS,
|
||||
cmocka_cb).strs;
|
||||
assert_int_equal(AMstrsSize(&forward), 0);
|
||||
AMstrs reverse = AMstrsReversed(&forward);
|
||||
assert_int_equal(AMstrsSize(&reverse), 0);
|
||||
assert_null(AMstrsNext(&forward, 1));
|
||||
assert_null(AMstrsPrev(&forward, 1));
|
||||
assert_null(AMstrsNext(&reverse, 1));
|
||||
assert_null(AMstrsPrev(&reverse, 1));
|
||||
AMfreeStack(&stack);
|
||||
}
|
||||
|
||||
static void test_AMkeys_list() {
|
||||
AMresultStack* stack = NULL;
|
||||
AMdoc* const doc = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
|
||||
AMfree(AMlistPutInt(doc, AM_ROOT, 0, true, 1));
|
||||
AMfree(AMlistPutInt(doc, AM_ROOT, 1, true, 2));
|
||||
AMfree(AMlistPutInt(doc, AM_ROOT, 2, true, 3));
|
||||
AMstrs forward = AMpush(&stack,
|
||||
AMkeys(doc, AM_ROOT, NULL),
|
||||
AM_VALUE_STRS,
|
||||
cmocka_cb).strs;
|
||||
assert_int_equal(AMstrsSize(&forward), 3);
|
||||
AMstrs reverse = AMstrsReversed(&forward);
|
||||
assert_int_equal(AMstrsSize(&reverse), 3);
|
||||
/* Forward iterator forward. */
|
||||
char const* str = AMstrsNext(&forward, 1);
|
||||
assert_ptr_equal(strstr(str, "1@"), str);
|
||||
str = AMstrsNext(&forward, 1);
|
||||
assert_ptr_equal(strstr(str, "2@"), str);
|
||||
str = AMstrsNext(&forward, 1);
|
||||
assert_ptr_equal(strstr(str, "3@"), str);
|
||||
assert_null(AMstrsNext(&forward, 1));
|
||||
/* Forward iterator reverse. */
|
||||
str = AMstrsPrev(&forward, 1);
|
||||
assert_ptr_equal(strstr(str, "3@"), str);
|
||||
str = AMstrsPrev(&forward, 1);
|
||||
assert_ptr_equal(strstr(str, "2@"), str);
|
||||
str = AMstrsPrev(&forward, 1);
|
||||
assert_ptr_equal(strstr(str, "1@"), str);
|
||||
assert_null(AMstrsPrev(&forward, 1));
|
||||
/* Reverse iterator forward. */
|
||||
str = AMstrsNext(&reverse, 1);
|
||||
assert_ptr_equal(strstr(str, "3@"), str);
|
||||
str = AMstrsNext(&reverse, 1);
|
||||
assert_ptr_equal(strstr(str, "2@"), str);
|
||||
str = AMstrsNext(&reverse, 1);
|
||||
assert_ptr_equal(strstr(str, "1@"), str);
|
||||
/* Reverse iterator reverse. */
|
||||
assert_null(AMstrsNext(&reverse, 1));
|
||||
str = AMstrsPrev(&reverse, 1);
|
||||
assert_ptr_equal(strstr(str, "1@"), str);
|
||||
str = AMstrsPrev(&reverse, 1);
|
||||
assert_ptr_equal(strstr(str, "2@"), str);
|
||||
str = AMstrsPrev(&reverse, 1);
|
||||
assert_ptr_equal(strstr(str, "3@"), str);
|
||||
assert_null(AMstrsPrev(&reverse, 1));
|
||||
AMfreeStack(&stack);
|
||||
}
|
||||
|
||||
static void test_AMkeys_map() {
|
||||
AMresultStack* stack = NULL;
|
||||
AMdoc* const doc = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
|
||||
AMfree(AMmapPutInt(doc, AM_ROOT, "one", 1));
|
||||
AMfree(AMmapPutInt(doc, AM_ROOT, "two", 2));
|
||||
AMfree(AMmapPutInt(doc, AM_ROOT, "three", 3));
|
||||
AMstrs forward = AMpush(&stack,
|
||||
AMkeys(doc, AM_ROOT, NULL),
|
||||
AM_VALUE_STRS,
|
||||
cmocka_cb).strs;
|
||||
assert_int_equal(AMstrsSize(&forward), 3);
|
||||
AMstrs reverse = AMstrsReversed(&forward);
|
||||
assert_int_equal(AMstrsSize(&reverse), 3);
|
||||
/* Forward iterator forward. */
|
||||
assert_string_equal(AMstrsNext(&forward, 1), "one");
|
||||
assert_string_equal(AMstrsNext(&forward, 1), "three");
|
||||
assert_string_equal(AMstrsNext(&forward, 1), "two");
|
||||
assert_null(AMstrsNext(&forward, 1));
|
||||
/* Forward iterator reverse. */
|
||||
assert_string_equal(AMstrsPrev(&forward, 1), "two");
|
||||
assert_string_equal(AMstrsPrev(&forward, 1), "three");
|
||||
assert_string_equal(AMstrsPrev(&forward, 1), "one");
|
||||
assert_null(AMstrsPrev(&forward, 1));
|
||||
/* Reverse iterator forward. */
|
||||
assert_string_equal(AMstrsNext(&reverse, 1), "two");
|
||||
assert_string_equal(AMstrsNext(&reverse, 1), "three");
|
||||
assert_string_equal(AMstrsNext(&reverse, 1), "one");
|
||||
assert_null(AMstrsNext(&reverse, 1));
|
||||
/* Reverse iterator reverse. */
|
||||
assert_string_equal(AMstrsPrev(&reverse, 1), "one");
|
||||
assert_string_equal(AMstrsPrev(&reverse, 1), "three");
|
||||
assert_string_equal(AMstrsPrev(&reverse, 1), "two");
|
||||
assert_null(AMstrsPrev(&reverse, 1));
|
||||
AMfreeStack(&stack);
|
||||
}
|
||||
|
||||
static void test_AMputActor_bytes(void **state) {
|
||||
TestState* test_state = *state;
|
||||
AMactorId const* actor_id = AMpush(&test_state->group_state->stack,
|
||||
AMactorIdInitBytes(
|
||||
test_state->actor_id_bytes,
|
||||
test_state->actor_id_size),
|
||||
AM_VALUE_ACTOR_ID,
|
||||
cmocka_cb).actor_id;
|
||||
AMfree(AMsetActorId(test_state->group_state->doc, actor_id));
|
||||
actor_id = AMpush(&test_state->group_state->stack,
|
||||
AMgetActorId(test_state->group_state->doc),
|
||||
AM_VALUE_ACTOR_ID,
|
||||
cmocka_cb).actor_id;
|
||||
AMbyteSpan const bytes = AMactorIdBytes(actor_id);
|
||||
assert_int_equal(bytes.count, test_state->actor_id_size);
|
||||
assert_memory_equal(bytes.src, test_state->actor_id_bytes, bytes.count);
|
||||
}
|
||||
|
||||
static void test_AMputActor_str(void **state) {
|
||||
TestState* test_state = *state;
|
||||
AMactorId const* actor_id = AMpush(&test_state->group_state->stack,
|
||||
AMactorIdInitStr(test_state->actor_id_str),
|
||||
AM_VALUE_ACTOR_ID,
|
||||
cmocka_cb).actor_id;
|
||||
AMfree(AMsetActorId(test_state->group_state->doc, actor_id));
|
||||
actor_id = AMpush(&test_state->group_state->stack,
|
||||
AMgetActorId(test_state->group_state->doc),
|
||||
AM_VALUE_ACTOR_ID,
|
||||
cmocka_cb).actor_id;
|
||||
char const* const str = AMactorIdStr(actor_id);
|
||||
assert_int_equal(strlen(str), test_state->actor_id_size * 2);
|
||||
assert_string_equal(str, test_state->actor_id_str);
|
||||
}
|
||||
|
||||
static void test_AMspliceText() {
|
||||
AMresultStack* stack = NULL;
|
||||
AMdoc* const doc = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
|
||||
AMfree(AMspliceText(doc, AM_ROOT, 0, 0, "one + "));
|
||||
AMfree(AMspliceText(doc, AM_ROOT, 4, 2, "two = "));
|
||||
AMfree(AMspliceText(doc, AM_ROOT, 8, 2, "three"));
|
||||
char const* const text = AMpush(&stack,
|
||||
AMtext(doc, AM_ROOT, NULL),
|
||||
AM_VALUE_STR,
|
||||
cmocka_cb).str;
|
||||
assert_string_equal(text, "one two three");
|
||||
AMfreeStack(&stack);
|
||||
}
|
||||
|
||||
int run_doc_tests(void) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_AMkeys_empty),
|
||||
cmocka_unit_test(test_AMkeys_list),
|
||||
cmocka_unit_test(test_AMkeys_map),
|
||||
cmocka_unit_test_setup_teardown(test_AMputActor_bytes, setup, teardown),
|
||||
cmocka_unit_test_setup_teardown(test_AMputActor_str, setup, teardown),
|
||||
cmocka_unit_test(test_AMspliceText),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
#include <setjmp.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* third-party */
|
||||
#include <cmocka.h>
|
||||
|
||||
/* local */
|
||||
#include "group_state.h"
|
||||
#include "stack_utils.h"
|
||||
|
||||
int group_setup(void** state) {
|
||||
GroupState* group_state = test_calloc(1, sizeof(GroupState));
|
||||
group_state->doc = AMpush(&group_state->stack,
|
||||
AMcreate(NULL),
|
||||
AM_VALUE_DOC,
|
||||
cmocka_cb).doc;
|
||||
*state = group_state;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int group_teardown(void** state) {
|
||||
GroupState* group_state = *state;
|
||||
AMfreeStack(&group_state->stack);
|
||||
test_free(group_state);
|
||||
return 0;
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
#ifndef GROUP_STATE_H
|
||||
#define GROUP_STATE_H
|
||||
|
||||
/* local */
|
||||
#include <automerge-c/automerge.h>
|
||||
|
||||
typedef struct {
|
||||
AMresultStack* stack;
|
||||
AMdoc* doc;
|
||||
} GroupState;
|
||||
|
||||
int group_setup(void** state);
|
||||
|
||||
int group_teardown(void** state);
|
||||
|
||||
#endif /* GROUP_STATE_H */
|
|
@ -1,379 +0,0 @@
|
|||
#include <float.h>
|
||||
#include <limits.h>
|
||||
#include <setjmp.h>
|
||||
#include <stdarg.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
/* third-party */
|
||||
#include <cmocka.h>
|
||||
|
||||
/* local */
|
||||
#include <automerge-c/automerge.h>
|
||||
#include "group_state.h"
|
||||
#include "macro_utils.h"
|
||||
#include "stack_utils.h"
|
||||
|
||||
static void test_AMlistIncrement(void** state) {
|
||||
GroupState* group_state = *state;
|
||||
AMfree(AMlistPutCounter(group_state->doc, AM_ROOT, 0, true, 0));
|
||||
assert_int_equal(AMpush(&group_state->stack,
|
||||
AMlistGet(group_state->doc, AM_ROOT, 0, NULL),
|
||||
AM_VALUE_COUNTER,
|
||||
cmocka_cb).counter, 0);
|
||||
AMfree(AMpop(&group_state->stack));
|
||||
AMfree(AMlistIncrement(group_state->doc, AM_ROOT, 0, 3));
|
||||
assert_int_equal(AMpush(&group_state->stack,
|
||||
AMlistGet(group_state->doc, AM_ROOT, 0, NULL),
|
||||
AM_VALUE_COUNTER,
|
||||
cmocka_cb).counter, 3);
|
||||
AMfree(AMpop(&group_state->stack));
|
||||
}
|
||||
|
||||
#define test_AMlistPut(suffix, mode) test_AMlistPut ## suffix ## _ ## mode
|
||||
|
||||
#define static_void_test_AMlistPut(suffix, mode, member, scalar_value) \
|
||||
static void test_AMlistPut ## suffix ## _ ## mode(void **state) { \
|
||||
GroupState* group_state = *state; \
|
||||
AMfree(AMlistPut ## suffix(group_state->doc, \
|
||||
AM_ROOT, \
|
||||
0, \
|
||||
!strcmp(#mode, "insert"), \
|
||||
scalar_value)); \
|
||||
assert_true(AMpush( \
|
||||
&group_state->stack, \
|
||||
AMlistGet(group_state->doc, AM_ROOT, 0, NULL), \
|
||||
AMvalue_discriminant(#suffix), \
|
||||
cmocka_cb).member == scalar_value); \
|
||||
AMfree(AMpop(&group_state->stack)); \
|
||||
}
|
||||
|
||||
#define test_AMlistPutBytes(mode) test_AMlistPutBytes ## _ ## mode
|
||||
|
||||
#define static_void_test_AMlistPutBytes(mode, bytes_value) \
|
||||
static void test_AMlistPutBytes_ ## mode(void **state) { \
|
||||
static size_t const BYTES_SIZE = sizeof(bytes_value) / sizeof(uint8_t); \
|
||||
\
|
||||
GroupState* group_state = *state; \
|
||||
AMfree(AMlistPutBytes(group_state->doc, \
|
||||
AM_ROOT, \
|
||||
0, \
|
||||
!strcmp(#mode, "insert"), \
|
||||
bytes_value, \
|
||||
BYTES_SIZE)); \
|
||||
AMbyteSpan const bytes = AMpush( \
|
||||
&group_state->stack, \
|
||||
AMlistGet(group_state->doc, AM_ROOT, 0, NULL), \
|
||||
AM_VALUE_BYTES, \
|
||||
cmocka_cb).bytes; \
|
||||
assert_int_equal(bytes.count, BYTES_SIZE); \
|
||||
assert_memory_equal(bytes.src, bytes_value, BYTES_SIZE); \
|
||||
AMfree(AMpop(&group_state->stack)); \
|
||||
}
|
||||
|
||||
#define test_AMlistPutNull(mode) test_AMlistPutNull_ ## mode
|
||||
|
||||
#define static_void_test_AMlistPutNull(mode) \
|
||||
static void test_AMlistPutNull_ ## mode(void **state) { \
|
||||
GroupState* group_state = *state; \
|
||||
AMfree(AMlistPutNull(group_state->doc, \
|
||||
AM_ROOT, \
|
||||
0, \
|
||||
!strcmp(#mode, "insert"))); \
|
||||
AMresult* const result = AMlistGet(group_state->doc, AM_ROOT, 0, NULL); \
|
||||
if (AMresultStatus(result) != AM_STATUS_OK) { \
|
||||
fail_msg("%s", AMerrorMessage(result)); \
|
||||
} \
|
||||
assert_int_equal(AMresultSize(result), 1); \
|
||||
assert_int_equal(AMresultValue(result).tag, AM_VALUE_NULL); \
|
||||
AMfree(result); \
|
||||
}
|
||||
|
||||
#define test_AMlistPutObject(label, mode) test_AMlistPutObject_ ## label ## _ ## mode
|
||||
|
||||
#define static_void_test_AMlistPutObject(label, mode) \
|
||||
static void test_AMlistPutObject_ ## label ## _ ## mode(void **state) { \
|
||||
GroupState* group_state = *state; \
|
||||
AMobjId const* const obj_id = AMpush( \
|
||||
&group_state->stack, \
|
||||
AMlistPutObject(group_state->doc, \
|
||||
AM_ROOT, \
|
||||
0, \
|
||||
!strcmp(#mode, "insert"), \
|
||||
AMobjType_tag(#label)), \
|
||||
AM_VALUE_OBJ_ID, \
|
||||
cmocka_cb).obj_id; \
|
||||
assert_non_null(obj_id); \
|
||||
assert_int_equal(AMobjSize(group_state->doc, obj_id, NULL), 0); \
|
||||
AMfree(AMpop(&group_state->stack)); \
|
||||
}
|
||||
|
||||
#define test_AMlistPutStr(mode) test_AMlistPutStr ## _ ## mode
|
||||
|
||||
#define static_void_test_AMlistPutStr(mode, str_value) \
|
||||
static void test_AMlistPutStr_ ## mode(void **state) { \
|
||||
GroupState* group_state = *state; \
|
||||
AMfree(AMlistPutStr(group_state->doc, \
|
||||
AM_ROOT, \
|
||||
0, \
|
||||
!strcmp(#mode, "insert"), \
|
||||
str_value)); \
|
||||
assert_string_equal(AMpush( \
|
||||
&group_state->stack, \
|
||||
AMlistGet(group_state->doc, AM_ROOT, 0, NULL), \
|
||||
AM_VALUE_STR, \
|
||||
cmocka_cb).str, str_value); \
|
||||
AMfree(AMpop(&group_state->stack)); \
|
||||
}
|
||||
|
||||
static_void_test_AMlistPut(Bool, insert, boolean, true)
|
||||
|
||||
static_void_test_AMlistPut(Bool, update, boolean, true)
|
||||
|
||||
static uint8_t const BYTES_VALUE[] = {INT8_MIN, INT8_MAX / 2, INT8_MAX};
|
||||
|
||||
static_void_test_AMlistPutBytes(insert, BYTES_VALUE)
|
||||
|
||||
static_void_test_AMlistPutBytes(update, BYTES_VALUE)
|
||||
|
||||
static_void_test_AMlistPut(Counter, insert, counter, INT64_MAX)
|
||||
|
||||
static_void_test_AMlistPut(Counter, update, counter, INT64_MAX)
|
||||
|
||||
static_void_test_AMlistPut(F64, insert, f64, DBL_MAX)
|
||||
|
||||
static_void_test_AMlistPut(F64, update, f64, DBL_MAX)
|
||||
|
||||
static_void_test_AMlistPut(Int, insert, int_, INT64_MAX)
|
||||
|
||||
static_void_test_AMlistPut(Int, update, int_, INT64_MAX)
|
||||
|
||||
static_void_test_AMlistPutNull(insert)
|
||||
|
||||
static_void_test_AMlistPutNull(update)
|
||||
|
||||
static_void_test_AMlistPutObject(List, insert)
|
||||
|
||||
static_void_test_AMlistPutObject(List, update)
|
||||
|
||||
static_void_test_AMlistPutObject(Map, insert)
|
||||
|
||||
static_void_test_AMlistPutObject(Map, update)
|
||||
|
||||
static_void_test_AMlistPutObject(Text, insert)
|
||||
|
||||
static_void_test_AMlistPutObject(Text, update)
|
||||
|
||||
static_void_test_AMlistPutStr(insert, "Hello, world!")
|
||||
|
||||
static_void_test_AMlistPutStr(update, "Hello, world!")
|
||||
|
||||
static_void_test_AMlistPut(Timestamp, insert, timestamp, INT64_MAX)
|
||||
|
||||
static_void_test_AMlistPut(Timestamp, update, timestamp, INT64_MAX)
|
||||
|
||||
static_void_test_AMlistPut(Uint, insert, uint, UINT64_MAX)
|
||||
|
||||
static_void_test_AMlistPut(Uint, update, uint, UINT64_MAX)
|
||||
|
||||
static void test_insert_at_index(void** state) {
|
||||
AMresultStack* stack = *state;
|
||||
AMdoc* const doc = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
|
||||
|
||||
AMobjId const* const list = AMpush(
|
||||
&stack,
|
||||
AMlistPutObject(doc, AM_ROOT, 0, true, AM_OBJ_TYPE_LIST),
|
||||
AM_VALUE_OBJ_ID,
|
||||
cmocka_cb).obj_id;
|
||||
/* Insert both at the same index. */
|
||||
AMfree(AMlistPutUint(doc, list, 0, true, 0));
|
||||
AMfree(AMlistPutUint(doc, list, 0, true, 1));
|
||||
|
||||
assert_int_equal(AMobjSize(doc, list, NULL), 2);
|
||||
AMstrs const keys = AMpush(&stack,
|
||||
AMkeys(doc, list, NULL),
|
||||
AM_VALUE_STRS,
|
||||
cmocka_cb).strs;
|
||||
assert_int_equal(AMstrsSize(&keys), 2);
|
||||
AMlistItems const range = AMpush(&stack,
|
||||
AMlistRange(doc, list, 0, SIZE_MAX, NULL),
|
||||
AM_VALUE_LIST_ITEMS,
|
||||
cmocka_cb).list_items;
|
||||
assert_int_equal(AMlistItemsSize(&range), 2);
|
||||
}
|
||||
|
||||
static void test_get_list_values(void** state) {
|
||||
AMresultStack* stack = *state;
|
||||
AMdoc* const doc1 = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
|
||||
AMobjId const* const list = AMpush(
|
||||
&stack,
|
||||
AMmapPutObject(doc1, AM_ROOT, "list", AM_OBJ_TYPE_LIST),
|
||||
AM_VALUE_OBJ_ID,
|
||||
cmocka_cb).obj_id;
|
||||
|
||||
/* Insert elements. */
|
||||
AMfree(AMlistPutStr(doc1, list, 0, true, "First"));
|
||||
AMfree(AMlistPutStr(doc1, list, 0, true, "Second"));
|
||||
AMfree(AMlistPutStr(doc1, list, 0, true, "Third"));
|
||||
AMfree(AMlistPutStr(doc1, list, 0, true, "Fourth"));
|
||||
AMfree(AMlistPutStr(doc1, list, 0, true, "Fifth"));
|
||||
AMfree(AMlistPutStr(doc1, list, 0, true, "Sixth"));
|
||||
AMfree(AMlistPutStr(doc1, list, 0, true, "Seventh"));
|
||||
AMfree(AMlistPutStr(doc1, list, 0, true, "Eighth"));
|
||||
AMfree(AMcommit(doc1, NULL, NULL));
|
||||
|
||||
AMchangeHashes const v1 = AMpush(&stack,
|
||||
AMgetHeads(doc1),
|
||||
AM_VALUE_CHANGE_HASHES,
|
||||
cmocka_cb).change_hashes;
|
||||
AMdoc* const doc2 = AMpush(&stack,
|
||||
AMfork(doc1, NULL),
|
||||
AM_VALUE_DOC,
|
||||
cmocka_cb).doc;
|
||||
|
||||
AMfree(AMlistPutStr(doc1, list, 2, false, "Third V2"));
|
||||
AMfree(AMcommit(doc1, NULL, NULL));
|
||||
|
||||
AMfree(AMlistPutStr(doc2, list, 2, false, "Third V3"));
|
||||
AMfree(AMcommit(doc2, NULL, NULL));
|
||||
|
||||
AMfree(AMmerge(doc1, doc2));
|
||||
|
||||
AMlistItems range = AMpush(&stack,
|
||||
AMlistRange(doc1, list, 0, SIZE_MAX, NULL),
|
||||
AM_VALUE_LIST_ITEMS,
|
||||
cmocka_cb).list_items;
|
||||
assert_int_equal(AMlistItemsSize(&range), 8);
|
||||
|
||||
AMlistItem const* list_item = NULL;
|
||||
while ((list_item = AMlistItemsNext(&range, 1)) != NULL) {
|
||||
AMvalue const val1 = AMlistItemValue(list_item);
|
||||
AMresult* result = AMlistGet(doc1, list, AMlistItemIndex(list_item), NULL);
|
||||
AMvalue const val2 = AMresultValue(result);
|
||||
assert_true(AMvalueEqual(&val1, &val2));
|
||||
assert_non_null(AMlistItemObjId(list_item));
|
||||
AMfree(result);
|
||||
}
|
||||
|
||||
range = AMpush(&stack,
|
||||
AMlistRange(doc1, list, 3, 6, NULL),
|
||||
AM_VALUE_LIST_ITEMS,
|
||||
cmocka_cb).list_items;
|
||||
AMlistItems range_back = AMlistItemsReversed(&range);
|
||||
assert_int_equal(AMlistItemsSize(&range), 3);
|
||||
assert_int_equal(AMlistItemIndex(AMlistItemsNext(&range, 1)), 3);
|
||||
assert_int_equal(AMlistItemIndex(AMlistItemsNext(&range_back, 1)), 5);
|
||||
|
||||
range = AMlistItemsRewound(&range);
|
||||
while ((list_item = AMlistItemsNext(&range, 1)) != NULL) {
|
||||
AMvalue const val1 = AMlistItemValue(list_item);
|
||||
AMresult* result = AMlistGet(doc1, list, AMlistItemIndex(list_item), NULL);
|
||||
AMvalue const val2 = AMresultValue(result);
|
||||
assert_true(AMvalueEqual(&val1, &val2));
|
||||
assert_non_null(AMlistItemObjId(list_item));
|
||||
AMfree(result);
|
||||
}
|
||||
|
||||
range = AMpush(&stack,
|
||||
AMlistRange(doc1, list, 0, SIZE_MAX, &v1),
|
||||
AM_VALUE_LIST_ITEMS,
|
||||
cmocka_cb).list_items;
|
||||
assert_int_equal(AMlistItemsSize(&range), 8);
|
||||
while ((list_item = AMlistItemsNext(&range, 1)) != NULL) {
|
||||
AMvalue const val1 = AMlistItemValue(list_item);
|
||||
AMresult* result = AMlistGet(doc1, list, AMlistItemIndex(list_item), &v1);
|
||||
AMvalue const val2 = AMresultValue(result);
|
||||
assert_true(AMvalueEqual(&val1, &val2));
|
||||
assert_non_null(AMlistItemObjId(list_item));
|
||||
AMfree(result);
|
||||
}
|
||||
|
||||
range = AMpush(&stack,
|
||||
AMlistRange(doc1, list, 3, 6, &v1),
|
||||
AM_VALUE_LIST_ITEMS,
|
||||
cmocka_cb).list_items;
|
||||
range_back = AMlistItemsReversed(&range);
|
||||
assert_int_equal(AMlistItemsSize(&range), 3);
|
||||
assert_int_equal(AMlistItemIndex(AMlistItemsNext(&range, 1)), 3);
|
||||
assert_int_equal(AMlistItemIndex(AMlistItemsNext(&range_back, 1)), 5);
|
||||
|
||||
range = AMlistItemsRewound(&range);
|
||||
while ((list_item = AMlistItemsNext(&range, 1)) != NULL) {
|
||||
AMvalue const val1 = AMlistItemValue(list_item);
|
||||
AMresult* result = AMlistGet(doc1, list, AMlistItemIndex(list_item), &v1);
|
||||
AMvalue const val2 = AMresultValue(result);
|
||||
assert_true(AMvalueEqual(&val1, &val2));
|
||||
assert_non_null(AMlistItemObjId(list_item));
|
||||
AMfree(result);
|
||||
}
|
||||
|
||||
range = AMpush(&stack,
|
||||
AMlistRange(doc1, list, 0, SIZE_MAX, NULL),
|
||||
AM_VALUE_LIST_ITEMS,
|
||||
cmocka_cb).list_items;
|
||||
AMobjItems values = AMpush(&stack,
|
||||
AMobjValues(doc1, list, NULL),
|
||||
AM_VALUE_OBJ_ITEMS,
|
||||
cmocka_cb).obj_items;
|
||||
assert_int_equal(AMlistItemsSize(&range), AMobjItemsSize(&values));
|
||||
AMobjItem const* value = NULL;
|
||||
while ((list_item = AMlistItemsNext(&range, 1)) != NULL &&
|
||||
(value = AMobjItemsNext(&values, 1)) != NULL) {
|
||||
AMvalue const val1 = AMlistItemValue(list_item);
|
||||
AMvalue const val2 = AMobjItemValue(value);
|
||||
assert_true(AMvalueEqual(&val1, &val2));
|
||||
assert_true(AMobjIdEqual(AMlistItemObjId(list_item), AMobjItemObjId(value)));
|
||||
}
|
||||
|
||||
range = AMpush(&stack,
|
||||
AMlistRange(doc1, list, 0, SIZE_MAX, &v1),
|
||||
AM_VALUE_LIST_ITEMS,
|
||||
cmocka_cb).list_items;
|
||||
values = AMpush(&stack,
|
||||
AMobjValues(doc1, list, &v1),
|
||||
AM_VALUE_OBJ_ITEMS,
|
||||
cmocka_cb).obj_items;
|
||||
assert_int_equal(AMlistItemsSize(&range), AMobjItemsSize(&values));
|
||||
while ((list_item = AMlistItemsNext(&range, 1)) != NULL &&
|
||||
(value = AMobjItemsNext(&values, 1)) != NULL) {
|
||||
AMvalue const val1 = AMlistItemValue(list_item);
|
||||
AMvalue const val2 = AMobjItemValue(value);
|
||||
assert_true(AMvalueEqual(&val1, &val2));
|
||||
assert_true(AMobjIdEqual(AMlistItemObjId(list_item), AMobjItemObjId(value)));
|
||||
}
|
||||
}
|
||||
|
||||
int run_list_tests(void) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_AMlistIncrement),
|
||||
cmocka_unit_test(test_AMlistPut(Bool, insert)),
|
||||
cmocka_unit_test(test_AMlistPut(Bool, update)),
|
||||
cmocka_unit_test(test_AMlistPutBytes(insert)),
|
||||
cmocka_unit_test(test_AMlistPutBytes(update)),
|
||||
cmocka_unit_test(test_AMlistPut(Counter, insert)),
|
||||
cmocka_unit_test(test_AMlistPut(Counter, update)),
|
||||
cmocka_unit_test(test_AMlistPut(F64, insert)),
|
||||
cmocka_unit_test(test_AMlistPut(F64, update)),
|
||||
cmocka_unit_test(test_AMlistPut(Int, insert)),
|
||||
cmocka_unit_test(test_AMlistPut(Int, update)),
|
||||
cmocka_unit_test(test_AMlistPutNull(insert)),
|
||||
cmocka_unit_test(test_AMlistPutNull(update)),
|
||||
cmocka_unit_test(test_AMlistPutObject(List, insert)),
|
||||
cmocka_unit_test(test_AMlistPutObject(List, update)),
|
||||
cmocka_unit_test(test_AMlistPutObject(Map, insert)),
|
||||
cmocka_unit_test(test_AMlistPutObject(Map, update)),
|
||||
cmocka_unit_test(test_AMlistPutObject(Text, insert)),
|
||||
cmocka_unit_test(test_AMlistPutObject(Text, update)),
|
||||
cmocka_unit_test(test_AMlistPutStr(insert)),
|
||||
cmocka_unit_test(test_AMlistPutStr(update)),
|
||||
cmocka_unit_test(test_AMlistPut(Timestamp, insert)),
|
||||
cmocka_unit_test(test_AMlistPut(Timestamp, update)),
|
||||
cmocka_unit_test(test_AMlistPut(Uint, insert)),
|
||||
cmocka_unit_test(test_AMlistPut(Uint, update)),
|
||||
cmocka_unit_test_setup_teardown(test_insert_at_index, setup_stack, teardown_stack),
|
||||
cmocka_unit_test_setup_teardown(test_get_list_values, setup_stack, teardown_stack),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, group_setup, group_teardown);
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
#include <string.h>
|
||||
|
||||
/* local */
|
||||
#include "macro_utils.h"
|
||||
|
||||
AMvalueVariant AMvalue_discriminant(char const* suffix) {
|
||||
if (!strcmp(suffix, "Bool")) return AM_VALUE_BOOLEAN;
|
||||
else if (!strcmp(suffix, "Bytes")) return AM_VALUE_BYTES;
|
||||
else if (!strcmp(suffix, "Counter")) return AM_VALUE_COUNTER;
|
||||
else if (!strcmp(suffix, "F64")) return AM_VALUE_F64;
|
||||
else if (!strcmp(suffix, "Int")) return AM_VALUE_INT;
|
||||
else if (!strcmp(suffix, "Null")) return AM_VALUE_NULL;
|
||||
else if (!strcmp(suffix, "Str")) return AM_VALUE_STR;
|
||||
else if (!strcmp(suffix, "Timestamp")) return AM_VALUE_TIMESTAMP;
|
||||
else if (!strcmp(suffix, "Uint")) return AM_VALUE_UINT;
|
||||
else return AM_VALUE_VOID;
|
||||
}
|
||||
|
||||
AMobjType AMobjType_tag(char const* obj_type_label) {
|
||||
if (!strcmp(obj_type_label, "List")) return AM_OBJ_TYPE_LIST;
|
||||
else if (!strcmp(obj_type_label, "Map")) return AM_OBJ_TYPE_MAP;
|
||||
else if (!strcmp(obj_type_label, "Text")) return AM_OBJ_TYPE_TEXT;
|
||||
else return 0;
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
#ifndef MACRO_UTILS_H
|
||||
#define MACRO_UTILS_H
|
||||
|
||||
/* local */
|
||||
#include <automerge-c/automerge.h>
|
||||
|
||||
/**
|
||||
* \brief Gets the result value discriminant corresponding to a function name
|
||||
* suffix.
|
||||
*
|
||||
* \param[in] suffix A string.
|
||||
* \return An `AMvalue` struct discriminant.
|
||||
*/
|
||||
AMvalueVariant AMvalue_discriminant(char const* suffix);
|
||||
|
||||
/**
|
||||
* \brief Gets the object type tag corresponding to an object type label.
|
||||
*
|
||||
* \param[in] obj_type_label A string.
|
||||
* \return An `AMobjType` enum tag.
|
||||
*/
|
||||
AMobjType AMobjType_tag(char const* obj_type_label);
|
||||
|
||||
#endif /* MACRO_UTILS_H */
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,30 +0,0 @@
|
|||
#include <setjmp.h>
|
||||
#include <stdarg.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* third-party */
|
||||
#include <cmocka.h>
|
||||
|
||||
/* local */
|
||||
#include "stack_utils.h"
|
||||
|
||||
void cmocka_cb(AMresultStack** stack, uint8_t discriminant) {
|
||||
assert_non_null(stack);
|
||||
assert_non_null(*stack);
|
||||
assert_non_null((*stack)->result);
|
||||
if (AMresultStatus((*stack)->result) != AM_STATUS_OK) {
|
||||
fail_msg("%s", AMerrorMessage((*stack)->result));
|
||||
}
|
||||
assert_int_equal(AMresultValue((*stack)->result).tag, discriminant);
|
||||
}
|
||||
|
||||
int setup_stack(void** state) {
|
||||
*state = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int teardown_stack(void** state) {
|
||||
AMresultStack* stack = *state;
|
||||
AMfreeStack(&stack);
|
||||
return 0;
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
#ifndef STACK_UTILS_H
|
||||
#define STACK_UTILS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* local */
|
||||
#include <automerge-c/automerge.h>
|
||||
|
||||
/**
|
||||
* \brief Reports an error through a cmocka assertion.
|
||||
*
|
||||
* \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
|
||||
* \param[in] discriminant An `AMvalueVariant` enum tag.
|
||||
* \pre \p stack` != NULL`.
|
||||
*/
|
||||
void cmocka_cb(AMresultStack** stack, uint8_t discriminant);
|
||||
|
||||
/**
|
||||
* \brief Allocates a result stack for storing the results allocated during one
|
||||
* or more test cases.
|
||||
*
|
||||
* \param[in,out] state A pointer to a pointer to an `AMresultStack` struct.
|
||||
* \pre \p state` != NULL`.
|
||||
* \warning The `AMresultStack` struct returned through \p state must be
|
||||
* deallocated with `teardown_stack()` in order to prevent memory leaks.
|
||||
*/
|
||||
int setup_stack(void** state);
|
||||
|
||||
/**
|
||||
* \brief Deallocates a result stack after deallocating any results that were
|
||||
* stored in it by one or more test cases.
|
||||
*
|
||||
* \param[in] state A pointer to a pointer to an `AMresultStack` struct.
|
||||
* \pre \p state` != NULL`.
|
||||
*/
|
||||
int teardown_stack(void** state);
|
||||
|
||||
#endif /* STACK_UTILS_H */
|
|
@ -1,14 +0,0 @@
|
|||
#ifndef STR_UTILS_H
|
||||
#define STR_UTILS_H
|
||||
|
||||
/**
|
||||
* \brief Converts a hexadecimal string into a sequence of bytes.
|
||||
*
|
||||
* \param[in] hex_str A string.
|
||||
* \param[in] src A pointer to a contiguous sequence of bytes.
|
||||
* \param[in] count The number of bytes to copy to \p src.
|
||||
* \pre \p count `<=` length of \p src.
|
||||
*/
|
||||
void hex_to_bytes(char const* hex_str, uint8_t* src, size_t const count);
|
||||
|
||||
#endif /* STR_UTILS_H */
|
857
automerge-cli/Cargo.lock
generated
857
automerge-cli/Cargo.lock
generated
|
@ -1,857 +0,0 @@
|
|||
# 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"
|
|
@ -1,18 +0,0 @@
|
|||
## Automerge
|
||||
|
||||
Automerge is a library of data structures for building collaborative
|
||||
applications, this package is the javascript implementation.
|
||||
|
||||
Please see [automerge.org](http://automerge.org/) for documentation.
|
||||
|
||||
## Setup
|
||||
|
||||
This package is a wrapper around a core library which is written in rust and
|
||||
compiled to WASM. In `node` this should be transparent to you, but in the
|
||||
browser you will need a bundler to include the WASM blob as part of your module
|
||||
hierarchy. There are examples of doing this with common bundlers in `./examples`.
|
||||
|
||||
## Meta
|
||||
|
||||
Copyright 2017–2021, the Automerge contributors. Released under the terms of the
|
||||
MIT license (see `LICENSE`).
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../dist/cjs"
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "es6",
|
||||
"outDir": "../dist/mjs"
|
||||
}
|
||||
}
|
|
@ -1,438 +0,0 @@
|
|||
import {once} from "events"
|
||||
import {setTimeout} from "timers/promises"
|
||||
import {spawn, ChildProcess} from "child_process"
|
||||
import * as child_process from "child_process"
|
||||
import {command, subcommands, run, array, multioption, option, Type} from "cmd-ts"
|
||||
import * as path from "path"
|
||||
import * as fsPromises from "fs/promises"
|
||||
import fetch from "node-fetch"
|
||||
|
||||
const VERDACCIO_DB_PATH = path.normalize(`${__dirname}/verdacciodb`)
|
||||
const VERDACCIO_CONFIG_PATH = path.normalize(`${__dirname}/verdaccio.yaml`)
|
||||
const AUTOMERGE_WASM_PATH = path.normalize(`${__dirname}/../../automerge-wasm`)
|
||||
const AUTOMERGE_JS_PATH = path.normalize(`${__dirname}/..`)
|
||||
const EXAMPLES_DIR = path.normalize(path.join(__dirname, "../", "examples"))
|
||||
|
||||
// The different example projects in "../examples"
|
||||
type Example = "webpack" | "vite" | "create-react-app"
|
||||
|
||||
// Type to parse strings to `Example` so the types line up for the `buildExamples` commmand
|
||||
const ReadExample: Type<string, Example> = {
|
||||
async from(str) {
|
||||
if (str === "webpack") {
|
||||
return "webpack"
|
||||
} else if (str === "vite") {
|
||||
return "vite"
|
||||
} else if (str === "create-react-app") {
|
||||
return "create-react-app"
|
||||
} else {
|
||||
throw new Error(`Unknown example type ${str}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Profile = "dev" | "release"
|
||||
|
||||
const ReadProfile: Type<string, Profile> = {
|
||||
async from(str) {
|
||||
if (str === "dev") {
|
||||
return "dev"
|
||||
} else if (str === "release") {
|
||||
return "release"
|
||||
} else {
|
||||
throw new Error(`Unknown profile ${str}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const buildjs = command({
|
||||
name: "buildjs",
|
||||
args: {
|
||||
profile: option({
|
||||
type: ReadProfile,
|
||||
long: "profile",
|
||||
short: "p",
|
||||
defaultValue: () => "dev" as Profile
|
||||
})
|
||||
},
|
||||
handler: ({profile}) => {
|
||||
console.log("building js")
|
||||
withPublishedWasm(profile, async (registryUrl: string) => {
|
||||
await buildAndPublishAutomergeJs(registryUrl)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const buildWasm = command({
|
||||
name: "buildwasm",
|
||||
args: {
|
||||
profile: option({
|
||||
type: ReadProfile,
|
||||
long: "profile",
|
||||
short: "p",
|
||||
defaultValue: () => "dev" as Profile
|
||||
})
|
||||
},
|
||||
handler: ({profile}) => {
|
||||
console.log("building automerge-wasm")
|
||||
withRegistry(
|
||||
buildAutomergeWasm(profile),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const buildexamples = command({
|
||||
name: "buildexamples",
|
||||
args: {
|
||||
examples: multioption({
|
||||
long: "example",
|
||||
short: "e",
|
||||
type: array(ReadExample),
|
||||
}),
|
||||
profile: option({
|
||||
type: ReadProfile,
|
||||
long: "profile",
|
||||
short: "p",
|
||||
defaultValue: () => "dev" as Profile
|
||||
})
|
||||
},
|
||||
handler: ({examples, profile}) => {
|
||||
if (examples.length === 0) {
|
||||
examples = ["webpack", "vite", "create-react-app"]
|
||||
}
|
||||
buildExamples(examples, profile)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const runRegistry = command({
|
||||
name: "run-registry",
|
||||
args: {
|
||||
profile: option({
|
||||
type: ReadProfile,
|
||||
long: "profile",
|
||||
short: "p",
|
||||
defaultValue: () => "dev" as Profile
|
||||
})
|
||||
},
|
||||
handler: ({profile}) => {
|
||||
withPublishedWasm(profile, async (registryUrl: string) => {
|
||||
await buildAndPublishAutomergeJs(registryUrl)
|
||||
console.log("\n************************")
|
||||
console.log(` Verdaccio NPM registry is running at ${registryUrl}`)
|
||||
console.log(" press CTRL-C to exit ")
|
||||
console.log("************************")
|
||||
await once(process, "SIGINT")
|
||||
}).catch(e => {
|
||||
console.error(`Failed: ${e}`)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const app = subcommands({
|
||||
name: "e2e",
|
||||
cmds: {buildjs, buildexamples, buildwasm: buildWasm, "run-registry": runRegistry}
|
||||
})
|
||||
|
||||
run(app, process.argv.slice(2))
|
||||
|
||||
async function buildExamples(examples: Array<Example>, profile: Profile) {
|
||||
await withPublishedWasm(profile, async (registryUrl) => {
|
||||
printHeader("building and publishing automerge")
|
||||
await buildAndPublishAutomergeJs(registryUrl)
|
||||
for (const example of examples) {
|
||||
printHeader(`building ${example} example`)
|
||||
if (example === "webpack") {
|
||||
const projectPath = path.join(EXAMPLES_DIR, example)
|
||||
await removeExistingAutomerge(projectPath)
|
||||
await fsPromises.rm(path.join(projectPath, "yarn.lock"), {force: true})
|
||||
await spawnAndWait("yarn", ["--cwd", projectPath, "install", "--registry", registryUrl, "--check-files"], {stdio: "inherit"})
|
||||
await spawnAndWait("yarn", ["--cwd", projectPath, "build"], {stdio: "inherit"})
|
||||
} else if (example === "vite") {
|
||||
const projectPath = path.join(EXAMPLES_DIR, example)
|
||||
await removeExistingAutomerge(projectPath)
|
||||
await fsPromises.rm(path.join(projectPath, "yarn.lock"), {force: true})
|
||||
await spawnAndWait("yarn", ["--cwd", projectPath, "install", "--registry", registryUrl, "--check-files"], {stdio: "inherit"})
|
||||
await spawnAndWait("yarn", ["--cwd", projectPath, "build"], {stdio: "inherit"})
|
||||
} else if (example === "create-react-app") {
|
||||
const projectPath = path.join(EXAMPLES_DIR, example)
|
||||
await removeExistingAutomerge(projectPath)
|
||||
await fsPromises.rm(path.join(projectPath, "yarn.lock"), {force: true})
|
||||
await spawnAndWait("yarn", ["--cwd", projectPath, "install", "--registry", registryUrl, "--check-files"], {stdio: "inherit"})
|
||||
await spawnAndWait("yarn", ["--cwd", projectPath, "build"], {stdio: "inherit"})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type WithRegistryAction = (registryUrl: string) => Promise<void>
|
||||
|
||||
async function withRegistry(action: WithRegistryAction, ...actions: Array<WithRegistryAction>) {
|
||||
// First, start verdaccio
|
||||
printHeader("Starting verdaccio NPM server")
|
||||
const verd = await VerdaccioProcess.start()
|
||||
actions.unshift(action)
|
||||
|
||||
for (const action of actions) {
|
||||
try {
|
||||
type Step = "verd-died" | "action-completed"
|
||||
const verdDied: () => Promise<Step> = async () => {
|
||||
await verd.died()
|
||||
return "verd-died"
|
||||
}
|
||||
const actionComplete: () => Promise<Step> = async () => {
|
||||
await action("http://localhost:4873")
|
||||
return "action-completed"
|
||||
}
|
||||
const result = await Promise.race([verdDied(), actionComplete()])
|
||||
if (result === "verd-died") {
|
||||
throw new Error("verdaccio unexpectedly exited")
|
||||
}
|
||||
} catch(e) {
|
||||
await verd.kill()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
await verd.kill()
|
||||
}
|
||||
|
||||
async function withPublishedWasm(profile: Profile, action: WithRegistryAction) {
|
||||
await withRegistry(
|
||||
buildAutomergeWasm(profile),
|
||||
publishAutomergeWasm,
|
||||
action
|
||||
)
|
||||
}
|
||||
|
||||
function buildAutomergeWasm(profile: Profile): WithRegistryAction {
|
||||
return async (registryUrl: string) => {
|
||||
printHeader("building automerge-wasm")
|
||||
await spawnAndWait("yarn", ["--cwd", AUTOMERGE_WASM_PATH, "--registry", registryUrl, "install"], {stdio: "inherit"})
|
||||
const cmd = profile === "release" ? "release" : "debug"
|
||||
await spawnAndWait("yarn", ["--cwd", AUTOMERGE_WASM_PATH, cmd], {stdio: "inherit"})
|
||||
}
|
||||
}
|
||||
|
||||
async function publishAutomergeWasm(registryUrl: string) {
|
||||
printHeader("Publishing automerge-wasm to verdaccio")
|
||||
await fsPromises.rm(path.join(VERDACCIO_DB_PATH, "@automerge/automerge-wasm"), { recursive: true, force: true} )
|
||||
await yarnPublish(registryUrl, AUTOMERGE_WASM_PATH)
|
||||
}
|
||||
|
||||
async function buildAndPublishAutomergeJs(registryUrl: string) {
|
||||
// Build the js package
|
||||
printHeader("Building automerge")
|
||||
await removeExistingAutomerge(AUTOMERGE_JS_PATH)
|
||||
await removeFromVerdaccio("@automerge/automerge")
|
||||
await fsPromises.rm(path.join(AUTOMERGE_JS_PATH, "yarn.lock"), {force: true})
|
||||
await spawnAndWait("yarn", ["--cwd", AUTOMERGE_JS_PATH, "install", "--registry", registryUrl, "--check-files"], {stdio: "inherit"})
|
||||
await spawnAndWait("yarn", ["--cwd", AUTOMERGE_JS_PATH, "build"], {stdio: "inherit"})
|
||||
await yarnPublish(registryUrl, AUTOMERGE_JS_PATH)
|
||||
}
|
||||
|
||||
/**
|
||||
* A running verdaccio process
|
||||
*
|
||||
*/
|
||||
class VerdaccioProcess {
|
||||
child: ChildProcess
|
||||
stdout: Array<Buffer>
|
||||
stderr: Array<Buffer>
|
||||
|
||||
constructor(child: ChildProcess) {
|
||||
this.child = child
|
||||
|
||||
// Collect stdout/stderr otherwise the subprocess gets blocked writing
|
||||
this.stdout = []
|
||||
this.stderr = []
|
||||
this.child.stdout && this.child.stdout.on("data", (data) => this.stdout.push(data))
|
||||
this.child.stderr && this.child.stderr.on("data", (data) => this.stderr.push(data))
|
||||
|
||||
const errCallback = (e: any) => {
|
||||
console.error("!!!!!!!!!ERROR IN VERDACCIO PROCESS!!!!!!!!!")
|
||||
console.error(" ", e)
|
||||
if (this.stdout.length > 0) {
|
||||
console.log("\n**Verdaccio stdout**")
|
||||
const stdout = Buffer.concat(this.stdout)
|
||||
process.stdout.write(stdout)
|
||||
}
|
||||
|
||||
if (this.stderr.length > 0) {
|
||||
console.log("\n**Verdaccio stderr**")
|
||||
const stdout = Buffer.concat(this.stderr)
|
||||
process.stdout.write(stdout)
|
||||
}
|
||||
process.exit(-1)
|
||||
}
|
||||
this.child.on("error", errCallback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a verdaccio process and wait for it to respond succesfully to http requests
|
||||
*
|
||||
* The returned `VerdaccioProcess` can be used to control the subprocess
|
||||
*/
|
||||
static async start() {
|
||||
const child = spawn("yarn", ["verdaccio", "--config", VERDACCIO_CONFIG_PATH], {env: { ...process.env, FORCE_COLOR: "true"}})
|
||||
|
||||
// Forward stdout and stderr whilst waiting for startup to complete
|
||||
const stdoutCallback = (data: Buffer) => process.stdout.write(data)
|
||||
const stderrCallback = (data: Buffer) => process.stderr.write(data)
|
||||
child.stdout && child.stdout.on("data", stdoutCallback)
|
||||
child.stderr && child.stderr.on("data", stderrCallback)
|
||||
|
||||
const healthCheck = async () => {
|
||||
while (true) {
|
||||
try {
|
||||
const resp = await fetch("http://localhost:4873")
|
||||
if (resp.status === 200) {
|
||||
return
|
||||
} else {
|
||||
console.log(`Healthcheck failed: bad status ${resp.status}`)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Healthcheck failed: ${e}`)
|
||||
}
|
||||
await setTimeout(500)
|
||||
}
|
||||
}
|
||||
await withTimeout(healthCheck(), 10000)
|
||||
|
||||
// Stop forwarding stdout/stderr
|
||||
child.stdout && child.stdout.off("data", stdoutCallback)
|
||||
child.stderr && child.stderr.off("data", stderrCallback)
|
||||
return new VerdaccioProcess(child)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a SIGKILL to the process and wait for it to stop
|
||||
*/
|
||||
async kill() {
|
||||
this.child.stdout && this.child.stdout.destroy()
|
||||
this.child.stderr && this.child.stderr.destroy()
|
||||
this.child.kill();
|
||||
try {
|
||||
await withTimeout(once(this.child, "close"), 500)
|
||||
} catch (e) {
|
||||
console.error("unable to kill verdaccio subprocess, trying -9")
|
||||
this.child.kill(9)
|
||||
await withTimeout(once(this.child, "close"), 500)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A promise which resolves if the subprocess exits for some reason
|
||||
*/
|
||||
async died(): Promise<number | null> {
|
||||
const [exit, _signal] = await once(this.child, "exit")
|
||||
return exit
|
||||
}
|
||||
}
|
||||
|
||||
function printHeader(header: string) {
|
||||
console.log("\n===============================")
|
||||
console.log(` ${header}`)
|
||||
console.log("===============================")
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the automerge, automerge-wasm, and automerge-js packages from
|
||||
* `$packageDir/node_modules`
|
||||
*
|
||||
* This is useful to force refreshing a package by use in combination with
|
||||
* `yarn install --check-files`, which checks if a package is present in
|
||||
* `node_modules` and if it is not forces a reinstall.
|
||||
*
|
||||
* @param packageDir - The directory containing the package.json of the target project
|
||||
*/
|
||||
async function removeExistingAutomerge(packageDir: string) {
|
||||
await fsPromises.rm(path.join(packageDir, "node_modules", "automerge-wasm"), {recursive: true, force: true})
|
||||
await fsPromises.rm(path.join(packageDir, "node_modules", "automerge"), {recursive: true, force: true})
|
||||
}
|
||||
|
||||
type SpawnResult = {
|
||||
stdout?: Buffer,
|
||||
stderr?: Buffer,
|
||||
}
|
||||
|
||||
async function spawnAndWait(cmd: string, args: Array<string>, options: child_process.SpawnOptions): Promise<SpawnResult> {
|
||||
const child = spawn(cmd, args, options)
|
||||
let stdout = null
|
||||
let stderr = null
|
||||
if (child.stdout) {
|
||||
stdout = []
|
||||
child.stdout.on("data", data => stdout.push(data))
|
||||
}
|
||||
if (child.stderr) {
|
||||
stderr = []
|
||||
child.stderr.on("data", data => stderr.push(data))
|
||||
}
|
||||
|
||||
const [exit, _signal] = await once(child, "exit")
|
||||
if (exit && exit !== 0) {
|
||||
throw new Error("nonzero exit code")
|
||||
}
|
||||
return {
|
||||
stderr: stderr? Buffer.concat(stderr) : null,
|
||||
stdout: stdout ? Buffer.concat(stdout) : null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a package from the verdaccio registry. This is necessary because we
|
||||
* often want to _replace_ a version rather than update the version number.
|
||||
* Obviously this is very bad and verboten in normal circumastances, but the
|
||||
* whole point here is to be able to test the entire packaging story so it's
|
||||
* okay I Promise.
|
||||
*/
|
||||
async function removeFromVerdaccio(packageName: string) {
|
||||
await fsPromises.rm(path.join(VERDACCIO_DB_PATH, packageName), {force: true, recursive: true})
|
||||
}
|
||||
|
||||
async function yarnPublish(registryUrl: string, cwd: string) {
|
||||
await spawnAndWait(
|
||||
"yarn",
|
||||
[
|
||||
"--registry",
|
||||
registryUrl,
|
||||
"--cwd",
|
||||
cwd,
|
||||
"publish",
|
||||
"--non-interactive",
|
||||
],
|
||||
{
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
FORCE_COLOR: "true",
|
||||
// This is a fake token, it just has to be the right format
|
||||
npm_config__auth: "//localhost:4873/:_authToken=Gp2Mgxm4faa/7wp0dMSuRA=="
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a given delay to resolve a promise, throwing an error if the
|
||||
* promise doesn't resolve with the timeout
|
||||
*
|
||||
* @param promise - the promise to wait for @param timeout - the delay in
|
||||
* milliseconds to wait before throwing
|
||||
*/
|
||||
async function withTimeout<T>(promise: Promise<T>, timeout: number): Promise<T> {
|
||||
type Step = "timed-out" | {result: T}
|
||||
const timedOut: () => Promise<Step> = async () => {
|
||||
await setTimeout(timeout)
|
||||
return "timed-out"
|
||||
}
|
||||
const succeeded: () => Promise<Step> = async () => {
|
||||
const result = await promise
|
||||
return {result}
|
||||
}
|
||||
const result = await Promise.race([timedOut(), succeeded()])
|
||||
if (result === "timed-out") {
|
||||
throw new Error("timed out")
|
||||
} else {
|
||||
return result.result
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"types": ["node"]
|
||||
},
|
||||
"module": "nodenext"
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
storage: "./verdacciodb"
|
||||
auth:
|
||||
htpasswd:
|
||||
file: ./htpasswd
|
||||
publish:
|
||||
allow_offline: true
|
||||
logs: {type: stdout, format: pretty, level: info}
|
||||
packages:
|
||||
"@automerge/automerge-wasm":
|
||||
access: "$all"
|
||||
publish: "$all"
|
||||
"@automerge/automerge":
|
||||
access: "$all"
|
||||
publish: "$all"
|
||||
"*":
|
||||
access: "$all"
|
||||
publish: "$all"
|
||||
proxy: npmjs
|
||||
"@*/*":
|
||||
access: "$all"
|
||||
publish: "$all"
|
||||
proxy: npmjs
|
||||
uplinks:
|
||||
npmjs:
|
||||
url: https://registry.npmjs.org/
|
|
@ -1,8 +0,0 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
|
@ -1,18 +0,0 @@
|
|||
import * as Automerge from "@automerge/automerge"
|
||||
|
||||
// hello world code that will run correctly on web or node
|
||||
|
||||
let doc = Automerge.init()
|
||||
doc = Automerge.change(doc, (d: any) => d.hello = "from automerge-js")
|
||||
const result = JSON.stringify(doc)
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
// browser
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = JSON.stringify(result)
|
||||
document.body.appendChild(element);
|
||||
} else {
|
||||
// server
|
||||
console.log("node:", result)
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { defineConfig } from "vite"
|
||||
import wasm from "vite-plugin-wasm"
|
||||
import topLevelAwait from "vite-plugin-top-level-await"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [topLevelAwait(), wasm()],
|
||||
|
||||
optimizeDeps: {
|
||||
// This is necessary because otherwise `vite dev` includes two separate
|
||||
// versions of the JS wrapper. This causes problems because the JS
|
||||
// wrapper has a module level variable to track JS side heap
|
||||
// allocations, initializing this twice causes horrible breakage
|
||||
exclude: ["@automerge/automerge-wasm"]
|
||||
}
|
||||
})
|
|
@ -1,36 +0,0 @@
|
|||
const path = require('path');
|
||||
const nodeExternals = require('webpack-node-externals');
|
||||
|
||||
// the most basic webpack config for node or web targets for automerge-wasm
|
||||
|
||||
const serverConfig = {
|
||||
// basic setup for bundling a node package
|
||||
target: 'node',
|
||||
externals: [nodeExternals()],
|
||||
externalsPresets: { node: true },
|
||||
|
||||
entry: './src/index.js',
|
||||
output: {
|
||||
filename: 'node.js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
},
|
||||
mode: "development", // or production
|
||||
};
|
||||
|
||||
const clientConfig = {
|
||||
experiments: { asyncWebAssembly: true },
|
||||
target: 'web',
|
||||
entry: './src/index.js',
|
||||
output: {
|
||||
filename: 'main.js',
|
||||
path: path.resolve(__dirname, 'public'),
|
||||
},
|
||||
mode: "development", // or production
|
||||
performance: { // we dont want the wasm blob to generate warnings
|
||||
hints: false,
|
||||
maxEntrypointSize: 512000,
|
||||
maxAssetSize: 512000
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = [serverConfig, clientConfig];
|
|
@ -1,63 +0,0 @@
|
|||
{
|
||||
"name": "@automerge/automerge",
|
||||
"collaborators": [
|
||||
"Orion Henry <orion@inkandswitch.com>",
|
||||
"Martin Kleppmann"
|
||||
],
|
||||
"version": "2.0.0-alpha.4",
|
||||
"description": "Javascript implementation of automerge, backed by @automerge/automerge-wasm",
|
||||
"homepage": "https://github.com/automerge/automerge-rs/tree/main/automerge-js",
|
||||
"repository": "github:automerge/automerge-rs",
|
||||
"files": [
|
||||
"README.md",
|
||||
"LICENSE",
|
||||
"package.json",
|
||||
"index.d.ts",
|
||||
"dist/*.d.ts",
|
||||
"dist/cjs/constants.js",
|
||||
"dist/cjs/types.js",
|
||||
"dist/cjs/numbers.js",
|
||||
"dist/cjs/index.js",
|
||||
"dist/cjs/uuid.js",
|
||||
"dist/cjs/counter.js",
|
||||
"dist/cjs/low_level.js",
|
||||
"dist/cjs/text.js",
|
||||
"dist/cjs/proxies.js",
|
||||
"dist/mjs/constants.js",
|
||||
"dist/mjs/types.js",
|
||||
"dist/mjs/numbers.js",
|
||||
"dist/mjs/index.js",
|
||||
"dist/mjs/uuid.js",
|
||||
"dist/mjs/counter.js",
|
||||
"dist/mjs/low_level.js",
|
||||
"dist/mjs/text.js",
|
||||
"dist/mjs/proxies.js"
|
||||
],
|
||||
"types": "./dist/index.d.ts",
|
||||
"module": "./dist/mjs/index.js",
|
||||
"main": "./dist/cjs/index.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "eslint src",
|
||||
"build": "tsc -p config/mjs.json && tsc -p config/cjs.json && tsc --emitDeclarationOnly",
|
||||
"test": "ts-mocha test/*.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/expect": "^24.3.0",
|
||||
"@types/mocha": "^9.1.1",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.25.0",
|
||||
"@typescript-eslint/parser": "^5.25.0",
|
||||
"eslint": "^8.15.0",
|
||||
"fast-sha256": "^1.3.0",
|
||||
"mocha": "^10.0.0",
|
||||
"pako": "^2.0.4",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.6.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@automerge/automerge-wasm": "0.1.9",
|
||||
"uuid": "^8.3"
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
// Properties of the document root object
|
||||
//const OPTIONS = Symbol('_options') // object containing options passed to init()
|
||||
//const CACHE = Symbol('_cache') // map from objectId to immutable object
|
||||
//export const STATE = Symbol.for('_am_state') // object containing metadata about current state (e.g. sequence numbers)
|
||||
export const STATE = Symbol.for('_am_meta') // object containing metadata about current state (e.g. sequence numbers)
|
||||
export const HEADS = Symbol.for('_am_heads') // object containing metadata about current state (e.g. sequence numbers)
|
||||
export const TRACE = Symbol.for('_am_trace') // object containing metadata about current state (e.g. sequence numbers)
|
||||
export const OBJECT_ID = Symbol.for('_am_objectId') // object containing metadata about current state (e.g. sequence numbers)
|
||||
export const READ_ONLY = Symbol.for('_am_readOnly') // object containing metadata about current state (e.g. sequence numbers)
|
||||
export const FROZEN = Symbol.for('_am_frozen') // object containing metadata about current state (e.g. sequence numbers)
|
||||
|
||||
export const UINT = Symbol.for('_am_uint')
|
||||
export const INT = Symbol.for('_am_int')
|
||||
export const F64 = Symbol.for('_am_f64')
|
||||
export const COUNTER = Symbol.for('_am_counter')
|
||||
export const TEXT = Symbol.for('_am_text')
|
||||
|
||||
// Properties of all Automerge objects
|
||||
//const OBJECT_ID = Symbol('_objectId') // the object ID of the current object (string)
|
||||
//const CONFLICTS = Symbol('_conflicts') // map or list (depending on object type) of conflicts
|
||||
//const CHANGE = Symbol('_change') // the context object on proxy objects used in change callback
|
||||
//const ELEM_IDS = Symbol('_elemIds') // list containing the element ID of each list element
|
||||
|
||||
|
|
@ -1,460 +0,0 @@
|
|||
|
||||
export { uuid } from './uuid'
|
||||
|
||||
import { rootProxy, listProxy, textProxy, mapProxy } from "./proxies"
|
||||
import { STATE, HEADS, TRACE, OBJECT_ID, READ_ONLY, FROZEN } from "./constants"
|
||||
|
||||
import { AutomergeValue, Text, Counter } from "./types"
|
||||
export { AutomergeValue, Text, Counter, Int, Uint, Float64 } from "./types"
|
||||
|
||||
import { type API, type Patch } from "@automerge/automerge-wasm";
|
||||
import { ApiHandler, UseApi } from "./low_level"
|
||||
|
||||
import { Actor as ActorId, Prop, ObjID, Change, DecodedChange, Heads, Automerge, MaterializeValue } from "@automerge/automerge-wasm"
|
||||
import { JsSyncState as SyncState, SyncMessage, DecodedSyncMessage } from "@automerge/automerge-wasm"
|
||||
|
||||
export type ChangeOptions<T> = { message?: string, time?: number, patchCallback?: PatchCallback<T> }
|
||||
export type ApplyOptions<T> = { patchCallback?: PatchCallback<T> }
|
||||
|
||||
export type Doc<T> = { readonly [P in keyof T]: T[P] }
|
||||
|
||||
export type ChangeFn<T> = (doc: T) => void
|
||||
|
||||
export type PatchCallback<T> = (patch: Patch, before: Doc<T>, after: Doc<T>) => void
|
||||
|
||||
export interface State<T> {
|
||||
change: DecodedChange
|
||||
snapshot: T
|
||||
}
|
||||
|
||||
export function use(api: API) {
|
||||
UseApi(api)
|
||||
}
|
||||
|
||||
import * as wasm from "@automerge/automerge-wasm"
|
||||
use(wasm)
|
||||
|
||||
export type InitOptions<T> = {
|
||||
actor?: ActorId,
|
||||
freeze?: boolean,
|
||||
patchCallback?: PatchCallback<T>,
|
||||
};
|
||||
|
||||
|
||||
interface InternalState<T> {
|
||||
handle: Automerge,
|
||||
heads: Heads | undefined,
|
||||
freeze: boolean,
|
||||
patchCallback?: PatchCallback<T>
|
||||
}
|
||||
|
||||
export function getBackend<T>(doc: Doc<T>) : Automerge {
|
||||
return _state(doc).handle
|
||||
}
|
||||
|
||||
function _state<T>(doc: Doc<T>, checkroot = true) : InternalState<T> {
|
||||
const state = Reflect.get(doc,STATE)
|
||||
if (state === undefined || (checkroot && _obj(doc) !== "_root")) {
|
||||
throw new RangeError("must be the document root")
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
function _frozen<T>(doc: Doc<T>) : boolean {
|
||||
return Reflect.get(doc,FROZEN) === true
|
||||
}
|
||||
|
||||
function _trace<T>(doc: Doc<T>) : string | undefined {
|
||||
return Reflect.get(doc,TRACE)
|
||||
}
|
||||
|
||||
function _set_heads<T>(doc: Doc<T>, heads: Heads) {
|
||||
_state(doc).heads = heads
|
||||
}
|
||||
|
||||
function _clear_heads<T>(doc: Doc<T>) {
|
||||
Reflect.set(doc,HEADS,undefined)
|
||||
Reflect.set(doc,TRACE,undefined)
|
||||
}
|
||||
|
||||
function _obj<T>(doc: Doc<T>) : ObjID {
|
||||
let proxy_objid = Reflect.get(doc,OBJECT_ID)
|
||||
if (proxy_objid) {
|
||||
return proxy_objid
|
||||
}
|
||||
if (Reflect.get(doc,STATE)) {
|
||||
return "_root"
|
||||
}
|
||||
throw new RangeError("invalid document passed to _obj()")
|
||||
}
|
||||
|
||||
function _readonly<T>(doc: Doc<T>) : boolean {
|
||||
return Reflect.get(doc,READ_ONLY) !== false
|
||||
}
|
||||
|
||||
function importOpts<T>(_actor?: ActorId | InitOptions<T>) : InitOptions<T> {
|
||||
if (typeof _actor === 'object') {
|
||||
return _actor
|
||||
} else {
|
||||
return { actor: _actor }
|
||||
}
|
||||
}
|
||||
|
||||
export function init<T>(_opts?: ActorId | InitOptions<T>) : Doc<T>{
|
||||
let opts = importOpts(_opts)
|
||||
let freeze = !!opts.freeze
|
||||
let patchCallback = opts.patchCallback
|
||||
const handle = ApiHandler.create(opts.actor)
|
||||
handle.enablePatches(true)
|
||||
//@ts-ignore
|
||||
handle.registerDatatype("counter", (n) => new Counter(n))
|
||||
//@ts-ignore
|
||||
handle.registerDatatype("text", (n) => new Text(n))
|
||||
//@ts-ignore
|
||||
const doc = handle.materialize("/", undefined, { handle, heads: undefined, freeze, patchCallback })
|
||||
//@ts-ignore
|
||||
return doc
|
||||
}
|
||||
|
||||
export function clone<T>(doc: Doc<T>) : Doc<T> {
|
||||
const state = _state(doc)
|
||||
const handle = state.heads ? state.handle.forkAt(state.heads) : state.handle.fork()
|
||||
//@ts-ignore
|
||||
const clonedDoc : any = handle.materialize("/", undefined, { ... state, handle })
|
||||
|
||||
return clonedDoc
|
||||
}
|
||||
|
||||
export function free<T>(doc: Doc<T>) {
|
||||
return _state(doc).handle.free()
|
||||
}
|
||||
|
||||
export function from<T extends Record<string, unknown>>(initialState: T | Doc<T>, actor?: ActorId): Doc<T> {
|
||||
return change(init(actor), (d) => Object.assign(d, initialState))
|
||||
}
|
||||
|
||||
export function change<T>(doc: Doc<T>, options: string | ChangeOptions<T> | ChangeFn<T>, callback?: ChangeFn<T>): Doc<T> {
|
||||
if (typeof options === 'function') {
|
||||
return _change(doc, {}, options)
|
||||
} else if (typeof callback === 'function') {
|
||||
if (typeof options === "string") {
|
||||
options = { message: options }
|
||||
}
|
||||
return _change(doc, options, callback)
|
||||
} else {
|
||||
throw RangeError("Invalid args for change")
|
||||
}
|
||||
}
|
||||
|
||||
function progressDocument<T>(doc: Doc<T>, heads: Heads, callback?: PatchCallback<T>): Doc<T> {
|
||||
let state = _state(doc)
|
||||
let nextState = { ... state, heads: undefined };
|
||||
// @ts-ignore
|
||||
let nextDoc = state.handle.applyPatches(doc, nextState, callback)
|
||||
state.heads = heads
|
||||
if (nextState.freeze) { Object.freeze(nextDoc) }
|
||||
return nextDoc
|
||||
}
|
||||
|
||||
function _change<T>(doc: Doc<T>, options: ChangeOptions<T>, callback: ChangeFn<T>): Doc<T> {
|
||||
|
||||
|
||||
if (typeof callback !== "function") {
|
||||
throw new RangeError("invalid change function");
|
||||
}
|
||||
|
||||
const state = _state(doc)
|
||||
|
||||
if (doc === undefined || state === undefined) {
|
||||
throw new RangeError("must be the document root");
|
||||
}
|
||||
if (state.heads) {
|
||||
throw new RangeError("Attempting to use an outdated Automerge document")
|
||||
}
|
||||
if (_readonly(doc) === false) {
|
||||
throw new RangeError("Calls to Automerge.change cannot be nested")
|
||||
}
|
||||
const heads = state.handle.getHeads()
|
||||
try {
|
||||
state.heads = heads
|
||||
const root : T = rootProxy(state.handle);
|
||||
callback(root)
|
||||
if (state.handle.pendingOps() === 0) {
|
||||
state.heads = undefined
|
||||
return doc
|
||||
} else {
|
||||
state.handle.commit(options.message, options.time)
|
||||
return progressDocument(doc, heads, options.patchCallback || state.patchCallback);
|
||||
}
|
||||
} catch (e) {
|
||||
//console.log("ERROR: ",e)
|
||||
state.heads = undefined
|
||||
state.handle.rollback()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export function emptyChange<T>(doc: Doc<T>, options: ChangeOptions<T>) {
|
||||
if (options === undefined) {
|
||||
options = {}
|
||||
}
|
||||
if (typeof options === "string") {
|
||||
options = { message: options }
|
||||
}
|
||||
|
||||
const state = _state(doc)
|
||||
|
||||
if (state.heads) {
|
||||
throw new RangeError("Attempting to use an outdated Automerge document")
|
||||
}
|
||||
if (_readonly(doc) === false) {
|
||||
throw new RangeError("Calls to Automerge.change cannot be nested")
|
||||
}
|
||||
|
||||
const heads = state.handle.getHeads()
|
||||
state.handle.commit(options.message, options.time)
|
||||
return progressDocument(doc, heads)
|
||||
}
|
||||
|
||||
export function load<T>(data: Uint8Array, _opts?: ActorId | InitOptions<T>) : Doc<T> {
|
||||
const opts = importOpts(_opts)
|
||||
const actor = opts.actor
|
||||
const patchCallback = opts.patchCallback
|
||||
const handle = ApiHandler.load(data, actor)
|
||||
handle.enablePatches(true)
|
||||
//@ts-ignore
|
||||
handle.registerDatatype("counter", (n) => new Counter(n))
|
||||
//@ts-ignore
|
||||
handle.registerDatatype("text", (n) => new Text(n))
|
||||
//@ts-ignore
|
||||
const doc : any = handle.materialize("/", undefined, { handle, heads: undefined, patchCallback })
|
||||
return doc
|
||||
}
|
||||
|
||||
export function loadIncremental<T>(doc: Doc<T>, data: Uint8Array, opts?: ApplyOptions<T>) : Doc<T> {
|
||||
if (!opts) { opts = {} }
|
||||
const state = _state(doc)
|
||||
if (state.heads) {
|
||||
throw new RangeError("Attempting to change an out of date document - set at: " + _trace(doc));
|
||||
}
|
||||
if (_readonly(doc) === false) {
|
||||
throw new RangeError("Calls to Automerge.change cannot be nested")
|
||||
}
|
||||
const heads = state.handle.getHeads()
|
||||
state.handle.loadIncremental(data)
|
||||
return progressDocument(doc, heads, opts.patchCallback || state.patchCallback)
|
||||
}
|
||||
|
||||
export function save<T>(doc: Doc<T>) : Uint8Array {
|
||||
return _state(doc).handle.save()
|
||||
}
|
||||
|
||||
export function merge<T>(local: Doc<T>, remote: Doc<T>) : Doc<T> {
|
||||
const localState = _state(local)
|
||||
|
||||
if (localState.heads) {
|
||||
throw new RangeError("Attempting to change an out of date document - set at: " + _trace(local));
|
||||
}
|
||||
const heads = localState.handle.getHeads()
|
||||
const remoteState = _state(remote)
|
||||
const changes = localState.handle.getChangesAdded(remoteState.handle)
|
||||
localState.handle.applyChanges(changes)
|
||||
return progressDocument(local, heads, localState.patchCallback)
|
||||
}
|
||||
|
||||
export function getActorId<T>(doc: Doc<T>) : ActorId {
|
||||
const state = _state(doc)
|
||||
return state.handle.getActorId()
|
||||
}
|
||||
|
||||
type Conflicts = { [key: string]: AutomergeValue }
|
||||
|
||||
function conflictAt(context : Automerge, objectId: ObjID, prop: Prop) : Conflicts | undefined {
|
||||
const values = context.getAll(objectId, prop)
|
||||
if (values.length <= 1) {
|
||||
return
|
||||
}
|
||||
const result : Conflicts = {}
|
||||
for (const fullVal of values) {
|
||||
switch (fullVal[0]) {
|
||||
case "map":
|
||||
result[fullVal[1]] = mapProxy(context, fullVal[1], [ prop ], true)
|
||||
break;
|
||||
case "list":
|
||||
result[fullVal[1]] = listProxy(context, fullVal[1], [ prop ], true)
|
||||
break;
|
||||
case "text":
|
||||
result[fullVal[1]] = textProxy(context, fullVal[1], [ prop ], true)
|
||||
break;
|
||||
//case "table":
|
||||
//case "cursor":
|
||||
case "str":
|
||||
case "uint":
|
||||
case "int":
|
||||
case "f64":
|
||||
case "boolean":
|
||||
case "bytes":
|
||||
case "null":
|
||||
result[fullVal[2]] = fullVal[1]
|
||||
break;
|
||||
case "counter":
|
||||
result[fullVal[2]] = new Counter(fullVal[1])
|
||||
break;
|
||||
case "timestamp":
|
||||
result[fullVal[2]] = new Date(fullVal[1])
|
||||
break;
|
||||
default:
|
||||
throw RangeError(`datatype ${fullVal[0]} unimplemented`)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function getConflicts<T>(doc: Doc<T>, prop: Prop) : Conflicts | undefined {
|
||||
const state = _state(doc, false)
|
||||
const objectId = _obj(doc)
|
||||
return conflictAt(state.handle, objectId, prop)
|
||||
}
|
||||
|
||||
export function getLastLocalChange<T>(doc: Doc<T>) : Change | undefined {
|
||||
const state = _state(doc)
|
||||
return state.handle.getLastLocalChange() || undefined
|
||||
}
|
||||
|
||||
export function getObjectId<T>(doc: Doc<T>) : ObjID {
|
||||
return _obj(doc)
|
||||
}
|
||||
|
||||
export function getChanges<T>(oldState: Doc<T>, newState: Doc<T>) : Change[] {
|
||||
const o = _state(oldState)
|
||||
const n = _state(newState)
|
||||
return n.handle.getChanges(getHeads(oldState))
|
||||
}
|
||||
|
||||
export function getAllChanges<T>(doc: Doc<T>) : Change[] {
|
||||
const state = _state(doc)
|
||||
return state.handle.getChanges([])
|
||||
}
|
||||
|
||||
export function applyChanges<T>(doc: Doc<T>, changes: Change[], opts?: ApplyOptions<T>) : [Doc<T>] {
|
||||
const state = _state(doc)
|
||||
if (!opts) { opts = {} }
|
||||
if (state.heads) {
|
||||
throw new RangeError("Attempting to use an outdated Automerge document")
|
||||
}
|
||||
if (_readonly(doc) === false) {
|
||||
throw new RangeError("Calls to Automerge.change cannot be nested")
|
||||
}
|
||||
const heads = state.handle.getHeads();
|
||||
state.handle.applyChanges(changes)
|
||||
state.heads = heads;
|
||||
return [progressDocument(doc, heads, opts.patchCallback || state.patchCallback )]
|
||||
}
|
||||
|
||||
export function getHistory<T>(doc: Doc<T>) : State<T>[] {
|
||||
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 <T>state
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// FIXME : no tests
|
||||
// FIXME can we just use deep equals now?
|
||||
export function equals(val1: unknown, val2: unknown) : boolean {
|
||||
if (!isObject(val1) || !isObject(val2)) return val1 === val2
|
||||
const keys1 = Object.keys(val1).sort(), keys2 = Object.keys(val2).sort()
|
||||
if (keys1.length !== keys2.length) return false
|
||||
for (let i = 0; i < keys1.length; i++) {
|
||||
if (keys1[i] !== keys2[i]) return false
|
||||
if (!equals(val1[keys1[i]], val2[keys2[i]])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function encodeSyncState(state: SyncState) : Uint8Array {
|
||||
return ApiHandler.encodeSyncState(ApiHandler.importSyncState(state))
|
||||
}
|
||||
|
||||
export function decodeSyncState(state: Uint8Array) : SyncState {
|
||||
return ApiHandler.exportSyncState(ApiHandler.decodeSyncState(state))
|
||||
}
|
||||
|
||||
export function generateSyncMessage<T>(doc: Doc<T>, inState: SyncState) : [ SyncState, SyncMessage | null ] {
|
||||
const state = _state(doc)
|
||||
const syncState = ApiHandler.importSyncState(inState)
|
||||
const message = state.handle.generateSyncMessage(syncState)
|
||||
const outState = ApiHandler.exportSyncState(syncState)
|
||||
return [ outState, message ]
|
||||
}
|
||||
|
||||
export function receiveSyncMessage<T>(doc: Doc<T>, inState: SyncState, message: SyncMessage, opts?: ApplyOptions<T>) : [ Doc<T>, SyncState, null ] {
|
||||
const syncState = ApiHandler.importSyncState(inState)
|
||||
if (!opts) { opts = {} }
|
||||
const state = _state(doc)
|
||||
if (state.heads) {
|
||||
throw new RangeError("Attempting to change an out of date document - set at: " + _trace(doc));
|
||||
}
|
||||
if (_readonly(doc) === false) {
|
||||
throw new RangeError("Calls to Automerge.change cannot be nested")
|
||||
}
|
||||
const heads = state.handle.getHeads()
|
||||
state.handle.receiveSyncMessage(syncState, message)
|
||||
const outSyncState = ApiHandler.exportSyncState(syncState)
|
||||
return [progressDocument(doc, heads, opts.patchCallback || state.patchCallback), outSyncState, null];
|
||||
}
|
||||
|
||||
export function initSyncState() : SyncState {
|
||||
return ApiHandler.exportSyncState(ApiHandler.initSyncState())
|
||||
}
|
||||
|
||||
export function encodeChange(change: DecodedChange) : Change {
|
||||
return ApiHandler.encodeChange(change)
|
||||
}
|
||||
|
||||
export function decodeChange(data: Change) : DecodedChange {
|
||||
return ApiHandler.decodeChange(data)
|
||||
}
|
||||
|
||||
export function encodeSyncMessage(message: DecodedSyncMessage) : SyncMessage {
|
||||
return ApiHandler.encodeSyncMessage(message)
|
||||
}
|
||||
|
||||
export function decodeSyncMessage(message: SyncMessage) : DecodedSyncMessage {
|
||||
return ApiHandler.decodeSyncMessage(message)
|
||||
}
|
||||
|
||||
export function getMissingDeps<T>(doc: Doc<T>, heads: Heads) : Heads {
|
||||
const state = _state(doc)
|
||||
return state.handle.getMissingDeps(heads)
|
||||
}
|
||||
|
||||
export function getHeads<T>(doc: Doc<T>) : Heads {
|
||||
const state = _state(doc)
|
||||
return state.heads || state.handle.getHeads()
|
||||
}
|
||||
|
||||
export function dump<T>(doc: Doc<T>) {
|
||||
const state = _state(doc)
|
||||
state.handle.dump()
|
||||
}
|
||||
|
||||
// FIXME - return T?
|
||||
export function toJS<T>(doc: Doc<T>) : MaterializeValue {
|
||||
const state = _state(doc)
|
||||
// @ts-ignore
|
||||
return state.handle.materialize("_root", state.heads, state)
|
||||
}
|
||||
|
||||
|
||||
function isObject(obj: unknown) : obj is Record<string,unknown> {
|
||||
return typeof obj === 'object' && obj !== null
|
||||
}
|
||||
|
||||
export type { API, SyncState, ActorId, Conflicts, Prop, Change, ObjID, DecodedChange, DecodedSyncMessage, Heads, MaterializeValue }
|
|
@ -1,25 +0,0 @@
|
|||
|
||||
import { Automerge, Change, DecodedChange, Actor, SyncState, SyncMessage, JsSyncState, DecodedSyncMessage } from "@automerge/automerge-wasm"
|
||||
import { API } from "@automerge/automerge-wasm"
|
||||
|
||||
export function UseApi(api: API) {
|
||||
for (const k in api) {
|
||||
ApiHandler[k] = api[k]
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable */
|
||||
export const ApiHandler : API = {
|
||||
create(actor?: Actor): Automerge { throw new RangeError("Automerge.use() not called") },
|
||||
load(data: Uint8Array, actor?: Actor): Automerge { throw new RangeError("Automerge.use() not called (load)") },
|
||||
encodeChange(change: DecodedChange): Change { throw new RangeError("Automerge.use() not called (encodeChange)") },
|
||||
decodeChange(change: Change): DecodedChange { throw new RangeError("Automerge.use() not called (decodeChange)") },
|
||||
initSyncState(): SyncState { throw new RangeError("Automerge.use() not called (initSyncState)") },
|
||||
encodeSyncMessage(message: DecodedSyncMessage): SyncMessage { throw new RangeError("Automerge.use() not called (encodeSyncMessage)") },
|
||||
decodeSyncMessage(msg: SyncMessage): DecodedSyncMessage { throw new RangeError("Automerge.use() not called (decodeSyncMessage)") },
|
||||
encodeSyncState(state: SyncState): Uint8Array { throw new RangeError("Automerge.use() not called (encodeSyncState)") },
|
||||
decodeSyncState(data: Uint8Array): SyncState { throw new RangeError("Automerge.use() not called (decodeSyncState)") },
|
||||
exportSyncState(state: SyncState): JsSyncState { throw new RangeError("Automerge.use() not called (exportSyncState)") },
|
||||
importSyncState(state: JsSyncState): SyncState { throw new RangeError("Automerge.use() not called (importSyncState)") },
|
||||
}
|
||||
/* eslint-enable */
|
|
@ -1,711 +0,0 @@
|
|||
|
||||
import { Automerge, Heads, ObjID } from "@automerge/automerge-wasm"
|
||||
import { Prop } from "@automerge/automerge-wasm"
|
||||
import { AutomergeValue, ScalarValue, MapValue, ListValue, TextValue } from "./types"
|
||||
import { Counter, getWriteableCounter } from "./counter"
|
||||
import { Text } from "./text"
|
||||
import { STATE, HEADS, TRACE, FROZEN, OBJECT_ID, READ_ONLY, COUNTER, INT, UINT, F64, TEXT } from "./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: Prop) : AutomergeValue | undefined {
|
||||
const { context, objectId, path, readonly, heads} = target
|
||||
const value = context.getWithType(objectId, prop, heads)
|
||||
if (value === null) {
|
||||
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[UINT]) {
|
||||
return [ value.value, "uint" ]
|
||||
} else if (value[INT]) {
|
||||
return [ value.value, "int" ]
|
||||
} else if (value[F64]) {
|
||||
return [ value.value, "f64" ]
|
||||
} else if (value[COUNTER]) {
|
||||
return [ value.value, "counter" ]
|
||||
} else if (value[TEXT]) {
|
||||
return [ value, "text" ]
|
||||
} 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 (Object.getPrototypeOf(value) === Object.getPrototypeOf({})) {
|
||||
return [ value, "map" ]
|
||||
} else if (value[OBJECT_ID]) {
|
||||
throw new RangeError('Cannot create a reference to an existing document object')
|
||||
} else {
|
||||
throw new RangeError(`Cannot assign unknown object: ${value}`)
|
||||
}
|
||||
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) : AutomergeValue {
|
||||
const { context, objectId, 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 === TRACE) return target.trace
|
||||
if (key === STATE) return context;
|
||||
if (!cache[key]) {
|
||||
cache[key] = valueAt(target, key)
|
||||
}
|
||||
return cache[key]
|
||||
},
|
||||
|
||||
set (target, key, val) {
|
||||
const { 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 true
|
||||
}
|
||||
if (key === HEADS) {
|
||||
target.heads = val
|
||||
return true
|
||||
}
|
||||
if (key === TRACE) {
|
||||
target.trace = val
|
||||
return true
|
||||
}
|
||||
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 "${key}" cannot be modified`)
|
||||
}
|
||||
switch (datatype) {
|
||||
case "list": {
|
||||
const list = context.putObject(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.putObject(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.putObject(objectId, key, {})
|
||||
const proxyMap = mapProxy(context, map, [ ... path, key ], readonly );
|
||||
for (const key in value) {
|
||||
proxyMap[key] = value[key]
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
context.put(objectId, key, value, datatype)
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
deleteProperty (target, key) {
|
||||
const { context, objectId, readonly } = target
|
||||
target.cache = {} // reset cache on delete
|
||||
if (readonly) {
|
||||
throw new RangeError(`Object property "${key}" cannot be modified`)
|
||||
}
|
||||
context.delete(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
|
||||
// FIXME - this is a tmp workaround until fix the dupe key bug in keys()
|
||||
const keys = context.keys(objectId, heads)
|
||||
return [...new Set<string>(keys)]
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
const ListHandler = {
|
||||
get (target, index) {
|
||||
const {context, objectId, readonly, frozen, heads } = target
|
||||
index = parseListIndex(index)
|
||||
if (index === Symbol.hasInstance) { return (instance) => { return Array.isArray(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 === TRACE) return target.trace
|
||||
if (index === STATE) return context;
|
||||
if (index === 'length') return context.length(objectId, heads);
|
||||
if (typeof index === 'number') {
|
||||
return valueAt(target, index)
|
||||
} else {
|
||||
return listMethods(target)[index]
|
||||
}
|
||||
},
|
||||
|
||||
set (target, index, val) {
|
||||
const {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 true
|
||||
}
|
||||
if (index === HEADS) {
|
||||
target.heads = val
|
||||
return true
|
||||
}
|
||||
if (index === TRACE) {
|
||||
target.trace = val
|
||||
return true
|
||||
}
|
||||
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.insertObject(objectId, index, [])
|
||||
} else {
|
||||
list = context.putObject(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.insertObject(objectId, index, "", "text")
|
||||
} else {
|
||||
text = context.putObject(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.insertObject(objectId, index, {})
|
||||
} else {
|
||||
map = context.putObject(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.put(objectId, index, value, datatype)
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
deleteProperty (target, index) {
|
||||
const {context, objectId} = target
|
||||
index = parseListIndex(index)
|
||||
if (context.get(objectId, index)[0] == "counter") {
|
||||
throw new TypeError('Unsupported operation: deleting a counter from a list')
|
||||
}
|
||||
context.delete(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, 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)
|
||||
|
||||
const value = valueAt(target, index)
|
||||
return { configurable: true, enumerable: true, value }
|
||||
},
|
||||
|
||||
getPrototypeOf(target) { return Object.getPrototypeOf(target) },
|
||||
ownKeys (/*target*/) : string[] {
|
||||
const keys : string[] = []
|
||||
// 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
|
||||
//const {context, objectId, heads } = target
|
||||
//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, readonly, frozen, heads } = target
|
||||
index = parseListIndex(index)
|
||||
if (index === Symbol.toStringTag) { return target[Symbol.toStringTag] }
|
||||
if (index === Symbol.hasInstance) { return (instance) => { return Array.isArray(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 === TRACE) return target.trace
|
||||
if (index === STATE) return context;
|
||||
if (index === 'length') return context.length(objectId, heads);
|
||||
if (typeof index === 'number') {
|
||||
return valueAt(target, index)
|
||||
} else {
|
||||
return textMethods(target)[index] || listMethods(target)[index]
|
||||
}
|
||||
},
|
||||
getPrototypeOf(/*target*/) {
|
||||
return Object.getPrototypeOf(new Text())
|
||||
},
|
||||
})
|
||||
|
||||
export function mapProxy(context: Automerge, objectId: ObjID, path?: Prop[], readonly?: boolean, heads?: Heads) : MapValue {
|
||||
return new Proxy({context, objectId, path, readonly: !!readonly, frozen: false, heads, cache: {}}, MapHandler)
|
||||
}
|
||||
|
||||
export function listProxy(context: Automerge, objectId: ObjID, path?: Prop[], readonly?: boolean, heads?: Heads) : ListValue {
|
||||
const target = []
|
||||
Object.assign(target, {context, objectId, path, readonly: !!readonly, frozen: false, heads, cache: {}})
|
||||
return new Proxy(target, ListHandler)
|
||||
}
|
||||
|
||||
export function textProxy(context: Automerge, objectId: ObjID, path?: Prop[], readonly?: boolean, heads?: Heads) : TextValue {
|
||||
const target = []
|
||||
Object.assign(target, {context, objectId, path, readonly: !!readonly, frozen: false, heads, cache: {}})
|
||||
return new Proxy(target, TextHandler)
|
||||
}
|
||||
|
||||
export function rootProxy<T>(context: Automerge, readonly?: boolean) : T {
|
||||
/* eslint-disable-next-line */
|
||||
return <any>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.delete(objectId, index)
|
||||
}
|
||||
return this
|
||||
},
|
||||
|
||||
fill(val: ScalarValue, start: number, end: number) {
|
||||
const [value, datatype] = import_value(val)
|
||||
const length = context.length(objectId)
|
||||
start = parseListIndex(start || 0)
|
||||
end = parseListIndex(end || length)
|
||||
for (let i = start; i < Math.min(end, length); i++) {
|
||||
context.put(objectId, i, value, datatype)
|
||||
}
|
||||
return this
|
||||
},
|
||||
|
||||
indexOf(o, start = 0) {
|
||||
const length = context.length(objectId)
|
||||
for (let i = start; i < length; i++) {
|
||||
const value = context.getWithType(objectId, i, heads)
|
||||
if (value && value[1] === o[OBJECT_ID] || value[1] === o) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
},
|
||||
|
||||
insertAt(index, ...values) {
|
||||
this.splice(index, 0, ...values)
|
||||
return this
|
||||
},
|
||||
|
||||
pop() {
|
||||
const length = context.length(objectId)
|
||||
if (length == 0) {
|
||||
return undefined
|
||||
}
|
||||
const last = valueAt(target, length - 1)
|
||||
context.delete(objectId, length - 1)
|
||||
return last
|
||||
},
|
||||
|
||||
push(...values) {
|
||||
const 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.delete(objectId, 0)
|
||||
return first
|
||||
},
|
||||
|
||||
splice(index, del, ...vals) {
|
||||
index = parseListIndex(index)
|
||||
del = parseListIndex(del)
|
||||
for (const 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")
|
||||
}
|
||||
const result : AutomergeValue[] = []
|
||||
for (let i = 0; i < del; i++) {
|
||||
const value = valueAt(target, index)
|
||||
if (value !== undefined) {
|
||||
result.push(value)
|
||||
}
|
||||
context.delete(objectId, index)
|
||||
}
|
||||
const values = vals.map((val) => import_value(val))
|
||||
for (const [value,datatype] of values) {
|
||||
switch (datatype) {
|
||||
case "list": {
|
||||
const list = context.insertObject(objectId, index, [])
|
||||
const proxyList = listProxy(context, list, [ ... path, index ], readonly);
|
||||
proxyList.splice(0,0,...value)
|
||||
break;
|
||||
}
|
||||
case "text": {
|
||||
const text = context.insertObject(objectId, index, "", "text")
|
||||
const proxyText = textProxy(context, text, [ ... path, index ], readonly);
|
||||
proxyText.splice(0,0,...value)
|
||||
break;
|
||||
}
|
||||
case "map": {
|
||||
const map = context.insertObject(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() {
|
||||
const i = 0;
|
||||
const iterator = {
|
||||
next: () => {
|
||||
const 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;
|
||||
const len = context.length(objectId, heads)
|
||||
const iterator = {
|
||||
next: () => {
|
||||
let value : undefined | number = undefined
|
||||
if (i < len) { value = i; i++ }
|
||||
return { value, done: true }
|
||||
}
|
||||
}
|
||||
return iterator
|
||||
},
|
||||
|
||||
values() {
|
||||
const i = 0;
|
||||
const iterator = {
|
||||
next: () => {
|
||||
const value = valueAt(target, i)
|
||||
if (value === undefined) {
|
||||
return { value: undefined, done: true }
|
||||
} else {
|
||||
return { value, done: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
return iterator
|
||||
},
|
||||
|
||||
toArray() : AutomergeValue[] {
|
||||
const list : AutomergeValue = []
|
||||
let value
|
||||
do {
|
||||
value = valueAt(target, list.length)
|
||||
if (value !== undefined) {
|
||||
list.push(value)
|
||||
}
|
||||
} while (value !== undefined)
|
||||
|
||||
return list
|
||||
},
|
||||
|
||||
map<T>(f: (AutomergeValue, number) => T) : T[] {
|
||||
return this.toArray().map(f)
|
||||
},
|
||||
|
||||
toString() : string {
|
||||
return this.toArray().toString()
|
||||
},
|
||||
|
||||
toLocaleString() : string {
|
||||
return this.toArray().toLocaleString()
|
||||
},
|
||||
|
||||
forEach(f: (AutomergeValue, number) => undefined ) {
|
||||
return this.toArray().forEach(f)
|
||||
},
|
||||
|
||||
// todo: real concat function is different
|
||||
concat(other: AutomergeValue[]) : AutomergeValue[] {
|
||||
return this.toArray().concat(other)
|
||||
},
|
||||
|
||||
every(f: (AutomergeValue, number) => boolean) : boolean {
|
||||
return this.toArray().every(f)
|
||||
},
|
||||
|
||||
filter(f: (AutomergeValue, number) => boolean) : AutomergeValue[] {
|
||||
return this.toArray().filter(f)
|
||||
},
|
||||
|
||||
find(f: (AutomergeValue, number) => boolean) : AutomergeValue | undefined {
|
||||
let index = 0
|
||||
for (let v of this) {
|
||||
if (f(v, index)) {
|
||||
return v
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
},
|
||||
|
||||
findIndex(f: (AutomergeValue, number) => boolean) : number {
|
||||
let index = 0
|
||||
for (let v of this) {
|
||||
if (f(v, index)) {
|
||||
return index
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
return -1
|
||||
},
|
||||
|
||||
includes(elem: AutomergeValue) : boolean {
|
||||
return this.find((e) => e === elem) !== undefined
|
||||
},
|
||||
|
||||
join(sep?: string) : string {
|
||||
return this.toArray().join(sep)
|
||||
},
|
||||
|
||||
// todo: remove the any
|
||||
reduce<T>(f: (any, AutomergeValue) => T, initalValue?: T) : T | undefined {
|
||||
return this.toArray().reduce(f,initalValue)
|
||||
},
|
||||
|
||||
// todo: remove the any
|
||||
reduceRight<T>(f: (any, AutomergeValue) => T, initalValue?: T) : T | undefined{
|
||||
return this.toArray().reduceRight(f,initalValue)
|
||||
},
|
||||
|
||||
lastIndexOf(search: AutomergeValue, fromIndex = +Infinity) : number {
|
||||
// this can be faster
|
||||
return this.toArray().lastIndexOf(search,fromIndex)
|
||||
},
|
||||
|
||||
slice(index?: number, num?: number) : AutomergeValue[] {
|
||||
return this.toArray().slice(index,num)
|
||||
},
|
||||
|
||||
some(f: (AutomergeValue, number) => boolean) : boolean {
|
||||
let index = 0;
|
||||
for (let v of this) {
|
||||
if (f(v,index)) {
|
||||
return true
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
||||
[Symbol.iterator]: function *() {
|
||||
let i = 0;
|
||||
let value = valueAt(target, i)
|
||||
while (value !== undefined) {
|
||||
yield value
|
||||
i += 1
|
||||
value = valueAt(target, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
return methods
|
||||
}
|
||||
|
||||
function textMethods(target) {
|
||||
const {context, objectId, heads } = target
|
||||
const methods = {
|
||||
set (index: number, value) {
|
||||
return this[index] = value
|
||||
},
|
||||
get (index: number) : AutomergeValue {
|
||||
return this[index]
|
||||
},
|
||||
toString () : string {
|
||||
return context.text(objectId, heads).replace(//g,'')
|
||||
},
|
||||
toSpans () : AutomergeValue[] {
|
||||
const spans : AutomergeValue[] = []
|
||||
let chars = ''
|
||||
const length = context.length(objectId)
|
||||
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 () : string {
|
||||
return this.toString()
|
||||
},
|
||||
indexOf(o, start = 0) {
|
||||
const text = context.text(objectId)
|
||||
return text.indexOf(o,start)
|
||||
}
|
||||
}
|
||||
return methods
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
|
||||
export { Text } from "./text"
|
||||
export { Counter } from "./counter"
|
||||
export { Int, Uint, Float64 } from "./numbers"
|
||||
|
||||
import { Counter } from "./counter"
|
||||
|
||||
export type AutomergeValue = ScalarValue | { [key: string]: AutomergeValue } | Array<AutomergeValue>
|
||||
export type MapValue = { [key: string]: AutomergeValue }
|
||||
export type ListValue = Array<AutomergeValue>
|
||||
export type TextValue = Array<AutomergeValue>
|
||||
export type ScalarValue = string | number | null | boolean | Date | Counter | Uint8Array
|
|
@ -1,21 +0,0 @@
|
|||
import { v4 } from 'uuid'
|
||||
|
||||
function defaultFactory() {
|
||||
return v4().replace(/-/g, '')
|
||||
}
|
||||
|
||||
let factory = defaultFactory
|
||||
|
||||
interface UUIDFactory extends Function {
|
||||
setFactory(f: typeof factory): void;
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
export const uuid : UUIDFactory = () => {
|
||||
return factory()
|
||||
}
|
||||
|
||||
uuid.setFactory = newFactory => { factory = newFactory }
|
||||
|
||||
uuid.reset = () => { factory = defaultFactory }
|
||||
|
|
@ -1,233 +0,0 @@
|
|||
import * as assert from 'assert'
|
||||
import * as Automerge from '../src'
|
||||
|
||||
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] });
|
||||
})
|
||||
|
||||
it('allows access to the backend', () => {
|
||||
let doc = Automerge.init()
|
||||
assert.deepEqual(Object.keys(Automerge.getBackend(doc)), ["ptr"])
|
||||
})
|
||||
|
||||
it('lists and text have indexof', () => {
|
||||
let doc = Automerge.from({ list: [0,1,2,3,4,5,6], text: new Automerge.Text("hello world") })
|
||||
console.log(doc.list.indexOf(5))
|
||||
console.log(doc.text.indexOf("world"))
|
||||
})
|
||||
})
|
||||
|
||||
describe('proxy lists', () => {
|
||||
it('behave like arrays', () => {
|
||||
let doc = Automerge.from({
|
||||
chars: ["a","b","c"],
|
||||
numbers: [20,3,100],
|
||||
repeats: [20,20,3,3,3,3,100,100]
|
||||
})
|
||||
let r1 = []
|
||||
doc = Automerge.change(doc, (d) => {
|
||||
assert.deepEqual(d.chars.concat([1,2]), ["a","b","c",1,2])
|
||||
assert.deepEqual(d.chars.map((n) => n + "!"), ["a!", "b!", "c!"])
|
||||
assert.deepEqual(d.numbers.map((n) => n + 10), [30, 13, 110])
|
||||
assert.deepEqual(d.numbers.toString(), "20,3,100")
|
||||
assert.deepEqual(d.numbers.toLocaleString(), "20,3,100")
|
||||
assert.deepEqual(d.numbers.forEach((n) => r1.push(n)), undefined)
|
||||
assert.deepEqual(d.numbers.every((n) => n > 1), true)
|
||||
assert.deepEqual(d.numbers.every((n) => n > 10), false)
|
||||
assert.deepEqual(d.numbers.filter((n) => n > 10), [20,100])
|
||||
assert.deepEqual(d.repeats.find((n) => n < 10), 3)
|
||||
assert.deepEqual(d.repeats.toArray().find((n) => n < 10), 3)
|
||||
assert.deepEqual(d.repeats.find((n) => n < 0), undefined)
|
||||
assert.deepEqual(d.repeats.findIndex((n) => n < 10), 2)
|
||||
assert.deepEqual(d.repeats.findIndex((n) => n < 0), -1)
|
||||
assert.deepEqual(d.repeats.toArray().findIndex((n) => n < 10), 2)
|
||||
assert.deepEqual(d.repeats.toArray().findIndex((n) => n < 0), -1)
|
||||
assert.deepEqual(d.numbers.includes(3), true)
|
||||
assert.deepEqual(d.numbers.includes(-3), false)
|
||||
assert.deepEqual(d.numbers.join("|"), "20|3|100")
|
||||
assert.deepEqual(d.numbers.join(), "20,3,100")
|
||||
assert.deepEqual(d.numbers.some((f) => f === 3), true)
|
||||
assert.deepEqual(d.numbers.some((f) => f < 0), false)
|
||||
assert.deepEqual(d.numbers.reduce((sum,n) => sum + n, 100), 223)
|
||||
assert.deepEqual(d.repeats.reduce((sum,n) => sum + n, 100), 352)
|
||||
assert.deepEqual(d.chars.reduce((sum,n) => sum + n, "="), "=abc")
|
||||
assert.deepEqual(d.chars.reduceRight((sum,n) => sum + n, "="), "=cba")
|
||||
assert.deepEqual(d.numbers.reduceRight((sum,n) => sum + n, 100), 223)
|
||||
assert.deepEqual(d.repeats.lastIndexOf(3), 5)
|
||||
assert.deepEqual(d.repeats.lastIndexOf(3,3), 3)
|
||||
})
|
||||
doc = Automerge.change(doc, (d) => {
|
||||
assert.deepEqual(d.numbers.fill(-1,1,2), [20,-1,100])
|
||||
assert.deepEqual(d.chars.fill("z",1,100), ["a","z","z"])
|
||||
})
|
||||
assert.deepEqual(r1, [20,3,100])
|
||||
assert.deepEqual(doc.numbers, [20,-1,100])
|
||||
assert.deepEqual(doc.chars, ["a","z","z"])
|
||||
})
|
||||
})
|
||||
|
||||
it('should obtain the same conflicts, regardless of merge order', () => {
|
||||
let s1 = Automerge.init()
|
||||
let s2 = Automerge.init()
|
||||
s1 = Automerge.change(s1, doc => { doc.x = 1; doc.y = 2 })
|
||||
s2 = Automerge.change(s2, doc => { doc.x = 3; doc.y = 4 })
|
||||
const m1 = Automerge.merge(Automerge.clone(s1), Automerge.clone(s2))
|
||||
const m2 = Automerge.merge(Automerge.clone(s2), Automerge.clone(s1))
|
||||
assert.deepStrictEqual(Automerge.getConflicts(m1, 'x'), Automerge.getConflicts(m2, 'x'))
|
||||
})
|
||||
})
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
import * as assert from 'assert'
|
||||
import { checkEncoded } from './helpers'
|
||||
import * as Automerge from '../src'
|
||||
import { encodeChange, decodeChange } from '../src'
|
||||
|
||||
describe('change encoding', () => {
|
||||
it('should encode text edits', () => {
|
||||
/*
|
||||
const change1 = {actor: 'aaaa', seq: 1, startOp: 1, time: 9, message: '', deps: [], ops: [
|
||||
{action: 'makeText', obj: '_root', key: 'text', insert: false, pred: []},
|
||||
{action: 'set', obj: '1@aaaa', elemId: '_head', insert: true, value: 'h', pred: []},
|
||||
{action: 'del', obj: '1@aaaa', elemId: '2@aaaa', insert: false, pred: ['2@aaaa']},
|
||||
{action: 'set', obj: '1@aaaa', elemId: '_head', insert: true, value: 'H', pred: []},
|
||||
{action: 'set', obj: '1@aaaa', elemId: '4@aaaa', insert: true, value: 'i', pred: []}
|
||||
]}
|
||||
*/
|
||||
const change1 = {actor: 'aaaa', seq: 1, startOp: 1, time: 9, message: null, deps: [], ops: [
|
||||
{action: 'makeText', obj: '_root', key: 'text', pred: []},
|
||||
{action: 'set', obj: '1@aaaa', elemId: '_head', insert: true, value: 'h', pred: []},
|
||||
{action: 'del', obj: '1@aaaa', elemId: '2@aaaa', pred: ['2@aaaa']},
|
||||
{action: 'set', obj: '1@aaaa', elemId: '_head', insert: true, value: 'H', pred: []},
|
||||
{action: 'set', obj: '1@aaaa', elemId: '4@aaaa', insert: true, value: 'i', pred: []}
|
||||
]}
|
||||
checkEncoded(encodeChange(change1), [
|
||||
0x85, 0x6f, 0x4a, 0x83, // magic bytes
|
||||
0xe2, 0xbd, 0xfb, 0xf5, // checksum
|
||||
1, 94, 0, 2, 0xaa, 0xaa, // chunkType: change, length, deps, actor 'aaaa'
|
||||
1, 1, 9, 0, 0, // seq, startOp, time, message, actor list
|
||||
12, 0x01, 4, 0x02, 4, // column count, objActor, objCtr
|
||||
0x11, 8, 0x13, 7, 0x15, 8, // keyActor, keyCtr, keyStr
|
||||
0x34, 4, 0x42, 6, // insert, action
|
||||
0x56, 6, 0x57, 3, // valLen, valRaw
|
||||
0x70, 6, 0x71, 2, 0x73, 2, // predNum, predActor, predCtr
|
||||
0, 1, 4, 0, // objActor column: null, 0, 0, 0, 0
|
||||
0, 1, 4, 1, // objCtr column: null, 1, 1, 1, 1
|
||||
0, 2, 0x7f, 0, 0, 1, 0x7f, 0, // keyActor column: null, null, 0, null, 0
|
||||
0, 1, 0x7c, 0, 2, 0x7e, 4, // keyCtr column: null, 0, 2, 0, 4
|
||||
0x7f, 4, 0x74, 0x65, 0x78, 0x74, 0, 4, // keyStr column: 'text', null, null, null, null
|
||||
1, 1, 1, 2, // insert column: false, true, false, true, true
|
||||
0x7d, 4, 1, 3, 2, 1, // action column: makeText, set, del, set, set
|
||||
0x7d, 0, 0x16, 0, 2, 0x16, // valLen column: 0, 0x16, 0, 0x16, 0x16
|
||||
0x68, 0x48, 0x69, // valRaw column: 'h', 'H', 'i'
|
||||
2, 0, 0x7f, 1, 2, 0, // predNum column: 0, 0, 1, 0, 0
|
||||
0x7f, 0, // predActor column: 0
|
||||
0x7f, 2 // predCtr column: 2
|
||||
])
|
||||
const decoded = decodeChange(encodeChange(change1))
|
||||
assert.deepStrictEqual(decoded, Object.assign({hash: decoded.hash}, change1))
|
||||
})
|
||||
|
||||
// FIXME - skipping this b/c it was never implemented in the rust impl and isnt trivial
|
||||
/*
|
||||
it.skip('should require strict ordering of preds', () => {
|
||||
const change = new Uint8Array([
|
||||
133, 111, 74, 131, 31, 229, 112, 44, 1, 105, 1, 58, 30, 190, 100, 253, 180, 180, 66, 49, 126,
|
||||
81, 142, 10, 3, 35, 140, 189, 231, 34, 145, 57, 66, 23, 224, 149, 64, 97, 88, 140, 168, 194,
|
||||
229, 4, 244, 209, 58, 138, 67, 140, 1, 152, 236, 250, 2, 0, 1, 4, 55, 234, 66, 242, 8, 21, 11,
|
||||
52, 1, 66, 2, 86, 3, 87, 10, 112, 2, 113, 3, 115, 4, 127, 9, 99, 111, 109, 109, 111, 110, 86,
|
||||
97, 114, 1, 127, 1, 127, 166, 1, 52, 48, 57, 49, 52, 57, 52, 53, 56, 50, 127, 2, 126, 0, 1,
|
||||
126, 139, 1, 0
|
||||
])
|
||||
assert.throws(() => { decodeChange(change) }, /operation IDs are not in ascending order/)
|
||||
})
|
||||
*/
|
||||
|
||||
describe('with trailing bytes', () => {
|
||||
let change = new Uint8Array([
|
||||
0x85, 0x6f, 0x4a, 0x83, // magic bytes
|
||||
0xb2, 0x98, 0x9e, 0xa9, // checksum
|
||||
1, 61, 0, 2, 0x12, 0x34, // chunkType: change, length, deps, actor '1234'
|
||||
1, 1, 252, 250, 220, 255, 5, // seq, startOp, time
|
||||
14, 73, 110, 105, 116, 105, 97, 108, 105, 122, 97, 116, 105, 111, 110, // message: 'Initialization'
|
||||
0, 6, // actor list, column count
|
||||
0x15, 3, 0x34, 1, 0x42, 2, // keyStr, insert, action
|
||||
0x56, 2, 0x57, 1, 0x70, 2, // valLen, valRaw, predNum
|
||||
0x7f, 1, 0x78, // keyStr: 'x'
|
||||
1, // insert: false
|
||||
0x7f, 1, // action: set
|
||||
0x7f, 19, // valLen: 1 byte of type uint
|
||||
1, // valRaw: 1
|
||||
0x7f, 0, // predNum: 0
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9 // 10 trailing bytes
|
||||
])
|
||||
|
||||
it('should allow decoding and re-encoding', () => {
|
||||
// NOTE: This calls the JavaScript encoding and decoding functions, even when the WebAssembly
|
||||
// backend is loaded. Should the wasm backend export its own functions for testing?
|
||||
checkEncoded(change, encodeChange(decodeChange(change)))
|
||||
})
|
||||
|
||||
it('should be preserved in document encoding', () => {
|
||||
const [doc] = Automerge.applyChanges(Automerge.init(), [change])
|
||||
const [reconstructed] = Automerge.getAllChanges(Automerge.load(Automerge.save(doc)))
|
||||
checkEncoded(change, reconstructed)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,20 +0,0 @@
|
|||
|
||||
import * as assert from 'assert'
|
||||
import * as Automerge from '../src'
|
||||
|
||||
describe('Automerge', () => {
|
||||
describe('basics', () => {
|
||||
it('should allow you to load incrementally', () => {
|
||||
let doc1 = Automerge.from({ foo: "bar" })
|
||||
let doc2 = Automerge.init();
|
||||
doc2 = Automerge.loadIncremental(doc2, Automerge.save(doc1))
|
||||
doc1 = Automerge.change(doc1, (d) => d.foo2 = "bar2")
|
||||
doc2 = Automerge.loadIncremental(doc2, Automerge.getBackend(doc1).saveIncremental() )
|
||||
doc1 = Automerge.change(doc1, (d) => d.foo = "bar2")
|
||||
doc2 = Automerge.loadIncremental(doc2, Automerge.getBackend(doc1).saveIncremental() )
|
||||
doc1 = Automerge.change(doc1, (d) => d.x = "y")
|
||||
doc2 = Automerge.loadIncremental(doc2, Automerge.getBackend(doc1).saveIncremental() )
|
||||
assert.deepEqual(doc1,doc2)
|
||||
})
|
||||
})
|
||||
})
|
File diff suppressed because it is too large
Load diff
|
@ -1,698 +0,0 @@
|
|||
import * as assert from 'assert'
|
||||
import * as Automerge from '../src'
|
||||
import { assertEqualsOneOf } from './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 replace control characters from toString()', () => {
|
||||
assert.strictEqual(s1.text.toString(), 'a\uFFFC')
|
||||
})
|
||||
|
||||
it('should allow control characters to be updated', () => {
|
||||
const s2 = Automerge.change(s1, doc => doc.text.get(1).attribute = 'italic')
|
||||
const s3 = Automerge.load(Automerge.save(s2))
|
||||
assert.strictEqual(s1.text.get(1).attribute, 'bold')
|
||||
assert.strictEqual(s2.text.get(1).attribute, 'italic')
|
||||
assert.strictEqual(s3.text.get(1).attribute, 'italic')
|
||||
})
|
||||
|
||||
describe('spans interface to Text', () => {
|
||||
it('should return a simple string as a single span', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('hello world')
|
||||
})
|
||||
assert.deepEqual(s1.text.toSpans(), ['hello world'])
|
||||
})
|
||||
it('should return an empty string as an empty array', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text()
|
||||
})
|
||||
assert.deepEqual(s1.text.toSpans(), [])
|
||||
})
|
||||
it('should split a span at a control character', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('hello world')
|
||||
doc.text.insertAt(5, { attributes: { bold: true } })
|
||||
})
|
||||
assert.deepEqual(s1.text.toSpans(),
|
||||
['hello', { attributes: { bold: true } }, ' world'])
|
||||
})
|
||||
it('should allow consecutive control characters', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('hello world')
|
||||
doc.text.insertAt(5, { attributes: { bold: true } })
|
||||
doc.text.insertAt(6, { attributes: { italic: true } })
|
||||
})
|
||||
assert.deepEqual(s1.text.toSpans(),
|
||||
['hello',
|
||||
{ attributes: { bold: true } },
|
||||
{ attributes: { italic: true } },
|
||||
' world'
|
||||
])
|
||||
})
|
||||
it('should allow non-consecutive control characters', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('hello world')
|
||||
doc.text.insertAt(5, { attributes: { bold: true } })
|
||||
doc.text.insertAt(12, { attributes: { italic: true } })
|
||||
})
|
||||
assert.deepEqual(s1.text.toSpans(),
|
||||
['hello',
|
||||
{ attributes: { bold: true } },
|
||||
' world',
|
||||
{ attributes: { italic: true } }
|
||||
])
|
||||
})
|
||||
|
||||
it('should be convertable into a Quill delta', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('Gandalf the Grey')
|
||||
doc.text.insertAt(0, { attributes: { bold: true } })
|
||||
doc.text.insertAt(7 + 1, { attributes: { bold: null } })
|
||||
doc.text.insertAt(12 + 2, { attributes: { color: '#cccccc' } })
|
||||
})
|
||||
|
||||
let deltaDoc = automergeTextToDeltaDoc(s1.text)
|
||||
|
||||
// From https://quilljs.com/docs/delta/
|
||||
let expectedDoc = [
|
||||
{ insert: 'Gandalf', attributes: { bold: true } },
|
||||
{ insert: ' the ' },
|
||||
{ insert: 'Grey', attributes: { color: '#cccccc' } }
|
||||
]
|
||||
|
||||
assert.deepEqual(deltaDoc, expectedDoc)
|
||||
})
|
||||
|
||||
it('should support embeds', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('')
|
||||
doc.text.insertAt(0, { attributes: { link: 'https://quilljs.com' } })
|
||||
doc.text.insertAt(1, {
|
||||
image: 'https://quilljs.com/assets/images/icon.png'
|
||||
})
|
||||
doc.text.insertAt(2, { attributes: { link: null } })
|
||||
})
|
||||
|
||||
let deltaDoc = automergeTextToDeltaDoc(s1.text)
|
||||
|
||||
// From https://quilljs.com/docs/delta/
|
||||
let expectedDoc = [{
|
||||
// An image link
|
||||
insert: {
|
||||
image: 'https://quilljs.com/assets/images/icon.png'
|
||||
},
|
||||
attributes: {
|
||||
link: 'https://quilljs.com'
|
||||
}
|
||||
}]
|
||||
|
||||
assert.deepEqual(deltaDoc, expectedDoc)
|
||||
})
|
||||
|
||||
it('should handle concurrent overlapping spans', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('Gandalf the Grey')
|
||||
})
|
||||
|
||||
let s2 = Automerge.merge(Automerge.init(), s1)
|
||||
|
||||
let s3 = Automerge.change(s1, doc => {
|
||||
doc.text.insertAt(8, { attributes: { bold: true } })
|
||||
doc.text.insertAt(16 + 1, { attributes: { bold: null } })
|
||||
})
|
||||
|
||||
let s4 = Automerge.change(s2, doc => {
|
||||
doc.text.insertAt(0, { attributes: { bold: true } })
|
||||
doc.text.insertAt(11 + 1, { attributes: { bold: null } })
|
||||
})
|
||||
|
||||
let merged = Automerge.merge(s3, s4)
|
||||
|
||||
let deltaDoc = automergeTextToDeltaDoc(merged.text)
|
||||
|
||||
// From https://quilljs.com/docs/delta/
|
||||
let expectedDoc = [
|
||||
{ insert: 'Gandalf the Grey', attributes: { bold: true } },
|
||||
]
|
||||
|
||||
assert.deepEqual(deltaDoc, expectedDoc)
|
||||
})
|
||||
|
||||
it('should handle debolding spans', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('Gandalf the Grey')
|
||||
})
|
||||
|
||||
let s2 = Automerge.merge(Automerge.init(), s1)
|
||||
|
||||
let s3 = Automerge.change(s1, doc => {
|
||||
doc.text.insertAt(0, { attributes: { bold: true } })
|
||||
doc.text.insertAt(16 + 1, { attributes: { bold: null } })
|
||||
})
|
||||
|
||||
let s4 = Automerge.change(s2, doc => {
|
||||
doc.text.insertAt(8, { attributes: { bold: null } })
|
||||
doc.text.insertAt(11 + 1, { attributes: { bold: true } })
|
||||
})
|
||||
|
||||
|
||||
let merged = Automerge.merge(s3, s4)
|
||||
|
||||
let deltaDoc = automergeTextToDeltaDoc(merged.text)
|
||||
|
||||
// From https://quilljs.com/docs/delta/
|
||||
let expectedDoc = [
|
||||
{ insert: 'Gandalf ', attributes: { bold: true } },
|
||||
{ insert: 'the' },
|
||||
{ insert: ' Grey', attributes: { bold: true } },
|
||||
]
|
||||
|
||||
assert.deepEqual(deltaDoc, expectedDoc)
|
||||
})
|
||||
|
||||
// xxx: how would this work for colors?
|
||||
it('should handle destyling across destyled spans', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('Gandalf the Grey')
|
||||
})
|
||||
|
||||
let s2 = Automerge.merge(Automerge.init(), s1)
|
||||
|
||||
let s3 = Automerge.change(s1, doc => {
|
||||
doc.text.insertAt(0, { attributes: { bold: true } })
|
||||
doc.text.insertAt(16 + 1, { attributes: { bold: null } })
|
||||
})
|
||||
|
||||
let s4 = Automerge.change(s2, doc => {
|
||||
doc.text.insertAt(8, { attributes: { bold: null } })
|
||||
doc.text.insertAt(11 + 1, { attributes: { bold: true } })
|
||||
})
|
||||
|
||||
let merged = Automerge.merge(s3, s4)
|
||||
|
||||
let final = Automerge.change(merged, doc => {
|
||||
doc.text.insertAt(3 + 1, { attributes: { bold: null } })
|
||||
doc.text.insertAt(doc.text.length, { attributes: { bold: true } })
|
||||
})
|
||||
|
||||
let deltaDoc = automergeTextToDeltaDoc(final.text)
|
||||
|
||||
// From https://quilljs.com/docs/delta/
|
||||
let expectedDoc = [
|
||||
{ insert: 'Gan', attributes: { bold: true } },
|
||||
{ insert: 'dalf the Grey' },
|
||||
]
|
||||
|
||||
assert.deepEqual(deltaDoc, expectedDoc)
|
||||
})
|
||||
|
||||
it('should apply an insert', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('Hello world')
|
||||
})
|
||||
|
||||
const delta = [
|
||||
{ retain: 6 },
|
||||
{ insert: 'reader' },
|
||||
{ delete: 5 }
|
||||
]
|
||||
|
||||
let s2 = Automerge.change(s1, doc => {
|
||||
applyDeltaDocToAutomergeText(delta, doc)
|
||||
})
|
||||
|
||||
//assert.strictEqual(s2.text.join(''), 'Hello reader')
|
||||
assert.strictEqual(s2.text.toString(), 'Hello reader')
|
||||
})
|
||||
|
||||
it('should apply an insert with control characters', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('Hello world')
|
||||
})
|
||||
|
||||
const delta = [
|
||||
{ retain: 6 },
|
||||
{ insert: 'reader', attributes: { bold: true } },
|
||||
{ delete: 5 },
|
||||
{ insert: '!' }
|
||||
]
|
||||
|
||||
let s2 = Automerge.change(s1, doc => {
|
||||
applyDeltaDocToAutomergeText(delta, doc)
|
||||
})
|
||||
|
||||
assert.strictEqual(s2.text.toString(), 'Hello \uFFFCreader\uFFFC!')
|
||||
assert.deepEqual(s2.text.toSpans(), [
|
||||
"Hello ",
|
||||
{ attributes: { bold: true } },
|
||||
"reader",
|
||||
{ attributes: { bold: null } },
|
||||
"!"
|
||||
])
|
||||
})
|
||||
|
||||
it('should account for control characters in retain/delete lengths', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('Hello world')
|
||||
doc.text.insertAt(4, { attributes: { color: '#ccc' } })
|
||||
doc.text.insertAt(10, { attributes: { color: '#f00' } })
|
||||
})
|
||||
|
||||
const delta = [
|
||||
{ retain: 6 },
|
||||
{ insert: 'reader', attributes: { bold: true } },
|
||||
{ delete: 5 },
|
||||
{ insert: '!' }
|
||||
]
|
||||
|
||||
let s2 = Automerge.change(s1, doc => {
|
||||
applyDeltaDocToAutomergeText(delta, doc)
|
||||
})
|
||||
|
||||
assert.strictEqual(s2.text.toString(), 'Hell\uFFFCo \uFFFCreader\uFFFC\uFFFC!')
|
||||
assert.deepEqual(s2.text.toSpans(), [
|
||||
"Hell",
|
||||
{ attributes: { color: '#ccc'} },
|
||||
"o ",
|
||||
{ attributes: { bold: true } },
|
||||
"reader",
|
||||
{ attributes: { bold: null } },
|
||||
{ attributes: { color: '#f00'} },
|
||||
"!"
|
||||
])
|
||||
})
|
||||
|
||||
it('should support embeds', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('')
|
||||
})
|
||||
|
||||
let deltaDoc = [{
|
||||
// An image link
|
||||
insert: {
|
||||
image: 'https://quilljs.com/assets/images/icon.png'
|
||||
},
|
||||
attributes: {
|
||||
link: 'https://quilljs.com'
|
||||
}
|
||||
}]
|
||||
|
||||
let s2 = Automerge.change(s1, doc => {
|
||||
applyDeltaDocToAutomergeText(deltaDoc, doc)
|
||||
})
|
||||
|
||||
assert.deepEqual(s2.text.toSpans(), [
|
||||
{ attributes: { link: 'https://quilljs.com' } },
|
||||
{ image: 'https://quilljs.com/assets/images/icon.png'},
|
||||
{ attributes: { link: null } },
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should support unicode when creating text', () => {
|
||||
s1 = Automerge.from({
|
||||
text: new Automerge.Text('🐦')
|
||||
})
|
||||
assert.strictEqual(s1.text.get(0), '🐦')
|
||||
})
|
||||
})
|
|
@ -1,32 +0,0 @@
|
|||
import * as assert from 'assert'
|
||||
import * as Automerge from '../src'
|
||||
|
||||
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,22 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"sourceMap": false,
|
||||
"declaration": true,
|
||||
"resolveJsonModule": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"noImplicitAny": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": [ "src/**/*" ],
|
||||
"exclude": [
|
||||
"./dist/**/*",
|
||||
"./node_modules"
|
||||
]
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: [
|
||||
'@typescript-eslint',
|
||||
],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
],
|
||||
};
|
|
@ -1,13 +0,0 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
{
|
||||
"collaborators": [
|
||||
"Orion Henry <orion@inkandswitch.com>",
|
||||
"Alex Good <alex@memoryandthought.me>",
|
||||
"Martin Kleppmann"
|
||||
],
|
||||
"name": "@automerge/automerge-wasm",
|
||||
"description": "wasm-bindgen bindings to the automerge rust implementation",
|
||||
"homepage": "https://github.com/automerge/automerge-rs/tree/main/automerge-wasm",
|
||||
"repository": "github:automerge/automerge-rs",
|
||||
"version": "0.1.9",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"README.md",
|
||||
"LICENSE",
|
||||
"package.json",
|
||||
"index.d.ts",
|
||||
"nodejs/bindgen.js",
|
||||
"nodejs/bindgen_bg.wasm",
|
||||
"bundler/bindgen.js",
|
||||
"bundler/bindgen_bg.js",
|
||||
"bundler/bindgen_bg.wasm"
|
||||
],
|
||||
"private": false,
|
||||
"types": "index.d.ts",
|
||||
"module": "./bundler/bindgen.js",
|
||||
"main": "./nodejs/bindgen.js",
|
||||
"scripts": {
|
||||
"lint": "eslint test/*.ts index.d.ts",
|
||||
"debug": "cross-env PROFILE=dev yarn buildall",
|
||||
"build": "cross-env PROFILE=dev FEATURES='' yarn buildall",
|
||||
"release": "cross-env PROFILE=release yarn buildall",
|
||||
"buildall": "cross-env TARGET=nodejs yarn target && cross-env TARGET=bundler yarn target",
|
||||
"target": "rimraf ./$TARGET && wasm-pack build --target $TARGET --$PROFILE --out-name bindgen -d $TARGET -- $FEATURES",
|
||||
"test": "ts-mocha -p tsconfig.json --type-check --bail --full-trace test/*.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/expect": "^24.3.0",
|
||||
"@types/jest": "^27.4.0",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"@types/node": "^17.0.13",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.25.0",
|
||||
"@typescript-eslint/parser": "^5.25.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.16.0",
|
||||
"fast-sha256": "^1.3.0",
|
||||
"mocha": "^9.1.3",
|
||||
"pako": "^2.0.4",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-mocha": "^9.0.2",
|
||||
"typescript": "^4.6.4"
|
||||
},
|
||||
"exports": {
|
||||
"browser": "./bundler/bindgen.js",
|
||||
"require": "./nodejs/bindgen.js"
|
||||
}
|
||||
}
|
|
@ -1,720 +0,0 @@
|
|||
use crate::value::Datatype;
|
||||
use crate::Automerge;
|
||||
use automerge as am;
|
||||
use automerge::transaction::Transactable;
|
||||
use automerge::{Change, ChangeHash, ObjType, Prop};
|
||||
use js_sys::{Array, Function, Object, Reflect, Symbol, Uint8Array};
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
use std::fmt::Display;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
use crate::{observer::Patch, ObjId, Value};
|
||||
|
||||
const RAW_DATA_SYMBOL: &str = "_am_raw_value_";
|
||||
const DATATYPE_SYMBOL: &str = "_am_datatype_";
|
||||
const RAW_OBJECT_SYMBOL: &str = "_am_objectId";
|
||||
const META_SYMBOL: &str = "_am_meta";
|
||||
|
||||
pub(crate) struct JS(pub(crate) JsValue);
|
||||
pub(crate) struct AR(pub(crate) 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 {
|
||||
JS(heads
|
||||
.iter()
|
||||
.map(|h| JsValue::from_str(&h.to_string()))
|
||||
.collect::<Array>()
|
||||
.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<BTreeSet<ChangeHash>> for JS {
|
||||
fn from(heads: BTreeSet<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(serde_wasm_bindgen::from_value(key).map_err(to_js_err)?);
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<JS> for BTreeSet<ChangeHash> {
|
||||
type Error = JsValue;
|
||||
|
||||
fn try_from(value: JS) -> Result<Self, Self::Error> {
|
||||
let mut result = BTreeSet::new();
|
||||
for key in Reflect::own_keys(&value.0)?.iter() {
|
||||
if let Some(true) = Reflect::get(&value.0, &key)?.as_bool() {
|
||||
result.insert(serde_wasm_bindgen::from_value(key).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(serde_wasm_bindgen::from_value).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(serde_wasm_bindgen::from_value).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 = changes.iter().try_fold(Vec::new(), |mut acc, arr| {
|
||||
match automerge::Change::try_from(arr.to_vec().as_slice()) {
|
||||
Ok(c) => acc.push(c),
|
||||
Err(e) => return Err(to_js_err(e)),
|
||||
}
|
||||
Ok(acc)
|
||||
})?;
|
||||
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<(ObjType, Vec<(Prop, JsValue)>)> {
|
||||
match datatype.as_deref() {
|
||||
Some("map") => {
|
||||
let map = value.clone().dyn_into::<js_sys::Object>().ok()?;
|
||||
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((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((ObjType::List, list))
|
||||
}
|
||||
Some("text") => {
|
||||
let text = value.as_string()?;
|
||||
let text = text
|
||||
.chars()
|
||||
.enumerate()
|
||||
.map(|(i, ch)| (i.into(), ch.to_string().into()))
|
||||
.collect();
|
||||
Some((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((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((ObjType::Map, map))
|
||||
} else if let Some(text) = value.as_string() {
|
||||
let text = text
|
||||
.chars()
|
||||
.enumerate()
|
||||
.map(|(i, ch)| (i.into(), ch.to_string().into()))
|
||||
.collect();
|
||||
Some((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(serde_wasm_bindgen::from_value).collect();
|
||||
heads.ok()
|
||||
}
|
||||
|
||||
impl Automerge {
|
||||
pub(crate) fn export_object(
|
||||
&self,
|
||||
obj: &ObjId,
|
||||
datatype: Datatype,
|
||||
heads: Option<&Vec<ChangeHash>>,
|
||||
meta: &JsValue,
|
||||
) -> Result<JsValue, JsValue> {
|
||||
let result = if datatype.is_sequence() {
|
||||
self.wrap_object(
|
||||
self.export_list(obj, heads, meta)?,
|
||||
datatype,
|
||||
&obj.to_string().into(),
|
||||
meta,
|
||||
)?
|
||||
} else {
|
||||
self.wrap_object(
|
||||
self.export_map(obj, heads, meta)?,
|
||||
datatype,
|
||||
&obj.to_string().into(),
|
||||
meta,
|
||||
)?
|
||||
};
|
||||
Ok(result.into())
|
||||
}
|
||||
|
||||
pub(crate) fn export_map(
|
||||
&self,
|
||||
obj: &ObjId,
|
||||
heads: Option<&Vec<ChangeHash>>,
|
||||
meta: &JsValue,
|
||||
) -> Result<Object, JsValue> {
|
||||
let keys = self.doc.keys(obj);
|
||||
let map = Object::new();
|
||||
for k in keys {
|
||||
let val_and_id = if let Some(heads) = heads {
|
||||
self.doc.get_at(obj, &k, heads)
|
||||
} else {
|
||||
self.doc.get(obj, &k)
|
||||
};
|
||||
if let Ok(Some((val, id))) = val_and_id {
|
||||
let subval = match val {
|
||||
Value::Object(o) => self.export_object(&id, o.into(), heads, meta)?,
|
||||
Value::Scalar(_) => self.export_value(alloc(&val))?,
|
||||
};
|
||||
Reflect::set(&map, &k.into(), &subval)?;
|
||||
};
|
||||
}
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
pub(crate) fn export_list(
|
||||
&self,
|
||||
obj: &ObjId,
|
||||
heads: Option<&Vec<ChangeHash>>,
|
||||
meta: &JsValue,
|
||||
) -> Result<Object, JsValue> {
|
||||
let len = self.doc.length(obj);
|
||||
let array = Array::new();
|
||||
for i in 0..len {
|
||||
let val_and_id = if let Some(heads) = heads {
|
||||
self.doc.get_at(obj, i as usize, heads)
|
||||
} else {
|
||||
self.doc.get(obj, i as usize)
|
||||
};
|
||||
if let Ok(Some((val, id))) = val_and_id {
|
||||
let subval = match val {
|
||||
Value::Object(o) => self.export_object(&id, o.into(), heads, meta)?,
|
||||
Value::Scalar(_) => self.export_value(alloc(&val))?,
|
||||
};
|
||||
array.push(&subval);
|
||||
};
|
||||
}
|
||||
|
||||
Ok(array.into())
|
||||
}
|
||||
|
||||
pub(crate) fn export_value(
|
||||
&self,
|
||||
(datatype, raw_value): (Datatype, JsValue),
|
||||
) -> Result<JsValue, JsValue> {
|
||||
if let Some(function) = self.external_types.get(&datatype) {
|
||||
let wrapped_value = function.call1(&JsValue::undefined(), &raw_value)?;
|
||||
if let Ok(o) = wrapped_value.dyn_into::<Object>() {
|
||||
let key = Symbol::for_(RAW_DATA_SYMBOL);
|
||||
set_hidden_value(&o, &key, &raw_value)?;
|
||||
let key = Symbol::for_(DATATYPE_SYMBOL);
|
||||
set_hidden_value(&o, &key, datatype)?;
|
||||
Ok(o.into())
|
||||
} else {
|
||||
Err(to_js_err(format!(
|
||||
"data handler for type {} did not return a valid object",
|
||||
datatype
|
||||
)))
|
||||
}
|
||||
} else {
|
||||
Ok(raw_value)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn unwrap_object(
|
||||
&self,
|
||||
ext_val: &Object,
|
||||
) -> Result<(Object, Datatype, JsValue), JsValue> {
|
||||
let inner = Reflect::get(ext_val, &Symbol::for_(RAW_DATA_SYMBOL))?;
|
||||
|
||||
let datatype = Reflect::get(ext_val, &Symbol::for_(DATATYPE_SYMBOL))?.try_into();
|
||||
|
||||
let mut id = Reflect::get(ext_val, &Symbol::for_(RAW_OBJECT_SYMBOL))?;
|
||||
if id.is_undefined() {
|
||||
id = "_root".into();
|
||||
}
|
||||
|
||||
let inner = inner
|
||||
.dyn_into::<Object>()
|
||||
.unwrap_or_else(|_| ext_val.clone());
|
||||
let datatype = datatype.unwrap_or_else(|_| {
|
||||
if Array::is_array(&inner) {
|
||||
Datatype::List
|
||||
} else {
|
||||
Datatype::Map
|
||||
}
|
||||
});
|
||||
Ok((inner, datatype, id))
|
||||
}
|
||||
|
||||
pub(crate) fn unwrap_scalar(&self, ext_val: JsValue) -> Result<JsValue, JsValue> {
|
||||
let inner = Reflect::get(&ext_val, &Symbol::for_(RAW_DATA_SYMBOL))?;
|
||||
if !inner.is_undefined() {
|
||||
Ok(inner)
|
||||
} else {
|
||||
Ok(ext_val)
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_wrap_object(
|
||||
&self,
|
||||
(datatype, raw_value): (Datatype, JsValue),
|
||||
id: &ObjId,
|
||||
meta: &JsValue,
|
||||
) -> Result<JsValue, JsValue> {
|
||||
if let Ok(obj) = raw_value.clone().dyn_into::<Object>() {
|
||||
let result = self.wrap_object(obj, datatype, &id.to_string().into(), meta)?;
|
||||
Ok(result.into())
|
||||
} else {
|
||||
self.export_value((datatype, raw_value))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn wrap_object(
|
||||
&self,
|
||||
value: Object,
|
||||
datatype: Datatype,
|
||||
id: &JsValue,
|
||||
meta: &JsValue,
|
||||
) -> Result<Object, JsValue> {
|
||||
let value = if let Some(function) = self.external_types.get(&datatype) {
|
||||
let wrapped_value = function.call1(&JsValue::undefined(), &value)?;
|
||||
let wrapped_object = wrapped_value.dyn_into::<Object>().map_err(|_| {
|
||||
to_js_err(format!(
|
||||
"data handler for type {} did not return a valid object",
|
||||
datatype
|
||||
))
|
||||
})?;
|
||||
set_hidden_value(&wrapped_object, &Symbol::for_(RAW_DATA_SYMBOL), value)?;
|
||||
wrapped_object
|
||||
} else {
|
||||
value
|
||||
};
|
||||
set_hidden_value(&value, &Symbol::for_(DATATYPE_SYMBOL), datatype)?;
|
||||
set_hidden_value(&value, &Symbol::for_(RAW_OBJECT_SYMBOL), id)?;
|
||||
set_hidden_value(&value, &Symbol::for_(META_SYMBOL), meta)?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub(crate) fn apply_patch_to_array(
|
||||
&self,
|
||||
array: &Object,
|
||||
patch: &Patch,
|
||||
meta: &JsValue,
|
||||
) -> Result<Object, JsValue> {
|
||||
let result = Array::from(array); // shallow copy
|
||||
match patch {
|
||||
Patch::PutSeq { index, value, .. } => {
|
||||
let sub_val = self.maybe_wrap_object(alloc(&value.0), &value.1, meta)?;
|
||||
Reflect::set(&result, &(*index as f64).into(), &sub_val)?;
|
||||
Ok(result.into())
|
||||
}
|
||||
Patch::DeleteSeq { index, .. } => self.sub_splice(result, *index, 1, &[], meta),
|
||||
Patch::Insert { index, values, .. } => self.sub_splice(result, *index, 0, values, meta),
|
||||
Patch::Increment { prop, value, .. } => {
|
||||
if let Prop::Seq(index) = prop {
|
||||
let index = (*index as f64).into();
|
||||
let old_val = Reflect::get(&result, &index)?;
|
||||
let old_val = self.unwrap_scalar(old_val)?;
|
||||
if let Some(old) = old_val.as_f64() {
|
||||
let new_value: Value<'_> =
|
||||
am::ScalarValue::counter(old as i64 + *value).into();
|
||||
Reflect::set(&result, &index, &self.export_value(alloc(&new_value))?)?;
|
||||
Ok(result.into())
|
||||
} else {
|
||||
Err(to_js_err("cant increment a non number value"))
|
||||
}
|
||||
} else {
|
||||
Err(to_js_err("cant increment a key on a seq"))
|
||||
}
|
||||
}
|
||||
Patch::DeleteMap { .. } => Err(to_js_err("cannot delete from a seq")),
|
||||
Patch::PutMap { .. } => Err(to_js_err("cannot set key in seq")),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn apply_patch_to_map(
|
||||
&self,
|
||||
map: &Object,
|
||||
patch: &Patch,
|
||||
meta: &JsValue,
|
||||
) -> Result<Object, JsValue> {
|
||||
let result = Object::assign(&Object::new(), map); // shallow copy
|
||||
match patch {
|
||||
Patch::PutMap { key, value, .. } => {
|
||||
let sub_val = self.maybe_wrap_object(alloc(&value.0), &value.1, meta)?;
|
||||
Reflect::set(&result, &key.into(), &sub_val)?;
|
||||
Ok(result)
|
||||
}
|
||||
Patch::DeleteMap { key, .. } => {
|
||||
Reflect::delete_property(&result, &key.into())?;
|
||||
Ok(result)
|
||||
}
|
||||
Patch::Increment { prop, value, .. } => {
|
||||
if let Prop::Map(key) = prop {
|
||||
let key = key.into();
|
||||
let old_val = Reflect::get(&result, &key)?;
|
||||
let old_val = self.unwrap_scalar(old_val)?;
|
||||
if let Some(old) = old_val.as_f64() {
|
||||
let new_value: Value<'_> =
|
||||
am::ScalarValue::counter(old as i64 + *value).into();
|
||||
Reflect::set(&result, &key, &self.export_value(alloc(&new_value))?)?;
|
||||
Ok(result)
|
||||
} else {
|
||||
Err(to_js_err("cant increment a non number value"))
|
||||
}
|
||||
} else {
|
||||
Err(to_js_err("cant increment an index on a map"))
|
||||
}
|
||||
}
|
||||
Patch::Insert { .. } => Err(to_js_err("cannot insert into map")),
|
||||
Patch::DeleteSeq { .. } => Err(to_js_err("cannot splice a map")),
|
||||
Patch::PutSeq { .. } => Err(to_js_err("cannot array index a map")),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn apply_patch(
|
||||
&self,
|
||||
obj: Object,
|
||||
patch: &Patch,
|
||||
depth: usize,
|
||||
meta: &JsValue,
|
||||
) -> Result<Object, JsValue> {
|
||||
let (inner, datatype, id) = self.unwrap_object(&obj)?;
|
||||
let prop = patch.path().get(depth).map(|p| prop_to_js(&p.1));
|
||||
let result = if let Some(prop) = prop {
|
||||
if let Ok(sub_obj) = Reflect::get(&inner, &prop)?.dyn_into::<Object>() {
|
||||
let new_value = self.apply_patch(sub_obj, patch, depth + 1, meta)?;
|
||||
let result = shallow_copy(&inner);
|
||||
Reflect::set(&result, &prop, &new_value)?;
|
||||
Ok(result)
|
||||
} else {
|
||||
// if a patch is trying to access a deleted object make no change
|
||||
// short circuit the wrap process
|
||||
return Ok(obj);
|
||||
}
|
||||
} else if Array::is_array(&inner) {
|
||||
self.apply_patch_to_array(&inner, patch, meta)
|
||||
} else {
|
||||
self.apply_patch_to_map(&inner, patch, meta)
|
||||
}?;
|
||||
|
||||
self.wrap_object(result, datatype, &id, meta)
|
||||
}
|
||||
|
||||
fn sub_splice(
|
||||
&self,
|
||||
o: Array,
|
||||
index: usize,
|
||||
num_del: usize,
|
||||
values: &[(Value<'_>, ObjId)],
|
||||
meta: &JsValue,
|
||||
) -> Result<Object, JsValue> {
|
||||
let args: Array = values
|
||||
.iter()
|
||||
.map(|v| self.maybe_wrap_object(alloc(&v.0), &v.1, meta))
|
||||
.collect::<Result<_, _>>()?;
|
||||
args.unshift(&(num_del as u32).into());
|
||||
args.unshift(&(index as u32).into());
|
||||
let method = Reflect::get(&o, &"splice".into())?.dyn_into::<Function>()?;
|
||||
Reflect::apply(&method, &o, &args)?;
|
||||
Ok(o.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn alloc(value: &Value<'_>) -> (Datatype, JsValue) {
|
||||
match value {
|
||||
am::Value::Object(o) => match o {
|
||||
ObjType::Map => (Datatype::Map, Object::new().into()),
|
||||
ObjType::Table => (Datatype::Table, Object::new().into()),
|
||||
ObjType::List => (Datatype::List, Array::new().into()),
|
||||
ObjType::Text => (Datatype::Text, Array::new().into()),
|
||||
},
|
||||
am::Value::Scalar(s) => match s.as_ref() {
|
||||
am::ScalarValue::Bytes(v) => (Datatype::Bytes, Uint8Array::from(v.as_slice()).into()),
|
||||
am::ScalarValue::Str(v) => (Datatype::Str, v.to_string().into()),
|
||||
am::ScalarValue::Int(v) => (Datatype::Int, (*v as f64).into()),
|
||||
am::ScalarValue::Uint(v) => (Datatype::Uint, (*v as f64).into()),
|
||||
am::ScalarValue::F64(v) => (Datatype::F64, (*v).into()),
|
||||
am::ScalarValue::Counter(v) => (Datatype::Counter, (f64::from(v)).into()),
|
||||
am::ScalarValue::Timestamp(v) => (
|
||||
Datatype::Timestamp,
|
||||
js_sys::Date::new(&(*v as f64).into()).into(),
|
||||
),
|
||||
am::ScalarValue::Boolean(v) => (Datatype::Boolean, (*v).into()),
|
||||
am::ScalarValue::Null => (Datatype::Null, JsValue::null()),
|
||||
am::ScalarValue::Unknown { bytes, type_code } => (
|
||||
Datatype::Unknown(*type_code),
|
||||
Uint8Array::from(bytes.as_slice()).into(),
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn set_hidden_value<V: Into<JsValue>>(o: &Object, key: &Symbol, value: V) -> Result<(), JsValue> {
|
||||
let definition = Object::new();
|
||||
js_set(&definition, "value", &value.into())?;
|
||||
js_set(&definition, "writable", false)?;
|
||||
js_set(&definition, "enumerable", false)?;
|
||||
js_set(&definition, "configurable", false)?;
|
||||
Object::define_property(o, &key.into(), &definition);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn shallow_copy(obj: &Object) -> Object {
|
||||
if Array::is_array(obj) {
|
||||
Array::from(obj).into()
|
||||
} else {
|
||||
Object::assign(&Object::new(), obj)
|
||||
}
|
||||
}
|
||||
|
||||
fn prop_to_js(prop: &Prop) -> JsValue {
|
||||
match prop {
|
||||
Prop::Map(key) => key.into(),
|
||||
Prop::Seq(index) => (*index as f64).into(),
|
||||
}
|
||||
}
|
|
@ -1,874 +0,0 @@
|
|||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/automerge/automerge-rs/main/img/brandmark.svg",
|
||||
html_favicon_url = "https:///raw.githubusercontent.com/automerge/automerge-rs/main/img/favicon.ico"
|
||||
)]
|
||||
#![warn(
|
||||
missing_debug_implementations,
|
||||
// missing_docs, // TODO: add documentation!
|
||||
rust_2021_compatibility,
|
||||
rust_2018_idioms,
|
||||
unreachable_pub,
|
||||
bad_style,
|
||||
const_err,
|
||||
dead_code,
|
||||
improper_ctypes,
|
||||
non_shorthand_field_patterns,
|
||||
no_mangle_generic_items,
|
||||
overflowing_literals,
|
||||
path_statements,
|
||||
patterns_in_fns_without_body,
|
||||
private_in_public,
|
||||
unconditional_recursion,
|
||||
unused,
|
||||
unused_allocation,
|
||||
unused_comparisons,
|
||||
unused_parens,
|
||||
while_true
|
||||
)]
|
||||
#![allow(clippy::unused_unit)]
|
||||
use am::transaction::CommitOptions;
|
||||
use am::transaction::Transactable;
|
||||
use automerge as am;
|
||||
use automerge::{Change, ObjId, ObjType, Prop, Value, ROOT};
|
||||
use js_sys::{Array, Function, Object, Uint8Array};
|
||||
use serde::ser::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
mod interop;
|
||||
mod observer;
|
||||
mod sync;
|
||||
mod value;
|
||||
|
||||
use observer::Observer;
|
||||
|
||||
use interop::{alloc, get_heads, js_get, js_set, to_js_err, to_objtype, to_prop, AR, JS};
|
||||
use sync::SyncState;
|
||||
use value::Datatype;
|
||||
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! log {
|
||||
( $( $t:tt )* ) => {
|
||||
web_sys::console::log_1(&format!( $( $t )* ).into());
|
||||
};
|
||||
}
|
||||
|
||||
type AutoCommit = am::AutoCommitWithObs<Observer>;
|
||||
|
||||
#[cfg(feature = "wee_alloc")]
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct Automerge {
|
||||
doc: AutoCommit,
|
||||
external_types: HashMap<Datatype, Function>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Automerge {
|
||||
pub fn new(actor: Option<String>) -> Result<Automerge, JsValue> {
|
||||
let mut doc = AutoCommit::default();
|
||||
if let Some(a) = actor {
|
||||
let a = automerge::ActorId::from(hex::decode(a).map_err(to_js_err)?.to_vec());
|
||||
doc.set_actor(a);
|
||||
}
|
||||
Ok(Automerge {
|
||||
doc,
|
||||
external_types: HashMap::default(),
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn clone(&mut self, actor: Option<String>) -> Result<Automerge, JsValue> {
|
||||
let mut automerge = Automerge {
|
||||
doc: self.doc.clone(),
|
||||
external_types: self.external_types.clone(),
|
||||
};
|
||||
if let Some(s) = actor {
|
||||
let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec());
|
||||
automerge.doc.set_actor(actor);
|
||||
}
|
||||
Ok(automerge)
|
||||
}
|
||||
|
||||
pub fn fork(&mut self, actor: Option<String>) -> Result<Automerge, JsValue> {
|
||||
let mut automerge = Automerge {
|
||||
doc: self.doc.fork(),
|
||||
external_types: self.external_types.clone(),
|
||||
};
|
||||
if let Some(s) = actor {
|
||||
let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec());
|
||||
automerge.doc.set_actor(actor);
|
||||
}
|
||||
Ok(automerge)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = forkAt)]
|
||||
pub fn fork_at(&mut self, heads: JsValue, actor: Option<String>) -> Result<Automerge, JsValue> {
|
||||
let deps: Vec<_> = JS(heads).try_into()?;
|
||||
let mut automerge = Automerge {
|
||||
doc: self.doc.fork_at(&deps)?,
|
||||
external_types: self.external_types.clone(),
|
||||
};
|
||||
if let Some(s) = actor {
|
||||
let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec());
|
||||
automerge.doc.set_actor(actor);
|
||||
}
|
||||
Ok(automerge)
|
||||
}
|
||||
|
||||
pub fn free(self) {}
|
||||
|
||||
#[wasm_bindgen(js_name = pendingOps)]
|
||||
pub fn pending_ops(&self) -> JsValue {
|
||||
(self.doc.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.doc.commit_with(commit_opts);
|
||||
JsValue::from_str(&hex::encode(&hash.0))
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, other: &mut Automerge) -> Result<Array, JsValue> {
|
||||
let heads = self.doc.merge(&mut other.doc)?;
|
||||
let heads: Array = heads
|
||||
.iter()
|
||||
.map(|h| JsValue::from_str(&hex::encode(&h.0)))
|
||||
.collect();
|
||||
Ok(heads)
|
||||
}
|
||||
|
||||
pub fn rollback(&mut self) -> f64 {
|
||||
self.doc.rollback() as f64
|
||||
}
|
||||
|
||||
pub fn keys(&self, obj: JsValue, heads: Option<Array>) -> Result<Array, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let result = if let Some(heads) = get_heads(heads) {
|
||||
self.doc
|
||||
.keys_at(&obj, &heads)
|
||||
.map(|s| JsValue::from_str(&s))
|
||||
.collect()
|
||||
} else {
|
||||
self.doc.keys(&obj).map(|s| JsValue::from_str(&s)).collect()
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn text(&self, obj: JsValue, heads: Option<Array>) -> Result<String, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
if let Some(heads) = get_heads(heads) {
|
||||
Ok(self.doc.text_at(&obj, &heads)?)
|
||||
} else {
|
||||
Ok(self.doc.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.doc.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.doc
|
||||
.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.doc.length(&obj);
|
||||
self.doc.insert(&obj, index, value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = pushObject)]
|
||||
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.doc.length(&obj);
|
||||
let opid = self.doc.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.doc.insert(&obj, index as usize, value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = insertObject)]
|
||||
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.doc.insert_object(&obj, index as usize, value)?;
|
||||
self.subset(&opid, subvals)?;
|
||||
Ok(opid.to_string().into())
|
||||
}
|
||||
|
||||
pub fn put(
|
||||
&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.doc.put(&obj, prop, value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = putObject)]
|
||||
pub fn put_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.doc.put_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.doc.put_object(obj, s, objtype)?)
|
||||
}
|
||||
(Prop::Map(s), Value::Scalar(scalar)) => {
|
||||
self.doc.put(obj, s, scalar.into_owned())?;
|
||||
None
|
||||
}
|
||||
(Prop::Seq(i), Value::Object(objtype)) => {
|
||||
Some(self.doc.insert_object(obj, i, objtype)?)
|
||||
}
|
||||
(Prop::Seq(i), Value::Scalar(scalar)) => {
|
||||
self.doc.insert(obj, i, scalar.into_owned())?;
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(opid) = opid {
|
||||
self.subset(&opid, subvals)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn increment(
|
||||
&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("increment needs a numeric value"))?;
|
||||
self.doc.increment(&obj, prop, value as i64)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = get)]
|
||||
pub fn get(
|
||||
&self,
|
||||
obj: JsValue,
|
||||
prop: JsValue,
|
||||
heads: Option<Array>,
|
||||
) -> Result<JsValue, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let prop = to_prop(prop);
|
||||
let heads = get_heads(heads);
|
||||
if let Ok(prop) = prop {
|
||||
let value = if let Some(h) = heads {
|
||||
self.doc.get_at(&obj, prop, &h)?
|
||||
} else {
|
||||
self.doc.get(&obj, prop)?
|
||||
};
|
||||
if let Some((value, id)) = value {
|
||||
match alloc(&value) {
|
||||
(datatype, js_value) if datatype.is_scalar() => Ok(js_value),
|
||||
_ => Ok(id.to_string().into()),
|
||||
}
|
||||
} else {
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
} else {
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = getWithType)]
|
||||
pub fn get_with_type(
|
||||
&self,
|
||||
obj: JsValue,
|
||||
prop: JsValue,
|
||||
heads: Option<Array>,
|
||||
) -> Result<JsValue, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let prop = to_prop(prop);
|
||||
let heads = get_heads(heads);
|
||||
if let Ok(prop) = prop {
|
||||
let value = if let Some(h) = heads {
|
||||
self.doc.get_at(&obj, prop, &h)?
|
||||
} else {
|
||||
self.doc.get(&obj, prop)?
|
||||
};
|
||||
if let Some(value) = value {
|
||||
match &value {
|
||||
(Value::Object(obj_type), obj_id) => {
|
||||
let result = Array::new();
|
||||
result.push(&obj_type.to_string().into());
|
||||
result.push(&obj_id.to_string().into());
|
||||
Ok(result.into())
|
||||
}
|
||||
(Value::Scalar(_), _) => {
|
||||
let result = Array::new();
|
||||
let (datatype, value) = alloc(&value.0);
|
||||
result.push(&datatype.into());
|
||||
result.push(&value);
|
||||
Ok(result.into())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(JsValue::null())
|
||||
}
|
||||
} else {
|
||||
Ok(JsValue::null())
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = getAll)]
|
||||
pub fn get_all(
|
||||
&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.doc.get_all_at(&obj, prop, &heads)
|
||||
} else {
|
||||
self.doc.get_all(&obj, prop)
|
||||
}
|
||||
.map_err(to_js_err)?;
|
||||
for (value, id) in values {
|
||||
let sub = Array::new();
|
||||
let (datatype, js_value) = alloc(&value);
|
||||
sub.push(&datatype.into());
|
||||
if value.is_scalar() {
|
||||
sub.push(&js_value);
|
||||
}
|
||||
sub.push(&id.to_string().into());
|
||||
result.push(&JsValue::from(&sub));
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = enablePatches)]
|
||||
pub fn enable_patches(&mut self, enable: JsValue) -> Result<(), JsValue> {
|
||||
let enable = enable
|
||||
.as_bool()
|
||||
.ok_or_else(|| to_js_err("must pass a bool to enable_patches"))?;
|
||||
self.doc.observer().enable(enable);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = registerDatatype)]
|
||||
pub fn register_datatype(
|
||||
&mut self,
|
||||
datatype: JsValue,
|
||||
function: JsValue,
|
||||
) -> Result<(), JsValue> {
|
||||
let datatype = Datatype::try_from(datatype)?;
|
||||
if let Ok(function) = function.dyn_into::<Function>() {
|
||||
self.external_types.insert(datatype, function);
|
||||
} else {
|
||||
self.external_types.remove(&datatype);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = applyPatches)]
|
||||
pub fn apply_patches(
|
||||
&mut self,
|
||||
object: JsValue,
|
||||
meta: JsValue,
|
||||
callback: JsValue,
|
||||
) -> Result<JsValue, JsValue> {
|
||||
let mut object = object.dyn_into::<Object>()?;
|
||||
let patches = self.doc.observer().take_patches();
|
||||
let callback = callback.dyn_into::<Function>().ok();
|
||||
|
||||
// even if there are no patches we may need to update the meta object
|
||||
// which requires that we update the object too
|
||||
if patches.is_empty() && !meta.is_undefined() {
|
||||
let (obj, datatype, id) = self.unwrap_object(&object)?;
|
||||
object = Object::assign(&Object::new(), &obj);
|
||||
object = self.wrap_object(object, datatype, &id, &meta)?;
|
||||
}
|
||||
|
||||
for p in patches {
|
||||
if let Some(c) = &callback {
|
||||
let before = object.clone();
|
||||
object = self.apply_patch(object, &p, 0, &meta)?;
|
||||
c.call3(&JsValue::undefined(), &p.try_into()?, &before, &object)?;
|
||||
} else {
|
||||
object = self.apply_patch(object, &p, 0, &meta)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(object.into())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = popPatches)]
|
||||
pub fn pop_patches(&mut self) -> Result<Array, JsValue> {
|
||||
// transactions send out observer updates as they occur, not waiting for them to be
|
||||
// committed.
|
||||
// If we pop the patches then we won't be able to revert them.
|
||||
|
||||
let patches = self.doc.observer().take_patches();
|
||||
let result = Array::new();
|
||||
for p in patches {
|
||||
result.push(&p.try_into()?);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn length(&self, obj: JsValue, heads: Option<Array>) -> Result<f64, JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
if let Some(heads) = get_heads(heads) {
|
||||
Ok(self.doc.length_at(&obj, &heads) as f64)
|
||||
} else {
|
||||
Ok(self.doc.length(&obj) as f64)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, obj: JsValue, prop: JsValue) -> Result<(), JsValue> {
|
||||
let obj = self.import(obj)?;
|
||||
let prop = to_prop(prop)?;
|
||||
self.doc.delete(&obj, prop).map_err(to_js_err)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save(&mut self) -> Uint8Array {
|
||||
Uint8Array::from(self.doc.save().as_slice())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = saveIncremental)]
|
||||
pub fn save_incremental(&mut self) -> Uint8Array {
|
||||
let bytes = self.doc.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.doc.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.doc.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.doc.get_changes(&deps)?;
|
||||
let changes: Array = changes
|
||||
.iter()
|
||||
.map(|c| Uint8Array::from(c.raw_bytes()))
|
||||
.collect();
|
||||
Ok(changes)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = getChangeByHash)]
|
||||
pub fn get_change_by_hash(&mut self, hash: JsValue) -> Result<JsValue, JsValue> {
|
||||
let hash = serde_wasm_bindgen::from_value(hash).map_err(to_js_err)?;
|
||||
let change = self.doc.get_change_by_hash(&hash);
|
||||
if let Some(c) = change {
|
||||
Ok(Uint8Array::from(c.raw_bytes()).into())
|
||||
} else {
|
||||
Ok(JsValue::null())
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = getChangesAdded)]
|
||||
pub fn get_changes_added(&mut self, other: &mut Automerge) -> Result<Array, JsValue> {
|
||||
let changes = self.doc.get_changes_added(&mut other.doc);
|
||||
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.doc.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(&self) -> String {
|
||||
let actor = self.doc.get_actor();
|
||||
actor.to_string()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = getLastLocalChange)]
|
||||
pub fn get_last_local_change(&mut self) -> Result<JsValue, JsValue> {
|
||||
if let Some(change) = self.doc.get_last_local_change() {
|
||||
Ok(Uint8Array::from(change.raw_bytes()).into())
|
||||
} else {
|
||||
Ok(JsValue::null())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dump(&mut self) {
|
||||
self.doc.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.doc.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.doc
|
||||
.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.doc.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, meta: JsValue) -> Result<JsValue, JsValue> {
|
||||
self.export_object(&ROOT, Datatype::Map, None, &meta)
|
||||
}
|
||||
|
||||
pub fn materialize(
|
||||
&mut self,
|
||||
obj: JsValue,
|
||||
heads: Option<Array>,
|
||||
meta: JsValue,
|
||||
) -> Result<JsValue, JsValue> {
|
||||
let obj = self.import(obj).unwrap_or(ROOT);
|
||||
let heads = get_heads(heads);
|
||||
let obj_type = self
|
||||
.doc
|
||||
.object_type(&obj)
|
||||
.ok_or_else(|| to_js_err(format!("invalid obj {}", obj)))?;
|
||||
let _patches = self.doc.observer().take_patches(); // throw away patches
|
||||
self.export_object(&obj, obj_type.into(), heads.as_ref(), &meta)
|
||||
}
|
||||
|
||||
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.doc.get(obj, prop)?
|
||||
} else {
|
||||
self.doc.get(obj, am::Prop::Seq(prop.parse().unwrap()))?
|
||||
};
|
||||
match val {
|
||||
Some((am::Value::Object(ObjType::Map), id)) => {
|
||||
is_map = true;
|
||||
obj = id;
|
||||
}
|
||||
Some((am::Value::Object(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.doc.import(&s)?)
|
||||
}
|
||||
} else {
|
||||
Err(to_js_err("invalid objid"))
|
||||
}
|
||||
}
|
||||
|
||||
fn import_prop(&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(&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("str") => value.as_string().map(|v| am::ScalarValue::Str(v.into())),
|
||||
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") => {
|
||||
if let Some(v) = value.as_f64() {
|
||||
Some(am::ScalarValue::Timestamp(v as i64))
|
||||
} else if let Ok(d) = value.clone().dyn_into::<js_sys::Date>() {
|
||||
Some(am::ScalarValue::Timestamp(d.get_time() as i64))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
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(
|
||||
&self,
|
||||
value: &JsValue,
|
||||
datatype: Option<String>,
|
||||
) -> Result<(Value<'static>, 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 = load)]
|
||||
pub fn load(data: Uint8Array, actor: Option<String>) -> Result<Automerge, JsValue> {
|
||||
let data = data.to_vec();
|
||||
let mut doc = 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());
|
||||
doc.set_actor(actor);
|
||||
}
|
||||
Ok(Automerge {
|
||||
doc,
|
||||
external_types: HashMap::default(),
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = encodeChange)]
|
||||
pub fn encode_change(change: JsValue) -> Result<Uint8Array, JsValue> {
|
||||
// Alex: Technically we should be using serde_wasm_bindgen::from_value instead of into_serde.
|
||||
// Unfortunately serde_wasm_bindgen::from_value fails for some inscrutable reason, so instead
|
||||
// we use into_serde (sorry to future me).
|
||||
#[allow(deprecated)]
|
||||
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();
|
||||
let serializer = serde_wasm_bindgen::Serializer::json_compatible();
|
||||
change.serialize(&serializer).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,313 +0,0 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
use crate::interop::{alloc, js_set};
|
||||
use automerge::{ObjId, OpObserver, Parents, Prop, Value};
|
||||
use js_sys::{Array, Object};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) struct Observer {
|
||||
enabled: bool,
|
||||
patches: Vec<Patch>,
|
||||
}
|
||||
|
||||
impl Observer {
|
||||
pub(crate) fn take_patches(&mut self) -> Vec<Patch> {
|
||||
std::mem::take(&mut self.patches)
|
||||
}
|
||||
pub(crate) fn enable(&mut self, enable: bool) {
|
||||
if self.enabled && !enable {
|
||||
self.patches.truncate(0)
|
||||
}
|
||||
self.enabled = enable;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum Patch {
|
||||
PutMap {
|
||||
obj: ObjId,
|
||||
path: Vec<(ObjId, Prop)>,
|
||||
key: String,
|
||||
value: (Value<'static>, ObjId),
|
||||
conflict: bool,
|
||||
},
|
||||
PutSeq {
|
||||
obj: ObjId,
|
||||
path: Vec<(ObjId, Prop)>,
|
||||
index: usize,
|
||||
value: (Value<'static>, ObjId),
|
||||
conflict: bool,
|
||||
},
|
||||
Insert {
|
||||
obj: ObjId,
|
||||
path: Vec<(ObjId, Prop)>,
|
||||
index: usize,
|
||||
values: Vec<(Value<'static>, ObjId)>,
|
||||
},
|
||||
Increment {
|
||||
obj: ObjId,
|
||||
path: Vec<(ObjId, Prop)>,
|
||||
prop: Prop,
|
||||
value: i64,
|
||||
},
|
||||
DeleteMap {
|
||||
obj: ObjId,
|
||||
path: Vec<(ObjId, Prop)>,
|
||||
key: String,
|
||||
},
|
||||
DeleteSeq {
|
||||
obj: ObjId,
|
||||
path: Vec<(ObjId, Prop)>,
|
||||
index: usize,
|
||||
length: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl OpObserver for Observer {
|
||||
fn insert(
|
||||
&mut self,
|
||||
mut parents: Parents<'_>,
|
||||
obj: ObjId,
|
||||
index: usize,
|
||||
tagged_value: (Value<'_>, ObjId),
|
||||
) {
|
||||
if self.enabled {
|
||||
let value = (tagged_value.0.to_owned(), tagged_value.1);
|
||||
if let Some(Patch::Insert {
|
||||
obj: tail_obj,
|
||||
index: tail_index,
|
||||
values,
|
||||
..
|
||||
}) = self.patches.last_mut()
|
||||
{
|
||||
if tail_obj == &obj && *tail_index + values.len() == index {
|
||||
values.push(value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let path = parents.path();
|
||||
let patch = Patch::Insert {
|
||||
path,
|
||||
obj,
|
||||
index,
|
||||
values: vec![value],
|
||||
};
|
||||
self.patches.push(patch);
|
||||
}
|
||||
}
|
||||
|
||||
fn put(
|
||||
&mut self,
|
||||
mut parents: Parents<'_>,
|
||||
obj: ObjId,
|
||||
prop: Prop,
|
||||
tagged_value: (Value<'_>, ObjId),
|
||||
conflict: bool,
|
||||
) {
|
||||
if self.enabled {
|
||||
let path = parents.path();
|
||||
let value = (tagged_value.0.to_owned(), tagged_value.1);
|
||||
let patch = match prop {
|
||||
Prop::Map(key) => Patch::PutMap {
|
||||
path,
|
||||
obj,
|
||||
key,
|
||||
value,
|
||||
conflict,
|
||||
},
|
||||
Prop::Seq(index) => Patch::PutSeq {
|
||||
path,
|
||||
obj,
|
||||
index,
|
||||
value,
|
||||
conflict,
|
||||
},
|
||||
};
|
||||
self.patches.push(patch);
|
||||
}
|
||||
}
|
||||
|
||||
fn increment(
|
||||
&mut self,
|
||||
mut parents: Parents<'_>,
|
||||
obj: ObjId,
|
||||
prop: Prop,
|
||||
tagged_value: (i64, ObjId),
|
||||
) {
|
||||
if self.enabled {
|
||||
let path = parents.path();
|
||||
let value = tagged_value.0;
|
||||
self.patches.push(Patch::Increment {
|
||||
path,
|
||||
obj,
|
||||
prop,
|
||||
value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn delete(&mut self, mut parents: Parents<'_>, obj: ObjId, prop: Prop) {
|
||||
if self.enabled {
|
||||
let path = parents.path();
|
||||
let patch = match prop {
|
||||
Prop::Map(key) => Patch::DeleteMap { path, obj, key },
|
||||
Prop::Seq(index) => Patch::DeleteSeq {
|
||||
path,
|
||||
obj,
|
||||
index,
|
||||
length: 1,
|
||||
},
|
||||
};
|
||||
self.patches.push(patch)
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, other: &Self) {
|
||||
self.patches.extend_from_slice(other.patches.as_slice())
|
||||
}
|
||||
|
||||
fn branch(&self) -> Self {
|
||||
Observer {
|
||||
patches: vec![],
|
||||
enabled: self.enabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prop_to_js(p: &Prop) -> JsValue {
|
||||
match p {
|
||||
Prop::Map(key) => JsValue::from_str(key),
|
||||
Prop::Seq(index) => JsValue::from_f64(*index as f64),
|
||||
}
|
||||
}
|
||||
|
||||
fn export_path(path: &[(ObjId, Prop)], end: &Prop) -> Array {
|
||||
let result = Array::new();
|
||||
for p in path {
|
||||
result.push(&prop_to_js(&p.1));
|
||||
}
|
||||
result.push(&prop_to_js(end));
|
||||
result
|
||||
}
|
||||
|
||||
impl Patch {
|
||||
pub(crate) fn path(&self) -> &[(ObjId, Prop)] {
|
||||
match &self {
|
||||
Self::PutMap { path, .. } => path.as_slice(),
|
||||
Self::PutSeq { path, .. } => path.as_slice(),
|
||||
Self::Increment { path, .. } => path.as_slice(),
|
||||
Self::Insert { path, .. } => path.as_slice(),
|
||||
Self::DeleteMap { path, .. } => path.as_slice(),
|
||||
Self::DeleteSeq { path, .. } => path.as_slice(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn obj(&self) -> &ObjId {
|
||||
match &self {
|
||||
Self::PutMap { obj, .. } => obj,
|
||||
Self::PutSeq { obj, .. } => obj,
|
||||
Self::Increment { obj, .. } => obj,
|
||||
Self::Insert { obj, .. } => obj,
|
||||
Self::DeleteMap { obj, .. } => obj,
|
||||
Self::DeleteSeq { obj, .. } => obj,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Patch> for JsValue {
|
||||
type Error = JsValue;
|
||||
|
||||
fn try_from(p: Patch) -> Result<Self, Self::Error> {
|
||||
let result = Object::new();
|
||||
match p {
|
||||
Patch::PutMap {
|
||||
path,
|
||||
key,
|
||||
value,
|
||||
conflict,
|
||||
..
|
||||
} => {
|
||||
js_set(&result, "action", "put")?;
|
||||
js_set(
|
||||
&result,
|
||||
"path",
|
||||
export_path(path.as_slice(), &Prop::Map(key)),
|
||||
)?;
|
||||
js_set(&result, "value", alloc(&value.0).1)?;
|
||||
js_set(&result, "conflict", &JsValue::from_bool(conflict))?;
|
||||
Ok(result.into())
|
||||
}
|
||||
Patch::PutSeq {
|
||||
path,
|
||||
index,
|
||||
value,
|
||||
conflict,
|
||||
..
|
||||
} => {
|
||||
js_set(&result, "action", "put")?;
|
||||
js_set(
|
||||
&result,
|
||||
"path",
|
||||
export_path(path.as_slice(), &Prop::Seq(index)),
|
||||
)?;
|
||||
js_set(&result, "value", alloc(&value.0).1)?;
|
||||
js_set(&result, "conflict", &JsValue::from_bool(conflict))?;
|
||||
Ok(result.into())
|
||||
}
|
||||
Patch::Insert {
|
||||
path,
|
||||
index,
|
||||
values,
|
||||
..
|
||||
} => {
|
||||
js_set(&result, "action", "splice")?;
|
||||
js_set(
|
||||
&result,
|
||||
"path",
|
||||
export_path(path.as_slice(), &Prop::Seq(index)),
|
||||
)?;
|
||||
js_set(
|
||||
&result,
|
||||
"values",
|
||||
values.iter().map(|v| alloc(&v.0).1).collect::<Array>(),
|
||||
)?;
|
||||
Ok(result.into())
|
||||
}
|
||||
Patch::Increment {
|
||||
path, prop, value, ..
|
||||
} => {
|
||||
js_set(&result, "action", "inc")?;
|
||||
js_set(&result, "path", export_path(path.as_slice(), &prop))?;
|
||||
js_set(&result, "value", &JsValue::from_f64(value as f64))?;
|
||||
Ok(result.into())
|
||||
}
|
||||
Patch::DeleteMap { path, key, .. } => {
|
||||
js_set(&result, "action", "del")?;
|
||||
js_set(
|
||||
&result,
|
||||
"path",
|
||||
export_path(path.as_slice(), &Prop::Map(key)),
|
||||
)?;
|
||||
Ok(result.into())
|
||||
}
|
||||
Patch::DeleteSeq {
|
||||
path,
|
||||
index,
|
||||
length,
|
||||
..
|
||||
} => {
|
||||
js_set(&result, "action", "del")?;
|
||||
js_set(
|
||||
&result,
|
||||
"path",
|
||||
export_path(path.as_slice(), &Prop::Seq(index)),
|
||||
)?;
|
||||
if length > 1 {
|
||||
js_set(&result, "length", length)?;
|
||||
}
|
||||
Ok(result.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
use crate::{
|
||||
clock::{Clock, ClockData},
|
||||
Change, ChangeHash,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub(crate) struct Clocks(HashMap<ChangeHash, Clock>);
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("attempted to derive a clock for a change with dependencies we don't have")]
|
||||
pub struct MissingDep(ChangeHash);
|
||||
|
||||
impl Clocks {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self(HashMap::new())
|
||||
}
|
||||
|
||||
pub(crate) fn add_change(
|
||||
&mut self,
|
||||
change: &Change,
|
||||
actor_index: usize,
|
||||
) -> Result<(), MissingDep> {
|
||||
let mut clock = Clock::new();
|
||||
for hash in change.deps() {
|
||||
let c = self.0.get(hash).ok_or(MissingDep(*hash))?;
|
||||
clock.merge(c);
|
||||
}
|
||||
clock.include(
|
||||
actor_index,
|
||||
ClockData {
|
||||
max_op: change.max_op(),
|
||||
seq: change.seq(),
|
||||
},
|
||||
);
|
||||
self.0.insert(change.hash(), clock);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Clocks> for HashMap<ChangeHash, Clock> {
|
||||
fn from(c: Clocks) -> Self {
|
||||
c.0
|
||||
}
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
use crate::ActorId;
|
||||
use serde::Serialize;
|
||||
use serde::Serializer;
|
||||
use std::cmp::{Ord, Ordering};
|
||||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExId {
|
||||
Root,
|
||||
Id(u64, ActorId, usize),
|
||||
}
|
||||
|
||||
impl PartialEq for ExId {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(ExId::Root, ExId::Root) => true,
|
||||
(ExId::Id(ctr1, actor1, _), ExId::Id(ctr2, actor2, _))
|
||||
if ctr1 == ctr2 && actor1 == actor2 =>
|
||||
{
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for ExId {}
|
||||
|
||||
impl fmt::Display for ExId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ExId::Root => write!(f, "_root"),
|
||||
ExId::Id(ctr, actor, _) => write!(f, "{}@{}", ctr, actor),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for ExId {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
match self {
|
||||
ExId::Root => 0.hash(state),
|
||||
ExId::Id(ctr, actor, _) => {
|
||||
ctr.hash(state);
|
||||
actor.hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for ExId {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
match (self, other) {
|
||||
(ExId::Root, ExId::Root) => Ordering::Equal,
|
||||
(ExId::Root, _) => Ordering::Less,
|
||||
(_, ExId::Root) => Ordering::Greater,
|
||||
(ExId::Id(c1, a1, _), ExId::Id(c2, a2, _)) if c1 == c2 => a2.cmp(a1),
|
||||
(ExId::Id(c1, _, _), ExId::Id(c2, _, _)) => c1.cmp(c2),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for ExId {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ExId {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<ExId> for ExId {
|
||||
fn as_ref(&self) -> &ExId {
|
||||
self
|
||||
}
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/automerge/automerge-rs/main/img/brandmark.svg",
|
||||
html_favicon_url = "https:///raw.githubusercontent.com/automerge/automerge-rs/main/img/favicon.ico"
|
||||
)]
|
||||
#![warn(
|
||||
missing_debug_implementations,
|
||||
// missing_docs, // TODO: add documentation!
|
||||
rust_2018_idioms,
|
||||
unreachable_pub,
|
||||
bad_style,
|
||||
const_err,
|
||||
dead_code,
|
||||
improper_ctypes,
|
||||
non_shorthand_field_patterns,
|
||||
no_mangle_generic_items,
|
||||
overflowing_literals,
|
||||
path_statements,
|
||||
patterns_in_fns_without_body,
|
||||
private_in_public,
|
||||
unconditional_recursion,
|
||||
unused,
|
||||
unused_allocation,
|
||||
unused_comparisons,
|
||||
unused_parens,
|
||||
while_true
|
||||
)]
|
||||
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! log {
|
||||
( $( $t:tt )* ) => {
|
||||
{
|
||||
use $crate::__log;
|
||||
__log!( $( $t )* );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "wasm", target_family = "wasm"))]
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! __log {
|
||||
( $( $t:tt )* ) => {
|
||||
web_sys::console::log_1(&format!( $( $t )* ).into());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "wasm", target_family = "wasm")))]
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! __log {
|
||||
( $( $t:tt )* ) => {
|
||||
println!( $( $t )* );
|
||||
}
|
||||
}
|
||||
|
||||
mod autocommit;
|
||||
mod automerge;
|
||||
mod autoserde;
|
||||
mod change;
|
||||
mod clock;
|
||||
mod clocks;
|
||||
mod columnar;
|
||||
mod convert;
|
||||
mod error;
|
||||
mod exid;
|
||||
mod indexed_cache;
|
||||
mod keys;
|
||||
mod keys_at;
|
||||
mod legacy;
|
||||
mod list_range;
|
||||
mod list_range_at;
|
||||
mod map_range;
|
||||
mod map_range_at;
|
||||
mod op_observer;
|
||||
mod op_set;
|
||||
mod op_tree;
|
||||
mod parents;
|
||||
mod query;
|
||||
mod storage;
|
||||
pub mod sync;
|
||||
pub mod transaction;
|
||||
mod types;
|
||||
mod value;
|
||||
mod values;
|
||||
#[cfg(feature = "optree-visualisation")]
|
||||
mod visualisation;
|
||||
|
||||
pub use crate::automerge::Automerge;
|
||||
pub use autocommit::{AutoCommit, AutoCommitWithObs};
|
||||
pub use autoserde::AutoSerde;
|
||||
pub use change::{Change, LoadError as LoadChangeError};
|
||||
pub use error::AutomergeError;
|
||||
pub use error::InvalidActorId;
|
||||
pub use error::InvalidChangeHashSlice;
|
||||
pub use exid::ExId as ObjId;
|
||||
pub use keys::Keys;
|
||||
pub use keys_at::KeysAt;
|
||||
pub use legacy::Change as ExpandedChange;
|
||||
pub use list_range::ListRange;
|
||||
pub use list_range_at::ListRangeAt;
|
||||
pub use map_range::MapRange;
|
||||
pub use map_range_at::MapRangeAt;
|
||||
pub use op_observer::OpObserver;
|
||||
pub use op_observer::Patch;
|
||||
pub use op_observer::VecOpObserver;
|
||||
pub use parents::Parents;
|
||||
pub use types::{ActorId, ChangeHash, ObjType, OpType, Prop};
|
||||
pub use value::{ScalarValue, Value};
|
||||
pub use values::Values;
|
||||
|
||||
pub const ROOT: ObjId = ObjId::Root;
|
|
@ -1,236 +0,0 @@
|
|||
use crate::exid::ExId;
|
||||
use crate::Parents;
|
||||
use crate::Prop;
|
||||
use crate::Value;
|
||||
|
||||
/// An observer of operations applied to the document.
|
||||
pub trait OpObserver: Default + Clone {
|
||||
/// A new value has been inserted into the given object.
|
||||
///
|
||||
/// - `parents`: A parents iterator that can be used to collect path information
|
||||
/// - `objid`: the object that has been inserted into.
|
||||
/// - `index`: the index the new value has been inserted at.
|
||||
/// - `tagged_value`: the value that has been inserted and the id of the operation that did the
|
||||
/// insert.
|
||||
fn insert(
|
||||
&mut self,
|
||||
parents: Parents<'_>,
|
||||
objid: ExId,
|
||||
index: usize,
|
||||
tagged_value: (Value<'_>, ExId),
|
||||
);
|
||||
|
||||
/// A new value has been put into the given object.
|
||||
///
|
||||
/// - `parents`: A parents iterator that can be used to collect path information
|
||||
/// - `objid`: the object that has been put into.
|
||||
/// - `prop`: the prop that the value as been put at.
|
||||
/// - `tagged_value`: the value that has been put into the object and the id of the operation
|
||||
/// that did the put.
|
||||
/// - `conflict`: whether this put conflicts with other operations.
|
||||
fn put(
|
||||
&mut self,
|
||||
parents: Parents<'_>,
|
||||
objid: ExId,
|
||||
prop: Prop,
|
||||
tagged_value: (Value<'_>, ExId),
|
||||
conflict: bool,
|
||||
);
|
||||
|
||||
/// A counter has been incremented.
|
||||
///
|
||||
/// - `parents`: A parents iterator that can be used to collect path information
|
||||
/// - `objid`: the object that contains the counter.
|
||||
/// - `prop`: they prop that the chounter is at.
|
||||
/// - `tagged_value`: the amount the counter has been incremented by, and the the id of the
|
||||
/// increment operation.
|
||||
fn increment(
|
||||
&mut self,
|
||||
parents: Parents<'_>,
|
||||
objid: ExId,
|
||||
prop: Prop,
|
||||
tagged_value: (i64, ExId),
|
||||
);
|
||||
|
||||
/// A value has beeen deleted.
|
||||
///
|
||||
/// - `parents`: A parents iterator that can be used to collect path information
|
||||
/// - `objid`: the object that has been deleted in.
|
||||
/// - `prop`: the prop of the value that has been deleted.
|
||||
fn delete(&mut self, parents: Parents<'_>, objid: ExId, prop: Prop);
|
||||
|
||||
/// Branch of a new op_observer later to be merged
|
||||
///
|
||||
/// Called by AutoCommit when creating a new transaction. Observer branch
|
||||
/// will be merged on `commit()` or thrown away on `rollback()`
|
||||
///
|
||||
fn branch(&self) -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Merge observed information from a transaction.
|
||||
///
|
||||
/// Called by AutoCommit on `commit()`
|
||||
///
|
||||
/// - `other`: Another Op Observer of the same type
|
||||
fn merge(&mut self, other: &Self);
|
||||
}
|
||||
|
||||
impl OpObserver for () {
|
||||
fn insert(
|
||||
&mut self,
|
||||
_parents: Parents<'_>,
|
||||
_objid: ExId,
|
||||
_index: usize,
|
||||
_tagged_value: (Value<'_>, ExId),
|
||||
) {
|
||||
}
|
||||
|
||||
fn put(
|
||||
&mut self,
|
||||
_parents: Parents<'_>,
|
||||
_objid: ExId,
|
||||
_prop: Prop,
|
||||
_tagged_value: (Value<'_>, ExId),
|
||||
_conflict: bool,
|
||||
) {
|
||||
}
|
||||
|
||||
fn increment(
|
||||
&mut self,
|
||||
_parents: Parents<'_>,
|
||||
_objid: ExId,
|
||||
_prop: Prop,
|
||||
_tagged_value: (i64, ExId),
|
||||
) {
|
||||
}
|
||||
|
||||
fn delete(&mut self, _parents: Parents<'_>, _objid: ExId, _prop: Prop) {}
|
||||
|
||||
fn merge(&mut self, _other: &Self) {}
|
||||
}
|
||||
|
||||
/// Capture operations into a [`Vec`] and store them as patches.
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct VecOpObserver {
|
||||
patches: Vec<Patch>,
|
||||
}
|
||||
|
||||
impl VecOpObserver {
|
||||
/// Take the current list of patches, leaving the internal list empty and ready for new
|
||||
/// patches.
|
||||
pub fn take_patches(&mut self) -> Vec<Patch> {
|
||||
std::mem::take(&mut self.patches)
|
||||
}
|
||||
}
|
||||
|
||||
impl OpObserver for VecOpObserver {
|
||||
fn insert(
|
||||
&mut self,
|
||||
mut parents: Parents<'_>,
|
||||
obj: ExId,
|
||||
index: usize,
|
||||
(value, id): (Value<'_>, ExId),
|
||||
) {
|
||||
let path = parents.path();
|
||||
self.patches.push(Patch::Insert {
|
||||
obj,
|
||||
path,
|
||||
index,
|
||||
value: (value.into_owned(), id),
|
||||
});
|
||||
}
|
||||
|
||||
fn put(
|
||||
&mut self,
|
||||
mut parents: Parents<'_>,
|
||||
obj: ExId,
|
||||
prop: Prop,
|
||||
(value, id): (Value<'_>, ExId),
|
||||
conflict: bool,
|
||||
) {
|
||||
let path = parents.path();
|
||||
self.patches.push(Patch::Put {
|
||||
obj,
|
||||
path,
|
||||
prop,
|
||||
value: (value.into_owned(), id),
|
||||
conflict,
|
||||
});
|
||||
}
|
||||
|
||||
fn increment(
|
||||
&mut self,
|
||||
mut parents: Parents<'_>,
|
||||
obj: ExId,
|
||||
prop: Prop,
|
||||
tagged_value: (i64, ExId),
|
||||
) {
|
||||
let path = parents.path();
|
||||
self.patches.push(Patch::Increment {
|
||||
obj,
|
||||
path,
|
||||
prop,
|
||||
value: tagged_value,
|
||||
});
|
||||
}
|
||||
|
||||
fn delete(&mut self, mut parents: Parents<'_>, obj: ExId, prop: Prop) {
|
||||
let path = parents.path();
|
||||
self.patches.push(Patch::Delete { obj, path, prop })
|
||||
}
|
||||
|
||||
fn merge(&mut self, other: &Self) {
|
||||
self.patches.extend_from_slice(other.patches.as_slice())
|
||||
}
|
||||
}
|
||||
|
||||
/// A notification to the application that something has changed in a document.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Patch {
|
||||
/// Associating a new value with a prop in a map, or an existing list element
|
||||
Put {
|
||||
/// path to the object
|
||||
path: Vec<(ExId, Prop)>,
|
||||
/// The object that was put into.
|
||||
obj: ExId,
|
||||
/// The prop that the new value was put at.
|
||||
prop: Prop,
|
||||
/// The value that was put, and the id of the operation that put it there.
|
||||
value: (Value<'static>, ExId),
|
||||
/// Whether this put conflicts with another.
|
||||
conflict: bool,
|
||||
},
|
||||
/// Inserting a new element into a list/text
|
||||
Insert {
|
||||
/// path to the object
|
||||
path: Vec<(ExId, Prop)>,
|
||||
/// The object that was inserted into.
|
||||
obj: ExId,
|
||||
/// The index that the new value was inserted at.
|
||||
index: usize,
|
||||
/// The value that was inserted, and the id of the operation that inserted it there.
|
||||
value: (Value<'static>, ExId),
|
||||
},
|
||||
/// Incrementing a counter.
|
||||
Increment {
|
||||
/// path to the object
|
||||
path: Vec<(ExId, Prop)>,
|
||||
/// The object that was incremented in.
|
||||
obj: ExId,
|
||||
/// The prop that was incremented.
|
||||
prop: Prop,
|
||||
/// The amount that the counter was incremented by, and the id of the operation that
|
||||
/// did the increment.
|
||||
value: (i64, ExId),
|
||||
},
|
||||
/// Deleting an element from a list/text
|
||||
Delete {
|
||||
/// path to the object
|
||||
path: Vec<(ExId, Prop)>,
|
||||
/// The object that was deleted from.
|
||||
obj: ExId,
|
||||
/// The prop that was deleted.
|
||||
prop: Prop,
|
||||
},
|
||||
}
|
|
@ -1,809 +0,0 @@
|
|||
use std::{
|
||||
cmp::{min, Ordering},
|
||||
fmt::Debug,
|
||||
mem,
|
||||
ops::RangeBounds,
|
||||
};
|
||||
|
||||
pub(crate) use crate::op_set::OpSetMetadata;
|
||||
use crate::{
|
||||
clock::Clock,
|
||||
query::{self, Index, QueryResult, ReplaceArgs, TreeQuery},
|
||||
};
|
||||
use crate::{
|
||||
types::{ObjId, Op, OpId},
|
||||
ObjType,
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub(crate) const B: usize = 16;
|
||||
|
||||
mod iter;
|
||||
pub(crate) use iter::OpTreeIter;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct OpTree {
|
||||
pub(crate) internal: OpTreeInternal,
|
||||
pub(crate) objtype: ObjType,
|
||||
/// The id of the parent object, root has no parent.
|
||||
pub(crate) parent: Option<ObjId>,
|
||||
}
|
||||
|
||||
impl OpTree {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
internal: Default::default(),
|
||||
objtype: ObjType::Map,
|
||||
parent: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> OpTreeIter<'_> {
|
||||
self.internal.iter()
|
||||
}
|
||||
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.internal.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct OpTreeInternal {
|
||||
pub(crate) root_node: Option<OpTreeNode>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct OpTreeNode {
|
||||
pub(crate) children: Vec<OpTreeNode>,
|
||||
pub(crate) elements: Vec<Op>,
|
||||
pub(crate) index: Index,
|
||||
length: usize,
|
||||
}
|
||||
|
||||
impl OpTreeInternal {
|
||||
/// Construct a new, empty, sequence.
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { root_node: None }
|
||||
}
|
||||
|
||||
/// Get the length of the sequence.
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.root_node.as_ref().map_or(0, |n| n.len())
|
||||
}
|
||||
|
||||
pub(crate) fn keys(&self) -> Option<query::Keys<'_>> {
|
||||
self.root_node.as_ref().map(query::Keys::new)
|
||||
}
|
||||
|
||||
pub(crate) fn keys_at(&self, clock: Clock) -> Option<query::KeysAt<'_>> {
|
||||
self.root_node
|
||||
.as_ref()
|
||||
.map(|root| query::KeysAt::new(root, clock))
|
||||
}
|
||||
|
||||
pub(crate) fn map_range<'a, R: RangeBounds<String>>(
|
||||
&'a self,
|
||||
range: R,
|
||||
meta: &'a OpSetMetadata,
|
||||
) -> Option<query::MapRange<'a, R>> {
|
||||
self.root_node
|
||||
.as_ref()
|
||||
.map(|node| query::MapRange::new(range, node, meta))
|
||||
}
|
||||
|
||||
pub(crate) fn map_range_at<'a, R: RangeBounds<String>>(
|
||||
&'a self,
|
||||
range: R,
|
||||
meta: &'a OpSetMetadata,
|
||||
clock: Clock,
|
||||
) -> Option<query::MapRangeAt<'a, R>> {
|
||||
self.root_node
|
||||
.as_ref()
|
||||
.map(|node| query::MapRangeAt::new(range, node, meta, clock))
|
||||
}
|
||||
|
||||
pub(crate) fn list_range<R: RangeBounds<usize>>(
|
||||
&self,
|
||||
range: R,
|
||||
) -> Option<query::ListRange<'_, R>> {
|
||||
self.root_node
|
||||
.as_ref()
|
||||
.map(|node| query::ListRange::new(range, node))
|
||||
}
|
||||
|
||||
pub(crate) fn list_range_at<R: RangeBounds<usize>>(
|
||||
&self,
|
||||
range: R,
|
||||
clock: Clock,
|
||||
) -> Option<query::ListRangeAt<'_, R>> {
|
||||
self.root_node
|
||||
.as_ref()
|
||||
.map(|node| query::ListRangeAt::new(range, clock, node))
|
||||
}
|
||||
|
||||
pub(crate) fn search<'a, 'b: 'a, Q>(&'b self, mut query: Q, m: &OpSetMetadata) -> Q
|
||||
where
|
||||
Q: TreeQuery<'a>,
|
||||
{
|
||||
self.root_node
|
||||
.as_ref()
|
||||
.map(|root| match query.query_node_with_metadata(root, m) {
|
||||
QueryResult::Descend => root.search(&mut query, m, None),
|
||||
QueryResult::Skip(skip) => root.search(&mut query, m, Some(skip)),
|
||||
_ => true,
|
||||
});
|
||||
query
|
||||
}
|
||||
|
||||
/// Create an iterator through the sequence.
|
||||
pub(crate) fn iter(&self) -> OpTreeIter<'_> {
|
||||
iter::OpTreeIter::new(self)
|
||||
}
|
||||
|
||||
/// Insert the `element` into the sequence at `index`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `index > len`.
|
||||
pub(crate) fn insert(&mut self, index: usize, element: Op) {
|
||||
assert!(
|
||||
index <= self.len(),
|
||||
"tried to insert at {} but len is {}",
|
||||
index,
|
||||
self.len()
|
||||
);
|
||||
|
||||
let old_len = self.len();
|
||||
if let Some(root) = self.root_node.as_mut() {
|
||||
#[cfg(debug_assertions)]
|
||||
root.check();
|
||||
|
||||
if root.is_full() {
|
||||
let original_len = root.len();
|
||||
let new_root = OpTreeNode::new();
|
||||
|
||||
// move new_root to root position
|
||||
let old_root = mem::replace(root, new_root);
|
||||
|
||||
root.length += old_root.len();
|
||||
root.index = old_root.index.clone();
|
||||
root.children.push(old_root);
|
||||
root.split_child(0);
|
||||
|
||||
assert_eq!(original_len, root.len());
|
||||
|
||||
// after splitting the root has one element and two children, find which child the
|
||||
// index is in
|
||||
let first_child_len = root.children[0].len();
|
||||
let (child, insertion_index) = if first_child_len < index {
|
||||
(&mut root.children[1], index - (first_child_len + 1))
|
||||
} else {
|
||||
(&mut root.children[0], index)
|
||||
};
|
||||
root.length += 1;
|
||||
root.index.insert(&element);
|
||||
child.insert_into_non_full_node(insertion_index, element)
|
||||
} else {
|
||||
root.insert_into_non_full_node(index, element)
|
||||
}
|
||||
} else {
|
||||
let mut root = OpTreeNode::new();
|
||||
root.insert_into_non_full_node(index, element);
|
||||
self.root_node = Some(root)
|
||||
}
|
||||
assert_eq!(self.len(), old_len + 1, "{:#?}", self);
|
||||
}
|
||||
|
||||
/// Get the `element` at `index` in the sequence.
|
||||
pub(crate) fn get(&self, index: usize) -> Option<&Op> {
|
||||
self.root_node.as_ref().and_then(|n| n.get(index))
|
||||
}
|
||||
|
||||
// this replaces get_mut() because it allows the indexes to update correctly
|
||||
pub(crate) fn update<F>(&mut self, index: usize, f: F)
|
||||
where
|
||||
F: FnMut(&mut Op),
|
||||
{
|
||||
if self.len() > index {
|
||||
self.root_node.as_mut().unwrap().update(index, f);
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the element at `index` from the sequence.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `index` is out of bounds.
|
||||
pub(crate) fn remove(&mut self, index: usize) -> Op {
|
||||
if let Some(root) = self.root_node.as_mut() {
|
||||
#[cfg(debug_assertions)]
|
||||
let len = root.check();
|
||||
let old = root.remove(index);
|
||||
|
||||
if root.elements.is_empty() {
|
||||
if root.is_leaf() {
|
||||
self.root_node = None;
|
||||
} else {
|
||||
self.root_node = Some(root.children.remove(0));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
debug_assert_eq!(len, self.root_node.as_ref().map_or(0, |r| r.check()) + 1);
|
||||
old
|
||||
} else {
|
||||
panic!("remove from empty tree")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OpTreeNode {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
elements: Vec::new(),
|
||||
children: Vec::new(),
|
||||
index: Default::default(),
|
||||
length: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn search<'a, 'b: 'a, Q>(
|
||||
&'b self,
|
||||
query: &mut Q,
|
||||
m: &OpSetMetadata,
|
||||
skip: Option<usize>,
|
||||
) -> bool
|
||||
where
|
||||
Q: TreeQuery<'a>,
|
||||
{
|
||||
if self.is_leaf() {
|
||||
let skip = skip.unwrap_or(0);
|
||||
for e in self.elements.iter().skip(skip) {
|
||||
if query.query_element_with_metadata(e, m) == QueryResult::Finish {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
} else {
|
||||
let mut skip = skip.unwrap_or(0);
|
||||
for (child_index, child) in self.children.iter().enumerate() {
|
||||
match skip.cmp(&child.len()) {
|
||||
Ordering::Greater => {
|
||||
// not in this child at all
|
||||
// take off the number of elements in the child as well as the next element
|
||||
skip -= child.len() + 1;
|
||||
}
|
||||
Ordering::Equal => {
|
||||
// just try the element
|
||||
skip -= child.len();
|
||||
if let Some(e) = self.elements.get(child_index) {
|
||||
if query.query_element_with_metadata(e, m) == QueryResult::Finish {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ordering::Less => {
|
||||
// descend and try find it
|
||||
match query.query_node_with_metadata(child, m) {
|
||||
QueryResult::Descend => {
|
||||
// search in the child node, passing in the number of items left to
|
||||
// skip
|
||||
if child.search(query, m, Some(skip)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
QueryResult::Finish => return true,
|
||||
QueryResult::Next => (),
|
||||
QueryResult::Skip(_) => panic!("had skip from non-root node"),
|
||||
}
|
||||
if let Some(e) = self.elements.get(child_index) {
|
||||
if query.query_element_with_metadata(e, m) == QueryResult::Finish {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// reset the skip to zero so we continue iterating normally
|
||||
skip = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.length
|
||||
}
|
||||
|
||||
fn reindex(&mut self) {
|
||||
let mut index = Index::new();
|
||||
for c in &self.children {
|
||||
index.merge(&c.index);
|
||||
}
|
||||
for e in &self.elements {
|
||||
index.insert(e);
|
||||
}
|
||||
self.index = index
|
||||
}
|
||||
|
||||
fn is_leaf(&self) -> bool {
|
||||
self.children.is_empty()
|
||||
}
|
||||
|
||||
fn is_full(&self) -> bool {
|
||||
self.elements.len() >= 2 * B - 1
|
||||
}
|
||||
|
||||
/// Returns the child index and the given index adjusted for the cumulative index before that
|
||||
/// child.
|
||||
fn find_child_index(&self, index: usize) -> (usize, usize) {
|
||||
let mut cumulative_len = 0;
|
||||
for (child_index, child) in self.children.iter().enumerate() {
|
||||
if cumulative_len + child.len() >= index {
|
||||
return (child_index, index - cumulative_len);
|
||||
} else {
|
||||
cumulative_len += child.len() + 1;
|
||||
}
|
||||
}
|
||||
panic!("index {} not found in node with len {}", index, self.len())
|
||||
}
|
||||
|
||||
fn insert_into_non_full_node(&mut self, index: usize, element: Op) {
|
||||
assert!(!self.is_full());
|
||||
|
||||
self.index.insert(&element);
|
||||
|
||||
if self.is_leaf() {
|
||||
self.length += 1;
|
||||
self.elements.insert(index, element);
|
||||
} else {
|
||||
let (child_index, sub_index) = self.find_child_index(index);
|
||||
let child = &mut self.children[child_index];
|
||||
|
||||
if child.is_full() {
|
||||
self.split_child(child_index);
|
||||
|
||||
// child structure has changed so we need to find the index again
|
||||
let (child_index, sub_index) = self.find_child_index(index);
|
||||
let child = &mut self.children[child_index];
|
||||
child.insert_into_non_full_node(sub_index, element);
|
||||
} else {
|
||||
child.insert_into_non_full_node(sub_index, element);
|
||||
}
|
||||
self.length += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// A utility function to split the child `full_child_index` of this node
|
||||
// Note that `full_child_index` must be full when this function is called.
|
||||
fn split_child(&mut self, full_child_index: usize) {
|
||||
let original_len_self = self.len();
|
||||
|
||||
let full_child = &mut self.children[full_child_index];
|
||||
|
||||
// Create a new node which is going to store (B-1) keys
|
||||
// of the full child.
|
||||
let mut successor_sibling = OpTreeNode::new();
|
||||
|
||||
let original_len = full_child.len();
|
||||
assert!(full_child.is_full());
|
||||
|
||||
successor_sibling.elements = full_child.elements.split_off(B);
|
||||
|
||||
if !full_child.is_leaf() {
|
||||
successor_sibling.children = full_child.children.split_off(B);
|
||||
}
|
||||
|
||||
let middle = full_child.elements.pop().unwrap();
|
||||
|
||||
full_child.length =
|
||||
full_child.elements.len() + full_child.children.iter().map(|c| c.len()).sum::<usize>();
|
||||
|
||||
successor_sibling.length = successor_sibling.elements.len()
|
||||
+ successor_sibling
|
||||
.children
|
||||
.iter()
|
||||
.map(|c| c.len())
|
||||
.sum::<usize>();
|
||||
|
||||
let z_len = successor_sibling.len();
|
||||
|
||||
let full_child_len = full_child.len();
|
||||
|
||||
full_child.reindex();
|
||||
successor_sibling.reindex();
|
||||
|
||||
self.children
|
||||
.insert(full_child_index + 1, successor_sibling);
|
||||
|
||||
self.elements.insert(full_child_index, middle);
|
||||
|
||||
assert_eq!(full_child_len + z_len + 1, original_len, "{:#?}", self);
|
||||
|
||||
assert_eq!(original_len_self, self.len());
|
||||
}
|
||||
|
||||
fn remove_from_leaf(&mut self, index: usize) -> Op {
|
||||
self.length -= 1;
|
||||
self.elements.remove(index)
|
||||
}
|
||||
|
||||
fn remove_element_from_non_leaf(&mut self, index: usize, element_index: usize) -> Op {
|
||||
self.length -= 1;
|
||||
if self.children[element_index].elements.len() >= B {
|
||||
let total_index = self.cumulative_index(element_index);
|
||||
// recursively delete index - 1 in predecessor_node
|
||||
let predecessor = self.children[element_index].remove(index - 1 - total_index);
|
||||
// replace element with that one
|
||||
mem::replace(&mut self.elements[element_index], predecessor)
|
||||
} else if self.children[element_index + 1].elements.len() >= B {
|
||||
// recursively delete index + 1 in successor_node
|
||||
let total_index = self.cumulative_index(element_index + 1);
|
||||
let successor = self.children[element_index + 1].remove(index + 1 - total_index);
|
||||
// replace element with that one
|
||||
mem::replace(&mut self.elements[element_index], successor)
|
||||
} else {
|
||||
let middle_element = self.elements.remove(element_index);
|
||||
let successor_child = self.children.remove(element_index + 1);
|
||||
self.children[element_index].merge(middle_element, successor_child);
|
||||
|
||||
let total_index = self.cumulative_index(element_index);
|
||||
self.children[element_index].remove(index - total_index)
|
||||
}
|
||||
}
|
||||
|
||||
fn cumulative_index(&self, child_index: usize) -> usize {
|
||||
self.children[0..child_index]
|
||||
.iter()
|
||||
.map(|c| c.len() + 1)
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn remove_from_internal_child(&mut self, index: usize, mut child_index: usize) -> Op {
|
||||
if self.children[child_index].elements.len() < B
|
||||
&& if child_index > 0 {
|
||||
self.children[child_index - 1].elements.len() < B
|
||||
} else {
|
||||
true
|
||||
}
|
||||
&& if child_index + 1 < self.children.len() {
|
||||
self.children[child_index + 1].elements.len() < B
|
||||
} else {
|
||||
true
|
||||
}
|
||||
{
|
||||
// if the child and its immediate siblings have B-1 elements merge the child
|
||||
// with one sibling, moving an element from this node into the new merged node
|
||||
// to be the median
|
||||
|
||||
if child_index > 0 {
|
||||
let middle = self.elements.remove(child_index - 1);
|
||||
|
||||
// use the predessor sibling
|
||||
let successor = self.children.remove(child_index);
|
||||
child_index -= 1;
|
||||
|
||||
self.children[child_index].merge(middle, successor);
|
||||
} else {
|
||||
let middle = self.elements.remove(child_index);
|
||||
|
||||
// use the sucessor sibling
|
||||
let successor = self.children.remove(child_index + 1);
|
||||
|
||||
self.children[child_index].merge(middle, successor);
|
||||
}
|
||||
} else if self.children[child_index].elements.len() < B {
|
||||
if child_index > 0
|
||||
&& self
|
||||
.children
|
||||
.get(child_index - 1)
|
||||
.map_or(false, |c| c.elements.len() >= B)
|
||||
{
|
||||
let last_element = self.children[child_index - 1].elements.pop().unwrap();
|
||||
assert!(!self.children[child_index - 1].elements.is_empty());
|
||||
self.children[child_index - 1].length -= 1;
|
||||
self.children[child_index - 1].index.remove(&last_element);
|
||||
|
||||
let parent_element =
|
||||
mem::replace(&mut self.elements[child_index - 1], last_element);
|
||||
|
||||
self.children[child_index].index.insert(&parent_element);
|
||||
self.children[child_index]
|
||||
.elements
|
||||
.insert(0, parent_element);
|
||||
self.children[child_index].length += 1;
|
||||
|
||||
if let Some(last_child) = self.children[child_index - 1].children.pop() {
|
||||
self.children[child_index - 1].length -= last_child.len();
|
||||
self.children[child_index - 1].reindex();
|
||||
self.children[child_index].length += last_child.len();
|
||||
self.children[child_index].children.insert(0, last_child);
|
||||
self.children[child_index].reindex();
|
||||
}
|
||||
} else if self
|
||||
.children
|
||||
.get(child_index + 1)
|
||||
.map_or(false, |c| c.elements.len() >= B)
|
||||
{
|
||||
let first_element = self.children[child_index + 1].elements.remove(0);
|
||||
self.children[child_index + 1].index.remove(&first_element);
|
||||
self.children[child_index + 1].length -= 1;
|
||||
|
||||
assert!(!self.children[child_index + 1].elements.is_empty());
|
||||
|
||||
let parent_element = mem::replace(&mut self.elements[child_index], first_element);
|
||||
|
||||
self.children[child_index].length += 1;
|
||||
self.children[child_index].index.insert(&parent_element);
|
||||
self.children[child_index].elements.push(parent_element);
|
||||
|
||||
if !self.children[child_index + 1].is_leaf() {
|
||||
let first_child = self.children[child_index + 1].children.remove(0);
|
||||
self.children[child_index + 1].length -= first_child.len();
|
||||
self.children[child_index + 1].reindex();
|
||||
self.children[child_index].length += first_child.len();
|
||||
|
||||
self.children[child_index].children.push(first_child);
|
||||
self.children[child_index].reindex();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.length -= 1;
|
||||
let total_index = self.cumulative_index(child_index);
|
||||
self.children[child_index].remove(index - total_index)
|
||||
}
|
||||
|
||||
fn check(&self) -> usize {
|
||||
let l = self.elements.len() + self.children.iter().map(|c| c.check()).sum::<usize>();
|
||||
assert_eq!(self.len(), l, "{:#?}", self);
|
||||
|
||||
l
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&mut self, index: usize) -> Op {
|
||||
let original_len = self.len();
|
||||
if self.is_leaf() {
|
||||
let v = self.remove_from_leaf(index);
|
||||
self.index.remove(&v);
|
||||
assert_eq!(original_len, self.len() + 1);
|
||||
debug_assert_eq!(self.check(), self.len());
|
||||
v
|
||||
} else {
|
||||
let mut total_index = 0;
|
||||
for (child_index, child) in self.children.iter().enumerate() {
|
||||
match (total_index + child.len()).cmp(&index) {
|
||||
Ordering::Less => {
|
||||
// should be later on in the loop
|
||||
total_index += child.len() + 1;
|
||||
continue;
|
||||
}
|
||||
Ordering::Equal => {
|
||||
let v = self.remove_element_from_non_leaf(
|
||||
index,
|
||||
min(child_index, self.elements.len() - 1),
|
||||
);
|
||||
self.index.remove(&v);
|
||||
assert_eq!(original_len, self.len() + 1);
|
||||
debug_assert_eq!(self.check(), self.len());
|
||||
return v;
|
||||
}
|
||||
Ordering::Greater => {
|
||||
let v = self.remove_from_internal_child(index, child_index);
|
||||
self.index.remove(&v);
|
||||
assert_eq!(original_len, self.len() + 1);
|
||||
debug_assert_eq!(self.check(), self.len());
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
||||
panic!(
|
||||
"index not found to remove {} {} {} {}",
|
||||
index,
|
||||
total_index,
|
||||
self.len(),
|
||||
self.check()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, middle: Op, successor_sibling: OpTreeNode) {
|
||||
self.index.insert(&middle);
|
||||
self.index.merge(&successor_sibling.index);
|
||||
self.elements.push(middle);
|
||||
self.elements.extend(successor_sibling.elements);
|
||||
self.children.extend(successor_sibling.children);
|
||||
self.length += successor_sibling.length + 1;
|
||||
assert!(self.is_full());
|
||||
}
|
||||
|
||||
/// Update the operation at the given index using the provided function.
|
||||
///
|
||||
/// This handles updating the indices after the update.
|
||||
pub(crate) fn update<F>(&mut self, index: usize, f: F) -> ReplaceArgs
|
||||
where
|
||||
F: FnOnce(&mut Op),
|
||||
{
|
||||
if self.is_leaf() {
|
||||
let new_element = self.elements.get_mut(index).unwrap();
|
||||
let old_id = new_element.id;
|
||||
let old_visible = new_element.visible();
|
||||
f(new_element);
|
||||
let replace_args = ReplaceArgs {
|
||||
old_id,
|
||||
new_id: new_element.id,
|
||||
old_visible,
|
||||
new_visible: new_element.visible(),
|
||||
new_key: new_element.elemid_or_key(),
|
||||
};
|
||||
self.index.replace(&replace_args);
|
||||
replace_args
|
||||
} else {
|
||||
let mut cumulative_len = 0;
|
||||
let len = self.len();
|
||||
for (child_index, child) in self.children.iter_mut().enumerate() {
|
||||
match (cumulative_len + child.len()).cmp(&index) {
|
||||
Ordering::Less => {
|
||||
cumulative_len += child.len() + 1;
|
||||
}
|
||||
Ordering::Equal => {
|
||||
let new_element = self.elements.get_mut(child_index).unwrap();
|
||||
let old_id = new_element.id;
|
||||
let old_visible = new_element.visible();
|
||||
f(new_element);
|
||||
let replace_args = ReplaceArgs {
|
||||
old_id,
|
||||
new_id: new_element.id,
|
||||
old_visible,
|
||||
new_visible: new_element.visible(),
|
||||
new_key: new_element.elemid_or_key(),
|
||||
};
|
||||
self.index.replace(&replace_args);
|
||||
return replace_args;
|
||||
}
|
||||
Ordering::Greater => {
|
||||
let replace_args = child.update(index - cumulative_len, f);
|
||||
self.index.replace(&replace_args);
|
||||
return replace_args;
|
||||
}
|
||||
}
|
||||
}
|
||||
panic!("Invalid index to set: {} but len was {}", index, len)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn last(&self) -> &Op {
|
||||
if self.is_leaf() {
|
||||
// node is never empty so this is safe
|
||||
self.elements.last().unwrap()
|
||||
} else {
|
||||
// if not a leaf then there is always at least one child
|
||||
self.children.last().unwrap().last()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get(&self, index: usize) -> Option<&Op> {
|
||||
if self.is_leaf() {
|
||||
return self.elements.get(index);
|
||||
} else {
|
||||
let mut cumulative_len = 0;
|
||||
for (child_index, child) in self.children.iter().enumerate() {
|
||||
match (cumulative_len + child.len()).cmp(&index) {
|
||||
Ordering::Less => {
|
||||
cumulative_len += child.len() + 1;
|
||||
}
|
||||
Ordering::Equal => return self.elements.get(child_index),
|
||||
Ordering::Greater => {
|
||||
return child.get(index - cumulative_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for OpTreeInternal {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for OpTreeInternal {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.len() == other.len() && self.iter().zip(other.iter()).all(|(a, b)| a == b)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a OpTreeInternal {
|
||||
type Item = &'a Op;
|
||||
|
||||
type IntoIter = Iter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
Iter {
|
||||
inner: self,
|
||||
index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Iter<'a> {
|
||||
inner: &'a OpTreeInternal,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Iter<'a> {
|
||||
type Item = &'a Op;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.index += 1;
|
||||
self.inner.get(self.index - 1)
|
||||
}
|
||||
|
||||
fn nth(&mut self, n: usize) -> Option<Self::Item> {
|
||||
self.index += n + 1;
|
||||
self.inner.get(self.index - 1)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct CounterData {
|
||||
pos: usize,
|
||||
val: i64,
|
||||
succ: HashSet<OpId>,
|
||||
op: Op,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::legacy as amp;
|
||||
use crate::types::{Op, OpId};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn op() -> Op {
|
||||
let zero = OpId(0, 0);
|
||||
Op {
|
||||
id: zero,
|
||||
action: amp::OpType::Put(0.into()),
|
||||
key: zero.into(),
|
||||
succ: Default::default(),
|
||||
pred: Default::default(),
|
||||
insert: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert() {
|
||||
let mut t: OpTree = OpTree::new();
|
||||
|
||||
t.internal.insert(0, op());
|
||||
t.internal.insert(1, op());
|
||||
t.internal.insert(0, op());
|
||||
t.internal.insert(0, op());
|
||||
t.internal.insert(0, op());
|
||||
t.internal.insert(3, op());
|
||||
t.internal.insert(4, op());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_book() {
|
||||
let mut t: OpTree = OpTree::new();
|
||||
|
||||
for i in 0..100 {
|
||||
t.internal.insert(i % 2, op());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_book_vec() {
|
||||
let mut t: OpTree = OpTree::new();
|
||||
let mut v = Vec::new();
|
||||
|
||||
for i in 0..100 {
|
||||
t.internal.insert(i % 3, op());
|
||||
v.insert(i % 3, op());
|
||||
|
||||
assert_eq!(v, t.internal.iter().cloned().collect::<Vec<_>>())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
use crate::op_set::OpSet;
|
||||
use crate::types::ObjId;
|
||||
use crate::{exid::ExId, Prop};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Parents<'a> {
|
||||
pub(crate) obj: ObjId,
|
||||
pub(crate) ops: &'a OpSet,
|
||||
}
|
||||
|
||||
impl<'a> Parents<'a> {
|
||||
pub fn path(&mut self) -> Vec<(ExId, Prop)> {
|
||||
let mut path = self.collect::<Vec<_>>();
|
||||
path.reverse();
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Parents<'a> {
|
||||
type Item = (ExId, Prop);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.obj.is_root() {
|
||||
None
|
||||
} else if let Some((obj, key)) = self.ops.parent_object(&self.obj) {
|
||||
self.obj = obj;
|
||||
Some((
|
||||
self.ops.id_to_exid(self.obj.0),
|
||||
self.ops.export_key(self.obj, key),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
use crate::{
|
||||
op_tree::OpTreeNode,
|
||||
types::{ElemId, Key},
|
||||
};
|
||||
|
||||
use super::{QueryResult, TreeQuery};
|
||||
|
||||
/// Lookup the index in the list that this elemid occupies.
|
||||
pub(crate) struct ElemIdPos {
|
||||
elemid: ElemId,
|
||||
pos: usize,
|
||||
found: bool,
|
||||
}
|
||||
|
||||
impl ElemIdPos {
|
||||
pub(crate) fn new(elemid: ElemId) -> Self {
|
||||
Self {
|
||||
elemid,
|
||||
pos: 0,
|
||||
found: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn index(&self) -> Option<usize> {
|
||||
if self.found {
|
||||
Some(self.pos)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TreeQuery<'a> for ElemIdPos {
|
||||
fn query_node(&mut self, child: &OpTreeNode) -> QueryResult {
|
||||
// if index has our element then we can continue
|
||||
if child.index.has_visible(&Key::Seq(self.elemid)) {
|
||||
// element is in this node somewhere
|
||||
QueryResult::Descend
|
||||
} else {
|
||||
// not in this node, try the next one
|
||||
self.pos += child.index.visible_len();
|
||||
QueryResult::Next
|
||||
}
|
||||
}
|
||||
|
||||
fn query_element(&mut self, element: &crate::types::Op) -> QueryResult {
|
||||
if element.elemid() == Some(self.elemid) {
|
||||
// this is it
|
||||
self.found = true;
|
||||
return QueryResult::Finish;
|
||||
} else if element.visible() {
|
||||
self.pos += 1;
|
||||
}
|
||||
QueryResult::Next
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
use crate::op_tree::OpTreeNode;
|
||||
use crate::query::{QueryResult, TreeQuery};
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct Len {
|
||||
pub(crate) len: usize,
|
||||
}
|
||||
|
||||
impl Len {
|
||||
pub(crate) fn new() -> Self {
|
||||
Len { len: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TreeQuery<'a> for Len {
|
||||
fn query_node(&mut self, child: &OpTreeNode) -> QueryResult {
|
||||
self.len = child.index.visible_len();
|
||||
QueryResult::Finish
|
||||
}
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
use crate::op_tree::{OpSetMetadata, OpTreeNode};
|
||||
use crate::query::{binary_search_by, QueryResult, TreeQuery};
|
||||
use crate::types::{Key, Op};
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct Prop<'a> {
|
||||
key: Key,
|
||||
pub(crate) ops: Vec<&'a Op>,
|
||||
pub(crate) ops_pos: Vec<usize>,
|
||||
pub(crate) pos: usize,
|
||||
start: Option<Start>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct Start {
|
||||
/// The index to start searching for in the optree
|
||||
idx: usize,
|
||||
/// The total length of the optree
|
||||
optree_len: usize,
|
||||
}
|
||||
|
||||
impl<'a> Prop<'a> {
|
||||
pub(crate) fn new(prop: usize) -> Self {
|
||||
Prop {
|
||||
key: Key::Map(prop),
|
||||
ops: vec![],
|
||||
ops_pos: vec![],
|
||||
pos: 0,
|
||||
start: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TreeQuery<'a> for Prop<'a> {
|
||||
fn query_node_with_metadata(
|
||||
&mut self,
|
||||
child: &'a OpTreeNode,
|
||||
m: &OpSetMetadata,
|
||||
) -> QueryResult {
|
||||
if let Some(Start {
|
||||
idx: start,
|
||||
optree_len,
|
||||
}) = self.start
|
||||
{
|
||||
if self.pos + child.len() >= start {
|
||||
// skip empty nodes
|
||||
if child.index.visible_len() == 0 {
|
||||
if self.pos + child.len() >= optree_len {
|
||||
self.pos = optree_len;
|
||||
QueryResult::Finish
|
||||
} else {
|
||||
self.pos += child.len();
|
||||
QueryResult::Next
|
||||
}
|
||||
} else {
|
||||
QueryResult::Descend
|
||||
}
|
||||
} else {
|
||||
self.pos += child.len();
|
||||
QueryResult::Next
|
||||
}
|
||||
} else {
|
||||
// in the root node find the first op position for the key
|
||||
let start = binary_search_by(child, |op| m.key_cmp(&op.key, &self.key));
|
||||
self.start = Some(Start {
|
||||
idx: start,
|
||||
optree_len: child.len(),
|
||||
});
|
||||
self.pos = start;
|
||||
QueryResult::Skip(start)
|
||||
}
|
||||
}
|
||||
|
||||
fn query_element(&mut self, op: &'a Op) -> QueryResult {
|
||||
// don't bother looking at things past our key
|
||||
if op.key != self.key {
|
||||
return QueryResult::Finish;
|
||||
}
|
||||
if op.visible() {
|
||||
self.ops.push(op);
|
||||
self.ops_pos.push(self.pos);
|
||||
}
|
||||
self.pos += 1;
|
||||
QueryResult::Next
|
||||
}
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
use crate::op_tree::{OpSetMetadata, OpTreeNode};
|
||||
use crate::query::{binary_search_by, QueryResult, TreeQuery};
|
||||
use crate::types::{Key, Op, HEAD};
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct SeekOp<'a> {
|
||||
/// the op we are looking for
|
||||
op: &'a Op,
|
||||
/// The position to insert at
|
||||
pub(crate) pos: usize,
|
||||
/// The indices of ops that this op overwrites
|
||||
pub(crate) succ: Vec<usize>,
|
||||
/// whether a position has been found
|
||||
found: bool,
|
||||
/// The found start position of the key if there is one yet (for map objects).
|
||||
start: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'a> SeekOp<'a> {
|
||||
pub(crate) fn new(op: &'a Op) -> Self {
|
||||
SeekOp {
|
||||
op,
|
||||
succ: vec![],
|
||||
pos: 0,
|
||||
found: false,
|
||||
start: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn lesser_insert(&self, op: &Op, m: &OpSetMetadata) -> bool {
|
||||
op.insert && m.lamport_cmp(op.id, self.op.id) == Ordering::Less
|
||||
}
|
||||
|
||||
fn greater_opid(&self, op: &Op, m: &OpSetMetadata) -> bool {
|
||||
m.lamport_cmp(op.id, self.op.id) == Ordering::Greater
|
||||
}
|
||||
|
||||
fn is_target_insert(&self, op: &Op) -> bool {
|
||||
op.insert && op.elemid() == self.op.key.elemid()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TreeQuery<'a> for SeekOp<'a> {
|
||||
fn query_node_with_metadata(&mut self, child: &OpTreeNode, m: &OpSetMetadata) -> QueryResult {
|
||||
if self.found {
|
||||
return QueryResult::Descend;
|
||||
}
|
||||
match self.op.key {
|
||||
Key::Seq(HEAD) => {
|
||||
while self.pos < child.len() {
|
||||
let op = child.get(self.pos).unwrap();
|
||||
if op.insert && m.lamport_cmp(op.id, self.op.id) == Ordering::Less {
|
||||
break;
|
||||
}
|
||||
self.pos += 1;
|
||||
}
|
||||
QueryResult::Finish
|
||||
}
|
||||
Key::Seq(e) => {
|
||||
if child.index.ops.contains(&e.0) {
|
||||
QueryResult::Descend
|
||||
} else {
|
||||
self.pos += child.len();
|
||||
QueryResult::Next
|
||||
}
|
||||
}
|
||||
Key::Map(_) => {
|
||||
if let Some(start) = self.start {
|
||||
if self.pos + child.len() >= start {
|
||||
// skip empty nodes
|
||||
if child.index.visible_len() == 0 {
|
||||
self.pos += child.len();
|
||||
QueryResult::Next
|
||||
} else {
|
||||
QueryResult::Descend
|
||||
}
|
||||
} else {
|
||||
self.pos += child.len();
|
||||
QueryResult::Next
|
||||
}
|
||||
} else {
|
||||
// in the root node find the first op position for the key
|
||||
let start = binary_search_by(child, |op| m.key_cmp(&op.key, &self.op.key));
|
||||
self.start = Some(start);
|
||||
self.pos = start;
|
||||
QueryResult::Skip(start)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn query_element_with_metadata(&mut self, e: &Op, m: &OpSetMetadata) -> QueryResult {
|
||||
match self.op.key {
|
||||
Key::Map(_) => {
|
||||
// don't bother looking at things past our key
|
||||
if e.key != self.op.key {
|
||||
return QueryResult::Finish;
|
||||
}
|
||||
|
||||
if self.op.overwrites(e) {
|
||||
self.succ.push(self.pos);
|
||||
}
|
||||
|
||||
if m.lamport_cmp(e.id, self.op.id) == Ordering::Greater {
|
||||
return QueryResult::Finish;
|
||||
}
|
||||
|
||||
self.pos += 1;
|
||||
QueryResult::Next
|
||||
}
|
||||
Key::Seq(_) => {
|
||||
if !self.found {
|
||||
if self.is_target_insert(e) {
|
||||
self.found = true;
|
||||
if self.op.overwrites(e) {
|
||||
self.succ.push(self.pos);
|
||||
}
|
||||
}
|
||||
self.pos += 1;
|
||||
QueryResult::Next
|
||||
} else {
|
||||
// we have already found the target
|
||||
if self.op.overwrites(e) {
|
||||
self.succ.push(self.pos);
|
||||
}
|
||||
if self.op.insert {
|
||||
if self.lesser_insert(e, m) {
|
||||
QueryResult::Finish
|
||||
} else {
|
||||
self.pos += 1;
|
||||
QueryResult::Next
|
||||
}
|
||||
} else if e.insert || self.greater_opid(e, m) {
|
||||
QueryResult::Finish
|
||||
} else {
|
||||
self.pos += 1;
|
||||
QueryResult::Next
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
use core::mem::size_of;
|
||||
use std::num::NonZeroU64;
|
||||
|
||||
use super::{take1, Input, ParseError, ParseResult};
|
||||
|
||||
#[derive(PartialEq, thiserror::Error, Debug, Clone)]
|
||||
pub(crate) enum Error {
|
||||
#[error("leb128 was too large for the destination type")]
|
||||
Leb128TooLarge,
|
||||
#[error("leb128 was zero when it was expected to be nonzero")]
|
||||
UnexpectedZero,
|
||||
}
|
||||
|
||||
macro_rules! impl_leb {
|
||||
($parser_name: ident, $ty: ty) => {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn $parser_name<'a, E>(input: Input<'a>) -> ParseResult<'a, $ty, E>
|
||||
where
|
||||
E: From<Error>,
|
||||
{
|
||||
let mut res = 0;
|
||||
let mut shift = 0;
|
||||
|
||||
let mut input = input;
|
||||
let mut pos = 0;
|
||||
loop {
|
||||
let (i, byte) = take1(input)?;
|
||||
input = i;
|
||||
if (byte & 0x80) == 0 {
|
||||
res |= (byte as $ty) << shift;
|
||||
return Ok((input, res));
|
||||
} else if pos == leb128_size::<$ty>() - 1 {
|
||||
return Err(ParseError::Error(Error::Leb128TooLarge.into()));
|
||||
} else {
|
||||
res |= ((byte & 0x7F) as $ty) << shift;
|
||||
}
|
||||
pos += 1;
|
||||
shift += 7;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_leb!(leb128_u64, u64);
|
||||
impl_leb!(leb128_u32, u32);
|
||||
impl_leb!(leb128_i64, i64);
|
||||
impl_leb!(leb128_i32, i32);
|
||||
|
||||
/// Parse a LEB128 encoded u64 from the input, throwing an error if it is `0`
|
||||
pub(crate) fn nonzero_leb128_u64<E>(input: Input<'_>) -> ParseResult<'_, NonZeroU64, E>
|
||||
where
|
||||
E: From<Error>,
|
||||
{
|
||||
let (input, num) = leb128_u64(input)?;
|
||||
let result =
|
||||
NonZeroU64::new(num).ok_or_else(|| ParseError::Error(Error::UnexpectedZero.into()))?;
|
||||
Ok((input, result))
|
||||
}
|
||||
|
||||
/// Maximum LEB128-encoded size of an integer type
|
||||
const fn leb128_size<T>() -> usize {
|
||||
let bits = size_of::<T>() * 8;
|
||||
(bits + 6) / 7 // equivalent to ceil(bits/7) w/o floats
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::Needed;
|
||||
use super::*;
|
||||
use std::{convert::TryFrom, num::NonZeroUsize};
|
||||
|
||||
const NEED_ONE: Needed = Needed::Size(unsafe { NonZeroUsize::new_unchecked(1) });
|
||||
|
||||
#[test]
|
||||
fn leb_128_unsigned() {
|
||||
let one = &[0b00000001_u8];
|
||||
let one_two_nine = &[0b10000001, 0b00000001];
|
||||
let one_and_more = &[0b00000001, 0b00000011];
|
||||
|
||||
let scenarios: Vec<(&'static [u8], ParseResult<'_, u64, Error>)> = vec![
|
||||
(one, Ok((Input::with_position(one, 1), 1))),
|
||||
(&[0b10000001_u8], Err(ParseError::Incomplete(NEED_ONE))),
|
||||
(
|
||||
one_two_nine,
|
||||
Ok((Input::with_position(one_two_nine, 2), 129)),
|
||||
),
|
||||
(one_and_more, Ok((Input::with_position(one_and_more, 1), 1))),
|
||||
(
|
||||
&[129, 129, 129, 129, 129, 129, 129, 129, 129, 129, 129, 129],
|
||||
Err(ParseError::Error(Error::Leb128TooLarge)),
|
||||
),
|
||||
];
|
||||
for (index, (input, expected)) in scenarios.clone().into_iter().enumerate() {
|
||||
let result = leb128_u64(Input::new(input));
|
||||
if result != expected {
|
||||
panic!(
|
||||
"Scenario {} failed for u64: expected {:?} got {:?}",
|
||||
index + 1,
|
||||
expected,
|
||||
result
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (index, (input, expected)) in scenarios.into_iter().enumerate() {
|
||||
let u32_expected = expected.map(|(i, e)| (i, u32::try_from(e).unwrap()));
|
||||
let result = leb128_u32(Input::new(input));
|
||||
if result != u32_expected {
|
||||
panic!(
|
||||
"Scenario {} failed for u32: expected {:?} got {:?}",
|
||||
index + 1,
|
||||
u32_expected,
|
||||
result
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,530 +0,0 @@
|
|||
use itertools::Itertools;
|
||||
use serde::ser::SerializeMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::{
|
||||
storage::{parse, Change as StoredChange, ReadChangeOpError},
|
||||
Automerge, AutomergeError, Change, ChangeHash, OpObserver,
|
||||
};
|
||||
|
||||
mod bloom;
|
||||
mod state;
|
||||
|
||||
pub use bloom::BloomFilter;
|
||||
pub use state::DecodeError as DecodeStateError;
|
||||
pub use state::{Have, State};
|
||||
|
||||
const MESSAGE_TYPE_SYNC: u8 = 0x42; // first byte of a sync message, for identification
|
||||
|
||||
impl Automerge {
|
||||
pub fn generate_sync_message(&self, sync_state: &mut State) -> Option<Message> {
|
||||
let our_heads = self.get_heads();
|
||||
|
||||
let our_need = self.get_missing_deps(sync_state.their_heads.as_ref().unwrap_or(&vec![]));
|
||||
|
||||
let their_heads_set = if let Some(ref heads) = sync_state.their_heads {
|
||||
heads.iter().collect::<HashSet<_>>()
|
||||
} else {
|
||||
HashSet::new()
|
||||
};
|
||||
let our_have = if our_need.iter().all(|hash| their_heads_set.contains(hash)) {
|
||||
vec![self.make_bloom_filter(sync_state.shared_heads.clone())]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
if let Some(ref their_have) = sync_state.their_have {
|
||||
if let Some(first_have) = their_have.first().as_ref() {
|
||||
if !first_have
|
||||
.last_sync
|
||||
.iter()
|
||||
.all(|hash| self.get_change_by_hash(hash).is_some())
|
||||
{
|
||||
let reset_msg = Message {
|
||||
heads: our_heads,
|
||||
need: Vec::new(),
|
||||
have: vec![Have::default()],
|
||||
changes: Vec::new(),
|
||||
};
|
||||
return Some(reset_msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let changes_to_send = if let (Some(their_have), Some(their_need)) = (
|
||||
sync_state.their_have.as_ref(),
|
||||
sync_state.their_need.as_ref(),
|
||||
) {
|
||||
self.get_changes_to_send(their_have, their_need)
|
||||
.expect("Should have only used hashes that are in the document")
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let heads_unchanged = sync_state.last_sent_heads == our_heads;
|
||||
|
||||
let heads_equal = if let Some(their_heads) = sync_state.their_heads.as_ref() {
|
||||
their_heads == &our_heads
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if heads_unchanged && heads_equal && changes_to_send.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// deduplicate the changes to send with those we have already sent and clone it now
|
||||
let changes_to_send = changes_to_send
|
||||
.into_iter()
|
||||
.filter_map(|change| {
|
||||
if !sync_state.sent_hashes.contains(&change.hash()) {
|
||||
Some(change.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
sync_state.last_sent_heads = our_heads.clone();
|
||||
sync_state
|
||||
.sent_hashes
|
||||
.extend(changes_to_send.iter().map(|c| c.hash()));
|
||||
|
||||
let sync_message = Message {
|
||||
heads: our_heads,
|
||||
have: our_have,
|
||||
need: our_need,
|
||||
changes: changes_to_send,
|
||||
};
|
||||
|
||||
Some(sync_message)
|
||||
}
|
||||
|
||||
pub fn receive_sync_message(
|
||||
&mut self,
|
||||
sync_state: &mut State,
|
||||
message: Message,
|
||||
) -> Result<(), AutomergeError> {
|
||||
self.receive_sync_message_with::<()>(sync_state, message, None)
|
||||
}
|
||||
|
||||
pub fn receive_sync_message_with<Obs: OpObserver>(
|
||||
&mut self,
|
||||
sync_state: &mut State,
|
||||
message: Message,
|
||||
op_observer: Option<&mut Obs>,
|
||||
) -> Result<(), AutomergeError> {
|
||||
let before_heads = self.get_heads();
|
||||
|
||||
let Message {
|
||||
heads: message_heads,
|
||||
changes: message_changes,
|
||||
need: message_need,
|
||||
have: message_have,
|
||||
} = message;
|
||||
|
||||
let changes_is_empty = message_changes.is_empty();
|
||||
if !changes_is_empty {
|
||||
self.apply_changes_with(message_changes, op_observer)?;
|
||||
sync_state.shared_heads = advance_heads(
|
||||
&before_heads.iter().collect(),
|
||||
&self.get_heads().into_iter().collect(),
|
||||
&sync_state.shared_heads,
|
||||
);
|
||||
}
|
||||
|
||||
// trim down the sent hashes to those that we know they haven't seen
|
||||
self.filter_changes(&message_heads, &mut sync_state.sent_hashes)?;
|
||||
|
||||
if changes_is_empty && message_heads == before_heads {
|
||||
sync_state.last_sent_heads = message_heads.clone();
|
||||
}
|
||||
|
||||
let known_heads = message_heads
|
||||
.iter()
|
||||
.filter(|head| self.get_change_by_hash(head).is_some())
|
||||
.collect::<Vec<_>>();
|
||||
if known_heads.len() == message_heads.len() {
|
||||
sync_state.shared_heads = message_heads.clone();
|
||||
// If the remote peer has lost all its data, reset our state to perform a full resync
|
||||
if message_heads.is_empty() {
|
||||
sync_state.last_sent_heads = Default::default();
|
||||
sync_state.sent_hashes = Default::default();
|
||||
}
|
||||
} else {
|
||||
sync_state.shared_heads = sync_state
|
||||
.shared_heads
|
||||
.iter()
|
||||
.chain(known_heads)
|
||||
.copied()
|
||||
.unique()
|
||||
.sorted()
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
|
||||
sync_state.their_have = Some(message_have);
|
||||
sync_state.their_heads = Some(message_heads);
|
||||
sync_state.their_need = Some(message_need);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn make_bloom_filter(&self, last_sync: Vec<ChangeHash>) -> Have {
|
||||
let new_changes = self
|
||||
.get_changes(&last_sync)
|
||||
.expect("Should have only used hashes that are in the document");
|
||||
let hashes = new_changes.iter().map(|change| change.hash());
|
||||
Have {
|
||||
last_sync,
|
||||
bloom: BloomFilter::from_hashes(hashes),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_changes_to_send(
|
||||
&self,
|
||||
have: &[Have],
|
||||
need: &[ChangeHash],
|
||||
) -> Result<Vec<&Change>, AutomergeError> {
|
||||
if have.is_empty() {
|
||||
Ok(need
|
||||
.iter()
|
||||
.filter_map(|hash| self.get_change_by_hash(hash))
|
||||
.collect())
|
||||
} else {
|
||||
let mut last_sync_hashes = HashSet::new();
|
||||
let mut bloom_filters = Vec::with_capacity(have.len());
|
||||
|
||||
for h in have {
|
||||
let Have { last_sync, bloom } = h;
|
||||
last_sync_hashes.extend(last_sync);
|
||||
bloom_filters.push(bloom);
|
||||
}
|
||||
let last_sync_hashes = last_sync_hashes.into_iter().copied().collect::<Vec<_>>();
|
||||
|
||||
let changes = self.get_changes(&last_sync_hashes)?;
|
||||
|
||||
let mut change_hashes = HashSet::with_capacity(changes.len());
|
||||
let mut dependents: HashMap<ChangeHash, Vec<ChangeHash>> = HashMap::new();
|
||||
let mut hashes_to_send = HashSet::new();
|
||||
|
||||
for change in &changes {
|
||||
change_hashes.insert(change.hash());
|
||||
|
||||
for dep in change.deps() {
|
||||
dependents.entry(*dep).or_default().push(change.hash());
|
||||
}
|
||||
|
||||
if bloom_filters
|
||||
.iter()
|
||||
.all(|bloom| !bloom.contains_hash(&change.hash()))
|
||||
{
|
||||
hashes_to_send.insert(change.hash());
|
||||
}
|
||||
}
|
||||
|
||||
let mut stack = hashes_to_send.iter().copied().collect::<Vec<_>>();
|
||||
while let Some(hash) = stack.pop() {
|
||||
if let Some(deps) = dependents.get(&hash) {
|
||||
for dep in deps {
|
||||
if hashes_to_send.insert(*dep) {
|
||||
stack.push(*dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut changes_to_send = Vec::new();
|
||||
for hash in need {
|
||||
hashes_to_send.insert(*hash);
|
||||
if !change_hashes.contains(hash) {
|
||||
let change = self.get_change_by_hash(hash);
|
||||
if let Some(change) = change {
|
||||
changes_to_send.push(change);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for change in changes {
|
||||
if hashes_to_send.contains(&change.hash()) {
|
||||
changes_to_send.push(change);
|
||||
}
|
||||
}
|
||||
Ok(changes_to_send)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ReadMessageError {
|
||||
#[error("expected {expected_one_of:?} but found {found}")]
|
||||
WrongType { expected_one_of: Vec<u8>, found: u8 },
|
||||
#[error("{0}")]
|
||||
Parse(String),
|
||||
#[error(transparent)]
|
||||
ReadChangeOps(#[from] ReadChangeOpError),
|
||||
#[error("not enough input")]
|
||||
NotEnoughInput,
|
||||
}
|
||||
|
||||
impl From<parse::leb128::Error> for ReadMessageError {
|
||||
fn from(e: parse::leb128::Error) -> Self {
|
||||
ReadMessageError::Parse(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bloom::ParseError> for ReadMessageError {
|
||||
fn from(e: bloom::ParseError) -> Self {
|
||||
ReadMessageError::Parse(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::storage::change::ParseError> for ReadMessageError {
|
||||
fn from(e: crate::storage::change::ParseError) -> Self {
|
||||
ReadMessageError::Parse(format!("error parsing changes: {}", e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ReadMessageError> for parse::ParseError<ReadMessageError> {
|
||||
fn from(e: ReadMessageError) -> Self {
|
||||
parse::ParseError::Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<parse::ParseError<ReadMessageError>> for ReadMessageError {
|
||||
fn from(p: parse::ParseError<ReadMessageError>) -> Self {
|
||||
match p {
|
||||
parse::ParseError::Error(e) => e,
|
||||
parse::ParseError::Incomplete(..) => Self::NotEnoughInput,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The sync message to be sent.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Message {
|
||||
/// The heads of the sender.
|
||||
pub heads: Vec<ChangeHash>,
|
||||
/// The hashes of any changes that are being explicitly requested from the recipient.
|
||||
pub need: Vec<ChangeHash>,
|
||||
/// A summary of the changes that the sender already has.
|
||||
pub have: Vec<Have>,
|
||||
/// The changes for the recipient to apply.
|
||||
pub changes: Vec<Change>,
|
||||
}
|
||||
|
||||
impl serde::Serialize for Message {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(Some(4))?;
|
||||
map.serialize_entry("heads", &self.heads)?;
|
||||
map.serialize_entry("need", &self.need)?;
|
||||
map.serialize_entry("have", &self.have)?;
|
||||
map.serialize_entry(
|
||||
"changes",
|
||||
&self
|
||||
.changes
|
||||
.iter()
|
||||
.map(crate::ExpandedChange::from)
|
||||
.collect::<Vec<_>>(),
|
||||
)?;
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_have(input: parse::Input<'_>) -> parse::ParseResult<'_, Have, ReadMessageError> {
|
||||
let (i, last_sync) = parse::length_prefixed(parse::change_hash)(input)?;
|
||||
let (i, bloom_bytes) = parse::length_prefixed_bytes(i)?;
|
||||
let (_, bloom) = BloomFilter::parse(parse::Input::new(bloom_bytes)).map_err(|e| e.lift())?;
|
||||
Ok((i, Have { last_sync, bloom }))
|
||||
}
|
||||
|
||||
impl Message {
|
||||
pub fn decode(input: &[u8]) -> Result<Self, ReadMessageError> {
|
||||
let input = parse::Input::new(input);
|
||||
match Self::parse(input) {
|
||||
Ok((_, msg)) => Ok(msg),
|
||||
Err(parse::ParseError::Error(e)) => Err(e),
|
||||
Err(parse::ParseError::Incomplete(_)) => Err(ReadMessageError::NotEnoughInput),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse(input: parse::Input<'_>) -> parse::ParseResult<'_, Self, ReadMessageError> {
|
||||
let (i, message_type) = parse::take1(input)?;
|
||||
if message_type != MESSAGE_TYPE_SYNC {
|
||||
return Err(parse::ParseError::Error(ReadMessageError::WrongType {
|
||||
expected_one_of: vec![MESSAGE_TYPE_SYNC],
|
||||
found: message_type,
|
||||
}));
|
||||
}
|
||||
|
||||
let (i, heads) = parse::length_prefixed(parse::change_hash)(i)?;
|
||||
let (i, need) = parse::length_prefixed(parse::change_hash)(i)?;
|
||||
let (i, have) = parse::length_prefixed(parse_have)(i)?;
|
||||
|
||||
let change_parser = |i| {
|
||||
let (i, bytes) = parse::length_prefixed_bytes(i)?;
|
||||
let (_, change) =
|
||||
StoredChange::parse(parse::Input::new(bytes)).map_err(|e| e.lift())?;
|
||||
Ok((i, change))
|
||||
};
|
||||
let (i, stored_changes) = parse::length_prefixed(change_parser)(i)?;
|
||||
let changes_len = stored_changes.len();
|
||||
let changes: Vec<Change> = stored_changes
|
||||
.into_iter()
|
||||
.try_fold::<_, _, Result<_, ReadMessageError>>(
|
||||
Vec::with_capacity(changes_len),
|
||||
|mut acc, stored| {
|
||||
let change = Change::new_from_unverified(stored.into_owned(), None)
|
||||
.map_err(ReadMessageError::ReadChangeOps)?;
|
||||
acc.push(change);
|
||||
Ok(acc)
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok((
|
||||
i,
|
||||
Message {
|
||||
heads,
|
||||
need,
|
||||
have,
|
||||
changes,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn encode(mut self) -> Vec<u8> {
|
||||
let mut buf = vec![MESSAGE_TYPE_SYNC];
|
||||
|
||||
encode_hashes(&mut buf, &self.heads);
|
||||
encode_hashes(&mut buf, &self.need);
|
||||
encode_many(&mut buf, self.have.iter(), |buf, h| {
|
||||
encode_hashes(buf, &h.last_sync);
|
||||
leb128::write::unsigned(buf, h.bloom.to_bytes().len() as u64).unwrap();
|
||||
buf.extend(h.bloom.to_bytes());
|
||||
});
|
||||
|
||||
encode_many(&mut buf, self.changes.iter_mut(), |buf, change| {
|
||||
leb128::write::unsigned(buf, change.raw_bytes().len() as u64).unwrap();
|
||||
buf.extend(change.raw_bytes().as_ref())
|
||||
});
|
||||
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_many<'a, I, It, F>(out: &mut Vec<u8>, data: I, f: F)
|
||||
where
|
||||
I: Iterator<Item = It> + ExactSizeIterator + 'a,
|
||||
F: Fn(&mut Vec<u8>, It),
|
||||
{
|
||||
leb128::write::unsigned(out, data.len() as u64).unwrap();
|
||||
for datum in data {
|
||||
f(out, datum)
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_hashes(buf: &mut Vec<u8>, hashes: &[ChangeHash]) {
|
||||
debug_assert!(
|
||||
hashes.windows(2).all(|h| h[0] <= h[1]),
|
||||
"hashes were not sorted"
|
||||
);
|
||||
encode_many(buf, hashes.iter(), |buf, hash| buf.extend(hash.as_bytes()))
|
||||
}
|
||||
|
||||
fn advance_heads(
|
||||
my_old_heads: &HashSet<&ChangeHash>,
|
||||
my_new_heads: &HashSet<ChangeHash>,
|
||||
our_old_shared_heads: &[ChangeHash],
|
||||
) -> Vec<ChangeHash> {
|
||||
let new_heads = my_new_heads
|
||||
.iter()
|
||||
.filter(|head| !my_old_heads.contains(head))
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let common_heads = our_old_shared_heads
|
||||
.iter()
|
||||
.filter(|head| my_new_heads.contains(head))
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut advanced_heads = HashSet::with_capacity(new_heads.len() + common_heads.len());
|
||||
for head in new_heads.into_iter().chain(common_heads) {
|
||||
advanced_heads.insert(head);
|
||||
}
|
||||
let mut advanced_heads = advanced_heads.into_iter().collect::<Vec<_>>();
|
||||
advanced_heads.sort();
|
||||
advanced_heads
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::change::gen::gen_change;
|
||||
use crate::storage::parse::Input;
|
||||
use crate::types::gen::gen_hash;
|
||||
use proptest::prelude::*;
|
||||
|
||||
prop_compose! {
|
||||
fn gen_bloom()(hashes in gen_sorted_hashes(0..10)) -> BloomFilter {
|
||||
BloomFilter::from_hashes(hashes.into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
prop_compose! {
|
||||
fn gen_have()(bloom in gen_bloom(), last_sync in gen_sorted_hashes(0..10)) -> Have {
|
||||
Have {
|
||||
bloom,
|
||||
last_sync,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_sorted_hashes(size: std::ops::Range<usize>) -> impl Strategy<Value = Vec<ChangeHash>> {
|
||||
proptest::collection::vec(gen_hash(), size).prop_map(|mut h| {
|
||||
h.sort();
|
||||
h
|
||||
})
|
||||
}
|
||||
|
||||
prop_compose! {
|
||||
fn gen_sync_message()(
|
||||
heads in gen_sorted_hashes(0..10),
|
||||
need in gen_sorted_hashes(0..10),
|
||||
have in proptest::collection::vec(gen_have(), 0..10),
|
||||
changes in proptest::collection::vec(gen_change(), 0..10),
|
||||
) -> Message {
|
||||
Message {
|
||||
heads,
|
||||
need,
|
||||
have,
|
||||
changes,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_decode_empty_message() {
|
||||
let msg = Message {
|
||||
heads: vec![],
|
||||
need: vec![],
|
||||
have: vec![],
|
||||
changes: vec![],
|
||||
};
|
||||
let encoded = msg.encode();
|
||||
Message::parse(Input::new(&encoded)).unwrap();
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn encode_decode_message(msg in gen_sync_message()) {
|
||||
let encoded = msg.clone().encode();
|
||||
let (i, decoded) = Message::parse(Input::new(&encoded)).unwrap();
|
||||
assert!(i.is_empty());
|
||||
assert_eq!(msg, decoded);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,462 +0,0 @@
|
|||
use std::num::NonZeroU64;
|
||||
|
||||
use crate::automerge::Actor;
|
||||
use crate::exid::ExId;
|
||||
use crate::query::{self, OpIdSearch};
|
||||
use crate::storage::Change as StoredChange;
|
||||
use crate::types::{Key, ObjId, OpId};
|
||||
use crate::{op_tree::OpSetMetadata, types::Op, Automerge, Change, ChangeHash, OpObserver, Prop};
|
||||
use crate::{AutomergeError, ObjType, OpType, ScalarValue};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct TransactionInner {
|
||||
pub(crate) actor: usize,
|
||||
pub(crate) seq: u64,
|
||||
pub(crate) start_op: NonZeroU64,
|
||||
pub(crate) time: i64,
|
||||
pub(crate) message: Option<String>,
|
||||
pub(crate) deps: Vec<ChangeHash>,
|
||||
pub(crate) operations: Vec<(ObjId, Prop, Op)>,
|
||||
}
|
||||
|
||||
impl TransactionInner {
|
||||
pub(crate) fn pending_ops(&self) -> usize {
|
||||
self.operations.len()
|
||||
}
|
||||
|
||||
/// Commit the operations performed in this transaction, returning the hashes corresponding to
|
||||
/// the new heads.
|
||||
#[tracing::instrument(skip(self, doc))]
|
||||
pub(crate) fn commit(
|
||||
mut self,
|
||||
doc: &mut Automerge,
|
||||
message: Option<String>,
|
||||
time: Option<i64>,
|
||||
) -> ChangeHash {
|
||||
if message.is_some() {
|
||||
self.message = message;
|
||||
}
|
||||
|
||||
if let Some(t) = time {
|
||||
self.time = t;
|
||||
}
|
||||
|
||||
let num_ops = self.pending_ops();
|
||||
let change = self.export(&doc.ops.m);
|
||||
let hash = change.hash();
|
||||
#[cfg(not(debug_assertions))]
|
||||
tracing::trace!(commit=?hash, deps=?change.deps(), "committing transaction");
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let ops = change.iter_ops().collect::<Vec<_>>();
|
||||
tracing::trace!(commit=?hash, ?ops, deps=?change.deps(), "committing transaction");
|
||||
}
|
||||
doc.update_history(change, num_ops);
|
||||
debug_assert_eq!(doc.get_heads(), vec![hash]);
|
||||
hash
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, metadata))]
|
||||
pub(crate) fn export(self, metadata: &OpSetMetadata) -> Change {
|
||||
use crate::storage::{change::PredOutOfOrder, convert::op_as_actor_id};
|
||||
|
||||
let actor = metadata.actors.get(self.actor).clone();
|
||||
let ops = self.operations.iter().map(|o| (&o.0, &o.2));
|
||||
//let (ops, other_actors) = encode_change_ops(ops, actor.clone(), actors, props);
|
||||
let deps = self.deps.clone();
|
||||
let stored = match StoredChange::builder()
|
||||
.with_actor(actor)
|
||||
.with_seq(self.seq)
|
||||
.with_start_op(self.start_op)
|
||||
.with_message(self.message.clone())
|
||||
.with_dependencies(deps)
|
||||
.with_timestamp(self.time)
|
||||
.build(
|
||||
ops.into_iter()
|
||||
.map(|(obj, op)| op_as_actor_id(obj, op, metadata)),
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(PredOutOfOrder) => {
|
||||
// SAFETY: types::Op::preds is `types::OpIds` which ensures ops are always sorted
|
||||
panic!("preds out of order");
|
||||
}
|
||||
};
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let realized_ops = self.operations.iter().collect::<Vec<_>>();
|
||||
tracing::trace!(?stored, ops=?realized_ops, "committing change");
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
tracing::trace!(?stored, "committing change");
|
||||
Change::new(stored)
|
||||
}
|
||||
|
||||
/// Undo the operations added in this transaction, returning the number of cancelled
|
||||
/// operations.
|
||||
pub(crate) fn rollback(self, doc: &mut Automerge) -> usize {
|
||||
let num = self.pending_ops();
|
||||
// remove in reverse order so sets are removed before makes etc...
|
||||
for (obj, _prop, op) in self.operations.into_iter().rev() {
|
||||
for pred_id in &op.pred {
|
||||
if let Some(p) = doc.ops.search(&obj, OpIdSearch::new(*pred_id)).index() {
|
||||
doc.ops.replace(&obj, p, |o| o.remove_succ(&op));
|
||||
}
|
||||
}
|
||||
if let Some(pos) = doc.ops.search(&obj, OpIdSearch::new(op.id)).index() {
|
||||
doc.ops.remove(&obj, pos);
|
||||
}
|
||||
}
|
||||
|
||||
// remove the actor from the cache so that it doesn't end up in the saved document
|
||||
if doc.states.get(&self.actor).is_none() && doc.ops.m.actors.len() > 0 {
|
||||
let actor = doc.ops.m.actors.remove_last();
|
||||
doc.actor = Actor::Unused(actor);
|
||||
}
|
||||
|
||||
num
|
||||
}
|
||||
|
||||
/// 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
|
||||
///
|
||||
/// # 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
|
||||
pub(crate) fn put<P: Into<Prop>, V: Into<ScalarValue>, Obs: OpObserver>(
|
||||
&mut self,
|
||||
doc: &mut Automerge,
|
||||
op_observer: &mut Obs,
|
||||
ex_obj: &ExId,
|
||||
prop: P,
|
||||
value: V,
|
||||
) -> Result<(), AutomergeError> {
|
||||
let obj = doc.exid_to_obj(ex_obj)?;
|
||||
let value = value.into();
|
||||
let prop = prop.into();
|
||||
self.local_op(doc, op_observer, obj, prop, value.into())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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
|
||||
///
|
||||
/// # 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
|
||||
pub(crate) fn put_object<P: Into<Prop>, Obs: OpObserver>(
|
||||
&mut self,
|
||||
doc: &mut Automerge,
|
||||
op_observer: &mut Obs,
|
||||
ex_obj: &ExId,
|
||||
prop: P,
|
||||
value: ObjType,
|
||||
) -> Result<ExId, AutomergeError> {
|
||||
let obj = doc.exid_to_obj(ex_obj)?;
|
||||
let prop = prop.into();
|
||||
let id = self
|
||||
.local_op(doc, op_observer, obj, prop, value.into())?
|
||||
.unwrap();
|
||||
let id = doc.id_to_exid(id);
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
fn next_id(&mut self) -> OpId {
|
||||
OpId(self.start_op.get() + self.pending_ops() as u64, self.actor)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn insert_local_op<Obs: OpObserver>(
|
||||
&mut self,
|
||||
doc: &mut Automerge,
|
||||
op_observer: &mut Obs,
|
||||
prop: Prop,
|
||||
op: Op,
|
||||
pos: usize,
|
||||
obj: ObjId,
|
||||
succ_pos: &[usize],
|
||||
) {
|
||||
doc.ops.add_succ(&obj, succ_pos.iter().copied(), &op);
|
||||
|
||||
if !op.is_delete() {
|
||||
doc.ops.insert(pos, &obj, op.clone());
|
||||
}
|
||||
|
||||
self.finalize_op(doc, op_observer, obj, prop, op);
|
||||
}
|
||||
|
||||
pub(crate) fn insert<V: Into<ScalarValue>, Obs: OpObserver>(
|
||||
&mut self,
|
||||
doc: &mut Automerge,
|
||||
op_observer: &mut Obs,
|
||||
ex_obj: &ExId,
|
||||
index: usize,
|
||||
value: V,
|
||||
) -> Result<(), AutomergeError> {
|
||||
let obj = doc.exid_to_obj(ex_obj)?;
|
||||
let value = value.into();
|
||||
tracing::trace!(obj=?obj, value=?value, "inserting value");
|
||||
self.do_insert(doc, op_observer, obj, index, value.into())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn insert_object<Obs: OpObserver>(
|
||||
&mut self,
|
||||
doc: &mut Automerge,
|
||||
op_observer: &mut Obs,
|
||||
ex_obj: &ExId,
|
||||
index: usize,
|
||||
value: ObjType,
|
||||
) -> Result<ExId, AutomergeError> {
|
||||
let obj = doc.exid_to_obj(ex_obj)?;
|
||||
let id = self.do_insert(doc, op_observer, obj, index, value.into())?;
|
||||
let id = doc.id_to_exid(id);
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
fn do_insert<Obs: OpObserver>(
|
||||
&mut self,
|
||||
doc: &mut Automerge,
|
||||
op_observer: &mut Obs,
|
||||
obj: ObjId,
|
||||
index: usize,
|
||||
action: OpType,
|
||||
) -> Result<OpId, AutomergeError> {
|
||||
let id = self.next_id();
|
||||
|
||||
let query = doc.ops.search(&obj, query::InsertNth::new(index));
|
||||
|
||||
let key = query.key()?;
|
||||
|
||||
let op = Op {
|
||||
id,
|
||||
action,
|
||||
key,
|
||||
succ: Default::default(),
|
||||
pred: Default::default(),
|
||||
insert: true,
|
||||
};
|
||||
|
||||
doc.ops.insert(query.pos(), &obj, op.clone());
|
||||
|
||||
self.finalize_op(doc, op_observer, obj, Prop::Seq(index), op);
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub(crate) fn local_op<Obs: OpObserver>(
|
||||
&mut self,
|
||||
doc: &mut Automerge,
|
||||
op_observer: &mut Obs,
|
||||
obj: ObjId,
|
||||
prop: Prop,
|
||||
action: OpType,
|
||||
) -> Result<Option<OpId>, AutomergeError> {
|
||||
match prop {
|
||||
Prop::Map(s) => self.local_map_op(doc, op_observer, obj, s, action),
|
||||
Prop::Seq(n) => self.local_list_op(doc, op_observer, obj, n, action),
|
||||
}
|
||||
}
|
||||
|
||||
fn local_map_op<Obs: OpObserver>(
|
||||
&mut self,
|
||||
doc: &mut Automerge,
|
||||
op_observer: &mut Obs,
|
||||
obj: ObjId,
|
||||
prop: String,
|
||||
action: OpType,
|
||||
) -> Result<Option<OpId>, AutomergeError> {
|
||||
if prop.is_empty() {
|
||||
return Err(AutomergeError::EmptyStringKey);
|
||||
}
|
||||
|
||||
let id = self.next_id();
|
||||
let prop_index = doc.ops.m.props.cache(prop.clone());
|
||||
let query = doc.ops.search(&obj, query::Prop::new(prop_index));
|
||||
|
||||
// no key present to delete
|
||||
if query.ops.is_empty() && action == OpType::Delete {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if query.ops.len() == 1 && query.ops[0].is_noop(&action) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// increment operations are only valid against counter values.
|
||||
// if there are multiple values (from conflicts) then we just need one of them to be a counter.
|
||||
if matches!(action, OpType::Increment(_)) && query.ops.iter().all(|op| !op.is_counter()) {
|
||||
return Err(AutomergeError::MissingCounter);
|
||||
}
|
||||
|
||||
let pred = doc.ops.m.sorted_opids(query.ops.iter().map(|o| o.id));
|
||||
|
||||
let op = Op {
|
||||
id,
|
||||
action,
|
||||
key: Key::Map(prop_index),
|
||||
succ: Default::default(),
|
||||
pred,
|
||||
insert: false,
|
||||
};
|
||||
|
||||
let pos = query.pos;
|
||||
let ops_pos = query.ops_pos;
|
||||
self.insert_local_op(doc, op_observer, Prop::Map(prop), op, pos, obj, &ops_pos);
|
||||
|
||||
Ok(Some(id))
|
||||
}
|
||||
|
||||
fn local_list_op<Obs: OpObserver>(
|
||||
&mut self,
|
||||
doc: &mut Automerge,
|
||||
op_observer: &mut Obs,
|
||||
obj: ObjId,
|
||||
index: usize,
|
||||
action: OpType,
|
||||
) -> Result<Option<OpId>, AutomergeError> {
|
||||
let query = doc.ops.search(&obj, query::Nth::new(index));
|
||||
|
||||
let id = self.next_id();
|
||||
let pred = doc.ops.m.sorted_opids(query.ops.iter().map(|o| o.id));
|
||||
let key = query.key()?;
|
||||
|
||||
if query.ops.len() == 1 && query.ops[0].is_noop(&action) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// increment operations are only valid against counter values.
|
||||
// if there are multiple values (from conflicts) then we just need one of them to be a counter.
|
||||
if matches!(action, OpType::Increment(_)) && query.ops.iter().all(|op| !op.is_counter()) {
|
||||
return Err(AutomergeError::MissingCounter);
|
||||
}
|
||||
|
||||
let op = Op {
|
||||
id,
|
||||
action,
|
||||
key,
|
||||
succ: Default::default(),
|
||||
pred,
|
||||
insert: false,
|
||||
};
|
||||
|
||||
let pos = query.pos;
|
||||
let ops_pos = query.ops_pos;
|
||||
self.insert_local_op(doc, op_observer, Prop::Seq(index), op, pos, obj, &ops_pos);
|
||||
|
||||
Ok(Some(id))
|
||||
}
|
||||
|
||||
pub(crate) fn increment<P: Into<Prop>, Obs: OpObserver>(
|
||||
&mut self,
|
||||
doc: &mut Automerge,
|
||||
op_observer: &mut Obs,
|
||||
obj: &ExId,
|
||||
prop: P,
|
||||
value: i64,
|
||||
) -> Result<(), AutomergeError> {
|
||||
let obj = doc.exid_to_obj(obj)?;
|
||||
self.local_op(doc, op_observer, obj, prop.into(), OpType::Increment(value))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn delete<P: Into<Prop>, Obs: OpObserver>(
|
||||
&mut self,
|
||||
doc: &mut Automerge,
|
||||
op_observer: &mut Obs,
|
||||
ex_obj: &ExId,
|
||||
prop: P,
|
||||
) -> Result<(), AutomergeError> {
|
||||
let obj = doc.exid_to_obj(ex_obj)?;
|
||||
let prop = prop.into();
|
||||
self.local_op(doc, op_observer, obj, prop, OpType::Delete)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Splice new elements into the given sequence. Returns a vector of the OpIds used to insert
|
||||
/// the new elements
|
||||
pub(crate) fn splice<Obs: OpObserver>(
|
||||
&mut self,
|
||||
doc: &mut Automerge,
|
||||
op_observer: &mut Obs,
|
||||
ex_obj: &ExId,
|
||||
mut pos: usize,
|
||||
del: usize,
|
||||
vals: impl IntoIterator<Item = ScalarValue>,
|
||||
) -> Result<(), AutomergeError> {
|
||||
let obj = doc.exid_to_obj(ex_obj)?;
|
||||
for _ in 0..del {
|
||||
// del()
|
||||
self.local_op(doc, op_observer, obj, pos.into(), OpType::Delete)?;
|
||||
}
|
||||
for v in vals {
|
||||
// insert()
|
||||
self.do_insert(doc, op_observer, obj, pos, v.clone().into())?;
|
||||
pos += 1;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn finalize_op<Obs: OpObserver>(
|
||||
&mut self,
|
||||
doc: &mut Automerge,
|
||||
op_observer: &mut Obs,
|
||||
obj: ObjId,
|
||||
prop: Prop,
|
||||
op: Op,
|
||||
) {
|
||||
// TODO - id_to_exid should be a noop if not used - change type to Into<ExId>?
|
||||
let ex_obj = doc.ops.id_to_exid(obj.0);
|
||||
let parents = doc.ops.parents(obj);
|
||||
if op.insert {
|
||||
let value = (op.value(), doc.ops.id_to_exid(op.id));
|
||||
match prop {
|
||||
Prop::Map(_) => panic!("insert into a map"),
|
||||
Prop::Seq(index) => op_observer.insert(parents, ex_obj, index, value),
|
||||
}
|
||||
} else if op.is_delete() {
|
||||
op_observer.delete(parents, ex_obj, prop.clone());
|
||||
} else if let Some(value) = op.get_increment_value() {
|
||||
op_observer.increment(
|
||||
parents,
|
||||
ex_obj,
|
||||
prop.clone(),
|
||||
(value, doc.ops.id_to_exid(op.id)),
|
||||
);
|
||||
} else {
|
||||
let value = (op.value(), doc.ops.id_to_exid(op.id));
|
||||
op_observer.put(parents, ex_obj, prop.clone(), value, false);
|
||||
}
|
||||
self.operations.push((obj, prop, op));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{transaction::Transactable, ROOT};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn map_rollback_doesnt_panic() {
|
||||
let mut doc = Automerge::new();
|
||||
let mut tx = doc.transaction();
|
||||
|
||||
let a = tx.put_object(ROOT, "a", ObjType::Map).unwrap();
|
||||
tx.put(&a, "b", 1).unwrap();
|
||||
assert!(tx.get(&a, "b").unwrap().is_some());
|
||||
}
|
||||
}
|
|
@ -1,200 +0,0 @@
|
|||
use std::ops::RangeBounds;
|
||||
|
||||
use crate::exid::ExId;
|
||||
use crate::{
|
||||
AutomergeError, ChangeHash, Keys, KeysAt, ListRange, ListRangeAt, MapRange, MapRangeAt,
|
||||
ObjType, Parents, Prop, ScalarValue, Value, Values,
|
||||
};
|
||||
|
||||
/// A way of mutating a document within a single change.
|
||||
pub trait Transactable {
|
||||
/// Get the number of pending operations in this transaction.
|
||||
fn pending_ops(&self) -> usize;
|
||||
|
||||
/// Set the value of property `P` to value `V` in object `obj`.
|
||||
///
|
||||
/// # 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 put<O: AsRef<ExId>, P: Into<Prop>, V: Into<ScalarValue>>(
|
||||
&mut self,
|
||||
obj: O,
|
||||
prop: P,
|
||||
value: V,
|
||||
) -> Result<(), AutomergeError>;
|
||||
|
||||
/// Set the value of property `P` to the new object `V` in object `obj`.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The id of the object which was created.
|
||||
///
|
||||
/// # 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 put_object<O: AsRef<ExId>, P: Into<Prop>>(
|
||||
&mut self,
|
||||
obj: O,
|
||||
prop: P,
|
||||
object: ObjType,
|
||||
) -> Result<ExId, AutomergeError>;
|
||||
|
||||
/// Insert a value into a list at the given index.
|
||||
fn insert<O: AsRef<ExId>, V: Into<ScalarValue>>(
|
||||
&mut self,
|
||||
obj: O,
|
||||
index: usize,
|
||||
value: V,
|
||||
) -> Result<(), AutomergeError>;
|
||||
|
||||
/// Insert an object into a list at the given index.
|
||||
fn insert_object<O: AsRef<ExId>>(
|
||||
&mut self,
|
||||
obj: O,
|
||||
index: usize,
|
||||
object: ObjType,
|
||||
) -> Result<ExId, AutomergeError>;
|
||||
|
||||
/// Increment the counter at the prop in the object by `value`.
|
||||
fn increment<O: AsRef<ExId>, P: Into<Prop>>(
|
||||
&mut self,
|
||||
obj: O,
|
||||
prop: P,
|
||||
value: i64,
|
||||
) -> Result<(), AutomergeError>;
|
||||
|
||||
/// Delete the value at prop in the object.
|
||||
fn delete<O: AsRef<ExId>, P: Into<Prop>>(
|
||||
&mut self,
|
||||
obj: O,
|
||||
prop: P,
|
||||
) -> Result<(), AutomergeError>;
|
||||
|
||||
fn splice<O: AsRef<ExId>, V: IntoIterator<Item = ScalarValue>>(
|
||||
&mut self,
|
||||
obj: O,
|
||||
pos: usize,
|
||||
del: usize,
|
||||
vals: V,
|
||||
) -> Result<(), AutomergeError>;
|
||||
|
||||
/// Like [`Self::splice`] but for text.
|
||||
fn splice_text<O: AsRef<ExId>>(
|
||||
&mut self,
|
||||
obj: O,
|
||||
pos: usize,
|
||||
del: usize,
|
||||
text: &str,
|
||||
) -> Result<(), AutomergeError> {
|
||||
let vals = text.chars().map(|c| c.into());
|
||||
self.splice(obj, pos, del, vals)
|
||||
}
|
||||
|
||||
/// Get the keys of the given object, it should be a map.
|
||||
fn keys<O: AsRef<ExId>>(&self, obj: O) -> Keys<'_, '_>;
|
||||
|
||||
/// Get the keys of the given object at a point in history.
|
||||
fn keys_at<O: AsRef<ExId>>(&self, obj: O, heads: &[ChangeHash]) -> KeysAt<'_, '_>;
|
||||
|
||||
fn map_range<O: AsRef<ExId>, R: RangeBounds<String>>(
|
||||
&self,
|
||||
obj: O,
|
||||
range: R,
|
||||
) -> MapRange<'_, R>;
|
||||
|
||||
fn map_range_at<O: AsRef<ExId>, R: RangeBounds<String>>(
|
||||
&self,
|
||||
obj: O,
|
||||
range: R,
|
||||
heads: &[ChangeHash],
|
||||
) -> MapRangeAt<'_, R>;
|
||||
|
||||
fn list_range<O: AsRef<ExId>, R: RangeBounds<usize>>(
|
||||
&self,
|
||||
obj: O,
|
||||
range: R,
|
||||
) -> ListRange<'_, R>;
|
||||
|
||||
fn list_range_at<O: AsRef<ExId>, R: RangeBounds<usize>>(
|
||||
&self,
|
||||
obj: O,
|
||||
range: R,
|
||||
heads: &[ChangeHash],
|
||||
) -> ListRangeAt<'_, R>;
|
||||
|
||||
fn values<O: AsRef<ExId>>(&self, obj: O) -> Values<'_>;
|
||||
|
||||
fn values_at<O: AsRef<ExId>>(&self, obj: O, heads: &[ChangeHash]) -> Values<'_>;
|
||||
|
||||
/// Get the length of the given object.
|
||||
fn length<O: AsRef<ExId>>(&self, obj: O) -> usize;
|
||||
|
||||
/// Get the length of the given object at a point in history.
|
||||
fn length_at<O: AsRef<ExId>>(&self, obj: O, heads: &[ChangeHash]) -> usize;
|
||||
|
||||
/// Get type for object
|
||||
fn object_type<O: AsRef<ExId>>(&self, obj: O) -> Option<ObjType>;
|
||||
|
||||
/// Get the string that this text object represents.
|
||||
fn text<O: AsRef<ExId>>(&self, obj: O) -> Result<String, AutomergeError>;
|
||||
|
||||
/// Get the string that this text object represents at a point in history.
|
||||
fn text_at<O: AsRef<ExId>>(
|
||||
&self,
|
||||
obj: O,
|
||||
heads: &[ChangeHash],
|
||||
) -> Result<String, AutomergeError>;
|
||||
|
||||
/// Get the value at this prop in the object.
|
||||
fn get<O: AsRef<ExId>, P: Into<Prop>>(
|
||||
&self,
|
||||
obj: O,
|
||||
prop: P,
|
||||
) -> Result<Option<(Value<'_>, ExId)>, AutomergeError>;
|
||||
|
||||
/// Get the value at this prop in the object at a point in history.
|
||||
fn get_at<O: AsRef<ExId>, P: Into<Prop>>(
|
||||
&self,
|
||||
obj: O,
|
||||
prop: P,
|
||||
heads: &[ChangeHash],
|
||||
) -> Result<Option<(Value<'_>, ExId)>, AutomergeError>;
|
||||
|
||||
fn get_all<O: AsRef<ExId>, P: Into<Prop>>(
|
||||
&self,
|
||||
obj: O,
|
||||
prop: P,
|
||||
) -> Result<Vec<(Value<'_>, ExId)>, AutomergeError>;
|
||||
|
||||
fn get_all_at<O: AsRef<ExId>, P: Into<Prop>>(
|
||||
&self,
|
||||
obj: O,
|
||||
prop: P,
|
||||
heads: &[ChangeHash],
|
||||
) -> Result<Vec<(Value<'_>, ExId)>, AutomergeError>;
|
||||
|
||||
/// Get the parents of an object in the document tree.
|
||||
///
|
||||
/// ### Errors
|
||||
///
|
||||
/// Returns an error when the id given is not the id of an object in this document.
|
||||
/// This function does not get the parents of scalar values contained within objects.
|
||||
///
|
||||
/// ### Experimental
|
||||
///
|
||||
/// This function may in future be changed to allow getting the parents from the id of a scalar
|
||||
/// value.
|
||||
fn parents<O: AsRef<ExId>>(&self, obj: O) -> Result<Parents<'_>, AutomergeError>;
|
||||
|
||||
fn path_to_object<O: AsRef<ExId>>(&self, obj: O) -> Result<Vec<(ExId, Prop)>, AutomergeError> {
|
||||
let mut path = self.parents(obj.as_ref().clone())?.collect::<Vec<_>>();
|
||||
path.reverse();
|
||||
Ok(path)
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
|
||||
// this assumes that the automerge-rs folder is checked out along side this repo
|
||||
// and someone has run
|
||||
|
||||
// # cd automerge-rs/automerge-backend-wasm
|
||||
// # yarn release
|
||||
|
||||
const { edits, finalText } = require('./editing-trace')
|
||||
const Automerge = require('../../automerge')
|
||||
const path = require('path')
|
||||
const wasmBackend = require(path.resolve("../../automerge-rs/automerge-backend-wasm"))
|
||||
Automerge.setDefaultBackend(wasmBackend)
|
||||
|
||||
const start = new Date()
|
||||
let state = Automerge.from({text: new Automerge.Text()})
|
||||
|
||||
state = Automerge.change(state, doc => {
|
||||
for (let i = 0; i < edits.length; i++) {
|
||||
if (i % 10000 === 0) {
|
||||
console.log(`Processed ${i} edits in ${new Date() - start} ms`)
|
||||
}
|
||||
if (edits[i][1] > 0) doc.text.deleteAt(edits[i][0], edits[i][1])
|
||||
if (edits[i].length > 2) doc.text.insertAt(edits[i][0], ...edits[i].slice(2))
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`Done in ${new Date() - start} ms`)
|
||||
|
||||
if (state.text.join('') !== finalText) {
|
||||
throw new RangeError('ERROR: final text did not match expectation')
|
||||
}
|
30
flake.lock
30
flake.lock
|
@ -2,11 +2,11 @@
|
|||
"nodes": {
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1642700792,
|
||||
"narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=",
|
||||
"lastModified": 1667395993,
|
||||
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "846b2ae0fc4cc943637d3d1def4454213e203cba",
|
||||
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -17,11 +17,11 @@
|
|||
},
|
||||
"flake-utils_2": {
|
||||
"locked": {
|
||||
"lastModified": 1637014545,
|
||||
"narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=",
|
||||
"lastModified": 1659877975,
|
||||
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4",
|
||||
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -32,11 +32,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1643805626,
|
||||
"narHash": "sha256-AXLDVMG+UaAGsGSpOtQHPIKB+IZ0KSd9WS77aanGzgc=",
|
||||
"lastModified": 1669542132,
|
||||
"narHash": "sha256-DRlg++NJAwPh8io3ExBJdNW7Djs3plVI5jgYQ+iXAZQ=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "554d2d8aa25b6e583575459c297ec23750adb6cb",
|
||||
"rev": "a115bb9bd56831941be3776c8a94005867f316a7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -48,11 +48,11 @@
|
|||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1637453606,
|
||||
"narHash": "sha256-Gy6cwUswft9xqsjWxFYEnx/63/qzaFUwatcbV5GF/GQ=",
|
||||
"lastModified": 1665296151,
|
||||
"narHash": "sha256-uOB0oxqxN9K7XGF1hcnY+PQnlQJ+3bP2vCn/+Ru/bbc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "8afc4e543663ca0a6a4f496262cd05233737e732",
|
||||
"rev": "14ccaaedd95a488dd7ae142757884d8e125b3363",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -75,11 +75,11 @@
|
|||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1643941258,
|
||||
"narHash": "sha256-uHyEuICSu8qQp6adPTqV33ajiwoF0sCh+Iazaz5r7fo=",
|
||||
"lastModified": 1669775522,
|
||||
"narHash": "sha256-6xxGArBqssX38DdHpDoPcPvB/e79uXyQBwpBcaO/BwY=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "674156c4c2f46dd6a6846466cb8f9fee84c211ca",
|
||||
"rev": "3158e47f6b85a288d12948aeb9a048e0ed4434d6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
44
flake.nix
44
flake.nix
|
@ -3,36 +3,39 @@
|
|||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
flake-utils = {
|
||||
url = "github:numtide/flake-utils";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, rust-overlay }:
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
rust-overlay,
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem
|
||||
(system:
|
||||
let
|
||||
(system: let
|
||||
pkgs = import nixpkgs {
|
||||
overlays = [ rust-overlay.overlay ];
|
||||
overlays = [rust-overlay.overlays.default];
|
||||
inherit system;
|
||||
};
|
||||
lib = pkgs.lib;
|
||||
rust = pkgs.rust-bin.stable.latest.default;
|
||||
cargoNix = pkgs.callPackage ./Cargo.nix {
|
||||
inherit pkgs;
|
||||
release = true;
|
||||
in {
|
||||
formatter = pkgs.alejandra;
|
||||
|
||||
packages = {
|
||||
deadnix = pkgs.runCommand "deadnix" {} ''
|
||||
${pkgs.deadnix}/bin/deadnix --fail ${./.}
|
||||
mkdir $out
|
||||
'';
|
||||
};
|
||||
debugCargoNix = pkgs.callPackage ./Cargo.nix {
|
||||
inherit pkgs;
|
||||
release = false;
|
||||
|
||||
checks = {
|
||||
inherit (self.packages.${system}) deadnix;
|
||||
};
|
||||
in
|
||||
{
|
||||
devShell = pkgs.mkShell {
|
||||
buildInputs = with pkgs;
|
||||
[
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
(rust.override {
|
||||
extensions = ["rust-src"];
|
||||
targets = ["wasm32-unknown-unknown"];
|
||||
|
@ -51,6 +54,7 @@
|
|||
|
||||
nodejs
|
||||
yarn
|
||||
deno
|
||||
|
||||
# c deps
|
||||
cmake
|
||||
|
|
3
javascript/.denoifyrc.json
Normal file
3
javascript/.denoifyrc.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"replacer": "scripts/denoify-replacer.mjs"
|
||||
}
|
15
javascript/.eslintrc.cjs
Normal file
15
javascript/.eslintrc.cjs
Normal file
|
@ -0,0 +1,15 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint"],
|
||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
/node_modules
|
||||
/yarn.lock
|
||||
dist
|
||||
docs/
|
||||
.vim
|
||||
deno_dist/
|
4
javascript/.prettierignore
Normal file
4
javascript/.prettierignore
Normal file
|
@ -0,0 +1,4 @@
|
|||
e2e/verdacciodb
|
||||
dist
|
||||
docs
|
||||
deno_dist
|
4
javascript/.prettierrc
Normal file
4
javascript/.prettierrc
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"semi": false,
|
||||
"arrowParens": "avoid"
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue