Compare commits
No commits in common. "main" and "tmp_pr" have entirely different histories.
317 changed files with 17612 additions and 26919 deletions
48
.github/workflows/ci.yaml
vendored
48
.github/workflows/ci.yaml
vendored
|
@ -2,10 +2,10 @@ name: CI
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- main
|
||||
jobs:
|
||||
fmt:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -14,7 +14,7 @@ jobs:
|
|||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.67.0
|
||||
toolchain: 1.60.0
|
||||
default: true
|
||||
components: rustfmt
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
|
@ -28,7 +28,7 @@ jobs:
|
|||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.67.0
|
||||
toolchain: 1.60.0
|
||||
default: true
|
||||
components: clippy
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
|
@ -42,7 +42,7 @@ jobs:
|
|||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.67.0
|
||||
toolchain: 1.60.0
|
||||
default: true
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- name: Build rust docs
|
||||
|
@ -51,6 +51,9 @@ jobs:
|
|||
- name: Install doxygen
|
||||
run: sudo apt-get install -y doxygen
|
||||
shell: bash
|
||||
- name: Build C docs
|
||||
run: ./scripts/ci/cmake-docs
|
||||
shell: bash
|
||||
|
||||
cargo-deny:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -64,7 +67,6 @@ jobs:
|
|||
- uses: actions/checkout@v2
|
||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||
with:
|
||||
arguments: '--manifest-path ./rust/Cargo.toml'
|
||||
command: check ${{ matrix.checks }}
|
||||
|
||||
wasm_tests:
|
||||
|
@ -77,28 +79,6 @@ jobs:
|
|||
run: rustup target add wasm32-unknown-unknown
|
||||
- name: run tests
|
||||
run: ./scripts/ci/wasm_tests
|
||||
deno_tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: v1.x
|
||||
- name: Install wasm-bindgen-cli
|
||||
run: cargo install wasm-bindgen-cli wasm-opt
|
||||
- name: Install wasm32 target
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- name: run tests
|
||||
run: ./scripts/ci/deno_tests
|
||||
|
||||
js_fmt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: install
|
||||
run: yarn global add prettier
|
||||
- name: format
|
||||
run: prettier -c javascript/.prettierrc javascript
|
||||
|
||||
js_tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -118,7 +98,7 @@ jobs:
|
|||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly-2023-01-26
|
||||
toolchain: 1.60.0
|
||||
default: true
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- name: Install CMocka
|
||||
|
@ -127,8 +107,6 @@ jobs:
|
|||
uses: jwlawson/actions-setup-cmake@v1.12
|
||||
with:
|
||||
cmake-version: latest
|
||||
- name: Install rust-src
|
||||
run: rustup component add rust-src
|
||||
- name: Build and test C bindings
|
||||
run: ./scripts/ci/cmake-build Release Static
|
||||
shell: bash
|
||||
|
@ -138,7 +116,9 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
toolchain:
|
||||
- 1.67.0
|
||||
- 1.60.0
|
||||
- nightly
|
||||
continue-on-error: ${{ matrix.toolchain == 'nightly' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
|
@ -157,7 +137,7 @@ jobs:
|
|||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.67.0
|
||||
toolchain: 1.60.0
|
||||
default: true
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- run: ./scripts/ci/build-test
|
||||
|
@ -170,7 +150,7 @@ jobs:
|
|||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.67.0
|
||||
toolchain: 1.60.0
|
||||
default: true
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- run: ./scripts/ci/build-test
|
||||
|
|
18
.github/workflows/docs.yaml
vendored
18
.github/workflows/docs.yaml
vendored
|
@ -30,16 +30,28 @@ jobs:
|
|||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clean
|
||||
args: --manifest-path ./rust/Cargo.toml --doc
|
||||
args: --doc
|
||||
|
||||
- name: Build Rust docs
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: doc
|
||||
args: --manifest-path ./rust/Cargo.toml --workspace --all-features --no-deps
|
||||
args: --workspace --all-features --no-deps
|
||||
|
||||
- name: Move Rust docs
|
||||
run: mkdir -p docs && mv rust/target/doc/* docs/.
|
||||
run: mkdir -p docs && mv target/doc/* docs/.
|
||||
shell: bash
|
||||
|
||||
- name: Install doxygen
|
||||
run: sudo apt-get install -y doxygen
|
||||
shell: bash
|
||||
|
||||
- name: Build C docs
|
||||
run: ./scripts/ci/cmake-docs
|
||||
shell: bash
|
||||
|
||||
- name: Move C docs
|
||||
run: mkdir -p docs/automerge-c && mv automerge-c/build/src/html/* docs/automerge-c/.
|
||||
shell: bash
|
||||
|
||||
- name: Configure root page
|
||||
|
|
214
.github/workflows/release.yaml
vendored
214
.github/workflows/release.yaml
vendored
|
@ -1,214 +0,0 @@
|
|||
name: Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
check_if_wasm_version_upgraded:
|
||||
name: Check if WASM version has been upgraded
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
wasm_version: ${{ steps.version-updated.outputs.current-package-version }}
|
||||
wasm_has_updated: ${{ steps.version-updated.outputs.has-updated }}
|
||||
steps:
|
||||
- uses: JiPaix/package-json-updated-action@v1.0.5
|
||||
id: version-updated
|
||||
with:
|
||||
path: rust/automerge-wasm/package.json
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish-wasm:
|
||||
name: Publish WASM package
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check_if_wasm_version_upgraded
|
||||
# We create release only if the version in the package.json has been upgraded
|
||||
if: needs.check_if_wasm_version_upgraded.outputs.wasm_has_updated == 'true'
|
||||
steps:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- uses: denoland/setup-deno@v1
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.ref }}
|
||||
- name: Get rid of local github workflows
|
||||
run: rm -r .github/workflows
|
||||
- name: Remove tmp_branch if it exists
|
||||
run: git push origin :tmp_branch || true
|
||||
- run: git checkout -b tmp_branch
|
||||
- name: Install wasm-bindgen-cli
|
||||
run: cargo install wasm-bindgen-cli wasm-opt
|
||||
- name: Install wasm32 target
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- name: run wasm js tests
|
||||
id: wasm_js_tests
|
||||
run: ./scripts/ci/wasm_tests
|
||||
- name: run wasm deno tests
|
||||
id: wasm_deno_tests
|
||||
run: ./scripts/ci/deno_tests
|
||||
- name: build release
|
||||
id: build_release
|
||||
run: |
|
||||
npm --prefix $GITHUB_WORKSPACE/rust/automerge-wasm run release
|
||||
- name: Collate deno release files
|
||||
if: steps.wasm_js_tests.outcome == 'success' && steps.wasm_deno_tests.outcome == 'success'
|
||||
run: |
|
||||
mkdir $GITHUB_WORKSPACE/deno_wasm_dist
|
||||
cp $GITHUB_WORKSPACE/rust/automerge-wasm/deno/* $GITHUB_WORKSPACE/deno_wasm_dist
|
||||
cp $GITHUB_WORKSPACE/rust/automerge-wasm/index.d.ts $GITHUB_WORKSPACE/deno_wasm_dist
|
||||
cp $GITHUB_WORKSPACE/rust/automerge-wasm/README.md $GITHUB_WORKSPACE/deno_wasm_dist
|
||||
cp $GITHUB_WORKSPACE/rust/automerge-wasm/LICENSE $GITHUB_WORKSPACE/deno_wasm_dist
|
||||
sed -i '1i /// <reference types="./index.d.ts" />' $GITHUB_WORKSPACE/deno_wasm_dist/automerge_wasm.js
|
||||
- name: Create npm release
|
||||
if: steps.wasm_js_tests.outcome == 'success' && steps.wasm_deno_tests.outcome == 'success'
|
||||
run: |
|
||||
if [ "$(npm --prefix $GITHUB_WORKSPACE/rust/automerge-wasm show . version)" = "$VERSION" ]; then
|
||||
echo "This version is already published"
|
||||
exit 0
|
||||
fi
|
||||
EXTRA_ARGS="--access public"
|
||||
if [[ $VERSION == *"alpha."* ]] || [[ $VERSION == *"beta."* ]] || [[ $VERSION == *"rc."* ]]; then
|
||||
echo "Is pre-release version"
|
||||
EXTRA_ARGS="$EXTRA_ARGS --tag next"
|
||||
fi
|
||||
if [ "$NODE_AUTH_TOKEN" = "" ]; then
|
||||
echo "Can't publish on NPM, You need a NPM_TOKEN secret."
|
||||
false
|
||||
fi
|
||||
npm publish $GITHUB_WORKSPACE/rust/automerge-wasm $EXTRA_ARGS
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
VERSION: ${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
|
||||
- name: Commit wasm deno release files
|
||||
run: |
|
||||
git config --global user.name "actions"
|
||||
git config --global user.email actions@github.com
|
||||
git add $GITHUB_WORKSPACE/deno_wasm_dist
|
||||
git commit -am "Add deno release files"
|
||||
git push origin tmp_branch
|
||||
- name: Tag wasm release
|
||||
if: steps.wasm_js_tests.outcome == 'success' && steps.wasm_deno_tests.outcome == 'success'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: Automerge Wasm v${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
|
||||
tag_name: js/automerge-wasm-${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
|
||||
target_commitish: tmp_branch
|
||||
generate_release_notes: false
|
||||
draft: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Remove tmp_branch
|
||||
run: git push origin :tmp_branch
|
||||
check_if_js_version_upgraded:
|
||||
name: Check if JS version has been upgraded
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
js_version: ${{ steps.version-updated.outputs.current-package-version }}
|
||||
js_has_updated: ${{ steps.version-updated.outputs.has-updated }}
|
||||
steps:
|
||||
- uses: JiPaix/package-json-updated-action@v1.0.5
|
||||
id: version-updated
|
||||
with:
|
||||
path: javascript/package.json
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish-js:
|
||||
name: Publish JS package
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check_if_js_version_upgraded
|
||||
- check_if_wasm_version_upgraded
|
||||
- publish-wasm
|
||||
# We create release only if the version in the package.json has been upgraded and after the WASM release
|
||||
if: |
|
||||
(always() && ! cancelled()) &&
|
||||
(needs.publish-wasm.result == 'success' || needs.publish-wasm.result == 'skipped') &&
|
||||
needs.check_if_js_version_upgraded.outputs.js_has_updated == 'true'
|
||||
steps:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- uses: denoland/setup-deno@v1
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.ref }}
|
||||
- name: Get rid of local github workflows
|
||||
run: rm -r .github/workflows
|
||||
- name: Remove js_tmp_branch if it exists
|
||||
run: git push origin :js_tmp_branch || true
|
||||
- run: git checkout -b js_tmp_branch
|
||||
- name: check js formatting
|
||||
run: |
|
||||
yarn global add prettier
|
||||
prettier -c javascript/.prettierrc javascript
|
||||
- name: run js tests
|
||||
id: js_tests
|
||||
run: |
|
||||
cargo install wasm-bindgen-cli wasm-opt
|
||||
rustup target add wasm32-unknown-unknown
|
||||
./scripts/ci/js_tests
|
||||
- name: build js release
|
||||
id: build_release
|
||||
run: |
|
||||
npm --prefix $GITHUB_WORKSPACE/javascript run build
|
||||
- name: build js deno release
|
||||
id: build_deno_release
|
||||
run: |
|
||||
VERSION=$WASM_VERSION npm --prefix $GITHUB_WORKSPACE/javascript run deno:build
|
||||
env:
|
||||
WASM_VERSION: ${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
|
||||
- name: run deno tests
|
||||
id: deno_tests
|
||||
run: |
|
||||
npm --prefix $GITHUB_WORKSPACE/javascript run deno:test
|
||||
- name: Collate deno release files
|
||||
if: steps.js_tests.outcome == 'success' && steps.deno_tests.outcome == 'success'
|
||||
run: |
|
||||
mkdir $GITHUB_WORKSPACE/deno_js_dist
|
||||
cp $GITHUB_WORKSPACE/javascript/deno_dist/* $GITHUB_WORKSPACE/deno_js_dist
|
||||
- name: Create npm release
|
||||
if: steps.js_tests.outcome == 'success' && steps.deno_tests.outcome == 'success'
|
||||
run: |
|
||||
if [ "$(npm --prefix $GITHUB_WORKSPACE/javascript show . version)" = "$VERSION" ]; then
|
||||
echo "This version is already published"
|
||||
exit 0
|
||||
fi
|
||||
EXTRA_ARGS="--access public"
|
||||
if [[ $VERSION == *"alpha."* ]] || [[ $VERSION == *"beta."* ]] || [[ $VERSION == *"rc."* ]]; then
|
||||
echo "Is pre-release version"
|
||||
EXTRA_ARGS="$EXTRA_ARGS --tag next"
|
||||
fi
|
||||
if [ "$NODE_AUTH_TOKEN" = "" ]; then
|
||||
echo "Can't publish on NPM, You need a NPM_TOKEN secret."
|
||||
false
|
||||
fi
|
||||
npm publish $GITHUB_WORKSPACE/javascript $EXTRA_ARGS
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
VERSION: ${{ needs.check_if_js_version_upgraded.outputs.js_version }}
|
||||
- name: Commit js deno release files
|
||||
run: |
|
||||
git config --global user.name "actions"
|
||||
git config --global user.email actions@github.com
|
||||
git add $GITHUB_WORKSPACE/deno_js_dist
|
||||
git commit -am "Add deno js release files"
|
||||
git push origin js_tmp_branch
|
||||
- name: Tag JS release
|
||||
if: steps.js_tests.outcome == 'success' && steps.deno_tests.outcome == 'success'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: Automerge v${{ needs.check_if_js_version_upgraded.outputs.js_version }}
|
||||
tag_name: js/automerge-${{ needs.check_if_js_version_upgraded.outputs.js_version }}
|
||||
target_commitish: js_tmp_branch
|
||||
generate_release_notes: false
|
||||
draft: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Remove js_tmp_branch
|
||||
run: git push origin :js_tmp_branch
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,4 +3,3 @@ perf.*
|
|||
/Cargo.lock
|
||||
build/
|
||||
.vim/*
|
||||
/target
|
||||
|
|
85
README.md
85
README.md
|
@ -25,7 +25,7 @@ If you're familiar with CRDTs and interested in the design of Automerge in
|
|||
particular take a look at https://automerge.org/docs/how-it-works/backend/
|
||||
|
||||
Finally, if you want to talk to us about this project please [join the
|
||||
Slack](https://join.slack.com/t/automerge/shared_invite/zt-e4p3760n-kKh7r3KRH1YwwNfiZM8ktw)
|
||||
Slack](https://join.slack.com/t/automerge/shared_invite/zt-1ho1ieas2-DnWZcRR82BRu65vCD4t3Xw)
|
||||
|
||||
## Status
|
||||
|
||||
|
@ -40,12 +40,11 @@ in that time.
|
|||
|
||||
In general we try and respect semver.
|
||||
|
||||
### JavaScript
|
||||
### JavaScript
|
||||
|
||||
A stable release of the javascript package is currently available as
|
||||
`@automerge/automerge@2.0.0` where. pre-release verisions of the `2.0.1` are
|
||||
available as `2.0.1-alpha.n`. `2.0.1*` packages are also available for Deno at
|
||||
https://deno.land/x/automerge
|
||||
An alpha release of the javascript package is currently available as
|
||||
`@automerge/automerge@2.0.0-alpha.n` where `n` is an integer. We are gathering
|
||||
feedback on the API and looking to release a `2.0.0` in the next few weeks.
|
||||
|
||||
### Rust
|
||||
|
||||
|
@ -53,90 +52,34 @@ The rust codebase is currently oriented around producing a performant backend
|
|||
for the Javascript wrapper and as such the API for Rust code is low level and
|
||||
not well documented. We will be returning to this over the next few months but
|
||||
for now you will need to be comfortable reading the tests and asking questions
|
||||
to figure out how to use it. If you are looking to build rust applications which
|
||||
use automerge you may want to look into
|
||||
[autosurgeon](https://github.com/alexjg/autosurgeon)
|
||||
to figure out how to use it.
|
||||
|
||||
|
||||
## Repository Organisation
|
||||
|
||||
- `./rust` - the rust rust implementation and also the Rust components of
|
||||
* `./rust` - the rust rust implementation and also the Rust components of
|
||||
platform specific wrappers (e.g. `automerge-wasm` for the WASM API or
|
||||
`automerge-c` for the C FFI bindings)
|
||||
- `./javascript` - The javascript library which uses `automerge-wasm`
|
||||
* `./javascript` - The javascript library which uses `automerge-wasm`
|
||||
internally but presents a more idiomatic javascript interface
|
||||
- `./scripts` - scripts which are useful to maintenance of the repository.
|
||||
* `./scripts` - scripts which are useful to maintenance of the repository.
|
||||
This includes the scripts which are run in CI.
|
||||
- `./img` - static assets for use in `.md` files
|
||||
* `./img` - static assets for use in `.md` files
|
||||
|
||||
## Building
|
||||
|
||||
To build this codebase you will need:
|
||||
|
||||
- `rust`
|
||||
- `wasm-bindgen-cli`
|
||||
- `wasm-opt`
|
||||
- `node`
|
||||
- `yarn`
|
||||
- `cmake`
|
||||
- `cmocka`
|
||||
|
||||
You will also need to install the following with `cargo install`
|
||||
|
||||
- `wasm-bindgen-cli`
|
||||
- `wasm-opt`
|
||||
- `cargo-deny`
|
||||
|
||||
And ensure you have added the `wasm32-unknown-unknown` target for rust cross-compilation.
|
||||
|
||||
The various subprojects (the rust code, the wrapper projects) have their own
|
||||
build instructions, but to run the tests that will be run in CI you can run
|
||||
`./scripts/ci/run`.
|
||||
|
||||
### For macOS
|
||||
|
||||
These instructions worked to build locally on macOS 13.1 (arm64) as of
|
||||
Nov 29th 2022.
|
||||
|
||||
```bash
|
||||
# clone the repo
|
||||
git clone https://github.com/automerge/automerge-rs
|
||||
cd automerge-rs
|
||||
|
||||
# install rustup
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
|
||||
# install homebrew
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
|
||||
# install cmake, node, cmocka
|
||||
brew install cmake node cmocka
|
||||
|
||||
# install yarn
|
||||
npm install --global yarn
|
||||
|
||||
# install javascript dependencies
|
||||
yarn --cwd ./javascript
|
||||
|
||||
# install rust dependencies
|
||||
cargo install wasm-bindgen-cli wasm-opt cargo-deny
|
||||
|
||||
# get nightly rust to produce optimized automerge-c builds
|
||||
rustup toolchain install nightly
|
||||
rustup component add rust-src --toolchain nightly
|
||||
|
||||
# add wasm target in addition to current architecture
|
||||
rustup target add wasm32-unknown-unknown
|
||||
|
||||
# Run ci script
|
||||
./scripts/ci/run
|
||||
```
|
||||
|
||||
If your build fails to find `cmocka.h` you may need to teach it about homebrew's
|
||||
installation location:
|
||||
|
||||
```
|
||||
export CPATH=/opt/homebrew/include
|
||||
export LIBRARY_PATH=/opt/homebrew/lib
|
||||
./scripts/ci/run
|
||||
```
|
||||
`./scripts/ci/run`.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
30
flake.lock
30
flake.lock
|
@ -2,11 +2,11 @@
|
|||
"nodes": {
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1667395993,
|
||||
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
||||
"lastModified": 1642700792,
|
||||
"narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
||||
"rev": "846b2ae0fc4cc943637d3d1def4454213e203cba",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -17,11 +17,11 @@
|
|||
},
|
||||
"flake-utils_2": {
|
||||
"locked": {
|
||||
"lastModified": 1659877975,
|
||||
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
||||
"lastModified": 1637014545,
|
||||
"narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
||||
"rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -32,11 +32,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1669542132,
|
||||
"narHash": "sha256-DRlg++NJAwPh8io3ExBJdNW7Djs3plVI5jgYQ+iXAZQ=",
|
||||
"lastModified": 1643805626,
|
||||
"narHash": "sha256-AXLDVMG+UaAGsGSpOtQHPIKB+IZ0KSd9WS77aanGzgc=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a115bb9bd56831941be3776c8a94005867f316a7",
|
||||
"rev": "554d2d8aa25b6e583575459c297ec23750adb6cb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -48,11 +48,11 @@
|
|||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1665296151,
|
||||
"narHash": "sha256-uOB0oxqxN9K7XGF1hcnY+PQnlQJ+3bP2vCn/+Ru/bbc=",
|
||||
"lastModified": 1637453606,
|
||||
"narHash": "sha256-Gy6cwUswft9xqsjWxFYEnx/63/qzaFUwatcbV5GF/GQ=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "14ccaaedd95a488dd7ae142757884d8e125b3363",
|
||||
"rev": "8afc4e543663ca0a6a4f496262cd05233737e732",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -75,11 +75,11 @@
|
|||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1669775522,
|
||||
"narHash": "sha256-6xxGArBqssX38DdHpDoPcPvB/e79uXyQBwpBcaO/BwY=",
|
||||
"lastModified": 1643941258,
|
||||
"narHash": "sha256-uHyEuICSu8qQp6adPTqV33ajiwoF0sCh+Iazaz5r7fo=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "3158e47f6b85a288d12948aeb9a048e0ed4434d6",
|
||||
"rev": "674156c4c2f46dd6a6846466cb8f9fee84c211ca",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
108
flake.nix
108
flake.nix
|
@ -3,67 +3,63 @@
|
|||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
flake-utils = {
|
||||
url = "github:numtide/flake-utils";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
};
|
||||
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
rust-overlay,
|
||||
}:
|
||||
outputs = { self, nixpkgs, flake-utils, rust-overlay }:
|
||||
flake-utils.lib.eachDefaultSystem
|
||||
(system: let
|
||||
pkgs = import nixpkgs {
|
||||
overlays = [rust-overlay.overlays.default];
|
||||
inherit system;
|
||||
};
|
||||
rust = pkgs.rust-bin.stable.latest.default;
|
||||
in {
|
||||
formatter = pkgs.alejandra;
|
||||
(system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
overlays = [ rust-overlay.overlay ];
|
||||
inherit system;
|
||||
};
|
||||
lib = pkgs.lib;
|
||||
rust = pkgs.rust-bin.stable.latest.default;
|
||||
cargoNix = pkgs.callPackage ./Cargo.nix {
|
||||
inherit pkgs;
|
||||
release = true;
|
||||
};
|
||||
debugCargoNix = pkgs.callPackage ./Cargo.nix {
|
||||
inherit pkgs;
|
||||
release = false;
|
||||
};
|
||||
in
|
||||
{
|
||||
devShell = pkgs.mkShell {
|
||||
buildInputs = with pkgs;
|
||||
[
|
||||
(rust.override {
|
||||
extensions = [ "rust-src" ];
|
||||
targets = [ "wasm32-unknown-unknown" ];
|
||||
})
|
||||
cargo-edit
|
||||
cargo-watch
|
||||
cargo-criterion
|
||||
cargo-fuzz
|
||||
cargo-flamegraph
|
||||
cargo-deny
|
||||
crate2nix
|
||||
wasm-pack
|
||||
pkgconfig
|
||||
openssl
|
||||
gnuplot
|
||||
|
||||
packages = {
|
||||
deadnix = pkgs.runCommand "deadnix" {} ''
|
||||
${pkgs.deadnix}/bin/deadnix --fail ${./.}
|
||||
mkdir $out
|
||||
'';
|
||||
};
|
||||
nodejs
|
||||
yarn
|
||||
|
||||
checks = {
|
||||
inherit (self.packages.${system}) deadnix;
|
||||
};
|
||||
# c deps
|
||||
cmake
|
||||
cmocka
|
||||
doxygen
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
(rust.override {
|
||||
extensions = ["rust-src"];
|
||||
targets = ["wasm32-unknown-unknown"];
|
||||
})
|
||||
cargo-edit
|
||||
cargo-watch
|
||||
cargo-criterion
|
||||
cargo-fuzz
|
||||
cargo-flamegraph
|
||||
cargo-deny
|
||||
crate2nix
|
||||
wasm-pack
|
||||
pkgconfig
|
||||
openssl
|
||||
gnuplot
|
||||
|
||||
nodejs
|
||||
yarn
|
||||
deno
|
||||
|
||||
# c deps
|
||||
cmake
|
||||
cmocka
|
||||
doxygen
|
||||
|
||||
rnix-lsp
|
||||
nixpkgs-fmt
|
||||
];
|
||||
};
|
||||
});
|
||||
rnix-lsp
|
||||
nixpkgs-fmt
|
||||
];
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"replacer": "scripts/denoify-replacer.mjs"
|
||||
}
|
|
@ -1,15 +1,11 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint"],
|
||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: [
|
||||
'@typescript-eslint',
|
||||
],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
],
|
||||
};
|
||||
|
|
2
javascript/.gitignore
vendored
2
javascript/.gitignore
vendored
|
@ -2,5 +2,3 @@
|
|||
/yarn.lock
|
||||
dist
|
||||
docs/
|
||||
.vim
|
||||
deno_dist/
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
e2e/verdacciodb
|
||||
dist
|
||||
docs
|
||||
deno_dist
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"semi": false,
|
||||
"arrowParens": "avoid"
|
||||
}
|
|
@ -8,7 +8,7 @@ Rust codebase and can be found in `~/automerge-wasm`). I.e. the responsibility
|
|||
of this codebase is
|
||||
|
||||
- To map from the javascript data model to the underlying `set`, `make`,
|
||||
`insert`, and `delete` operations of Automerge.
|
||||
`insert`, and `delete` operations of Automerge.
|
||||
- To expose a more convenient interface to functions in `automerge-wasm` which
|
||||
generate messages to send over the network or compressed file formats to store
|
||||
on disk
|
||||
|
@ -37,3 +37,4 @@ yarn test
|
|||
|
||||
If you make changes to the `automerge-wasm` package you will need to re-run
|
||||
`yarn e2e buildjs`
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ data](#make-some-data). If you're in a browser you need a bundler
|
|||
|
||||
### Bundler setup
|
||||
|
||||
|
||||
`@automerge/automerge` is a wrapper around a core library which is written in
|
||||
rust, compiled to WebAssembly and distributed as a separate package called
|
||||
`@automerge/automerge-wasm`. Browsers don't currently support WebAssembly
|
||||
|
@ -53,28 +54,28 @@ import * as automerge from "@automerge/automerge"
|
|||
import * as assert from "assert"
|
||||
|
||||
let doc1 = automerge.from({
|
||||
tasks: [
|
||||
{ description: "feed fish", done: false },
|
||||
{ description: "water plants", done: false },
|
||||
],
|
||||
tasks: [
|
||||
{description: "feed fish", done: false},
|
||||
{description: "water plants", done: false},
|
||||
]
|
||||
})
|
||||
|
||||
// Create a new thread of execution
|
||||
// Create a new thread of execution
|
||||
let doc2 = automerge.clone(doc1)
|
||||
|
||||
// Now we concurrently make changes to doc1 and doc2
|
||||
|
||||
// Complete a task in doc2
|
||||
doc2 = automerge.change(doc2, d => {
|
||||
d.tasks[0].done = true
|
||||
d.tasks[0].done = true
|
||||
})
|
||||
|
||||
// Add a task in doc1
|
||||
doc1 = automerge.change(doc1, d => {
|
||||
d.tasks.push({
|
||||
description: "water fish",
|
||||
done: false,
|
||||
})
|
||||
d.tasks.push({
|
||||
description: "water fish",
|
||||
done: false
|
||||
})
|
||||
})
|
||||
|
||||
// Merge changes from both docs
|
||||
|
@ -83,19 +84,19 @@ doc2 = automerge.merge(doc2, doc1)
|
|||
|
||||
// Both docs are merged and identical
|
||||
assert.deepEqual(doc1, {
|
||||
tasks: [
|
||||
{ description: "feed fish", done: true },
|
||||
{ description: "water plants", done: false },
|
||||
{ description: "water fish", done: false },
|
||||
],
|
||||
tasks: [
|
||||
{description: "feed fish", done: true},
|
||||
{description: "water plants", done: false},
|
||||
{description: "water fish", done: false},
|
||||
]
|
||||
})
|
||||
|
||||
assert.deepEqual(doc2, {
|
||||
tasks: [
|
||||
{ description: "feed fish", done: true },
|
||||
{ description: "water plants", done: false },
|
||||
{ description: "water fish", done: false },
|
||||
],
|
||||
tasks: [
|
||||
{description: "feed fish", done: true},
|
||||
{description: "water plants", done: false},
|
||||
{description: "water fish", done: false},
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"exclude": [
|
||||
"../dist/**/*",
|
||||
"../node_modules",
|
||||
"../test/**/*",
|
||||
"../src/**/*.deno.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"outDir": "../dist/cjs"
|
||||
}
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../dist/cjs"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"exclude": [
|
||||
"../dist/**/*",
|
||||
"../node_modules",
|
||||
"../test/**/*",
|
||||
"../src/**/*.deno.ts"
|
||||
],
|
||||
"emitDeclarationOnly": true,
|
||||
"compilerOptions": {
|
||||
"outDir": "../dist"
|
||||
}
|
||||
}
|
|
@ -1,14 +1,8 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"exclude": [
|
||||
"../dist/**/*",
|
||||
"../node_modules",
|
||||
"../test/**/*",
|
||||
"../src/**/*.deno.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "es6",
|
||||
"outDir": "../dist/mjs"
|
||||
}
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "es6",
|
||||
"outDir": "../dist/mjs"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
import * as Automerge from "../deno_dist/index.ts"
|
||||
|
||||
Deno.test("It should create, clone and free", () => {
|
||||
let doc1 = Automerge.init()
|
||||
let doc2 = Automerge.clone(doc1)
|
||||
|
||||
// this is only needed if weakrefs are not supported
|
||||
Automerge.free(doc1)
|
||||
Automerge.free(doc2)
|
||||
})
|
|
@ -54,7 +54,7 @@ yarn e2e buildexamples -e webpack
|
|||
If you're experimenting with a project which is not in the `examples` folder
|
||||
you'll need a running registry. `run-registry` builds and publishes
|
||||
`automerge-js` and `automerge-wasm` and then runs the registry at
|
||||
`localhost:4873`.
|
||||
`localhost:4873`.
|
||||
|
||||
```
|
||||
yarn e2e run-registry
|
||||
|
@ -63,6 +63,7 @@ yarn e2e run-registry
|
|||
You can now run `yarn install --registry http://localhost:4873` to experiment
|
||||
with the built packages.
|
||||
|
||||
|
||||
## Using the `dev` build of `automerge-wasm`
|
||||
|
||||
All the commands above take a `-p` flag which can be either `release` or
|
||||
|
|
|
@ -1,25 +1,15 @@
|
|||
import { once } from "events"
|
||||
import { setTimeout } from "timers/promises"
|
||||
import { spawn, ChildProcess } from "child_process"
|
||||
import {once} from "events"
|
||||
import {setTimeout} from "timers/promises"
|
||||
import {spawn, ChildProcess} from "child_process"
|
||||
import * as child_process from "child_process"
|
||||
import {
|
||||
command,
|
||||
subcommands,
|
||||
run,
|
||||
array,
|
||||
multioption,
|
||||
option,
|
||||
Type,
|
||||
} from "cmd-ts"
|
||||
import {command, subcommands, run, array, multioption, option, Type} from "cmd-ts"
|
||||
import * as path from "path"
|
||||
import * as fsPromises from "fs/promises"
|
||||
import fetch from "node-fetch"
|
||||
|
||||
const VERDACCIO_DB_PATH = path.normalize(`${__dirname}/verdacciodb`)
|
||||
const VERDACCIO_CONFIG_PATH = path.normalize(`${__dirname}/verdaccio.yaml`)
|
||||
const AUTOMERGE_WASM_PATH = path.normalize(
|
||||
`${__dirname}/../../rust/automerge-wasm`
|
||||
)
|
||||
const AUTOMERGE_WASM_PATH = path.normalize(`${__dirname}/../../rust/automerge-wasm`)
|
||||
const AUTOMERGE_JS_PATH = path.normalize(`${__dirname}/..`)
|
||||
const EXAMPLES_DIR = path.normalize(path.join(__dirname, "../", "examples"))
|
||||
|
||||
|
@ -28,286 +18,217 @@ type Example = "webpack" | "vite" | "create-react-app"
|
|||
|
||||
// Type to parse strings to `Example` so the types line up for the `buildExamples` commmand
|
||||
const ReadExample: Type<string, Example> = {
|
||||
async from(str) {
|
||||
if (str === "webpack") {
|
||||
return "webpack"
|
||||
} else if (str === "vite") {
|
||||
return "vite"
|
||||
} else if (str === "create-react-app") {
|
||||
return "create-react-app"
|
||||
} else {
|
||||
throw new Error(`Unknown example type ${str}`)
|
||||
async from(str) {
|
||||
if (str === "webpack") {
|
||||
return "webpack"
|
||||
} else if (str === "vite") {
|
||||
return "vite"
|
||||
} else if (str === "create-react-app") {
|
||||
return "create-react-app"
|
||||
} else {
|
||||
throw new Error(`Unknown example type ${str}`)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
type Profile = "dev" | "release"
|
||||
|
||||
const ReadProfile: Type<string, Profile> = {
|
||||
async from(str) {
|
||||
if (str === "dev") {
|
||||
return "dev"
|
||||
} else if (str === "release") {
|
||||
return "release"
|
||||
} else {
|
||||
throw new Error(`Unknown profile ${str}`)
|
||||
async from(str) {
|
||||
if (str === "dev") {
|
||||
return "dev"
|
||||
} else if (str === "release") {
|
||||
return "release"
|
||||
} else {
|
||||
throw new Error(`Unknown profile ${str}`)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const buildjs = command({
|
||||
name: "buildjs",
|
||||
args: {
|
||||
profile: option({
|
||||
type: ReadProfile,
|
||||
long: "profile",
|
||||
short: "p",
|
||||
defaultValue: () => "dev" as Profile,
|
||||
}),
|
||||
},
|
||||
handler: ({ profile }) => {
|
||||
console.log("building js")
|
||||
withPublishedWasm(profile, async (registryUrl: string) => {
|
||||
await buildAndPublishAutomergeJs(registryUrl)
|
||||
})
|
||||
},
|
||||
name: "buildjs",
|
||||
args: {
|
||||
profile: option({
|
||||
type: ReadProfile,
|
||||
long: "profile",
|
||||
short: "p",
|
||||
defaultValue: () => "dev" as Profile
|
||||
})
|
||||
},
|
||||
handler: ({profile}) => {
|
||||
console.log("building js")
|
||||
withPublishedWasm(profile, async (registryUrl: string) => {
|
||||
await buildAndPublishAutomergeJs(registryUrl)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const buildWasm = command({
|
||||
name: "buildwasm",
|
||||
args: {
|
||||
profile: option({
|
||||
type: ReadProfile,
|
||||
long: "profile",
|
||||
short: "p",
|
||||
defaultValue: () => "dev" as Profile,
|
||||
}),
|
||||
},
|
||||
handler: ({ profile }) => {
|
||||
console.log("building automerge-wasm")
|
||||
withRegistry(buildAutomergeWasm(profile))
|
||||
},
|
||||
name: "buildwasm",
|
||||
args: {
|
||||
profile: option({
|
||||
type: ReadProfile,
|
||||
long: "profile",
|
||||
short: "p",
|
||||
defaultValue: () => "dev" as Profile
|
||||
})
|
||||
},
|
||||
handler: ({profile}) => {
|
||||
console.log("building automerge-wasm")
|
||||
withRegistry(
|
||||
buildAutomergeWasm(profile),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const buildexamples = command({
|
||||
name: "buildexamples",
|
||||
args: {
|
||||
examples: multioption({
|
||||
long: "example",
|
||||
short: "e",
|
||||
type: array(ReadExample),
|
||||
}),
|
||||
profile: option({
|
||||
type: ReadProfile,
|
||||
long: "profile",
|
||||
short: "p",
|
||||
defaultValue: () => "dev" as Profile,
|
||||
}),
|
||||
},
|
||||
handler: ({ examples, profile }) => {
|
||||
if (examples.length === 0) {
|
||||
examples = ["webpack", "vite", "create-react-app"]
|
||||
name: "buildexamples",
|
||||
args: {
|
||||
examples: multioption({
|
||||
long: "example",
|
||||
short: "e",
|
||||
type: array(ReadExample),
|
||||
}),
|
||||
profile: option({
|
||||
type: ReadProfile,
|
||||
long: "profile",
|
||||
short: "p",
|
||||
defaultValue: () => "dev" as Profile
|
||||
})
|
||||
},
|
||||
handler: ({examples, profile}) => {
|
||||
if (examples.length === 0) {
|
||||
examples = ["webpack", "vite", "create-react-app"]
|
||||
}
|
||||
buildExamples(examples, profile)
|
||||
}
|
||||
buildExamples(examples, profile)
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
const runRegistry = command({
|
||||
name: "run-registry",
|
||||
args: {
|
||||
profile: option({
|
||||
type: ReadProfile,
|
||||
long: "profile",
|
||||
short: "p",
|
||||
defaultValue: () => "dev" as Profile,
|
||||
}),
|
||||
},
|
||||
handler: ({ profile }) => {
|
||||
withPublishedWasm(profile, async (registryUrl: string) => {
|
||||
await buildAndPublishAutomergeJs(registryUrl)
|
||||
console.log("\n************************")
|
||||
console.log(` Verdaccio NPM registry is running at ${registryUrl}`)
|
||||
console.log(" press CTRL-C to exit ")
|
||||
console.log("************************")
|
||||
await once(process, "SIGINT")
|
||||
}).catch(e => {
|
||||
console.error(`Failed: ${e}`)
|
||||
})
|
||||
},
|
||||
name: "run-registry",
|
||||
args: {
|
||||
profile: option({
|
||||
type: ReadProfile,
|
||||
long: "profile",
|
||||
short: "p",
|
||||
defaultValue: () => "dev" as Profile
|
||||
})
|
||||
},
|
||||
handler: ({profile}) => {
|
||||
withPublishedWasm(profile, async (registryUrl: string) => {
|
||||
await buildAndPublishAutomergeJs(registryUrl)
|
||||
console.log("\n************************")
|
||||
console.log(` Verdaccio NPM registry is running at ${registryUrl}`)
|
||||
console.log(" press CTRL-C to exit ")
|
||||
console.log("************************")
|
||||
await once(process, "SIGINT")
|
||||
}).catch(e => {
|
||||
console.error(`Failed: ${e}`)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const app = subcommands({
|
||||
name: "e2e",
|
||||
cmds: {
|
||||
buildjs,
|
||||
buildexamples,
|
||||
buildwasm: buildWasm,
|
||||
"run-registry": runRegistry,
|
||||
},
|
||||
name: "e2e",
|
||||
cmds: {buildjs, buildexamples, buildwasm: buildWasm, "run-registry": runRegistry}
|
||||
})
|
||||
|
||||
run(app, process.argv.slice(2))
|
||||
|
||||
async function buildExamples(examples: Array<Example>, profile: Profile) {
|
||||
await withPublishedWasm(profile, async registryUrl => {
|
||||
printHeader("building and publishing automerge")
|
||||
await buildAndPublishAutomergeJs(registryUrl)
|
||||
for (const example of examples) {
|
||||
printHeader(`building ${example} example`)
|
||||
if (example === "webpack") {
|
||||
const projectPath = path.join(EXAMPLES_DIR, example)
|
||||
await removeExistingAutomerge(projectPath)
|
||||
await fsPromises.rm(path.join(projectPath, "yarn.lock"), {
|
||||
force: true,
|
||||
})
|
||||
await spawnAndWait(
|
||||
"yarn",
|
||||
[
|
||||
"--cwd",
|
||||
projectPath,
|
||||
"install",
|
||||
"--registry",
|
||||
registryUrl,
|
||||
"--check-files",
|
||||
],
|
||||
{ stdio: "inherit" }
|
||||
)
|
||||
await spawnAndWait("yarn", ["--cwd", projectPath, "build"], {
|
||||
stdio: "inherit",
|
||||
})
|
||||
} else if (example === "vite") {
|
||||
const projectPath = path.join(EXAMPLES_DIR, example)
|
||||
await removeExistingAutomerge(projectPath)
|
||||
await fsPromises.rm(path.join(projectPath, "yarn.lock"), {
|
||||
force: true,
|
||||
})
|
||||
await spawnAndWait(
|
||||
"yarn",
|
||||
[
|
||||
"--cwd",
|
||||
projectPath,
|
||||
"install",
|
||||
"--registry",
|
||||
registryUrl,
|
||||
"--check-files",
|
||||
],
|
||||
{ stdio: "inherit" }
|
||||
)
|
||||
await spawnAndWait("yarn", ["--cwd", projectPath, "build"], {
|
||||
stdio: "inherit",
|
||||
})
|
||||
} else if (example === "create-react-app") {
|
||||
const projectPath = path.join(EXAMPLES_DIR, example)
|
||||
await removeExistingAutomerge(projectPath)
|
||||
await fsPromises.rm(path.join(projectPath, "yarn.lock"), {
|
||||
force: true,
|
||||
})
|
||||
await spawnAndWait(
|
||||
"yarn",
|
||||
[
|
||||
"--cwd",
|
||||
projectPath,
|
||||
"install",
|
||||
"--registry",
|
||||
registryUrl,
|
||||
"--check-files",
|
||||
],
|
||||
{ stdio: "inherit" }
|
||||
)
|
||||
await spawnAndWait("yarn", ["--cwd", projectPath, "build"], {
|
||||
stdio: "inherit",
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
await withPublishedWasm(profile, async (registryUrl) => {
|
||||
printHeader("building and publishing automerge")
|
||||
await buildAndPublishAutomergeJs(registryUrl)
|
||||
for (const example of examples) {
|
||||
printHeader(`building ${example} example`)
|
||||
if (example === "webpack") {
|
||||
const projectPath = path.join(EXAMPLES_DIR, example)
|
||||
await removeExistingAutomerge(projectPath)
|
||||
await fsPromises.rm(path.join(projectPath, "yarn.lock"), {force: true})
|
||||
await spawnAndWait("yarn", ["--cwd", projectPath, "install", "--registry", registryUrl, "--check-files"], {stdio: "inherit"})
|
||||
await spawnAndWait("yarn", ["--cwd", projectPath, "build"], {stdio: "inherit"})
|
||||
} else if (example === "vite") {
|
||||
const projectPath = path.join(EXAMPLES_DIR, example)
|
||||
await removeExistingAutomerge(projectPath)
|
||||
await fsPromises.rm(path.join(projectPath, "yarn.lock"), {force: true})
|
||||
await spawnAndWait("yarn", ["--cwd", projectPath, "install", "--registry", registryUrl, "--check-files"], {stdio: "inherit"})
|
||||
await spawnAndWait("yarn", ["--cwd", projectPath, "build"], {stdio: "inherit"})
|
||||
} else if (example === "create-react-app") {
|
||||
const projectPath = path.join(EXAMPLES_DIR, example)
|
||||
await removeExistingAutomerge(projectPath)
|
||||
await fsPromises.rm(path.join(projectPath, "yarn.lock"), {force: true})
|
||||
await spawnAndWait("yarn", ["--cwd", projectPath, "install", "--registry", registryUrl, "--check-files"], {stdio: "inherit"})
|
||||
await spawnAndWait("yarn", ["--cwd", projectPath, "build"], {stdio: "inherit"})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type WithRegistryAction = (registryUrl: string) => Promise<void>
|
||||
|
||||
async function withRegistry(
|
||||
action: WithRegistryAction,
|
||||
...actions: Array<WithRegistryAction>
|
||||
) {
|
||||
// First, start verdaccio
|
||||
printHeader("Starting verdaccio NPM server")
|
||||
const verd = await VerdaccioProcess.start()
|
||||
actions.unshift(action)
|
||||
async function withRegistry(action: WithRegistryAction, ...actions: Array<WithRegistryAction>) {
|
||||
// First, start verdaccio
|
||||
printHeader("Starting verdaccio NPM server")
|
||||
const verd = await VerdaccioProcess.start()
|
||||
actions.unshift(action)
|
||||
|
||||
for (const action of actions) {
|
||||
try {
|
||||
type Step = "verd-died" | "action-completed"
|
||||
const verdDied: () => Promise<Step> = async () => {
|
||||
await verd.died()
|
||||
return "verd-died"
|
||||
}
|
||||
const actionComplete: () => Promise<Step> = async () => {
|
||||
await action("http://localhost:4873")
|
||||
return "action-completed"
|
||||
}
|
||||
const result = await Promise.race([verdDied(), actionComplete()])
|
||||
if (result === "verd-died") {
|
||||
throw new Error("verdaccio unexpectedly exited")
|
||||
}
|
||||
} catch (e) {
|
||||
await verd.kill()
|
||||
throw e
|
||||
for (const action of actions) {
|
||||
try {
|
||||
type Step = "verd-died" | "action-completed"
|
||||
const verdDied: () => Promise<Step> = async () => {
|
||||
await verd.died()
|
||||
return "verd-died"
|
||||
}
|
||||
const actionComplete: () => Promise<Step> = async () => {
|
||||
await action("http://localhost:4873")
|
||||
return "action-completed"
|
||||
}
|
||||
const result = await Promise.race([verdDied(), actionComplete()])
|
||||
if (result === "verd-died") {
|
||||
throw new Error("verdaccio unexpectedly exited")
|
||||
}
|
||||
} catch(e) {
|
||||
await verd.kill()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
await verd.kill()
|
||||
await verd.kill()
|
||||
}
|
||||
|
||||
async function withPublishedWasm(profile: Profile, action: WithRegistryAction) {
|
||||
await withRegistry(buildAutomergeWasm(profile), publishAutomergeWasm, action)
|
||||
await withRegistry(
|
||||
buildAutomergeWasm(profile),
|
||||
publishAutomergeWasm,
|
||||
action
|
||||
)
|
||||
}
|
||||
|
||||
function buildAutomergeWasm(profile: Profile): WithRegistryAction {
|
||||
return async (registryUrl: string) => {
|
||||
printHeader("building automerge-wasm")
|
||||
await spawnAndWait(
|
||||
"yarn",
|
||||
["--cwd", AUTOMERGE_WASM_PATH, "--registry", registryUrl, "install"],
|
||||
{ stdio: "inherit" }
|
||||
)
|
||||
const cmd = profile === "release" ? "release" : "debug"
|
||||
await spawnAndWait("yarn", ["--cwd", AUTOMERGE_WASM_PATH, cmd], {
|
||||
stdio: "inherit",
|
||||
})
|
||||
}
|
||||
return async (registryUrl: string) => {
|
||||
printHeader("building automerge-wasm")
|
||||
await spawnAndWait("yarn", ["--cwd", AUTOMERGE_WASM_PATH, "--registry", registryUrl, "install"], {stdio: "inherit"})
|
||||
const cmd = profile === "release" ? "release" : "debug"
|
||||
await spawnAndWait("yarn", ["--cwd", AUTOMERGE_WASM_PATH, cmd], {stdio: "inherit"})
|
||||
}
|
||||
}
|
||||
|
||||
async function publishAutomergeWasm(registryUrl: string) {
|
||||
printHeader("Publishing automerge-wasm to verdaccio")
|
||||
await fsPromises.rm(
|
||||
path.join(VERDACCIO_DB_PATH, "@automerge/automerge-wasm"),
|
||||
{ recursive: true, force: true }
|
||||
)
|
||||
await yarnPublish(registryUrl, AUTOMERGE_WASM_PATH)
|
||||
printHeader("Publishing automerge-wasm to verdaccio")
|
||||
await fsPromises.rm(path.join(VERDACCIO_DB_PATH, "@automerge/automerge-wasm"), { recursive: true, force: true} )
|
||||
await yarnPublish(registryUrl, AUTOMERGE_WASM_PATH)
|
||||
}
|
||||
|
||||
async function buildAndPublishAutomergeJs(registryUrl: string) {
|
||||
// Build the js package
|
||||
printHeader("Building automerge")
|
||||
await removeExistingAutomerge(AUTOMERGE_JS_PATH)
|
||||
await removeFromVerdaccio("@automerge/automerge")
|
||||
await fsPromises.rm(path.join(AUTOMERGE_JS_PATH, "yarn.lock"), {
|
||||
force: true,
|
||||
})
|
||||
await spawnAndWait(
|
||||
"yarn",
|
||||
[
|
||||
"--cwd",
|
||||
AUTOMERGE_JS_PATH,
|
||||
"install",
|
||||
"--registry",
|
||||
registryUrl,
|
||||
"--check-files",
|
||||
],
|
||||
{ stdio: "inherit" }
|
||||
)
|
||||
await spawnAndWait("yarn", ["--cwd", AUTOMERGE_JS_PATH, "build"], {
|
||||
stdio: "inherit",
|
||||
})
|
||||
await yarnPublish(registryUrl, AUTOMERGE_JS_PATH)
|
||||
// Build the js package
|
||||
printHeader("Building automerge")
|
||||
await removeExistingAutomerge(AUTOMERGE_JS_PATH)
|
||||
await removeFromVerdaccio("@automerge/automerge")
|
||||
await fsPromises.rm(path.join(AUTOMERGE_JS_PATH, "yarn.lock"), {force: true})
|
||||
await spawnAndWait("yarn", ["--cwd", AUTOMERGE_JS_PATH, "install", "--registry", registryUrl, "--check-files"], {stdio: "inherit"})
|
||||
await spawnAndWait("yarn", ["--cwd", AUTOMERGE_JS_PATH, "build"], {stdio: "inherit"})
|
||||
await yarnPublish(registryUrl, AUTOMERGE_JS_PATH)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -315,110 +236,104 @@ async function buildAndPublishAutomergeJs(registryUrl: string) {
|
|||
*
|
||||
*/
|
||||
class VerdaccioProcess {
|
||||
child: ChildProcess
|
||||
stdout: Array<Buffer>
|
||||
stderr: Array<Buffer>
|
||||
child: ChildProcess
|
||||
stdout: Array<Buffer>
|
||||
stderr: Array<Buffer>
|
||||
|
||||
constructor(child: ChildProcess) {
|
||||
this.child = child
|
||||
constructor(child: ChildProcess) {
|
||||
this.child = child
|
||||
|
||||
// Collect stdout/stderr otherwise the subprocess gets blocked writing
|
||||
this.stdout = []
|
||||
this.stderr = []
|
||||
this.child.stdout &&
|
||||
this.child.stdout.on("data", data => this.stdout.push(data))
|
||||
this.child.stderr &&
|
||||
this.child.stderr.on("data", data => this.stderr.push(data))
|
||||
// Collect stdout/stderr otherwise the subprocess gets blocked writing
|
||||
this.stdout = []
|
||||
this.stderr = []
|
||||
this.child.stdout && this.child.stdout.on("data", (data) => this.stdout.push(data))
|
||||
this.child.stderr && this.child.stderr.on("data", (data) => this.stderr.push(data))
|
||||
|
||||
const errCallback = (e: any) => {
|
||||
console.error("!!!!!!!!!ERROR IN VERDACCIO PROCESS!!!!!!!!!")
|
||||
console.error(" ", e)
|
||||
if (this.stdout.length > 0) {
|
||||
console.log("\n**Verdaccio stdout**")
|
||||
const stdout = Buffer.concat(this.stdout)
|
||||
process.stdout.write(stdout)
|
||||
}
|
||||
const errCallback = (e: any) => {
|
||||
console.error("!!!!!!!!!ERROR IN VERDACCIO PROCESS!!!!!!!!!")
|
||||
console.error(" ", e)
|
||||
if (this.stdout.length > 0) {
|
||||
console.log("\n**Verdaccio stdout**")
|
||||
const stdout = Buffer.concat(this.stdout)
|
||||
process.stdout.write(stdout)
|
||||
}
|
||||
|
||||
if (this.stderr.length > 0) {
|
||||
console.log("\n**Verdaccio stderr**")
|
||||
const stdout = Buffer.concat(this.stderr)
|
||||
process.stdout.write(stdout)
|
||||
}
|
||||
process.exit(-1)
|
||||
}
|
||||
this.child.on("error", errCallback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a verdaccio process and wait for it to respond succesfully to http requests
|
||||
*
|
||||
* The returned `VerdaccioProcess` can be used to control the subprocess
|
||||
*/
|
||||
static async start() {
|
||||
const child = spawn(
|
||||
"yarn",
|
||||
["verdaccio", "--config", VERDACCIO_CONFIG_PATH],
|
||||
{ env: { ...process.env, FORCE_COLOR: "true" } }
|
||||
)
|
||||
|
||||
// Forward stdout and stderr whilst waiting for startup to complete
|
||||
const stdoutCallback = (data: Buffer) => process.stdout.write(data)
|
||||
const stderrCallback = (data: Buffer) => process.stderr.write(data)
|
||||
child.stdout && child.stdout.on("data", stdoutCallback)
|
||||
child.stderr && child.stderr.on("data", stderrCallback)
|
||||
|
||||
const healthCheck = async () => {
|
||||
while (true) {
|
||||
try {
|
||||
const resp = await fetch("http://localhost:4873")
|
||||
if (resp.status === 200) {
|
||||
return
|
||||
} else {
|
||||
console.log(`Healthcheck failed: bad status ${resp.status}`)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Healthcheck failed: ${e}`)
|
||||
if (this.stderr.length > 0) {
|
||||
console.log("\n**Verdaccio stderr**")
|
||||
const stdout = Buffer.concat(this.stderr)
|
||||
process.stdout.write(stdout)
|
||||
}
|
||||
process.exit(-1)
|
||||
}
|
||||
await setTimeout(500)
|
||||
}
|
||||
this.child.on("error", errCallback)
|
||||
}
|
||||
await withTimeout(healthCheck(), 10000)
|
||||
|
||||
// Stop forwarding stdout/stderr
|
||||
child.stdout && child.stdout.off("data", stdoutCallback)
|
||||
child.stderr && child.stderr.off("data", stderrCallback)
|
||||
return new VerdaccioProcess(child)
|
||||
}
|
||||
/**
|
||||
* Spawn a verdaccio process and wait for it to respond succesfully to http requests
|
||||
*
|
||||
* The returned `VerdaccioProcess` can be used to control the subprocess
|
||||
*/
|
||||
static async start() {
|
||||
const child = spawn("yarn", ["verdaccio", "--config", VERDACCIO_CONFIG_PATH], {env: { ...process.env, FORCE_COLOR: "true"}})
|
||||
|
||||
/**
|
||||
* Send a SIGKILL to the process and wait for it to stop
|
||||
*/
|
||||
async kill() {
|
||||
this.child.stdout && this.child.stdout.destroy()
|
||||
this.child.stderr && this.child.stderr.destroy()
|
||||
this.child.kill()
|
||||
try {
|
||||
await withTimeout(once(this.child, "close"), 500)
|
||||
} catch (e) {
|
||||
console.error("unable to kill verdaccio subprocess, trying -9")
|
||||
this.child.kill(9)
|
||||
await withTimeout(once(this.child, "close"), 500)
|
||||
// Forward stdout and stderr whilst waiting for startup to complete
|
||||
const stdoutCallback = (data: Buffer) => process.stdout.write(data)
|
||||
const stderrCallback = (data: Buffer) => process.stderr.write(data)
|
||||
child.stdout && child.stdout.on("data", stdoutCallback)
|
||||
child.stderr && child.stderr.on("data", stderrCallback)
|
||||
|
||||
const healthCheck = async () => {
|
||||
while (true) {
|
||||
try {
|
||||
const resp = await fetch("http://localhost:4873")
|
||||
if (resp.status === 200) {
|
||||
return
|
||||
} else {
|
||||
console.log(`Healthcheck failed: bad status ${resp.status}`)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Healthcheck failed: ${e}`)
|
||||
}
|
||||
await setTimeout(500)
|
||||
}
|
||||
}
|
||||
await withTimeout(healthCheck(), 10000)
|
||||
|
||||
// Stop forwarding stdout/stderr
|
||||
child.stdout && child.stdout.off("data", stdoutCallback)
|
||||
child.stderr && child.stderr.off("data", stderrCallback)
|
||||
return new VerdaccioProcess(child)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A promise which resolves if the subprocess exits for some reason
|
||||
*/
|
||||
async died(): Promise<number | null> {
|
||||
const [exit, _signal] = await once(this.child, "exit")
|
||||
return exit
|
||||
}
|
||||
/**
|
||||
* Send a SIGKILL to the process and wait for it to stop
|
||||
*/
|
||||
async kill() {
|
||||
this.child.stdout && this.child.stdout.destroy()
|
||||
this.child.stderr && this.child.stderr.destroy()
|
||||
this.child.kill();
|
||||
try {
|
||||
await withTimeout(once(this.child, "close"), 500)
|
||||
} catch (e) {
|
||||
console.error("unable to kill verdaccio subprocess, trying -9")
|
||||
this.child.kill(9)
|
||||
await withTimeout(once(this.child, "close"), 500)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A promise which resolves if the subprocess exits for some reason
|
||||
*/
|
||||
async died(): Promise<number | null> {
|
||||
const [exit, _signal] = await once(this.child, "exit")
|
||||
return exit
|
||||
}
|
||||
}
|
||||
|
||||
function printHeader(header: string) {
|
||||
console.log("\n===============================")
|
||||
console.log(` ${header}`)
|
||||
console.log("===============================")
|
||||
console.log("\n===============================")
|
||||
console.log(` ${header}`)
|
||||
console.log("===============================")
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -432,46 +347,36 @@ function printHeader(header: string) {
|
|||
* @param packageDir - The directory containing the package.json of the target project
|
||||
*/
|
||||
async function removeExistingAutomerge(packageDir: string) {
|
||||
await fsPromises.rm(path.join(packageDir, "node_modules", "@automerge"), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
})
|
||||
await fsPromises.rm(path.join(packageDir, "node_modules", "automerge"), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
})
|
||||
await fsPromises.rm(path.join(packageDir, "node_modules", "@automerge"), {recursive: true, force: true})
|
||||
await fsPromises.rm(path.join(packageDir, "node_modules", "automerge"), {recursive: true, force: true})
|
||||
}
|
||||
|
||||
type SpawnResult = {
|
||||
stdout?: Buffer
|
||||
stderr?: Buffer
|
||||
stdout?: Buffer,
|
||||
stderr?: Buffer,
|
||||
}
|
||||
|
||||
async function spawnAndWait(
|
||||
cmd: string,
|
||||
args: Array<string>,
|
||||
options: child_process.SpawnOptions
|
||||
): Promise<SpawnResult> {
|
||||
const child = spawn(cmd, args, options)
|
||||
let stdout = null
|
||||
let stderr = null
|
||||
if (child.stdout) {
|
||||
stdout = []
|
||||
child.stdout.on("data", data => stdout.push(data))
|
||||
}
|
||||
if (child.stderr) {
|
||||
stderr = []
|
||||
child.stderr.on("data", data => stderr.push(data))
|
||||
}
|
||||
async function spawnAndWait(cmd: string, args: Array<string>, options: child_process.SpawnOptions): Promise<SpawnResult> {
|
||||
const child = spawn(cmd, args, options)
|
||||
let stdout = null
|
||||
let stderr = null
|
||||
if (child.stdout) {
|
||||
stdout = []
|
||||
child.stdout.on("data", data => stdout.push(data))
|
||||
}
|
||||
if (child.stderr) {
|
||||
stderr = []
|
||||
child.stderr.on("data", data => stderr.push(data))
|
||||
}
|
||||
|
||||
const [exit, _signal] = await once(child, "exit")
|
||||
if (exit && exit !== 0) {
|
||||
throw new Error("nonzero exit code")
|
||||
}
|
||||
return {
|
||||
stderr: stderr ? Buffer.concat(stderr) : null,
|
||||
stdout: stdout ? Buffer.concat(stdout) : null,
|
||||
}
|
||||
const [exit, _signal] = await once(child, "exit")
|
||||
if (exit && exit !== 0) {
|
||||
throw new Error("nonzero exit code")
|
||||
}
|
||||
return {
|
||||
stderr: stderr? Buffer.concat(stderr) : null,
|
||||
stdout: stdout ? Buffer.concat(stdout) : null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -482,27 +387,29 @@ async function spawnAndWait(
|
|||
* okay I Promise.
|
||||
*/
|
||||
async function removeFromVerdaccio(packageName: string) {
|
||||
await fsPromises.rm(path.join(VERDACCIO_DB_PATH, packageName), {
|
||||
force: true,
|
||||
recursive: true,
|
||||
})
|
||||
await fsPromises.rm(path.join(VERDACCIO_DB_PATH, packageName), {force: true, recursive: true})
|
||||
}
|
||||
|
||||
async function yarnPublish(registryUrl: string, cwd: string) {
|
||||
await spawnAndWait(
|
||||
"yarn",
|
||||
["--registry", registryUrl, "--cwd", cwd, "publish", "--non-interactive"],
|
||||
{
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
FORCE_COLOR: "true",
|
||||
// This is a fake token, it just has to be the right format
|
||||
npm_config__auth:
|
||||
"//localhost:4873/:_authToken=Gp2Mgxm4faa/7wp0dMSuRA==",
|
||||
},
|
||||
}
|
||||
)
|
||||
await spawnAndWait(
|
||||
"yarn",
|
||||
[
|
||||
"--registry",
|
||||
registryUrl,
|
||||
"--cwd",
|
||||
cwd,
|
||||
"publish",
|
||||
"--non-interactive",
|
||||
],
|
||||
{
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
FORCE_COLOR: "true",
|
||||
// This is a fake token, it just has to be the right format
|
||||
npm_config__auth: "//localhost:4873/:_authToken=Gp2Mgxm4faa/7wp0dMSuRA=="
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -512,23 +419,20 @@ async function yarnPublish(registryUrl: string, cwd: string) {
|
|||
* @param promise - the promise to wait for @param timeout - the delay in
|
||||
* milliseconds to wait before throwing
|
||||
*/
|
||||
async function withTimeout<T>(
|
||||
promise: Promise<T>,
|
||||
timeout: number
|
||||
): Promise<T> {
|
||||
type Step = "timed-out" | { result: T }
|
||||
const timedOut: () => Promise<Step> = async () => {
|
||||
await setTimeout(timeout)
|
||||
return "timed-out"
|
||||
}
|
||||
const succeeded: () => Promise<Step> = async () => {
|
||||
const result = await promise
|
||||
return { result }
|
||||
}
|
||||
const result = await Promise.race([timedOut(), succeeded()])
|
||||
if (result === "timed-out") {
|
||||
throw new Error("timed out")
|
||||
} else {
|
||||
return result.result
|
||||
}
|
||||
async function withTimeout<T>(promise: Promise<T>, timeout: number): Promise<T> {
|
||||
type Step = "timed-out" | {result: T}
|
||||
const timedOut: () => Promise<Step> = async () => {
|
||||
await setTimeout(timeout)
|
||||
return "timed-out"
|
||||
}
|
||||
const succeeded: () => Promise<Step> = async () => {
|
||||
const result = await promise
|
||||
return {result}
|
||||
}
|
||||
const result = await Promise.race([timedOut(), succeeded()])
|
||||
if (result === "timed-out") {
|
||||
throw new Error("timed out")
|
||||
} else {
|
||||
return result.result
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"types": ["node"]
|
||||
},
|
||||
"module": "nodenext"
|
||||
"compilerOptions": {
|
||||
"types": ["node"]
|
||||
},
|
||||
"module": "nodenext"
|
||||
}
|
||||
|
|
|
@ -4,22 +4,22 @@ auth:
|
|||
file: ./htpasswd
|
||||
publish:
|
||||
allow_offline: true
|
||||
logs: { type: stdout, format: pretty, level: info }
|
||||
packages:
|
||||
logs: {type: stdout, format: pretty, level: info}
|
||||
packages:
|
||||
"@automerge/automerge-wasm":
|
||||
access: "$all"
|
||||
publish: "$all"
|
||||
access: "$all"
|
||||
publish: "$all"
|
||||
"@automerge/automerge":
|
||||
access: "$all"
|
||||
publish: "$all"
|
||||
access: "$all"
|
||||
publish: "$all"
|
||||
"*":
|
||||
access: "$all"
|
||||
publish: "$all"
|
||||
proxy: npmjs
|
||||
access: "$all"
|
||||
publish: "$all"
|
||||
proxy: npmjs
|
||||
"@*/*":
|
||||
access: "$all"
|
||||
publish: "$all"
|
||||
proxy: npmjs
|
||||
access: "$all"
|
||||
publish: "$all"
|
||||
proxy: npmjs
|
||||
uplinks:
|
||||
npmjs:
|
||||
url: https://registry.npmjs.org/
|
||||
|
|
|
@ -54,6 +54,6 @@ In the root of the project add the following contents to `craco.config.js`
|
|||
const cracoWasm = require("craco-wasm")
|
||||
|
||||
module.exports = {
|
||||
plugins: [cracoWasm()],
|
||||
plugins: [cracoWasm()]
|
||||
}
|
||||
```
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const cracoWasm = require("craco-wasm")
|
||||
|
||||
module.exports = {
|
||||
plugins: [cracoWasm()],
|
||||
plugins: [cracoWasm()]
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@automerge/automerge": "2.0.0-alpha.7",
|
||||
"@automerge/automerge": "2.0.0-alpha.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1",
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import * as Automerge from "@automerge/automerge"
|
||||
import logo from "./logo.svg"
|
||||
import "./App.css"
|
||||
import logo from './logo.svg';
|
||||
import './App.css';
|
||||
|
||||
let doc = Automerge.init()
|
||||
doc = Automerge.change(doc, d => (d.hello = "from automerge"))
|
||||
doc = Automerge.change(doc, (d) => d.hello = "from automerge")
|
||||
const result = JSON.stringify(doc)
|
||||
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
|
@ -14,7 +15,7 @@ function App() {
|
|||
<p>{result}</p>
|
||||
</header>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { render, screen } from "@testing-library/react"
|
||||
import App from "./App"
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test("renders learn react link", () => {
|
||||
render(<App />)
|
||||
const linkElement = screen.getByText(/learn react/i)
|
||||
expect(linkElement).toBeInTheDocument()
|
||||
})
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import "./index.css"
|
||||
import App from "./App"
|
||||
import reportWebVitals from "./reportWebVitals"
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root"))
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
)
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals()
|
||||
reportWebVitals();
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry)
|
||||
getFID(onPerfEntry)
|
||||
getFCP(onPerfEntry)
|
||||
getLCP(onPerfEntry)
|
||||
getTTFB(onPerfEntry)
|
||||
})
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals
|
||||
export default reportWebVitals;
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import "@testing-library/jest-dom"
|
||||
import '@testing-library/jest-dom';
|
||||
|
|
|
@ -24,17 +24,17 @@
|
|||
jsonpointer "^5.0.0"
|
||||
leven "^3.1.0"
|
||||
|
||||
"@automerge/automerge-wasm@0.1.12":
|
||||
version "0.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@automerge/automerge-wasm/-/automerge-wasm-0.1.12.tgz#8ce25255d95d4ed6fb387de6858f7b7b7e2ed4a9"
|
||||
integrity sha512-/xjX1217QYJ+QaoT6iHQw4hGNUIoc3xc65c9eCnfX5v9J9BkTOl05p2Cnr51O2rPc/M6TqZLmlvpvNVdcH9JpA==
|
||||
"@automerge/automerge-wasm@0.1.9":
|
||||
version "0.1.9"
|
||||
resolved "http://localhost:4873/@automerge%2fautomerge-wasm/-/automerge-wasm-0.1.9.tgz#b2def5e8b643f1802bc696843b7755dc444dc2eb"
|
||||
integrity sha512-S+sjJUJ3aPn2F37vKYAzKxz8CDgbHpOOGVjKSgkLjkAqe1pQ+wp4BpiELXafX73w8DVIrGx1zzru4w3t+Eo8gw==
|
||||
|
||||
"@automerge/automerge@2.0.0-alpha.7":
|
||||
version "2.0.0-alpha.7"
|
||||
resolved "https://registry.yarnpkg.com/@automerge/automerge/-/automerge-2.0.0-alpha.7.tgz#2ee220d51bcd796074a18af74eeabb5f177e1f36"
|
||||
integrity sha512-Wd2/GNeqtBybUtXclEE7bWBmmEkhv3q2ITQmLh18V0VvMPbqMBpcOKYzQFnKCyiPyRe5XcYeQAyGyunhE5V0ug==
|
||||
"@automerge/automerge@2.0.0-alpha.4":
|
||||
version "2.0.0-alpha.4"
|
||||
resolved "http://localhost:4873/@automerge%2fautomerge/-/automerge-2.0.0-alpha.4.tgz#df406f5364960a4d21040044da55ebd47406ea3a"
|
||||
integrity sha512-PVRD1dmLy0U4GttyMvlWr99wyr6xvskJbOkxJDHnp+W2VAFfcqa4QKouaFbJ4W3iIsYX8DfQJ+uhRxa6UnvkHg==
|
||||
dependencies:
|
||||
"@automerge/automerge-wasm" "0.1.12"
|
||||
"@automerge/automerge-wasm" "0.1.9"
|
||||
uuid "^8.3"
|
||||
|
||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.8.3":
|
||||
|
@ -2827,7 +2827,7 @@ bfj@^7.0.2:
|
|||
|
||||
big.js@^5.2.2:
|
||||
version "5.2.2"
|
||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
|
||||
resolved "http://localhost:4873/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
|
||||
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
|
||||
|
||||
binary-extensions@^2.0.0:
|
||||
|
@ -3817,7 +3817,7 @@ emoji-regex@^9.2.2:
|
|||
|
||||
emojis-list@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
|
||||
resolved "http://localhost:4873/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
|
||||
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
|
||||
|
||||
encodeurl@~1.0.2:
|
||||
|
@ -5845,9 +5845,9 @@ json-stable-stringify-without-jsonify@^1.0.1:
|
|||
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
|
||||
|
||||
json5@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
|
||||
integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
|
||||
version "1.0.1"
|
||||
resolved "http://localhost:4873/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
|
||||
integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
|
||||
dependencies:
|
||||
minimist "^1.2.0"
|
||||
|
||||
|
@ -5942,9 +5942,9 @@ loader-runner@^4.2.0:
|
|||
integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==
|
||||
|
||||
loader-utils@^2.0.0:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c"
|
||||
integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==
|
||||
version "2.0.2"
|
||||
resolved "http://localhost:4873/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129"
|
||||
integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==
|
||||
dependencies:
|
||||
big.js "^5.2.2"
|
||||
emojis-list "^3.0.0"
|
||||
|
@ -6165,9 +6165,9 @@ minimatch@^5.0.1:
|
|||
brace-expansion "^2.0.1"
|
||||
|
||||
minimist@^1.2.0, minimist@^1.2.6:
|
||||
version "1.2.7"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
|
||||
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
|
||||
version "1.2.6"
|
||||
resolved "http://localhost:4873/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
|
||||
mkdirp@~0.5.1:
|
||||
version "0.5.6"
|
||||
|
|
|
@ -7,7 +7,6 @@ There are three things you need to do to get WASM packaging working with vite:
|
|||
3. Exclude `automerge-wasm` from the optimizer
|
||||
|
||||
First, install the packages we need:
|
||||
|
||||
```bash
|
||||
yarn add vite-plugin-top-level-await
|
||||
yarn add vite-plugin-wasm
|
||||
|
@ -21,22 +20,22 @@ import wasm from "vite-plugin-wasm"
|
|||
import topLevelAwait from "vite-plugin-top-level-await"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [topLevelAwait(), wasm()],
|
||||
|
||||
// This is only necessary if you are using `SharedWorker` or `WebWorker`, as
|
||||
// documented in https://vitejs.dev/guide/features.html#import-with-constructors
|
||||
worker: {
|
||||
format: "es",
|
||||
plugins: [topLevelAwait(), wasm()],
|
||||
},
|
||||
|
||||
// This is only necessary if you are using `SharedWorker` or `WebWorker`, as
|
||||
// documented in https://vitejs.dev/guide/features.html#import-with-constructors
|
||||
worker: {
|
||||
format: "es",
|
||||
plugins: [topLevelAwait(), wasm()]
|
||||
},
|
||||
|
||||
optimizeDeps: {
|
||||
// This is necessary because otherwise `vite dev` includes two separate
|
||||
// versions of the JS wrapper. This causes problems because the JS
|
||||
// wrapper has a module level variable to track JS side heap
|
||||
// allocations, initializing this twice causes horrible breakage
|
||||
exclude: ["@automerge/automerge-wasm"],
|
||||
},
|
||||
optimizeDeps: {
|
||||
// This is necessary because otherwise `vite dev` includes two separate
|
||||
// versions of the JS wrapper. This causes problems because the JS
|
||||
// wrapper has a module level variable to track JS side heap
|
||||
// allocations, initializing this twice causes horrible breakage
|
||||
exclude: ["@automerge/automerge-wasm"]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
|
@ -52,3 +51,4 @@ yarn vite
|
|||
yarn install
|
||||
yarn dev
|
||||
```
|
||||
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import * as Automerge from "/node_modules/.vite/deps/automerge-js.js?v=6e973f28"
|
||||
console.log(Automerge)
|
||||
let doc = Automerge.init()
|
||||
doc = Automerge.change(doc, d => (d.hello = "from automerge-js"))
|
||||
console.log(doc)
|
||||
const result = JSON.stringify(doc)
|
||||
import * as Automerge from "/node_modules/.vite/deps/automerge-js.js?v=6e973f28";
|
||||
console.log(Automerge);
|
||||
let doc = Automerge.init();
|
||||
doc = Automerge.change(doc, (d) => d.hello = "from automerge-js");
|
||||
console.log(doc);
|
||||
const result = JSON.stringify(doc);
|
||||
if (typeof document !== "undefined") {
|
||||
const element = document.createElement("div")
|
||||
element.innerHTML = JSON.stringify(result)
|
||||
document.body.appendChild(element)
|
||||
const element = document.createElement("div");
|
||||
element.innerHTML = JSON.stringify(result);
|
||||
document.body.appendChild(element);
|
||||
} else {
|
||||
console.log("node:", result)
|
||||
console.log("node:", result);
|
||||
}
|
||||
|
||||
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi9ob21lL2FsZXgvUHJvamVjdHMvYXV0b21lcmdlL2F1dG9tZXJnZS1ycy9hdXRvbWVyZ2UtanMvZXhhbXBsZXMvdml0ZS9zcmMvbWFpbi50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBBdXRvbWVyZ2UgZnJvbSBcImF1dG9tZXJnZS1qc1wiXG5cbi8vIGhlbGxvIHdvcmxkIGNvZGUgdGhhdCB3aWxsIHJ1biBjb3JyZWN0bHkgb24gd2ViIG9yIG5vZGVcblxuY29uc29sZS5sb2coQXV0b21lcmdlKVxubGV0IGRvYyA9IEF1dG9tZXJnZS5pbml0KClcbmRvYyA9IEF1dG9tZXJnZS5jaGFuZ2UoZG9jLCAoZDogYW55KSA9PiBkLmhlbGxvID0gXCJmcm9tIGF1dG9tZXJnZS1qc1wiKVxuY29uc29sZS5sb2coZG9jKVxuY29uc3QgcmVzdWx0ID0gSlNPTi5zdHJpbmdpZnkoZG9jKVxuXG5pZiAodHlwZW9mIGRvY3VtZW50ICE9PSAndW5kZWZpbmVkJykge1xuICAgIC8vIGJyb3dzZXJcbiAgICBjb25zdCBlbGVtZW50ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnZGl2Jyk7XG4gICAgZWxlbWVudC5pbm5lckhUTUwgPSBKU09OLnN0cmluZ2lmeShyZXN1bHQpXG4gICAgZG9jdW1lbnQuYm9keS5hcHBlbmRDaGlsZChlbGVtZW50KTtcbn0gZWxzZSB7XG4gICAgLy8gc2VydmVyXG4gICAgY29uc29sZS5sb2coXCJub2RlOlwiLCByZXN1bHQpXG59XG5cbiJdLCJtYXBwaW5ncyI6IkFBQUEsWUFBWSxlQUFlO0FBSTNCLFFBQVEsSUFBSSxTQUFTO0FBQ3JCLElBQUksTUFBTSxVQUFVLEtBQUs7QUFDekIsTUFBTSxVQUFVLE9BQU8sS0FBSyxDQUFDLE1BQVcsRUFBRSxRQUFRLG1CQUFtQjtBQUNyRSxRQUFRLElBQUksR0FBRztBQUNmLE1BQU0sU0FBUyxLQUFLLFVBQVUsR0FBRztBQUVqQyxJQUFJLE9BQU8sYUFBYSxhQUFhO0FBRWpDLFFBQU0sVUFBVSxTQUFTLGNBQWMsS0FBSztBQUM1QyxVQUFRLFlBQVksS0FBSyxVQUFVLE1BQU07QUFDekMsV0FBUyxLQUFLLFlBQVksT0FBTztBQUNyQyxPQUFPO0FBRUgsVUFBUSxJQUFJLFNBQVMsTUFBTTtBQUMvQjsiLCJuYW1lcyI6W119
|
||||
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi9ob21lL2FsZXgvUHJvamVjdHMvYXV0b21lcmdlL2F1dG9tZXJnZS1ycy9hdXRvbWVyZ2UtanMvZXhhbXBsZXMvdml0ZS9zcmMvbWFpbi50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBBdXRvbWVyZ2UgZnJvbSBcImF1dG9tZXJnZS1qc1wiXG5cbi8vIGhlbGxvIHdvcmxkIGNvZGUgdGhhdCB3aWxsIHJ1biBjb3JyZWN0bHkgb24gd2ViIG9yIG5vZGVcblxuY29uc29sZS5sb2coQXV0b21lcmdlKVxubGV0IGRvYyA9IEF1dG9tZXJnZS5pbml0KClcbmRvYyA9IEF1dG9tZXJnZS5jaGFuZ2UoZG9jLCAoZDogYW55KSA9PiBkLmhlbGxvID0gXCJmcm9tIGF1dG9tZXJnZS1qc1wiKVxuY29uc29sZS5sb2coZG9jKVxuY29uc3QgcmVzdWx0ID0gSlNPTi5zdHJpbmdpZnkoZG9jKVxuXG5pZiAodHlwZW9mIGRvY3VtZW50ICE9PSAndW5kZWZpbmVkJykge1xuICAgIC8vIGJyb3dzZXJcbiAgICBjb25zdCBlbGVtZW50ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnZGl2Jyk7XG4gICAgZWxlbWVudC5pbm5lckhUTUwgPSBKU09OLnN0cmluZ2lmeShyZXN1bHQpXG4gICAgZG9jdW1lbnQuYm9keS5hcHBlbmRDaGlsZChlbGVtZW50KTtcbn0gZWxzZSB7XG4gICAgLy8gc2VydmVyXG4gICAgY29uc29sZS5sb2coXCJub2RlOlwiLCByZXN1bHQpXG59XG5cbiJdLCJtYXBwaW5ncyI6IkFBQUEsWUFBWSxlQUFlO0FBSTNCLFFBQVEsSUFBSSxTQUFTO0FBQ3JCLElBQUksTUFBTSxVQUFVLEtBQUs7QUFDekIsTUFBTSxVQUFVLE9BQU8sS0FBSyxDQUFDLE1BQVcsRUFBRSxRQUFRLG1CQUFtQjtBQUNyRSxRQUFRLElBQUksR0FBRztBQUNmLE1BQU0sU0FBUyxLQUFLLFVBQVUsR0FBRztBQUVqQyxJQUFJLE9BQU8sYUFBYSxhQUFhO0FBRWpDLFFBQU0sVUFBVSxTQUFTLGNBQWMsS0FBSztBQUM1QyxVQUFRLFlBQVksS0FBSyxVQUFVLE1BQU07QUFDekMsV0FBUyxLQUFLLFlBQVksT0FBTztBQUNyQyxPQUFPO0FBRUgsVUFBUSxJQUFJLFNBQVMsTUFBTTtBQUMvQjsiLCJuYW1lcyI6W119
|
|
@ -9,7 +9,7 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@automerge/automerge": "2.0.0-alpha.7"
|
||||
"@automerge/automerge": "2.0.0-alpha.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^4.6.4",
|
||||
|
|
|
@ -4,6 +4,6 @@ export function setupCounter(element: HTMLButtonElement) {
|
|||
counter = count
|
||||
element.innerHTML = `count is ${counter}`
|
||||
}
|
||||
element.addEventListener("click", () => setCounter(++counter))
|
||||
element.addEventListener('click', () => setCounter(++counter))
|
||||
setCounter(0)
|
||||
}
|
||||
|
|
|
@ -3,15 +3,16 @@ import * as Automerge from "@automerge/automerge"
|
|||
// hello world code that will run correctly on web or node
|
||||
|
||||
let doc = Automerge.init()
|
||||
doc = Automerge.change(doc, (d: any) => (d.hello = "from automerge"))
|
||||
doc = Automerge.change(doc, (d: any) => d.hello = "from automerge")
|
||||
const result = JSON.stringify(doc)
|
||||
|
||||
if (typeof document !== "undefined") {
|
||||
// browser
|
||||
const element = document.createElement("div")
|
||||
element.innerHTML = JSON.stringify(result)
|
||||
document.body.appendChild(element)
|
||||
if (typeof document !== 'undefined') {
|
||||
// browser
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = JSON.stringify(result)
|
||||
document.body.appendChild(element);
|
||||
} else {
|
||||
// server
|
||||
console.log("node:", result)
|
||||
// server
|
||||
console.log("node:", result)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,20 +3,20 @@ import wasm from "vite-plugin-wasm"
|
|||
import topLevelAwait from "vite-plugin-top-level-await"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [topLevelAwait(), wasm()],
|
||||
|
||||
// This is only necessary if you are using `SharedWorker` or `WebWorker`, as
|
||||
// documented in https://vitejs.dev/guide/features.html#import-with-constructors
|
||||
worker: {
|
||||
format: "es",
|
||||
plugins: [topLevelAwait(), wasm()],
|
||||
},
|
||||
|
||||
optimizeDeps: {
|
||||
// This is necessary because otherwise `vite dev` includes two separate
|
||||
// versions of the JS wrapper. This causes problems because the JS
|
||||
// wrapper has a module level variable to track JS side heap
|
||||
// allocations, initializing this twice causes horrible breakage
|
||||
exclude: ["@automerge/automerge-wasm"],
|
||||
},
|
||||
// This is only necessary if you are using `SharedWorker` or `WebWorker`, as
|
||||
// documented in https://vitejs.dev/guide/features.html#import-with-constructors
|
||||
worker: {
|
||||
format: "es",
|
||||
plugins: [topLevelAwait(), wasm()]
|
||||
},
|
||||
|
||||
optimizeDeps: {
|
||||
// This is necessary because otherwise `vite dev` includes two separate
|
||||
// versions of the JS wrapper. This causes problems because the JS
|
||||
// wrapper has a module level variable to track JS side heap
|
||||
// allocations, initializing this twice causes horrible breakage
|
||||
exclude: ["@automerge/automerge-wasm"]
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,34 +1,36 @@
|
|||
# Webpack + Automerge
|
||||
|
||||
|
||||
Getting WASM working in webpack 5 is very easy. You just need to enable the
|
||||
`asyncWebAssembly`
|
||||
[experiment](https://webpack.js.org/configuration/experiments/). For example:
|
||||
|
||||
|
||||
```javascript
|
||||
const path = require("path")
|
||||
const path = require('path');
|
||||
|
||||
const clientConfig = {
|
||||
experiments: { asyncWebAssembly: true },
|
||||
target: "web",
|
||||
entry: "./src/index.js",
|
||||
target: 'web',
|
||||
entry: './src/index.js',
|
||||
output: {
|
||||
filename: "main.js",
|
||||
path: path.resolve(__dirname, "public"),
|
||||
filename: 'main.js',
|
||||
path: path.resolve(__dirname, 'public'),
|
||||
},
|
||||
mode: "development", // or production
|
||||
performance: {
|
||||
// we dont want the wasm blob to generate warnings
|
||||
hints: false,
|
||||
maxEntrypointSize: 512000,
|
||||
maxAssetSize: 512000,
|
||||
},
|
||||
}
|
||||
performance: { // we dont want the wasm blob to generate warnings
|
||||
hints: false,
|
||||
maxEntrypointSize: 512000,
|
||||
maxAssetSize: 512000
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = clientConfig
|
||||
```
|
||||
|
||||
## Running the example
|
||||
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
yarn start
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
},
|
||||
"author": "",
|
||||
"dependencies": {
|
||||
"@automerge/automerge": "2.0.0-alpha.7"
|
||||
"@automerge/automerge": "2.0.0-alpha.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"serve": "^13.0.2",
|
||||
|
|
|
@ -3,15 +3,16 @@ import * as Automerge from "@automerge/automerge"
|
|||
// hello world code that will run correctly on web or node
|
||||
|
||||
let doc = Automerge.init()
|
||||
doc = Automerge.change(doc, d => (d.hello = "from automerge"))
|
||||
doc = Automerge.change(doc, (d) => d.hello = "from automerge")
|
||||
const result = JSON.stringify(doc)
|
||||
|
||||
if (typeof document !== "undefined") {
|
||||
if (typeof document !== 'undefined') {
|
||||
// browser
|
||||
const element = document.createElement("div")
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = JSON.stringify(result)
|
||||
document.body.appendChild(element)
|
||||
document.body.appendChild(element);
|
||||
} else {
|
||||
// server
|
||||
console.log("node:", result)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,37 +1,36 @@
|
|||
const path = require("path")
|
||||
const nodeExternals = require("webpack-node-externals")
|
||||
const path = require('path');
|
||||
const nodeExternals = require('webpack-node-externals');
|
||||
|
||||
// the most basic webpack config for node or web targets for automerge-wasm
|
||||
|
||||
const serverConfig = {
|
||||
// basic setup for bundling a node package
|
||||
target: "node",
|
||||
target: 'node',
|
||||
externals: [nodeExternals()],
|
||||
externalsPresets: { node: true },
|
||||
|
||||
entry: "./src/index.js",
|
||||
entry: './src/index.js',
|
||||
output: {
|
||||
filename: "node.js",
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
filename: 'node.js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
},
|
||||
mode: "development", // or production
|
||||
}
|
||||
};
|
||||
|
||||
const clientConfig = {
|
||||
experiments: { asyncWebAssembly: true },
|
||||
target: "web",
|
||||
entry: "./src/index.js",
|
||||
target: 'web',
|
||||
entry: './src/index.js',
|
||||
output: {
|
||||
filename: "main.js",
|
||||
path: path.resolve(__dirname, "public"),
|
||||
filename: 'main.js',
|
||||
path: path.resolve(__dirname, 'public'),
|
||||
},
|
||||
mode: "development", // or production
|
||||
performance: {
|
||||
// we dont want the wasm blob to generate warnings
|
||||
hints: false,
|
||||
maxEntrypointSize: 512000,
|
||||
maxAssetSize: 512000,
|
||||
},
|
||||
}
|
||||
performance: { // we dont want the wasm blob to generate warnings
|
||||
hints: false,
|
||||
maxEntrypointSize: 512000,
|
||||
maxAssetSize: 512000
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = [serverConfig, clientConfig]
|
||||
module.exports = [serverConfig, clientConfig];
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"Orion Henry <orion@inkandswitch.com>",
|
||||
"Martin Kleppmann"
|
||||
],
|
||||
"version": "2.0.2",
|
||||
"version": "2.0.0-alpha.5",
|
||||
"description": "Javascript implementation of automerge, backed by @automerge/automerge-wasm",
|
||||
"homepage": "https://github.com/automerge/automerge-rs/tree/main/wrappers/javascript",
|
||||
"repository": "github:automerge/automerge-rs",
|
||||
|
@ -12,10 +12,26 @@
|
|||
"README.md",
|
||||
"LICENSE",
|
||||
"package.json",
|
||||
"dist/index.d.ts",
|
||||
"dist/cjs/**/*.js",
|
||||
"dist/mjs/**/*.js",
|
||||
"dist/*.d.ts"
|
||||
"index.d.ts",
|
||||
"dist/*.d.ts",
|
||||
"dist/cjs/constants.js",
|
||||
"dist/cjs/types.js",
|
||||
"dist/cjs/numbers.js",
|
||||
"dist/cjs/index.js",
|
||||
"dist/cjs/uuid.js",
|
||||
"dist/cjs/counter.js",
|
||||
"dist/cjs/low_level.js",
|
||||
"dist/cjs/text.js",
|
||||
"dist/cjs/proxies.js",
|
||||
"dist/mjs/constants.js",
|
||||
"dist/mjs/types.js",
|
||||
"dist/mjs/numbers.js",
|
||||
"dist/mjs/index.js",
|
||||
"dist/mjs/uuid.js",
|
||||
"dist/mjs/counter.js",
|
||||
"dist/mjs/low_level.js",
|
||||
"dist/mjs/text.js",
|
||||
"dist/mjs/proxies.js"
|
||||
],
|
||||
"types": "./dist/index.d.ts",
|
||||
"module": "./dist/mjs/index.js",
|
||||
|
@ -23,31 +39,26 @@
|
|||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "eslint src",
|
||||
"build": "tsc -p config/mjs.json && tsc -p config/cjs.json && tsc -p config/declonly.json --emitDeclarationOnly",
|
||||
"test": "ts-mocha test/*.ts",
|
||||
"deno:build": "denoify && node ./scripts/deno-prefixer.mjs",
|
||||
"deno:test": "deno test ./deno-tests/deno.ts --allow-read --allow-net",
|
||||
"watch-docs": "typedoc src/index.ts --watch --readme none"
|
||||
"build": "tsc -p config/mjs.json && tsc -p config/cjs.json && tsc --emitDeclarationOnly",
|
||||
"test": "ts-mocha test/*.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/expect": "^24.3.0",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.46.0",
|
||||
"@typescript-eslint/parser": "^5.46.0",
|
||||
"denoify": "^1.4.5",
|
||||
"eslint": "^8.29.0",
|
||||
"@types/mocha": "^9.1.1",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.25.0",
|
||||
"@typescript-eslint/parser": "^5.25.0",
|
||||
"eslint": "^8.15.0",
|
||||
"fast-sha256": "^1.3.0",
|
||||
"mocha": "^10.2.0",
|
||||
"pako": "^2.1.0",
|
||||
"prettier": "^2.8.1",
|
||||
"mocha": "^10.0.0",
|
||||
"pako": "^2.0.4",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typedoc": "^0.23.22",
|
||||
"typescript": "^4.9.4"
|
||||
"typedoc": "^0.23.16",
|
||||
"typescript": "^4.6.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@automerge/automerge-wasm": "0.1.25",
|
||||
"uuid": "^9.0.0"
|
||||
"@automerge/automerge-wasm": "0.1.10",
|
||||
"uuid": "^8.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import * as fs from "fs"
|
||||
|
||||
const files = ["./deno_dist/proxies.ts"]
|
||||
for (const filepath of files) {
|
||||
const data = fs.readFileSync(filepath)
|
||||
fs.writeFileSync(filepath, "// @ts-nocheck \n" + data)
|
||||
|
||||
console.log('Prepended "// @ts-nocheck" to ' + filepath)
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
// @denoify-ignore
|
||||
|
||||
import { makeThisModuleAnExecutableReplacer } from "denoify"
|
||||
// import { assert } from "tsafe";
|
||||
// import * as path from "path";
|
||||
|
||||
makeThisModuleAnExecutableReplacer(
|
||||
async ({ parsedImportExportStatement, destDirPath, version }) => {
|
||||
version = process.env.VERSION || version
|
||||
|
||||
switch (parsedImportExportStatement.parsedArgument.nodeModuleName) {
|
||||
case "@automerge/automerge-wasm":
|
||||
{
|
||||
const moduleRoot =
|
||||
process.env.ROOT_MODULE ||
|
||||
`https://deno.land/x/automerge_wasm@${version}`
|
||||
/*
|
||||
*We expect not to run against statements like
|
||||
*import(..).then(...)
|
||||
*or
|
||||
*export * from "..."
|
||||
*in our code.
|
||||
*/
|
||||
if (
|
||||
!parsedImportExportStatement.isAsyncImport &&
|
||||
(parsedImportExportStatement.statementType === "import" ||
|
||||
parsedImportExportStatement.statementType === "export")
|
||||
) {
|
||||
if (parsedImportExportStatement.isTypeOnly) {
|
||||
return `${parsedImportExportStatement.statementType} type ${parsedImportExportStatement.target} from "${moduleRoot}/index.d.ts";`
|
||||
} else {
|
||||
return `${parsedImportExportStatement.statementType} ${parsedImportExportStatement.target} from "${moduleRoot}/automerge_wasm.js";`
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
//The replacer should return undefined when we want to let denoify replace the statement
|
||||
return undefined
|
||||
}
|
||||
)
|
|
@ -1,100 +0,0 @@
|
|||
import { Counter, type AutomergeValue } from "./types"
|
||||
import { Text } from "./text"
|
||||
import { type AutomergeValue as UnstableAutomergeValue } from "./unstable_types"
|
||||
import { type Target, Text1Target, Text2Target } from "./proxies"
|
||||
import { mapProxy, listProxy, ValueType } from "./proxies"
|
||||
import type { Prop, ObjID } from "@automerge/automerge-wasm"
|
||||
import { Automerge } from "@automerge/automerge-wasm"
|
||||
|
||||
export type ConflictsF<T extends Target> = { [key: string]: ValueType<T> }
|
||||
export type Conflicts = ConflictsF<Text1Target>
|
||||
export type UnstableConflicts = ConflictsF<Text2Target>
|
||||
|
||||
export function stableConflictAt(
|
||||
context: Automerge,
|
||||
objectId: ObjID,
|
||||
prop: Prop
|
||||
): Conflicts | undefined {
|
||||
return conflictAt<Text1Target>(
|
||||
context,
|
||||
objectId,
|
||||
prop,
|
||||
true,
|
||||
(context: Automerge, conflictId: ObjID): AutomergeValue => {
|
||||
return new Text(context.text(conflictId))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function unstableConflictAt(
|
||||
context: Automerge,
|
||||
objectId: ObjID,
|
||||
prop: Prop
|
||||
): UnstableConflicts | undefined {
|
||||
return conflictAt<Text2Target>(
|
||||
context,
|
||||
objectId,
|
||||
prop,
|
||||
true,
|
||||
(context: Automerge, conflictId: ObjID): UnstableAutomergeValue => {
|
||||
return context.text(conflictId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function conflictAt<T extends Target>(
|
||||
context: Automerge,
|
||||
objectId: ObjID,
|
||||
prop: Prop,
|
||||
textV2: boolean,
|
||||
handleText: (a: Automerge, conflictId: ObjID) => ValueType<T>
|
||||
): ConflictsF<T> | undefined {
|
||||
const values = context.getAll(objectId, prop)
|
||||
if (values.length <= 1) {
|
||||
return
|
||||
}
|
||||
const result: ConflictsF<T> = {}
|
||||
for (const fullVal of values) {
|
||||
switch (fullVal[0]) {
|
||||
case "map":
|
||||
result[fullVal[1]] = mapProxy<T>(
|
||||
context,
|
||||
fullVal[1],
|
||||
textV2,
|
||||
[prop],
|
||||
true
|
||||
)
|
||||
break
|
||||
case "list":
|
||||
result[fullVal[1]] = listProxy<T>(
|
||||
context,
|
||||
fullVal[1],
|
||||
textV2,
|
||||
[prop],
|
||||
true
|
||||
)
|
||||
break
|
||||
case "text":
|
||||
result[fullVal[1]] = handleText(context, fullVal[1] as ObjID)
|
||||
break
|
||||
case "str":
|
||||
case "uint":
|
||||
case "int":
|
||||
case "f64":
|
||||
case "boolean":
|
||||
case "bytes":
|
||||
case "null":
|
||||
result[fullVal[2]] = fullVal[1] as ValueType<T>
|
||||
break
|
||||
case "counter":
|
||||
result[fullVal[2]] = new Counter(fullVal[1]) as ValueType<T>
|
||||
break
|
||||
case "timestamp":
|
||||
result[fullVal[2]] = new Date(fullVal[1]) as ValueType<T>
|
||||
break
|
||||
default:
|
||||
throw RangeError(`datatype ${fullVal[0]} unimplemented`)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
|
@ -1,12 +1,24 @@
|
|||
// Properties of the document root object
|
||||
//const OPTIONS = Symbol('_options') // object containing options passed to init()
|
||||
//const CACHE = Symbol('_cache') // map from objectId to immutable object
|
||||
//export const STATE = Symbol.for('_am_state') // object containing metadata about current state (e.g. sequence numbers)
|
||||
export const STATE = Symbol.for('_am_meta') // object containing metadata about current state (e.g. sequence numbers)
|
||||
export const HEADS = Symbol.for('_am_heads') // object containing metadata about current state (e.g. sequence numbers)
|
||||
export const TRACE = Symbol.for('_am_trace') // object containing metadata about current state (e.g. sequence numbers)
|
||||
export const OBJECT_ID = Symbol.for('_am_objectId') // object containing metadata about current state (e.g. sequence numbers)
|
||||
export const READ_ONLY = Symbol.for('_am_readOnly') // object containing metadata about current state (e.g. sequence numbers)
|
||||
export const FROZEN = Symbol.for('_am_frozen') // object containing metadata about current state (e.g. sequence numbers)
|
||||
|
||||
export const UINT = Symbol.for('_am_uint')
|
||||
export const INT = Symbol.for('_am_int')
|
||||
export const F64 = Symbol.for('_am_f64')
|
||||
export const COUNTER = Symbol.for('_am_counter')
|
||||
export const TEXT = Symbol.for('_am_text')
|
||||
|
||||
// Properties of all Automerge objects
|
||||
//const OBJECT_ID = Symbol('_objectId') // the object ID of the current object (string)
|
||||
//const CONFLICTS = Symbol('_conflicts') // map or list (depending on object type) of conflicts
|
||||
//const CHANGE = Symbol('_change') // the context object on proxy objects used in change callback
|
||||
//const ELEM_IDS = Symbol('_elemIds') // list containing the element ID of each list element
|
||||
|
||||
export const STATE = Symbol.for("_am_meta") // symbol used to hide application metadata on automerge objects
|
||||
export const TRACE = Symbol.for("_am_trace") // used for debugging
|
||||
export const OBJECT_ID = Symbol.for("_am_objectId") // symbol used to hide the object id on automerge objects
|
||||
export const IS_PROXY = Symbol.for("_am_isProxy") // symbol used to test if the document is a proxy object
|
||||
|
||||
export const UINT = Symbol.for("_am_uint")
|
||||
export const INT = Symbol.for("_am_int")
|
||||
export const F64 = Symbol.for("_am_f64")
|
||||
export const COUNTER = Symbol.for("_am_counter")
|
||||
export const TEXT = Symbol.for("_am_text")
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Automerge, type ObjID, type Prop } from "@automerge/automerge-wasm"
|
||||
import { Automerge, ObjID, Prop } from "@automerge/automerge-wasm"
|
||||
import { COUNTER } from "./constants"
|
||||
/**
|
||||
* The most basic CRDT: an integer value that can be changed only by
|
||||
|
@ -6,7 +6,7 @@ import { COUNTER } from "./constants"
|
|||
* the value trivially converges.
|
||||
*/
|
||||
export class Counter {
|
||||
value: number
|
||||
value : number;
|
||||
|
||||
constructor(value?: number) {
|
||||
this.value = value || 0
|
||||
|
@ -21,7 +21,7 @@ export class Counter {
|
|||
* concatenating it with another string, as in `x + ''`.
|
||||
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/valueOf
|
||||
*/
|
||||
valueOf(): number {
|
||||
valueOf() : number {
|
||||
return this.value
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ export class Counter {
|
|||
* this method is called e.g. when you do `['value: ', x].join('')` or when
|
||||
* you use string interpolation: `value: ${x}`.
|
||||
*/
|
||||
toString(): string {
|
||||
toString() : string {
|
||||
return this.valueOf().toString()
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,7 @@ export class Counter {
|
|||
* Returns the counter value, so that a JSON serialization of an Automerge
|
||||
* document represents the counter simply as an integer.
|
||||
*/
|
||||
toJSON(): number {
|
||||
toJSON() : number {
|
||||
return this.value
|
||||
}
|
||||
}
|
||||
|
@ -49,30 +49,24 @@ export class Counter {
|
|||
*/
|
||||
class WriteableCounter extends Counter {
|
||||
context: Automerge
|
||||
path: Prop[]
|
||||
path: string[]
|
||||
objectId: ObjID
|
||||
key: Prop
|
||||
|
||||
constructor(
|
||||
value: number,
|
||||
context: Automerge,
|
||||
path: Prop[],
|
||||
objectId: ObjID,
|
||||
key: Prop
|
||||
) {
|
||||
constructor(value: number, context: Automerge, path: string[], objectId: ObjID, key: Prop) {
|
||||
super(value)
|
||||
this.context = context
|
||||
this.path = path
|
||||
this.objectId = objectId
|
||||
this.key = key
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Increases the value of the counter by `delta`. If `delta` is not given,
|
||||
* increases the value of the counter by 1.
|
||||
*/
|
||||
increment(delta: number): number {
|
||||
delta = typeof delta === "number" ? delta : 1
|
||||
increment(delta: number) : number {
|
||||
delta = typeof delta === 'number' ? delta : 1
|
||||
this.context.increment(this.objectId, this.key, delta)
|
||||
this.value += delta
|
||||
return this.value
|
||||
|
@ -82,8 +76,8 @@ class WriteableCounter extends Counter {
|
|||
* Decreases the value of the counter by `delta`. If `delta` is not given,
|
||||
* decreases the value of the counter by 1.
|
||||
*/
|
||||
decrement(delta: number): number {
|
||||
return this.increment(typeof delta === "number" ? -delta : -1)
|
||||
decrement(delta: number) : number {
|
||||
return this.increment(typeof delta === 'number' ? -delta : -1)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,14 +87,8 @@ class WriteableCounter extends Counter {
|
|||
* `objectId` is the ID of the object containing the counter, and `key` is
|
||||
* the property name (key in map, or index in list) where the counter is
|
||||
* located.
|
||||
*/
|
||||
export function getWriteableCounter(
|
||||
value: number,
|
||||
context: Automerge,
|
||||
path: Prop[],
|
||||
objectId: ObjID,
|
||||
key: Prop
|
||||
): WriteableCounter {
|
||||
*/
|
||||
export function getWriteableCounter(value: number, context: Automerge, path: string[], objectId: ObjID, key: Prop) {
|
||||
return new WriteableCounter(value, context, path, objectId, key)
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,43 +0,0 @@
|
|||
import { type ObjID, type Heads, Automerge } from "@automerge/automerge-wasm"
|
||||
|
||||
import { STATE, OBJECT_ID, TRACE, IS_PROXY } from "./constants"
|
||||
|
||||
import type { Doc, PatchCallback } from "./types"
|
||||
|
||||
export interface InternalState<T> {
|
||||
handle: Automerge
|
||||
heads: Heads | undefined
|
||||
freeze: boolean
|
||||
patchCallback?: PatchCallback<T>
|
||||
textV2: boolean
|
||||
}
|
||||
|
||||
export function _state<T>(doc: Doc<T>, checkroot = true): InternalState<T> {
|
||||
if (typeof doc !== "object") {
|
||||
throw new RangeError("must be the document root")
|
||||
}
|
||||
const state = Reflect.get(doc, STATE) as InternalState<T>
|
||||
if (
|
||||
state === undefined ||
|
||||
state == null ||
|
||||
(checkroot && _obj(doc) !== "_root")
|
||||
) {
|
||||
throw new RangeError("must be the document root")
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
export function _trace<T>(doc: Doc<T>): string | undefined {
|
||||
return Reflect.get(doc, TRACE) as string
|
||||
}
|
||||
|
||||
export function _obj<T>(doc: Doc<T>): ObjID | null {
|
||||
if (!(typeof doc === "object") || doc === null) {
|
||||
return null
|
||||
}
|
||||
return Reflect.get(doc, OBJECT_ID) as ObjID
|
||||
}
|
||||
|
||||
export function _is_proxy<T>(doc: Doc<T>): boolean {
|
||||
return !!Reflect.get(doc, IS_PROXY)
|
||||
}
|
|
@ -1,58 +1,25 @@
|
|||
import {
|
||||
type API,
|
||||
Automerge,
|
||||
type Change,
|
||||
type DecodedChange,
|
||||
type Actor,
|
||||
SyncState,
|
||||
type SyncMessage,
|
||||
type JsSyncState,
|
||||
type DecodedSyncMessage,
|
||||
type ChangeToEncode,
|
||||
} from "@automerge/automerge-wasm"
|
||||
export type { ChangeToEncode } from "@automerge/automerge-wasm"
|
||||
|
||||
import { Automerge, Change, DecodedChange, Actor, SyncState, SyncMessage, JsSyncState, DecodedSyncMessage } from "@automerge/automerge-wasm"
|
||||
import { API } from "@automerge/automerge-wasm"
|
||||
|
||||
export function UseApi(api: API) {
|
||||
for (const k in api) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-extra-semi,@typescript-eslint/no-explicit-any
|
||||
;(ApiHandler as any)[k] = (api as any)[k]
|
||||
ApiHandler[k] = api[k]
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable */
|
||||
export const ApiHandler: API = {
|
||||
create(textV2: boolean, actor?: Actor): Automerge {
|
||||
throw new RangeError("Automerge.use() not called")
|
||||
},
|
||||
load(data: Uint8Array, textV2: boolean, actor?: Actor): Automerge {
|
||||
throw new RangeError("Automerge.use() not called (load)")
|
||||
},
|
||||
encodeChange(change: ChangeToEncode): Change {
|
||||
throw new RangeError("Automerge.use() not called (encodeChange)")
|
||||
},
|
||||
decodeChange(change: Change): DecodedChange {
|
||||
throw new RangeError("Automerge.use() not called (decodeChange)")
|
||||
},
|
||||
initSyncState(): SyncState {
|
||||
throw new RangeError("Automerge.use() not called (initSyncState)")
|
||||
},
|
||||
encodeSyncMessage(message: DecodedSyncMessage): SyncMessage {
|
||||
throw new RangeError("Automerge.use() not called (encodeSyncMessage)")
|
||||
},
|
||||
decodeSyncMessage(msg: SyncMessage): DecodedSyncMessage {
|
||||
throw new RangeError("Automerge.use() not called (decodeSyncMessage)")
|
||||
},
|
||||
encodeSyncState(state: SyncState): Uint8Array {
|
||||
throw new RangeError("Automerge.use() not called (encodeSyncState)")
|
||||
},
|
||||
decodeSyncState(data: Uint8Array): SyncState {
|
||||
throw new RangeError("Automerge.use() not called (decodeSyncState)")
|
||||
},
|
||||
exportSyncState(state: SyncState): JsSyncState {
|
||||
throw new RangeError("Automerge.use() not called (exportSyncState)")
|
||||
},
|
||||
importSyncState(state: JsSyncState): SyncState {
|
||||
throw new RangeError("Automerge.use() not called (importSyncState)")
|
||||
},
|
||||
export const ApiHandler : API = {
|
||||
create(actor?: Actor): Automerge { throw new RangeError("Automerge.use() not called") },
|
||||
load(data: Uint8Array, actor?: Actor): Automerge { throw new RangeError("Automerge.use() not called (load)") },
|
||||
encodeChange(change: DecodedChange): Change { throw new RangeError("Automerge.use() not called (encodeChange)") },
|
||||
decodeChange(change: Change): DecodedChange { throw new RangeError("Automerge.use() not called (decodeChange)") },
|
||||
initSyncState(): SyncState { throw new RangeError("Automerge.use() not called (initSyncState)") },
|
||||
encodeSyncMessage(message: DecodedSyncMessage): SyncMessage { throw new RangeError("Automerge.use() not called (encodeSyncMessage)") },
|
||||
decodeSyncMessage(msg: SyncMessage): DecodedSyncMessage { throw new RangeError("Automerge.use() not called (decodeSyncMessage)") },
|
||||
encodeSyncState(state: SyncState): Uint8Array { throw new RangeError("Automerge.use() not called (encodeSyncState)") },
|
||||
decodeSyncState(data: Uint8Array): SyncState { throw new RangeError("Automerge.use() not called (decodeSyncState)") },
|
||||
exportSyncState(state: SyncState): JsSyncState { throw new RangeError("Automerge.use() not called (exportSyncState)") },
|
||||
importSyncState(state: JsSyncState): SyncState { throw new RangeError("Automerge.use() not called (importSyncState)") },
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
|
|
@ -1,18 +1,12 @@
|
|||
// Convenience classes to allow users to strictly specify the number type they want
|
||||
// Convience classes to allow users to stricly specify the number type they want
|
||||
|
||||
import { INT, UINT, F64 } from "./constants"
|
||||
|
||||
export class Int {
|
||||
value: number
|
||||
value: number;
|
||||
|
||||
constructor(value: number) {
|
||||
if (
|
||||
!(
|
||||
Number.isInteger(value) &&
|
||||
value <= Number.MAX_SAFE_INTEGER &&
|
||||
value >= Number.MIN_SAFE_INTEGER
|
||||
)
|
||||
) {
|
||||
if (!(Number.isInteger(value) && value <= Number.MAX_SAFE_INTEGER && value >= Number.MIN_SAFE_INTEGER)) {
|
||||
throw new RangeError(`Value ${value} cannot be a uint`)
|
||||
}
|
||||
this.value = value
|
||||
|
@ -22,16 +16,10 @@ export class Int {
|
|||
}
|
||||
|
||||
export class Uint {
|
||||
value: number
|
||||
value: number;
|
||||
|
||||
constructor(value: number) {
|
||||
if (
|
||||
!(
|
||||
Number.isInteger(value) &&
|
||||
value <= Number.MAX_SAFE_INTEGER &&
|
||||
value >= 0
|
||||
)
|
||||
) {
|
||||
if (!(Number.isInteger(value) && value <= Number.MAX_SAFE_INTEGER && value >= 0)) {
|
||||
throw new RangeError(`Value ${value} cannot be a uint`)
|
||||
}
|
||||
this.value = value
|
||||
|
@ -41,10 +29,10 @@ export class Uint {
|
|||
}
|
||||
|
||||
export class Float64 {
|
||||
value: number
|
||||
value: number;
|
||||
|
||||
constructor(value: number) {
|
||||
if (typeof value !== "number") {
|
||||
if (typeof value !== 'number') {
|
||||
throw new RangeError(`Value ${value} cannot be a float64`)
|
||||
}
|
||||
this.value = value || 0.0
|
||||
|
@ -52,3 +40,4 @@ export class Float64 {
|
|||
Object.freeze(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,6 +0,0 @@
|
|||
export class RawString {
|
||||
val: string
|
||||
constructor(val: string) {
|
||||
this.val = val
|
||||
}
|
||||
}
|
|
@ -1,944 +0,0 @@
|
|||
/** @hidden **/
|
||||
export { /** @hidden */ uuid } from "./uuid"
|
||||
|
||||
import { rootProxy } from "./proxies"
|
||||
import { STATE } from "./constants"
|
||||
|
||||
import {
|
||||
type AutomergeValue,
|
||||
Counter,
|
||||
type Doc,
|
||||
type PatchCallback,
|
||||
} from "./types"
|
||||
export {
|
||||
type AutomergeValue,
|
||||
Counter,
|
||||
type Doc,
|
||||
Int,
|
||||
Uint,
|
||||
Float64,
|
||||
type Patch,
|
||||
type PatchCallback,
|
||||
type ScalarValue,
|
||||
} from "./types"
|
||||
|
||||
import { Text } from "./text"
|
||||
export { Text } from "./text"
|
||||
|
||||
import type {
|
||||
API as WasmAPI,
|
||||
Actor as ActorId,
|
||||
Prop,
|
||||
ObjID,
|
||||
Change,
|
||||
DecodedChange,
|
||||
Heads,
|
||||
MaterializeValue,
|
||||
JsSyncState,
|
||||
SyncMessage,
|
||||
DecodedSyncMessage,
|
||||
} from "@automerge/automerge-wasm"
|
||||
export type {
|
||||
PutPatch,
|
||||
DelPatch,
|
||||
SpliceTextPatch,
|
||||
InsertPatch,
|
||||
IncPatch,
|
||||
SyncMessage,
|
||||
} from "@automerge/automerge-wasm"
|
||||
|
||||
/** @hidden **/
|
||||
type API = WasmAPI
|
||||
|
||||
const SyncStateSymbol = Symbol("_syncstate")
|
||||
|
||||
/**
|
||||
* An opaque type tracking the state of sync with a remote peer
|
||||
*/
|
||||
type SyncState = JsSyncState & { _opaque: typeof SyncStateSymbol }
|
||||
|
||||
import { ApiHandler, type ChangeToEncode, UseApi } from "./low_level"
|
||||
|
||||
import { Automerge } from "@automerge/automerge-wasm"
|
||||
|
||||
import { RawString } from "./raw_string"
|
||||
|
||||
import { _state, _is_proxy, _trace, _obj } from "./internal_state"
|
||||
|
||||
import { stableConflictAt } from "./conflicts"
|
||||
|
||||
/** Options passed to {@link change}, and {@link emptyChange}
|
||||
* @typeParam T - The type of value contained in the document
|
||||
*/
|
||||
export type ChangeOptions<T> = {
|
||||
/** A message which describes the changes */
|
||||
message?: string
|
||||
/** The unix timestamp of the change (purely advisory, not used in conflict resolution) */
|
||||
time?: number
|
||||
/** A callback which will be called to notify the caller of any changes to the document */
|
||||
patchCallback?: PatchCallback<T>
|
||||
}
|
||||
|
||||
/** Options passed to {@link loadIncremental}, {@link applyChanges}, and {@link receiveSyncMessage}
|
||||
* @typeParam T - The type of value contained in the document
|
||||
*/
|
||||
export type ApplyOptions<T> = { patchCallback?: PatchCallback<T> }
|
||||
|
||||
/**
|
||||
* A List is an extended Array that adds the two helper methods `deleteAt` and `insertAt`.
|
||||
*/
|
||||
export interface List<T> extends Array<T> {
|
||||
insertAt(index: number, ...args: T[]): List<T>
|
||||
deleteAt(index: number, numDelete?: number): List<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* To extend an arbitrary type, we have to turn any arrays that are part of the type's definition into Lists.
|
||||
* So we recurse through the properties of T, turning any Arrays we find into Lists.
|
||||
*/
|
||||
export type Extend<T> =
|
||||
// is it an array? make it a list (we recursively extend the type of the array's elements as well)
|
||||
T extends Array<infer T>
|
||||
? List<Extend<T>>
|
||||
: // is it an object? recursively extend all of its properties
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
T extends Object
|
||||
? { [P in keyof T]: Extend<T[P]> }
|
||||
: // otherwise leave the type alone
|
||||
T
|
||||
|
||||
/**
|
||||
* Function which is called by {@link change} when making changes to a `Doc<T>`
|
||||
* @typeParam T - The type of value contained in the document
|
||||
*
|
||||
* This function may mutate `doc`
|
||||
*/
|
||||
export type ChangeFn<T> = (doc: Extend<T>) => void
|
||||
|
||||
/** @hidden **/
|
||||
export interface State<T> {
|
||||
change: DecodedChange
|
||||
snapshot: T
|
||||
}
|
||||
|
||||
/** @hidden **/
|
||||
export function use(api: API) {
|
||||
UseApi(api)
|
||||
}
|
||||
|
||||
import * as wasm from "@automerge/automerge-wasm"
|
||||
use(wasm)
|
||||
|
||||
/**
|
||||
* Options to be passed to {@link init} or {@link load}
|
||||
* @typeParam T - The type of the value the document contains
|
||||
*/
|
||||
export type InitOptions<T> = {
|
||||
/** The actor ID to use for this document, a random one will be generated if `null` is passed */
|
||||
actor?: ActorId
|
||||
freeze?: boolean
|
||||
/** A callback which will be called with the initial patch once the document has finished loading */
|
||||
patchCallback?: PatchCallback<T>
|
||||
/** @hidden */
|
||||
enableTextV2?: boolean
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
export function getBackend<T>(doc: Doc<T>): Automerge {
|
||||
return _state(doc).handle
|
||||
}
|
||||
|
||||
function importOpts<T>(_actor?: ActorId | InitOptions<T>): InitOptions<T> {
|
||||
if (typeof _actor === "object") {
|
||||
return _actor
|
||||
} else {
|
||||
return { actor: _actor }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new automerge document
|
||||
*
|
||||
* @typeParam T - The type of value contained in the document. This will be the
|
||||
* type that is passed to the change closure in {@link change}
|
||||
* @param _opts - Either an actorId or an {@link InitOptions} (which may
|
||||
* contain an actorId). If this is null the document will be initialised with a
|
||||
* random actor ID
|
||||
*/
|
||||
export function init<T>(_opts?: ActorId | InitOptions<T>): Doc<T> {
|
||||
const opts = importOpts(_opts)
|
||||
const freeze = !!opts.freeze
|
||||
const patchCallback = opts.patchCallback
|
||||
const handle = ApiHandler.create(opts.enableTextV2 || false, opts.actor)
|
||||
handle.enablePatches(true)
|
||||
handle.enableFreeze(!!opts.freeze)
|
||||
handle.registerDatatype("counter", (n: number) => new Counter(n))
|
||||
const textV2 = opts.enableTextV2 || false
|
||||
if (textV2) {
|
||||
handle.registerDatatype("str", (n: string) => new RawString(n))
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
handle.registerDatatype("text", (n: any) => new Text(n))
|
||||
}
|
||||
const doc = handle.materialize("/", undefined, {
|
||||
handle,
|
||||
heads: undefined,
|
||||
freeze,
|
||||
patchCallback,
|
||||
textV2,
|
||||
}) as Doc<T>
|
||||
return doc
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an immutable view of an automerge document as at `heads`
|
||||
*
|
||||
* @remarks
|
||||
* The document returned from this function cannot be passed to {@link change}.
|
||||
* This is because it shares the same underlying memory as `doc`, but it is
|
||||
* consequently a very cheap copy.
|
||||
*
|
||||
* Note that this function will throw an error if any of the hashes in `heads`
|
||||
* are not in the document.
|
||||
*
|
||||
* @typeParam T - The type of the value contained in the document
|
||||
* @param doc - The document to create a view of
|
||||
* @param heads - The hashes of the heads to create a view at
|
||||
*/
|
||||
export function view<T>(doc: Doc<T>, heads: Heads): Doc<T> {
|
||||
const state = _state(doc)
|
||||
const handle = state.handle
|
||||
return state.handle.materialize("/", heads, {
|
||||
...state,
|
||||
handle,
|
||||
heads,
|
||||
}) as Doc<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a full writable copy of an automerge document
|
||||
*
|
||||
* @remarks
|
||||
* Unlike {@link view} this function makes a full copy of the memory backing
|
||||
* the document and can thus be passed to {@link change}. It also generates a
|
||||
* new actor ID so that changes made in the new document do not create duplicate
|
||||
* sequence numbers with respect to the old document. If you need control over
|
||||
* the actor ID which is generated you can pass the actor ID as the second
|
||||
* argument
|
||||
*
|
||||
* @typeParam T - The type of the value contained in the document
|
||||
* @param doc - The document to clone
|
||||
* @param _opts - Either an actor ID to use for the new doc or an {@link InitOptions}
|
||||
*/
|
||||
export function clone<T>(
|
||||
doc: Doc<T>,
|
||||
_opts?: ActorId | InitOptions<T>
|
||||
): Doc<T> {
|
||||
const state = _state(doc)
|
||||
const heads = state.heads
|
||||
const opts = importOpts(_opts)
|
||||
const handle = state.handle.fork(opts.actor, heads)
|
||||
|
||||
// `change` uses the presence of state.heads to determine if we are in a view
|
||||
// set it to undefined to indicate that this is a full fat document
|
||||
const { heads: _oldHeads, ...stateSansHeads } = state
|
||||
return handle.applyPatches(doc, { ...stateSansHeads, handle })
|
||||
}
|
||||
|
||||
/** Explicity free the memory backing a document. Note that this is note
|
||||
* necessary in environments which support
|
||||
* [`FinalizationRegistry`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry)
|
||||
*/
|
||||
export function free<T>(doc: Doc<T>) {
|
||||
return _state(doc).handle.free()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an automerge document from a POJO
|
||||
*
|
||||
* @param initialState - The initial state which will be copied into the document
|
||||
* @typeParam T - The type of the value passed to `from` _and_ the type the resulting document will contain
|
||||
* @typeParam actor - The actor ID of the resulting document, if this is null a random actor ID will be used
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* const doc = automerge.from({
|
||||
* tasks: [
|
||||
* {description: "feed dogs", done: false}
|
||||
* ]
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function from<T extends Record<string, unknown>>(
|
||||
initialState: T | Doc<T>,
|
||||
_opts?: ActorId | InitOptions<T>
|
||||
): Doc<T> {
|
||||
return change(init(_opts), d => Object.assign(d, initialState))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the contents of an automerge document
|
||||
* @typeParam T - The type of the value contained in the document
|
||||
* @param doc - The document to update
|
||||
* @param options - Either a message, an {@link ChangeOptions}, or a {@link ChangeFn}
|
||||
* @param callback - A `ChangeFn` to be used if `options` was a `string`
|
||||
*
|
||||
* Note that if the second argument is a function it will be used as the `ChangeFn` regardless of what the third argument is.
|
||||
*
|
||||
* @example A simple change
|
||||
* ```
|
||||
* let doc1 = automerge.init()
|
||||
* doc1 = automerge.change(doc1, d => {
|
||||
* d.key = "value"
|
||||
* })
|
||||
* assert.equal(doc1.key, "value")
|
||||
* ```
|
||||
*
|
||||
* @example A change with a message
|
||||
*
|
||||
* ```
|
||||
* doc1 = automerge.change(doc1, "add another value", d => {
|
||||
* d.key2 = "value2"
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @example A change with a message and a timestamp
|
||||
*
|
||||
* ```
|
||||
* doc1 = automerge.change(doc1, {message: "add another value", time: 1640995200}, d => {
|
||||
* d.key2 = "value2"
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @example responding to a patch callback
|
||||
* ```
|
||||
* let patchedPath
|
||||
* let patchCallback = patch => {
|
||||
* patchedPath = patch.path
|
||||
* }
|
||||
* doc1 = automerge.change(doc1, {message, "add another value", time: 1640995200, patchCallback}, d => {
|
||||
* d.key2 = "value2"
|
||||
* })
|
||||
* assert.equal(patchedPath, ["key2"])
|
||||
* ```
|
||||
*/
|
||||
export function change<T>(
|
||||
doc: Doc<T>,
|
||||
options: string | ChangeOptions<T> | ChangeFn<T>,
|
||||
callback?: ChangeFn<T>
|
||||
): Doc<T> {
|
||||
if (typeof options === "function") {
|
||||
return _change(doc, {}, options)
|
||||
} else if (typeof callback === "function") {
|
||||
if (typeof options === "string") {
|
||||
options = { message: options }
|
||||
}
|
||||
return _change(doc, options, callback)
|
||||
} else {
|
||||
throw RangeError("Invalid args for change")
|
||||
}
|
||||
}
|
||||
|
||||
function progressDocument<T>(
|
||||
doc: Doc<T>,
|
||||
heads: Heads | null,
|
||||
callback?: PatchCallback<T>
|
||||
): Doc<T> {
|
||||
if (heads == null) {
|
||||
return doc
|
||||
}
|
||||
const state = _state(doc)
|
||||
const nextState = { ...state, heads: undefined }
|
||||
const nextDoc = state.handle.applyPatches(doc, nextState, callback)
|
||||
state.heads = heads
|
||||
return nextDoc
|
||||
}
|
||||
|
||||
function _change<T>(
|
||||
doc: Doc<T>,
|
||||
options: ChangeOptions<T>,
|
||||
callback: ChangeFn<T>
|
||||
): Doc<T> {
|
||||
if (typeof callback !== "function") {
|
||||
throw new RangeError("invalid change function")
|
||||
}
|
||||
|
||||
const state = _state(doc)
|
||||
|
||||
if (doc === undefined || state === undefined) {
|
||||
throw new RangeError("must be the document root")
|
||||
}
|
||||
if (state.heads) {
|
||||
throw new RangeError(
|
||||
"Attempting to change an outdated document. Use Automerge.clone() if you wish to make a writable copy."
|
||||
)
|
||||
}
|
||||
if (_is_proxy(doc)) {
|
||||
throw new RangeError("Calls to Automerge.change cannot be nested")
|
||||
}
|
||||
const heads = state.handle.getHeads()
|
||||
try {
|
||||
state.heads = heads
|
||||
const root: T = rootProxy(state.handle, state.textV2)
|
||||
callback(root as Extend<T>)
|
||||
if (state.handle.pendingOps() === 0) {
|
||||
state.heads = undefined
|
||||
return doc
|
||||
} else {
|
||||
state.handle.commit(options.message, options.time)
|
||||
return progressDocument(
|
||||
doc,
|
||||
heads,
|
||||
options.patchCallback || state.patchCallback
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
state.heads = undefined
|
||||
state.handle.rollback()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a change to a document which does not modify the document
|
||||
*
|
||||
* @param doc - The doc to add the empty change to
|
||||
* @param options - Either a message or a {@link ChangeOptions} for the new change
|
||||
*
|
||||
* Why would you want to do this? One reason might be that you have merged
|
||||
* changes from some other peers and you want to generate a change which
|
||||
* depends on those merged changes so that you can sign the new change with all
|
||||
* of the merged changes as part of the new change.
|
||||
*/
|
||||
export function emptyChange<T>(
|
||||
doc: Doc<T>,
|
||||
options: string | ChangeOptions<T> | void
|
||||
) {
|
||||
if (options === undefined) {
|
||||
options = {}
|
||||
}
|
||||
if (typeof options === "string") {
|
||||
options = { message: options }
|
||||
}
|
||||
|
||||
const state = _state(doc)
|
||||
|
||||
if (state.heads) {
|
||||
throw new RangeError(
|
||||
"Attempting to change an outdated document. Use Automerge.clone() if you wish to make a writable copy."
|
||||
)
|
||||
}
|
||||
if (_is_proxy(doc)) {
|
||||
throw new RangeError("Calls to Automerge.change cannot be nested")
|
||||
}
|
||||
|
||||
const heads = state.handle.getHeads()
|
||||
state.handle.emptyChange(options.message, options.time)
|
||||
return progressDocument(doc, heads)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an automerge document from a compressed document produce by {@link save}
|
||||
*
|
||||
* @typeParam T - The type of the value which is contained in the document.
|
||||
* Note that no validation is done to make sure this type is in
|
||||
* fact the type of the contained value so be a bit careful
|
||||
* @param data - The compressed document
|
||||
* @param _opts - Either an actor ID or some {@link InitOptions}, if the actor
|
||||
* ID is null a random actor ID will be created
|
||||
*
|
||||
* Note that `load` will throw an error if passed incomplete content (for
|
||||
* example if you are receiving content over the network and don't know if you
|
||||
* have the complete document yet). If you need to handle incomplete content use
|
||||
* {@link init} followed by {@link loadIncremental}.
|
||||
*/
|
||||
export function load<T>(
|
||||
data: Uint8Array,
|
||||
_opts?: ActorId | InitOptions<T>
|
||||
): Doc<T> {
|
||||
const opts = importOpts(_opts)
|
||||
const actor = opts.actor
|
||||
const patchCallback = opts.patchCallback
|
||||
const handle = ApiHandler.load(data, opts.enableTextV2 || false, actor)
|
||||
handle.enablePatches(true)
|
||||
handle.enableFreeze(!!opts.freeze)
|
||||
handle.registerDatatype("counter", (n: number) => new Counter(n))
|
||||
const textV2 = opts.enableTextV2 || false
|
||||
if (textV2) {
|
||||
handle.registerDatatype("str", (n: string) => new RawString(n))
|
||||
} else {
|
||||
handle.registerDatatype("text", (n: string) => new Text(n))
|
||||
}
|
||||
const doc = handle.materialize("/", undefined, {
|
||||
handle,
|
||||
heads: undefined,
|
||||
patchCallback,
|
||||
textV2,
|
||||
}) as Doc<T>
|
||||
return doc
|
||||
}
|
||||
|
||||
/**
|
||||
* Load changes produced by {@link saveIncremental}, or partial changes
|
||||
*
|
||||
* @typeParam T - The type of the value which is contained in the document.
|
||||
* Note that no validation is done to make sure this type is in
|
||||
* fact the type of the contained value so be a bit careful
|
||||
* @param data - The compressedchanges
|
||||
* @param opts - an {@link ApplyOptions}
|
||||
*
|
||||
* This function is useful when staying up to date with a connected peer.
|
||||
* Perhaps the other end sent you a full compresed document which you loaded
|
||||
* with {@link load} and they're sending you the result of
|
||||
* {@link getLastLocalChange} every time they make a change.
|
||||
*
|
||||
* Note that this function will succesfully load the results of {@link save} as
|
||||
* well as {@link getLastLocalChange} or any other incremental change.
|
||||
*/
|
||||
export function loadIncremental<T>(
|
||||
doc: Doc<T>,
|
||||
data: Uint8Array,
|
||||
opts?: ApplyOptions<T>
|
||||
): Doc<T> {
|
||||
if (!opts) {
|
||||
opts = {}
|
||||
}
|
||||
const state = _state(doc)
|
||||
if (state.heads) {
|
||||
throw new RangeError(
|
||||
"Attempting to change an out of date document - set at: " + _trace(doc)
|
||||
)
|
||||
}
|
||||
if (_is_proxy(doc)) {
|
||||
throw new RangeError("Calls to Automerge.change cannot be nested")
|
||||
}
|
||||
const heads = state.handle.getHeads()
|
||||
state.handle.loadIncremental(data)
|
||||
return progressDocument(doc, heads, opts.patchCallback || state.patchCallback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the contents of a document to a compressed format
|
||||
*
|
||||
* @param doc - The doc to save
|
||||
*
|
||||
* The returned bytes can be passed to {@link load} or {@link loadIncremental}
|
||||
*/
|
||||
export function save<T>(doc: Doc<T>): Uint8Array {
|
||||
return _state(doc).handle.save()
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge `local` into `remote`
|
||||
* @typeParam T - The type of values contained in each document
|
||||
* @param local - The document to merge changes into
|
||||
* @param remote - The document to merge changes from
|
||||
*
|
||||
* @returns - The merged document
|
||||
*
|
||||
* Often when you are merging documents you will also need to clone them. Both
|
||||
* arguments to `merge` are frozen after the call so you can no longer call
|
||||
* mutating methods (such as {@link change}) on them. The symtom of this will be
|
||||
* an error which says "Attempting to change an out of date document". To
|
||||
* overcome this call {@link clone} on the argument before passing it to {@link
|
||||
* merge}.
|
||||
*/
|
||||
export function merge<T>(local: Doc<T>, remote: Doc<T>): Doc<T> {
|
||||
const localState = _state(local)
|
||||
|
||||
if (localState.heads) {
|
||||
throw new RangeError(
|
||||
"Attempting to change an out of date document - set at: " + _trace(local)
|
||||
)
|
||||
}
|
||||
const heads = localState.handle.getHeads()
|
||||
const remoteState = _state(remote)
|
||||
const changes = localState.handle.getChangesAdded(remoteState.handle)
|
||||
localState.handle.applyChanges(changes)
|
||||
return progressDocument(local, heads, localState.patchCallback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actor ID associated with the document
|
||||
*/
|
||||
export function getActorId<T>(doc: Doc<T>): ActorId {
|
||||
const state = _state(doc)
|
||||
return state.handle.getActorId()
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of conflicts for particular key or index
|
||||
*
|
||||
* Maps and sequences in automerge can contain conflicting values for a
|
||||
* particular key or index. In this case {@link getConflicts} can be used to
|
||||
* obtain a `Conflicts` representing the multiple values present for the property
|
||||
*
|
||||
* A `Conflicts` is a map from a unique (per property or index) key to one of
|
||||
* the possible conflicting values for the given property.
|
||||
*/
|
||||
type Conflicts = { [key: string]: AutomergeValue }
|
||||
|
||||
/**
|
||||
* Get the conflicts associated with a property
|
||||
*
|
||||
* The values of properties in a map in automerge can be conflicted if there
|
||||
* are concurrent "put" operations to the same key. Automerge chooses one value
|
||||
* arbitrarily (but deterministically, any two nodes who have the same set of
|
||||
* changes will choose the same value) from the set of conflicting values to
|
||||
* present as the value of the key.
|
||||
*
|
||||
* Sometimes you may want to examine these conflicts, in this case you can use
|
||||
* {@link getConflicts} to get the conflicts for the key.
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* import * as automerge from "@automerge/automerge"
|
||||
*
|
||||
* type Profile = {
|
||||
* pets: Array<{name: string, type: string}>
|
||||
* }
|
||||
*
|
||||
* let doc1 = automerge.init<Profile>("aaaa")
|
||||
* doc1 = automerge.change(doc1, d => {
|
||||
* d.pets = [{name: "Lassie", type: "dog"}]
|
||||
* })
|
||||
* let doc2 = automerge.init<Profile>("bbbb")
|
||||
* doc2 = automerge.merge(doc2, automerge.clone(doc1))
|
||||
*
|
||||
* doc2 = automerge.change(doc2, d => {
|
||||
* d.pets[0].name = "Beethoven"
|
||||
* })
|
||||
*
|
||||
* doc1 = automerge.change(doc1, d => {
|
||||
* d.pets[0].name = "Babe"
|
||||
* })
|
||||
*
|
||||
* const doc3 = automerge.merge(doc1, doc2)
|
||||
*
|
||||
* // Note that here we pass `doc3.pets`, not `doc3`
|
||||
* let conflicts = automerge.getConflicts(doc3.pets[0], "name")
|
||||
*
|
||||
* // The two conflicting values are the keys of the conflicts object
|
||||
* assert.deepEqual(Object.values(conflicts), ["Babe", Beethoven"])
|
||||
* ```
|
||||
*/
|
||||
export function getConflicts<T>(
|
||||
doc: Doc<T>,
|
||||
prop: Prop
|
||||
): Conflicts | undefined {
|
||||
const state = _state(doc, false)
|
||||
if (state.textV2) {
|
||||
throw new Error("use unstable.getConflicts for an unstable document")
|
||||
}
|
||||
const objectId = _obj(doc)
|
||||
if (objectId != null) {
|
||||
return stableConflictAt(state.handle, objectId, prop)
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the binary representation of the last change which was made to this doc
|
||||
*
|
||||
* This is most useful when staying in sync with other peers, every time you
|
||||
* make a change locally via {@link change} you immediately call {@link
|
||||
* getLastLocalChange} and send the result over the network to other peers.
|
||||
*/
|
||||
export function getLastLocalChange<T>(doc: Doc<T>): Change | undefined {
|
||||
const state = _state(doc)
|
||||
return state.handle.getLastLocalChange() || undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the object ID of an arbitrary javascript value
|
||||
*
|
||||
* This is useful to determine if something is actually an automerge document,
|
||||
* if `doc` is not an automerge document this will return null.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function getObjectId(doc: any, prop?: Prop): ObjID | null {
|
||||
if (prop) {
|
||||
const state = _state(doc, false)
|
||||
const objectId = _obj(doc)
|
||||
if (!state || !objectId) {
|
||||
return null
|
||||
}
|
||||
return state.handle.get(objectId, prop) as ObjID
|
||||
} else {
|
||||
return _obj(doc)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the changes which are in `newState` but not in `oldState`. The returned
|
||||
* changes can be loaded in `oldState` via {@link applyChanges}.
|
||||
*
|
||||
* Note that this will crash if there are changes in `oldState` which are not in `newState`.
|
||||
*/
|
||||
export function getChanges<T>(oldState: Doc<T>, newState: Doc<T>): Change[] {
|
||||
const n = _state(newState)
|
||||
return n.handle.getChanges(getHeads(oldState))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the changes in a document
|
||||
*
|
||||
* This is different to {@link save} because the output is an array of changes
|
||||
* which can be individually applied via {@link applyChanges}`
|
||||
*
|
||||
*/
|
||||
export function getAllChanges<T>(doc: Doc<T>): Change[] {
|
||||
const state = _state(doc)
|
||||
return state.handle.getChanges([])
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply changes received from another document
|
||||
*
|
||||
* `doc` will be updated to reflect the `changes`. If there are changes which
|
||||
* we do not have dependencies for yet those will be stored in the document and
|
||||
* applied when the depended on changes arrive.
|
||||
*
|
||||
* You can use the {@link ApplyOptions} to pass a patchcallback which will be
|
||||
* informed of any changes which occur as a result of applying the changes
|
||||
*
|
||||
*/
|
||||
export function applyChanges<T>(
|
||||
doc: Doc<T>,
|
||||
changes: Change[],
|
||||
opts?: ApplyOptions<T>
|
||||
): [Doc<T>] {
|
||||
const state = _state(doc)
|
||||
if (!opts) {
|
||||
opts = {}
|
||||
}
|
||||
if (state.heads) {
|
||||
throw new RangeError(
|
||||
"Attempting to change an outdated document. Use Automerge.clone() if you wish to make a writable copy."
|
||||
)
|
||||
}
|
||||
if (_is_proxy(doc)) {
|
||||
throw new RangeError("Calls to Automerge.change cannot be nested")
|
||||
}
|
||||
const heads = state.handle.getHeads()
|
||||
state.handle.applyChanges(changes)
|
||||
state.heads = heads
|
||||
return [
|
||||
progressDocument(doc, heads, opts.patchCallback || state.patchCallback),
|
||||
]
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
export function getHistory<T>(doc: Doc<T>): State<T>[] {
|
||||
const textV2 = _state(doc).textV2
|
||||
const history = getAllChanges(doc)
|
||||
return history.map((change, index) => ({
|
||||
get change() {
|
||||
return decodeChange(change)
|
||||
},
|
||||
get snapshot() {
|
||||
const [state] = applyChanges(
|
||||
init({ enableTextV2: textV2 }),
|
||||
history.slice(0, index + 1)
|
||||
)
|
||||
return <T>state
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
// FIXME : no tests
|
||||
// FIXME can we just use deep equals now?
|
||||
export function equals(val1: unknown, val2: unknown): boolean {
|
||||
if (!isObject(val1) || !isObject(val2)) return val1 === val2
|
||||
const keys1 = Object.keys(val1).sort(),
|
||||
keys2 = Object.keys(val2).sort()
|
||||
if (keys1.length !== keys2.length) return false
|
||||
for (let i = 0; i < keys1.length; i++) {
|
||||
if (keys1[i] !== keys2[i]) return false
|
||||
if (!equals(val1[keys1[i]], val2[keys2[i]])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* encode a {@link SyncState} into binary to send over the network
|
||||
*
|
||||
* @group sync
|
||||
* */
|
||||
export function encodeSyncState(state: SyncState): Uint8Array {
|
||||
const sync = ApiHandler.importSyncState(state)
|
||||
const result = ApiHandler.encodeSyncState(sync)
|
||||
sync.free()
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode some binary data into a {@link SyncState}
|
||||
*
|
||||
* @group sync
|
||||
*/
|
||||
export function decodeSyncState(state: Uint8Array): SyncState {
|
||||
const sync = ApiHandler.decodeSyncState(state)
|
||||
const result = ApiHandler.exportSyncState(sync)
|
||||
sync.free()
|
||||
return result as SyncState
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a sync message to send to the peer represented by `inState`
|
||||
* @param doc - The doc to generate messages about
|
||||
* @param inState - The {@link SyncState} representing the peer we are talking to
|
||||
*
|
||||
* @group sync
|
||||
*
|
||||
* @returns An array of `[newSyncState, syncMessage | null]` where
|
||||
* `newSyncState` should replace `inState` and `syncMessage` should be sent to
|
||||
* the peer if it is not null. If `syncMessage` is null then we are up to date.
|
||||
*/
|
||||
export function generateSyncMessage<T>(
|
||||
doc: Doc<T>,
|
||||
inState: SyncState
|
||||
): [SyncState, SyncMessage | null] {
|
||||
const state = _state(doc)
|
||||
const syncState = ApiHandler.importSyncState(inState)
|
||||
const message = state.handle.generateSyncMessage(syncState)
|
||||
const outState = ApiHandler.exportSyncState(syncState) as SyncState
|
||||
return [outState, message]
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a document and our sync state on receiving a sync message
|
||||
*
|
||||
* @group sync
|
||||
*
|
||||
* @param doc - The doc the sync message is about
|
||||
* @param inState - The {@link SyncState} for the peer we are communicating with
|
||||
* @param message - The message which was received
|
||||
* @param opts - Any {@link ApplyOption}s, used for passing a
|
||||
* {@link PatchCallback} which will be informed of any changes
|
||||
* in `doc` which occur because of the received sync message.
|
||||
*
|
||||
* @returns An array of `[newDoc, newSyncState, syncMessage | null]` where
|
||||
* `newDoc` is the updated state of `doc`, `newSyncState` should replace
|
||||
* `inState` and `syncMessage` should be sent to the peer if it is not null. If
|
||||
* `syncMessage` is null then we are up to date.
|
||||
*/
|
||||
export function receiveSyncMessage<T>(
|
||||
doc: Doc<T>,
|
||||
inState: SyncState,
|
||||
message: SyncMessage,
|
||||
opts?: ApplyOptions<T>
|
||||
): [Doc<T>, SyncState, null] {
|
||||
const syncState = ApiHandler.importSyncState(inState)
|
||||
if (!opts) {
|
||||
opts = {}
|
||||
}
|
||||
const state = _state(doc)
|
||||
if (state.heads) {
|
||||
throw new RangeError(
|
||||
"Attempting to change an outdated document. Use Automerge.clone() if you wish to make a writable copy."
|
||||
)
|
||||
}
|
||||
if (_is_proxy(doc)) {
|
||||
throw new RangeError("Calls to Automerge.change cannot be nested")
|
||||
}
|
||||
const heads = state.handle.getHeads()
|
||||
state.handle.receiveSyncMessage(syncState, message)
|
||||
const outSyncState = ApiHandler.exportSyncState(syncState) as SyncState
|
||||
return [
|
||||
progressDocument(doc, heads, opts.patchCallback || state.patchCallback),
|
||||
outSyncState,
|
||||
null,
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new, blank {@link SyncState}
|
||||
*
|
||||
* When communicating with a peer for the first time use this to generate a new
|
||||
* {@link SyncState} for them
|
||||
*
|
||||
* @group sync
|
||||
*/
|
||||
export function initSyncState(): SyncState {
|
||||
return ApiHandler.exportSyncState(ApiHandler.initSyncState()) as SyncState
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
export function encodeChange(change: ChangeToEncode): Change {
|
||||
return ApiHandler.encodeChange(change)
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
export function decodeChange(data: Change): DecodedChange {
|
||||
return ApiHandler.decodeChange(data)
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
export function encodeSyncMessage(message: DecodedSyncMessage): SyncMessage {
|
||||
return ApiHandler.encodeSyncMessage(message)
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
export function decodeSyncMessage(message: SyncMessage): DecodedSyncMessage {
|
||||
return ApiHandler.decodeSyncMessage(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get any changes in `doc` which are not dependencies of `heads`
|
||||
*/
|
||||
export function getMissingDeps<T>(doc: Doc<T>, heads: Heads): Heads {
|
||||
const state = _state(doc)
|
||||
return state.handle.getMissingDeps(heads)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the hashes of the heads of this document
|
||||
*/
|
||||
export function getHeads<T>(doc: Doc<T>): Heads {
|
||||
const state = _state(doc)
|
||||
return state.heads || state.handle.getHeads()
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
export function dump<T>(doc: Doc<T>) {
|
||||
const state = _state(doc)
|
||||
state.handle.dump()
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
export function toJS<T>(doc: Doc<T>): T {
|
||||
const state = _state(doc)
|
||||
const enabled = state.handle.enableFreeze(false)
|
||||
const result = state.handle.materialize()
|
||||
state.handle.enableFreeze(enabled)
|
||||
return result as T
|
||||
}
|
||||
|
||||
export function isAutomerge(doc: unknown): boolean {
|
||||
if (typeof doc == "object" && doc !== null) {
|
||||
return getObjectId(doc) === "_root" && !!Reflect.get(doc, STATE)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function isObject(obj: unknown): obj is Record<string, unknown> {
|
||||
return typeof obj === "object" && obj !== null
|
||||
}
|
||||
|
||||
export type {
|
||||
API,
|
||||
SyncState,
|
||||
ActorId,
|
||||
Conflicts,
|
||||
Prop,
|
||||
Change,
|
||||
ObjID,
|
||||
DecodedChange,
|
||||
DecodedSyncMessage,
|
||||
Heads,
|
||||
MaterializeValue,
|
||||
}
|
|
@ -1,18 +1,13 @@
|
|||
import type { Value } from "@automerge/automerge-wasm"
|
||||
import { Value } from "@automerge/automerge-wasm"
|
||||
import { TEXT, STATE } from "./constants"
|
||||
import type { InternalState } from "./internal_state"
|
||||
|
||||
export class Text {
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
elems: Array<any>
|
||||
elems: Value[]
|
||||
str: string | undefined
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
spans: Array<any> | undefined;
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[STATE]?: InternalState<any>
|
||||
spans: Value[] | undefined
|
||||
|
||||
constructor(text?: string | string[] | Value[]) {
|
||||
if (typeof text === "string") {
|
||||
constructor (text?: string | string[] | Value[]) {
|
||||
if (typeof text === 'string') {
|
||||
this.elems = [...text]
|
||||
} else if (Array.isArray(text)) {
|
||||
this.elems = text
|
||||
|
@ -24,12 +19,11 @@ export class Text {
|
|||
Reflect.defineProperty(this, TEXT, { value: true })
|
||||
}
|
||||
|
||||
get length(): number {
|
||||
get length () : number {
|
||||
return this.elems.length
|
||||
}
|
||||
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
get(index: number): any {
|
||||
get (index: number) : Value | undefined {
|
||||
return this.elems[index]
|
||||
}
|
||||
|
||||
|
@ -37,18 +31,18 @@ export class Text {
|
|||
* Iterates over the text elements character by character, including any
|
||||
* inline objects.
|
||||
*/
|
||||
[Symbol.iterator]() {
|
||||
[Symbol.iterator] () {
|
||||
const elems = this.elems
|
||||
let index = -1
|
||||
return {
|
||||
next() {
|
||||
next () {
|
||||
index += 1
|
||||
if (index < elems.length) {
|
||||
return { done: false, value: elems[index] }
|
||||
return {done: false, value: elems[index]}
|
||||
} else {
|
||||
return { done: true }
|
||||
return {done: true}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,15 +50,15 @@ export class Text {
|
|||
* Returns the content of the Text object as a simple string, ignoring any
|
||||
* non-character elements.
|
||||
*/
|
||||
toString(): string {
|
||||
toString() : string {
|
||||
if (!this.str) {
|
||||
// Concatting to a string is faster than creating an array and then
|
||||
// .join()ing for small (<100KB) arrays.
|
||||
// https://jsperf.com/join-vs-loop-w-type-test
|
||||
this.str = ""
|
||||
this.str = ''
|
||||
for (const elem of this.elems) {
|
||||
if (typeof elem === "string") this.str += elem
|
||||
else this.str += "\uFFFC"
|
||||
if (typeof elem === 'string') this.str += elem
|
||||
else this.str += '\uFFFC'
|
||||
}
|
||||
}
|
||||
return this.str
|
||||
|
@ -74,20 +68,20 @@ export class Text {
|
|||
* Returns the content of the Text object as a sequence of strings,
|
||||
* interleaved with non-character elements.
|
||||
*
|
||||
* For example, the value `['a', 'b', {x: 3}, 'c', 'd']` has spans:
|
||||
* `=> ['ab', {x: 3}, 'cd']`
|
||||
* For example, the value ['a', 'b', {x: 3}, 'c', 'd'] has spans:
|
||||
* => ['ab', {x: 3}, 'cd']
|
||||
*/
|
||||
toSpans(): Array<Value | object> {
|
||||
toSpans() : Value[] {
|
||||
if (!this.spans) {
|
||||
this.spans = []
|
||||
let chars = ""
|
||||
let chars = ''
|
||||
for (const elem of this.elems) {
|
||||
if (typeof elem === "string") {
|
||||
if (typeof elem === 'string') {
|
||||
chars += elem
|
||||
} else {
|
||||
if (chars.length > 0) {
|
||||
this.spans.push(chars)
|
||||
chars = ""
|
||||
chars = ''
|
||||
}
|
||||
this.spans.push(elem)
|
||||
}
|
||||
|
@ -103,18 +97,16 @@ export class Text {
|
|||
* Returns the content of the Text object as a simple string, so that the
|
||||
* JSON serialization of an Automerge document represents text nicely.
|
||||
*/
|
||||
toJSON(): string {
|
||||
toJSON() : string {
|
||||
return this.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the list item at position `index` to a new value `value`.
|
||||
*/
|
||||
set(index: number, value: Value) {
|
||||
set (index: number, value: Value) {
|
||||
if (this[STATE]) {
|
||||
throw new RangeError(
|
||||
"object cannot be modified outside of a change block"
|
||||
)
|
||||
throw new RangeError("object cannot be modified outside of a change block")
|
||||
}
|
||||
this.elems[index] = value
|
||||
}
|
||||
|
@ -122,13 +114,11 @@ export class Text {
|
|||
/**
|
||||
* Inserts new list items `values` starting at position `index`.
|
||||
*/
|
||||
insertAt(index: number, ...values: Array<Value | object>) {
|
||||
insertAt(index: number, ...values: Value[]) {
|
||||
if (this[STATE]) {
|
||||
throw new RangeError(
|
||||
"object cannot be modified outside of a change block"
|
||||
)
|
||||
throw new RangeError("object cannot be modified outside of a change block")
|
||||
}
|
||||
this.elems.splice(index, 0, ...values)
|
||||
this.elems.splice(index, 0, ... values)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -137,14 +127,12 @@ export class Text {
|
|||
*/
|
||||
deleteAt(index: number, numDelete = 1) {
|
||||
if (this[STATE]) {
|
||||
throw new RangeError(
|
||||
"object cannot be modified outside of a change block"
|
||||
)
|
||||
throw new RangeError("object cannot be modified outside of a change block")
|
||||
}
|
||||
this.elems.splice(index, numDelete)
|
||||
}
|
||||
|
||||
map<T>(callback: (e: Value | object) => T) {
|
||||
map<T>(callback: (e: Value) => T) {
|
||||
this.elems.map(callback)
|
||||
}
|
||||
|
||||
|
@ -152,31 +140,31 @@ export class Text {
|
|||
this.elems.lastIndexOf(searchElement, fromIndex)
|
||||
}
|
||||
|
||||
concat(other: Text): Text {
|
||||
concat(other: Text) : Text {
|
||||
return new Text(this.elems.concat(other.elems))
|
||||
}
|
||||
|
||||
every(test: (v: Value) => boolean): boolean {
|
||||
every(test: (Value) => boolean) : boolean {
|
||||
return this.elems.every(test)
|
||||
}
|
||||
|
||||
filter(test: (v: Value) => boolean): Text {
|
||||
filter(test: (Value) => boolean) : Text {
|
||||
return new Text(this.elems.filter(test))
|
||||
}
|
||||
|
||||
find(test: (v: Value) => boolean): Value | undefined {
|
||||
find(test: (Value) => boolean) : Value | undefined {
|
||||
return this.elems.find(test)
|
||||
}
|
||||
|
||||
findIndex(test: (v: Value) => boolean): number | undefined {
|
||||
findIndex(test: (Value) => boolean) : number | undefined {
|
||||
return this.elems.findIndex(test)
|
||||
}
|
||||
|
||||
forEach(f: (v: Value) => undefined) {
|
||||
forEach(f: (Value) => undefined) {
|
||||
this.elems.forEach(f)
|
||||
}
|
||||
|
||||
includes(elem: Value): boolean {
|
||||
includes(elem: Value) : boolean {
|
||||
return this.elems.includes(elem)
|
||||
}
|
||||
|
||||
|
@ -184,37 +172,23 @@ export class Text {
|
|||
return this.elems.indexOf(elem)
|
||||
}
|
||||
|
||||
join(sep?: string): string {
|
||||
join(sep?: string) : string{
|
||||
return this.elems.join(sep)
|
||||
}
|
||||
|
||||
reduce(
|
||||
f: (
|
||||
previousValue: Value,
|
||||
currentValue: Value,
|
||||
currentIndex: number,
|
||||
array: Value[]
|
||||
) => Value
|
||||
) {
|
||||
reduce(f: (previousValue: Value, currentValue: Value, currentIndex: number, array: Value[]) => Value) {
|
||||
this.elems.reduce(f)
|
||||
}
|
||||
|
||||
reduceRight(
|
||||
f: (
|
||||
previousValue: Value,
|
||||
currentValue: Value,
|
||||
currentIndex: number,
|
||||
array: Value[]
|
||||
) => Value
|
||||
) {
|
||||
reduceRight(f: (previousValue: Value, currentValue: Value, currentIndex: number, array: Value[]) => Value) {
|
||||
this.elems.reduceRight(f)
|
||||
}
|
||||
|
||||
slice(start?: number, end?: number) {
|
||||
new Text(this.elems.slice(start, end))
|
||||
new Text(this.elems.slice(start,end))
|
||||
}
|
||||
|
||||
some(test: (arg: Value) => boolean): boolean {
|
||||
some(test: (Value) => boolean) : boolean {
|
||||
return this.elems.some(test)
|
||||
}
|
||||
|
||||
|
@ -222,3 +196,4 @@ export class Text {
|
|||
this.toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,46 +1,13 @@
|
|||
export { Text } from "./text"
|
||||
|
||||
import { Text } from "./text"
|
||||
export { Counter } from "./counter"
|
||||
export { Int, Uint, Float64 } from "./numbers"
|
||||
export { Text } from "./text"
|
||||
export { Counter } from "./counter"
|
||||
export { Int, Uint, Float64 } from "./numbers"
|
||||
|
||||
import { Counter } from "./counter"
|
||||
import type { Patch } from "@automerge/automerge-wasm"
|
||||
export type { Patch } from "@automerge/automerge-wasm"
|
||||
|
||||
export type AutomergeValue =
|
||||
| ScalarValue
|
||||
| { [key: string]: AutomergeValue }
|
||||
| Array<AutomergeValue>
|
||||
| Text
|
||||
export type MapValue = { [key: string]: AutomergeValue }
|
||||
export type ListValue = Array<AutomergeValue>
|
||||
export type ScalarValue =
|
||||
| string
|
||||
| number
|
||||
| null
|
||||
| boolean
|
||||
| Date
|
||||
| Counter
|
||||
| Uint8Array
|
||||
|
||||
/**
|
||||
* An automerge document.
|
||||
* @typeParam T - The type of the value contained in this document
|
||||
*
|
||||
* Note that this provides read only access to the fields of the value. To
|
||||
* modify the value use {@link change}
|
||||
*/
|
||||
export type Doc<T> = { readonly [P in keyof T]: T[P] }
|
||||
|
||||
/**
|
||||
* Callback which is called by various methods in this library to notify the
|
||||
* user of what changes have been made.
|
||||
* @param patch - A description of the changes made
|
||||
* @param before - The document before the change was made
|
||||
* @param after - The document after the change was made
|
||||
*/
|
||||
export type PatchCallback<T> = (
|
||||
patches: Array<Patch>,
|
||||
before: Doc<T>,
|
||||
after: Doc<T>
|
||||
) => void
|
||||
export type AutomergeValue = ScalarValue | { [key: string]: AutomergeValue } | Array<AutomergeValue> | Text
|
||||
export type MapValue = { [key: string]: AutomergeValue }
|
||||
export type ListValue = Array<AutomergeValue>
|
||||
export type TextValue = Array<AutomergeValue>
|
||||
export type ScalarValue = string | number | null | boolean | Date | Counter | Uint8Array
|
||||
|
|
|
@ -1,294 +0,0 @@
|
|||
/**
|
||||
* # The unstable API
|
||||
*
|
||||
* This module contains new features we are working on which are either not yet
|
||||
* ready for a stable release and/or which will result in backwards incompatible
|
||||
* API changes. The API of this module may change in arbitrary ways between
|
||||
* point releases - we will always document what these changes are in the
|
||||
* [CHANGELOG](#changelog) below, but only depend on this module if you are prepared to deal
|
||||
* with frequent changes.
|
||||
*
|
||||
* ## Differences from stable
|
||||
*
|
||||
* In the stable API text objects are represented using the {@link Text} class.
|
||||
* This means you must decide up front whether your string data might need
|
||||
* concurrent merges in the future and if you change your mind you have to
|
||||
* figure out how to migrate your data. In the unstable API the `Text` class is
|
||||
* gone and all `string`s are represented using the text CRDT, allowing for
|
||||
* concurrent changes. Modifying a string is done using the {@link splice}
|
||||
* function. You can still access the old behaviour of strings which do not
|
||||
* support merging behaviour via the {@link RawString} class.
|
||||
*
|
||||
* This leads to the following differences from `stable`:
|
||||
*
|
||||
* * There is no `unstable.Text` class, all strings are text objects
|
||||
* * Reading strings in an `unstable` document is the same as reading any other
|
||||
* javascript string
|
||||
* * To modify strings in an `unstable` document use {@link splice}
|
||||
* * The {@link AutomergeValue} type does not include the {@link Text}
|
||||
* class but the {@link RawString} class is included in the {@link ScalarValue}
|
||||
* type
|
||||
*
|
||||
* ## CHANGELOG
|
||||
* * Introduce this module to expose the new API which has no `Text` class
|
||||
*
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
export {
|
||||
Counter,
|
||||
type Doc,
|
||||
Int,
|
||||
Uint,
|
||||
Float64,
|
||||
type Patch,
|
||||
type PatchCallback,
|
||||
type AutomergeValue,
|
||||
type ScalarValue,
|
||||
} from "./unstable_types"
|
||||
|
||||
import type { PatchCallback } from "./stable"
|
||||
|
||||
import { type UnstableConflicts as Conflicts } from "./conflicts"
|
||||
import { unstableConflictAt } from "./conflicts"
|
||||
|
||||
export type {
|
||||
PutPatch,
|
||||
DelPatch,
|
||||
SpliceTextPatch,
|
||||
InsertPatch,
|
||||
IncPatch,
|
||||
SyncMessage,
|
||||
} from "@automerge/automerge-wasm"
|
||||
|
||||
export type { ChangeOptions, ApplyOptions, ChangeFn } from "./stable"
|
||||
export {
|
||||
view,
|
||||
free,
|
||||
getHeads,
|
||||
change,
|
||||
emptyChange,
|
||||
loadIncremental,
|
||||
save,
|
||||
merge,
|
||||
getActorId,
|
||||
getLastLocalChange,
|
||||
getChanges,
|
||||
getAllChanges,
|
||||
applyChanges,
|
||||
getHistory,
|
||||
equals,
|
||||
encodeSyncState,
|
||||
decodeSyncState,
|
||||
generateSyncMessage,
|
||||
receiveSyncMessage,
|
||||
initSyncState,
|
||||
encodeChange,
|
||||
decodeChange,
|
||||
encodeSyncMessage,
|
||||
decodeSyncMessage,
|
||||
getMissingDeps,
|
||||
dump,
|
||||
toJS,
|
||||
isAutomerge,
|
||||
getObjectId,
|
||||
} from "./stable"
|
||||
|
||||
export type InitOptions<T> = {
|
||||
/** The actor ID to use for this document, a random one will be generated if `null` is passed */
|
||||
actor?: ActorId
|
||||
freeze?: boolean
|
||||
/** A callback which will be called with the initial patch once the document has finished loading */
|
||||
patchCallback?: PatchCallback<T>
|
||||
}
|
||||
|
||||
import { ActorId, Doc } from "./stable"
|
||||
import * as stable from "./stable"
|
||||
export { RawString } from "./raw_string"
|
||||
|
||||
/** @hidden */
|
||||
export const getBackend = stable.getBackend
|
||||
|
||||
import { _is_proxy, _state, _obj } from "./internal_state"
|
||||
|
||||
/**
|
||||
* Create a new automerge document
|
||||
*
|
||||
* @typeParam T - The type of value contained in the document. This will be the
|
||||
* type that is passed to the change closure in {@link change}
|
||||
* @param _opts - Either an actorId or an {@link InitOptions} (which may
|
||||
* contain an actorId). If this is null the document will be initialised with a
|
||||
* random actor ID
|
||||
*/
|
||||
export function init<T>(_opts?: ActorId | InitOptions<T>): Doc<T> {
|
||||
const opts = importOpts(_opts)
|
||||
opts.enableTextV2 = true
|
||||
return stable.init(opts)
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a full writable copy of an automerge document
|
||||
*
|
||||
* @remarks
|
||||
* Unlike {@link view} this function makes a full copy of the memory backing
|
||||
* the document and can thus be passed to {@link change}. It also generates a
|
||||
* new actor ID so that changes made in the new document do not create duplicate
|
||||
* sequence numbers with respect to the old document. If you need control over
|
||||
* the actor ID which is generated you can pass the actor ID as the second
|
||||
* argument
|
||||
*
|
||||
* @typeParam T - The type of the value contained in the document
|
||||
* @param doc - The document to clone
|
||||
* @param _opts - Either an actor ID to use for the new doc or an {@link InitOptions}
|
||||
*/
|
||||
export function clone<T>(
|
||||
doc: Doc<T>,
|
||||
_opts?: ActorId | InitOptions<T>
|
||||
): Doc<T> {
|
||||
const opts = importOpts(_opts)
|
||||
opts.enableTextV2 = true
|
||||
return stable.clone(doc, opts)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an automerge document from a POJO
|
||||
*
|
||||
* @param initialState - The initial state which will be copied into the document
|
||||
* @typeParam T - The type of the value passed to `from` _and_ the type the resulting document will contain
|
||||
* @typeParam actor - The actor ID of the resulting document, if this is null a random actor ID will be used
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* const doc = automerge.from({
|
||||
* tasks: [
|
||||
* {description: "feed dogs", done: false}
|
||||
* ]
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function from<T extends Record<string, unknown>>(
|
||||
initialState: T | Doc<T>,
|
||||
_opts?: ActorId | InitOptions<T>
|
||||
): Doc<T> {
|
||||
const opts = importOpts(_opts)
|
||||
opts.enableTextV2 = true
|
||||
return stable.from(initialState, opts)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an automerge document from a compressed document produce by {@link save}
|
||||
*
|
||||
* @typeParam T - The type of the value which is contained in the document.
|
||||
* Note that no validation is done to make sure this type is in
|
||||
* fact the type of the contained value so be a bit careful
|
||||
* @param data - The compressed document
|
||||
* @param _opts - Either an actor ID or some {@link InitOptions}, if the actor
|
||||
* ID is null a random actor ID will be created
|
||||
*
|
||||
* Note that `load` will throw an error if passed incomplete content (for
|
||||
* example if you are receiving content over the network and don't know if you
|
||||
* have the complete document yet). If you need to handle incomplete content use
|
||||
* {@link init} followed by {@link loadIncremental}.
|
||||
*/
|
||||
export function load<T>(
|
||||
data: Uint8Array,
|
||||
_opts?: ActorId | InitOptions<T>
|
||||
): Doc<T> {
|
||||
const opts = importOpts(_opts)
|
||||
opts.enableTextV2 = true
|
||||
return stable.load(data, opts)
|
||||
}
|
||||
|
||||
function importOpts<T>(
|
||||
_actor?: ActorId | InitOptions<T>
|
||||
): stable.InitOptions<T> {
|
||||
if (typeof _actor === "object") {
|
||||
return _actor
|
||||
} else {
|
||||
return { actor: _actor }
|
||||
}
|
||||
}
|
||||
|
||||
export function splice<T>(
|
||||
doc: Doc<T>,
|
||||
prop: stable.Prop,
|
||||
index: number,
|
||||
del: number,
|
||||
newText?: string
|
||||
) {
|
||||
if (!_is_proxy(doc)) {
|
||||
throw new RangeError("object cannot be modified outside of a change block")
|
||||
}
|
||||
const state = _state(doc, false)
|
||||
const objectId = _obj(doc)
|
||||
if (!objectId) {
|
||||
throw new RangeError("invalid object for splice")
|
||||
}
|
||||
const value = `${objectId}/${prop}`
|
||||
try {
|
||||
return state.handle.splice(value, index, del, newText)
|
||||
} catch (e) {
|
||||
throw new RangeError(`Cannot splice: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the conflicts associated with a property
|
||||
*
|
||||
* The values of properties in a map in automerge can be conflicted if there
|
||||
* are concurrent "put" operations to the same key. Automerge chooses one value
|
||||
* arbitrarily (but deterministically, any two nodes who have the same set of
|
||||
* changes will choose the same value) from the set of conflicting values to
|
||||
* present as the value of the key.
|
||||
*
|
||||
* Sometimes you may want to examine these conflicts, in this case you can use
|
||||
* {@link getConflicts} to get the conflicts for the key.
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* import * as automerge from "@automerge/automerge"
|
||||
*
|
||||
* type Profile = {
|
||||
* pets: Array<{name: string, type: string}>
|
||||
* }
|
||||
*
|
||||
* let doc1 = automerge.init<Profile>("aaaa")
|
||||
* doc1 = automerge.change(doc1, d => {
|
||||
* d.pets = [{name: "Lassie", type: "dog"}]
|
||||
* })
|
||||
* let doc2 = automerge.init<Profile>("bbbb")
|
||||
* doc2 = automerge.merge(doc2, automerge.clone(doc1))
|
||||
*
|
||||
* doc2 = automerge.change(doc2, d => {
|
||||
* d.pets[0].name = "Beethoven"
|
||||
* })
|
||||
*
|
||||
* doc1 = automerge.change(doc1, d => {
|
||||
* d.pets[0].name = "Babe"
|
||||
* })
|
||||
*
|
||||
* const doc3 = automerge.merge(doc1, doc2)
|
||||
*
|
||||
* // Note that here we pass `doc3.pets`, not `doc3`
|
||||
* let conflicts = automerge.getConflicts(doc3.pets[0], "name")
|
||||
*
|
||||
* // The two conflicting values are the keys of the conflicts object
|
||||
* assert.deepEqual(Object.values(conflicts), ["Babe", Beethoven"])
|
||||
* ```
|
||||
*/
|
||||
export function getConflicts<T>(
|
||||
doc: Doc<T>,
|
||||
prop: stable.Prop
|
||||
): Conflicts | undefined {
|
||||
const state = _state(doc, false)
|
||||
if (!state.textV2) {
|
||||
throw new Error("use getConflicts for a stable document")
|
||||
}
|
||||
const objectId = _obj(doc)
|
||||
if (objectId != null) {
|
||||
return unstableConflictAt(state.handle, objectId, prop)
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
import { Counter } from "./types"
|
||||
|
||||
export {
|
||||
Counter,
|
||||
type Doc,
|
||||
Int,
|
||||
Uint,
|
||||
Float64,
|
||||
type Patch,
|
||||
type PatchCallback,
|
||||
} from "./types"
|
||||
|
||||
import { RawString } from "./raw_string"
|
||||
export { RawString } from "./raw_string"
|
||||
|
||||
export type AutomergeValue =
|
||||
| ScalarValue
|
||||
| { [key: string]: AutomergeValue }
|
||||
| Array<AutomergeValue>
|
||||
export type MapValue = { [key: string]: AutomergeValue }
|
||||
export type ListValue = Array<AutomergeValue>
|
||||
export type ScalarValue =
|
||||
| string
|
||||
| number
|
||||
| null
|
||||
| boolean
|
||||
| Date
|
||||
| Counter
|
||||
| Uint8Array
|
||||
| RawString
|
|
@ -1,26 +0,0 @@
|
|||
import * as v4 from "https://deno.land/x/uuid@v0.1.2/mod.ts"
|
||||
|
||||
// this file is a deno only port of the uuid module
|
||||
|
||||
function defaultFactory() {
|
||||
return v4.uuid().replace(/-/g, "")
|
||||
}
|
||||
|
||||
let factory = defaultFactory
|
||||
|
||||
interface UUIDFactory extends Function {
|
||||
setFactory(f: typeof factory): void
|
||||
reset(): void
|
||||
}
|
||||
|
||||
export const uuid: UUIDFactory = () => {
|
||||
return factory()
|
||||
}
|
||||
|
||||
uuid.setFactory = newFactory => {
|
||||
factory = newFactory
|
||||
}
|
||||
|
||||
uuid.reset = () => {
|
||||
factory = defaultFactory
|
||||
}
|
|
@ -1,24 +1,21 @@
|
|||
import { v4 } from "uuid"
|
||||
import { v4 } from 'uuid'
|
||||
|
||||
function defaultFactory() {
|
||||
return v4().replace(/-/g, "")
|
||||
return v4().replace(/-/g, '')
|
||||
}
|
||||
|
||||
let factory = defaultFactory
|
||||
|
||||
interface UUIDFactory extends Function {
|
||||
setFactory(f: typeof factory): void
|
||||
reset(): void
|
||||
setFactory(f: typeof factory): void;
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
export const uuid: UUIDFactory = () => {
|
||||
export const uuid : UUIDFactory = () => {
|
||||
return factory()
|
||||
}
|
||||
|
||||
uuid.setFactory = newFactory => {
|
||||
factory = newFactory
|
||||
}
|
||||
uuid.setFactory = newFactory => { factory = newFactory }
|
||||
|
||||
uuid.reset = () => { factory = defaultFactory }
|
||||
|
||||
uuid.reset = () => {
|
||||
factory = defaultFactory
|
||||
}
|
||||
|
|
|
@ -1,488 +1,267 @@
|
|||
import * as assert from "assert"
|
||||
import { unstable as Automerge } from "../src"
|
||||
import * as WASM from "@automerge/automerge-wasm"
|
||||
import * as assert from 'assert'
|
||||
import {Counter} from 'automerge'
|
||||
import * as Automerge from '../src'
|
||||
|
||||
describe("Automerge", () => {
|
||||
describe("basics", () => {
|
||||
it("should init clone and free", () => {
|
||||
let doc1 = Automerge.init()
|
||||
let doc2 = Automerge.clone(doc1)
|
||||
|
||||
// this is only needed if weakrefs are not supported
|
||||
Automerge.free(doc1)
|
||||
Automerge.free(doc2)
|
||||
})
|
||||
|
||||
it("should be able to make a view with specifc heads", () => {
|
||||
let doc1 = Automerge.init<any>()
|
||||
let doc2 = Automerge.change(doc1, d => (d.value = 1))
|
||||
let heads2 = Automerge.getHeads(doc2)
|
||||
let doc3 = Automerge.change(doc2, d => (d.value = 2))
|
||||
let doc2_v2 = Automerge.view(doc3, heads2)
|
||||
assert.deepEqual(doc2, doc2_v2)
|
||||
let doc2_v2_clone = Automerge.clone(doc2, "aabbcc")
|
||||
assert.deepEqual(doc2, doc2_v2_clone)
|
||||
assert.equal(Automerge.getActorId(doc2_v2_clone), "aabbcc")
|
||||
})
|
||||
|
||||
it("should allow you to change a clone of a view", () => {
|
||||
let doc1 = Automerge.init<any>()
|
||||
doc1 = Automerge.change(doc1, d => (d.key = "value"))
|
||||
let heads = Automerge.getHeads(doc1)
|
||||
doc1 = Automerge.change(doc1, d => (d.key = "value2"))
|
||||
let fork = Automerge.clone(Automerge.view(doc1, heads))
|
||||
assert.deepEqual(fork, { key: "value" })
|
||||
fork = Automerge.change(fork, d => (d.key = "value3"))
|
||||
assert.deepEqual(fork, { key: "value3" })
|
||||
})
|
||||
|
||||
it("handle basic set and read on root object", () => {
|
||||
let doc1 = Automerge.init<any>()
|
||||
let doc2 = Automerge.change(doc1, d => {
|
||||
d.hello = "world"
|
||||
d.big = "little"
|
||||
d.zip = "zop"
|
||||
d.app = "dap"
|
||||
assert.deepEqual(d, {
|
||||
hello: "world",
|
||||
big: "little",
|
||||
zip: "zop",
|
||||
app: "dap",
|
||||
describe('Automerge', () => {
|
||||
describe('basics', () => {
|
||||
it('should init clone and free', () => {
|
||||
let doc1 = Automerge.init()
|
||||
let doc2 = Automerge.clone(doc1);
|
||||
})
|
||||
})
|
||||
assert.deepEqual(doc2, {
|
||||
hello: "world",
|
||||
big: "little",
|
||||
zip: "zop",
|
||||
app: "dap",
|
||||
})
|
||||
})
|
||||
|
||||
it("should be able to insert and delete a large number of properties", () => {
|
||||
let doc = Automerge.init<any>()
|
||||
|
||||
doc = Automerge.change(doc, doc => {
|
||||
doc["k1"] = true
|
||||
})
|
||||
|
||||
for (let idx = 1; idx <= 200; idx++) {
|
||||
doc = Automerge.change(doc, doc => {
|
||||
delete doc["k" + idx]
|
||||
doc["k" + (idx + 1)] = true
|
||||
assert(Object.keys(doc).length == 1)
|
||||
it('handle basic set and read on root object', () => {
|
||||
let doc1 = Automerge.init()
|
||||
let doc2 = Automerge.change(doc1, (d) => {
|
||||
d.hello = "world"
|
||||
d.big = "little"
|
||||
d.zip = "zop"
|
||||
d.app = "dap"
|
||||
assert.deepEqual(d, { hello: "world", big: "little", zip: "zop", app: "dap" })
|
||||
})
|
||||
assert.deepEqual(doc2, { hello: "world", big: "little", zip: "zop", app: "dap" })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it("can detect an automerge doc with isAutomerge()", () => {
|
||||
const doc1 = Automerge.from({ sub: { object: true } })
|
||||
assert(Automerge.isAutomerge(doc1))
|
||||
assert(!Automerge.isAutomerge(doc1.sub))
|
||||
assert(!Automerge.isAutomerge("String"))
|
||||
assert(!Automerge.isAutomerge({ sub: { object: true } }))
|
||||
assert(!Automerge.isAutomerge(undefined))
|
||||
const jsObj = Automerge.toJS(doc1)
|
||||
assert(!Automerge.isAutomerge(jsObj))
|
||||
assert.deepEqual(jsObj, doc1)
|
||||
})
|
||||
it('handle basic sets over many changes', () => {
|
||||
let doc1 = Automerge.init()
|
||||
let timestamp = new Date();
|
||||
let counter = new Automerge.Counter(100);
|
||||
let bytes = new Uint8Array([10,11,12]);
|
||||
let doc2 = Automerge.change(doc1, (d) => {
|
||||
d.hello = "world"
|
||||
})
|
||||
let doc3 = Automerge.change(doc2, (d) => {
|
||||
d.counter1 = counter
|
||||
})
|
||||
let doc4 = Automerge.change(doc3, (d) => {
|
||||
d.timestamp1 = timestamp
|
||||
})
|
||||
let doc5 = Automerge.change(doc4, (d) => {
|
||||
d.app = null
|
||||
})
|
||||
let doc6 = Automerge.change(doc5, (d) => {
|
||||
d.bytes1 = bytes
|
||||
})
|
||||
let doc7 = Automerge.change(doc6, (d) => {
|
||||
d.uint = new Automerge.Uint(1)
|
||||
d.int = new Automerge.Int(-1)
|
||||
d.float64 = new Automerge.Float64(5.5)
|
||||
d.number1 = 100
|
||||
d.number2 = -45.67
|
||||
d.true = true
|
||||
d.false = false
|
||||
})
|
||||
|
||||
it("it should recursively freeze the document if requested", () => {
|
||||
let doc1 = Automerge.init<any>({ freeze: true })
|
||||
let doc2 = Automerge.init<any>()
|
||||
assert.deepEqual(doc7, { hello: "world", true: true, false: false, int: -1, uint: 1, float64: 5.5, number1: 100, number2: -45.67, counter1: counter, timestamp1: timestamp, bytes1: bytes, app: null })
|
||||
|
||||
assert(Object.isFrozen(doc1))
|
||||
assert(!Object.isFrozen(doc2))
|
||||
|
||||
// will also freeze sub objects
|
||||
doc1 = Automerge.change(
|
||||
doc1,
|
||||
doc => (doc.book = { title: "how to win friends" })
|
||||
)
|
||||
doc2 = Automerge.merge(doc2, doc1)
|
||||
assert(Object.isFrozen(doc1))
|
||||
assert(Object.isFrozen(doc1.book))
|
||||
assert(!Object.isFrozen(doc2))
|
||||
assert(!Object.isFrozen(doc2.book))
|
||||
|
||||
// works on from
|
||||
let doc3 = Automerge.from({ sub: { obj: "inner" } }, { freeze: true })
|
||||
assert(Object.isFrozen(doc3))
|
||||
assert(Object.isFrozen(doc3.sub))
|
||||
|
||||
// works on load
|
||||
let doc4 = Automerge.load<any>(Automerge.save(doc3), { freeze: true })
|
||||
assert(Object.isFrozen(doc4))
|
||||
assert(Object.isFrozen(doc4.sub))
|
||||
|
||||
// follows clone
|
||||
let doc5 = Automerge.clone(doc4)
|
||||
assert(Object.isFrozen(doc5))
|
||||
assert(Object.isFrozen(doc5.sub))
|
||||
|
||||
// toJS does not freeze
|
||||
let exported = Automerge.toJS(doc5)
|
||||
assert(!Object.isFrozen(exported))
|
||||
})
|
||||
|
||||
it("handle basic sets over many changes", () => {
|
||||
let doc1 = Automerge.init<any>()
|
||||
let timestamp = new Date()
|
||||
let counter = new Automerge.Counter(100)
|
||||
let bytes = new Uint8Array([10, 11, 12])
|
||||
let doc2 = Automerge.change(doc1, d => {
|
||||
d.hello = "world"
|
||||
})
|
||||
let doc3 = Automerge.change(doc2, d => {
|
||||
d.counter1 = counter
|
||||
})
|
||||
let doc4 = Automerge.change(doc3, d => {
|
||||
d.timestamp1 = timestamp
|
||||
})
|
||||
let doc5 = Automerge.change(doc4, d => {
|
||||
d.app = null
|
||||
})
|
||||
let doc6 = Automerge.change(doc5, d => {
|
||||
d.bytes1 = bytes
|
||||
})
|
||||
let doc7 = Automerge.change(doc6, d => {
|
||||
d.uint = new Automerge.Uint(1)
|
||||
d.int = new Automerge.Int(-1)
|
||||
d.float64 = new Automerge.Float64(5.5)
|
||||
d.number1 = 100
|
||||
d.number2 = -45.67
|
||||
d.true = true
|
||||
d.false = false
|
||||
})
|
||||
|
||||
assert.deepEqual(doc7, {
|
||||
hello: "world",
|
||||
true: true,
|
||||
false: false,
|
||||
int: -1,
|
||||
uint: 1,
|
||||
float64: 5.5,
|
||||
number1: 100,
|
||||
number2: -45.67,
|
||||
counter1: counter,
|
||||
timestamp1: timestamp,
|
||||
bytes1: bytes,
|
||||
app: null,
|
||||
})
|
||||
|
||||
let changes = Automerge.getAllChanges(doc7)
|
||||
let t1 = Automerge.init()
|
||||
let [t2] = Automerge.applyChanges(t1, changes)
|
||||
assert.deepEqual(doc7, t2)
|
||||
})
|
||||
|
||||
it("handle overwrites to values", () => {
|
||||
let doc1 = Automerge.init<any>()
|
||||
let doc2 = Automerge.change(doc1, d => {
|
||||
d.hello = "world1"
|
||||
})
|
||||
let doc3 = Automerge.change(doc2, d => {
|
||||
d.hello = "world2"
|
||||
})
|
||||
let doc4 = Automerge.change(doc3, d => {
|
||||
d.hello = "world3"
|
||||
})
|
||||
let doc5 = Automerge.change(doc4, d => {
|
||||
d.hello = "world4"
|
||||
})
|
||||
assert.deepEqual(doc5, { hello: "world4" })
|
||||
})
|
||||
|
||||
it("handle set with object value", () => {
|
||||
let doc1 = Automerge.init<any>()
|
||||
let doc2 = Automerge.change(doc1, d => {
|
||||
d.subobj = { hello: "world", subsubobj: { zip: "zop" } }
|
||||
})
|
||||
assert.deepEqual(doc2, {
|
||||
subobj: { hello: "world", subsubobj: { zip: "zop" } },
|
||||
})
|
||||
})
|
||||
|
||||
it("handle simple list creation", () => {
|
||||
let doc1 = Automerge.init<any>()
|
||||
let doc2 = Automerge.change(doc1, d => (d.list = []))
|
||||
assert.deepEqual(doc2, { list: [] })
|
||||
})
|
||||
|
||||
it("handle simple lists", () => {
|
||||
let doc1 = Automerge.init<any>()
|
||||
let doc2 = Automerge.change(doc1, d => {
|
||||
d.list = [1, 2, 3]
|
||||
})
|
||||
assert.deepEqual(doc2.list.length, 3)
|
||||
assert.deepEqual(doc2.list[0], 1)
|
||||
assert.deepEqual(doc2.list[1], 2)
|
||||
assert.deepEqual(doc2.list[2], 3)
|
||||
assert.deepEqual(doc2, { list: [1, 2, 3] })
|
||||
// assert.deepStrictEqual(Automerge.toJS(doc2), { list: [1,2,3] })
|
||||
|
||||
let doc3 = Automerge.change(doc2, d => {
|
||||
d.list[1] = "a"
|
||||
})
|
||||
|
||||
assert.deepEqual(doc3.list.length, 3)
|
||||
assert.deepEqual(doc3.list[0], 1)
|
||||
assert.deepEqual(doc3.list[1], "a")
|
||||
assert.deepEqual(doc3.list[2], 3)
|
||||
assert.deepEqual(doc3, { list: [1, "a", 3] })
|
||||
})
|
||||
it("handle simple lists", () => {
|
||||
let doc1 = Automerge.init<any>()
|
||||
let doc2 = Automerge.change(doc1, d => {
|
||||
d.list = [1, 2, 3]
|
||||
})
|
||||
let changes = Automerge.getChanges(doc1, doc2)
|
||||
let docB1 = Automerge.init()
|
||||
let [docB2] = Automerge.applyChanges(docB1, changes)
|
||||
assert.deepEqual(docB2, doc2)
|
||||
})
|
||||
it("handle text", () => {
|
||||
let doc1 = Automerge.init<any>()
|
||||
let doc2 = Automerge.change(doc1, d => {
|
||||
d.list = "hello"
|
||||
Automerge.splice(d, "list", 2, 0, "Z")
|
||||
})
|
||||
let changes = Automerge.getChanges(doc1, doc2)
|
||||
let docB1 = Automerge.init()
|
||||
let [docB2] = Automerge.applyChanges(docB1, changes)
|
||||
assert.deepEqual(docB2, doc2)
|
||||
})
|
||||
|
||||
it("handle non-text strings", () => {
|
||||
let doc1 = WASM.create(true)
|
||||
doc1.put("_root", "text", "hello world")
|
||||
let doc2 = Automerge.load<any>(doc1.save())
|
||||
assert.throws(() => {
|
||||
Automerge.change(doc2, d => {
|
||||
Automerge.splice(d, "text", 1, 0, "Z")
|
||||
let changes = Automerge.getAllChanges(doc7)
|
||||
let t1 = Automerge.init()
|
||||
;let [t2] = Automerge.applyChanges(t1, changes)
|
||||
assert.deepEqual(doc7,t2)
|
||||
})
|
||||
|
||||
it('handle overwrites to values', () => {
|
||||
let doc1 = Automerge.init()
|
||||
let doc2 = Automerge.change(doc1, (d) => {
|
||||
d.hello = "world1"
|
||||
})
|
||||
let doc3 = Automerge.change(doc2, (d) => {
|
||||
d.hello = "world2"
|
||||
})
|
||||
let doc4 = Automerge.change(doc3, (d) => {
|
||||
d.hello = "world3"
|
||||
})
|
||||
let doc5 = Automerge.change(doc4, (d) => {
|
||||
d.hello = "world4"
|
||||
})
|
||||
assert.deepEqual(doc5, { hello: "world4" } )
|
||||
})
|
||||
|
||||
it('handle set with object value', () => {
|
||||
let doc1 = Automerge.init()
|
||||
let doc2 = Automerge.change(doc1, (d) => {
|
||||
d.subobj = { hello: "world", subsubobj: { zip: "zop" } }
|
||||
})
|
||||
assert.deepEqual(doc2, { subobj: { hello: "world", subsubobj: { zip: "zop" } } })
|
||||
})
|
||||
|
||||
it('handle simple list creation', () => {
|
||||
let doc1 = Automerge.init()
|
||||
let doc2 = Automerge.change(doc1, (d) => d.list = [])
|
||||
assert.deepEqual(doc2, { list: []})
|
||||
})
|
||||
|
||||
it('handle simple lists', () => {
|
||||
let doc1 = Automerge.init()
|
||||
let doc2 = Automerge.change(doc1, (d) => {
|
||||
d.list = [ 1, 2, 3 ]
|
||||
})
|
||||
assert.deepEqual(doc2.list.length, 3)
|
||||
assert.deepEqual(doc2.list[0], 1)
|
||||
assert.deepEqual(doc2.list[1], 2)
|
||||
assert.deepEqual(doc2.list[2], 3)
|
||||
assert.deepEqual(doc2, { list: [1,2,3] })
|
||||
// assert.deepStrictEqual(Automerge.toJS(doc2), { list: [1,2,3] })
|
||||
|
||||
let doc3 = Automerge.change(doc2, (d) => {
|
||||
d.list[1] = "a"
|
||||
})
|
||||
|
||||
assert.deepEqual(doc3.list.length, 3)
|
||||
assert.deepEqual(doc3.list[0], 1)
|
||||
assert.deepEqual(doc3.list[1], "a")
|
||||
assert.deepEqual(doc3.list[2], 3)
|
||||
assert.deepEqual(doc3, { list: [1,"a",3] })
|
||||
})
|
||||
it('handle simple lists', () => {
|
||||
let doc1 = Automerge.init()
|
||||
let doc2 = Automerge.change(doc1, (d) => {
|
||||
d.list = [ 1, 2, 3 ]
|
||||
})
|
||||
let changes = Automerge.getChanges(doc1, doc2)
|
||||
let docB1 = Automerge.init()
|
||||
;let [docB2] = Automerge.applyChanges(docB1, changes)
|
||||
assert.deepEqual(docB2, doc2);
|
||||
})
|
||||
it('handle text', () => {
|
||||
let doc1 = Automerge.init()
|
||||
let tmp = new Automerge.Text("hello")
|
||||
let doc2 = Automerge.change(doc1, (d) => {
|
||||
d.list = new Automerge.Text("hello")
|
||||
d.list.insertAt(2,"Z")
|
||||
})
|
||||
let changes = Automerge.getChanges(doc1, doc2)
|
||||
let docB1 = Automerge.init()
|
||||
;let [docB2] = Automerge.applyChanges(docB1, changes)
|
||||
assert.deepEqual(docB2, doc2);
|
||||
})
|
||||
|
||||
it('have many list methods', () => {
|
||||
let doc1 = Automerge.from({ list: [1,2,3] })
|
||||
assert.deepEqual(doc1, { list: [1,2,3] });
|
||||
let doc2 = Automerge.change(doc1, (d) => {
|
||||
d.list.splice(1,1,9,10)
|
||||
})
|
||||
assert.deepEqual(doc2, { list: [1,9,10,3] });
|
||||
let doc3 = Automerge.change(doc2, (d) => {
|
||||
d.list.push(11,12)
|
||||
})
|
||||
assert.deepEqual(doc3, { list: [1,9,10,3,11,12] });
|
||||
let doc4 = Automerge.change(doc3, (d) => {
|
||||
d.list.unshift(2,2)
|
||||
})
|
||||
assert.deepEqual(doc4, { list: [2,2,1,9,10,3,11,12] });
|
||||
let doc5 = Automerge.change(doc4, (d) => {
|
||||
d.list.shift()
|
||||
})
|
||||
assert.deepEqual(doc5, { list: [2,1,9,10,3,11,12] });
|
||||
let doc6 = Automerge.change(doc5, (d) => {
|
||||
d.list.insertAt(3,100,101)
|
||||
})
|
||||
assert.deepEqual(doc6, { list: [2,1,9,100,101,10,3,11,12] });
|
||||
})
|
||||
|
||||
it('allows access to the backend', () => {
|
||||
let doc = Automerge.init()
|
||||
assert.deepEqual(Object.keys(Automerge.getBackend(doc)), ["ptr"])
|
||||
})
|
||||
|
||||
it('lists and text have indexof', () => {
|
||||
let doc = Automerge.from({ list: [0,1,2,3,4,5,6], text: new Automerge.Text("hello world") })
|
||||
console.log(doc.list.indexOf(5))
|
||||
console.log(doc.text.indexOf("world"))
|
||||
})
|
||||
}, /Cannot splice/)
|
||||
})
|
||||
|
||||
it("have many list methods", () => {
|
||||
let doc1 = Automerge.from({ list: [1, 2, 3] })
|
||||
assert.deepEqual(doc1, { list: [1, 2, 3] })
|
||||
let doc2 = Automerge.change(doc1, d => {
|
||||
d.list.splice(1, 1, 9, 10)
|
||||
})
|
||||
assert.deepEqual(doc2, { list: [1, 9, 10, 3] })
|
||||
let doc3 = Automerge.change(doc2, d => {
|
||||
d.list.push(11, 12)
|
||||
})
|
||||
assert.deepEqual(doc3, { list: [1, 9, 10, 3, 11, 12] })
|
||||
let doc4 = Automerge.change(doc3, d => {
|
||||
d.list.unshift(2, 2)
|
||||
})
|
||||
assert.deepEqual(doc4, { list: [2, 2, 1, 9, 10, 3, 11, 12] })
|
||||
let doc5 = Automerge.change(doc4, d => {
|
||||
d.list.shift()
|
||||
})
|
||||
assert.deepEqual(doc5, { list: [2, 1, 9, 10, 3, 11, 12] })
|
||||
let doc6 = Automerge.change(doc5, d => {
|
||||
d.list.insertAt(3, 100, 101)
|
||||
})
|
||||
assert.deepEqual(doc6, { list: [2, 1, 9, 100, 101, 10, 3, 11, 12] })
|
||||
describe('proxy lists', () => {
|
||||
it('behave like arrays', () => {
|
||||
let doc = Automerge.from({
|
||||
chars: ["a","b","c"],
|
||||
numbers: [20,3,100],
|
||||
repeats: [20,20,3,3,3,3,100,100]
|
||||
})
|
||||
let r1 = []
|
||||
doc = Automerge.change(doc, (d) => {
|
||||
assert.deepEqual(d.chars.concat([1,2]), ["a","b","c",1,2])
|
||||
assert.deepEqual(d.chars.map((n) => n + "!"), ["a!", "b!", "c!"])
|
||||
assert.deepEqual(d.numbers.map((n) => n + 10), [30, 13, 110])
|
||||
assert.deepEqual(d.numbers.toString(), "20,3,100")
|
||||
assert.deepEqual(d.numbers.toLocaleString(), "20,3,100")
|
||||
assert.deepEqual(d.numbers.forEach((n) => r1.push(n)), undefined)
|
||||
assert.deepEqual(d.numbers.every((n) => n > 1), true)
|
||||
assert.deepEqual(d.numbers.every((n) => n > 10), false)
|
||||
assert.deepEqual(d.numbers.filter((n) => n > 10), [20,100])
|
||||
assert.deepEqual(d.repeats.find((n) => n < 10), 3)
|
||||
assert.deepEqual(d.repeats.toArray().find((n) => n < 10), 3)
|
||||
assert.deepEqual(d.repeats.find((n) => n < 0), undefined)
|
||||
assert.deepEqual(d.repeats.findIndex((n) => n < 10), 2)
|
||||
assert.deepEqual(d.repeats.findIndex((n) => n < 0), -1)
|
||||
assert.deepEqual(d.repeats.toArray().findIndex((n) => n < 10), 2)
|
||||
assert.deepEqual(d.repeats.toArray().findIndex((n) => n < 0), -1)
|
||||
assert.deepEqual(d.numbers.includes(3), true)
|
||||
assert.deepEqual(d.numbers.includes(-3), false)
|
||||
assert.deepEqual(d.numbers.join("|"), "20|3|100")
|
||||
assert.deepEqual(d.numbers.join(), "20,3,100")
|
||||
assert.deepEqual(d.numbers.some((f) => f === 3), true)
|
||||
assert.deepEqual(d.numbers.some((f) => f < 0), false)
|
||||
assert.deepEqual(d.numbers.reduce((sum,n) => sum + n, 100), 223)
|
||||
assert.deepEqual(d.repeats.reduce((sum,n) => sum + n, 100), 352)
|
||||
assert.deepEqual(d.chars.reduce((sum,n) => sum + n, "="), "=abc")
|
||||
assert.deepEqual(d.chars.reduceRight((sum,n) => sum + n, "="), "=cba")
|
||||
assert.deepEqual(d.numbers.reduceRight((sum,n) => sum + n, 100), 223)
|
||||
assert.deepEqual(d.repeats.lastIndexOf(3), 5)
|
||||
assert.deepEqual(d.repeats.lastIndexOf(3,3), 3)
|
||||
})
|
||||
doc = Automerge.change(doc, (d) => {
|
||||
assert.deepEqual(d.numbers.fill(-1,1,2), [20,-1,100])
|
||||
assert.deepEqual(d.chars.fill("z",1,100), ["a","z","z"])
|
||||
})
|
||||
assert.deepEqual(r1, [20,3,100])
|
||||
assert.deepEqual(doc.numbers, [20,-1,100])
|
||||
assert.deepEqual(doc.chars, ["a","z","z"])
|
||||
})
|
||||
})
|
||||
|
||||
it('should obtain the same conflicts, regardless of merge order', () => {
|
||||
let s1 = Automerge.init()
|
||||
let s2 = Automerge.init()
|
||||
s1 = Automerge.change(s1, doc => { doc.x = 1; doc.y = 2 })
|
||||
s2 = Automerge.change(s2, doc => { doc.x = 3; doc.y = 4 })
|
||||
const m1 = Automerge.merge(Automerge.clone(s1), Automerge.clone(s2))
|
||||
const m2 = Automerge.merge(Automerge.clone(s2), Automerge.clone(s1))
|
||||
assert.deepStrictEqual(Automerge.getConflicts(m1, 'x'), Automerge.getConflicts(m2, 'x'))
|
||||
})
|
||||
|
||||
it("allows access to the backend", () => {
|
||||
let doc = Automerge.init()
|
||||
assert.deepEqual(Object.keys(Automerge.getBackend(doc)), ["ptr"])
|
||||
})
|
||||
describe("getObjectId", () => {
|
||||
let s1 = Automerge.from({
|
||||
"string": "string",
|
||||
"number": 1,
|
||||
"null": null,
|
||||
"date": new Date(),
|
||||
"counter": new Automerge.Counter(),
|
||||
"bytes": new Uint8Array(10),
|
||||
"text": new Automerge.Text(),
|
||||
"list": [],
|
||||
"map": {}
|
||||
})
|
||||
|
||||
it("lists and text have indexof", () => {
|
||||
let doc = Automerge.from({
|
||||
list: [0, 1, 2, 3, 4, 5, 6],
|
||||
text: "hello world",
|
||||
})
|
||||
assert.deepEqual(doc.list.indexOf(5), 5)
|
||||
assert.deepEqual(doc.text.indexOf("world"), 6)
|
||||
})
|
||||
})
|
||||
it("should return null for scalar values", () => {
|
||||
assert.equal(Automerge.getObjectId(s1.string), null)
|
||||
assert.equal(Automerge.getObjectId(s1.number), null)
|
||||
assert.equal(Automerge.getObjectId(s1.null), null)
|
||||
assert.equal(Automerge.getObjectId(s1.date), null)
|
||||
assert.equal(Automerge.getObjectId(s1.counter), null)
|
||||
assert.equal(Automerge.getObjectId(s1.bytes), null)
|
||||
})
|
||||
|
||||
describe("emptyChange", () => {
|
||||
it("should generate a hash", () => {
|
||||
let doc = Automerge.init()
|
||||
doc = Automerge.change<any>(doc, d => {
|
||||
d.key = "value"
|
||||
})
|
||||
Automerge.save(doc)
|
||||
let headsBefore = Automerge.getHeads(doc)
|
||||
headsBefore.sort()
|
||||
doc = Automerge.emptyChange(doc, "empty change")
|
||||
let headsAfter = Automerge.getHeads(doc)
|
||||
headsAfter.sort()
|
||||
assert.notDeepEqual(headsBefore, headsAfter)
|
||||
})
|
||||
})
|
||||
it("should return _root for the root object", () => {
|
||||
assert.equal(Automerge.getObjectId(s1), "_root")
|
||||
})
|
||||
|
||||
describe("proxy lists", () => {
|
||||
it("behave like arrays", () => {
|
||||
let doc = Automerge.from({
|
||||
chars: ["a", "b", "c"],
|
||||
numbers: [20, 3, 100],
|
||||
repeats: [20, 20, 3, 3, 3, 3, 100, 100],
|
||||
})
|
||||
let r1: Array<number> = []
|
||||
doc = Automerge.change(doc, d => {
|
||||
assert.deepEqual((d.chars as any[]).concat([1, 2]), [
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
1,
|
||||
2,
|
||||
])
|
||||
assert.deepEqual(
|
||||
d.chars.map(n => n + "!"),
|
||||
["a!", "b!", "c!"]
|
||||
)
|
||||
assert.deepEqual(
|
||||
d.numbers.map(n => n + 10),
|
||||
[30, 13, 110]
|
||||
)
|
||||
assert.deepEqual(d.numbers.toString(), "20,3,100")
|
||||
assert.deepEqual(d.numbers.toLocaleString(), "20,3,100")
|
||||
assert.deepEqual(
|
||||
d.numbers.forEach((n: number) => r1.push(n)),
|
||||
undefined
|
||||
)
|
||||
assert.deepEqual(
|
||||
d.numbers.every(n => n > 1),
|
||||
true
|
||||
)
|
||||
assert.deepEqual(
|
||||
d.numbers.every(n => n > 10),
|
||||
false
|
||||
)
|
||||
assert.deepEqual(
|
||||
d.numbers.filter(n => n > 10),
|
||||
[20, 100]
|
||||
)
|
||||
assert.deepEqual(
|
||||
d.repeats.find(n => n < 10),
|
||||
3
|
||||
)
|
||||
assert.deepEqual(
|
||||
d.repeats.find(n => n < 10),
|
||||
3
|
||||
)
|
||||
assert.deepEqual(
|
||||
d.repeats.find(n => n < 0),
|
||||
undefined
|
||||
)
|
||||
assert.deepEqual(
|
||||
d.repeats.findIndex(n => n < 10),
|
||||
2
|
||||
)
|
||||
assert.deepEqual(
|
||||
d.repeats.findIndex(n => n < 0),
|
||||
-1
|
||||
)
|
||||
assert.deepEqual(
|
||||
d.repeats.findIndex(n => n < 10),
|
||||
2
|
||||
)
|
||||
assert.deepEqual(
|
||||
d.repeats.findIndex(n => n < 0),
|
||||
-1
|
||||
)
|
||||
assert.deepEqual(d.numbers.includes(3), true)
|
||||
assert.deepEqual(d.numbers.includes(-3), false)
|
||||
assert.deepEqual(d.numbers.join("|"), "20|3|100")
|
||||
assert.deepEqual(d.numbers.join(), "20,3,100")
|
||||
assert.deepEqual(
|
||||
d.numbers.some(f => f === 3),
|
||||
true
|
||||
)
|
||||
assert.deepEqual(
|
||||
d.numbers.some(f => f < 0),
|
||||
false
|
||||
)
|
||||
assert.deepEqual(
|
||||
d.numbers.reduce((sum, n) => sum + n, 100),
|
||||
223
|
||||
)
|
||||
assert.deepEqual(
|
||||
d.repeats.reduce((sum, n) => sum + n, 100),
|
||||
352
|
||||
)
|
||||
assert.deepEqual(
|
||||
d.chars.reduce((sum, n) => sum + n, "="),
|
||||
"=abc"
|
||||
)
|
||||
assert.deepEqual(
|
||||
d.chars.reduceRight((sum, n) => sum + n, "="),
|
||||
"=cba"
|
||||
)
|
||||
assert.deepEqual(
|
||||
d.numbers.reduceRight((sum, n) => sum + n, 100),
|
||||
223
|
||||
)
|
||||
assert.deepEqual(d.repeats.lastIndexOf(3), 5)
|
||||
assert.deepEqual(d.repeats.lastIndexOf(3, 3), 3)
|
||||
})
|
||||
doc = Automerge.change(doc, d => {
|
||||
assert.deepEqual(d.numbers.fill(-1, 1, 2), [20, -1, 100])
|
||||
assert.deepEqual(d.chars.fill("z", 1, 100), ["a", "z", "z"])
|
||||
})
|
||||
assert.deepEqual(r1, [20, 3, 100])
|
||||
assert.deepEqual(doc.numbers, [20, -1, 100])
|
||||
assert.deepEqual(doc.chars, ["a", "z", "z"])
|
||||
it("should return non-null for map, list, text, and objects", () => {
|
||||
assert.notEqual(Automerge.getObjectId(s1.text), null)
|
||||
assert.notEqual(Automerge.getObjectId(s1.list), null)
|
||||
assert.notEqual(Automerge.getObjectId(s1.map), null)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("should obtain the same conflicts, regardless of merge order", () => {
|
||||
let s1 = Automerge.init<any>()
|
||||
let s2 = Automerge.init<any>()
|
||||
s1 = Automerge.change(s1, doc => {
|
||||
doc.x = 1
|
||||
doc.y = 2
|
||||
})
|
||||
s2 = Automerge.change(s2, doc => {
|
||||
doc.x = 3
|
||||
doc.y = 4
|
||||
})
|
||||
const m1 = Automerge.merge(Automerge.clone(s1), Automerge.clone(s2))
|
||||
const m2 = Automerge.merge(Automerge.clone(s2), Automerge.clone(s1))
|
||||
assert.deepStrictEqual(
|
||||
Automerge.getConflicts(m1, "x"),
|
||||
Automerge.getConflicts(m2, "x")
|
||||
)
|
||||
})
|
||||
|
||||
describe("getObjectId", () => {
|
||||
let s1 = Automerge.from({
|
||||
string: "string",
|
||||
number: 1,
|
||||
null: null,
|
||||
date: new Date(),
|
||||
counter: new Automerge.Counter(),
|
||||
bytes: new Uint8Array(10),
|
||||
text: "",
|
||||
list: [],
|
||||
map: {},
|
||||
})
|
||||
|
||||
it("should return null for scalar values", () => {
|
||||
assert.equal(Automerge.getObjectId(s1.string), null)
|
||||
assert.equal(Automerge.getObjectId(s1.number), null)
|
||||
assert.equal(Automerge.getObjectId(s1.null!), null)
|
||||
assert.equal(Automerge.getObjectId(s1.date), null)
|
||||
assert.equal(Automerge.getObjectId(s1.counter), null)
|
||||
assert.equal(Automerge.getObjectId(s1.bytes), null)
|
||||
})
|
||||
|
||||
it("should return _root for the root object", () => {
|
||||
assert.equal(Automerge.getObjectId(s1), "_root")
|
||||
})
|
||||
|
||||
it("should return non-null for map, list, text, and objects", () => {
|
||||
assert.equal(Automerge.getObjectId(s1.text), null)
|
||||
assert.notEqual(Automerge.getObjectId(s1.list), null)
|
||||
assert.notEqual(Automerge.getObjectId(s1.map), null)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
97
javascript/test/columnar_test.ts
Normal file
97
javascript/test/columnar_test.ts
Normal file
|
@ -0,0 +1,97 @@
|
|||
import * as assert from 'assert'
|
||||
import { checkEncoded } from './helpers'
|
||||
import * as Automerge from '../src'
|
||||
import { encodeChange, decodeChange } from '../src'
|
||||
|
||||
describe('change encoding', () => {
|
||||
it('should encode text edits', () => {
|
||||
/*
|
||||
const change1 = {actor: 'aaaa', seq: 1, startOp: 1, time: 9, message: '', deps: [], ops: [
|
||||
{action: 'makeText', obj: '_root', key: 'text', insert: false, pred: []},
|
||||
{action: 'set', obj: '1@aaaa', elemId: '_head', insert: true, value: 'h', pred: []},
|
||||
{action: 'del', obj: '1@aaaa', elemId: '2@aaaa', insert: false, pred: ['2@aaaa']},
|
||||
{action: 'set', obj: '1@aaaa', elemId: '_head', insert: true, value: 'H', pred: []},
|
||||
{action: 'set', obj: '1@aaaa', elemId: '4@aaaa', insert: true, value: 'i', pred: []}
|
||||
]}
|
||||
*/
|
||||
const change1 = {actor: 'aaaa', seq: 1, startOp: 1, time: 9, message: null, deps: [], ops: [
|
||||
{action: 'makeText', obj: '_root', key: 'text', pred: []},
|
||||
{action: 'set', obj: '1@aaaa', elemId: '_head', insert: true, value: 'h', pred: []},
|
||||
{action: 'del', obj: '1@aaaa', elemId: '2@aaaa', pred: ['2@aaaa']},
|
||||
{action: 'set', obj: '1@aaaa', elemId: '_head', insert: true, value: 'H', pred: []},
|
||||
{action: 'set', obj: '1@aaaa', elemId: '4@aaaa', insert: true, value: 'i', pred: []}
|
||||
]}
|
||||
checkEncoded(encodeChange(change1), [
|
||||
0x85, 0x6f, 0x4a, 0x83, // magic bytes
|
||||
0xe2, 0xbd, 0xfb, 0xf5, // checksum
|
||||
1, 94, 0, 2, 0xaa, 0xaa, // chunkType: change, length, deps, actor 'aaaa'
|
||||
1, 1, 9, 0, 0, // seq, startOp, time, message, actor list
|
||||
12, 0x01, 4, 0x02, 4, // column count, objActor, objCtr
|
||||
0x11, 8, 0x13, 7, 0x15, 8, // keyActor, keyCtr, keyStr
|
||||
0x34, 4, 0x42, 6, // insert, action
|
||||
0x56, 6, 0x57, 3, // valLen, valRaw
|
||||
0x70, 6, 0x71, 2, 0x73, 2, // predNum, predActor, predCtr
|
||||
0, 1, 4, 0, // objActor column: null, 0, 0, 0, 0
|
||||
0, 1, 4, 1, // objCtr column: null, 1, 1, 1, 1
|
||||
0, 2, 0x7f, 0, 0, 1, 0x7f, 0, // keyActor column: null, null, 0, null, 0
|
||||
0, 1, 0x7c, 0, 2, 0x7e, 4, // keyCtr column: null, 0, 2, 0, 4
|
||||
0x7f, 4, 0x74, 0x65, 0x78, 0x74, 0, 4, // keyStr column: 'text', null, null, null, null
|
||||
1, 1, 1, 2, // insert column: false, true, false, true, true
|
||||
0x7d, 4, 1, 3, 2, 1, // action column: makeText, set, del, set, set
|
||||
0x7d, 0, 0x16, 0, 2, 0x16, // valLen column: 0, 0x16, 0, 0x16, 0x16
|
||||
0x68, 0x48, 0x69, // valRaw column: 'h', 'H', 'i'
|
||||
2, 0, 0x7f, 1, 2, 0, // predNum column: 0, 0, 1, 0, 0
|
||||
0x7f, 0, // predActor column: 0
|
||||
0x7f, 2 // predCtr column: 2
|
||||
])
|
||||
const decoded = decodeChange(encodeChange(change1))
|
||||
assert.deepStrictEqual(decoded, Object.assign({hash: decoded.hash}, change1))
|
||||
})
|
||||
|
||||
// FIXME - skipping this b/c it was never implemented in the rust impl and isnt trivial
|
||||
/*
|
||||
it.skip('should require strict ordering of preds', () => {
|
||||
const change = new Uint8Array([
|
||||
133, 111, 74, 131, 31, 229, 112, 44, 1, 105, 1, 58, 30, 190, 100, 253, 180, 180, 66, 49, 126,
|
||||
81, 142, 10, 3, 35, 140, 189, 231, 34, 145, 57, 66, 23, 224, 149, 64, 97, 88, 140, 168, 194,
|
||||
229, 4, 244, 209, 58, 138, 67, 140, 1, 152, 236, 250, 2, 0, 1, 4, 55, 234, 66, 242, 8, 21, 11,
|
||||
52, 1, 66, 2, 86, 3, 87, 10, 112, 2, 113, 3, 115, 4, 127, 9, 99, 111, 109, 109, 111, 110, 86,
|
||||
97, 114, 1, 127, 1, 127, 166, 1, 52, 48, 57, 49, 52, 57, 52, 53, 56, 50, 127, 2, 126, 0, 1,
|
||||
126, 139, 1, 0
|
||||
])
|
||||
assert.throws(() => { decodeChange(change) }, /operation IDs are not in ascending order/)
|
||||
})
|
||||
*/
|
||||
|
||||
describe('with trailing bytes', () => {
|
||||
let change = new Uint8Array([
|
||||
0x85, 0x6f, 0x4a, 0x83, // magic bytes
|
||||
0xb2, 0x98, 0x9e, 0xa9, // checksum
|
||||
1, 61, 0, 2, 0x12, 0x34, // chunkType: change, length, deps, actor '1234'
|
||||
1, 1, 252, 250, 220, 255, 5, // seq, startOp, time
|
||||
14, 73, 110, 105, 116, 105, 97, 108, 105, 122, 97, 116, 105, 111, 110, // message: 'Initialization'
|
||||
0, 6, // actor list, column count
|
||||
0x15, 3, 0x34, 1, 0x42, 2, // keyStr, insert, action
|
||||
0x56, 2, 0x57, 1, 0x70, 2, // valLen, valRaw, predNum
|
||||
0x7f, 1, 0x78, // keyStr: 'x'
|
||||
1, // insert: false
|
||||
0x7f, 1, // action: set
|
||||
0x7f, 19, // valLen: 1 byte of type uint
|
||||
1, // valRaw: 1
|
||||
0x7f, 0, // predNum: 0
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9 // 10 trailing bytes
|
||||
])
|
||||
|
||||
it('should allow decoding and re-encoding', () => {
|
||||
// NOTE: This calls the JavaScript encoding and decoding functions, even when the WebAssembly
|
||||
// backend is loaded. Should the wasm backend export its own functions for testing?
|
||||
checkEncoded(change, encodeChange(decodeChange(change)))
|
||||
})
|
||||
|
||||
it('should be preserved in document encoding', () => {
|
||||
const [doc] = Automerge.applyChanges(Automerge.init(), [change])
|
||||
const [reconstructed] = Automerge.getAllChanges(Automerge.load(Automerge.save(doc)))
|
||||
checkEncoded(change, reconstructed)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,28 +1,20 @@
|
|||
import * as assert from "assert"
|
||||
import { unstable as Automerge } from "../src"
|
||||
|
||||
describe("Automerge", () => {
|
||||
describe("basics", () => {
|
||||
it("should allow you to load incrementally", () => {
|
||||
let doc1 = Automerge.from<any>({ foo: "bar" })
|
||||
let doc2 = Automerge.init<any>()
|
||||
doc2 = Automerge.loadIncremental(doc2, Automerge.save(doc1))
|
||||
doc1 = Automerge.change(doc1, d => (d.foo2 = "bar2"))
|
||||
doc2 = Automerge.loadIncremental(
|
||||
doc2,
|
||||
Automerge.getBackend(doc1).saveIncremental()
|
||||
)
|
||||
doc1 = Automerge.change(doc1, d => (d.foo = "bar2"))
|
||||
doc2 = Automerge.loadIncremental(
|
||||
doc2,
|
||||
Automerge.getBackend(doc1).saveIncremental()
|
||||
)
|
||||
doc1 = Automerge.change(doc1, d => (d.x = "y"))
|
||||
doc2 = Automerge.loadIncremental(
|
||||
doc2,
|
||||
Automerge.getBackend(doc1).saveIncremental()
|
||||
)
|
||||
assert.deepEqual(doc1, doc2)
|
||||
import * as assert from 'assert'
|
||||
import * as Automerge from '../src'
|
||||
|
||||
describe('Automerge', () => {
|
||||
describe('basics', () => {
|
||||
it('should allow you to load incrementally', () => {
|
||||
let doc1 = Automerge.from({ foo: "bar" })
|
||||
let doc2 = Automerge.init();
|
||||
doc2 = Automerge.loadIncremental(doc2, Automerge.save(doc1))
|
||||
doc1 = Automerge.change(doc1, (d) => d.foo2 = "bar2")
|
||||
doc2 = Automerge.loadIncremental(doc2, Automerge.getBackend(doc1).saveIncremental() )
|
||||
doc1 = Automerge.change(doc1, (d) => d.foo = "bar2")
|
||||
doc2 = Automerge.loadIncremental(doc2, Automerge.getBackend(doc1).saveIncremental() )
|
||||
doc1 = Automerge.change(doc1, (d) => d.x = "y")
|
||||
doc2 = Automerge.loadIncremental(doc2, Automerge.getBackend(doc1).saveIncremental() )
|
||||
assert.deepEqual(doc1,doc2)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
import * as assert from "assert"
|
||||
import { Encoder } from "./legacy/encoding"
|
||||
import * as assert from 'assert'
|
||||
import { Encoder } from './legacy/encoding'
|
||||
|
||||
// Assertion that succeeds if the first argument deepStrictEquals at least one of the
|
||||
// subsequent arguments (but we don't care which one)
|
||||
export function assertEqualsOneOf(actual, ...expected) {
|
||||
function assertEqualsOneOf(actual, ...expected) {
|
||||
assert(expected.length > 0)
|
||||
for (let i = 0; i < expected.length; i++) {
|
||||
try {
|
||||
assert.deepStrictEqual(actual, expected[i])
|
||||
return // if we get here without an exception, that means success
|
||||
} catch (e) {
|
||||
if (e instanceof assert.AssertionError) {
|
||||
if (!e.name.match(/^AssertionError/) || i === expected.length - 1)
|
||||
throw e
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
if (!e.name.match(/^AssertionError/) || i === expected.length - 1) throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,13 +19,14 @@ export function assertEqualsOneOf(actual, ...expected) {
|
|||
* Asserts that the byte array maintained by `encoder` contains the same byte
|
||||
* sequence as the array `bytes`.
|
||||
*/
|
||||
export function checkEncoded(encoder, bytes, detail?) {
|
||||
const encoded = encoder instanceof Encoder ? encoder.buffer : encoder
|
||||
function checkEncoded(encoder, bytes, detail) {
|
||||
const encoded = (encoder instanceof Encoder) ? encoder.buffer : encoder
|
||||
const expected = new Uint8Array(bytes)
|
||||
const message =
|
||||
(detail ? `${detail}: ` : "") + `${encoded} expected to equal ${expected}`
|
||||
const message = (detail ? `${detail}: ` : '') + `${encoded} expected to equal ${expected}`
|
||||
assert(encoded.byteLength === expected.byteLength, message)
|
||||
for (let i = 0; i < encoded.byteLength; i++) {
|
||||
assert(encoded[i] === expected[i], message)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { assertEqualsOneOf, checkEncoded }
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
|||
function isObject(obj) {
|
||||
return typeof obj === "object" && obj !== null
|
||||
return typeof obj === 'object' && obj !== null
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -20,11 +20,11 @@ function copyObject(obj) {
|
|||
* with an actor ID, separated by an `@` sign) and returns an object `{counter, actorId}`.
|
||||
*/
|
||||
function parseOpId(opId) {
|
||||
const match = /^(\d+)@(.*)$/.exec(opId || "")
|
||||
const match = /^(\d+)@(.*)$/.exec(opId || '')
|
||||
if (!match) {
|
||||
throw new RangeError(`Not a valid opId: ${opId}`)
|
||||
}
|
||||
return { counter: parseInt(match[1], 10), actorId: match[2] }
|
||||
return {counter: parseInt(match[1], 10), actorId: match[2]}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -32,7 +32,7 @@ function parseOpId(opId) {
|
|||
*/
|
||||
function equalBytes(array1, array2) {
|
||||
if (!(array1 instanceof Uint8Array) || !(array2 instanceof Uint8Array)) {
|
||||
throw new TypeError("equalBytes can only compare Uint8Arrays")
|
||||
throw new TypeError('equalBytes can only compare Uint8Arrays')
|
||||
}
|
||||
if (array1.byteLength !== array2.byteLength) return false
|
||||
for (let i = 0; i < array1.byteLength; i++) {
|
||||
|
@ -51,9 +51,5 @@ function createArrayOfNulls(length) {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
isObject,
|
||||
copyObject,
|
||||
parseOpId,
|
||||
equalBytes,
|
||||
createArrayOfNulls,
|
||||
isObject, copyObject, parseOpId, equalBytes, createArrayOfNulls
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* https://github.com/anonyco/FastestSmallestTextEncoderDecoder
|
||||
*/
|
||||
const utf8encoder = new TextEncoder()
|
||||
const utf8decoder = new TextDecoder("utf-8")
|
||||
const utf8decoder = new TextDecoder('utf-8')
|
||||
|
||||
function stringToUtf8(string) {
|
||||
return utf8encoder.encode(string)
|
||||
|
@ -20,48 +20,30 @@ function utf8ToString(buffer) {
|
|||
* Converts a string consisting of hexadecimal digits into an Uint8Array.
|
||||
*/
|
||||
function hexStringToBytes(value) {
|
||||
if (typeof value !== "string") {
|
||||
throw new TypeError("value is not a string")
|
||||
if (typeof value !== 'string') {
|
||||
throw new TypeError('value is not a string')
|
||||
}
|
||||
if (!/^([0-9a-f][0-9a-f])*$/.test(value)) {
|
||||
throw new RangeError("value is not hexadecimal")
|
||||
throw new RangeError('value is not hexadecimal')
|
||||
}
|
||||
if (value === "") {
|
||||
if (value === '') {
|
||||
return new Uint8Array(0)
|
||||
} else {
|
||||
return new Uint8Array(value.match(/../g).map(b => parseInt(b, 16)))
|
||||
}
|
||||
}
|
||||
|
||||
const NIBBLE_TO_HEX = [
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"d",
|
||||
"e",
|
||||
"f",
|
||||
]
|
||||
const NIBBLE_TO_HEX = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']
|
||||
const BYTE_TO_HEX = new Array(256)
|
||||
for (let i = 0; i < 256; i++) {
|
||||
BYTE_TO_HEX[i] = `${NIBBLE_TO_HEX[(i >>> 4) & 0xf]}${NIBBLE_TO_HEX[i & 0xf]}`
|
||||
BYTE_TO_HEX[i] = `${NIBBLE_TO_HEX[(i >>> 4) & 0xf]}${NIBBLE_TO_HEX[i & 0xf]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Uint8Array into the equivalent hexadecimal string.
|
||||
*/
|
||||
function bytesToHexString(bytes) {
|
||||
let hex = "",
|
||||
len = bytes.byteLength
|
||||
let hex = '', len = bytes.byteLength
|
||||
for (let i = 0; i < len; i++) {
|
||||
hex += BYTE_TO_HEX[bytes[i]]
|
||||
}
|
||||
|
@ -113,17 +95,14 @@ class Encoder {
|
|||
* appends it to the buffer. Returns the number of bytes written.
|
||||
*/
|
||||
appendUint32(value) {
|
||||
if (!Number.isInteger(value))
|
||||
throw new RangeError("value is not an integer")
|
||||
if (value < 0 || value > 0xffffffff)
|
||||
throw new RangeError("number out of range")
|
||||
if (!Number.isInteger(value)) throw new RangeError('value is not an integer')
|
||||
if (value < 0 || value > 0xffffffff) throw new RangeError('number out of range')
|
||||
|
||||
const numBytes = Math.max(1, Math.ceil((32 - Math.clz32(value)) / 7))
|
||||
if (this.offset + numBytes > this.buf.byteLength) this.grow()
|
||||
|
||||
for (let i = 0; i < numBytes; i++) {
|
||||
this.buf[this.offset + i] =
|
||||
(value & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
|
||||
this.buf[this.offset + i] = (value & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
|
||||
value >>>= 7 // zero-filling right shift
|
||||
}
|
||||
this.offset += numBytes
|
||||
|
@ -136,19 +115,14 @@ class Encoder {
|
|||
* it to the buffer. Returns the number of bytes written.
|
||||
*/
|
||||
appendInt32(value) {
|
||||
if (!Number.isInteger(value))
|
||||
throw new RangeError("value is not an integer")
|
||||
if (value < -0x80000000 || value > 0x7fffffff)
|
||||
throw new RangeError("number out of range")
|
||||
if (!Number.isInteger(value)) throw new RangeError('value is not an integer')
|
||||
if (value < -0x80000000 || value > 0x7fffffff) throw new RangeError('number out of range')
|
||||
|
||||
const numBytes = Math.ceil(
|
||||
(33 - Math.clz32(value >= 0 ? value : -value - 1)) / 7
|
||||
)
|
||||
const numBytes = Math.ceil((33 - Math.clz32(value >= 0 ? value : -value - 1)) / 7)
|
||||
if (this.offset + numBytes > this.buf.byteLength) this.grow()
|
||||
|
||||
for (let i = 0; i < numBytes; i++) {
|
||||
this.buf[this.offset + i] =
|
||||
(value & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
|
||||
this.buf[this.offset + i] = (value & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
|
||||
value >>= 7 // sign-propagating right shift
|
||||
}
|
||||
this.offset += numBytes
|
||||
|
@ -161,10 +135,9 @@ class Encoder {
|
|||
* (53 bits).
|
||||
*/
|
||||
appendUint53(value) {
|
||||
if (!Number.isInteger(value))
|
||||
throw new RangeError("value is not an integer")
|
||||
if (!Number.isInteger(value)) throw new RangeError('value is not an integer')
|
||||
if (value < 0 || value > Number.MAX_SAFE_INTEGER) {
|
||||
throw new RangeError("number out of range")
|
||||
throw new RangeError('number out of range')
|
||||
}
|
||||
const high32 = Math.floor(value / 0x100000000)
|
||||
const low32 = (value & 0xffffffff) >>> 0 // right shift to interpret as unsigned
|
||||
|
@ -177,10 +150,9 @@ class Encoder {
|
|||
* (53 bits).
|
||||
*/
|
||||
appendInt53(value) {
|
||||
if (!Number.isInteger(value))
|
||||
throw new RangeError("value is not an integer")
|
||||
if (!Number.isInteger(value)) throw new RangeError('value is not an integer')
|
||||
if (value < Number.MIN_SAFE_INTEGER || value > Number.MAX_SAFE_INTEGER) {
|
||||
throw new RangeError("number out of range")
|
||||
throw new RangeError('number out of range')
|
||||
}
|
||||
const high32 = Math.floor(value / 0x100000000)
|
||||
const low32 = (value & 0xffffffff) >>> 0 // right shift to interpret as unsigned
|
||||
|
@ -195,10 +167,10 @@ class Encoder {
|
|||
*/
|
||||
appendUint64(high32, low32) {
|
||||
if (!Number.isInteger(high32) || !Number.isInteger(low32)) {
|
||||
throw new RangeError("value is not an integer")
|
||||
throw new RangeError('value is not an integer')
|
||||
}
|
||||
if (high32 < 0 || high32 > 0xffffffff || low32 < 0 || low32 > 0xffffffff) {
|
||||
throw new RangeError("number out of range")
|
||||
throw new RangeError('number out of range')
|
||||
}
|
||||
if (high32 === 0) return this.appendUint32(low32)
|
||||
|
||||
|
@ -208,12 +180,10 @@ class Encoder {
|
|||
this.buf[this.offset + i] = (low32 & 0x7f) | 0x80
|
||||
low32 >>>= 7 // zero-filling right shift
|
||||
}
|
||||
this.buf[this.offset + 4] =
|
||||
(low32 & 0x0f) | ((high32 & 0x07) << 4) | (numBytes === 5 ? 0x00 : 0x80)
|
||||
this.buf[this.offset + 4] = (low32 & 0x0f) | ((high32 & 0x07) << 4) | (numBytes === 5 ? 0x00 : 0x80)
|
||||
high32 >>>= 3
|
||||
for (let i = 5; i < numBytes; i++) {
|
||||
this.buf[this.offset + i] =
|
||||
(high32 & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
|
||||
this.buf[this.offset + i] = (high32 & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
|
||||
high32 >>>= 7
|
||||
}
|
||||
this.offset += numBytes
|
||||
|
@ -230,35 +200,25 @@ class Encoder {
|
|||
*/
|
||||
appendInt64(high32, low32) {
|
||||
if (!Number.isInteger(high32) || !Number.isInteger(low32)) {
|
||||
throw new RangeError("value is not an integer")
|
||||
throw new RangeError('value is not an integer')
|
||||
}
|
||||
if (
|
||||
high32 < -0x80000000 ||
|
||||
high32 > 0x7fffffff ||
|
||||
low32 < -0x80000000 ||
|
||||
low32 > 0xffffffff
|
||||
) {
|
||||
throw new RangeError("number out of range")
|
||||
if (high32 < -0x80000000 || high32 > 0x7fffffff || low32 < -0x80000000 || low32 > 0xffffffff) {
|
||||
throw new RangeError('number out of range')
|
||||
}
|
||||
low32 >>>= 0 // interpret as unsigned
|
||||
if (high32 === 0 && low32 <= 0x7fffffff) return this.appendInt32(low32)
|
||||
if (high32 === -1 && low32 >= 0x80000000)
|
||||
return this.appendInt32(low32 - 0x100000000)
|
||||
if (high32 === -1 && low32 >= 0x80000000) return this.appendInt32(low32 - 0x100000000)
|
||||
|
||||
const numBytes = Math.ceil(
|
||||
(65 - Math.clz32(high32 >= 0 ? high32 : -high32 - 1)) / 7
|
||||
)
|
||||
const numBytes = Math.ceil((65 - Math.clz32(high32 >= 0 ? high32 : -high32 - 1)) / 7)
|
||||
if (this.offset + numBytes > this.buf.byteLength) this.grow()
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.buf[this.offset + i] = (low32 & 0x7f) | 0x80
|
||||
low32 >>>= 7 // zero-filling right shift
|
||||
}
|
||||
this.buf[this.offset + 4] =
|
||||
(low32 & 0x0f) | ((high32 & 0x07) << 4) | (numBytes === 5 ? 0x00 : 0x80)
|
||||
this.buf[this.offset + 4] = (low32 & 0x0f) | ((high32 & 0x07) << 4) | (numBytes === 5 ? 0x00 : 0x80)
|
||||
high32 >>= 3 // sign-propagating right shift
|
||||
for (let i = 5; i < numBytes; i++) {
|
||||
this.buf[this.offset + i] =
|
||||
(high32 & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
|
||||
this.buf[this.offset + i] = (high32 & 0x7f) | (i === numBytes - 1 ? 0x00 : 0x80)
|
||||
high32 >>= 7
|
||||
}
|
||||
this.offset += numBytes
|
||||
|
@ -283,7 +243,7 @@ class Encoder {
|
|||
* number of bytes appended.
|
||||
*/
|
||||
appendRawString(value) {
|
||||
if (typeof value !== "string") throw new TypeError("value is not a string")
|
||||
if (typeof value !== 'string') throw new TypeError('value is not a string')
|
||||
return this.appendRawBytes(stringToUtf8(value))
|
||||
}
|
||||
|
||||
|
@ -302,7 +262,7 @@ class Encoder {
|
|||
* (where the length is encoded as an unsigned LEB128 integer).
|
||||
*/
|
||||
appendPrefixedString(value) {
|
||||
if (typeof value !== "string") throw new TypeError("value is not a string")
|
||||
if (typeof value !== 'string') throw new TypeError('value is not a string')
|
||||
this.appendPrefixedBytes(stringToUtf8(value))
|
||||
return this
|
||||
}
|
||||
|
@ -321,7 +281,8 @@ class Encoder {
|
|||
* Flushes any unwritten data to the buffer. Call this before reading from
|
||||
* the buffer constructed by this Encoder.
|
||||
*/
|
||||
finish() {}
|
||||
finish() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -360,7 +321,7 @@ class Decoder {
|
|||
*/
|
||||
skip(bytes) {
|
||||
if (this.offset + bytes > this.buf.byteLength) {
|
||||
throw new RangeError("cannot skip beyond end of buffer")
|
||||
throw new RangeError('cannot skip beyond end of buffer')
|
||||
}
|
||||
this.offset += bytes
|
||||
}
|
||||
|
@ -378,20 +339,18 @@ class Decoder {
|
|||
* Throws an exception if the value doesn't fit in a 32-bit unsigned int.
|
||||
*/
|
||||
readUint32() {
|
||||
let result = 0,
|
||||
shift = 0
|
||||
let result = 0, shift = 0
|
||||
while (this.offset < this.buf.byteLength) {
|
||||
const nextByte = this.buf[this.offset]
|
||||
if (shift === 28 && (nextByte & 0xf0) !== 0) {
|
||||
// more than 5 bytes, or value > 0xffffffff
|
||||
throw new RangeError("number out of range")
|
||||
if (shift === 28 && (nextByte & 0xf0) !== 0) { // more than 5 bytes, or value > 0xffffffff
|
||||
throw new RangeError('number out of range')
|
||||
}
|
||||
result = (result | ((nextByte & 0x7f) << shift)) >>> 0 // right shift to interpret value as unsigned
|
||||
result = (result | (nextByte & 0x7f) << shift) >>> 0 // right shift to interpret value as unsigned
|
||||
shift += 7
|
||||
this.offset++
|
||||
if ((nextByte & 0x80) === 0) return result
|
||||
}
|
||||
throw new RangeError("buffer ended with incomplete number")
|
||||
throw new RangeError('buffer ended with incomplete number')
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -399,17 +358,13 @@ class Decoder {
|
|||
* Throws an exception if the value doesn't fit in a 32-bit signed int.
|
||||
*/
|
||||
readInt32() {
|
||||
let result = 0,
|
||||
shift = 0
|
||||
let result = 0, shift = 0
|
||||
while (this.offset < this.buf.byteLength) {
|
||||
const nextByte = this.buf[this.offset]
|
||||
if (
|
||||
(shift === 28 && (nextByte & 0x80) !== 0) || // more than 5 bytes
|
||||
(shift === 28 && (nextByte & 0x40) === 0 && (nextByte & 0x38) !== 0) || // positive int > 0x7fffffff
|
||||
(shift === 28 && (nextByte & 0x40) !== 0 && (nextByte & 0x38) !== 0x38)
|
||||
) {
|
||||
// negative int < -0x80000000
|
||||
throw new RangeError("number out of range")
|
||||
if ((shift === 28 && (nextByte & 0x80) !== 0) || // more than 5 bytes
|
||||
(shift === 28 && (nextByte & 0x40) === 0 && (nextByte & 0x38) !== 0) || // positive int > 0x7fffffff
|
||||
(shift === 28 && (nextByte & 0x40) !== 0 && (nextByte & 0x38) !== 0x38)) { // negative int < -0x80000000
|
||||
throw new RangeError('number out of range')
|
||||
}
|
||||
result |= (nextByte & 0x7f) << shift
|
||||
shift += 7
|
||||
|
@ -423,7 +378,7 @@ class Decoder {
|
|||
}
|
||||
}
|
||||
}
|
||||
throw new RangeError("buffer ended with incomplete number")
|
||||
throw new RangeError('buffer ended with incomplete number')
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -434,7 +389,7 @@ class Decoder {
|
|||
readUint53() {
|
||||
const { low32, high32 } = this.readUint64()
|
||||
if (high32 < 0 || high32 > 0x1fffff) {
|
||||
throw new RangeError("number out of range")
|
||||
throw new RangeError('number out of range')
|
||||
}
|
||||
return high32 * 0x100000000 + low32
|
||||
}
|
||||
|
@ -446,12 +401,8 @@ class Decoder {
|
|||
*/
|
||||
readInt53() {
|
||||
const { low32, high32 } = this.readInt64()
|
||||
if (
|
||||
high32 < -0x200000 ||
|
||||
(high32 === -0x200000 && low32 === 0) ||
|
||||
high32 > 0x1fffff
|
||||
) {
|
||||
throw new RangeError("number out of range")
|
||||
if (high32 < -0x200000 || (high32 === -0x200000 && low32 === 0) || high32 > 0x1fffff) {
|
||||
throw new RangeError('number out of range')
|
||||
}
|
||||
return high32 * 0x100000000 + low32
|
||||
}
|
||||
|
@ -463,12 +414,10 @@ class Decoder {
|
|||
* `{high32, low32}`.
|
||||
*/
|
||||
readUint64() {
|
||||
let low32 = 0,
|
||||
high32 = 0,
|
||||
shift = 0
|
||||
let low32 = 0, high32 = 0, shift = 0
|
||||
while (this.offset < this.buf.byteLength && shift <= 28) {
|
||||
const nextByte = this.buf[this.offset]
|
||||
low32 = (low32 | ((nextByte & 0x7f) << shift)) >>> 0 // right shift to interpret value as unsigned
|
||||
low32 = (low32 | (nextByte & 0x7f) << shift) >>> 0 // right shift to interpret value as unsigned
|
||||
if (shift === 28) {
|
||||
high32 = (nextByte & 0x70) >>> 4
|
||||
}
|
||||
|
@ -480,16 +429,15 @@ class Decoder {
|
|||
shift = 3
|
||||
while (this.offset < this.buf.byteLength) {
|
||||
const nextByte = this.buf[this.offset]
|
||||
if (shift === 31 && (nextByte & 0xfe) !== 0) {
|
||||
// more than 10 bytes, or value > 2^64 - 1
|
||||
throw new RangeError("number out of range")
|
||||
if (shift === 31 && (nextByte & 0xfe) !== 0) { // more than 10 bytes, or value > 2^64 - 1
|
||||
throw new RangeError('number out of range')
|
||||
}
|
||||
high32 = (high32 | ((nextByte & 0x7f) << shift)) >>> 0
|
||||
high32 = (high32 | (nextByte & 0x7f) << shift) >>> 0
|
||||
shift += 7
|
||||
this.offset++
|
||||
if ((nextByte & 0x80) === 0) return { high32, low32 }
|
||||
}
|
||||
throw new RangeError("buffer ended with incomplete number")
|
||||
throw new RangeError('buffer ended with incomplete number')
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -500,20 +448,17 @@ class Decoder {
|
|||
* sign of the `high32` half indicates the sign of the 64-bit number.
|
||||
*/
|
||||
readInt64() {
|
||||
let low32 = 0,
|
||||
high32 = 0,
|
||||
shift = 0
|
||||
let low32 = 0, high32 = 0, shift = 0
|
||||
while (this.offset < this.buf.byteLength && shift <= 28) {
|
||||
const nextByte = this.buf[this.offset]
|
||||
low32 = (low32 | ((nextByte & 0x7f) << shift)) >>> 0 // right shift to interpret value as unsigned
|
||||
low32 = (low32 | (nextByte & 0x7f) << shift) >>> 0 // right shift to interpret value as unsigned
|
||||
if (shift === 28) {
|
||||
high32 = (nextByte & 0x70) >>> 4
|
||||
}
|
||||
shift += 7
|
||||
this.offset++
|
||||
if ((nextByte & 0x80) === 0) {
|
||||
if ((nextByte & 0x40) !== 0) {
|
||||
// sign-extend negative integer
|
||||
if ((nextByte & 0x40) !== 0) { // sign-extend negative integer
|
||||
if (shift < 32) low32 = (low32 | (-1 << shift)) >>> 0
|
||||
high32 |= -1 << Math.max(shift - 32, 0)
|
||||
}
|
||||
|
@ -527,20 +472,19 @@ class Decoder {
|
|||
// On the 10th byte there are only two valid values: all 7 value bits zero
|
||||
// (if the value is positive) or all 7 bits one (if the value is negative)
|
||||
if (shift === 31 && nextByte !== 0 && nextByte !== 0x7f) {
|
||||
throw new RangeError("number out of range")
|
||||
throw new RangeError('number out of range')
|
||||
}
|
||||
high32 |= (nextByte & 0x7f) << shift
|
||||
shift += 7
|
||||
this.offset++
|
||||
if ((nextByte & 0x80) === 0) {
|
||||
if ((nextByte & 0x40) !== 0 && shift < 32) {
|
||||
// sign-extend negative integer
|
||||
if ((nextByte & 0x40) !== 0 && shift < 32) { // sign-extend negative integer
|
||||
high32 |= -1 << shift
|
||||
}
|
||||
return { high32, low32 }
|
||||
}
|
||||
}
|
||||
throw new RangeError("buffer ended with incomplete number")
|
||||
throw new RangeError('buffer ended with incomplete number')
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -550,7 +494,7 @@ class Decoder {
|
|||
readRawBytes(length) {
|
||||
const start = this.offset
|
||||
if (start + length > this.buf.byteLength) {
|
||||
throw new RangeError("subarray exceeds buffer size")
|
||||
throw new RangeError('subarray exceeds buffer size')
|
||||
}
|
||||
this.offset += length
|
||||
return this.buf.subarray(start, this.offset)
|
||||
|
@ -615,7 +559,7 @@ class RLEEncoder extends Encoder {
|
|||
constructor(type) {
|
||||
super()
|
||||
this.type = type
|
||||
this.state = "empty"
|
||||
this.state = 'empty'
|
||||
this.lastValue = undefined
|
||||
this.count = 0
|
||||
this.literal = []
|
||||
|
@ -634,81 +578,76 @@ class RLEEncoder extends Encoder {
|
|||
*/
|
||||
_appendValue(value, repetitions = 1) {
|
||||
if (repetitions <= 0) return
|
||||
if (this.state === "empty") {
|
||||
this.state =
|
||||
value === null
|
||||
? "nulls"
|
||||
: repetitions === 1
|
||||
? "loneValue"
|
||||
: "repetition"
|
||||
if (this.state === 'empty') {
|
||||
this.state = (value === null ? 'nulls' : (repetitions === 1 ? 'loneValue' : 'repetition'))
|
||||
this.lastValue = value
|
||||
this.count = repetitions
|
||||
} else if (this.state === "loneValue") {
|
||||
} else if (this.state === 'loneValue') {
|
||||
if (value === null) {
|
||||
this.flush()
|
||||
this.state = "nulls"
|
||||
this.state = 'nulls'
|
||||
this.count = repetitions
|
||||
} else if (value === this.lastValue) {
|
||||
this.state = "repetition"
|
||||
this.state = 'repetition'
|
||||
this.count = 1 + repetitions
|
||||
} else if (repetitions > 1) {
|
||||
this.flush()
|
||||
this.state = "repetition"
|
||||
this.state = 'repetition'
|
||||
this.count = repetitions
|
||||
this.lastValue = value
|
||||
} else {
|
||||
this.state = "literal"
|
||||
this.state = 'literal'
|
||||
this.literal = [this.lastValue]
|
||||
this.lastValue = value
|
||||
}
|
||||
} else if (this.state === "repetition") {
|
||||
} else if (this.state === 'repetition') {
|
||||
if (value === null) {
|
||||
this.flush()
|
||||
this.state = "nulls"
|
||||
this.state = 'nulls'
|
||||
this.count = repetitions
|
||||
} else if (value === this.lastValue) {
|
||||
this.count += repetitions
|
||||
} else if (repetitions > 1) {
|
||||
this.flush()
|
||||
this.state = "repetition"
|
||||
this.state = 'repetition'
|
||||
this.count = repetitions
|
||||
this.lastValue = value
|
||||
} else {
|
||||
this.flush()
|
||||
this.state = "loneValue"
|
||||
this.state = 'loneValue'
|
||||
this.lastValue = value
|
||||
}
|
||||
} else if (this.state === "literal") {
|
||||
} else if (this.state === 'literal') {
|
||||
if (value === null) {
|
||||
this.literal.push(this.lastValue)
|
||||
this.flush()
|
||||
this.state = "nulls"
|
||||
this.state = 'nulls'
|
||||
this.count = repetitions
|
||||
} else if (value === this.lastValue) {
|
||||
this.flush()
|
||||
this.state = "repetition"
|
||||
this.state = 'repetition'
|
||||
this.count = 1 + repetitions
|
||||
} else if (repetitions > 1) {
|
||||
this.literal.push(this.lastValue)
|
||||
this.flush()
|
||||
this.state = "repetition"
|
||||
this.state = 'repetition'
|
||||
this.count = repetitions
|
||||
this.lastValue = value
|
||||
} else {
|
||||
this.literal.push(this.lastValue)
|
||||
this.lastValue = value
|
||||
}
|
||||
} else if (this.state === "nulls") {
|
||||
} else if (this.state === 'nulls') {
|
||||
if (value === null) {
|
||||
this.count += repetitions
|
||||
} else if (repetitions > 1) {
|
||||
this.flush()
|
||||
this.state = "repetition"
|
||||
this.state = 'repetition'
|
||||
this.count = repetitions
|
||||
this.lastValue = value
|
||||
} else {
|
||||
this.flush()
|
||||
this.state = "loneValue"
|
||||
this.state = 'loneValue'
|
||||
this.lastValue = value
|
||||
}
|
||||
}
|
||||
|
@ -727,16 +666,13 @@ class RLEEncoder extends Encoder {
|
|||
*/
|
||||
copyFrom(decoder, options = {}) {
|
||||
const { count, sumValues, sumShift } = options
|
||||
if (!(decoder instanceof RLEDecoder) || decoder.type !== this.type) {
|
||||
throw new TypeError("incompatible type of decoder")
|
||||
if (!(decoder instanceof RLEDecoder) || (decoder.type !== this.type)) {
|
||||
throw new TypeError('incompatible type of decoder')
|
||||
}
|
||||
let remaining = typeof count === "number" ? count : Number.MAX_SAFE_INTEGER
|
||||
let nonNullValues = 0,
|
||||
sum = 0
|
||||
if (count && remaining > 0 && decoder.done)
|
||||
throw new RangeError(`cannot copy ${count} values`)
|
||||
if (remaining === 0 || decoder.done)
|
||||
return sumValues ? { nonNullValues, sum } : { nonNullValues }
|
||||
let remaining = (typeof count === 'number' ? count : Number.MAX_SAFE_INTEGER)
|
||||
let nonNullValues = 0, sum = 0
|
||||
if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)
|
||||
if (remaining === 0 || decoder.done) return sumValues ? {nonNullValues, sum} : {nonNullValues}
|
||||
|
||||
// Copy a value so that we have a well-defined starting state. NB: when super.copyFrom() is
|
||||
// called by the DeltaEncoder subclass, the following calls to readValue() and appendValue()
|
||||
|
@ -748,101 +684,87 @@ class RLEEncoder extends Encoder {
|
|||
remaining -= numNulls
|
||||
decoder.count -= numNulls - 1
|
||||
this.appendValue(null, numNulls)
|
||||
if (count && remaining > 0 && decoder.done)
|
||||
throw new RangeError(`cannot copy ${count} values`)
|
||||
if (remaining === 0 || decoder.done)
|
||||
return sumValues ? { nonNullValues, sum } : { nonNullValues }
|
||||
if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)
|
||||
if (remaining === 0 || decoder.done) return sumValues ? {nonNullValues, sum} : {nonNullValues}
|
||||
firstValue = decoder.readValue()
|
||||
if (firstValue === null)
|
||||
throw new RangeError("null run must be followed by non-null value")
|
||||
if (firstValue === null) throw new RangeError('null run must be followed by non-null value')
|
||||
}
|
||||
this.appendValue(firstValue)
|
||||
remaining--
|
||||
nonNullValues++
|
||||
if (sumValues) sum += sumShift ? firstValue >>> sumShift : firstValue
|
||||
if (count && remaining > 0 && decoder.done)
|
||||
throw new RangeError(`cannot copy ${count} values`)
|
||||
if (remaining === 0 || decoder.done)
|
||||
return sumValues ? { nonNullValues, sum } : { nonNullValues }
|
||||
if (sumValues) sum += (sumShift ? (firstValue >>> sumShift) : firstValue)
|
||||
if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)
|
||||
if (remaining === 0 || decoder.done) return sumValues ? {nonNullValues, sum} : {nonNullValues}
|
||||
|
||||
// Copy data at the record level without expanding repetitions
|
||||
let firstRun = decoder.count > 0
|
||||
let firstRun = (decoder.count > 0)
|
||||
while (remaining > 0 && !decoder.done) {
|
||||
if (!firstRun) decoder.readRecord()
|
||||
const numValues = Math.min(decoder.count, remaining)
|
||||
decoder.count -= numValues
|
||||
|
||||
if (decoder.state === "literal") {
|
||||
if (decoder.state === 'literal') {
|
||||
nonNullValues += numValues
|
||||
for (let i = 0; i < numValues; i++) {
|
||||
if (decoder.done) throw new RangeError("incomplete literal")
|
||||
if (decoder.done) throw new RangeError('incomplete literal')
|
||||
const value = decoder.readRawValue()
|
||||
if (value === decoder.lastValue)
|
||||
throw new RangeError(
|
||||
"Repetition of values is not allowed in literal"
|
||||
)
|
||||
if (value === decoder.lastValue) throw new RangeError('Repetition of values is not allowed in literal')
|
||||
decoder.lastValue = value
|
||||
this._appendValue(value)
|
||||
if (sumValues) sum += sumShift ? value >>> sumShift : value
|
||||
if (sumValues) sum += (sumShift ? (value >>> sumShift) : value)
|
||||
}
|
||||
} else if (decoder.state === "repetition") {
|
||||
} else if (decoder.state === 'repetition') {
|
||||
nonNullValues += numValues
|
||||
if (sumValues)
|
||||
sum +=
|
||||
numValues *
|
||||
(sumShift ? decoder.lastValue >>> sumShift : decoder.lastValue)
|
||||
if (sumValues) sum += numValues * (sumShift ? (decoder.lastValue >>> sumShift) : decoder.lastValue)
|
||||
const value = decoder.lastValue
|
||||
this._appendValue(value)
|
||||
if (numValues > 1) {
|
||||
this._appendValue(value)
|
||||
if (this.state !== "repetition")
|
||||
throw new RangeError(`Unexpected state ${this.state}`)
|
||||
if (this.state !== 'repetition') throw new RangeError(`Unexpected state ${this.state}`)
|
||||
this.count += numValues - 2
|
||||
}
|
||||
} else if (decoder.state === "nulls") {
|
||||
} else if (decoder.state === 'nulls') {
|
||||
this._appendValue(null)
|
||||
if (this.state !== "nulls")
|
||||
throw new RangeError(`Unexpected state ${this.state}`)
|
||||
if (this.state !== 'nulls') throw new RangeError(`Unexpected state ${this.state}`)
|
||||
this.count += numValues - 1
|
||||
}
|
||||
|
||||
firstRun = false
|
||||
remaining -= numValues
|
||||
}
|
||||
if (count && remaining > 0 && decoder.done)
|
||||
throw new RangeError(`cannot copy ${count} values`)
|
||||
return sumValues ? { nonNullValues, sum } : { nonNullValues }
|
||||
if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)
|
||||
return sumValues ? {nonNullValues, sum} : {nonNullValues}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method, do not call from outside the class.
|
||||
*/
|
||||
flush() {
|
||||
if (this.state === "loneValue") {
|
||||
if (this.state === 'loneValue') {
|
||||
this.appendInt32(-1)
|
||||
this.appendRawValue(this.lastValue)
|
||||
} else if (this.state === "repetition") {
|
||||
} else if (this.state === 'repetition') {
|
||||
this.appendInt53(this.count)
|
||||
this.appendRawValue(this.lastValue)
|
||||
} else if (this.state === "literal") {
|
||||
} else if (this.state === 'literal') {
|
||||
this.appendInt53(-this.literal.length)
|
||||
for (let v of this.literal) this.appendRawValue(v)
|
||||
} else if (this.state === "nulls") {
|
||||
} else if (this.state === 'nulls') {
|
||||
this.appendInt32(0)
|
||||
this.appendUint53(this.count)
|
||||
}
|
||||
this.state = "empty"
|
||||
this.state = 'empty'
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method, do not call from outside the class.
|
||||
*/
|
||||
appendRawValue(value) {
|
||||
if (this.type === "int") {
|
||||
if (this.type === 'int') {
|
||||
this.appendInt53(value)
|
||||
} else if (this.type === "uint") {
|
||||
} else if (this.type === 'uint') {
|
||||
this.appendUint53(value)
|
||||
} else if (this.type === "utf8") {
|
||||
} else if (this.type === 'utf8') {
|
||||
this.appendPrefixedString(value)
|
||||
} else {
|
||||
throw new RangeError(`Unknown RLEEncoder datatype: ${this.type}`)
|
||||
|
@ -854,9 +776,9 @@ class RLEEncoder extends Encoder {
|
|||
* the buffer constructed by this Encoder.
|
||||
*/
|
||||
finish() {
|
||||
if (this.state === "literal") this.literal.push(this.lastValue)
|
||||
if (this.state === 'literal') this.literal.push(this.lastValue)
|
||||
// Don't write anything if the only values we have seen are nulls
|
||||
if (this.state !== "nulls" || this.offset > 0) this.flush()
|
||||
if (this.state !== 'nulls' || this.offset > 0) this.flush()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -878,7 +800,7 @@ class RLEDecoder extends Decoder {
|
|||
* position, and true if we are at the end of the buffer.
|
||||
*/
|
||||
get done() {
|
||||
return this.count === 0 && this.offset === this.buf.byteLength
|
||||
return (this.count === 0) && (this.offset === this.buf.byteLength)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -899,10 +821,9 @@ class RLEDecoder extends Decoder {
|
|||
if (this.done) return null
|
||||
if (this.count === 0) this.readRecord()
|
||||
this.count -= 1
|
||||
if (this.state === "literal") {
|
||||
if (this.state === 'literal') {
|
||||
const value = this.readRawValue()
|
||||
if (value === this.lastValue)
|
||||
throw new RangeError("Repetition of values is not allowed in literal")
|
||||
if (value === this.lastValue) throw new RangeError('Repetition of values is not allowed in literal')
|
||||
this.lastValue = value
|
||||
return value
|
||||
} else {
|
||||
|
@ -918,22 +839,20 @@ class RLEDecoder extends Decoder {
|
|||
if (this.count === 0) {
|
||||
this.count = this.readInt53()
|
||||
if (this.count > 0) {
|
||||
this.lastValue =
|
||||
this.count <= numSkip ? this.skipRawValues(1) : this.readRawValue()
|
||||
this.state = "repetition"
|
||||
this.lastValue = (this.count <= numSkip) ? this.skipRawValues(1) : this.readRawValue()
|
||||
this.state = 'repetition'
|
||||
} else if (this.count < 0) {
|
||||
this.count = -this.count
|
||||
this.state = "literal"
|
||||
} else {
|
||||
// this.count == 0
|
||||
this.state = 'literal'
|
||||
} else { // this.count == 0
|
||||
this.count = this.readUint53()
|
||||
this.lastValue = null
|
||||
this.state = "nulls"
|
||||
this.state = 'nulls'
|
||||
}
|
||||
}
|
||||
|
||||
const consume = Math.min(numSkip, this.count)
|
||||
if (this.state === "literal") this.skipRawValues(consume)
|
||||
if (this.state === 'literal') this.skipRawValues(consume)
|
||||
numSkip -= consume
|
||||
this.count -= consume
|
||||
}
|
||||
|
@ -947,34 +866,23 @@ class RLEDecoder extends Decoder {
|
|||
this.count = this.readInt53()
|
||||
if (this.count > 1) {
|
||||
const value = this.readRawValue()
|
||||
if (
|
||||
(this.state === "repetition" || this.state === "literal") &&
|
||||
this.lastValue === value
|
||||
) {
|
||||
throw new RangeError(
|
||||
"Successive repetitions with the same value are not allowed"
|
||||
)
|
||||
if ((this.state === 'repetition' || this.state === 'literal') && this.lastValue === value) {
|
||||
throw new RangeError('Successive repetitions with the same value are not allowed')
|
||||
}
|
||||
this.state = "repetition"
|
||||
this.state = 'repetition'
|
||||
this.lastValue = value
|
||||
} else if (this.count === 1) {
|
||||
throw new RangeError(
|
||||
"Repetition count of 1 is not allowed, use a literal instead"
|
||||
)
|
||||
throw new RangeError('Repetition count of 1 is not allowed, use a literal instead')
|
||||
} else if (this.count < 0) {
|
||||
this.count = -this.count
|
||||
if (this.state === "literal")
|
||||
throw new RangeError("Successive literals are not allowed")
|
||||
this.state = "literal"
|
||||
} else {
|
||||
// this.count == 0
|
||||
if (this.state === "nulls")
|
||||
throw new RangeError("Successive null runs are not allowed")
|
||||
if (this.state === 'literal') throw new RangeError('Successive literals are not allowed')
|
||||
this.state = 'literal'
|
||||
} else { // this.count == 0
|
||||
if (this.state === 'nulls') throw new RangeError('Successive null runs are not allowed')
|
||||
this.count = this.readUint53()
|
||||
if (this.count === 0)
|
||||
throw new RangeError("Zero-length null runs are not allowed")
|
||||
if (this.count === 0) throw new RangeError('Zero-length null runs are not allowed')
|
||||
this.lastValue = null
|
||||
this.state = "nulls"
|
||||
this.state = 'nulls'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -983,11 +891,11 @@ class RLEDecoder extends Decoder {
|
|||
* Reads one value of the datatype configured on construction.
|
||||
*/
|
||||
readRawValue() {
|
||||
if (this.type === "int") {
|
||||
if (this.type === 'int') {
|
||||
return this.readInt53()
|
||||
} else if (this.type === "uint") {
|
||||
} else if (this.type === 'uint') {
|
||||
return this.readUint53()
|
||||
} else if (this.type === "utf8") {
|
||||
} else if (this.type === 'utf8') {
|
||||
return this.readPrefixedString()
|
||||
} else {
|
||||
throw new RangeError(`Unknown RLEDecoder datatype: ${this.type}`)
|
||||
|
@ -999,14 +907,14 @@ class RLEDecoder extends Decoder {
|
|||
* Skips over `num` values of the datatype configured on construction.
|
||||
*/
|
||||
skipRawValues(num) {
|
||||
if (this.type === "utf8") {
|
||||
if (this.type === 'utf8') {
|
||||
for (let i = 0; i < num; i++) this.skip(this.readUint53())
|
||||
} else {
|
||||
while (num > 0 && this.offset < this.buf.byteLength) {
|
||||
if ((this.buf[this.offset] & 0x80) === 0) num--
|
||||
this.offset++
|
||||
}
|
||||
if (num > 0) throw new RangeError("cannot skip beyond end of buffer")
|
||||
if (num > 0) throw new RangeError('cannot skip beyond end of buffer')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1023,7 +931,7 @@ class RLEDecoder extends Decoder {
|
|||
*/
|
||||
class DeltaEncoder extends RLEEncoder {
|
||||
constructor() {
|
||||
super("int")
|
||||
super('int')
|
||||
this.absoluteValue = 0
|
||||
}
|
||||
|
||||
|
@ -1033,7 +941,7 @@ class DeltaEncoder extends RLEEncoder {
|
|||
*/
|
||||
appendValue(value, repetitions = 1) {
|
||||
if (repetitions <= 0) return
|
||||
if (typeof value === "number") {
|
||||
if (typeof value === 'number') {
|
||||
super.appendValue(value - this.absoluteValue, 1)
|
||||
this.absoluteValue = value
|
||||
if (repetitions > 1) super.appendValue(0, repetitions - 1)
|
||||
|
@ -1049,29 +957,26 @@ class DeltaEncoder extends RLEEncoder {
|
|||
*/
|
||||
copyFrom(decoder, options = {}) {
|
||||
if (options.sumValues) {
|
||||
throw new RangeError("unsupported options for DeltaEncoder.copyFrom()")
|
||||
throw new RangeError('unsupported options for DeltaEncoder.copyFrom()')
|
||||
}
|
||||
if (!(decoder instanceof DeltaDecoder)) {
|
||||
throw new TypeError("incompatible type of decoder")
|
||||
throw new TypeError('incompatible type of decoder')
|
||||
}
|
||||
|
||||
let remaining = options.count
|
||||
if (remaining > 0 && decoder.done)
|
||||
throw new RangeError(`cannot copy ${remaining} values`)
|
||||
if (remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${remaining} values`)
|
||||
if (remaining === 0 || decoder.done) return
|
||||
|
||||
// Copy any null values, and the first non-null value, so that appendValue() computes the
|
||||
// difference between the encoder's last value and the decoder's first (absolute) value.
|
||||
let value = decoder.readValue(),
|
||||
nulls = 0
|
||||
let value = decoder.readValue(), nulls = 0
|
||||
this.appendValue(value)
|
||||
if (value === null) {
|
||||
nulls = decoder.count + 1
|
||||
if (remaining !== undefined && remaining < nulls) nulls = remaining
|
||||
decoder.count -= nulls - 1
|
||||
this.count += nulls - 1
|
||||
if (remaining > nulls && decoder.done)
|
||||
throw new RangeError(`cannot copy ${remaining} values`)
|
||||
if (remaining > nulls && decoder.done) throw new RangeError(`cannot copy ${remaining} values`)
|
||||
if (remaining === nulls || decoder.done) return
|
||||
|
||||
// The next value read is certain to be non-null because we're not at the end of the decoder,
|
||||
|
@ -1084,10 +989,7 @@ class DeltaEncoder extends RLEEncoder {
|
|||
// value, while subsequent values are relative. Thus, the sum of all of the (non-null) copied
|
||||
// values must equal the absolute value of the final element copied.
|
||||
if (remaining !== undefined) remaining -= nulls + 1
|
||||
const { nonNullValues, sum } = super.copyFrom(decoder, {
|
||||
count: remaining,
|
||||
sumValues: true,
|
||||
})
|
||||
const { nonNullValues, sum } = super.copyFrom(decoder, {count: remaining, sumValues: true})
|
||||
if (nonNullValues > 0) {
|
||||
this.absoluteValue = sum
|
||||
decoder.absoluteValue = sum
|
||||
|
@ -1101,7 +1003,7 @@ class DeltaEncoder extends RLEEncoder {
|
|||
*/
|
||||
class DeltaDecoder extends RLEDecoder {
|
||||
constructor(buffer) {
|
||||
super("int", buffer)
|
||||
super('int', buffer)
|
||||
this.absoluteValue = 0
|
||||
}
|
||||
|
||||
|
@ -1134,12 +1036,12 @@ class DeltaDecoder extends RLEDecoder {
|
|||
while (numSkip > 0 && !this.done) {
|
||||
if (this.count === 0) this.readRecord()
|
||||
const consume = Math.min(numSkip, this.count)
|
||||
if (this.state === "literal") {
|
||||
if (this.state === 'literal') {
|
||||
for (let i = 0; i < consume; i++) {
|
||||
this.lastValue = this.readRawValue()
|
||||
this.absoluteValue += this.lastValue
|
||||
}
|
||||
} else if (this.state === "repetition") {
|
||||
} else if (this.state === 'repetition') {
|
||||
this.absoluteValue += consume * this.lastValue
|
||||
}
|
||||
numSkip -= consume
|
||||
|
@ -1188,13 +1090,12 @@ class BooleanEncoder extends Encoder {
|
|||
*/
|
||||
copyFrom(decoder, options = {}) {
|
||||
if (!(decoder instanceof BooleanDecoder)) {
|
||||
throw new TypeError("incompatible type of decoder")
|
||||
throw new TypeError('incompatible type of decoder')
|
||||
}
|
||||
|
||||
const { count } = options
|
||||
let remaining = typeof count === "number" ? count : Number.MAX_SAFE_INTEGER
|
||||
if (count && remaining > 0 && decoder.done)
|
||||
throw new RangeError(`cannot copy ${count} values`)
|
||||
let remaining = (typeof count === 'number' ? count : Number.MAX_SAFE_INTEGER)
|
||||
if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)
|
||||
if (remaining === 0 || decoder.done) return
|
||||
|
||||
// Copy one value to bring decoder and encoder state into sync, then finish that value's repetitions
|
||||
|
@ -1207,8 +1108,7 @@ class BooleanEncoder extends Encoder {
|
|||
|
||||
while (remaining > 0 && !decoder.done) {
|
||||
decoder.count = decoder.readUint53()
|
||||
if (decoder.count === 0)
|
||||
throw new RangeError("Zero-length runs are not allowed")
|
||||
if (decoder.count === 0) throw new RangeError('Zero-length runs are not allowed')
|
||||
decoder.lastValue = !decoder.lastValue
|
||||
this.appendUint53(this.count)
|
||||
|
||||
|
@ -1219,8 +1119,7 @@ class BooleanEncoder extends Encoder {
|
|||
remaining -= numCopied
|
||||
}
|
||||
|
||||
if (count && remaining > 0 && decoder.done)
|
||||
throw new RangeError(`cannot copy ${count} values`)
|
||||
if (count && remaining > 0 && decoder.done) throw new RangeError(`cannot copy ${count} values`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1252,7 +1151,7 @@ class BooleanDecoder extends Decoder {
|
|||
* position, and true if we are at the end of the buffer.
|
||||
*/
|
||||
get done() {
|
||||
return this.count === 0 && this.offset === this.buf.byteLength
|
||||
return (this.count === 0) && (this.offset === this.buf.byteLength)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1275,7 +1174,7 @@ class BooleanDecoder extends Decoder {
|
|||
this.count = this.readUint53()
|
||||
this.lastValue = !this.lastValue
|
||||
if (this.count === 0 && !this.firstRun) {
|
||||
throw new RangeError("Zero-length runs are not allowed")
|
||||
throw new RangeError('Zero-length runs are not allowed')
|
||||
}
|
||||
this.firstRun = false
|
||||
}
|
||||
|
@ -1291,8 +1190,7 @@ class BooleanDecoder extends Decoder {
|
|||
if (this.count === 0) {
|
||||
this.count = this.readUint53()
|
||||
this.lastValue = !this.lastValue
|
||||
if (this.count === 0)
|
||||
throw new RangeError("Zero-length runs are not allowed")
|
||||
if (this.count === 0) throw new RangeError('Zero-length runs are not allowed')
|
||||
}
|
||||
if (this.count < numSkip) {
|
||||
numSkip -= this.count
|
||||
|
@ -1306,16 +1204,6 @@ class BooleanDecoder extends Decoder {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
stringToUtf8,
|
||||
utf8ToString,
|
||||
hexStringToBytes,
|
||||
bytesToHexString,
|
||||
Encoder,
|
||||
Decoder,
|
||||
RLEEncoder,
|
||||
RLEDecoder,
|
||||
DeltaEncoder,
|
||||
DeltaDecoder,
|
||||
BooleanEncoder,
|
||||
BooleanDecoder,
|
||||
stringToUtf8, utf8ToString, hexStringToBytes, bytesToHexString,
|
||||
Encoder, Decoder, RLEEncoder, RLEDecoder, DeltaEncoder, DeltaDecoder, BooleanEncoder, BooleanDecoder
|
||||
}
|
||||
|
|
|
@ -17,14 +17,9 @@
|
|||
*/
|
||||
|
||||
const Backend = null //require('./backend')
|
||||
const {
|
||||
hexStringToBytes,
|
||||
bytesToHexString,
|
||||
Encoder,
|
||||
Decoder,
|
||||
} = require("./encoding")
|
||||
const { decodeChangeMeta } = require("./columnar")
|
||||
const { copyObject } = require("./common")
|
||||
const { hexStringToBytes, bytesToHexString, Encoder, Decoder } = require('./encoding')
|
||||
const { decodeChangeMeta } = require('./columnar')
|
||||
const { copyObject } = require('./common')
|
||||
|
||||
const HASH_SIZE = 32 // 256 bits = 32 bytes
|
||||
const MESSAGE_TYPE_SYNC = 0x42 // first byte of a sync message, for identification
|
||||
|
@ -33,8 +28,7 @@ const PEER_STATE_TYPE = 0x43 // first byte of an encoded peer state, for identif
|
|||
// These constants correspond to a 1% false positive rate. The values can be changed without
|
||||
// breaking compatibility of the network protocol, since the parameters used for a particular
|
||||
// Bloom filter are encoded in the wire format.
|
||||
const BITS_PER_ENTRY = 10,
|
||||
NUM_PROBES = 7
|
||||
const BITS_PER_ENTRY = 10, NUM_PROBES = 7
|
||||
|
||||
/**
|
||||
* A Bloom filter implementation that can be serialised to a byte array for transmission
|
||||
|
@ -42,15 +36,13 @@ const BITS_PER_ENTRY = 10,
|
|||
* so this implementation does not perform its own hashing.
|
||||
*/
|
||||
class BloomFilter {
|
||||
constructor(arg) {
|
||||
constructor (arg) {
|
||||
if (Array.isArray(arg)) {
|
||||
// arg is an array of SHA256 hashes in hexadecimal encoding
|
||||
this.numEntries = arg.length
|
||||
this.numBitsPerEntry = BITS_PER_ENTRY
|
||||
this.numProbes = NUM_PROBES
|
||||
this.bits = new Uint8Array(
|
||||
Math.ceil((this.numEntries * this.numBitsPerEntry) / 8)
|
||||
)
|
||||
this.bits = new Uint8Array(Math.ceil(this.numEntries * this.numBitsPerEntry / 8))
|
||||
for (let hash of arg) this.addHash(hash)
|
||||
} else if (arg instanceof Uint8Array) {
|
||||
if (arg.byteLength === 0) {
|
||||
|
@ -63,12 +55,10 @@ class BloomFilter {
|
|||
this.numEntries = decoder.readUint32()
|
||||
this.numBitsPerEntry = decoder.readUint32()
|
||||
this.numProbes = decoder.readUint32()
|
||||
this.bits = decoder.readRawBytes(
|
||||
Math.ceil((this.numEntries * this.numBitsPerEntry) / 8)
|
||||
)
|
||||
this.bits = decoder.readRawBytes(Math.ceil(this.numEntries * this.numBitsPerEntry / 8))
|
||||
}
|
||||
} else {
|
||||
throw new TypeError("invalid argument")
|
||||
throw new TypeError('invalid argument')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,32 +86,12 @@ class BloomFilter {
|
|||
* http://www.ccis.northeastern.edu/home/pete/pub/bloom-filters-verification.pdf
|
||||
*/
|
||||
getProbes(hash) {
|
||||
const hashBytes = hexStringToBytes(hash),
|
||||
modulo = 8 * this.bits.byteLength
|
||||
if (hashBytes.byteLength !== 32)
|
||||
throw new RangeError(`Not a 256-bit hash: ${hash}`)
|
||||
const hashBytes = hexStringToBytes(hash), modulo = 8 * this.bits.byteLength
|
||||
if (hashBytes.byteLength !== 32) throw new RangeError(`Not a 256-bit hash: ${hash}`)
|
||||
// on the next three lines, the right shift means interpret value as unsigned
|
||||
let x =
|
||||
((hashBytes[0] |
|
||||
(hashBytes[1] << 8) |
|
||||
(hashBytes[2] << 16) |
|
||||
(hashBytes[3] << 24)) >>>
|
||||
0) %
|
||||
modulo
|
||||
let y =
|
||||
((hashBytes[4] |
|
||||
(hashBytes[5] << 8) |
|
||||
(hashBytes[6] << 16) |
|
||||
(hashBytes[7] << 24)) >>>
|
||||
0) %
|
||||
modulo
|
||||
let z =
|
||||
((hashBytes[8] |
|
||||
(hashBytes[9] << 8) |
|
||||
(hashBytes[10] << 16) |
|
||||
(hashBytes[11] << 24)) >>>
|
||||
0) %
|
||||
modulo
|
||||
let x = ((hashBytes[0] | hashBytes[1] << 8 | hashBytes[2] << 16 | hashBytes[3] << 24) >>> 0) % modulo
|
||||
let y = ((hashBytes[4] | hashBytes[5] << 8 | hashBytes[6] << 16 | hashBytes[7] << 24) >>> 0) % modulo
|
||||
let z = ((hashBytes[8] | hashBytes[9] << 8 | hashBytes[10] << 16 | hashBytes[11] << 24) >>> 0) % modulo
|
||||
const probes = [x]
|
||||
for (let i = 1; i < this.numProbes; i++) {
|
||||
x = (x + y) % modulo
|
||||
|
@ -158,14 +128,12 @@ class BloomFilter {
|
|||
* Encodes a sorted array of SHA-256 hashes (as hexadecimal strings) into a byte array.
|
||||
*/
|
||||
function encodeHashes(encoder, hashes) {
|
||||
if (!Array.isArray(hashes)) throw new TypeError("hashes must be an array")
|
||||
if (!Array.isArray(hashes)) throw new TypeError('hashes must be an array')
|
||||
encoder.appendUint32(hashes.length)
|
||||
for (let i = 0; i < hashes.length; i++) {
|
||||
if (i > 0 && hashes[i - 1] >= hashes[i])
|
||||
throw new RangeError("hashes must be sorted")
|
||||
if (i > 0 && hashes[i - 1] >= hashes[i]) throw new RangeError('hashes must be sorted')
|
||||
const bytes = hexStringToBytes(hashes[i])
|
||||
if (bytes.byteLength !== HASH_SIZE)
|
||||
throw new TypeError("heads hashes must be 256 bits")
|
||||
if (bytes.byteLength !== HASH_SIZE) throw new TypeError('heads hashes must be 256 bits')
|
||||
encoder.appendRawBytes(bytes)
|
||||
}
|
||||
}
|
||||
|
@ -175,8 +143,7 @@ function encodeHashes(encoder, hashes) {
|
|||
* array of hex strings.
|
||||
*/
|
||||
function decodeHashes(decoder) {
|
||||
let length = decoder.readUint32(),
|
||||
hashes = []
|
||||
let length = decoder.readUint32(), hashes = []
|
||||
for (let i = 0; i < length; i++) {
|
||||
hashes.push(bytesToHexString(decoder.readRawBytes(HASH_SIZE)))
|
||||
}
|
||||
|
@ -216,11 +183,11 @@ function decodeSyncMessage(bytes) {
|
|||
const heads = decodeHashes(decoder)
|
||||
const need = decodeHashes(decoder)
|
||||
const haveCount = decoder.readUint32()
|
||||
let message = { heads, need, have: [], changes: [] }
|
||||
let message = {heads, need, have: [], changes: []}
|
||||
for (let i = 0; i < haveCount; i++) {
|
||||
const lastSync = decodeHashes(decoder)
|
||||
const bloom = decoder.readPrefixedBytes(decoder)
|
||||
message.have.push({ lastSync, bloom })
|
||||
message.have.push({lastSync, bloom})
|
||||
}
|
||||
const changeCount = decoder.readUint32()
|
||||
for (let i = 0; i < changeCount; i++) {
|
||||
|
@ -267,7 +234,7 @@ function decodeSyncState(bytes) {
|
|||
function makeBloomFilter(backend, lastSync) {
|
||||
const newChanges = Backend.getChanges(backend, lastSync)
|
||||
const hashes = newChanges.map(change => decodeChangeMeta(change, true).hash)
|
||||
return { lastSync, bloom: new BloomFilter(hashes).bytes }
|
||||
return {lastSync, bloom: new BloomFilter(hashes).bytes}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -278,26 +245,20 @@ function makeBloomFilter(backend, lastSync) {
|
|||
*/
|
||||
function getChangesToSend(backend, have, need) {
|
||||
if (have.length === 0) {
|
||||
return need
|
||||
.map(hash => Backend.getChangeByHash(backend, hash))
|
||||
.filter(change => change !== undefined)
|
||||
return need.map(hash => Backend.getChangeByHash(backend, hash)).filter(change => change !== undefined)
|
||||
}
|
||||
|
||||
let lastSyncHashes = {},
|
||||
bloomFilters = []
|
||||
let lastSyncHashes = {}, bloomFilters = []
|
||||
for (let h of have) {
|
||||
for (let hash of h.lastSync) lastSyncHashes[hash] = true
|
||||
bloomFilters.push(new BloomFilter(h.bloom))
|
||||
}
|
||||
|
||||
// Get all changes that were added since the last sync
|
||||
const changes = Backend.getChanges(backend, Object.keys(lastSyncHashes)).map(
|
||||
change => decodeChangeMeta(change, true)
|
||||
)
|
||||
const changes = Backend.getChanges(backend, Object.keys(lastSyncHashes))
|
||||
.map(change => decodeChangeMeta(change, true))
|
||||
|
||||
let changeHashes = {},
|
||||
dependents = {},
|
||||
hashesToSend = {}
|
||||
let changeHashes = {}, dependents = {}, hashesToSend = {}
|
||||
for (let change of changes) {
|
||||
changeHashes[change.hash] = true
|
||||
|
||||
|
@ -331,8 +292,7 @@ function getChangesToSend(backend, have, need) {
|
|||
let changesToSend = []
|
||||
for (let hash of need) {
|
||||
hashesToSend[hash] = true
|
||||
if (!changeHashes[hash]) {
|
||||
// Change is not among those returned by getMissingChanges()?
|
||||
if (!changeHashes[hash]) { // Change is not among those returned by getMissingChanges()?
|
||||
const change = Backend.getChangeByHash(backend, hash)
|
||||
if (change) changesToSend.push(change)
|
||||
}
|
||||
|
@ -357,7 +317,7 @@ function initSyncState() {
|
|||
}
|
||||
|
||||
function compareArrays(a, b) {
|
||||
return a.length === b.length && a.every((v, i) => v === b[i])
|
||||
return (a.length === b.length) && a.every((v, i) => v === b[i])
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -369,19 +329,10 @@ function generateSyncMessage(backend, syncState) {
|
|||
throw new Error("generateSyncMessage called with no Automerge document")
|
||||
}
|
||||
if (!syncState) {
|
||||
throw new Error(
|
||||
"generateSyncMessage requires a syncState, which can be created with initSyncState()"
|
||||
)
|
||||
throw new Error("generateSyncMessage requires a syncState, which can be created with initSyncState()")
|
||||
}
|
||||
|
||||
let {
|
||||
sharedHeads,
|
||||
lastSentHeads,
|
||||
theirHeads,
|
||||
theirNeed,
|
||||
theirHave,
|
||||
sentHashes,
|
||||
} = syncState
|
||||
let { sharedHeads, lastSentHeads, theirHeads, theirNeed, theirHave, sentHashes } = syncState
|
||||
const ourHeads = Backend.getHeads(backend)
|
||||
|
||||
// Hashes to explicitly request from the remote peer: any missing dependencies of unapplied
|
||||
|
@ -405,28 +356,18 @@ function generateSyncMessage(backend, syncState) {
|
|||
const lastSync = theirHave[0].lastSync
|
||||
if (!lastSync.every(hash => Backend.getChangeByHash(backend, hash))) {
|
||||
// we need to queue them to send us a fresh sync message, the one they sent is uninteligible so we don't know what they need
|
||||
const resetMsg = {
|
||||
heads: ourHeads,
|
||||
need: [],
|
||||
have: [{ lastSync: [], bloom: new Uint8Array(0) }],
|
||||
changes: [],
|
||||
}
|
||||
const resetMsg = {heads: ourHeads, need: [], have: [{ lastSync: [], bloom: new Uint8Array(0) }], changes: []}
|
||||
return [syncState, encodeSyncMessage(resetMsg)]
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: we should limit ourselves to only sending a subset of all the messages, probably limited by a total message size
|
||||
// these changes should ideally be RLE encoded but we haven't implemented that yet.
|
||||
let changesToSend =
|
||||
Array.isArray(theirHave) && Array.isArray(theirNeed)
|
||||
? getChangesToSend(backend, theirHave, theirNeed)
|
||||
: []
|
||||
let changesToSend = Array.isArray(theirHave) && Array.isArray(theirNeed) ? getChangesToSend(backend, theirHave, theirNeed) : []
|
||||
|
||||
// If the heads are equal, we're in sync and don't need to do anything further
|
||||
const headsUnchanged =
|
||||
Array.isArray(lastSentHeads) && compareArrays(ourHeads, lastSentHeads)
|
||||
const headsEqual =
|
||||
Array.isArray(theirHeads) && compareArrays(ourHeads, theirHeads)
|
||||
const headsUnchanged = Array.isArray(lastSentHeads) && compareArrays(ourHeads, lastSentHeads)
|
||||
const headsEqual = Array.isArray(theirHeads) && compareArrays(ourHeads, theirHeads)
|
||||
if (headsUnchanged && headsEqual && changesToSend.length === 0) {
|
||||
// no need to send a sync message if we know we're synced!
|
||||
return [syncState, null]
|
||||
|
@ -434,19 +375,12 @@ function generateSyncMessage(backend, syncState) {
|
|||
|
||||
// TODO: this recomputes the SHA-256 hash of each change; we should restructure this to avoid the
|
||||
// unnecessary recomputation
|
||||
changesToSend = changesToSend.filter(
|
||||
change => !sentHashes[decodeChangeMeta(change, true).hash]
|
||||
)
|
||||
changesToSend = changesToSend.filter(change => !sentHashes[decodeChangeMeta(change, true).hash])
|
||||
|
||||
// Regular response to a sync message: send any changes that the other node
|
||||
// doesn't have. We leave the "have" field empty because the previous message
|
||||
// generated by `syncStart` already indicated what changes we have.
|
||||
const syncMessage = {
|
||||
heads: ourHeads,
|
||||
have: ourHave,
|
||||
need: ourNeed,
|
||||
changes: changesToSend,
|
||||
}
|
||||
const syncMessage = {heads: ourHeads, have: ourHave, need: ourNeed, changes: changesToSend}
|
||||
if (changesToSend.length > 0) {
|
||||
sentHashes = copyObject(sentHashes)
|
||||
for (const change of changesToSend) {
|
||||
|
@ -454,10 +388,7 @@ function generateSyncMessage(backend, syncState) {
|
|||
}
|
||||
}
|
||||
|
||||
syncState = Object.assign({}, syncState, {
|
||||
lastSentHeads: ourHeads,
|
||||
sentHashes,
|
||||
})
|
||||
syncState = Object.assign({}, syncState, {lastSentHeads: ourHeads, sentHashes})
|
||||
return [syncState, encodeSyncMessage(syncMessage)]
|
||||
}
|
||||
|
||||
|
@ -475,14 +406,13 @@ function generateSyncMessage(backend, syncState) {
|
|||
* another peer, that means that peer had those changes, and therefore we now both know about them.
|
||||
*/
|
||||
function advanceHeads(myOldHeads, myNewHeads, ourOldSharedHeads) {
|
||||
const newHeads = myNewHeads.filter(head => !myOldHeads.includes(head))
|
||||
const commonHeads = ourOldSharedHeads.filter(head =>
|
||||
myNewHeads.includes(head)
|
||||
)
|
||||
const newHeads = myNewHeads.filter((head) => !myOldHeads.includes(head))
|
||||
const commonHeads = ourOldSharedHeads.filter((head) => myNewHeads.includes(head))
|
||||
const advancedHeads = [...new Set([...newHeads, ...commonHeads])].sort()
|
||||
return advancedHeads
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given a backend, a message message and the state of our peer, apply any changes, update what
|
||||
* we believe about the peer, and (if there were applied changes) produce a patch for the frontend
|
||||
|
@ -492,13 +422,10 @@ function receiveSyncMessage(backend, oldSyncState, binaryMessage) {
|
|||
throw new Error("generateSyncMessage called with no Automerge document")
|
||||
}
|
||||
if (!oldSyncState) {
|
||||
throw new Error(
|
||||
"generateSyncMessage requires a syncState, which can be created with initSyncState()"
|
||||
)
|
||||
throw new Error("generateSyncMessage requires a syncState, which can be created with initSyncState()")
|
||||
}
|
||||
|
||||
let { sharedHeads, lastSentHeads, sentHashes } = oldSyncState,
|
||||
patch = null
|
||||
let { sharedHeads, lastSentHeads, sentHashes } = oldSyncState, patch = null
|
||||
const message = decodeSyncMessage(binaryMessage)
|
||||
const beforeHeads = Backend.getHeads(backend)
|
||||
|
||||
|
@ -507,27 +434,18 @@ function receiveSyncMessage(backend, oldSyncState, binaryMessage) {
|
|||
// changes without applying them. The set of changes may also be incomplete if the sender decided
|
||||
// to break a large set of changes into chunks.
|
||||
if (message.changes.length > 0) {
|
||||
;[backend, patch] = Backend.applyChanges(backend, message.changes)
|
||||
sharedHeads = advanceHeads(
|
||||
beforeHeads,
|
||||
Backend.getHeads(backend),
|
||||
sharedHeads
|
||||
)
|
||||
[backend, patch] = Backend.applyChanges(backend, message.changes)
|
||||
sharedHeads = advanceHeads(beforeHeads, Backend.getHeads(backend), sharedHeads)
|
||||
}
|
||||
|
||||
// If heads are equal, indicate we don't need to send a response message
|
||||
if (
|
||||
message.changes.length === 0 &&
|
||||
compareArrays(message.heads, beforeHeads)
|
||||
) {
|
||||
if (message.changes.length === 0 && compareArrays(message.heads, beforeHeads)) {
|
||||
lastSentHeads = message.heads
|
||||
}
|
||||
|
||||
// If all of the remote heads are known to us, that means either our heads are equal, or we are
|
||||
// ahead of the remote peer. In this case, take the remote heads to be our shared heads.
|
||||
const knownHeads = message.heads.filter(head =>
|
||||
Backend.getChangeByHash(backend, head)
|
||||
)
|
||||
const knownHeads = message.heads.filter(head => Backend.getChangeByHash(backend, head))
|
||||
if (knownHeads.length === message.heads.length) {
|
||||
sharedHeads = message.heads
|
||||
// If the remote peer has lost all its data, reset our state to perform a full resync
|
||||
|
@ -549,18 +467,14 @@ function receiveSyncMessage(backend, oldSyncState, binaryMessage) {
|
|||
theirHave: message.have, // the information we need to calculate the changes they need
|
||||
theirHeads: message.heads,
|
||||
theirNeed: message.need,
|
||||
sentHashes,
|
||||
sentHashes
|
||||
}
|
||||
return [backend, syncState, patch]
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
receiveSyncMessage,
|
||||
generateSyncMessage,
|
||||
encodeSyncMessage,
|
||||
decodeSyncMessage,
|
||||
initSyncState,
|
||||
encodeSyncState,
|
||||
decodeSyncState,
|
||||
BloomFilter, // BloomFilter is a private API, exported only for testing purposes
|
||||
receiveSyncMessage, generateSyncMessage,
|
||||
encodeSyncMessage, decodeSyncMessage,
|
||||
initSyncState, encodeSyncState, decodeSyncState,
|
||||
BloomFilter // BloomFilter is a private API, exported only for testing purposes
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,99 +0,0 @@
|
|||
import * as assert from "assert"
|
||||
import * as stable from "../src"
|
||||
import { unstable } from "../src"
|
||||
|
||||
describe("stable/unstable interop", () => {
|
||||
it("should allow reading Text from stable as strings in unstable", () => {
|
||||
let stableDoc = stable.from({
|
||||
text: new stable.Text("abc"),
|
||||
})
|
||||
let unstableDoc = unstable.init<any>()
|
||||
unstableDoc = unstable.merge(unstableDoc, stableDoc)
|
||||
assert.deepStrictEqual(unstableDoc.text, "abc")
|
||||
})
|
||||
|
||||
it("should allow string from stable as Text in unstable", () => {
|
||||
let unstableDoc = unstable.from({
|
||||
text: "abc",
|
||||
})
|
||||
let stableDoc = stable.init<any>()
|
||||
stableDoc = unstable.merge(stableDoc, unstableDoc)
|
||||
assert.deepStrictEqual(stableDoc.text, new stable.Text("abc"))
|
||||
})
|
||||
|
||||
it("should allow reading strings from stable as RawString in unstable", () => {
|
||||
let stableDoc = stable.from({
|
||||
text: "abc",
|
||||
})
|
||||
let unstableDoc = unstable.init<any>()
|
||||
unstableDoc = unstable.merge(unstableDoc, stableDoc)
|
||||
assert.deepStrictEqual(unstableDoc.text, new unstable.RawString("abc"))
|
||||
})
|
||||
|
||||
it("should allow reading RawString from unstable as string in stable", () => {
|
||||
let unstableDoc = unstable.from({
|
||||
text: new unstable.RawString("abc"),
|
||||
})
|
||||
let stableDoc = stable.init<any>()
|
||||
stableDoc = unstable.merge(stableDoc, unstableDoc)
|
||||
assert.deepStrictEqual(stableDoc.text, "abc")
|
||||
})
|
||||
|
||||
it("should show conflicts on text objects", () => {
|
||||
let doc1 = stable.from({ text: new stable.Text("abc") }, "bb")
|
||||
let doc2 = stable.from({ text: new stable.Text("def") }, "aa")
|
||||
doc1 = stable.merge(doc1, doc2)
|
||||
let conflicts = stable.getConflicts(doc1, "text")!
|
||||
assert.equal(conflicts["1@bb"]!.toString(), "abc")
|
||||
assert.equal(conflicts["1@aa"]!.toString(), "def")
|
||||
|
||||
let unstableDoc = unstable.init<any>()
|
||||
unstableDoc = unstable.merge(unstableDoc, doc1)
|
||||
let conflicts2 = unstable.getConflicts(unstableDoc, "text")!
|
||||
assert.equal(conflicts2["1@bb"]!.toString(), "abc")
|
||||
assert.equal(conflicts2["1@aa"]!.toString(), "def")
|
||||
})
|
||||
|
||||
it("should allow filling a list with text in stable", () => {
|
||||
let doc = stable.from<{ list: Array<stable.Text | null> }>({
|
||||
list: [null, null, null],
|
||||
})
|
||||
doc = stable.change(doc, doc => {
|
||||
doc.list.fill(new stable.Text("abc"), 0, 3)
|
||||
})
|
||||
assert.deepStrictEqual(doc.list, [
|
||||
new stable.Text("abc"),
|
||||
new stable.Text("abc"),
|
||||
new stable.Text("abc"),
|
||||
])
|
||||
})
|
||||
|
||||
it("should allow filling a list with text in unstable", () => {
|
||||
let doc = unstable.from<{ list: Array<string | null> }>({
|
||||
list: [null, null, null],
|
||||
})
|
||||
doc = stable.change(doc, doc => {
|
||||
doc.list.fill("abc", 0, 3)
|
||||
})
|
||||
assert.deepStrictEqual(doc.list, ["abc", "abc", "abc"])
|
||||
})
|
||||
|
||||
it("should allow splicing text into a list on stable", () => {
|
||||
let doc = stable.from<{ list: Array<stable.Text> }>({ list: [] })
|
||||
doc = stable.change(doc, doc => {
|
||||
doc.list.splice(0, 0, new stable.Text("abc"), new stable.Text("def"))
|
||||
})
|
||||
assert.deepStrictEqual(doc.list, [
|
||||
new stable.Text("abc"),
|
||||
new stable.Text("def"),
|
||||
])
|
||||
})
|
||||
|
||||
it("should allow splicing text into a list on unstable", () => {
|
||||
let doc = unstable.from<{ list: Array<string> }>({ list: [] })
|
||||
doc = unstable.change(doc, doc => {
|
||||
doc.list.splice(0, 0, "abc", "def")
|
||||
})
|
||||
assert.deepStrictEqual(doc.list, ["abc", "def"])
|
||||
})
|
||||
})
|
File diff suppressed because it is too large
Load diff
|
@ -1,111 +1,698 @@
|
|||
import * as assert from "assert"
|
||||
import { unstable as Automerge } from "../src"
|
||||
import { assertEqualsOneOf } from "./helpers"
|
||||
import * as assert from 'assert'
|
||||
import * as Automerge from '../src'
|
||||
import { assertEqualsOneOf } from './helpers'
|
||||
|
||||
type DocType = {
|
||||
text: string
|
||||
[key: string]: any
|
||||
function attributeStateToAttributes(accumulatedAttributes) {
|
||||
const attributes = {}
|
||||
Object.entries(accumulatedAttributes).forEach(([key, values]) => {
|
||||
if (values.length && values[0] !== null) {
|
||||
attributes[key] = values[0]
|
||||
}
|
||||
})
|
||||
return attributes
|
||||
}
|
||||
|
||||
describe("Automerge.Text", () => {
|
||||
let s1: Automerge.Doc<DocType>, s2: Automerge.Doc<DocType>
|
||||
beforeEach(() => {
|
||||
s1 = Automerge.change(Automerge.init<DocType>(), doc => (doc.text = ""))
|
||||
s2 = Automerge.merge(Automerge.init<DocType>(), s1)
|
||||
function isEquivalent(a, b) {
|
||||
const aProps = Object.getOwnPropertyNames(a)
|
||||
const bProps = Object.getOwnPropertyNames(b)
|
||||
|
||||
if (aProps.length != bProps.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (let i = 0; i < aProps.length; i++) {
|
||||
const propName = aProps[i]
|
||||
if (a[propName] !== b[propName]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function isControlMarker(pseudoCharacter) {
|
||||
return typeof pseudoCharacter === 'object' && pseudoCharacter.attributes
|
||||
}
|
||||
|
||||
function opFrom(text, attributes) {
|
||||
let op = { insert: text }
|
||||
if (Object.keys(attributes).length > 0) {
|
||||
op.attributes = attributes
|
||||
}
|
||||
return op
|
||||
}
|
||||
|
||||
function accumulateAttributes(span, accumulatedAttributes) {
|
||||
Object.entries(span).forEach(([key, value]) => {
|
||||
if (!accumulatedAttributes[key]) {
|
||||
accumulatedAttributes[key] = []
|
||||
}
|
||||
if (value === null) {
|
||||
if (accumulatedAttributes[key].length === 0 || accumulatedAttributes[key] === null) {
|
||||
accumulatedAttributes[key].unshift(null)
|
||||
} else {
|
||||
accumulatedAttributes[key].shift()
|
||||
}
|
||||
} else {
|
||||
if (accumulatedAttributes[key][0] === null) {
|
||||
accumulatedAttributes[key].shift()
|
||||
} else {
|
||||
accumulatedAttributes[key].unshift(value)
|
||||
}
|
||||
}
|
||||
})
|
||||
return accumulatedAttributes
|
||||
}
|
||||
|
||||
function automergeTextToDeltaDoc(text) {
|
||||
let ops = []
|
||||
let controlState = {}
|
||||
let currentString = ""
|
||||
let attributes = {}
|
||||
text.toSpans().forEach((span) => {
|
||||
if (isControlMarker(span)) {
|
||||
controlState = accumulateAttributes(span.attributes, controlState)
|
||||
} else {
|
||||
let next = attributeStateToAttributes(controlState)
|
||||
|
||||
// if the next span has the same calculated attributes as the current span
|
||||
// don't bother outputting it as a separate span, just let it ride
|
||||
if (typeof span === 'string' && isEquivalent(next, attributes)) {
|
||||
currentString = currentString + span
|
||||
return
|
||||
}
|
||||
|
||||
if (currentString) {
|
||||
ops.push(opFrom(currentString, attributes))
|
||||
}
|
||||
|
||||
// If we've got a string, we might be able to concatenate it to another
|
||||
// same-attributed-string, so remember it and go to the next iteration.
|
||||
if (typeof span === 'string') {
|
||||
currentString = span
|
||||
attributes = next
|
||||
} else {
|
||||
// otherwise we have an embed "character" and should output it immediately.
|
||||
// embeds are always one-"character" in length.
|
||||
ops.push(opFrom(span, next))
|
||||
currentString = ''
|
||||
attributes = {}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it("should support insertion", () => {
|
||||
s1 = Automerge.change(s1, doc => Automerge.splice(doc, "text", 0, 0, "a"))
|
||||
// at the end, flush any accumulated string out
|
||||
if (currentString) {
|
||||
ops.push(opFrom(currentString, attributes))
|
||||
}
|
||||
|
||||
return ops
|
||||
}
|
||||
|
||||
function inverseAttributes(attributes) {
|
||||
let invertedAttributes = {}
|
||||
Object.keys(attributes).forEach((key) => {
|
||||
invertedAttributes[key] = null
|
||||
})
|
||||
return invertedAttributes
|
||||
}
|
||||
|
||||
function applyDeleteOp(text, offset, op) {
|
||||
let length = op.delete
|
||||
while (length > 0) {
|
||||
if (isControlMarker(text.get(offset))) {
|
||||
offset += 1
|
||||
} else {
|
||||
// we need to not delete control characters, but we do delete embed characters
|
||||
text.deleteAt(offset, 1)
|
||||
length -= 1
|
||||
}
|
||||
}
|
||||
return [text, offset]
|
||||
}
|
||||
|
||||
function applyRetainOp(text, offset, op) {
|
||||
let length = op.retain
|
||||
|
||||
if (op.attributes) {
|
||||
text.insertAt(offset, { attributes: op.attributes })
|
||||
offset += 1
|
||||
}
|
||||
|
||||
while (length > 0) {
|
||||
const char = text.get(offset)
|
||||
offset += 1
|
||||
if (!isControlMarker(char)) {
|
||||
length -= 1
|
||||
}
|
||||
}
|
||||
|
||||
if (op.attributes) {
|
||||
text.insertAt(offset, { attributes: inverseAttributes(op.attributes) })
|
||||
offset += 1
|
||||
}
|
||||
|
||||
return [text, offset]
|
||||
}
|
||||
|
||||
|
||||
function applyInsertOp(text, offset, op) {
|
||||
let originalOffset = offset
|
||||
|
||||
if (typeof op.insert === 'string') {
|
||||
text.insertAt(offset, ...op.insert.split(''))
|
||||
offset += op.insert.length
|
||||
} else {
|
||||
// we have an embed or something similar
|
||||
text.insertAt(offset, op.insert)
|
||||
offset += 1
|
||||
}
|
||||
|
||||
if (op.attributes) {
|
||||
text.insertAt(originalOffset, { attributes: op.attributes })
|
||||
offset += 1
|
||||
}
|
||||
if (op.attributes) {
|
||||
text.insertAt(offset, { attributes: inverseAttributes(op.attributes) })
|
||||
offset += 1
|
||||
}
|
||||
return [text, offset]
|
||||
}
|
||||
|
||||
// XXX: uhhhhh, why can't I pass in text?
|
||||
function applyDeltaDocToAutomergeText(delta, doc) {
|
||||
let offset = 0
|
||||
|
||||
delta.forEach(op => {
|
||||
if (op.retain) {
|
||||
[, offset] = applyRetainOp(doc.text, offset, op)
|
||||
} else if (op.delete) {
|
||||
[, offset] = applyDeleteOp(doc.text, offset, op)
|
||||
} else if (op.insert) {
|
||||
[, offset] = applyInsertOp(doc.text, offset, op)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('Automerge.Text', () => {
|
||||
let s1, s2
|
||||
beforeEach(() => {
|
||||
s1 = Automerge.change(Automerge.init(), doc => doc.text = new Automerge.Text())
|
||||
s2 = Automerge.merge(Automerge.init(), s1)
|
||||
})
|
||||
|
||||
it('should support insertion', () => {
|
||||
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, 'a'))
|
||||
assert.strictEqual(s1.text.length, 1)
|
||||
assert.strictEqual(s1.text[0], "a")
|
||||
assert.strictEqual(s1.text, "a")
|
||||
assert.strictEqual(s1.text.get(0), 'a')
|
||||
assert.strictEqual(s1.text.toString(), 'a')
|
||||
//assert.strictEqual(s1.text.getElemId(0), `2@${Automerge.getActorId(s1)}`)
|
||||
})
|
||||
|
||||
it("should support deletion", () => {
|
||||
s1 = Automerge.change(s1, doc => Automerge.splice(doc, "text", 0, 0, "abc"))
|
||||
s1 = Automerge.change(s1, doc => Automerge.splice(doc, "text", 1, 1))
|
||||
it('should support deletion', () => {
|
||||
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, 'a', 'b', 'c'))
|
||||
s1 = Automerge.change(s1, doc => doc.text.deleteAt(1, 1))
|
||||
assert.strictEqual(s1.text.length, 2)
|
||||
assert.strictEqual(s1.text[0], "a")
|
||||
assert.strictEqual(s1.text[1], "c")
|
||||
assert.strictEqual(s1.text, "ac")
|
||||
assert.strictEqual(s1.text.get(0), 'a')
|
||||
assert.strictEqual(s1.text.get(1), 'c')
|
||||
assert.strictEqual(s1.text.toString(), 'ac')
|
||||
})
|
||||
|
||||
it("should support implicit and explicit deletion", () => {
|
||||
s1 = Automerge.change(s1, doc => Automerge.splice(doc, "text", 0, 0, "abc"))
|
||||
s1 = Automerge.change(s1, doc => Automerge.splice(doc, "text", 1, 1))
|
||||
s1 = Automerge.change(s1, doc => Automerge.splice(doc, "text", 1, 0))
|
||||
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, "a", "b", "c"))
|
||||
s1 = Automerge.change(s1, doc => doc.text.deleteAt(1))
|
||||
s1 = Automerge.change(s1, doc => doc.text.deleteAt(1, 0))
|
||||
assert.strictEqual(s1.text.length, 2)
|
||||
assert.strictEqual(s1.text[0], "a")
|
||||
assert.strictEqual(s1.text[1], "c")
|
||||
assert.strictEqual(s1.text, "ac")
|
||||
assert.strictEqual(s1.text.get(0), "a")
|
||||
assert.strictEqual(s1.text.get(1), "c")
|
||||
assert.strictEqual(s1.text.toString(), "ac")
|
||||
})
|
||||
|
||||
it("should handle concurrent insertion", () => {
|
||||
s1 = Automerge.change(s1, doc => Automerge.splice(doc, "text", 0, 0, "abc"))
|
||||
s2 = Automerge.change(s2, doc => Automerge.splice(doc, "text", 0, 0, "xyz"))
|
||||
it('should handle concurrent insertion', () => {
|
||||
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, 'a', 'b', 'c'))
|
||||
s2 = Automerge.change(s2, doc => doc.text.insertAt(0, 'x', 'y', 'z'))
|
||||
s1 = Automerge.merge(s1, s2)
|
||||
assert.strictEqual(s1.text.length, 6)
|
||||
assertEqualsOneOf(s1.text, "abcxyz", "xyzabc")
|
||||
assertEqualsOneOf(s1.text.toString(), 'abcxyz', 'xyzabc')
|
||||
assertEqualsOneOf(s1.text.join(''), 'abcxyz', 'xyzabc')
|
||||
})
|
||||
|
||||
it("should handle text and other ops in the same change", () => {
|
||||
it('should handle text and other ops in the same change', () => {
|
||||
s1 = Automerge.change(s1, doc => {
|
||||
doc.foo = "bar"
|
||||
Automerge.splice(doc, "text", 0, 0, "a")
|
||||
doc.foo = 'bar'
|
||||
doc.text.insertAt(0, 'a')
|
||||
})
|
||||
assert.strictEqual(s1.foo, "bar")
|
||||
assert.strictEqual(s1.text, "a")
|
||||
assert.strictEqual(s1.text, "a")
|
||||
assert.strictEqual(s1.foo, 'bar')
|
||||
assert.strictEqual(s1.text.toString(), 'a')
|
||||
assert.strictEqual(s1.text.join(''), 'a')
|
||||
})
|
||||
|
||||
it("should serialize to JSON as a simple string", () => {
|
||||
s1 = Automerge.change(s1, doc => Automerge.splice(doc, "text", 0, 0, 'a"b'))
|
||||
it('should serialize to JSON as a simple string', () => {
|
||||
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, 'a', '"', 'b'))
|
||||
assert.strictEqual(JSON.stringify(s1), '{"text":"a\\"b"}')
|
||||
})
|
||||
|
||||
it("should allow modification after an object is assigned to a document", () => {
|
||||
it('should allow modification before an object is assigned to a document', () => {
|
||||
s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = ""
|
||||
Automerge.splice(doc, "text", 0, 0, "abcd")
|
||||
Automerge.splice(doc, "text", 2, 1)
|
||||
assert.strictEqual(doc.text, "abd")
|
||||
const text = new Automerge.Text()
|
||||
text.insertAt(0, 'a', 'b', 'c', 'd')
|
||||
text.deleteAt(2)
|
||||
doc.text = text
|
||||
assert.strictEqual(doc.text.toString(), 'abd')
|
||||
assert.strictEqual(doc.text.join(''), 'abd')
|
||||
})
|
||||
assert.strictEqual(s1.text, "abd")
|
||||
assert.strictEqual(s1.text.toString(), 'abd')
|
||||
assert.strictEqual(s1.text.join(''), 'abd')
|
||||
})
|
||||
|
||||
it("should not allow modification outside of a change callback", () => {
|
||||
assert.throws(
|
||||
() => Automerge.splice(s1, "text", 0, 0, "a"),
|
||||
/object cannot be modified outside of a change block/
|
||||
)
|
||||
it('should allow modification after an object is assigned to a document', () => {
|
||||
s1 = Automerge.change(Automerge.init(), doc => {
|
||||
const text = new Automerge.Text()
|
||||
doc.text = text
|
||||
doc.text.insertAt(0, 'a', 'b', 'c', 'd')
|
||||
doc.text.deleteAt(2)
|
||||
assert.strictEqual(doc.text.toString(), 'abd')
|
||||
assert.strictEqual(doc.text.join(''), 'abd')
|
||||
})
|
||||
assert.strictEqual(s1.text.join(''), 'abd')
|
||||
})
|
||||
|
||||
describe("with initial value", () => {
|
||||
it("should initialize text in Automerge.from()", () => {
|
||||
let s1 = Automerge.from({ text: "init" })
|
||||
it('should not allow modification outside of a change callback', () => {
|
||||
assert.throws(() => s1.text.insertAt(0, 'a'), /object cannot be modified outside of a change block/)
|
||||
})
|
||||
|
||||
describe('with initial value', () => {
|
||||
it('should accept a string as initial value', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => doc.text = new Automerge.Text('init'))
|
||||
assert.strictEqual(s1.text.length, 4)
|
||||
assert.strictEqual(s1.text[0], "i")
|
||||
assert.strictEqual(s1.text[1], "n")
|
||||
assert.strictEqual(s1.text[2], "i")
|
||||
assert.strictEqual(s1.text[3], "t")
|
||||
assert.strictEqual(s1.text, "init")
|
||||
assert.strictEqual(s1.text.get(0), 'i')
|
||||
assert.strictEqual(s1.text.get(1), 'n')
|
||||
assert.strictEqual(s1.text.get(2), 'i')
|
||||
assert.strictEqual(s1.text.get(3), 't')
|
||||
assert.strictEqual(s1.text.toString(), 'init')
|
||||
})
|
||||
|
||||
it("should encode the initial value as a change", () => {
|
||||
const s1 = Automerge.from({ text: "init" })
|
||||
it('should accept an array as initial value', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => doc.text = new Automerge.Text(['i', 'n', 'i', 't']))
|
||||
assert.strictEqual(s1.text.length, 4)
|
||||
assert.strictEqual(s1.text.get(0), 'i')
|
||||
assert.strictEqual(s1.text.get(1), 'n')
|
||||
assert.strictEqual(s1.text.get(2), 'i')
|
||||
assert.strictEqual(s1.text.get(3), 't')
|
||||
assert.strictEqual(s1.text.toString(), 'init')
|
||||
})
|
||||
|
||||
it('should initialize text in Automerge.from()', () => {
|
||||
let s1 = Automerge.from({text: new Automerge.Text('init')})
|
||||
assert.strictEqual(s1.text.length, 4)
|
||||
assert.strictEqual(s1.text.get(0), 'i')
|
||||
assert.strictEqual(s1.text.get(1), 'n')
|
||||
assert.strictEqual(s1.text.get(2), 'i')
|
||||
assert.strictEqual(s1.text.get(3), 't')
|
||||
assert.strictEqual(s1.text.toString(), 'init')
|
||||
})
|
||||
|
||||
it('should encode the initial value as a change', () => {
|
||||
const s1 = Automerge.from({text: new Automerge.Text('init')})
|
||||
const changes = Automerge.getAllChanges(s1)
|
||||
assert.strictEqual(changes.length, 1)
|
||||
const [s2] = Automerge.applyChanges(Automerge.init<DocType>(), changes)
|
||||
assert.strictEqual(s2.text, "init")
|
||||
assert.strictEqual(s2.text, "init")
|
||||
const [s2] = Automerge.applyChanges(Automerge.init(), changes)
|
||||
assert.strictEqual(s2.text instanceof Automerge.Text, true)
|
||||
assert.strictEqual(s2.text.toString(), 'init')
|
||||
assert.strictEqual(s2.text.join(''), 'init')
|
||||
})
|
||||
|
||||
it('should allow immediate access to the value', () => {
|
||||
Automerge.change(Automerge.init(), doc => {
|
||||
const text = new Automerge.Text('init')
|
||||
assert.strictEqual(text.length, 4)
|
||||
assert.strictEqual(text.get(0), 'i')
|
||||
assert.strictEqual(text.toString(), 'init')
|
||||
doc.text = text
|
||||
assert.strictEqual(doc.text.length, 4)
|
||||
assert.strictEqual(doc.text.get(0), 'i')
|
||||
assert.strictEqual(doc.text.toString(), 'init')
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow pre-assignment modification of the initial value', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
const text = new Automerge.Text('init')
|
||||
text.deleteAt(3)
|
||||
assert.strictEqual(text.join(''), 'ini')
|
||||
doc.text = text
|
||||
assert.strictEqual(doc.text.join(''), 'ini')
|
||||
assert.strictEqual(doc.text.toString(), 'ini')
|
||||
})
|
||||
assert.strictEqual(s1.text.toString(), 'ini')
|
||||
assert.strictEqual(s1.text.join(''), 'ini')
|
||||
})
|
||||
|
||||
it('should allow post-assignment modification of the initial value', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
const text = new Automerge.Text('init')
|
||||
doc.text = text
|
||||
doc.text.deleteAt(0)
|
||||
doc.text.insertAt(0, 'I')
|
||||
assert.strictEqual(doc.text.join(''), 'Init')
|
||||
assert.strictEqual(doc.text.toString(), 'Init')
|
||||
})
|
||||
assert.strictEqual(s1.text.join(''), 'Init')
|
||||
assert.strictEqual(s1.text.toString(), 'Init')
|
||||
})
|
||||
})
|
||||
|
||||
it("should support unicode when creating text", () => {
|
||||
s1 = Automerge.from({
|
||||
text: "🐦",
|
||||
describe('non-textual control characters', () => {
|
||||
let s1
|
||||
beforeEach(() => {
|
||||
s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text()
|
||||
doc.text.insertAt(0, 'a')
|
||||
doc.text.insertAt(1, { attribute: 'bold' })
|
||||
})
|
||||
})
|
||||
assert.strictEqual(s1.text, "🐦")
|
||||
|
||||
it('should allow fetching non-textual characters', () => {
|
||||
assert.deepEqual(s1.text.get(1), { attribute: 'bold' })
|
||||
//assert.strictEqual(s1.text.getElemId(1), `3@${Automerge.getActorId(s1)}`)
|
||||
})
|
||||
|
||||
it('should include control characters in string length', () => {
|
||||
assert.strictEqual(s1.text.length, 2)
|
||||
assert.strictEqual(s1.text.get(0), 'a')
|
||||
})
|
||||
|
||||
it('should replace control characters from toString()', () => {
|
||||
assert.strictEqual(s1.text.toString(), 'a\uFFFC')
|
||||
})
|
||||
|
||||
it('should allow control characters to be updated', () => {
|
||||
const s2 = Automerge.change(s1, doc => doc.text.get(1).attribute = 'italic')
|
||||
const s3 = Automerge.load(Automerge.save(s2))
|
||||
assert.strictEqual(s1.text.get(1).attribute, 'bold')
|
||||
assert.strictEqual(s2.text.get(1).attribute, 'italic')
|
||||
assert.strictEqual(s3.text.get(1).attribute, 'italic')
|
||||
})
|
||||
|
||||
describe('spans interface to Text', () => {
|
||||
it('should return a simple string as a single span', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('hello world')
|
||||
})
|
||||
assert.deepEqual(s1.text.toSpans(), ['hello world'])
|
||||
})
|
||||
it('should return an empty string as an empty array', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text()
|
||||
})
|
||||
assert.deepEqual(s1.text.toSpans(), [])
|
||||
})
|
||||
it('should split a span at a control character', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('hello world')
|
||||
doc.text.insertAt(5, { attributes: { bold: true } })
|
||||
})
|
||||
assert.deepEqual(s1.text.toSpans(),
|
||||
['hello', { attributes: { bold: true } }, ' world'])
|
||||
})
|
||||
it('should allow consecutive control characters', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('hello world')
|
||||
doc.text.insertAt(5, { attributes: { bold: true } })
|
||||
doc.text.insertAt(6, { attributes: { italic: true } })
|
||||
})
|
||||
assert.deepEqual(s1.text.toSpans(),
|
||||
['hello',
|
||||
{ attributes: { bold: true } },
|
||||
{ attributes: { italic: true } },
|
||||
' world'
|
||||
])
|
||||
})
|
||||
it('should allow non-consecutive control characters', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('hello world')
|
||||
doc.text.insertAt(5, { attributes: { bold: true } })
|
||||
doc.text.insertAt(12, { attributes: { italic: true } })
|
||||
})
|
||||
assert.deepEqual(s1.text.toSpans(),
|
||||
['hello',
|
||||
{ attributes: { bold: true } },
|
||||
' world',
|
||||
{ attributes: { italic: true } }
|
||||
])
|
||||
})
|
||||
|
||||
it('should be convertable into a Quill delta', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('Gandalf the Grey')
|
||||
doc.text.insertAt(0, { attributes: { bold: true } })
|
||||
doc.text.insertAt(7 + 1, { attributes: { bold: null } })
|
||||
doc.text.insertAt(12 + 2, { attributes: { color: '#cccccc' } })
|
||||
})
|
||||
|
||||
let deltaDoc = automergeTextToDeltaDoc(s1.text)
|
||||
|
||||
// From https://quilljs.com/docs/delta/
|
||||
let expectedDoc = [
|
||||
{ insert: 'Gandalf', attributes: { bold: true } },
|
||||
{ insert: ' the ' },
|
||||
{ insert: 'Grey', attributes: { color: '#cccccc' } }
|
||||
]
|
||||
|
||||
assert.deepEqual(deltaDoc, expectedDoc)
|
||||
})
|
||||
|
||||
it('should support embeds', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('')
|
||||
doc.text.insertAt(0, { attributes: { link: 'https://quilljs.com' } })
|
||||
doc.text.insertAt(1, {
|
||||
image: 'https://quilljs.com/assets/images/icon.png'
|
||||
})
|
||||
doc.text.insertAt(2, { attributes: { link: null } })
|
||||
})
|
||||
|
||||
let deltaDoc = automergeTextToDeltaDoc(s1.text)
|
||||
|
||||
// From https://quilljs.com/docs/delta/
|
||||
let expectedDoc = [{
|
||||
// An image link
|
||||
insert: {
|
||||
image: 'https://quilljs.com/assets/images/icon.png'
|
||||
},
|
||||
attributes: {
|
||||
link: 'https://quilljs.com'
|
||||
}
|
||||
}]
|
||||
|
||||
assert.deepEqual(deltaDoc, expectedDoc)
|
||||
})
|
||||
|
||||
it('should handle concurrent overlapping spans', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('Gandalf the Grey')
|
||||
})
|
||||
|
||||
let s2 = Automerge.merge(Automerge.init(), s1)
|
||||
|
||||
let s3 = Automerge.change(s1, doc => {
|
||||
doc.text.insertAt(8, { attributes: { bold: true } })
|
||||
doc.text.insertAt(16 + 1, { attributes: { bold: null } })
|
||||
})
|
||||
|
||||
let s4 = Automerge.change(s2, doc => {
|
||||
doc.text.insertAt(0, { attributes: { bold: true } })
|
||||
doc.text.insertAt(11 + 1, { attributes: { bold: null } })
|
||||
})
|
||||
|
||||
let merged = Automerge.merge(s3, s4)
|
||||
|
||||
let deltaDoc = automergeTextToDeltaDoc(merged.text)
|
||||
|
||||
// From https://quilljs.com/docs/delta/
|
||||
let expectedDoc = [
|
||||
{ insert: 'Gandalf the Grey', attributes: { bold: true } },
|
||||
]
|
||||
|
||||
assert.deepEqual(deltaDoc, expectedDoc)
|
||||
})
|
||||
|
||||
it('should handle debolding spans', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('Gandalf the Grey')
|
||||
})
|
||||
|
||||
let s2 = Automerge.merge(Automerge.init(), s1)
|
||||
|
||||
let s3 = Automerge.change(s1, doc => {
|
||||
doc.text.insertAt(0, { attributes: { bold: true } })
|
||||
doc.text.insertAt(16 + 1, { attributes: { bold: null } })
|
||||
})
|
||||
|
||||
let s4 = Automerge.change(s2, doc => {
|
||||
doc.text.insertAt(8, { attributes: { bold: null } })
|
||||
doc.text.insertAt(11 + 1, { attributes: { bold: true } })
|
||||
})
|
||||
|
||||
|
||||
let merged = Automerge.merge(s3, s4)
|
||||
|
||||
let deltaDoc = automergeTextToDeltaDoc(merged.text)
|
||||
|
||||
// From https://quilljs.com/docs/delta/
|
||||
let expectedDoc = [
|
||||
{ insert: 'Gandalf ', attributes: { bold: true } },
|
||||
{ insert: 'the' },
|
||||
{ insert: ' Grey', attributes: { bold: true } },
|
||||
]
|
||||
|
||||
assert.deepEqual(deltaDoc, expectedDoc)
|
||||
})
|
||||
|
||||
// xxx: how would this work for colors?
|
||||
it('should handle destyling across destyled spans', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('Gandalf the Grey')
|
||||
})
|
||||
|
||||
let s2 = Automerge.merge(Automerge.init(), s1)
|
||||
|
||||
let s3 = Automerge.change(s1, doc => {
|
||||
doc.text.insertAt(0, { attributes: { bold: true } })
|
||||
doc.text.insertAt(16 + 1, { attributes: { bold: null } })
|
||||
})
|
||||
|
||||
let s4 = Automerge.change(s2, doc => {
|
||||
doc.text.insertAt(8, { attributes: { bold: null } })
|
||||
doc.text.insertAt(11 + 1, { attributes: { bold: true } })
|
||||
})
|
||||
|
||||
let merged = Automerge.merge(s3, s4)
|
||||
|
||||
let final = Automerge.change(merged, doc => {
|
||||
doc.text.insertAt(3 + 1, { attributes: { bold: null } })
|
||||
doc.text.insertAt(doc.text.length, { attributes: { bold: true } })
|
||||
})
|
||||
|
||||
let deltaDoc = automergeTextToDeltaDoc(final.text)
|
||||
|
||||
// From https://quilljs.com/docs/delta/
|
||||
let expectedDoc = [
|
||||
{ insert: 'Gan', attributes: { bold: true } },
|
||||
{ insert: 'dalf the Grey' },
|
||||
]
|
||||
|
||||
assert.deepEqual(deltaDoc, expectedDoc)
|
||||
})
|
||||
|
||||
it('should apply an insert', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('Hello world')
|
||||
})
|
||||
|
||||
const delta = [
|
||||
{ retain: 6 },
|
||||
{ insert: 'reader' },
|
||||
{ delete: 5 }
|
||||
]
|
||||
|
||||
let s2 = Automerge.change(s1, doc => {
|
||||
applyDeltaDocToAutomergeText(delta, doc)
|
||||
})
|
||||
|
||||
//assert.strictEqual(s2.text.join(''), 'Hello reader')
|
||||
assert.strictEqual(s2.text.toString(), 'Hello reader')
|
||||
})
|
||||
|
||||
it('should apply an insert with control characters', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('Hello world')
|
||||
})
|
||||
|
||||
const delta = [
|
||||
{ retain: 6 },
|
||||
{ insert: 'reader', attributes: { bold: true } },
|
||||
{ delete: 5 },
|
||||
{ insert: '!' }
|
||||
]
|
||||
|
||||
let s2 = Automerge.change(s1, doc => {
|
||||
applyDeltaDocToAutomergeText(delta, doc)
|
||||
})
|
||||
|
||||
assert.strictEqual(s2.text.toString(), 'Hello \uFFFCreader\uFFFC!')
|
||||
assert.deepEqual(s2.text.toSpans(), [
|
||||
"Hello ",
|
||||
{ attributes: { bold: true } },
|
||||
"reader",
|
||||
{ attributes: { bold: null } },
|
||||
"!"
|
||||
])
|
||||
})
|
||||
|
||||
it('should account for control characters in retain/delete lengths', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('Hello world')
|
||||
doc.text.insertAt(4, { attributes: { color: '#ccc' } })
|
||||
doc.text.insertAt(10, { attributes: { color: '#f00' } })
|
||||
})
|
||||
|
||||
const delta = [
|
||||
{ retain: 6 },
|
||||
{ insert: 'reader', attributes: { bold: true } },
|
||||
{ delete: 5 },
|
||||
{ insert: '!' }
|
||||
]
|
||||
|
||||
let s2 = Automerge.change(s1, doc => {
|
||||
applyDeltaDocToAutomergeText(delta, doc)
|
||||
})
|
||||
|
||||
assert.strictEqual(s2.text.toString(), 'Hell\uFFFCo \uFFFCreader\uFFFC\uFFFC!')
|
||||
assert.deepEqual(s2.text.toSpans(), [
|
||||
"Hell",
|
||||
{ attributes: { color: '#ccc'} },
|
||||
"o ",
|
||||
{ attributes: { bold: true } },
|
||||
"reader",
|
||||
{ attributes: { bold: null } },
|
||||
{ attributes: { color: '#f00'} },
|
||||
"!"
|
||||
])
|
||||
})
|
||||
|
||||
it('should support embeds', () => {
|
||||
let s1 = Automerge.change(Automerge.init(), doc => {
|
||||
doc.text = new Automerge.Text('')
|
||||
})
|
||||
|
||||
let deltaDoc = [{
|
||||
// An image link
|
||||
insert: {
|
||||
image: 'https://quilljs.com/assets/images/icon.png'
|
||||
},
|
||||
attributes: {
|
||||
link: 'https://quilljs.com'
|
||||
}
|
||||
}]
|
||||
|
||||
let s2 = Automerge.change(s1, doc => {
|
||||
applyDeltaDocToAutomergeText(deltaDoc, doc)
|
||||
})
|
||||
|
||||
assert.deepEqual(s2.text.toSpans(), [
|
||||
{ attributes: { link: 'https://quilljs.com' } },
|
||||
{ image: 'https://quilljs.com/assets/images/icon.png'},
|
||||
{ attributes: { link: null } },
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should support unicode when creating text', () => {
|
||||
s1 = Automerge.from({
|
||||
text: new Automerge.Text('🐦')
|
||||
})
|
||||
assert.strictEqual(s1.text.get(0), '🐦')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,281 +0,0 @@
|
|||
import * as assert from "assert"
|
||||
import * as Automerge from "../src"
|
||||
import { assertEqualsOneOf } from "./helpers"
|
||||
|
||||
type DocType = { text: Automerge.Text; [key: string]: any }
|
||||
|
||||
describe("Automerge.Text", () => {
|
||||
let s1: Automerge.Doc<DocType>, s2: Automerge.Doc<DocType>
|
||||
beforeEach(() => {
|
||||
s1 = Automerge.change(
|
||||
Automerge.init<DocType>(),
|
||||
doc => (doc.text = new Automerge.Text())
|
||||
)
|
||||
s2 = Automerge.merge(Automerge.init(), s1)
|
||||
})
|
||||
|
||||
it("should support insertion", () => {
|
||||
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, "a"))
|
||||
assert.strictEqual(s1.text.length, 1)
|
||||
assert.strictEqual(s1.text.get(0), "a")
|
||||
assert.strictEqual(s1.text.toString(), "a")
|
||||
//assert.strictEqual(s1.text.getElemId(0), `2@${Automerge.getActorId(s1)}`)
|
||||
})
|
||||
|
||||
it("should support deletion", () => {
|
||||
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, "a", "b", "c"))
|
||||
s1 = Automerge.change(s1, doc => doc.text.deleteAt(1, 1))
|
||||
assert.strictEqual(s1.text.length, 2)
|
||||
assert.strictEqual(s1.text.get(0), "a")
|
||||
assert.strictEqual(s1.text.get(1), "c")
|
||||
assert.strictEqual(s1.text.toString(), "ac")
|
||||
})
|
||||
|
||||
it("should support implicit and explicit deletion", () => {
|
||||
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, "a", "b", "c"))
|
||||
s1 = Automerge.change(s1, doc => doc.text.deleteAt(1))
|
||||
s1 = Automerge.change(s1, doc => doc.text.deleteAt(1, 0))
|
||||
assert.strictEqual(s1.text.length, 2)
|
||||
assert.strictEqual(s1.text.get(0), "a")
|
||||
assert.strictEqual(s1.text.get(1), "c")
|
||||
assert.strictEqual(s1.text.toString(), "ac")
|
||||
})
|
||||
|
||||
it("should handle concurrent insertion", () => {
|
||||
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, "a", "b", "c"))
|
||||
s2 = Automerge.change(s2, doc => doc.text.insertAt(0, "x", "y", "z"))
|
||||
s1 = Automerge.merge(s1, s2)
|
||||
assert.strictEqual(s1.text.length, 6)
|
||||
assertEqualsOneOf(s1.text.toString(), "abcxyz", "xyzabc")
|
||||
assertEqualsOneOf(s1.text.join(""), "abcxyz", "xyzabc")
|
||||
})
|
||||
|
||||
it("should handle text and other ops in the same change", () => {
|
||||
s1 = Automerge.change(s1, doc => {
|
||||
doc.foo = "bar"
|
||||
doc.text.insertAt(0, "a")
|
||||
})
|
||||
assert.strictEqual(s1.foo, "bar")
|
||||
assert.strictEqual(s1.text.toString(), "a")
|
||||
assert.strictEqual(s1.text.join(""), "a")
|
||||
})
|
||||
|
||||
it("should serialize to JSON as a simple string", () => {
|
||||
s1 = Automerge.change(s1, doc => doc.text.insertAt(0, "a", '"', "b"))
|
||||
assert.strictEqual(JSON.stringify(s1), '{"text":"a\\"b"}')
|
||||
})
|
||||
|
||||
it("should allow modification before an object is assigned to a document", () => {
|
||||
s1 = Automerge.change(Automerge.init(), doc => {
|
||||
const text = new Automerge.Text()
|
||||
text.insertAt(0, "a", "b", "c", "d")
|
||||
text.deleteAt(2)
|
||||
doc.text = text
|
||||
assert.strictEqual(doc.text.toString(), "abd")
|
||||
assert.strictEqual(doc.text.join(""), "abd")
|
||||
})
|
||||
assert.strictEqual(s1.text.toString(), "abd")
|
||||
assert.strictEqual(s1.text.join(""), "abd")
|
||||
})
|
||||
|
||||
it("should allow modification after an object is assigned to a document", () => {
|
||||
s1 = Automerge.change(Automerge.init(), doc => {
|
||||
const text = new Automerge.Text()
|
||||
doc.text = text
|
||||
doc.text.insertAt(0, "a", "b", "c", "d")
|
||||
doc.text.deleteAt(2)
|
||||
assert.strictEqual(doc.text.toString(), "abd")
|
||||
assert.strictEqual(doc.text.join(""), "abd")
|
||||
})
|
||||
assert.strictEqual(s1.text.join(""), "abd")
|
||||
})
|
||||
|
||||
it("should not allow modification outside of a change callback", () => {
|
||||
assert.throws(
|
||||
() => s1.text.insertAt(0, "a"),
|
||||
/object cannot be modified outside of a change block/
|
||||
)
|
||||
})
|
||||
|
||||
describe("with initial value", () => {
|
||||
it("should accept a string as initial value", () => {
|
||||
let s1 = Automerge.change(
|
||||
Automerge.init<DocType>(),
|
||||
doc => (doc.text = new Automerge.Text("init"))
|
||||
)
|
||||
assert.strictEqual(s1.text.length, 4)
|
||||
assert.strictEqual(s1.text.get(0), "i")
|
||||
assert.strictEqual(s1.text.get(1), "n")
|
||||
assert.strictEqual(s1.text.get(2), "i")
|
||||
assert.strictEqual(s1.text.get(3), "t")
|
||||
assert.strictEqual(s1.text.toString(), "init")
|
||||
})
|
||||
|
||||
it("should accept an array as initial value", () => {
|
||||
let s1 = Automerge.change(
|
||||
Automerge.init<DocType>(),
|
||||
doc => (doc.text = new Automerge.Text(["i", "n", "i", "t"]))
|
||||
)
|
||||
assert.strictEqual(s1.text.length, 4)
|
||||
assert.strictEqual(s1.text.get(0), "i")
|
||||
assert.strictEqual(s1.text.get(1), "n")
|
||||
assert.strictEqual(s1.text.get(2), "i")
|
||||
assert.strictEqual(s1.text.get(3), "t")
|
||||
assert.strictEqual(s1.text.toString(), "init")
|
||||
})
|
||||
|
||||
it("should initialize text in Automerge.from()", () => {
|
||||
let s1 = Automerge.from({ text: new Automerge.Text("init") })
|
||||
assert.strictEqual(s1.text.length, 4)
|
||||
assert.strictEqual(s1.text.get(0), "i")
|
||||
assert.strictEqual(s1.text.get(1), "n")
|
||||
assert.strictEqual(s1.text.get(2), "i")
|
||||
assert.strictEqual(s1.text.get(3), "t")
|
||||
assert.strictEqual(s1.text.toString(), "init")
|
||||
})
|
||||
|
||||
it("should encode the initial value as a change", () => {
|
||||
const s1 = Automerge.from({ text: new Automerge.Text("init") })
|
||||
const changes = Automerge.getAllChanges(s1)
|
||||
assert.strictEqual(changes.length, 1)
|
||||
const [s2] = Automerge.applyChanges(Automerge.init<DocType>(), changes)
|
||||
assert.strictEqual(s2.text instanceof Automerge.Text, true)
|
||||
assert.strictEqual(s2.text.toString(), "init")
|
||||
assert.strictEqual(s2.text.join(""), "init")
|
||||
})
|
||||
|
||||
it("should allow immediate access to the value", () => {
|
||||
Automerge.change(Automerge.init<DocType>(), doc => {
|
||||
const text = new Automerge.Text("init")
|
||||
assert.strictEqual(text.length, 4)
|
||||
assert.strictEqual(text.get(0), "i")
|
||||
assert.strictEqual(text.toString(), "init")
|
||||
doc.text = text
|
||||
assert.strictEqual(doc.text.length, 4)
|
||||
assert.strictEqual(doc.text.get(0), "i")
|
||||
assert.strictEqual(doc.text.toString(), "init")
|
||||
})
|
||||
})
|
||||
|
||||
it("should allow pre-assignment modification of the initial value", () => {
|
||||
let s1 = Automerge.change(Automerge.init<DocType>(), doc => {
|
||||
const text = new Automerge.Text("init")
|
||||
text.deleteAt(3)
|
||||
assert.strictEqual(text.join(""), "ini")
|
||||
doc.text = text
|
||||
assert.strictEqual(doc.text.join(""), "ini")
|
||||
assert.strictEqual(doc.text.toString(), "ini")
|
||||
})
|
||||
assert.strictEqual(s1.text.toString(), "ini")
|
||||
assert.strictEqual(s1.text.join(""), "ini")
|
||||
})
|
||||
|
||||
it("should allow post-assignment modification of the initial value", () => {
|
||||
let s1 = Automerge.change(Automerge.init<DocType>(), doc => {
|
||||
const text = new Automerge.Text("init")
|
||||
doc.text = text
|
||||
doc.text.deleteAt(0)
|
||||
doc.text.insertAt(0, "I")
|
||||
assert.strictEqual(doc.text.join(""), "Init")
|
||||
assert.strictEqual(doc.text.toString(), "Init")
|
||||
})
|
||||
assert.strictEqual(s1.text.join(""), "Init")
|
||||
assert.strictEqual(s1.text.toString(), "Init")
|
||||
})
|
||||
})
|
||||
|
||||
describe("non-textual control characters", () => {
|
||||
let s1: Automerge.Doc<DocType>
|
||||
beforeEach(() => {
|
||||
s1 = Automerge.change(Automerge.init<DocType>(), doc => {
|
||||
doc.text = new Automerge.Text()
|
||||
doc.text.insertAt(0, "a")
|
||||
doc.text.insertAt(1, { attribute: "bold" })
|
||||
})
|
||||
})
|
||||
|
||||
it("should allow fetching non-textual characters", () => {
|
||||
assert.deepEqual(s1.text.get(1), { attribute: "bold" })
|
||||
//assert.strictEqual(s1.text.getElemId(1), `3@${Automerge.getActorId(s1)}`)
|
||||
})
|
||||
|
||||
it("should include control characters in string length", () => {
|
||||
assert.strictEqual(s1.text.length, 2)
|
||||
assert.strictEqual(s1.text.get(0), "a")
|
||||
})
|
||||
|
||||
it("should replace control characters from toString()", () => {
|
||||
assert.strictEqual(s1.text.toString(), "a\uFFFC")
|
||||
})
|
||||
|
||||
it("should allow control characters to be updated", () => {
|
||||
const s2 = Automerge.change(
|
||||
s1,
|
||||
doc => (doc.text.get(1)!.attribute = "italic")
|
||||
)
|
||||
const s3 = Automerge.load<DocType>(Automerge.save(s2))
|
||||
assert.strictEqual(s1.text.get(1).attribute, "bold")
|
||||
assert.strictEqual(s2.text.get(1).attribute, "italic")
|
||||
assert.strictEqual(s3.text.get(1).attribute, "italic")
|
||||
})
|
||||
|
||||
describe("spans interface to Text", () => {
|
||||
it("should return a simple string as a single span", () => {
|
||||
let s1 = Automerge.change(Automerge.init<DocType>(), doc => {
|
||||
doc.text = new Automerge.Text("hello world")
|
||||
})
|
||||
assert.deepEqual(s1.text.toSpans(), ["hello world"])
|
||||
})
|
||||
it("should return an empty string as an empty array", () => {
|
||||
let s1 = Automerge.change(Automerge.init<DocType>(), doc => {
|
||||
doc.text = new Automerge.Text()
|
||||
})
|
||||
assert.deepEqual(s1.text.toSpans(), [])
|
||||
})
|
||||
it("should split a span at a control character", () => {
|
||||
let s1 = Automerge.change(Automerge.init<DocType>(), doc => {
|
||||
doc.text = new Automerge.Text("hello world")
|
||||
doc.text.insertAt(5, { attributes: { bold: true } })
|
||||
})
|
||||
assert.deepEqual(s1.text.toSpans(), [
|
||||
"hello",
|
||||
{ attributes: { bold: true } },
|
||||
" world",
|
||||
])
|
||||
})
|
||||
it("should allow consecutive control characters", () => {
|
||||
let s1 = Automerge.change(Automerge.init<DocType>(), doc => {
|
||||
doc.text = new Automerge.Text("hello world")
|
||||
doc.text.insertAt(5, { attributes: { bold: true } })
|
||||
doc.text.insertAt(6, { attributes: { italic: true } })
|
||||
})
|
||||
assert.deepEqual(s1.text.toSpans(), [
|
||||
"hello",
|
||||
{ attributes: { bold: true } },
|
||||
{ attributes: { italic: true } },
|
||||
" world",
|
||||
])
|
||||
})
|
||||
it("should allow non-consecutive control characters", () => {
|
||||
let s1 = Automerge.change(Automerge.init<DocType>(), doc => {
|
||||
doc.text = new Automerge.Text("hello world")
|
||||
doc.text.insertAt(5, { attributes: { bold: true } })
|
||||
doc.text.insertAt(12, { attributes: { italic: true } })
|
||||
})
|
||||
assert.deepEqual(s1.text.toSpans(), [
|
||||
"hello",
|
||||
{ attributes: { bold: true } },
|
||||
" world",
|
||||
{ attributes: { italic: true } },
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("should support unicode when creating text", () => {
|
||||
s1 = Automerge.from({
|
||||
text: new Automerge.Text("🐦"),
|
||||
})
|
||||
assert.strictEqual(s1.text.get(0), "🐦")
|
||||
})
|
||||
})
|
|
@ -1,20 +1,20 @@
|
|||
import * as assert from "assert"
|
||||
import * as Automerge from "../src"
|
||||
import * as assert from 'assert'
|
||||
import * as Automerge from '../src'
|
||||
|
||||
const uuid = Automerge.uuid
|
||||
|
||||
describe("uuid", () => {
|
||||
describe('uuid', () => {
|
||||
afterEach(() => {
|
||||
uuid.reset()
|
||||
})
|
||||
|
||||
describe("default implementation", () => {
|
||||
it("generates unique values", () => {
|
||||
describe('default implementation', () => {
|
||||
it('generates unique values', () => {
|
||||
assert.notEqual(uuid(), uuid())
|
||||
})
|
||||
})
|
||||
|
||||
describe("custom implementation", () => {
|
||||
describe('custom implementation', () => {
|
||||
let counter
|
||||
|
||||
function customUuid() {
|
||||
|
@ -22,11 +22,11 @@ describe("uuid", () => {
|
|||
}
|
||||
|
||||
before(() => uuid.setFactory(customUuid))
|
||||
beforeEach(() => (counter = 0))
|
||||
beforeEach(() => counter = 0)
|
||||
|
||||
it("invokes the custom factory", () => {
|
||||
assert.equal(uuid(), "custom-uuid-0")
|
||||
assert.equal(uuid(), "custom-uuid-1")
|
||||
it('invokes the custom factory', () => {
|
||||
assert.equal(uuid(), 'custom-uuid-0')
|
||||
assert.equal(uuid(), 'custom-uuid-1')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"sourceMap": false,
|
||||
"declaration": true,
|
||||
"resolveJsonModule": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"noImplicitAny": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/**/*", "test/**/*"],
|
||||
"exclude": ["./dist/**/*", "./node_modules", "./src/**/*.deno.ts"]
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"sourceMap": false,
|
||||
"declaration": true,
|
||||
"resolveJsonModule": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"noImplicitAny": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": [ "src/**/*" ],
|
||||
"exclude": [
|
||||
"./dist/**/*",
|
||||
"./node_modules"
|
||||
]
|
||||
}
|
||||
|
|
165
javascript/typedoc-readme.md
Normal file
165
javascript/typedoc-readme.md
Normal file
|
@ -0,0 +1,165 @@
|
|||
# Automerge
|
||||
|
||||
This library provides the core automerge data structure and sync algorithms.
|
||||
Other libraries can be built on top of this one which provide IO and
|
||||
persistence.
|
||||
|
||||
An automerge document can be though of an immutable POJO (plain old javascript
|
||||
object) which `automerge` tracks the history of, allowing it to be merged with
|
||||
any other automerge document.
|
||||
|
||||
## Creating and modifying a document
|
||||
|
||||
You can create a document with {@link init} or {@link from} and then make
|
||||
changes to it with {@link change}, you can merge two documents with {@link
|
||||
merge}.
|
||||
|
||||
```javascript
|
||||
import * as automerge from "@automerge/automerge"
|
||||
|
||||
type DocType = {ideas: Array<automerge.Text>}
|
||||
|
||||
let doc1 = automerge.init<DocType>()
|
||||
doc1 = automerge.change(doc1, d => {
|
||||
d.ideas = [new automerge.Text("an immutable document")]
|
||||
})
|
||||
|
||||
let doc2 = automerge.init<DocType>()
|
||||
doc2 = automerge.merge(doc2, automerge.clone(doc1))
|
||||
doc2 = automerge.change<DocType>(doc2, d => {
|
||||
d.ideas.push(new automerge.Text("which records it's history"))
|
||||
})
|
||||
|
||||
// Note the `automerge.clone` call, see the "cloning" section of this readme for
|
||||
// more detail
|
||||
doc1 = automerge.merge(doc1, automerge.clone(doc2))
|
||||
doc1 = automerge.change(doc1, d => {
|
||||
d.ideas[0].deleteAt(13, 8)
|
||||
d.ideas[0].insertAt(13, "object")
|
||||
})
|
||||
|
||||
let doc3 = automerge.merge(doc1, doc2)
|
||||
// doc3 is now {ideas: ["an immutable object", "which records it's history"]}
|
||||
```
|
||||
|
||||
## Applying changes from another document
|
||||
|
||||
You can get a representation of the result of the last {@link change} you made
|
||||
to a document with {@link getLastLocalChange} and you can apply that change to
|
||||
another document using {@link applyChanges}.
|
||||
|
||||
If you need to get just the changes which are in one document but not in another
|
||||
you can use {@link getHeads} to get the heads of the document without the
|
||||
changes and then {@link getMissingDeps}, passing the result of {@link getHeads}
|
||||
on the document with the changes.
|
||||
|
||||
## Saving and loading documents
|
||||
|
||||
You can {@link save} a document to generate a compresed binary representation of
|
||||
the document which can be loaded with {@link load}. If you have a document which
|
||||
you have recently made changes to you can generate recent changes with {@link
|
||||
saveIncremental}, this will generate all the changes since you last called
|
||||
`saveIncremental`, the changes generated can be applied to another document with
|
||||
{@link loadIncremental}.
|
||||
|
||||
## Syncing
|
||||
|
||||
The sync protocol is stateful. This means that we start by creating a {@link
|
||||
SyncState} for each peer we are communicating with using {@link initSyncState}.
|
||||
Then we generate a message to send to the peer by calling {@link
|
||||
generateSyncMessage}. When we receive a message from the peer we call {@link
|
||||
receiveSyncMessage}. Here's a simple example of a loop which just keeps two
|
||||
peers in sync.
|
||||
|
||||
|
||||
```javascript
|
||||
let sync1 = automerge.initSyncState()
|
||||
let msg: Uint8Array | null
|
||||
[sync1, msg] = automerge.generateSyncMessage(doc1, sync1)
|
||||
|
||||
while (true) {
|
||||
if (msg != null) {
|
||||
network.send(msg)
|
||||
}
|
||||
let resp: Uint8Array = network.receive()
|
||||
[doc1, sync1, _ignore] = automerge.receiveSyncMessage(doc1, sync1, resp)
|
||||
[sync1, msg] = automerge.generateSyncMessage(doc1, sync1)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Conflicts
|
||||
|
||||
The only time conflicts occur in automerge documents is in concurrent
|
||||
assignments to the same key in an object. In this case automerge
|
||||
deterministically chooses an arbitrary value to present to the application but
|
||||
you can examine the conflicts using {@link getConflicts}.
|
||||
|
||||
```
|
||||
import * as automerge from "@automerge/automerge"
|
||||
|
||||
type Profile = {
|
||||
pets: Array<{name: string, type: string}>
|
||||
}
|
||||
|
||||
let doc1 = automerge.init<Profile>("aaaa")
|
||||
doc1 = automerge.change(doc1, d => {
|
||||
d.pets = [{name: "Lassie", type: "dog"}]
|
||||
})
|
||||
let doc2 = automerge.init<Profile>("bbbb")
|
||||
doc2 = automerge.merge(doc2, automerge.clone(doc1))
|
||||
|
||||
doc2 = automerge.change(doc2, d => {
|
||||
d.pets[0].name = "Beethoven"
|
||||
})
|
||||
|
||||
doc1 = automerge.change(doc1, d => {
|
||||
d.pets[0].name = "Babe"
|
||||
})
|
||||
|
||||
const doc3 = automerge.merge(doc1, doc2)
|
||||
|
||||
// Note that here we pass `doc3.pets`, not `doc3`
|
||||
let conflicts = automerge.getConflicts(doc3.pets[0], "name")
|
||||
|
||||
// The two conflicting values are the keys of the conflicts object
|
||||
assert.deepEqual(Object.values(conflicts), ["Babe", Beethoven"])
|
||||
```
|
||||
|
||||
## Actor IDs
|
||||
|
||||
By default automerge will generate a random actor ID for you, but most methods
|
||||
for creating a document allow you to set the actor ID. You can get the actor ID
|
||||
associated with the document by calling {@link getActorId}. Actor IDs must not
|
||||
be used in concurrent threads of executiong - all changes by a given actor ID
|
||||
are expected to be sequential.
|
||||
|
||||
|
||||
## Listening to patches
|
||||
|
||||
Sometimes you want to respond to changes made to an automerge document. In this
|
||||
case you can use the {@link PatchCallback} type to receive notifications when
|
||||
changes have been made.
|
||||
|
||||
## Cloning
|
||||
|
||||
Currently you cannot make mutating changes (i.e. call {@link change}) to a
|
||||
document which you have two pointers to. For example, in this code:
|
||||
|
||||
```javascript
|
||||
let doc1 = automerge.init()
|
||||
let doc2 = automerge.change(doc1, d => d.key = "value")
|
||||
```
|
||||
|
||||
`doc1` and `doc2` are both pointers to the same state. Any attempt to call
|
||||
mutating methods on `doc1` will now result in an error like
|
||||
|
||||
Attempting to change an out of date document
|
||||
|
||||
If you encounter this you need to clone the original document, the above sample
|
||||
would work as:
|
||||
|
||||
```javascript
|
||||
let doc1 = automerge.init()
|
||||
let doc2 = automerge.change(automerge.clone(doc1), d => d.key = "value")
|
||||
```
|
|
@ -3,15 +3,19 @@ members = [
|
|||
"automerge",
|
||||
"automerge-c",
|
||||
"automerge-cli",
|
||||
"automerge-test",
|
||||
"automerge-wasm",
|
||||
"edit-trace",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
opt-level = 'z'
|
||||
|
||||
[profile.bench]
|
||||
debug = true
|
||||
debug = true
|
||||
|
||||
[profile.release.package.automerge-wasm]
|
||||
debug = false
|
||||
opt-level = 'z'
|
||||
|
|
|
@ -1,250 +0,0 @@
|
|||
---
|
||||
Language: Cpp
|
||||
# BasedOnStyle: Chromium
|
||||
AccessModifierOffset: -1
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignArrayOfStructures: None
|
||||
AlignConsecutiveAssignments:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: true
|
||||
AlignConsecutiveBitFields:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: false
|
||||
AlignConsecutiveDeclarations:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: false
|
||||
AlignConsecutiveMacros:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: false
|
||||
AlignEscapedNewlines: Left
|
||||
AlignOperands: Align
|
||||
AlignTrailingComments: true
|
||||
AllowAllArgumentsOnNextLine: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: false
|
||||
AllowShortEnumsOnASingleLine: true
|
||||
AllowShortBlocksOnASingleLine: Never
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortFunctionsOnASingleLine: Inline
|
||||
AllowShortLambdasOnASingleLine: All
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: true
|
||||
AlwaysBreakTemplateDeclarations: Yes
|
||||
AttributeMacros:
|
||||
- __capability
|
||||
BinPackArguments: true
|
||||
BinPackParameters: false
|
||||
BraceWrapping:
|
||||
AfterCaseLabel: false
|
||||
AfterClass: false
|
||||
AfterControlStatement: Never
|
||||
AfterEnum: false
|
||||
AfterFunction: false
|
||||
AfterNamespace: false
|
||||
AfterObjCDeclaration: false
|
||||
AfterStruct: false
|
||||
AfterUnion: false
|
||||
AfterExternBlock: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
BeforeLambdaBody: false
|
||||
BeforeWhile: false
|
||||
IndentBraces: false
|
||||
SplitEmptyFunction: true
|
||||
SplitEmptyRecord: true
|
||||
SplitEmptyNamespace: true
|
||||
BreakBeforeBinaryOperators: None
|
||||
BreakBeforeConceptDeclarations: Always
|
||||
BreakBeforeBraces: Attach
|
||||
BreakBeforeInheritanceComma: false
|
||||
BreakInheritanceList: BeforeColon
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
BreakConstructorInitializers: BeforeColon
|
||||
BreakAfterJavaFieldAnnotations: false
|
||||
BreakStringLiterals: true
|
||||
ColumnLimit: 120
|
||||
CommentPragmas: '^ IWYU pragma:'
|
||||
QualifierAlignment: Leave
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: true
|
||||
DeriveLineEnding: true
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
EmptyLineAfterAccessModifier: Never
|
||||
EmptyLineBeforeAccessModifier: LogicalBlock
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
PackConstructorInitializers: NextLine
|
||||
BasedOnStyle: ''
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
AllowAllConstructorInitializersOnNextLine: true
|
||||
FixNamespaceComments: true
|
||||
ForEachMacros:
|
||||
- foreach
|
||||
- Q_FOREACH
|
||||
- BOOST_FOREACH
|
||||
IfMacros:
|
||||
- KJ_IF_MAYBE
|
||||
IncludeBlocks: Preserve
|
||||
IncludeCategories:
|
||||
- Regex: '^<ext/.*\.h>'
|
||||
Priority: 2
|
||||
SortPriority: 0
|
||||
CaseSensitive: false
|
||||
- Regex: '^<.*\.h>'
|
||||
Priority: 1
|
||||
SortPriority: 0
|
||||
CaseSensitive: false
|
||||
- Regex: '^<.*'
|
||||
Priority: 2
|
||||
SortPriority: 0
|
||||
CaseSensitive: false
|
||||
- Regex: '.*'
|
||||
Priority: 3
|
||||
SortPriority: 0
|
||||
CaseSensitive: false
|
||||
IncludeIsMainRegex: '([-_](test|unittest))?$'
|
||||
IncludeIsMainSourceRegex: ''
|
||||
IndentAccessModifiers: false
|
||||
IndentCaseLabels: true
|
||||
IndentCaseBlocks: false
|
||||
IndentGotoLabels: true
|
||||
IndentPPDirectives: None
|
||||
IndentExternBlock: AfterExternBlock
|
||||
IndentRequiresClause: true
|
||||
IndentWidth: 4
|
||||
IndentWrappedFunctionNames: false
|
||||
InsertBraces: false
|
||||
InsertTrailingCommas: None
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||
LambdaBodyIndentation: Signature
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: None
|
||||
ObjCBinPackProtocolList: Never
|
||||
ObjCBlockIndentWidth: 2
|
||||
ObjCBreakBeforeNestedBlockParam: true
|
||||
ObjCSpaceAfterProperty: false
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PenaltyBreakAssignment: 2
|
||||
PenaltyBreakBeforeFirstCallParameter: 1
|
||||
PenaltyBreakComment: 300
|
||||
PenaltyBreakFirstLessLess: 120
|
||||
PenaltyBreakOpenParenthesis: 0
|
||||
PenaltyBreakString: 1000
|
||||
PenaltyBreakTemplateDeclaration: 10
|
||||
PenaltyExcessCharacter: 1000000
|
||||
PenaltyReturnTypeOnItsOwnLine: 200
|
||||
PenaltyIndentedWhitespace: 0
|
||||
PointerAlignment: Left
|
||||
PPIndentWidth: -1
|
||||
RawStringFormats:
|
||||
- Language: Cpp
|
||||
Delimiters:
|
||||
- cc
|
||||
- CC
|
||||
- cpp
|
||||
- Cpp
|
||||
- CPP
|
||||
- 'c++'
|
||||
- 'C++'
|
||||
CanonicalDelimiter: ''
|
||||
BasedOnStyle: google
|
||||
- Language: TextProto
|
||||
Delimiters:
|
||||
- pb
|
||||
- PB
|
||||
- proto
|
||||
- PROTO
|
||||
EnclosingFunctions:
|
||||
- EqualsProto
|
||||
- EquivToProto
|
||||
- PARSE_PARTIAL_TEXT_PROTO
|
||||
- PARSE_TEST_PROTO
|
||||
- PARSE_TEXT_PROTO
|
||||
- ParseTextOrDie
|
||||
- ParseTextProtoOrDie
|
||||
- ParseTestProto
|
||||
- ParsePartialTestProto
|
||||
CanonicalDelimiter: pb
|
||||
BasedOnStyle: google
|
||||
ReferenceAlignment: Pointer
|
||||
ReflowComments: true
|
||||
RemoveBracesLLVM: false
|
||||
RequiresClausePosition: OwnLine
|
||||
SeparateDefinitionBlocks: Leave
|
||||
ShortNamespaceLines: 1
|
||||
SortIncludes: CaseSensitive
|
||||
SortJavaStaticImport: Before
|
||||
SortUsingDeclarations: true
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterLogicalNot: false
|
||||
SpaceAfterTemplateKeyword: true
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeCaseColon: false
|
||||
SpaceBeforeCpp11BracedList: false
|
||||
SpaceBeforeCtorInitializerColon: true
|
||||
SpaceBeforeInheritanceColon: true
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceBeforeParensOptions:
|
||||
AfterControlStatements: true
|
||||
AfterForeachMacros: true
|
||||
AfterFunctionDefinitionName: false
|
||||
AfterFunctionDeclarationName: false
|
||||
AfterIfMacros: true
|
||||
AfterOverloadedOperator: false
|
||||
AfterRequiresInClause: false
|
||||
AfterRequiresInExpression: false
|
||||
BeforeNonEmptyParentheses: false
|
||||
SpaceAroundPointerQualifiers: Default
|
||||
SpaceBeforeRangeBasedForLoopColon: true
|
||||
SpaceInEmptyBlock: false
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 2
|
||||
SpacesInAngles: Never
|
||||
SpacesInConditionalStatement: false
|
||||
SpacesInContainerLiterals: true
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInLineCommentPrefix:
|
||||
Minimum: 1
|
||||
Maximum: -1
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
SpaceBeforeSquareBrackets: false
|
||||
BitFieldColonSpacing: Both
|
||||
Standard: Auto
|
||||
StatementAttributeLikeMacros:
|
||||
- Q_EMIT
|
||||
StatementMacros:
|
||||
- Q_UNUSED
|
||||
- QT_REQUIRE_VERSION
|
||||
TabWidth: 8
|
||||
UseCRLF: false
|
||||
UseTab: Never
|
||||
WhitespaceSensitiveMacros:
|
||||
- STRINGIZE
|
||||
- PP_STRINGIZE
|
||||
- BOOST_PP_STRINGIZE
|
||||
- NS_SWIFT_NAME
|
||||
- CF_SWIFT_NAME
|
||||
...
|
||||
|
7
rust/automerge-c/.gitignore
vendored
7
rust/automerge-c/.gitignore
vendored
|
@ -1,10 +1,3 @@
|
|||
automerge
|
||||
automerge.h
|
||||
automerge.o
|
||||
build/
|
||||
CMakeCache.txt
|
||||
CMakeFiles
|
||||
CMakePresets.json
|
||||
Makefile
|
||||
DartConfiguration.tcl
|
||||
out/
|
||||
|
|
|
@ -1,297 +1,97 @@
|
|||
cmake_minimum_required(VERSION 3.23 FATAL_ERROR)
|
||||
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
|
||||
|
||||
project(automerge-c VERSION 0.1.0
|
||||
LANGUAGES C
|
||||
DESCRIPTION "C bindings for the Automerge Rust library.")
|
||||
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
|
||||
|
||||
set(LIBRARY_NAME "automerge")
|
||||
# Parse the library name, project name and project version out of Cargo's TOML file.
|
||||
set(CARGO_LIB_SECTION OFF)
|
||||
|
||||
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
|
||||
set(LIBRARY_NAME "")
|
||||
|
||||
option(BUILD_SHARED_LIBS "Enable the choice of a shared or static library.")
|
||||
set(CARGO_PKG_SECTION OFF)
|
||||
|
||||
set(CARGO_PKG_NAME "")
|
||||
|
||||
set(CARGO_PKG_VERSION "")
|
||||
|
||||
file(READ Cargo.toml TOML_STRING)
|
||||
|
||||
string(REPLACE ";" "\\\\;" TOML_STRING "${TOML_STRING}")
|
||||
|
||||
string(REPLACE "\n" ";" TOML_LINES "${TOML_STRING}")
|
||||
|
||||
foreach(TOML_LINE IN ITEMS ${TOML_LINES})
|
||||
string(REGEX MATCH "^\\[(lib|package)\\]$" _ ${TOML_LINE})
|
||||
|
||||
if(CMAKE_MATCH_1 STREQUAL "lib")
|
||||
set(CARGO_LIB_SECTION ON)
|
||||
|
||||
set(CARGO_PKG_SECTION OFF)
|
||||
elseif(CMAKE_MATCH_1 STREQUAL "package")
|
||||
set(CARGO_LIB_SECTION OFF)
|
||||
|
||||
set(CARGO_PKG_SECTION ON)
|
||||
endif()
|
||||
|
||||
string(REGEX MATCH "^name += +\"([^\"]+)\"$" _ ${TOML_LINE})
|
||||
|
||||
if(CMAKE_MATCH_1 AND (CARGO_LIB_SECTION AND NOT CARGO_PKG_SECTION))
|
||||
set(LIBRARY_NAME "${CMAKE_MATCH_1}")
|
||||
elseif(CMAKE_MATCH_1 AND (NOT CARGO_LIB_SECTION AND CARGO_PKG_SECTION))
|
||||
set(CARGO_PKG_NAME "${CMAKE_MATCH_1}")
|
||||
endif()
|
||||
|
||||
string(REGEX MATCH "^version += +\"([^\"]+)\"$" _ ${TOML_LINE})
|
||||
|
||||
if(CMAKE_MATCH_1 AND CARGO_PKG_SECTION)
|
||||
set(CARGO_PKG_VERSION "${CMAKE_MATCH_1}")
|
||||
endif()
|
||||
|
||||
if(LIBRARY_NAME AND (CARGO_PKG_NAME AND CARGO_PKG_VERSION))
|
||||
break()
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
project(${CARGO_PKG_NAME} VERSION ${CARGO_PKG_VERSION} LANGUAGES C DESCRIPTION "C bindings for the Automerge Rust backend.")
|
||||
|
||||
include(CTest)
|
||||
|
||||
option(BUILD_SHARED_LIBS "Enable the choice of a shared or static library.")
|
||||
|
||||
include(CMakePackageConfigHelpers)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
|
||||
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
|
||||
|
||||
string(MAKE_C_IDENTIFIER ${PROJECT_NAME} SYMBOL_PREFIX)
|
||||
|
||||
string(TOUPPER ${SYMBOL_PREFIX} SYMBOL_PREFIX)
|
||||
|
||||
set(CARGO_TARGET_DIR "${CMAKE_BINARY_DIR}/Cargo/target")
|
||||
set(CARGO_TARGET_DIR "${CMAKE_CURRENT_BINARY_DIR}/Cargo/target")
|
||||
|
||||
set(CBINDGEN_INCLUDEDIR "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_INCLUDEDIR}")
|
||||
set(CBINDGEN_INCLUDEDIR "${CARGO_TARGET_DIR}/${CMAKE_INSTALL_INCLUDEDIR}")
|
||||
|
||||
set(CBINDGEN_TARGET_DIR "${CBINDGEN_INCLUDEDIR}/${PROJECT_NAME}")
|
||||
|
||||
find_program (
|
||||
CARGO_CMD
|
||||
"cargo"
|
||||
PATHS "$ENV{CARGO_HOME}/bin"
|
||||
DOC "The Cargo command"
|
||||
)
|
||||
add_subdirectory(src)
|
||||
|
||||
if(NOT CARGO_CMD)
|
||||
message(FATAL_ERROR "Cargo (Rust package manager) not found! "
|
||||
"Please install it and/or set the CARGO_HOME "
|
||||
"environment variable to its path.")
|
||||
endif()
|
||||
|
||||
string(TOLOWER "${CMAKE_BUILD_TYPE}" BUILD_TYPE_LOWER)
|
||||
|
||||
# In order to build with -Z build-std, we need to pass target explicitly.
|
||||
# https://doc.rust-lang.org/cargo/reference/unstable.html#build-std
|
||||
execute_process (
|
||||
COMMAND rustc -vV
|
||||
OUTPUT_VARIABLE RUSTC_VERSION
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
string(REGEX REPLACE ".*host: ([^ \n]*).*" "\\1"
|
||||
CARGO_TARGET
|
||||
${RUSTC_VERSION}
|
||||
)
|
||||
|
||||
if(BUILD_TYPE_LOWER STREQUAL debug)
|
||||
set(CARGO_BUILD_TYPE "debug")
|
||||
|
||||
set(CARGO_FLAG --target=${CARGO_TARGET})
|
||||
else()
|
||||
set(CARGO_BUILD_TYPE "release")
|
||||
|
||||
if (NOT RUSTC_VERSION MATCHES "nightly")
|
||||
set(RUSTUP_TOOLCHAIN nightly)
|
||||
endif()
|
||||
|
||||
set(RUSTFLAGS -C\ panic=abort)
|
||||
|
||||
set(CARGO_FLAG -Z build-std=std,panic_abort --release --target=${CARGO_TARGET})
|
||||
endif()
|
||||
|
||||
set(CARGO_FEATURES "")
|
||||
|
||||
set(CARGO_BINARY_DIR "${CARGO_TARGET_DIR}/${CARGO_TARGET}/${CARGO_BUILD_TYPE}")
|
||||
|
||||
set(BINDINGS_NAME "${LIBRARY_NAME}_core")
|
||||
|
||||
configure_file(
|
||||
${CMAKE_MODULE_PATH}/Cargo.toml.in
|
||||
${CMAKE_SOURCE_DIR}/Cargo.toml
|
||||
@ONLY
|
||||
NEWLINE_STYLE LF
|
||||
)
|
||||
|
||||
set(INCLUDE_GUARD_PREFIX "${SYMBOL_PREFIX}")
|
||||
|
||||
configure_file(
|
||||
${CMAKE_MODULE_PATH}/cbindgen.toml.in
|
||||
${CMAKE_SOURCE_DIR}/cbindgen.toml
|
||||
@ONLY
|
||||
NEWLINE_STYLE LF
|
||||
)
|
||||
|
||||
set(CARGO_OUTPUT
|
||||
${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
|
||||
${CARGO_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${BINDINGS_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}
|
||||
)
|
||||
|
||||
# \note cbindgen's naming behavior isn't fully configurable and it ignores
|
||||
# `const fn` calls (https://github.com/eqrion/cbindgen/issues/252).
|
||||
add_custom_command(
|
||||
OUTPUT
|
||||
${CARGO_OUTPUT}
|
||||
COMMAND
|
||||
# \note cbindgen won't regenerate its output header file after it's been removed but it will after its
|
||||
# configuration file has been updated.
|
||||
${CMAKE_COMMAND} -DCONDITION=NOT_EXISTS -P ${CMAKE_SOURCE_DIR}/cmake/file-touch.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h ${CMAKE_SOURCE_DIR}/cbindgen.toml
|
||||
COMMAND
|
||||
${CMAKE_COMMAND} -E env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} CBINDGEN_TARGET_DIR=${CBINDGEN_TARGET_DIR} RUSTUP_TOOLCHAIN=${RUSTUP_TOOLCHAIN} RUSTFLAGS=${RUSTFLAGS} ${CARGO_CMD} build ${CARGO_FLAG} ${CARGO_FEATURES}
|
||||
COMMAND
|
||||
# Compensate for cbindgen's translation of consecutive uppercase letters to "ScreamingSnakeCase".
|
||||
${CMAKE_COMMAND} -DMATCH_REGEX=A_M\([^_]+\)_ -DREPLACE_EXPR=AM_\\1_ -P ${CMAKE_SOURCE_DIR}/cmake/file-regex-replace.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
|
||||
COMMAND
|
||||
# Compensate for cbindgen ignoring `std:mem::size_of<usize>()` calls.
|
||||
${CMAKE_COMMAND} -DMATCH_REGEX=USIZE_ -DREPLACE_EXPR=\+${CMAKE_SIZEOF_VOID_P} -P ${CMAKE_SOURCE_DIR}/cmake/file-regex-replace.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
|
||||
MAIN_DEPENDENCY
|
||||
src/lib.rs
|
||||
DEPENDS
|
||||
src/actor_id.rs
|
||||
src/byte_span.rs
|
||||
src/change.rs
|
||||
src/doc.rs
|
||||
src/doc/list.rs
|
||||
src/doc/map.rs
|
||||
src/doc/utils.rs
|
||||
src/index.rs
|
||||
src/item.rs
|
||||
src/items.rs
|
||||
src/obj.rs
|
||||
src/result.rs
|
||||
src/sync.rs
|
||||
src/sync/have.rs
|
||||
src/sync/message.rs
|
||||
src/sync/state.rs
|
||||
${CMAKE_SOURCE_DIR}/build.rs
|
||||
${CMAKE_MODULE_PATH}/Cargo.toml.in
|
||||
${CMAKE_MODULE_PATH}/cbindgen.toml.in
|
||||
WORKING_DIRECTORY
|
||||
${CMAKE_SOURCE_DIR}
|
||||
COMMENT
|
||||
"Producing the bindings' artifacts with Cargo..."
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_custom_target(${BINDINGS_NAME}_artifacts ALL
|
||||
DEPENDS ${CARGO_OUTPUT}
|
||||
)
|
||||
|
||||
add_library(${BINDINGS_NAME} STATIC IMPORTED GLOBAL)
|
||||
|
||||
target_include_directories(${BINDINGS_NAME} INTERFACE "${CBINDGEN_INCLUDEDIR}")
|
||||
|
||||
set_target_properties(
|
||||
${BINDINGS_NAME}
|
||||
PROPERTIES
|
||||
# \note Cargo writes a debug build into a nested directory instead of
|
||||
# decorating its name.
|
||||
DEBUG_POSTFIX ""
|
||||
DEFINE_SYMBOL ""
|
||||
IMPORTED_IMPLIB ""
|
||||
IMPORTED_LOCATION "${CARGO_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${BINDINGS_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}"
|
||||
IMPORTED_NO_SONAME "TRUE"
|
||||
IMPORTED_SONAME ""
|
||||
LINKER_LANGUAGE C
|
||||
PUBLIC_HEADER "${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h"
|
||||
SOVERSION "${PROJECT_VERSION_MAJOR}"
|
||||
VERSION "${PROJECT_VERSION}"
|
||||
# \note Cargo exports all of the symbols automatically.
|
||||
WINDOWS_EXPORT_ALL_SYMBOLS "TRUE"
|
||||
)
|
||||
|
||||
target_compile_definitions(${BINDINGS_NAME} INTERFACE $<TARGET_PROPERTY:${BINDINGS_NAME},DEFINE_SYMBOL>)
|
||||
|
||||
set(UTILS_SUBDIR "utils")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT
|
||||
${CBINDGEN_TARGET_DIR}/${UTILS_SUBDIR}/enum_string.h
|
||||
${CMAKE_BINARY_DIR}/src/${UTILS_SUBDIR}/enum_string.c
|
||||
COMMAND
|
||||
${CMAKE_COMMAND} -DPROJECT_NAME=${PROJECT_NAME} -DLIBRARY_NAME=${LIBRARY_NAME} -DSUBDIR=${UTILS_SUBDIR} -P ${CMAKE_SOURCE_DIR}/cmake/enum-string-functions-gen.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h ${CBINDGEN_TARGET_DIR}/${UTILS_SUBDIR}/enum_string.h ${CMAKE_BINARY_DIR}/src/${UTILS_SUBDIR}/enum_string.c
|
||||
MAIN_DEPENDENCY
|
||||
${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
|
||||
DEPENDS
|
||||
${CMAKE_SOURCE_DIR}/cmake/enum-string-functions-gen.cmake
|
||||
WORKING_DIRECTORY
|
||||
${CMAKE_SOURCE_DIR}
|
||||
COMMENT
|
||||
"Generating the enum string functions with CMake..."
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_custom_target(${LIBRARY_NAME}_utilities
|
||||
DEPENDS ${CBINDGEN_TARGET_DIR}/${UTILS_SUBDIR}/enum_string.h
|
||||
${CMAKE_BINARY_DIR}/src/${UTILS_SUBDIR}/enum_string.c
|
||||
)
|
||||
|
||||
add_library(${LIBRARY_NAME})
|
||||
|
||||
target_compile_features(${LIBRARY_NAME} PRIVATE c_std_99)
|
||||
|
||||
set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
|
||||
|
||||
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
set(LIBRARY_DEPENDENCIES Threads::Threads ${CMAKE_DL_LIBS})
|
||||
|
||||
if(WIN32)
|
||||
list(APPEND LIBRARY_DEPENDENCIES Bcrypt userenv ws2_32)
|
||||
else()
|
||||
list(APPEND LIBRARY_DEPENDENCIES m)
|
||||
endif()
|
||||
|
||||
target_link_libraries(${LIBRARY_NAME}
|
||||
PUBLIC ${BINDINGS_NAME}
|
||||
${LIBRARY_DEPENDENCIES}
|
||||
)
|
||||
|
||||
# \note An imported library's INTERFACE_INCLUDE_DIRECTORIES property can't
|
||||
# contain a non-existent path so its build-time include directory
|
||||
# must be specified for all of its dependent targets instead.
|
||||
target_include_directories(${LIBRARY_NAME}
|
||||
PUBLIC "$<BUILD_INTERFACE:${CBINDGEN_INCLUDEDIR};${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}>"
|
||||
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
|
||||
)
|
||||
|
||||
add_dependencies(${LIBRARY_NAME} ${BINDINGS_NAME}_artifacts)
|
||||
|
||||
# Generate the configuration header.
|
||||
# Generate and install the configuration header.
|
||||
math(EXPR INTEGER_PROJECT_VERSION_MAJOR "${PROJECT_VERSION_MAJOR} * 100000")
|
||||
|
||||
math(EXPR INTEGER_PROJECT_VERSION_MINOR "${PROJECT_VERSION_MINOR} * 100")
|
||||
|
||||
math(EXPR INTEGER_PROJECT_VERSION_PATCH "${PROJECT_VERSION_PATCH}")
|
||||
|
||||
math(EXPR INTEGER_PROJECT_VERSION "${INTEGER_PROJECT_VERSION_MAJOR} + \
|
||||
${INTEGER_PROJECT_VERSION_MINOR} + \
|
||||
${INTEGER_PROJECT_VERSION_PATCH}")
|
||||
math(EXPR INTEGER_PROJECT_VERSION "${INTEGER_PROJECT_VERSION_MAJOR} + ${INTEGER_PROJECT_VERSION_MINOR} + ${INTEGER_PROJECT_VERSION_PATCH}")
|
||||
|
||||
configure_file(
|
||||
${CMAKE_MODULE_PATH}/config.h.in
|
||||
${CBINDGEN_TARGET_DIR}/config.h
|
||||
config.h
|
||||
@ONLY
|
||||
NEWLINE_STYLE LF
|
||||
)
|
||||
|
||||
target_sources(${LIBRARY_NAME}
|
||||
PRIVATE
|
||||
src/${UTILS_SUBDIR}/result.c
|
||||
src/${UTILS_SUBDIR}/stack_callback_data.c
|
||||
src/${UTILS_SUBDIR}/stack.c
|
||||
src/${UTILS_SUBDIR}/string.c
|
||||
${CMAKE_BINARY_DIR}/src/${UTILS_SUBDIR}/enum_string.c
|
||||
PUBLIC
|
||||
FILE_SET api TYPE HEADERS
|
||||
BASE_DIRS
|
||||
${CBINDGEN_INCLUDEDIR}
|
||||
${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}
|
||||
FILES
|
||||
${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
|
||||
${CBINDGEN_TARGET_DIR}/${UTILS_SUBDIR}/enum_string.h
|
||||
${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/result.h
|
||||
${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/stack_callback_data.h
|
||||
${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/stack.h
|
||||
${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/string.h
|
||||
INTERFACE
|
||||
FILE_SET config TYPE HEADERS
|
||||
BASE_DIRS
|
||||
${CBINDGEN_INCLUDEDIR}
|
||||
FILES
|
||||
${CBINDGEN_TARGET_DIR}/config.h
|
||||
)
|
||||
|
||||
install(
|
||||
TARGETS ${LIBRARY_NAME}
|
||||
EXPORT ${PROJECT_NAME}-config
|
||||
FILE_SET api
|
||||
FILE_SET config
|
||||
)
|
||||
|
||||
# \note Install the Cargo-built core bindings to enable direct linkage.
|
||||
install(
|
||||
FILES $<TARGET_PROPERTY:${BINDINGS_NAME},IMPORTED_LOCATION>
|
||||
DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
)
|
||||
|
||||
install(EXPORT ${PROJECT_NAME}-config
|
||||
FILE ${PROJECT_NAME}-config.cmake
|
||||
NAMESPACE "${PROJECT_NAME}::"
|
||||
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${LIB}
|
||||
FILES ${CMAKE_BINARY_DIR}/config.h
|
||||
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}
|
||||
)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
|
@ -300,6 +100,42 @@ if(BUILD_TESTING)
|
|||
enable_testing()
|
||||
endif()
|
||||
|
||||
add_subdirectory(docs)
|
||||
|
||||
add_subdirectory(examples EXCLUDE_FROM_ALL)
|
||||
|
||||
# Generate and install .cmake files
|
||||
set(PROJECT_CONFIG_NAME "${PROJECT_NAME}-config")
|
||||
|
||||
set(PROJECT_CONFIG_VERSION_NAME "${PROJECT_CONFIG_NAME}-version")
|
||||
|
||||
write_basic_package_version_file(
|
||||
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_VERSION_NAME}.cmake
|
||||
VERSION ${PROJECT_VERSION}
|
||||
COMPATIBILITY ExactVersion
|
||||
)
|
||||
|
||||
# The namespace label starts with the title-cased library name.
|
||||
string(SUBSTRING ${LIBRARY_NAME} 0 1 NS_FIRST)
|
||||
|
||||
string(SUBSTRING ${LIBRARY_NAME} 1 -1 NS_REST)
|
||||
|
||||
string(TOUPPER ${NS_FIRST} NS_FIRST)
|
||||
|
||||
string(TOLOWER ${NS_REST} NS_REST)
|
||||
|
||||
string(CONCAT NAMESPACE ${NS_FIRST} ${NS_REST} "::")
|
||||
|
||||
# \note CMake doesn't automate the exporting of an imported library's targets
|
||||
# so the package configuration script must do it.
|
||||
configure_package_config_file(
|
||||
${CMAKE_MODULE_PATH}/${PROJECT_CONFIG_NAME}.cmake.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_NAME}.cmake
|
||||
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
|
||||
)
|
||||
|
||||
install(
|
||||
FILES
|
||||
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_NAME}.cmake
|
||||
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_VERSION_NAME}.cmake
|
||||
DESTINATION
|
||||
${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
|
||||
)
|
||||
|
|
|
@ -7,8 +7,8 @@ license = "MIT"
|
|||
rust-version = "1.57.0"
|
||||
|
||||
[lib]
|
||||
name = "automerge_core"
|
||||
crate-type = ["staticlib"]
|
||||
name = "automerge"
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
bench = false
|
||||
doc = false
|
||||
|
||||
|
@ -19,4 +19,4 @@ libc = "^0.2"
|
|||
smol_str = "^0.1.21"
|
||||
|
||||
[build-dependencies]
|
||||
cbindgen = "^0.24"
|
||||
cbindgen = "^0.20"
|
||||
|
|
|
@ -1,207 +1,97 @@
|
|||
# Overview
|
||||
|
||||
automerge-c exposes a C API that can either be used directly or as the basis
|
||||
for other language bindings that have good support for calling C functions.
|
||||
## Methods we need to support
|
||||
|
||||
# Installing
|
||||
### Basic management
|
||||
|
||||
See the main README for instructions on getting your environment set up and then
|
||||
you can build the automerge-c library and install its constituent files within
|
||||
a root directory of your choosing (e.g. "/usr/local") like so:
|
||||
```shell
|
||||
cmake -E make_directory automerge-c/build
|
||||
cmake -S automerge-c -B automerge-c/build
|
||||
cmake --build automerge-c/build
|
||||
cmake --install automerge-c/build --prefix "/usr/local"
|
||||
```
|
||||
Installation is important because the name, location and structure of CMake's
|
||||
out-of-source build subdirectory is subject to change based on the platform and
|
||||
the release version; generated headers like `automerge-c/config.h` and
|
||||
`automerge-c/utils/enum_string.h` are only sure to be found within their
|
||||
installed locations.
|
||||
1. `AMcreate()`
|
||||
1. `AMclone(doc)`
|
||||
1. `AMfree(doc)`
|
||||
1. `AMconfig(doc, key, val)` // set actor
|
||||
1. `actor = get_actor(doc)`
|
||||
|
||||
It's not obvious because they are versioned but the `Cargo.toml` and
|
||||
`cbindgen.toml` configuration files are also generated in order to ensure that
|
||||
the project name, project version and library name that they contain match those
|
||||
specified within the top-level `CMakeLists.txt` file.
|
||||
### Transactions
|
||||
|
||||
If you'd like to cross compile the library for different platforms you can do so
|
||||
using [cross](https://github.com/cross-rs/cross). For example:
|
||||
1. `AMpendingOps(doc)`
|
||||
1. `AMcommit(doc, message, time)`
|
||||
1. `AMrollback(doc)`
|
||||
|
||||
- `cross build --manifest-path rust/automerge-c/Cargo.toml -r --target aarch64-unknown-linux-gnu`
|
||||
### Write
|
||||
|
||||
This will output a shared library in the directory `rust/target/aarch64-unknown-linux-gnu/release/`.
|
||||
1. `AMset{Map|List}(doc, obj, prop, value)`
|
||||
1. `AMinsert(doc, obj, index, value)`
|
||||
1. `AMpush(doc, obj, value)`
|
||||
1. `AMdel{Map|List}(doc, obj, prop)`
|
||||
1. `AMinc{Map|List}(doc, obj, prop, value)`
|
||||
1. `AMspliceText(doc, obj, start, num_del, text)`
|
||||
|
||||
You can replace `aarch64-unknown-linux-gnu` with any
|
||||
[cross supported targets](https://github.com/cross-rs/cross#supported-targets).
|
||||
The targets below are known to work, though other targets are expected to work
|
||||
too:
|
||||
### Read (the heads argument is optional and can be on an `at` variant)
|
||||
|
||||
- `x86_64-apple-darwin`
|
||||
- `aarch64-apple-darwin`
|
||||
- `x86_64-unknown-linux-gnu`
|
||||
- `aarch64-unknown-linux-gnu`
|
||||
1. `AMkeys(doc, obj, heads)`
|
||||
1. `AMlength(doc, obj, heads)`
|
||||
1. `AMlistRange(doc, obj, heads)`
|
||||
1. `AMmapRange(doc, obj, heads)`
|
||||
1. `AMvalues(doc, obj, heads)`
|
||||
1. `AMtext(doc, obj, heads)`
|
||||
|
||||
As a caveat, CMake generates the `automerge.h` header file in terms of the
|
||||
processor architecture of the computer on which it was built so, for example,
|
||||
don't use a header generated for a 64-bit processor if your target is a 32-bit
|
||||
processor.
|
||||
### Sync
|
||||
|
||||
# Usage
|
||||
1. `AMgenerateSyncMessage(doc, state)`
|
||||
1. `AMreceiveSyncMessage(doc, state, message)`
|
||||
1. `AMinitSyncState()`
|
||||
|
||||
You can build and view the C API's HTML reference documentation like so:
|
||||
```shell
|
||||
cmake -E make_directory automerge-c/build
|
||||
cmake -S automerge-c -B automerge-c/build
|
||||
cmake --build automerge-c/build --target automerge_docs
|
||||
firefox automerge-c/build/src/html/index.html
|
||||
```
|
||||
### Save / Load
|
||||
|
||||
To get started quickly, look at the
|
||||
[examples](https://github.com/automerge/automerge-rs/tree/main/rust/automerge-c/examples).
|
||||
1. `AMload(data)`
|
||||
1. `AMloadIncremental(doc, data)`
|
||||
1. `AMsave(doc)`
|
||||
1. `AMsaveIncremental(doc)`
|
||||
|
||||
Almost all operations in automerge-c act on an Automerge document
|
||||
(`AMdoc` struct) which is structurally similar to a JSON document.
|
||||
### Low Level Access
|
||||
|
||||
You can get a document by calling either `AMcreate()` or `AMload()`. Operations
|
||||
on a given document are not thread-safe so you must use a mutex or similar to
|
||||
avoid calling more than one function on the same one concurrently.
|
||||
1. `AMapplyChanges(doc, changes)`
|
||||
1. `AMgetChanges(doc, deps)`
|
||||
1. `AMgetChangesAdded(doc1, doc2)`
|
||||
1. `AMgetHeads(doc)`
|
||||
1. `AMgetLastLocalChange(doc)`
|
||||
1. `AMgetMissingDeps(doc, heads)`
|
||||
|
||||
A C API function that could succeed or fail returns a result (`AMresult` struct)
|
||||
containing a status code (`AMstatus` enum) and either a sequence of at least one
|
||||
item (`AMitem` struct) or a read-only view onto a UTF-8 error message string
|
||||
(`AMbyteSpan` struct).
|
||||
An item contains up to three components: an index within its parent object
|
||||
(`AMbyteSpan` struct or `size_t`), a unique identifier (`AMobjId` struct) and a
|
||||
value.
|
||||
The result of a successful function call that doesn't produce any values will
|
||||
contain a single item that is void (`AM_VAL_TYPE_VOID`).
|
||||
A returned result **must** be passed to `AMresultFree()` once the item(s) or
|
||||
error message it contains is no longer needed in order to avoid a memory leak.
|
||||
```
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <automerge-c/automerge.h>
|
||||
#include <automerge-c/utils/string.h>
|
||||
### Encode/Decode
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
AMresult *docResult = AMcreate(NULL);
|
||||
1. `AMencodeChange(change)`
|
||||
1. `AMdecodeChange(change)`
|
||||
1. `AMencodeSyncMessage(change)`
|
||||
1. `AMdecodeSyncMessage(change)`
|
||||
1. `AMencodeSyncState(change)`
|
||||
1. `AMdecodeSyncState(change)`
|
||||
|
||||
if (AMresultStatus(docResult) != AM_STATUS_OK) {
|
||||
char* const err_msg = AMstrdup(AMresultError(docResult), NULL);
|
||||
printf("failed to create doc: %s", err_msg);
|
||||
free(err_msg);
|
||||
goto cleanup;
|
||||
}
|
||||
## Open Question - Memory management
|
||||
|
||||
AMdoc *doc;
|
||||
AMitemToDoc(AMresultItem(docResult), &doc);
|
||||
Most of these calls return one or more items of arbitrary length. Doing memory management in C is tricky. This is my proposed solution...
|
||||
|
||||
// useful code goes here!
|
||||
###
|
||||
|
||||
cleanup:
|
||||
AMresultFree(docResult);
|
||||
}
|
||||
```
|
||||
```
|
||||
// returns 1 or zero opids
|
||||
n = automerge_set(doc, "_root", "hello", datatype, value);
|
||||
if (n) {
|
||||
automerge_pop(doc, &obj, len);
|
||||
}
|
||||
|
||||
If you are writing an application in C, the `AMstackItem()`, `AMstackItems()`
|
||||
and `AMstackResult()` functions enable the lifetimes of anonymous results to be
|
||||
centrally managed and allow the same validation logic to be reused without
|
||||
relying upon the `goto` statement (see examples/quickstart.c).
|
||||
// returns n values
|
||||
n = automerge_values(doc, "_root", "hello");
|
||||
for (i = 0; i<n ;i ++) {
|
||||
automerge_pop_value(doc, &value, &datatype, len);
|
||||
}
|
||||
```
|
||||
|
||||
If you are wrapping automerge-c in another language, particularly one that has a
|
||||
garbage collector, you can call the `AMresultFree()` function within a finalizer
|
||||
to ensure that memory is reclaimed when it is no longer needed.
|
||||
|
||||
Automerge documents consist of a mutable root which is always a map from string
|
||||
keys to values. A value can be one of the following types:
|
||||
|
||||
- A number of type double / int64_t / uint64_t
|
||||
- An explicit true / false / null
|
||||
- An immutable UTF-8 string (`AMbyteSpan`).
|
||||
- An immutable array of arbitrary bytes (`AMbyteSpan`).
|
||||
- A mutable map from string keys to values.
|
||||
- A mutable list of values.
|
||||
- A mutable UTF-8 string.
|
||||
|
||||
If you read from a location in the document with no value, an item with type
|
||||
`AM_VAL_TYPE_VOID` will be returned, but you cannot write such a value
|
||||
explicitly.
|
||||
|
||||
Under the hood, automerge references a mutable object by its object identifier
|
||||
where `AM_ROOT` signifies a document's root map object.
|
||||
|
||||
There are functions to put each type of value into either a map or a list, and
|
||||
functions to read the current or a historical value from a map or a list. As (in general) collaborators
|
||||
may edit the document at any time, you cannot guarantee that the type of the
|
||||
value at a given part of the document will stay the same. As a result, reading
|
||||
from the document will return an `AMitem` struct that you can inspect to
|
||||
determine the type of value that it contains.
|
||||
|
||||
Strings in automerge-c are represented using an `AMbyteSpan` which contains a
|
||||
pointer and a length. Strings must be valid UTF-8 and may contain NUL (`0`)
|
||||
characters.
|
||||
For your convenience, you can call `AMstr()` to get the `AMbyteSpan` struct
|
||||
equivalent of a null-terminated byte string or `AMstrdup()` to get the
|
||||
representation of an `AMbyteSpan` struct as a null-terminated byte string
|
||||
wherein its NUL characters have been removed/replaced as you choose.
|
||||
|
||||
Putting all of that together, to read and write from the root of the document
|
||||
you can do this:
|
||||
|
||||
```
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <automerge-c/automerge.h>
|
||||
#include <automerge-c/utils/string.h>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
// ...previous example...
|
||||
AMdoc *doc;
|
||||
AMitemToDoc(AMresultItem(docResult), &doc);
|
||||
|
||||
AMresult *putResult = AMmapPutStr(doc, AM_ROOT, AMstr("key"), AMstr("value"));
|
||||
if (AMresultStatus(putResult) != AM_STATUS_OK) {
|
||||
char* const err_msg = AMstrdup(AMresultError(putResult), NULL);
|
||||
printf("failed to put: %s", err_msg);
|
||||
free(err_msg);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
AMresult *getResult = AMmapGet(doc, AM_ROOT, AMstr("key"), NULL);
|
||||
if (AMresultStatus(getResult) != AM_STATUS_OK) {
|
||||
char* const err_msg = AMstrdup(AMresultError(putResult), NULL);
|
||||
printf("failed to get: %s", err_msg);
|
||||
free(err_msg);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
AMbyteSpan got;
|
||||
if (AMitemToStr(AMresultItem(getResult), &got)) {
|
||||
char* const c_str = AMstrdup(got, NULL);
|
||||
printf("Got %zu-character string \"%s\"", got.count, c_str);
|
||||
free(c_str);
|
||||
} else {
|
||||
printf("expected to read a string!");
|
||||
goto cleanup;
|
||||
}
|
||||
There would be one pop method per object type. Users allocs and frees the buffers. Multiple return values would result in multiple pops. Too small buffers would error and allow retry.
|
||||
|
||||
|
||||
cleanup:
|
||||
AMresultFree(getResult);
|
||||
AMresultFree(putResult);
|
||||
AMresultFree(docResult);
|
||||
}
|
||||
```
|
||||
### Formats
|
||||
|
||||
Functions that do not return an `AMresult` (for example `AMitemKey()`) do
|
||||
not allocate memory but rather reference memory that was previously
|
||||
allocated. It's therefore important to keep the original `AMresult` alive (in
|
||||
this case the one returned by `AMmapRange()`) until after you are finished with
|
||||
the items that it contains. However, the memory for an individual `AMitem` can
|
||||
be shared with a new `AMresult` by calling `AMitemResult()` on it. In other
|
||||
words, a select group of items can be filtered out of a collection and only each
|
||||
one's corresponding `AMresult` must be kept alive from that point forward; the
|
||||
originating collection's `AMresult` can be safely freed.
|
||||
Actors - We could do (bytes,len) or a hex encoded string?.
|
||||
ObjIds - We could do flat bytes of the ExId struct but lets do human readable strings for now - the struct would be faster but opque
|
||||
Heads - Might as well make it a flat buffer `(n, hash, hash, ...)`
|
||||
Changes - Put them all in a flat concatenated buffer
|
||||
Encode/Decode - to json strings?
|
||||
|
||||
Beyond that, good luck!
|
||||
|
|
|
@ -10,7 +10,7 @@ fn main() {
|
|||
let config = cbindgen::Config::from_file("cbindgen.toml")
|
||||
.expect("Unable to find cbindgen.toml configuration file");
|
||||
|
||||
if let Ok(writer) = cbindgen::generate_with_config(crate_dir, config) {
|
||||
if let Ok(writer) = cbindgen::generate_with_config(&crate_dir, config) {
|
||||
// \note CMake sets this environment variable before invoking Cargo so
|
||||
// that it can direct the generated header file into its
|
||||
// out-of-source build directory for post-processing.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
after_includes = """\n
|
||||
/**
|
||||
* \\defgroup enumerations Public Enumerations
|
||||
* Symbolic names for integer constants.
|
||||
Symbolic names for integer constants.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -12,23 +12,21 @@ after_includes = """\n
|
|||
#define AM_ROOT NULL
|
||||
|
||||
/**
|
||||
* \\memberof AMdoc
|
||||
* \\memberof AMchangeHash
|
||||
* \\def AM_CHANGE_HASH_SIZE
|
||||
* \\brief The count of bytes in a change hash.
|
||||
*/
|
||||
#define AM_CHANGE_HASH_SIZE 32
|
||||
"""
|
||||
autogen_warning = """
|
||||
/**
|
||||
* \\file
|
||||
* \\brief All constants, functions and types in the core Automerge C API.
|
||||
*
|
||||
* \\warning This file is auto-generated by cbindgen.
|
||||
*/
|
||||
"""
|
||||
autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"
|
||||
documentation = true
|
||||
documentation_style = "doxy"
|
||||
include_guard = "AUTOMERGE_C_H"
|
||||
header = """
|
||||
/** \\file
|
||||
* All constants, functions and types in the Automerge library's C API.
|
||||
*/
|
||||
"""
|
||||
include_guard = "AUTOMERGE_H"
|
||||
includes = []
|
||||
language = "C"
|
||||
line_length = 140
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
[package]
|
||||
name = "@PROJECT_NAME@"
|
||||
version = "@PROJECT_VERSION@"
|
||||
authors = ["Orion Henry <orion.henry@gmail.com>", "Jason Kankiewicz <jason.kankiewicz@gmail.com>"]
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
rust-version = "1.57.0"
|
||||
|
||||
[lib]
|
||||
name = "@BINDINGS_NAME@"
|
||||
crate-type = ["staticlib"]
|
||||
bench = false
|
||||
doc = false
|
||||
|
||||
[dependencies]
|
||||
@LIBRARY_NAME@ = { path = "../@LIBRARY_NAME@" }
|
||||
hex = "^0.4.3"
|
||||
libc = "^0.2"
|
||||
smol_str = "^0.1.21"
|
||||
|
||||
[build-dependencies]
|
||||
cbindgen = "^0.24"
|
|
@ -1,48 +0,0 @@
|
|||
after_includes = """\n
|
||||
/**
|
||||
* \\defgroup enumerations Public Enumerations
|
||||
* Symbolic names for integer constants.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \\memberof AMdoc
|
||||
* \\def AM_ROOT
|
||||
* \\brief The root object of a document.
|
||||
*/
|
||||
#define AM_ROOT NULL
|
||||
|
||||
/**
|
||||
* \\memberof AMdoc
|
||||
* \\def AM_CHANGE_HASH_SIZE
|
||||
* \\brief The count of bytes in a change hash.
|
||||
*/
|
||||
#define AM_CHANGE_HASH_SIZE 32
|
||||
"""
|
||||
autogen_warning = """
|
||||
/**
|
||||
* \\file
|
||||
* \\brief All constants, functions and types in the core Automerge C API.
|
||||
*
|
||||
* \\warning This file is auto-generated by cbindgen.
|
||||
*/
|
||||
"""
|
||||
documentation = true
|
||||
documentation_style = "doxy"
|
||||
include_guard = "@INCLUDE_GUARD_PREFIX@_H"
|
||||
includes = []
|
||||
language = "C"
|
||||
line_length = 140
|
||||
no_includes = true
|
||||
style = "both"
|
||||
sys_includes = ["stdbool.h", "stddef.h", "stdint.h", "time.h"]
|
||||
usize_is_size_t = true
|
||||
|
||||
[enum]
|
||||
derive_const_casts = true
|
||||
enum_class = true
|
||||
must_use = "MUST_USE_ENUM"
|
||||
prefix_with_name = true
|
||||
rename_variants = "ScreamingSnakeCase"
|
||||
|
||||
[export]
|
||||
item_types = ["constants", "enums", "functions", "opaque", "structs", "typedefs"]
|
|
@ -1,35 +1,14 @@
|
|||
#ifndef @INCLUDE_GUARD_PREFIX@_CONFIG_H
|
||||
#define @INCLUDE_GUARD_PREFIX@_CONFIG_H
|
||||
/**
|
||||
* \file
|
||||
* \brief Configuration pararameters defined by the build system.
|
||||
*
|
||||
* \warning This file is auto-generated by CMake.
|
||||
*/
|
||||
#ifndef @SYMBOL_PREFIX@_CONFIG_H
|
||||
#define @SYMBOL_PREFIX@_CONFIG_H
|
||||
|
||||
/* This header is auto-generated by CMake. */
|
||||
|
||||
/**
|
||||
* \def @SYMBOL_PREFIX@_VERSION
|
||||
* \brief Denotes a semantic version of the form {MAJOR}{MINOR}{PATCH} as three,
|
||||
* two-digit decimal numbers without leading zeros (e.g. 100 is 0.1.0).
|
||||
*/
|
||||
#define @SYMBOL_PREFIX@_VERSION @INTEGER_PROJECT_VERSION@
|
||||
|
||||
/**
|
||||
* \def @SYMBOL_PREFIX@_MAJOR_VERSION
|
||||
* \brief Denotes a semantic major version as a decimal number.
|
||||
*/
|
||||
#define @SYMBOL_PREFIX@_MAJOR_VERSION (@SYMBOL_PREFIX@_VERSION / 100000)
|
||||
|
||||
/**
|
||||
* \def @SYMBOL_PREFIX@_MINOR_VERSION
|
||||
* \brief Denotes a semantic minor version as a decimal number.
|
||||
*/
|
||||
#define @SYMBOL_PREFIX@_MINOR_VERSION ((@SYMBOL_PREFIX@_VERSION / 100) % 1000)
|
||||
|
||||
/**
|
||||
* \def @SYMBOL_PREFIX@_PATCH_VERSION
|
||||
* \brief Denotes a semantic patch version as a decimal number.
|
||||
*/
|
||||
#define @SYMBOL_PREFIX@_PATCH_VERSION (@SYMBOL_PREFIX@_VERSION % 100)
|
||||
|
||||
#endif /* @INCLUDE_GUARD_PREFIX@_CONFIG_H */
|
||||
#endif /* @SYMBOL_PREFIX@_CONFIG_H */
|
||||
|
|
|
@ -1,183 +0,0 @@
|
|||
# This CMake script is used to generate a header and a source file for utility
|
||||
# functions that convert the tags of generated enum types into strings and
|
||||
# strings into the tags of generated enum types.
|
||||
cmake_minimum_required(VERSION 3.23 FATAL_ERROR)
|
||||
|
||||
# Seeks the starting line of the source enum's declaration.
|
||||
macro(seek_enum_mode)
|
||||
if (line MATCHES "^(typedef[ \t]+)?enum ")
|
||||
string(REGEX REPLACE "^enum ([0-9a-zA-Z_]+).*$" "\\1" enum_name "${line}")
|
||||
set(mode "read_tags")
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
# Scans the input for the current enum's tags.
|
||||
macro(read_tags_mode)
|
||||
if(line MATCHES "^}")
|
||||
set(mode "generate")
|
||||
elseif(line MATCHES "^[A-Z0-9_]+.*$")
|
||||
string(REGEX REPLACE "^([A-Za-z0-9_]+).*$" "\\1" tmp "${line}")
|
||||
list(APPEND enum_tags "${tmp}")
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
macro(write_header_file)
|
||||
# Generate a to-string function declaration.
|
||||
list(APPEND header_body
|
||||
"/**\n"
|
||||
" * \\ingroup enumerations\n"
|
||||
" * \\brief Gets the string representation of an `${enum_name}` enum tag.\n"
|
||||
" *\n"
|
||||
" * \\param[in] tag An `${enum_name}` enum tag.\n"
|
||||
" * \\return A null-terminated byte string.\n"
|
||||
" */\n"
|
||||
"char const* ${enum_name}ToString(${enum_name} const tag)\;\n"
|
||||
"\n")
|
||||
# Generate a from-string function declaration.
|
||||
list(APPEND header_body
|
||||
"/**\n"
|
||||
" * \\ingroup enumerations\n"
|
||||
" * \\brief Gets an `${enum_name}` enum tag from its string representation.\n"
|
||||
" *\n"
|
||||
" * \\param[out] dest An `${enum_name}` enum tag pointer.\n"
|
||||
" * \\param[in] src A null-terminated byte string.\n"
|
||||
" * \\return `true` if \\p src matches the string representation of an\n"
|
||||
" * `${enum_name}` enum tag, `false` otherwise.\n"
|
||||
" */\n"
|
||||
"bool ${enum_name}FromString(${enum_name}* dest, char const* const src)\;\n"
|
||||
"\n")
|
||||
endmacro()
|
||||
|
||||
macro(write_source_file)
|
||||
# Generate a to-string function implementation.
|
||||
list(APPEND source_body
|
||||
"char const* ${enum_name}ToString(${enum_name} const tag) {\n"
|
||||
" switch (tag) {\n"
|
||||
" default:\n"
|
||||
" return \"???\"\;\n")
|
||||
foreach(label IN LISTS enum_tags)
|
||||
list(APPEND source_body
|
||||
" case ${label}:\n"
|
||||
" return \"${label}\"\;\n")
|
||||
endforeach()
|
||||
list(APPEND source_body
|
||||
" }\n"
|
||||
"}\n"
|
||||
"\n")
|
||||
# Generate a from-string function implementation.
|
||||
list(APPEND source_body
|
||||
"bool ${enum_name}FromString(${enum_name}* dest, char const* const src) {\n")
|
||||
foreach(label IN LISTS enum_tags)
|
||||
list(APPEND source_body
|
||||
" if (!strcmp(src, \"${label}\")) {\n"
|
||||
" *dest = ${label}\;\n"
|
||||
" return true\;\n"
|
||||
" }\n")
|
||||
endforeach()
|
||||
list(APPEND source_body
|
||||
" return false\;\n"
|
||||
"}\n"
|
||||
"\n")
|
||||
endmacro()
|
||||
|
||||
function(main)
|
||||
set(header_body "")
|
||||
# File header and includes.
|
||||
list(APPEND header_body
|
||||
"#ifndef ${include_guard}\n"
|
||||
"#define ${include_guard}\n"
|
||||
"/**\n"
|
||||
" * \\file\n"
|
||||
" * \\brief Utility functions for converting enum tags into null-terminated\n"
|
||||
" * byte strings and vice versa.\n"
|
||||
" *\n"
|
||||
" * \\warning This file is auto-generated by CMake.\n"
|
||||
" */\n"
|
||||
"\n"
|
||||
"#include <stdbool.h>\n"
|
||||
"\n"
|
||||
"#include <${library_include}>\n"
|
||||
"\n")
|
||||
set(source_body "")
|
||||
# File includes.
|
||||
list(APPEND source_body
|
||||
"/** \\warning This file is auto-generated by CMake. */\n"
|
||||
"\n"
|
||||
"#include \"stdio.h\"\n"
|
||||
"#include \"string.h\"\n"
|
||||
"\n"
|
||||
"#include <${header_include}>\n"
|
||||
"\n")
|
||||
set(enum_name "")
|
||||
set(enum_tags "")
|
||||
set(mode "seek_enum")
|
||||
file(STRINGS "${input_path}" lines)
|
||||
foreach(line IN LISTS lines)
|
||||
string(REGEX REPLACE "^(.+)(//.*)?" "\\1" line "${line}")
|
||||
string(STRIP "${line}" line)
|
||||
if(mode STREQUAL "seek_enum")
|
||||
seek_enum_mode()
|
||||
elseif(mode STREQUAL "read_tags")
|
||||
read_tags_mode()
|
||||
else()
|
||||
# The end of the enum declaration was reached.
|
||||
if(NOT enum_name)
|
||||
# The end of the file was reached.
|
||||
return()
|
||||
endif()
|
||||
if(NOT enum_tags)
|
||||
message(FATAL_ERROR "No tags found for `${enum_name}`.")
|
||||
endif()
|
||||
string(TOLOWER "${enum_name}" output_stem_prefix)
|
||||
string(CONCAT output_stem "${output_stem_prefix}" "_string")
|
||||
cmake_path(REPLACE_EXTENSION output_stem "h" OUTPUT_VARIABLE output_header_basename)
|
||||
write_header_file()
|
||||
write_source_file()
|
||||
set(enum_name "")
|
||||
set(enum_tags "")
|
||||
set(mode "seek_enum")
|
||||
endif()
|
||||
endforeach()
|
||||
# File footer.
|
||||
list(APPEND header_body
|
||||
"#endif /* ${include_guard} */\n")
|
||||
message(STATUS "Generating header file \"${output_header_path}\"...")
|
||||
file(WRITE "${output_header_path}" ${header_body})
|
||||
message(STATUS "Generating source file \"${output_source_path}\"...")
|
||||
file(WRITE "${output_source_path}" ${source_body})
|
||||
endfunction()
|
||||
|
||||
if(NOT DEFINED PROJECT_NAME)
|
||||
message(FATAL_ERROR "Variable PROJECT_NAME is not defined.")
|
||||
elseif(NOT DEFINED LIBRARY_NAME)
|
||||
message(FATAL_ERROR "Variable LIBRARY_NAME is not defined.")
|
||||
elseif(NOT DEFINED SUBDIR)
|
||||
message(FATAL_ERROR "Variable SUBDIR is not defined.")
|
||||
elseif(${CMAKE_ARGC} LESS 9)
|
||||
message(FATAL_ERROR "Too few arguments.")
|
||||
elseif(${CMAKE_ARGC} GREATER 10)
|
||||
message(FATAL_ERROR "Too many arguments.")
|
||||
elseif(NOT EXISTS ${CMAKE_ARGV5})
|
||||
message(FATAL_ERROR "Input header \"${CMAKE_ARGV7}\" not found.")
|
||||
endif()
|
||||
cmake_path(CONVERT "${CMAKE_ARGV7}" TO_CMAKE_PATH_LIST input_path NORMALIZE)
|
||||
cmake_path(CONVERT "${CMAKE_ARGV8}" TO_CMAKE_PATH_LIST output_header_path NORMALIZE)
|
||||
cmake_path(CONVERT "${CMAKE_ARGV9}" TO_CMAKE_PATH_LIST output_source_path NORMALIZE)
|
||||
string(TOLOWER "${PROJECT_NAME}" project_root)
|
||||
cmake_path(CONVERT "${SUBDIR}" TO_CMAKE_PATH_LIST project_subdir NORMALIZE)
|
||||
string(TOLOWER "${project_subdir}" project_subdir)
|
||||
string(TOLOWER "${LIBRARY_NAME}" library_stem)
|
||||
cmake_path(REPLACE_EXTENSION library_stem "h" OUTPUT_VARIABLE library_basename)
|
||||
string(JOIN "/" library_include "${project_root}" "${library_basename}")
|
||||
string(TOUPPER "${PROJECT_NAME}" project_name_upper)
|
||||
string(TOUPPER "${project_subdir}" include_guard_infix)
|
||||
string(REGEX REPLACE "/" "_" include_guard_infix "${include_guard_infix}")
|
||||
string(REGEX REPLACE "-" "_" include_guard_prefix "${project_name_upper}")
|
||||
string(JOIN "_" include_guard_prefix "${include_guard_prefix}" "${include_guard_infix}")
|
||||
string(JOIN "/" output_header_prefix "${project_root}" "${project_subdir}")
|
||||
cmake_path(GET output_header_path STEM output_header_stem)
|
||||
string(TOUPPER "${output_header_stem}" include_guard_stem)
|
||||
string(JOIN "_" include_guard "${include_guard_prefix}" "${include_guard_stem}" "H")
|
||||
cmake_path(GET output_header_path FILENAME output_header_basename)
|
||||
string(JOIN "/" header_include "${output_header_prefix}" "${output_header_basename}")
|
||||
main()
|
|
@ -1,6 +1,4 @@
|
|||
# This CMake script is used to perform string substitutions within a generated
|
||||
# file.
|
||||
cmake_minimum_required(VERSION 3.23 FATAL_ERROR)
|
||||
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
|
||||
|
||||
if(NOT DEFINED MATCH_REGEX)
|
||||
message(FATAL_ERROR "Variable \"MATCH_REGEX\" is not defined.")
|
|
@ -1,6 +1,4 @@
|
|||
# This CMake script is used to force Cargo to regenerate the header file for the
|
||||
# core bindings after the out-of-source build directory has been cleaned.
|
||||
cmake_minimum_required(VERSION 3.23 FATAL_ERROR)
|
||||
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
|
||||
|
||||
if(NOT DEFINED CONDITION)
|
||||
message(FATAL_ERROR "Variable \"CONDITION\" is not defined.")
|
|
@ -1,35 +0,0 @@
|
|||
find_package(Doxygen OPTIONAL_COMPONENTS dot)
|
||||
|
||||
if(DOXYGEN_FOUND)
|
||||
set(DOXYGEN_ALIASES "installed_headerfile=\\headerfile ${LIBRARY_NAME}.h <${PROJECT_NAME}/${LIBRARY_NAME}.h>")
|
||||
|
||||
set(DOXYGEN_GENERATE_LATEX YES)
|
||||
|
||||
set(DOXYGEN_PDF_HYPERLINKS YES)
|
||||
|
||||
set(DOXYGEN_PROJECT_LOGO "${CMAKE_CURRENT_SOURCE_DIR}/img/brandmark.png")
|
||||
|
||||
set(DOXYGEN_SORT_BRIEF_DOCS YES)
|
||||
|
||||
set(DOXYGEN_USE_MDFILE_AS_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md")
|
||||
|
||||
doxygen_add_docs(
|
||||
${LIBRARY_NAME}_docs
|
||||
"${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h"
|
||||
"${CBINDGEN_TARGET_DIR}/config.h"
|
||||
"${CBINDGEN_TARGET_DIR}/${UTILS_SUBDIR}/enum_string.h"
|
||||
"${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/result.h"
|
||||
"${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/stack_callback_data.h"
|
||||
"${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/stack.h"
|
||||
"${CMAKE_SOURCE_DIR}/${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${UTILS_SUBDIR}/string.h"
|
||||
"${CMAKE_SOURCE_DIR}/README.md"
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
COMMENT "Producing documentation with Doxygen..."
|
||||
)
|
||||
|
||||
# \note A Doxygen input file isn't a file-level dependency so the Doxygen
|
||||
# command must instead depend upon a target that either outputs the
|
||||
# file or depends upon it also or it will just output an error message
|
||||
# when it can't be found.
|
||||
add_dependencies(${LIBRARY_NAME}_docs ${BINDINGS_NAME}_artifacts ${LIBRARY_NAME}_utilities)
|
||||
endif()
|
|
@ -1,39 +1,41 @@
|
|||
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
|
||||
|
||||
add_executable(
|
||||
${LIBRARY_NAME}_quickstart
|
||||
example_quickstart
|
||||
quickstart.c
|
||||
)
|
||||
|
||||
set_target_properties(${LIBRARY_NAME}_quickstart PROPERTIES LINKER_LANGUAGE C)
|
||||
set_target_properties(example_quickstart PROPERTIES LINKER_LANGUAGE C)
|
||||
|
||||
# \note An imported library's INTERFACE_INCLUDE_DIRECTORIES property can't
|
||||
# contain a non-existent path so its build-time include directory
|
||||
# must be specified for all of its dependent targets instead.
|
||||
target_include_directories(
|
||||
${LIBRARY_NAME}_quickstart
|
||||
example_quickstart
|
||||
PRIVATE "$<BUILD_INTERFACE:${CBINDGEN_INCLUDEDIR}>"
|
||||
)
|
||||
|
||||
target_link_libraries(${LIBRARY_NAME}_quickstart PRIVATE ${LIBRARY_NAME})
|
||||
target_link_libraries(example_quickstart PRIVATE ${LIBRARY_NAME})
|
||||
|
||||
add_dependencies(${LIBRARY_NAME}_quickstart ${BINDINGS_NAME}_artifacts)
|
||||
add_dependencies(example_quickstart ${LIBRARY_NAME}_artifacts)
|
||||
|
||||
if(BUILD_SHARED_LIBS AND WIN32)
|
||||
add_custom_command(
|
||||
TARGET ${LIBRARY_NAME}_quickstart
|
||||
TARGET example_quickstart
|
||||
POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_SHARED_LIBRARY_SUFFIX}
|
||||
${CMAKE_BINARY_DIR}
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
COMMENT "Copying the DLL built by Cargo into the examples directory..."
|
||||
VERBATIM
|
||||
)
|
||||
endif()
|
||||
|
||||
add_custom_command(
|
||||
TARGET ${LIBRARY_NAME}_quickstart
|
||||
TARGET example_quickstart
|
||||
POST_BUILD
|
||||
COMMAND
|
||||
${LIBRARY_NAME}_quickstart
|
||||
example_quickstart
|
||||
COMMENT
|
||||
"Running the example quickstart..."
|
||||
VERBATIM
|
||||
|
|
|
@ -5,5 +5,5 @@
|
|||
```shell
|
||||
cmake -E make_directory automerge-c/build
|
||||
cmake -S automerge-c -B automerge-c/build
|
||||
cmake --build automerge-c/build --target automerge_quickstart
|
||||
cmake --build automerge-c/build --target example_quickstart
|
||||
```
|
||||
|
|
|
@ -3,127 +3,144 @@
|
|||
#include <string.h>
|
||||
|
||||
#include <automerge-c/automerge.h>
|
||||
#include <automerge-c/utils/enum_string.h>
|
||||
#include <automerge-c/utils/stack.h>
|
||||
#include <automerge-c/utils/stack_callback_data.h>
|
||||
#include <automerge-c/utils/string.h>
|
||||
|
||||
static bool abort_cb(AMstack**, void*);
|
||||
static void abort_cb(AMresultStack**, uint8_t);
|
||||
|
||||
/**
|
||||
* \brief Based on https://automerge.github.io/docs/quickstart
|
||||
*/
|
||||
int main(int argc, char** argv) {
|
||||
AMstack* stack = NULL;
|
||||
AMdoc* doc1;
|
||||
AMitemToDoc(AMstackItem(&stack, AMcreate(NULL), abort_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc1);
|
||||
AMobjId const* const cards =
|
||||
AMitemObjId(AMstackItem(&stack, AMmapPutObject(doc1, AM_ROOT, AMstr("cards"), AM_OBJ_TYPE_LIST), abort_cb,
|
||||
AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
|
||||
AMobjId const* const card1 =
|
||||
AMitemObjId(AMstackItem(&stack, AMlistPutObject(doc1, cards, SIZE_MAX, true, AM_OBJ_TYPE_MAP), abort_cb,
|
||||
AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
|
||||
AMstackItem(NULL, AMmapPutStr(doc1, card1, AMstr("title"), AMstr("Rewrite everything in Clojure")), abort_cb,
|
||||
AMexpect(AM_VAL_TYPE_VOID));
|
||||
AMstackItem(NULL, AMmapPutBool(doc1, card1, AMstr("done"), false), abort_cb, AMexpect(AM_VAL_TYPE_VOID));
|
||||
AMobjId const* const card2 =
|
||||
AMitemObjId(AMstackItem(&stack, AMlistPutObject(doc1, cards, SIZE_MAX, true, AM_OBJ_TYPE_MAP), abort_cb,
|
||||
AMexpect(AM_VAL_TYPE_OBJ_TYPE)));
|
||||
AMstackItem(NULL, AMmapPutStr(doc1, card2, AMstr("title"), AMstr("Rewrite everything in Haskell")), abort_cb,
|
||||
AMexpect(AM_VAL_TYPE_VOID));
|
||||
AMstackItem(NULL, AMmapPutBool(doc1, card2, AMstr("done"), false), abort_cb, AMexpect(AM_VAL_TYPE_VOID));
|
||||
AMstackItem(NULL, AMcommit(doc1, AMstr("Add card"), NULL), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
|
||||
AMresultStack* stack = NULL;
|
||||
AMdoc* const doc1 = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, abort_cb).doc;
|
||||
AMobjId const* const cards = AMpush(&stack,
|
||||
AMmapPutObject(doc1, AM_ROOT, "cards", AM_OBJ_TYPE_LIST),
|
||||
AM_VALUE_OBJ_ID,
|
||||
abort_cb).obj_id;
|
||||
AMobjId const* const card1 = AMpush(&stack,
|
||||
AMlistPutObject(doc1, cards, SIZE_MAX, true, AM_OBJ_TYPE_MAP),
|
||||
AM_VALUE_OBJ_ID,
|
||||
abort_cb).obj_id;
|
||||
AMfree(AMmapPutStr(doc1, card1, "title", "Rewrite everything in Clojure"));
|
||||
AMfree(AMmapPutBool(doc1, card1, "done", false));
|
||||
AMobjId const* const card2 = AMpush(&stack,
|
||||
AMlistPutObject(doc1, cards, SIZE_MAX, true, AM_OBJ_TYPE_MAP),
|
||||
AM_VALUE_OBJ_ID,
|
||||
abort_cb).obj_id;
|
||||
AMfree(AMmapPutStr(doc1, card2, "title", "Rewrite everything in Haskell"));
|
||||
AMfree(AMmapPutBool(doc1, card2, "done", false));
|
||||
AMfree(AMcommit(doc1, "Add card", NULL));
|
||||
|
||||
AMdoc* doc2;
|
||||
AMitemToDoc(AMstackItem(&stack, AMcreate(NULL), abort_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc2);
|
||||
AMstackItem(NULL, AMmerge(doc2, doc1), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
|
||||
AMdoc* doc2 = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, abort_cb).doc;
|
||||
AMfree(AMmerge(doc2, doc1));
|
||||
|
||||
AMbyteSpan binary;
|
||||
AMitemToBytes(AMstackItem(&stack, AMsave(doc1), abort_cb, AMexpect(AM_VAL_TYPE_BYTES)), &binary);
|
||||
AMitemToDoc(AMstackItem(&stack, AMload(binary.src, binary.count), abort_cb, AMexpect(AM_VAL_TYPE_DOC)), &doc2);
|
||||
AMbyteSpan const binary = AMpush(&stack, AMsave(doc1), AM_VALUE_BYTES, abort_cb).bytes;
|
||||
doc2 = AMpush(&stack, AMload(binary.src, binary.count), AM_VALUE_DOC, abort_cb).doc;
|
||||
|
||||
AMstackItem(NULL, AMmapPutBool(doc1, card1, AMstr("done"), true), abort_cb, AMexpect(AM_VAL_TYPE_VOID));
|
||||
AMstackItem(NULL, AMcommit(doc1, AMstr("Mark card as done"), NULL), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
|
||||
AMfree(AMmapPutBool(doc1, card1, "done", true));
|
||||
AMfree(AMcommit(doc1, "Mark card as done", NULL));
|
||||
|
||||
AMstackItem(NULL, AMlistDelete(doc2, cards, 0), abort_cb, AMexpect(AM_VAL_TYPE_VOID));
|
||||
AMstackItem(NULL, AMcommit(doc2, AMstr("Delete card"), NULL), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
|
||||
AMfree(AMlistDelete(doc2, cards, 0));
|
||||
AMfree(AMcommit(doc2, "Delete card", NULL));
|
||||
|
||||
AMstackItem(NULL, AMmerge(doc1, doc2), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE_HASH));
|
||||
AMfree(AMmerge(doc1, doc2));
|
||||
|
||||
AMitems changes = AMstackItems(&stack, AMgetChanges(doc1, NULL), abort_cb, AMexpect(AM_VAL_TYPE_CHANGE));
|
||||
AMitem* item = NULL;
|
||||
while ((item = AMitemsNext(&changes, 1)) != NULL) {
|
||||
AMchange const* change;
|
||||
AMitemToChange(item, &change);
|
||||
AMitems const heads = AMstackItems(&stack, AMitemFromChangeHash(AMchangeHash(change)), abort_cb,
|
||||
AMexpect(AM_VAL_TYPE_CHANGE_HASH));
|
||||
char* const c_msg = AMstrdup(AMchangeMessage(change), NULL);
|
||||
printf("%s %zu\n", c_msg, AMobjSize(doc1, cards, &heads));
|
||||
free(c_msg);
|
||||
AMchanges changes = AMpush(&stack, AMgetChanges(doc1, NULL), AM_VALUE_CHANGES, abort_cb).changes;
|
||||
AMchange const* change = NULL;
|
||||
while ((change = AMchangesNext(&changes, 1)) != NULL) {
|
||||
AMbyteSpan const change_hash = AMchangeHash(change);
|
||||
AMchangeHashes const heads = AMpush(&stack,
|
||||
AMchangeHashesInit(&change_hash, 1),
|
||||
AM_VALUE_CHANGE_HASHES,
|
||||
abort_cb).change_hashes;
|
||||
printf("%s %ld\n", AMchangeMessage(change), AMobjSize(doc1, cards, &heads));
|
||||
}
|
||||
AMstackFree(&stack);
|
||||
AMfreeStack(&stack);
|
||||
}
|
||||
|
||||
static char const* discriminant_suffix(AMvalueVariant const);
|
||||
|
||||
/**
|
||||
* \brief Examines the result at the top of the given stack and, if it's
|
||||
* invalid, prints an error message to `stderr`, deallocates all results
|
||||
* in the stack and exits.
|
||||
* \brief Prints an error message to `stderr`, deallocates all results in the
|
||||
* given stack and exits.
|
||||
*
|
||||
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
|
||||
* \param[in] data A pointer to an owned `AMstackCallbackData` struct or `NULL`.
|
||||
* \return `true` if the top `AMresult` in \p stack is valid, `false` otherwise.
|
||||
* \pre \p stack `!= NULL`.
|
||||
* \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
|
||||
* \param[in] discriminant An `AMvalueVariant` enum tag.
|
||||
* \pre \p stack` != NULL`.
|
||||
* \post `*stack == NULL`.
|
||||
*/
|
||||
static bool abort_cb(AMstack** stack, void* data) {
|
||||
static void abort_cb(AMresultStack** stack, uint8_t discriminant) {
|
||||
static char buffer[512] = {0};
|
||||
|
||||
char const* suffix = NULL;
|
||||
if (!stack) {
|
||||
suffix = "Stack*";
|
||||
} else if (!*stack) {
|
||||
}
|
||||
else if (!*stack) {
|
||||
suffix = "Stack";
|
||||
} else if (!(*stack)->result) {
|
||||
}
|
||||
else if (!(*stack)->result) {
|
||||
suffix = "";
|
||||
}
|
||||
if (suffix) {
|
||||
fprintf(stderr, "Null `AMresult%s*`.\n", suffix);
|
||||
AMstackFree(stack);
|
||||
fprintf(stderr, "Null `AMresult%s*`.", suffix);
|
||||
AMfreeStack(stack);
|
||||
exit(EXIT_FAILURE);
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
AMstatus const status = AMresultStatus((*stack)->result);
|
||||
switch (status) {
|
||||
case AM_STATUS_ERROR:
|
||||
strcpy(buffer, "Error");
|
||||
break;
|
||||
case AM_STATUS_INVALID_RESULT:
|
||||
strcpy(buffer, "Invalid result");
|
||||
break;
|
||||
case AM_STATUS_OK:
|
||||
break;
|
||||
default:
|
||||
sprintf(buffer, "Unknown `AMstatus` tag %d", status);
|
||||
case AM_STATUS_ERROR: strcpy(buffer, "Error"); break;
|
||||
case AM_STATUS_INVALID_RESULT: strcpy(buffer, "Invalid result"); break;
|
||||
case AM_STATUS_OK: break;
|
||||
default: sprintf(buffer, "Unknown `AMstatus` tag %d", status);
|
||||
}
|
||||
if (buffer[0]) {
|
||||
char* const c_msg = AMstrdup(AMresultError((*stack)->result), NULL);
|
||||
fprintf(stderr, "%s; %s.\n", buffer, c_msg);
|
||||
free(c_msg);
|
||||
AMstackFree(stack);
|
||||
fprintf(stderr, "%s; %s.", buffer, AMerrorMessage((*stack)->result));
|
||||
AMfreeStack(stack);
|
||||
exit(EXIT_FAILURE);
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
if (data) {
|
||||
AMstackCallbackData* sc_data = (AMstackCallbackData*)data;
|
||||
AMvalType const tag = AMitemValType(AMresultItem((*stack)->result));
|
||||
if (tag != sc_data->bitmask) {
|
||||
fprintf(stderr, "Unexpected tag `%s` (%d) instead of `%s` at %s:%d.\n", AMvalTypeToString(tag), tag,
|
||||
AMvalTypeToString(sc_data->bitmask), sc_data->file, sc_data->line);
|
||||
free(sc_data);
|
||||
AMstackFree(stack);
|
||||
exit(EXIT_FAILURE);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
free(data);
|
||||
return true;
|
||||
AMvalue const value = AMresultValue((*stack)->result);
|
||||
fprintf(stderr, "Unexpected tag `AM_VALUE_%s` (%d); expected `AM_VALUE_%s`.",
|
||||
discriminant_suffix(value.tag),
|
||||
value.tag,
|
||||
discriminant_suffix(discriminant));
|
||||
AMfreeStack(stack);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Gets the suffix for a discriminant's corresponding string
|
||||
* representation.
|
||||
*
|
||||
* \param[in] discriminant An `AMvalueVariant` enum tag.
|
||||
* \return A UTF-8 string.
|
||||
*/
|
||||
static char const* discriminant_suffix(AMvalueVariant const discriminant) {
|
||||
char const* suffix = NULL;
|
||||
switch (discriminant) {
|
||||
case AM_VALUE_ACTOR_ID: suffix = "ACTOR_ID"; break;
|
||||
case AM_VALUE_BOOLEAN: suffix = "BOOLEAN"; break;
|
||||
case AM_VALUE_BYTES: suffix = "BYTES"; break;
|
||||
case AM_VALUE_CHANGE_HASHES: suffix = "CHANGE_HASHES"; break;
|
||||
case AM_VALUE_CHANGES: suffix = "CHANGES"; break;
|
||||
case AM_VALUE_COUNTER: suffix = "COUNTER"; break;
|
||||
case AM_VALUE_DOC: suffix = "DOC"; break;
|
||||
case AM_VALUE_F64: suffix = "F64"; break;
|
||||
case AM_VALUE_INT: suffix = "INT"; break;
|
||||
case AM_VALUE_LIST_ITEMS: suffix = "LIST_ITEMS"; break;
|
||||
case AM_VALUE_MAP_ITEMS: suffix = "MAP_ITEMS"; break;
|
||||
case AM_VALUE_NULL: suffix = "NULL"; break;
|
||||
case AM_VALUE_OBJ_ID: suffix = "OBJ_ID"; break;
|
||||
case AM_VALUE_OBJ_ITEMS: suffix = "OBJ_ITEMS"; break;
|
||||
case AM_VALUE_STR: suffix = "STR"; break;
|
||||
case AM_VALUE_STRS: suffix = "STRINGS"; break;
|
||||
case AM_VALUE_SYNC_MESSAGE: suffix = "SYNC_MESSAGE"; break;
|
||||
case AM_VALUE_SYNC_STATE: suffix = "SYNC_STATE"; break;
|
||||
case AM_VALUE_TIMESTAMP: suffix = "TIMESTAMP"; break;
|
||||
case AM_VALUE_UINT: suffix = "UINT"; break;
|
||||
case AM_VALUE_VOID: suffix = "VOID"; break;
|
||||
default: suffix = "...";
|
||||
}
|
||||
return suffix;
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
@ -1,30 +0,0 @@
|
|||
#ifndef AUTOMERGE_C_UTILS_RESULT_H
|
||||
#define AUTOMERGE_C_UTILS_RESULT_H
|
||||
/**
|
||||
* \file
|
||||
* \brief Utility functions for use with `AMresult` structs.
|
||||
*/
|
||||
|
||||
#include <stdarg.h>
|
||||
|
||||
#include <automerge-c/automerge.h>
|
||||
|
||||
/**
|
||||
* \brief Transfers the items within an arbitrary list of results into a
|
||||
* new result in their order of specification.
|
||||
* \param[in] count The count of subsequent arguments.
|
||||
* \param[in] ... A \p count list of arguments, each of which is a pointer to
|
||||
* an `AMresult` struct whose items will be transferred out of it
|
||||
* and which is subsequently freed.
|
||||
* \return A pointer to an `AMresult` struct or `NULL`.
|
||||
* \pre `∀𝑥 ∈` \p ... `, AMresultStatus(𝑥) == AM_STATUS_OK`
|
||||
* \post `(∃𝑥 ∈` \p ... `, AMresultStatus(𝑥) != AM_STATUS_OK) -> NULL`
|
||||
* \attention All `AMresult` struct pointer arguments are passed to
|
||||
* `AMresultFree()` regardless of success; use `AMresultCat()`
|
||||
* instead if you wish to pass them to `AMresultFree()` yourself.
|
||||
* \warning The returned `AMresult` struct pointer must be passed to
|
||||
* `AMresultFree()` in order to avoid a memory leak.
|
||||
*/
|
||||
AMresult* AMresultFrom(int count, ...);
|
||||
|
||||
#endif /* AUTOMERGE_C_UTILS_RESULT_H */
|
|
@ -1,130 +0,0 @@
|
|||
#ifndef AUTOMERGE_C_UTILS_STACK_H
|
||||
#define AUTOMERGE_C_UTILS_STACK_H
|
||||
/**
|
||||
* \file
|
||||
* \brief Utility data structures and functions for hiding `AMresult` structs,
|
||||
* managing their lifetimes, and automatically applying custom
|
||||
* validation logic to the `AMitem` structs that they contain.
|
||||
*
|
||||
* \note The `AMstack` struct and its related functions drastically reduce the
|
||||
* need for boilerplate code and/or `goto` statement usage within a C
|
||||
* application but a higher-level programming language offers even better
|
||||
* ways to do the same things.
|
||||
*/
|
||||
|
||||
#include <automerge-c/automerge.h>
|
||||
|
||||
/**
|
||||
* \struct AMstack
|
||||
* \brief A node in a singly-linked list of result pointers.
|
||||
*/
|
||||
typedef struct AMstack {
|
||||
/** A result to be deallocated. */
|
||||
AMresult* result;
|
||||
/** The previous node in the singly-linked list or `NULL`. */
|
||||
struct AMstack* prev;
|
||||
} AMstack;
|
||||
|
||||
/**
|
||||
* \memberof AMstack
|
||||
* \brief The prototype of a function that examines the result at the top of
|
||||
* the given stack in terms of some arbitrary data.
|
||||
*
|
||||
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
|
||||
* \param[in] data A pointer to arbitrary data or `NULL`.
|
||||
* \return `true` if the top `AMresult` struct in \p stack is valid, `false`
|
||||
* otherwise.
|
||||
* \pre \p stack `!= NULL`.
|
||||
*/
|
||||
typedef bool (*AMstackCallback)(AMstack** stack, void* data);
|
||||
|
||||
/**
|
||||
* \memberof AMstack
|
||||
* \brief Deallocates the storage for a stack of results.
|
||||
*
|
||||
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
|
||||
* \pre \p stack `!= NULL`
|
||||
* \post `*stack == NULL`
|
||||
*/
|
||||
void AMstackFree(AMstack** stack);
|
||||
|
||||
/**
|
||||
* \memberof AMstack
|
||||
* \brief Gets a result from the stack after removing it.
|
||||
*
|
||||
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
|
||||
* \param[in] result A pointer to the `AMresult` to be popped or `NULL` to
|
||||
* select the top result in \p stack.
|
||||
* \return A pointer to an `AMresult` struct or `NULL`.
|
||||
* \pre \p stack `!= NULL`
|
||||
* \warning The returned `AMresult` struct pointer must be passed to
|
||||
* `AMresultFree()` in order to avoid a memory leak.
|
||||
*/
|
||||
AMresult* AMstackPop(AMstack** stack, AMresult const* result);
|
||||
|
||||
/**
|
||||
* \memberof AMstack
|
||||
* \brief Pushes the given result onto the given stack, calls the given
|
||||
* callback with the given data to validate it and then either gets the
|
||||
* result if it's valid or gets `NULL` instead.
|
||||
*
|
||||
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
|
||||
* \param[in] result A pointer to an `AMresult` struct.
|
||||
* \param[in] callback A pointer to a function with the same signature as
|
||||
* `AMstackCallback()` or `NULL`.
|
||||
* \param[in] data A pointer to arbitrary data or `NULL` which is passed to
|
||||
* \p callback.
|
||||
* \return \p result or `NULL`.
|
||||
* \warning If \p stack `== NULL` then \p result is deallocated in order to
|
||||
* avoid a memory leak.
|
||||
*/
|
||||
AMresult* AMstackResult(AMstack** stack, AMresult* result, AMstackCallback callback, void* data);
|
||||
|
||||
/**
|
||||
* \memberof AMstack
|
||||
* \brief Pushes the given result onto the given stack, calls the given
|
||||
* callback with the given data to validate it and then either gets the
|
||||
* first item in the sequence of items within that result if it's valid
|
||||
* or gets `NULL` instead.
|
||||
*
|
||||
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
|
||||
* \param[in] result A pointer to an `AMresult` struct.
|
||||
* \param[in] callback A pointer to a function with the same signature as
|
||||
* `AMstackCallback()` or `NULL`.
|
||||
* \param[in] data A pointer to arbitrary data or `NULL` which is passed to
|
||||
* \p callback.
|
||||
* \return A pointer to an `AMitem` struct or `NULL`.
|
||||
* \warning If \p stack `== NULL` then \p result is deallocated in order to
|
||||
* avoid a memory leak.
|
||||
*/
|
||||
AMitem* AMstackItem(AMstack** stack, AMresult* result, AMstackCallback callback, void* data);
|
||||
|
||||
/**
|
||||
* \memberof AMstack
|
||||
* \brief Pushes the given result onto the given stack, calls the given
|
||||
* callback with the given data to validate it and then either gets an
|
||||
* `AMitems` struct over the sequence of items within that result if it's
|
||||
* valid or gets an empty `AMitems` instead.
|
||||
*
|
||||
* \param[in,out] stack A pointer to a pointer to an `AMstack` struct.
|
||||
* \param[in] result A pointer to an `AMresult` struct.
|
||||
* \param[in] callback A pointer to a function with the same signature as
|
||||
* `AMstackCallback()` or `NULL`.
|
||||
* \param[in] data A pointer to arbitrary data or `NULL` which is passed to
|
||||
* \p callback.
|
||||
* \return An `AMitems` struct.
|
||||
* \warning If \p stack `== NULL` then \p result is deallocated immediately
|
||||
* in order to avoid a memory leak.
|
||||
*/
|
||||
AMitems AMstackItems(AMstack** stack, AMresult* result, AMstackCallback callback, void* data);
|
||||
|
||||
/**
|
||||
* \memberof AMstack
|
||||
* \brief Gets the count of results that have been pushed onto the stack.
|
||||
*
|
||||
* \param[in,out] stack A pointer to an `AMstack` struct.
|
||||
* \return A 64-bit unsigned integer.
|
||||
*/
|
||||
size_t AMstackSize(AMstack const* const stack);
|
||||
|
||||
#endif /* AUTOMERGE_C_UTILS_STACK_H */
|
|
@ -1,53 +0,0 @@
|
|||
#ifndef AUTOMERGE_C_UTILS_PUSH_CALLBACK_DATA_H
|
||||
#define AUTOMERGE_C_UTILS_PUSH_CALLBACK_DATA_H
|
||||
/**
|
||||
* \file
|
||||
* \brief Utility data structures, functions and macros for supplying
|
||||
* parameters to the custom validation logic applied to `AMitem`
|
||||
* structs.
|
||||
*/
|
||||
|
||||
#include <automerge-c/automerge.h>
|
||||
|
||||
/**
|
||||
* \struct AMstackCallbackData
|
||||
* \brief A data structure for passing the parameters of an item value test
|
||||
* to an implementation of the `AMstackCallback` function prototype.
|
||||
*/
|
||||
typedef struct {
|
||||
/** A bitmask of `AMvalType` tags. */
|
||||
AMvalType bitmask;
|
||||
/** A null-terminated file path string. */
|
||||
char const* file;
|
||||
/** The ordinal number of a line within a file. */
|
||||
int line;
|
||||
} AMstackCallbackData;
|
||||
|
||||
/**
|
||||
* \memberof AMstackCallbackData
|
||||
* \brief Allocates a new `AMstackCallbackData` struct and initializes its
|
||||
* members from their corresponding arguments.
|
||||
*
|
||||
* \param[in] bitmask A bitmask of `AMvalType` tags.
|
||||
* \param[in] file A null-terminated file path string.
|
||||
* \param[in] line The ordinal number of a line within a file.
|
||||
* \return A pointer to a disowned `AMstackCallbackData` struct.
|
||||
* \warning The returned pointer must be passed to `free()` to avoid a memory
|
||||
* leak.
|
||||
*/
|
||||
AMstackCallbackData* AMstackCallbackDataInit(AMvalType const bitmask, char const* const file, int const line);
|
||||
|
||||
/**
|
||||
* \memberof AMstackCallbackData
|
||||
* \def AMexpect
|
||||
* \brief Allocates a new `AMstackCallbackData` struct and initializes it from
|
||||
* an `AMvalueType` bitmask.
|
||||
*
|
||||
* \param[in] bitmask A bitmask of `AMvalType` tags.
|
||||
* \return A pointer to a disowned `AMstackCallbackData` struct.
|
||||
* \warning The returned pointer must be passed to `free()` to avoid a memory
|
||||
* leak.
|
||||
*/
|
||||
#define AMexpect(bitmask) AMstackCallbackDataInit(bitmask, __FILE__, __LINE__)
|
||||
|
||||
#endif /* AUTOMERGE_C_UTILS_PUSH_CALLBACK_DATA_H */
|
|
@ -1,29 +0,0 @@
|
|||
#ifndef AUTOMERGE_C_UTILS_STRING_H
|
||||
#define AUTOMERGE_C_UTILS_STRING_H
|
||||
/**
|
||||
* \file
|
||||
* \brief Utility functions for use with `AMbyteSpan` structs that provide
|
||||
* UTF-8 string views.
|
||||
*/
|
||||
|
||||
#include <automerge-c/automerge.h>
|
||||
|
||||
/**
|
||||
* \memberof AMbyteSpan
|
||||
* \brief Returns a pointer to a null-terminated byte string which is a
|
||||
* duplicate of the given UTF-8 string view except for the substitution
|
||||
* of its NUL (0) characters with the specified null-terminated byte
|
||||
* string.
|
||||
*
|
||||
* \param[in] str A UTF-8 string view as an `AMbyteSpan` struct.
|
||||
* \param[in] nul A null-terminated byte string to substitute for NUL characters
|
||||
* or `NULL` to substitute `"\\0"` for NUL characters.
|
||||
* \return A disowned null-terminated byte string.
|
||||
* \pre \p str.src `!= NULL`
|
||||
* \pre \p str.count `<= sizeof(`\p str.src `)`
|
||||
* \warning The returned pointer must be passed to `free()` to avoid a memory
|
||||
* leak.
|
||||
*/
|
||||
char* AMstrdup(AMbyteSpan const str, char const* nul);
|
||||
|
||||
#endif /* AUTOMERGE_C_UTILS_STRING_H */
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue