Compare commits

..

145 commits

Author SHA1 Message Date
alexjg
cb409b6ffe
docs: timestamp -> time in automerge.change examples (#548) 2023-03-09 18:10:23 +00:00
Conrad Irwin
b34b46fa16
smaller automerge c (#545)
* Fix automerge-c tests on mac

* Generate significantly smaller automerge-c builds

This cuts the size of libautomerge_core.a from 25Mb to 1.6Mb on macOS
and 53Mb to 2.7Mb on Linux.

As a side-effect of setting codegen-units = 1 for all release builds the
optimized wasm files are also 100kb smaller.
2023-03-09 15:09:43 +00:00
Conrad Irwin
7b747b8341
Error instead of corrupt large op counters (#543)
Since b78211ca6, OpIds have been silently truncated to 2**32. This
causes corruption in the case the op id overflows.

This change converts the silent error to a panic, and guards against the
panic on the codepath found by the fuzzer.
2023-03-07 16:49:04 +00:00
Conrad Irwin
2c1970f664
Fix panic on invalid action (#541)
We make the validation on parsing operations in the encoded changes stricter to avoid a possible panic when applying changes.
2023-03-04 12:09:08 +00:00
christine betts
63b761c0d1
Suppress clippy warning in parse.rs + bump toolchain (#542)
* Fix rust error in parse.rs
* Bump toolchain to 1.67.0
2023-03-03 22:42:40 +00:00
Conrad Irwin
44fa7ac416
Don't panic on missing deps of change chunks (#538)
* Fix doubly-reported ops in load of change chunks

Since c3c04128f5, observers have been
called twice when calling Automerge::load() with change chunks.

* Better handle change chunks with missing deps

Before this change Automerge::load would panic if you passed a change
chunk that was missing a dependency, or multiple change chunks not in
strict dependency order. After this change these cases will error
instead.
2023-02-27 20:12:09 +00:00
Jason Kankiewicz
8de2fa9bd4
C API 2 (#530)
The AMvalue union, AMlistItem struct, AMmapItem struct, and AMobjItem struct are gone, replaced by the AMitem struct.

The AMchangeHashes, AMchanges, AMlistItems, AMmapItems, AMobjItems, AMstrs, and AMsyncHaves iterators are gone, replaced by the AMitems iterator.

The AMitem struct is opaque, getting and setting values is now achieved exclusively through function calls.

The AMitemsNext(), AMitemsPrev(), and AMresultItem() functions return a pointer to an AMitem struct so you ultimately get the same thing whether you're iterating over a sequence or calling AMmapGet() or AMlistGet().

Calling AMitemResult() on an AMitem struct will produce a new AMresult struct referencing its storage so now the AMresult struct for an iterator can be subsequently freed without affecting the AMitem structs that were filtered out of it.

The storage for a set of AMitem structs can be recombined into a single AMresult struct by passing pointers to their corresponding AMresult structs to AMresultCat().

For C/C++ programmers, I've added AMstrCmp(), AMstrdup(), AM{idxType,objType,status,valType}ToString() and AM{idxType,objType,status,valType}FromString(). It's also now possible to pass arbitrary parameters through AMstack{Item,Items,Result}() to a callback function.
2023-02-25 18:47:00 +00:00
Philip Schatz
407faefa6e
A few setup fixes (#529)
* include deno in dependencies

* install javascript dependencies

* remove redundant operation
2023-02-15 09:23:02 +00:00
Alex Good
1425af43cd @automerge/automerge@2.0.2 2023-02-15 00:06:23 +00:00
Alex Good
c92d042c87 @automerge/automerge-wasm@0.1.24 and @automerge/automerge@2.0.2-alpha.2 2023-02-14 17:59:23 +00:00
Alex Good
9271b20cf5 Correct logic when skip = B and fix formatting
A few tests were failing which exposed the fact that if skip is `B` (the
out factor of the OpTree) then we set `skip = None` and this causes us
to attempt to return `Skip` in a non root node. I ported the failing
test from JS to Rust and fixed the problem.

I also fixed the formatting issues.
2023-02-14 17:21:59 +00:00
Orion Henry
5e82dbc3c8 rework how skip works to push the logic into node 2023-02-14 17:21:59 +00:00
Conrad Irwin
2cd7427f35 Use our leb128 parser for values
This ensures that values in automerge documents are encoded correctly,
and that no extra data is smuggled in any LEB fields.
2023-02-09 15:46:22 +00:00
Alex Good
11f063cbfe
Remove nightly from CI 2023-02-09 11:06:24 +00:00
Alex Good
a24d536d16 Move automerge::SequenceTree to automerge_wasm::SequenceTree
The `SequenceTree` is only ever used in `automerge_wasm` so move it
there.
2023-02-05 11:08:33 +00:00
Alex Good
c5fde2802f @automerge/automerge-wasm@0.1.24 and @automerge/automerge@2.0.2-alpha.1 2023-02-03 16:31:46 +00:00
Alex Good
13a775ed9a Speed up loading by generating clocks on demand
Context: currently we store a mapping from ChangeHash -> Clock, where
`Clock` is the set of (ActorId, (Sequence number, max Op)) pairs derived
from the given change and it's dependencies. This clock is used to
determine what operations are visible at a given set of heads.

Problem: populating this mapping for documents with large histories
containing many actors can be very slow as for each change we have to
allocate and merge a bunch of hashmaps.

Solution: instead of creating the clocks on load, create an adjacency
list based representation of the change graph and then derive the clock
from this graph when it is needed. Traversing even large graphs is still
almost as fast as looking up the clock in a hashmap.
2023-02-03 16:15:15 +00:00
Alex Good
1e33c9d9e0 Use Automerge::load instead of load_incremental if empty
Problem: when running the sync protocol for a new document the API
requires that the user create an empty document and then call
`receive_sync_message` on that document. This results in the OpObserver
for the new document being called with every single op in the document
history. For documents with a large history this can be extremely time
consuming, but the OpObserver doesn't need to know about all the hidden
states.

Solution: Modify `Automerge::load_with` and
`Automerge::apply_changes_with` to check if the document is empty before
applying changes. If the document _is_ empty then we don't call the
observer for every change, but instead use
`automerge::observe_current_state` to notify the observer of the new
state once all the changes have been applied.
2023-02-03 10:01:12 +00:00
Alex Good
c3c04128f5 Only observe the current state on load
Problem: When loading a document whilst passing an `OpObserver` we call
the OpObserver for every change in the loaded document. This slows down
the loading process for two reasons: 1) we have to make a call to the
observer for every op 2) we cannot just stream the ops into the OpSet in
topological order but must instead buffer them to pass to the observer.

Solution: Construct the OpSet first, then only traverse the visible ops
in the OpSet, calling the observer. For documents with a deep history
this results in vastly fewer calls to the observer and also allows us to
construct the OpSet much more quickly. It is slightly different
semantically because the observer never gets notified of changes which
are not visible, but that shouldn't matter to most observers.
2023-02-03 10:01:12 +00:00
Alex Good
da55dfac7a refactor: make fields of Automerge private
The fields of `automerge::Automerge` were crate public, which made it
hard to change the structure of `Automerge` with confidence. Make all
fields private and put them behind accessors where necessary to allow
for easy internal changes.
2023-02-03 10:01:12 +00:00
alexjg
9195e9cb76
Fix deny errors (#518)
* Ignore deny errors on duplicate windows-sys

* Delete spurious lockfile in automerge-cli
2023-02-02 15:02:53 +00:00
dependabot[bot]
f8d5a8ea98
Bump json5 from 1.0.1 to 1.0.2 in /javascript/examples/create-react-app (#487)
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2. in javascript/examples/create-react-app
2023-02-01 09:15:54 +00:00
alexjg
2a9652e642
typescript: Hide API type and make SyncState opaque (#514) 2023-02-01 09:15:00 +00:00
Conrad Irwin
a6959e70e8
More robust leb128 parsing (#515)
Before this change i64 decoding did not work for negative numbers (not a
real problem because it is only used for the timestamp of a change),
and both u64 and i64 would allow overlong LEB encodings.
2023-01-31 17:54:54 +00:00
alexjg
de5af2fffa
automerge-rs 0.3.0 and automerge-test 0.2.0 (#512) 2023-01-30 19:58:35 +00:00
alexjg
08801ab580
automerge-rs: Introduce ReadDoc and SyncDoc traits and add documentation (#511)
The Rust API has so far grown somewhat organically driven by the needs of the
javascript implementation. This has led to an API which is quite awkward and
unfamiliar to Rust programmers. Additionally there is no documentation to speak
of. This commit is the first movement towards cleaning things up a bit. We touch
a lot of files but the changes are all very mechanical. We introduce a few
traits to abstract over the common operations between `Automerge` and
`AutoCommit`, and add a whole bunch of documentation.

* Add a `ReadDoc` trait to describe methods which read value from a document.
  make `Transactable` extend `ReadDoc`
* Add a `SyncDoc` trait to describe methods necessary for synchronizing
  documents.
* Put the `SyncDoc` implementation for `AutoCommit` behind `AutoCommit::sync` to
  ensure that any open transactions are closed before taking part in the sync
  protocol
* Split `OpObserver` into two traits: `OpObserver` + `BranchableObserver`.
  `BranchableObserver` captures the methods which are only needed for observing
  transactions.
* Add a whole bunch of documentation.

The main changes Rust users will need to make is:

* Import the `ReadDoc` trait wherever you are using the methods which have been
  moved to it. Optionally change concrete paramters on functions to `ReadDoc`
  constraints.
* Likewise import the `SyncDoc` trait wherever you are doing synchronisation
  work
* If you are using the `AutoCommit::*_sync_message` methods you will need to add
  a call to `AutoCommit::sync()` first. E.g. `doc.generate_sync_message` becomes
  `doc.sync().generate_sync_message`
* If you have an implementation of `OpObserver` which you are using in an
  `AutoCommit` then split it into an implementation of `OpObserver` and
  `BranchableObserver`
2023-01-30 19:37:03 +00:00
alexjg
89a0866272
@automerge/automerge@2.0.1 (#510) 2023-01-28 21:22:45 +00:00
Alex Good
9b6a3c8691
Update README 2023-01-28 09:32:21 +00:00
alexjg
58a7a06b75
@automerge/automerge-wasm@0.1.23 and @automerge/automerge@2.0.1-alpha.6 (#509) 2023-01-27 20:27:11 +00:00
alexjg
f428fe0169
Improve typescript types (#508) 2023-01-27 17:23:13 +00:00
Conrad Irwin
931ee7e77b
Add Fuzz Testing (#498)
* Add fuzz testing for document load

* Fix fuzz crashers and add to test suite
2023-01-25 16:03:05 +00:00
alexjg
819767cc33
fix: use saturating_sub when updating cached text width (#505)
Problem: In `automerge::query::Index::change_vis` we use `-=` to
subtract the width of an operation which is being hidden from the text
widths which we store on the index of each node in the optree. This
index represents the width of all the visible text operations in this
node and below. This was causing an integer underflow error when
encountering some list operations. More specifically, when a
`ScalarValue::Str` in a list was made invisible by a later operation
which contained a _shorter_ string, the width subtracted from the indexed
text widths could be longer than the current index.

Solution: use `saturating_sub` instead. This is technically papering
over the problem because really the width should never go below zero,
but the text widths are only relevant for text objects where the
existing logic works as advertised because we don't have a `set`
operation for text indices. A more robust solution would be to track the
type of the Index (and consequently of the `OpTree`) at the type level,
but time is limited and problems are infinite.

Also, add a lengthy description of the reason we are using
`saturating_sub` so that when I read it in about a month I don't have
to redo the painful debugging process that got me to this commit.
2023-01-23 19:19:55 +00:00
Alex Currie-Clark
78adbc4ff9
Update patch types (#499)
* Update `Patch` types

* Clarify that the splice patch applies to text

* Add Splice patch type to exports

* Add new patches to javascript
2023-01-23 17:02:02 +00:00
Andrew Jeffery
1f7b109dcd
Add From<SmolStr> for ScalarValue::Str (#506) 2023-01-23 17:01:41 +00:00
Conrad Irwin
98e755106f
Fix and simplify lebsize calculations (#503)
Before this change numbits_i64() was incorrect for every value of the
form 0 - 2^x. This only manifested in a visible error if x%7 == 6 (so
for -64, -8192, etc.) at which point `lebsize` would return a value one
too large, causing a panic in commit().
2023-01-23 11:01:05 +00:00
alexjg
6b0ee6da2e
Bump js to 2.0.1-alpha.5 and automerge-wasm to 0.1.22 (#497) 2023-01-19 22:15:06 +00:00
alexjg
9b44a75f69
fix: don't panic when generating parents for hidden objects (#500)
Problem: the `OpSet::export_key` method uses `query::ElemIdPos` to
determine the index of sequence elements when exporting a key. This
query returned `None` for invisible elements. The `Parents` iterator
which is used to generate paths to objects in patches in
`automerge-wasm` used `export_key`. The end result is that applying a
remote change which deletes an object in a sequence would panic as it
tries to generate a path for an invisible object.

Solution: modify `query::ElemIdPos` to include invisible objects. This
does mean that the path generated will refer to the previous visible
object in the sequence as it's index, but this is probably fine as for
an invisible object the path shouldn't be used anyway.

While we're here also change the return value of `OpSet::export_key` to
an `Option` and make `query::Index::ops` private as obeisance to the
Lady of the Golden Blade.
2023-01-19 21:11:36 +00:00
alexjg
d8baa116e7
automerge-rs: Add ExId::to_bytes (#491)
The `ExId` structure has some internal details which make lookups for
object IDs which were produced by the document doing the looking up
faster. These internal details are quite specific to the implementation
so we don't want to expose them as a public API. On the other hand, we
need to be able to serialize `ExId`s so that FFI clients can hold on to
them without referencing memory which is owned by the document (ahem,
looking at you Java).

Introduce `ExId::to_bytes` and `TryFrom<&[u8]> ExId` implementing a
canonical serialization which includes a version tag, giveing us
compatibility options if we decide to change the implementation.
2023-01-19 17:02:47 +00:00
alexjg
5629a7bec4
Various CI script fixes (#501)
Some of the scripts in scripts/ci were not reliable detecting the path
they were operating in. Additionally the deno_tests script was not
correctly picking up the ROOT_MODULE environment variable. Add more
robust path handling and fix the deno_tests script.
2023-01-19 15:38:27 +00:00
alexjg
964ae2bd81
Fix SeekOpWithPatch on optrees with only internal optrees (#496)
In #480 we fixed an issue where `SeekOp` calculated an incorrect
insertion index on optrees where the only visible ops were on internal
nodes. We forgot to port this fix to `SeekOpWithPatch`, which has almost
the same logic just with additional work done in order to notify an
`OpObserver` of changes. Add a test and fix to `SeekOpWithPatch`
2023-01-14 11:27:48 +00:00
Alex Good
d8df1707d9
Update rust toolchain for "linux" step 2023-01-14 11:06:58 +00:00
Alex Currie-Clark
681a3f1f3f
Add github action to deploy deno package 2023-01-13 10:33:47 +00:00
Alex Good
22e9915fac automerge-wasm: publish release build in Github Action 2023-01-12 12:42:19 +00:00
Alex Good
2d8df12522
re-enable version check for WASM release 2023-01-12 11:35:48 +00:00
Alex Good
f073dbf701
use setup-node prior to attempting to publish in release action 2023-01-12 11:04:22 +00:00
Alex Good
5c02445bee
Bump automerge-wasm, again
In order to re-trigger the release action we are testing we bump the
version which was de-bumped in the last commit.
2023-01-12 10:39:11 +00:00
Alex Good
3ef60747f4
Roll back automerge-wasm to test release action
The release action we are working conditionally executes based on the
version of `automerge-wasm` in the previous commit. We need to trigger
it even though the version has not changed so we roll back the version
in this commit and the commit immediately following this will bump it
again.
2023-01-12 10:37:11 +00:00
Alex Good
d12bd3bb06
correctly call npm publish in release action 2023-01-12 10:27:03 +00:00
Alex Good
a0d698dc8e
Version bump js and wasm
js: 2.0.1-alpha.3
wasm: 0.1.20
2023-01-12 09:55:12 +00:00
Alex Currie-Clark
93a257896e Release action: Fix for check that WASM version has been updated before publishing 2023-01-12 09:44:48 +00:00
Alex Currie-Clark
9c3d0976c8 Add workflow to generate a deno.land and npm release when pushing a new automerge-wasm version to #main 2023-01-11 17:19:24 +00:00
Orion Henry
1ca1cc38ef
Merge pull request #484 from automerge/text2-compat
Text2 compat
2023-01-10 09:16:22 -08:00
Alex Good
0e7fb6cc10
javascript: Add @packageDocumentation TSDoc
Instead of using the `--readme` argument to `typedoc` use the
`@packageDocumentation` TSDoc tag to include the readme text in the
typedoc output.
2023-01-10 15:02:56 +00:00
Alex Good
d1220b9dd0
javascript: Use glob to list files in package.json
We have been listing all the files to be included in the distributed
package in package.json:files. This is tedious and error prone. We
change to using globs instead, to do this without also including the
test and src files when outputting declarations we add a new typescript
config file for the declaration generation which excludes tests.
2023-01-10 12:52:21 +00:00
Alex Good
6c0d102032
automerge-js: Add backwards compatibility text layer
The new text features are faster and more ergonomic but not backwards
compatible. In order to make them backwards compatible re-expose the
original functionality and move the new API under a `future` export.
This allows users to interoperably use both implementations.
2023-01-10 12:52:21 +00:00
Alex Good
5763210b07
wasm: Allow a choice of text representations
The wasm codebase assumed that clients want to represent text as a
string of characters. This is faster, but in order to enable backwards
compatibility we add a `TextRepresentation` argument to
`automerge_wasm::Automerge::new` to allow clients to choose between a
`string` or `Array<any>` representation. The `automerge_wasm::Observer`
will consult this setting to determine what kind of diffs to generate.
2023-01-10 12:52:19 +00:00
Alex Good
18a3f61704 Update rust toolchain to 1.66 2023-01-10 12:51:56 +00:00
Alex Currie-Clark
0306ade939 Update action name on IncPatch type 2023-01-06 15:23:41 +00:00
Alex Good
1e7dcdedec automerge-js: Add prettier
It's christmas, everyone is on holiday, it's time to change every single
file in the repository!
2022-12-22 17:33:14 +00:00
Alex Good
8a645bb193 js: Enable typescript for the JS tests
The tsconfig.json was setup to not include the JS tests. Update the
config to include the tests when checking typescript and fix all the
consequent errors. None of this is semantically meaningful _except_ for
a few incorrect usages of the API which were leading to flaky tests.
Hooray for types!
2022-12-22 11:48:06 +00:00
Alex Good
4de0756bb4 Correctly handle ops on optree node boundaries
The `SeekOp` query can produce incorrect results when the optree it is
searching only has visible ops on the internal nodes. Add some tests to
demonstrate the issue as well as a fix.
2022-12-20 20:38:29 +00:00
Alex Good
d678280b57 automerge-cli: Add an examine-sync command
This is useful when receiving sync messages that behave in unexptected
ways
2022-12-19 16:30:14 +00:00
Alex Good
f682db3039 automerge-cli: Add a flag to skip verifiying heads 2022-12-19 16:30:14 +00:00
Alex Good
6da93b6adc Correctly implement colored json
My quickly thrown together implementation had somem mistakes in it which
meant that the JSON produced was malformed.
2022-12-19 16:30:14 +00:00
Alex Good
0f90fe4d02 Add a method for loading a document without verifying heads
This is primarily useful when debugging documents which have been
corrupted somehow so you would like to see the ops even if you can't
trust them. Note that this is _not_ currently useful for performance
reasons as the hash graph is still constructed, just not verified.
2022-12-19 16:30:14 +00:00
alexjg
8aff1296b9
automerge-cli: remove a bunch of bad dependencies (#478)
Automerge CLI depends transitively (via and old version of `clap` and
via `colored_json` on `atty` and `ansi_term`. These crates are both
marked as unmaintained and this generates irritating `cargo deny`
messages. To avoid this, implement colored JSON ourselves using the
`termcolor` crate - colored JSON is pretty mechanical. Also update
criterion and cbindgen dependencies and ignore the criterion tree in
deny.toml as we only ever use it in benchmarks.

All that's left now is a warning about atty in cbindgen, we'll just have
to wait for cbindgen to fix that, it's a build time dependency anyway so
it's not really an issue.
2022-12-14 18:06:19 +00:00
Conrad Irwin
6dad2b7df1
Don't panic on invalid gzip stream (#477)
* Don't panic on invalid gzip stream

Before this change automerge-rs would panic if the gzip data in
a raw column was invalid; after this change the error is propagated
to the caller correctly.
2022-12-14 17:34:22 +00:00
patryk
e75ca2a834
Update README.md (Update Slack invite link) (#475)
Slack invite link updated to the one used on the website, as the current one returns "This link is no longer active".
2022-12-14 11:41:21 +00:00
Orion Henry
3229548fc7
update js dependencies and some lint errors (#474) 2022-12-11 21:26:00 +00:00
Orion Henry
a96f77c96b
Merge pull request #458 from automerge/dependabot/npm_and_yarn/javascript/examples/create-react-app/loader-utils-2.0.4
Bump loader-utils from 2.0.2 to 2.0.4 in /javascript/examples/create-react-app
2022-12-11 11:36:38 -08:00
Orion Henry
b78211ca65
change opid to (u32,u32) - 10% performance uptick (#473) 2022-12-11 18:56:20 +00:00
Orion Henry
1222fc0df1
rewrite opnode to store usize instead of Op (#471) 2022-12-10 10:36:05 +00:00
Orion Henry
2db9e78f2a
Text v2. JS Api now uses text by default (#462) 2022-12-09 23:48:07 +00:00
Conrad Irwin
b05c9e83a4
Use AMbyteSpan for AM{list,map}PutBytes (#464)
* Use AMbyteSpan for byte values

Before this change there was an inconsistency between AMmapPutString
(which took an AMbyteSpan) and AMmapPutBytes (which took a pointer +
length).

Either is fine, but we should do the same in both places. I chose this
path to make it clear that the value passed in was an automerge value,
and to be symmetric with AMvalue.bytes when you do an AMmapGet().

I did not update other APIs (like load) that take a pointer + length, as
that is idiomatic usage for C, and these functions are not operating on
byte values stored in automerge.
2022-12-09 16:11:23 +00:00
Conrad Irwin
c3932e6267
Improve docs for building automerge-c on a mac (#465)
* More detailed instructions in README

I struggled to get the project to build for a while when first getting
started, so have added some instructions; and also some usage
instructions for automerge-c that show more clearly what is happening
without `AMpush()`
2022-12-09 13:46:23 +00:00
Alex Good
becc301877
automerge-wasm@0.1.19 & automerge-js@2.0.1-alpha.2 2022-12-02 15:10:24 +00:00
Alex Good
0ab6a770d8 wasm: improve error messages
The error messages produced by various conversions in `automerge-wasm`
were quite uninformative - often consisting of just returning the
offending value with no description of the problem. The logic of these
error messages was often hard to trace due to the use of `JsValue` to
represent both error conditions and valid values - evidenced by most of
the public functions of `automerge-wasm` having return types of
`Result<JsValue, JsValue>`. Change these return types to mention
specific errors, thus enlisting the compilers help in ensuring that
specific error messages are emitted.
2022-12-02 14:42:55 +00:00
Alex Currie-Clark
2826f4f08c
automerge-wasm: Add deno as a target 2022-12-02 14:42:13 +00:00
Alex Good
de16adbcc5 Explicity create empty changes
Transactions with no ops in them are generally undesirable. They take up
space in the change log but do nothing else. They are not useless
though, it may occasionally be necessary to create an empty change in
order to list all the current heads of the document as dependents of the
empty change.

The current API makes no distinction between empty changes and non-empty
changes. If the user calls `Transaction::commit` a change is created
regardless of whether there are ops to commit. To provide a more useful
API modify `commit` so that if there is a no-op transaction then no
changes are created, but provide explicit methods to create an empty
change via `Transaction::empty_change`, `Automerge::empty_change` and
`Autocommit::empty_change`. Also make these APIs available in Javascript
and C.
2022-12-02 12:12:54 +00:00
Alex Good
ea5688e418 rust: Make fields of Transaction and TransactionInner private
It's tricky to modify these structs with the fields public as every
change requires scanning the codebase for references to make sure you're
not breaking any invariants. Make the fields private to ease
development.
2022-12-02 12:12:54 +00:00
Alex Good
149f870102 rust: Remove Default constraint from OpObserver 2022-12-02 12:12:54 +00:00
Andrew Jeffery
e0b2bc995a
Update nix flake and add formatter and dead code check (#466)
* Add formatter for flake

* Update flake inputs

* Remove unused vars in flake

* Add deadnix check and fixup devshells naming
2022-11-30 12:57:59 +00:00
Orion Henry
aaddb3c9ea fix error message 2022-11-28 15:43:27 -06:00
Orion Henry
2400d67755
Merge pull request #457 from jkankiewicz/return_NUL_string_as_bytes
Prevent panic when string contains a null character.
2022-11-28 12:34:45 -08:00
Jason Kankiewicz
d3885a3443 Hard-coded automerge-c's initial independent
version number to "0.0.1" for @alexjg.
2022-11-28 00:08:33 -08:00
Jason Kankiewicz
f8428896bd Added a test case for a map key containing NUL
('\0') based on #455.
2022-11-27 23:52:47 -08:00
Jason Kankiewicz
fb0c69cc52 Updated the quickstart example to work with
`AMbyteSpan` values instead of `*const libc::c_char` values.
2022-11-27 23:52:47 -08:00
Jason Kankiewicz
edbb33522d Replaced the C string (*const libc::c_char)
value of the `AMresult::Error` variant with a UTF-8 string view
(`AMbyteSpan`).
2022-11-27 23:52:47 -08:00
Jason Kankiewicz
625f48f33a Fixed clippy violations. 2022-11-27 23:52:47 -08:00
Jason Kankiewicz
7c9f927136 Fixed code formatting violations. 2022-11-27 23:52:47 -08:00
Jason Kankiewicz
b60c310f5c Changed Default::default() calls to be through
the trait.
2022-11-27 23:52:47 -08:00
Jason Kankiewicz
3dd954d5b7 Moved the to_obj_id macro in with AMobjId. 2022-11-27 23:52:47 -08:00
Jason Kankiewicz
3e2e697504 Replaced C string (*const libc::c_char) values
with UTF-8 string view  (`AMbyteSpan`) values except with the
`AMresult::Error` variant.
Added `AMstr()` for creating an `AMbyteSpan` from a C string.
2022-11-27 23:52:47 -08:00
Jason Kankiewicz
a324b02005 Added automerge::AutomergeError::InvalidActorId.
Added `automerge::AutomergeError::InvalidCharacter`.
Alphabetized the `automerge::AutomergeError` variants.
2022-11-27 23:52:47 -08:00
Alex Good
d26cb0c0cb
rust:automerge-test:0.1.0 2022-11-27 16:54:41 +00:00
Alex Good
ed108ba6fc
rust:automerge:0.2.0 2022-11-27 16:44:26 +00:00
Alex Good
484a5bac4f
rust: Add Transactable::base_heads
Sometimes it is necessary to query the heads of a document at the time a
transaction started without having a mutable reference to the
transactable. Add `Transactable::base_heads` to do this.
2022-11-27 16:39:02 +00:00
Alex Good
01350c2b3f
automerge-wasm@0.1.18 and automerge@2.0.1-alpha.1 2022-11-22 19:37:01 +00:00
alexjg
22d60987f6
Dont send duplicate sync messages (#460)
The API of Automerge::generate_sync_message requires that the user keep
track of in flight messages themselves if they want to avoid sending
duplicate messages. To avoid this add a flag to `automerge::sync::State`
to track if there are any in flight messages and return `None` from
`generate_sync_message` if there are.
2022-11-22 18:29:06 +00:00
Alex Good
bbf729e1d6
@automerge/automerge 2.0.0 2022-11-22 12:13:42 +00:00
Orion Henry
ca25ed0ca0
automerge-wasm: Use a SequenceTree in the OpObserver
Generating patches to text objects (a la the edit-trace benchmark) was
very slow due to appending to the back of a Vec. Use the SequenceTree
(effectively a B-tree) instead so as to speed up sequence patch
generation.
2022-11-22 12:13:42 +00:00
Alex Good
03b3da203d
@automerge/automerge-wasm 0.1.16 2022-11-22 00:02:28 +00:00
Alex Good
e713c35d21
Fix some typescript errors 2022-11-21 18:26:28 +00:00
dependabot[bot]
92c044eadb
Bump loader-utils in /javascript/examples/create-react-app
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.2 to 2.0.4.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v2.0.2...v2.0.4)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-16 13:35:34 +00:00
Jason Kankiewicz
a7656b999b
Add AMobjObjType() (#454)
automerge-c: Add AmobjObjType()
2022-11-07 23:10:53 +00:00
Alex Good
05093071ce
rust/automerge-test: add From<f64> for RealizedObject 2022-11-07 12:08:12 +00:00
Alex Good
bcab3b6e47 Move automerge/tests::helpers to crate automerge-test
The assert_doc and assert_obj macros in automerge/tests::helpers are
useful for writing tests for any application working with automerge
documents. Typically however, you only want these utilities in tests so
rather than packaging them in the main `automerge` crate move them to a
new crate (in the spirit of `tokio_test`)
2022-11-06 19:52:21 +00:00
Alex Good
b53584bec0
Ritual obeisance before the altar of clippy 2022-11-05 22:48:43 +00:00
Orion Henry
91f313bb83 revert compiler flags to max opt 2022-11-04 18:02:32 +00:00
tosti007
6bbed76f0f Update uuid dependency to v1.2.1 2022-11-01 11:39:24 +00:00
Alex Good
bba4fe2c36
@automerge/automerge@2.0.0-beta.4 2022-10-28 11:31:51 +01:00
Alex Good
61aaa52718 Allow changing a cloned document
The logic for `clone` which was updated to support cloning a viewed
document inadverantly left the heads of the cloned document state in
place, which meant that cloned documents could not be `change`d. Set
state.heads to undefined when cloning to allow changing them.
2022-10-27 19:20:41 +01:00
Alex Good
20d543d28d
@automerge/automerge@2.0.0-beta.3 2022-10-26 14:14:01 +01:00
Alex Good
5adb6952e9
@automerge/automerge@2.0.0-beta.2 and @automerge/automerge-wasm@0.1.15 2022-10-26 14:03:12 +01:00
Orion Henry
3705212747
js: Add Automerge.clone(_, heads) and Automerge.view
Sometimes you need a cheap copy of a document at a given set of heads
just so you can see what has changed. Cloning the document to do this is
quite expensive when you don't need a writable copy. Add automerge.view
to allow a cheap read only copy of a document at a given set of heads
and add an additional heads argument to clone for when you do want a
writable copy.
2022-10-26 14:01:11 +01:00
Orion Henry
d7d2916acb tiny change that might remove a bloom filter false positive error 2022-10-21 15:15:30 -05:00
Alex Good
3482e06b15
javascript 2.0.0-beta1 2022-10-18 19:43:46 +01:00
Orion Henry
59289f67b1 consolidate inserts and deletes more aggressivly into a single splice 2022-10-18 13:29:56 +01:00
Alex Good
a4a3dd9ed3
Fix docs CI 2022-10-18 13:08:08 +01:00
Alex Good
ac6eeb8711
Another attempt at fixing cmake build CI 2022-10-18 12:46:22 +01:00
Alex Good
20adff0071
Fix cmake CI
The cmake CI seemed to reference a few nonexistent targets for docs and
tests. Remove the doc generation step and point the test CI script at
the generated test program.
2022-10-18 11:56:37 +01:00
Alex Good
6bb611e4b3
Update CI to rust 1.64.0 2022-10-18 11:49:46 +01:00
Alex Good
e8309495ce
Update cargo deny to point at rust subdirectory 2022-10-18 11:28:56 +01:00
Orion Henry
4755c5bf5e
Merge pull request #444 from automerge/freeze
make freeze work recursively
2022-10-17 16:31:52 -07:00
Orion Henry
38205fbcc2 enableFreeze() instead of implicit freeze 2022-10-17 17:35:34 -05:00
Orion Henry
a2704bac4b Merge remote-tracking branch 'origin/main' into f2 2022-10-17 16:32:23 -05:00
Orion Henry
ac90f8f028 Merge remote-tracking branch 'origin/freeze' into f2 2022-10-17 16:21:35 -05:00
Orion Henry
c602e9e7ed update build to match directory restructuring 2022-10-17 16:20:25 -05:00
Alex Good
1c6da6f9a3
Add JS worker config to Vite app example
Vite apps which use SharedWorker of WebWorker require additional
configuration to get WebAssembly imports to work effectively, add these
to the example.
2022-10-17 01:09:13 +01:00
Alex Good
24dcf8270a
Add typedoc comments to the entire public JS API 2022-10-17 00:41:06 +01:00
Alex Good
e189ec9ca8
Add some READMEs to the javascript directory 2022-10-16 20:01:49 +01:00
Alex Good
96f15c6e00
Update main README to reflect new repo layout 2022-10-16 20:01:45 +01:00
Alex Good
8e131922e7
Move wrappers/javascript -> javascript
Continuing our theme of treating all languages equally, move
wrappers/javascript to javascrpit. Automerge libraries for new languages
should be built at this top level if possible.
2022-10-16 19:55:54 +01:00
Alex Good
dd3c6d1303
Move rust workspace into ./rust
After some discussion with PVH I realise that the repo structure in the
last reorg was very rust-centric. In an attempt to put each language on
a level footing move the rust code and project files into ./rust
2022-10-16 19:55:51 +01:00
Orion Henry
5ce3a556a9 weak_refs 2022-10-16 19:55:25 +01:00
Orion Henry
dd5edafa9d make freeze work recursively 2022-10-15 21:21:18 -05:00
Alex Good
cd2997e63f
@automerge/automerge@2.0.0-alpha.5 and @automerge/automerge-wasm@0.1.10 2022-10-13 23:13:09 +01:00
Orion Henry
f0f036eb89
add loadIncremental to js 2022-10-13 23:03:01 +01:00
Alex Good
ee0c3ef3ac javascript: Make getObjectId tolerate non object arguments
Fixes #433. `getObjectId` was previously throwing an error if passed
something which was not an object. In the process of fixing this I
simplified the logic of `getObjectId` by modifying automerge-wasm to not
set the OBJECT_ID hidden property on objects which are not maps, lists,
or text - it was previously setting this property on anything which was
a JS object, including `Date` and `Uint8Array`.
2022-10-13 21:37:37 +01:00
Orion Henry
e6d1828c12
Merge pull request #440 from automerge/repo-reorg
Repo reorg
2022-10-12 10:07:02 -07:00
Alex Good
4c17fd9c00
Update README
We're making this project the primary implementation of automerge.
Update the README to provide more context and signpost other resources.
2022-10-12 16:25:43 +01:00
Alex Good
660678d038
remove unneeded files 2022-10-12 16:25:43 +01:00
Alex Good
a7a4bd42f1
Move automerge-js -> wrappers/javascript
Whilst we only have one wrapper library, we anticipate more.
Furthermore, the naming of the `wrappers` directory makes it clear what
the role of the JS codebase is.
2022-10-12 16:25:43 +01:00
Alex Good
352a0127c7
Move all rust code into crates/*
For larger rust projects it's common to put all rust code in a directory
called `crates`. This helps in general by reducing the number of
directories in the top level but it's particularly helpful for us
because some directories _do not_ contain Rust code. In particular
`automerge-js`. Move rust code into `/crates` to make the repo easier
to navigate.
2022-10-12 16:25:38 +01:00
Alex Good
ed0da24020
Track whether a transaction is observed in types
With the `OpObserver` moving to the transaction rather than being passed
in to the `Transaction::commit` method we have needed to add a way to
get the observer back out of the transaction (via
`Transaction::observer` and `AutoCommit::observer`). This `Observer`
type is then used to handle patch generation logic. However, there are
cases where we might not want an `OpObserver` and in these cases we can
execute various things fast - so we need to have something like an
`Option<OpObserver>`. In order to track the presence or otherwise of the
observer at the type level introduce
`automerge::transaction::observation`, which is a type level `Option`.
This allows us to efficiently choose the right code paths whilst
maintaining correct types for `Transaction::observer` and
`AutoCommit::observer`
2022-10-12 16:11:23 +01:00
507 changed files with 35272 additions and 25236 deletions

View file

@ -14,7 +14,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.60.0
toolchain: 1.67.0
default: true
components: rustfmt
- uses: Swatinem/rust-cache@v1
@ -28,7 +28,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.60.0
toolchain: 1.67.0
default: true
components: clippy
- uses: Swatinem/rust-cache@v1
@ -42,7 +42,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.60.0
toolchain: 1.67.0
default: true
- uses: Swatinem/rust-cache@v1
- name: Build rust docs
@ -51,9 +51,6 @@ jobs:
- name: Install doxygen
run: sudo apt-get install -y doxygen
shell: bash
- name: Build C docs
run: ./scripts/ci/cmake-docs
shell: bash
cargo-deny:
runs-on: ubuntu-latest
@ -67,23 +64,50 @@ jobs:
- uses: actions/checkout@v2
- uses: EmbarkStudios/cargo-deny-action@v1
with:
arguments: '--manifest-path ./rust/Cargo.toml'
command: check ${{ matrix.checks }}
wasm_tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Install wasm-bindgen-cli
run: cargo install wasm-bindgen-cli wasm-opt
- name: Install wasm32 target
run: rustup target add wasm32-unknown-unknown
- name: run tests
run: ./scripts/ci/wasm_tests
deno_tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: denoland/setup-deno@v1
with:
deno-version: v1.x
- name: Install wasm-bindgen-cli
run: cargo install wasm-bindgen-cli wasm-opt
- name: Install wasm32 target
run: rustup target add wasm32-unknown-unknown
- name: run tests
run: ./scripts/ci/deno_tests
js_fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: install
run: yarn global add prettier
- name: format
run: prettier -c javascript/.prettierrc javascript
js_tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Install wasm-bindgen-cli
run: cargo install wasm-bindgen-cli wasm-opt
- name: Install wasm32 target
run: rustup target add wasm32-unknown-unknown
- name: run tests
run: ./scripts/ci/js_tests
@ -94,7 +118,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.60.0
toolchain: nightly-2023-01-26
default: true
- uses: Swatinem/rust-cache@v1
- name: Install CMocka
@ -103,6 +127,8 @@ jobs:
uses: jwlawson/actions-setup-cmake@v1.12
with:
cmake-version: latest
- name: Install rust-src
run: rustup component add rust-src
- name: Build and test C bindings
run: ./scripts/ci/cmake-build Release Static
shell: bash
@ -112,9 +138,7 @@ jobs:
strategy:
matrix:
toolchain:
- 1.60.0
- nightly
continue-on-error: ${{ matrix.toolchain == 'nightly' }}
- 1.67.0
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
@ -133,7 +157,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.60.0
toolchain: 1.67.0
default: true
- uses: Swatinem/rust-cache@v1
- run: ./scripts/ci/build-test
@ -146,7 +170,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.60.0
toolchain: 1.67.0
default: true
- uses: Swatinem/rust-cache@v1
- run: ./scripts/ci/build-test

View file

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

214
.github/workflows/release.yaml vendored Normal file
View file

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

3
.gitignore vendored
View file

@ -1,7 +1,6 @@
/target
/.direnv
perf.*
/Cargo.lock
build/
automerge/proptest-regressions/
.vim/*
/target

View file

@ -1,20 +0,0 @@
.PHONY: rust
rust:
cd automerge && cargo test
.PHONY: wasm
wasm:
cd automerge-wasm && yarn
cd automerge-wasm && yarn build
cd automerge-wasm && yarn test
cd automerge-wasm && yarn link
.PHONY: js
js: wasm
cd automerge-js && yarn
cd automerge-js && yarn link "automerge-wasm"
cd automerge-js && yarn test
.PHONY: clean
clean:
git clean -x -d -f

198
README.md
View file

@ -1,4 +1,4 @@
# Automerge RS
# Automerge
<img src='./img/sign.svg' width='500' alt='Automerge logo' />
@ -7,103 +7,141 @@
[![ci](https://github.com/automerge/automerge-rs/actions/workflows/ci.yaml/badge.svg)](https://github.com/automerge/automerge-rs/actions/workflows/ci.yaml)
[![docs](https://github.com/automerge/automerge-rs/actions/workflows/docs.yaml/badge.svg)](https://github.com/automerge/automerge-rs/actions/workflows/docs.yaml)
This is a Rust library implementation of the [Automerge](https://github.com/automerge/automerge) file format and network protocol. Its focus is to support the creation of Automerge implementations in other languages, currently; WASM, JS and C. A `libautomerge` if you will.
Automerge is a library which provides fast implementations of several different
CRDTs, a compact compression format for these CRDTs, and a sync protocol for
efficiently transmitting those changes over the network. The objective of the
project is to support [local-first](https://www.inkandswitch.com/local-first/) applications in the same way that relational
databases support server applications - by providing mechanisms for persistence
which allow application developers to avoid thinking about hard distributed
computing problems. Automerge aims to be PostgreSQL for your local-first app.
The original [Automerge](https://github.com/automerge/automerge) project (written in JS from the ground up) is still very much maintained and recommended. Indeed it is because of the success of that project that the next stage of Automerge is being explored here. Hopefully Rust can offer a more performant and scalable Automerge, opening up even more use cases.
If you're looking for documentation on the JavaScript implementation take a look
at https://automerge.org/docs/hello/. There are other implementations in both
Rust and C, but they are earlier and don't have documentation yet. You can find
them in `rust/automerge` and `rust/automerge-c` if you are comfortable
reading the code and tests to figure out how to use them.
If you're familiar with CRDTs and interested in the design of Automerge in
particular take a look at https://automerge.org/docs/how-it-works/backend/
Finally, if you want to talk to us about this project please [join the
Slack](https://join.slack.com/t/automerge/shared_invite/zt-e4p3760n-kKh7r3KRH1YwwNfiZM8ktw)
## Status
The project has 5 components:
This project is formed of a core Rust implementation which is exposed via FFI in
javascript+WASM, C, and soon other languages. Alex
([@alexjg](https://github.com/alexjg/)]) is working full time on maintaining
automerge, other members of Ink and Switch are also contributing time and there
are several other maintainers. The focus is currently on shipping the new JS
package. We expect to be iterating the API and adding new features over the next
six months so there will likely be several major version bumps in all packages
in that time.
1. [_automerge_](automerge) - The main Rust implementation of the library.
2. [_automerge-wasm_](automerge-wasm) - A JS/WASM interface to the underlying Rust library. This API is generally mature and in use in a handful of projects.
3. [_automerge-js_](automerge-js) - This is a Javascript library using the WASM interface to export the same public API of the primary Automerge project. Currently this project passes all of Automerge's tests but has not been used in any real project or packaged as an NPM. Alpha testers welcome.
4. [_automerge-c_](automerge-c) - This is a C library intended to be an FFI integration point for all other languages. It is currently a work in progress and not yet ready for any testing.
5. [_automerge-cli_](automerge-cli) - An experimental CLI wrapper around the Rust library. Currently not functional.
In general we try and respect semver.
## How?
### JavaScript
The magic of the architecture is built around the `OpTree`. This is a data structure
which supports efficiently inserting new operations and realising values of
existing operations. Most interactions with the `OpTree` are in the form of
implementations of `TreeQuery` - a trait which can be used to traverse the
`OpTree` and producing state of some kind. User facing operations are exposed on
an `Automerge` object, under the covers these operations typically instantiate
some `TreeQuery` and run it over the `OpTree`.
A stable release of the javascript package is currently available as
`@automerge/automerge@2.0.0` where. pre-release verisions of the `2.0.1` are
available as `2.0.1-alpha.n`. `2.0.1*` packages are also available for Deno at
https://deno.land/x/automerge
## Development
### Rust
Please feel free to open issues and pull requests.
The rust codebase is currently oriented around producing a performant backend
for the Javascript wrapper and as such the API for Rust code is low level and
not well documented. We will be returning to this over the next few months but
for now you will need to be comfortable reading the tests and asking questions
to figure out how to use it. If you are looking to build rust applications which
use automerge you may want to look into
[autosurgeon](https://github.com/alexjg/autosurgeon)
### Running CI
## Repository Organisation
The steps CI will run are all defined in `./scripts/ci`. Obviously CI will run
everything when you submit a PR, but if you want to run everything locally
before you push you can run `./scripts/ci/run` to run everything.
- `./rust` - the rust rust implementation and also the Rust components of
platform specific wrappers (e.g. `automerge-wasm` for the WASM API or
`automerge-c` for the C FFI bindings)
- `./javascript` - The javascript library which uses `automerge-wasm`
internally but presents a more idiomatic javascript interface
- `./scripts` - scripts which are useful to maintenance of the repository.
This includes the scripts which are run in CI.
- `./img` - static assets for use in `.md` files
### Running the JS tests
## Building
You will need to have [node](https://nodejs.org/en/), [yarn](https://yarnpkg.com/getting-started/install), [rust](https://rustup.rs/) and [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/) installed.
To build this codebase you will need:
To build and test the rust library:
- `rust`
- `node`
- `yarn`
- `cmake`
- `cmocka`
```shell
$ cd automerge
$ cargo test
You will also need to install the following with `cargo install`
- `wasm-bindgen-cli`
- `wasm-opt`
- `cargo-deny`
And ensure you have added the `wasm32-unknown-unknown` target for rust cross-compilation.
The various subprojects (the rust code, the wrapper projects) have their own
build instructions, but to run the tests that will be run in CI you can run
`./scripts/ci/run`.
### For macOS
These instructions worked to build locally on macOS 13.1 (arm64) as of
Nov 29th 2022.
```bash
# clone the repo
git clone https://github.com/automerge/automerge-rs
cd automerge-rs
# install rustup
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# install homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# install cmake, node, cmocka
brew install cmake node cmocka
# install yarn
npm install --global yarn
# install javascript dependencies
yarn --cwd ./javascript
# install rust dependencies
cargo install wasm-bindgen-cli wasm-opt cargo-deny
# get nightly rust to produce optimized automerge-c builds
rustup toolchain install nightly
rustup component add rust-src --toolchain nightly
# add wasm target in addition to current architecture
rustup target add wasm32-unknown-unknown
# Run ci script
./scripts/ci/run
```
To build and test the wasm library:
If your build fails to find `cmocka.h` you may need to teach it about homebrew's
installation location:
```shell
## setup
$ cd automerge-wasm
$ yarn
## building or testing
$ yarn build
$ yarn test
## without this the js library wont automatically use changes
$ yarn link
## cutting a release or doing benchmarking
$ yarn release
```
export CPATH=/opt/homebrew/include
export LIBRARY_PATH=/opt/homebrew/lib
./scripts/ci/run
```
To test the js library. This is where most of the tests reside.
## Contributing
```shell
## setup
$ cd automerge-js
$ yarn
$ yarn link "automerge-wasm"
## testing
$ yarn test
```
And finally, to build and test the C bindings with CMake:
```shell
## setup
$ cd automerge-c
$ mkdir -p build
$ cd build
$ cmake -S .. -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF
## building and testing
$ cmake --build . --target test_automerge
```
To add debugging symbols, replace `Release` with `Debug`.
To build a shared library instead of a static one, replace `OFF` with `ON`.
The C bindings can be built and tested on any platform for which CMake is
available but the steps for doing so vary across platforms and are too numerous
to list here.
## Benchmarking
The [`edit-trace`](edit-trace) folder has the main code for running the edit trace benchmarking.
## The old Rust project
If you are looking for the origional `automerge-rs` project that can be used as a wasm backend to the javascript implementation, it can be found [here](https://github.com/automerge/automerge-rs/tree/automerge-1.0).
Please try and split your changes up into relatively independent commits which
change one subsystem at a time and add good commit messages which describe what
the change is and why you're making it (err on the side of longer commit
messages). `git blame` should give future maintainers a good idea of why
something is the way it is.

32
TODO.md
View file

@ -1,32 +0,0 @@
### next steps:
1. C API
2. port rust command line tool
3. fast load
### ergonomics:
1. value() -> () or something that into's a value
### automerge:
1. single pass (fast) load
2. micro-patches / bare bones observation API / fully hydrated documents
### future:
1. handle columns with unknown data in and out
2. branches with different indexes
### Peritext
1. add mark / remove mark -- type, start/end elemid (inclusive,exclusive)
2. track any formatting ops that start or end on a character
3. ops right before the character, ops right after that character
4. query a single character - character, plus marks that start or end on that character
what is its current formatting,
what are the ops that include that in their span,
None = same as last time, Set( bold, italic ),
keep these on index
5. op probably belongs with the start character - possible packed at the beginning or end of the list
### maybe:
1. tables
### no:
1. cursors

View file

@ -1,3 +0,0 @@
automerge
automerge.h
automerge.o

View file

@ -1,141 +0,0 @@
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
# Parse the library name, project name and project version out of Cargo's TOML file.
set(CARGO_LIB_SECTION OFF)
set(LIBRARY_NAME "")
set(CARGO_PKG_SECTION OFF)
set(CARGO_PKG_NAME "")
set(CARGO_PKG_VERSION "")
file(READ Cargo.toml TOML_STRING)
string(REPLACE ";" "\\\\;" TOML_STRING "${TOML_STRING}")
string(REPLACE "\n" ";" TOML_LINES "${TOML_STRING}")
foreach(TOML_LINE IN ITEMS ${TOML_LINES})
string(REGEX MATCH "^\\[(lib|package)\\]$" _ ${TOML_LINE})
if(CMAKE_MATCH_1 STREQUAL "lib")
set(CARGO_LIB_SECTION ON)
set(CARGO_PKG_SECTION OFF)
elseif(CMAKE_MATCH_1 STREQUAL "package")
set(CARGO_LIB_SECTION OFF)
set(CARGO_PKG_SECTION ON)
endif()
string(REGEX MATCH "^name += +\"([^\"]+)\"$" _ ${TOML_LINE})
if(CMAKE_MATCH_1 AND (CARGO_LIB_SECTION AND NOT CARGO_PKG_SECTION))
set(LIBRARY_NAME "${CMAKE_MATCH_1}")
elseif(CMAKE_MATCH_1 AND (NOT CARGO_LIB_SECTION AND CARGO_PKG_SECTION))
set(CARGO_PKG_NAME "${CMAKE_MATCH_1}")
endif()
string(REGEX MATCH "^version += +\"([^\"]+)\"$" _ ${TOML_LINE})
if(CMAKE_MATCH_1 AND CARGO_PKG_SECTION)
set(CARGO_PKG_VERSION "${CMAKE_MATCH_1}")
endif()
if(LIBRARY_NAME AND (CARGO_PKG_NAME AND CARGO_PKG_VERSION))
break()
endif()
endforeach()
project(${CARGO_PKG_NAME} VERSION ${CARGO_PKG_VERSION} LANGUAGES C DESCRIPTION "C bindings for the Automerge Rust backend.")
include(CTest)
option(BUILD_SHARED_LIBS "Enable the choice of a shared or static library.")
include(CMakePackageConfigHelpers)
include(GNUInstallDirs)
string(MAKE_C_IDENTIFIER ${PROJECT_NAME} SYMBOL_PREFIX)
string(TOUPPER ${SYMBOL_PREFIX} SYMBOL_PREFIX)
set(CARGO_TARGET_DIR "${CMAKE_CURRENT_BINARY_DIR}/Cargo/target")
set(CBINDGEN_INCLUDEDIR "${CARGO_TARGET_DIR}/${CMAKE_INSTALL_INCLUDEDIR}")
set(CBINDGEN_TARGET_DIR "${CBINDGEN_INCLUDEDIR}/${PROJECT_NAME}")
add_subdirectory(src)
# Generate and install the configuration header.
math(EXPR INTEGER_PROJECT_VERSION_MAJOR "${PROJECT_VERSION_MAJOR} * 100000")
math(EXPR INTEGER_PROJECT_VERSION_MINOR "${PROJECT_VERSION_MINOR} * 100")
math(EXPR INTEGER_PROJECT_VERSION_PATCH "${PROJECT_VERSION_PATCH}")
math(EXPR INTEGER_PROJECT_VERSION "${INTEGER_PROJECT_VERSION_MAJOR} + ${INTEGER_PROJECT_VERSION_MINOR} + ${INTEGER_PROJECT_VERSION_PATCH}")
configure_file(
${CMAKE_MODULE_PATH}/config.h.in
config.h
@ONLY
NEWLINE_STYLE LF
)
install(
FILES ${CMAKE_BINARY_DIR}/config.h
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}
)
if(BUILD_TESTING)
add_subdirectory(test EXCLUDE_FROM_ALL)
enable_testing()
endif()
add_subdirectory(examples EXCLUDE_FROM_ALL)
# Generate and install .cmake files
set(PROJECT_CONFIG_NAME "${PROJECT_NAME}-config")
set(PROJECT_CONFIG_VERSION_NAME "${PROJECT_CONFIG_NAME}-version")
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_VERSION_NAME}.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY ExactVersion
)
# The namespace label starts with the title-cased library name.
string(SUBSTRING ${LIBRARY_NAME} 0 1 NS_FIRST)
string(SUBSTRING ${LIBRARY_NAME} 1 -1 NS_REST)
string(TOUPPER ${NS_FIRST} NS_FIRST)
string(TOLOWER ${NS_REST} NS_REST)
string(CONCAT NAMESPACE ${NS_FIRST} ${NS_REST} "::")
# \note CMake doesn't automate the exporting of an imported library's targets
# so the package configuration script must do it.
configure_package_config_file(
${CMAKE_MODULE_PATH}/${PROJECT_CONFIG_NAME}.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_NAME}.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
)
install(
FILES
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_NAME}.cmake
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_VERSION_NAME}.cmake
DESTINATION
${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
)

View file

@ -1,97 +0,0 @@
## Methods we need to support
### Basic management
1. `AMcreate()`
1. `AMclone(doc)`
1. `AMfree(doc)`
1. `AMconfig(doc, key, val)` // set actor
1. `actor = get_actor(doc)`
### Transactions
1. `AMpendingOps(doc)`
1. `AMcommit(doc, message, time)`
1. `AMrollback(doc)`
### Write
1. `AMset{Map|List}(doc, obj, prop, value)`
1. `AMinsert(doc, obj, index, value)`
1. `AMpush(doc, obj, value)`
1. `AMdel{Map|List}(doc, obj, prop)`
1. `AMinc{Map|List}(doc, obj, prop, value)`
1. `AMspliceText(doc, obj, start, num_del, text)`
### Read (the heads argument is optional and can be on an `at` variant)
1. `AMkeys(doc, obj, heads)`
1. `AMlength(doc, obj, heads)`
1. `AMlistRange(doc, obj, heads)`
1. `AMmapRange(doc, obj, heads)`
1. `AMvalues(doc, obj, heads)`
1. `AMtext(doc, obj, heads)`
### Sync
1. `AMgenerateSyncMessage(doc, state)`
1. `AMreceiveSyncMessage(doc, state, message)`
1. `AMinitSyncState()`
### Save / Load
1. `AMload(data)`
1. `AMloadIncremental(doc, data)`
1. `AMsave(doc)`
1. `AMsaveIncremental(doc)`
### Low Level Access
1. `AMapplyChanges(doc, changes)`
1. `AMgetChanges(doc, deps)`
1. `AMgetChangesAdded(doc1, doc2)`
1. `AMgetHeads(doc)`
1. `AMgetLastLocalChange(doc)`
1. `AMgetMissingDeps(doc, heads)`
### Encode/Decode
1. `AMencodeChange(change)`
1. `AMdecodeChange(change)`
1. `AMencodeSyncMessage(change)`
1. `AMdecodeSyncMessage(change)`
1. `AMencodeSyncState(change)`
1. `AMdecodeSyncState(change)`
## Open Question - Memory management
Most of these calls return one or more items of arbitrary length. Doing memory management in C is tricky. This is my proposed solution...
###
```
// returns 1 or zero opids
n = automerge_set(doc, "_root", "hello", datatype, value);
if (n) {
automerge_pop(doc, &obj, len);
}
// returns n values
n = automerge_values(doc, "_root", "hello");
for (i = 0; i<n ;i ++) {
automerge_pop_value(doc, &value, &datatype, len);
}
```
There would be one pop method per object type. Users allocs and frees the buffers. Multiple return values would result in multiple pops. Too small buffers would error and allow retry.
### Formats
Actors - We could do (bytes,len) or a hex encoded string?.
ObjIds - We could do flat bytes of the ExId struct but lets do human readable strings for now - the struct would be faster but opque
Heads - Might as well make it a flat buffer `(n, hash, hash, ...)`
Changes - Put them all in a flat concatenated buffer
Encode/Decode - to json strings?

View file

@ -1,14 +0,0 @@
#ifndef @SYMBOL_PREFIX@_CONFIG_H
#define @SYMBOL_PREFIX@_CONFIG_H
/* This header is auto-generated by CMake. */
#define @SYMBOL_PREFIX@_VERSION @INTEGER_PROJECT_VERSION@
#define @SYMBOL_PREFIX@_MAJOR_VERSION (@SYMBOL_PREFIX@_VERSION / 100000)
#define @SYMBOL_PREFIX@_MINOR_VERSION ((@SYMBOL_PREFIX@_VERSION / 100) % 1000)
#define @SYMBOL_PREFIX@_PATCH_VERSION (@SYMBOL_PREFIX@_VERSION % 100)
#endif /* @SYMBOL_PREFIX@_CONFIG_H */

View file

@ -1,146 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <automerge-c/automerge.h>
static void abort_cb(AMresultStack**, uint8_t);
/**
* \brief Based on https://automerge.github.io/docs/quickstart
*/
int main(int argc, char** argv) {
AMresultStack* stack = NULL;
AMdoc* const doc1 = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, abort_cb).doc;
AMobjId const* const cards = AMpush(&stack,
AMmapPutObject(doc1, AM_ROOT, "cards", AM_OBJ_TYPE_LIST),
AM_VALUE_OBJ_ID,
abort_cb).obj_id;
AMobjId const* const card1 = AMpush(&stack,
AMlistPutObject(doc1, cards, SIZE_MAX, true, AM_OBJ_TYPE_MAP),
AM_VALUE_OBJ_ID,
abort_cb).obj_id;
AMfree(AMmapPutStr(doc1, card1, "title", "Rewrite everything in Clojure"));
AMfree(AMmapPutBool(doc1, card1, "done", false));
AMobjId const* const card2 = AMpush(&stack,
AMlistPutObject(doc1, cards, SIZE_MAX, true, AM_OBJ_TYPE_MAP),
AM_VALUE_OBJ_ID,
abort_cb).obj_id;
AMfree(AMmapPutStr(doc1, card2, "title", "Rewrite everything in Haskell"));
AMfree(AMmapPutBool(doc1, card2, "done", false));
AMfree(AMcommit(doc1, "Add card", NULL));
AMdoc* doc2 = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, abort_cb).doc;
AMfree(AMmerge(doc2, doc1));
AMbyteSpan const binary = AMpush(&stack, AMsave(doc1), AM_VALUE_BYTES, abort_cb).bytes;
doc2 = AMpush(&stack, AMload(binary.src, binary.count), AM_VALUE_DOC, abort_cb).doc;
AMfree(AMmapPutBool(doc1, card1, "done", true));
AMfree(AMcommit(doc1, "Mark card as done", NULL));
AMfree(AMlistDelete(doc2, cards, 0));
AMfree(AMcommit(doc2, "Delete card", NULL));
AMfree(AMmerge(doc1, doc2));
AMchanges changes = AMpush(&stack, AMgetChanges(doc1, NULL), AM_VALUE_CHANGES, abort_cb).changes;
AMchange const* change = NULL;
while ((change = AMchangesNext(&changes, 1)) != NULL) {
AMbyteSpan const change_hash = AMchangeHash(change);
AMchangeHashes const heads = AMpush(&stack,
AMchangeHashesInit(&change_hash, 1),
AM_VALUE_CHANGE_HASHES,
abort_cb).change_hashes;
printf("%s %ld\n", AMchangeMessage(change), AMobjSize(doc1, cards, &heads));
}
AMfreeStack(&stack);
}
static char const* discriminant_suffix(AMvalueVariant const);
/**
* \brief Prints an error message to `stderr`, deallocates all results in the
* given stack and exits.
*
* \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
* \param[in] discriminant An `AMvalueVariant` enum tag.
* \pre \p stack` != NULL`.
* \post `*stack == NULL`.
*/
static void abort_cb(AMresultStack** stack, uint8_t discriminant) {
static char buffer[512] = {0};
char const* suffix = NULL;
if (!stack) {
suffix = "Stack*";
}
else if (!*stack) {
suffix = "Stack";
}
else if (!(*stack)->result) {
suffix = "";
}
if (suffix) {
fprintf(stderr, "Null `AMresult%s*`.", suffix);
AMfreeStack(stack);
exit(EXIT_FAILURE);
return;
}
AMstatus const status = AMresultStatus((*stack)->result);
switch (status) {
case AM_STATUS_ERROR: strcpy(buffer, "Error"); break;
case AM_STATUS_INVALID_RESULT: strcpy(buffer, "Invalid result"); break;
case AM_STATUS_OK: break;
default: sprintf(buffer, "Unknown `AMstatus` tag %d", status);
}
if (buffer[0]) {
fprintf(stderr, "%s; %s.", buffer, AMerrorMessage((*stack)->result));
AMfreeStack(stack);
exit(EXIT_FAILURE);
return;
}
AMvalue const value = AMresultValue((*stack)->result);
fprintf(stderr, "Unexpected tag `AM_VALUE_%s` (%d); expected `AM_VALUE_%s`.",
discriminant_suffix(value.tag),
value.tag,
discriminant_suffix(discriminant));
AMfreeStack(stack);
exit(EXIT_FAILURE);
}
/**
* \brief Gets the suffix for a discriminant's corresponding string
* representation.
*
* \param[in] discriminant An `AMvalueVariant` enum tag.
* \return A UTF-8 string.
*/
static char const* discriminant_suffix(AMvalueVariant const discriminant) {
char const* suffix = NULL;
switch (discriminant) {
case AM_VALUE_ACTOR_ID: suffix = "ACTOR_ID"; break;
case AM_VALUE_BOOLEAN: suffix = "BOOLEAN"; break;
case AM_VALUE_BYTES: suffix = "BYTES"; break;
case AM_VALUE_CHANGE_HASHES: suffix = "CHANGE_HASHES"; break;
case AM_VALUE_CHANGES: suffix = "CHANGES"; break;
case AM_VALUE_COUNTER: suffix = "COUNTER"; break;
case AM_VALUE_DOC: suffix = "DOC"; break;
case AM_VALUE_F64: suffix = "F64"; break;
case AM_VALUE_INT: suffix = "INT"; break;
case AM_VALUE_LIST_ITEMS: suffix = "LIST_ITEMS"; break;
case AM_VALUE_MAP_ITEMS: suffix = "MAP_ITEMS"; break;
case AM_VALUE_NULL: suffix = "NULL"; break;
case AM_VALUE_OBJ_ID: suffix = "OBJ_ID"; break;
case AM_VALUE_OBJ_ITEMS: suffix = "OBJ_ITEMS"; break;
case AM_VALUE_STR: suffix = "STR"; break;
case AM_VALUE_STRS: suffix = "STRINGS"; break;
case AM_VALUE_SYNC_MESSAGE: suffix = "SYNC_MESSAGE"; break;
case AM_VALUE_SYNC_STATE: suffix = "SYNC_STATE"; break;
case AM_VALUE_TIMESTAMP: suffix = "TIMESTAMP"; break;
case AM_VALUE_UINT: suffix = "UINT"; break;
case AM_VALUE_VOID: suffix = "VOID"; break;
default: suffix = "...";
}
return suffix;
}

View file

@ -1,250 +0,0 @@
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
find_program (
CARGO_CMD
"cargo"
PATHS "$ENV{CARGO_HOME}/bin"
DOC "The Cargo command"
)
if(NOT CARGO_CMD)
message(FATAL_ERROR "Cargo (Rust package manager) not found! Install it and/or set the CARGO_HOME environment variable.")
endif()
string(TOLOWER "${CMAKE_BUILD_TYPE}" BUILD_TYPE_LOWER)
if(BUILD_TYPE_LOWER STREQUAL debug)
set(CARGO_BUILD_TYPE "debug")
set(CARGO_FLAG "")
else()
set(CARGO_BUILD_TYPE "release")
set(CARGO_FLAG "--release")
endif()
set(CARGO_FEATURES "")
set(CARGO_CURRENT_BINARY_DIR "${CARGO_TARGET_DIR}/${CARGO_BUILD_TYPE}")
set(
CARGO_OUTPUT
${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}
${CARGO_CURRENT_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}
)
if(WIN32)
# \note The basename of an import library output by Cargo is the filename
# of its corresponding shared library.
list(APPEND CARGO_OUTPUT ${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}${CMAKE_STATIC_LIBRARY_SUFFIX})
endif()
add_custom_command(
OUTPUT
${CARGO_OUTPUT}
COMMAND
# \note cbindgen won't regenerate its output header file after it's
# been removed but it will after its configuration file has been
# updated.
${CMAKE_COMMAND} -DCONDITION=NOT_EXISTS -P ${CMAKE_SOURCE_DIR}/cmake/file_touch.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h ${CMAKE_SOURCE_DIR}/cbindgen.toml
COMMAND
${CMAKE_COMMAND} -E env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} CBINDGEN_TARGET_DIR=${CBINDGEN_TARGET_DIR} ${CARGO_CMD} build ${CARGO_FLAG} ${CARGO_FEATURES}
MAIN_DEPENDENCY
lib.rs
DEPENDS
actor_id.rs
byte_span.rs
change_hashes.rs
change.rs
changes.rs
doc.rs
doc/list.rs
doc/list/item.rs
doc/list/items.rs
doc/map.rs
doc/map/item.rs
doc/map/items.rs
doc/utils.rs
obj.rs
obj/item.rs
obj/items.rs
result.rs
result_stack.rs
strs.rs
sync.rs
sync/have.rs
sync/haves.rs
sync/message.rs
sync/state.rs
${CMAKE_SOURCE_DIR}/build.rs
${CMAKE_SOURCE_DIR}/Cargo.toml
${CMAKE_SOURCE_DIR}/cbindgen.toml
WORKING_DIRECTORY
${CMAKE_SOURCE_DIR}
COMMENT
"Producing the library artifacts with Cargo..."
VERBATIM
)
add_custom_target(
${LIBRARY_NAME}_artifacts ALL
DEPENDS ${CARGO_OUTPUT}
)
# \note cbindgen's naming behavior isn't fully configurable and it ignores
# `const fn` calls (https://github.com/eqrion/cbindgen/issues/252).
add_custom_command(
TARGET ${LIBRARY_NAME}_artifacts
POST_BUILD
COMMAND
# Compensate for cbindgen's variant struct naming.
${CMAKE_COMMAND} -DMATCH_REGEX=AM\([^_]+_[^_]+\)_Body -DREPLACE_EXPR=AM\\1 -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
COMMAND
# Compensate for cbindgen's union tag enum type naming.
${CMAKE_COMMAND} -DMATCH_REGEX=AM\([^_]+\)_Tag -DREPLACE_EXPR=AM\\1Variant -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
COMMAND
# Compensate for cbindgen's translation of consecutive uppercase letters to "ScreamingSnakeCase".
${CMAKE_COMMAND} -DMATCH_REGEX=A_M\([^_]+\)_ -DREPLACE_EXPR=AM_\\1_ -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
COMMAND
# Compensate for cbindgen ignoring `std:mem::size_of<usize>()` calls.
${CMAKE_COMMAND} -DMATCH_REGEX=USIZE_ -DREPLACE_EXPR=\+${CMAKE_SIZEOF_VOID_P} -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h
WORKING_DIRECTORY
${CMAKE_SOURCE_DIR}
COMMENT
"Compensating for cbindgen deficits..."
VERBATIM
)
if(BUILD_SHARED_LIBS)
if(WIN32)
set(LIBRARY_DESTINATION "${CMAKE_INSTALL_BINDIR}")
else()
set(LIBRARY_DESTINATION "${CMAKE_INSTALL_LIBDIR}")
endif()
set(LIBRARY_DEFINE_SYMBOL "${SYMBOL_PREFIX}_EXPORTS")
# \note The basename of an import library output by Cargo is the filename
# of its corresponding shared library.
set(LIBRARY_IMPLIB "${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}${CMAKE_STATIC_LIBRARY_SUFFIX}")
set(LIBRARY_LOCATION "${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}")
set(LIBRARY_NO_SONAME "${WIN32}")
set(LIBRARY_SONAME "${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_SHARED_LIBRARY_SUFFIX}")
set(LIBRARY_TYPE "SHARED")
else()
set(LIBRARY_DEFINE_SYMBOL "")
set(LIBRARY_DESTINATION "${CMAKE_INSTALL_LIBDIR}")
set(LIBRARY_IMPLIB "")
set(LIBRARY_LOCATION "${CARGO_CURRENT_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}")
set(LIBRARY_NO_SONAME "TRUE")
set(LIBRARY_SONAME "")
set(LIBRARY_TYPE "STATIC")
endif()
add_library(${LIBRARY_NAME} ${LIBRARY_TYPE} IMPORTED GLOBAL)
set_target_properties(
${LIBRARY_NAME}
PROPERTIES
# \note Cargo writes a debug build into a nested directory instead of
# decorating its name.
DEBUG_POSTFIX ""
DEFINE_SYMBOL "${LIBRARY_DEFINE_SYMBOL}"
IMPORTED_IMPLIB "${LIBRARY_IMPLIB}"
IMPORTED_LOCATION "${LIBRARY_LOCATION}"
IMPORTED_NO_SONAME "${LIBRARY_NO_SONAME}"
IMPORTED_SONAME "${LIBRARY_SONAME}"
LINKER_LANGUAGE C
PUBLIC_HEADER "${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h"
SOVERSION "${PROJECT_VERSION_MAJOR}"
VERSION "${PROJECT_VERSION}"
# \note Cargo exports all of the symbols automatically.
WINDOWS_EXPORT_ALL_SYMBOLS "TRUE"
)
target_compile_definitions(${LIBRARY_NAME} INTERFACE $<TARGET_PROPERTY:${LIBRARY_NAME},DEFINE_SYMBOL>)
target_include_directories(
${LIBRARY_NAME}
INTERFACE
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}>"
)
set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
find_package(Threads REQUIRED)
set(LIBRARY_DEPENDENCIES Threads::Threads ${CMAKE_DL_LIBS})
if(WIN32)
list(APPEND LIBRARY_DEPENDENCIES Bcrypt userenv ws2_32)
else()
list(APPEND LIBRARY_DEPENDENCIES m)
endif()
target_link_libraries(${LIBRARY_NAME} INTERFACE ${LIBRARY_DEPENDENCIES})
install(
FILES $<TARGET_PROPERTY:${LIBRARY_NAME},IMPORTED_IMPLIB>
TYPE LIB
# \note The basename of an import library output by Cargo is the filename
# of its corresponding shared library.
RENAME "${CMAKE_STATIC_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_STATIC_LIBRARY_SUFFIX}"
OPTIONAL
)
set(LIBRARY_FILE_NAME "${CMAKE_${LIBRARY_TYPE}_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_${LIBRARY_TYPE}_LIBRARY_SUFFIX}")
install(
FILES $<TARGET_PROPERTY:${LIBRARY_NAME},IMPORTED_LOCATION>
RENAME "${LIBRARY_FILE_NAME}"
DESTINATION ${LIBRARY_DESTINATION}
)
install(
FILES $<TARGET_PROPERTY:${LIBRARY_NAME},PUBLIC_HEADER>
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}
)
find_package(Doxygen OPTIONAL_COMPONENTS dot)
if(DOXYGEN_FOUND)
set(DOXYGEN_ALIASES "installed_headerfile=\\headerfile ${LIBRARY_NAME}.h <${PROJECT_NAME}/${LIBRARY_NAME}.h>")
set(DOXYGEN_GENERATE_LATEX YES)
set(DOXYGEN_PDF_HYPERLINKS YES)
set(DOXYGEN_PROJECT_LOGO "${CMAKE_SOURCE_DIR}/img/brandmark.png")
set(DOXYGEN_SORT_BRIEF_DOCS YES)
set(DOXYGEN_USE_MDFILE_AS_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md")
doxygen_add_docs(
${LIBRARY_NAME}_docs
"${CBINDGEN_TARGET_DIR}/${LIBRARY_NAME}.h"
"${CMAKE_SOURCE_DIR}/README.md"
USE_STAMP_FILE
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Producing documentation with Doxygen..."
)
# \note A Doxygen input file isn't a file-level dependency so the Doxygen
# command must instead depend upon a target that outputs the file or
# it will just output an error message when it can't be found.
add_dependencies(${LIBRARY_NAME}_docs ${LIBRARY_NAME}_artifacts)
endif()

View file

@ -1,166 +0,0 @@
use automerge as am;
use std::cell::RefCell;
use std::cmp::Ordering;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::str::FromStr;
use crate::byte_span::AMbyteSpan;
use crate::result::{to_result, AMresult};
/// \struct AMactorId
/// \installed_headerfile
/// \brief An actor's unique identifier.
#[derive(Eq, PartialEq)]
pub struct AMactorId {
body: *const am::ActorId,
c_str: RefCell<Option<CString>>,
}
impl AMactorId {
pub fn new(actor_id: &am::ActorId) -> Self {
Self {
body: actor_id,
c_str: Default::default(),
}
}
pub fn as_c_str(&self) -> *const c_char {
let mut c_str = self.c_str.borrow_mut();
match c_str.as_mut() {
None => {
let hex_str = unsafe { (*self.body).to_hex_string() };
c_str.insert(CString::new(hex_str).unwrap()).as_ptr()
}
Some(hex_str) => hex_str.as_ptr(),
}
}
}
impl AsRef<am::ActorId> for AMactorId {
fn as_ref(&self) -> &am::ActorId {
unsafe { &*self.body }
}
}
/// \memberof AMactorId
/// \brief Gets the value of an actor identifier as a sequence of bytes.
///
/// \param[in] actor_id A pointer to an `AMactorId` struct.
/// \pre \p actor_id `!= NULL`.
/// \return An `AMbyteSpan` struct.
/// \internal
///
/// # Safety
/// actor_id must be a valid pointer to an AMactorId
#[no_mangle]
pub unsafe extern "C" fn AMactorIdBytes(actor_id: *const AMactorId) -> AMbyteSpan {
match actor_id.as_ref() {
Some(actor_id) => actor_id.as_ref().into(),
None => AMbyteSpan::default(),
}
}
/// \memberof AMactorId
/// \brief Compares two actor identifiers.
///
/// \param[in] actor_id1 A pointer to an `AMactorId` struct.
/// \param[in] actor_id2 A pointer to an `AMactorId` struct.
/// \return `-1` if \p actor_id1 `<` \p actor_id2, `0` if
/// \p actor_id1 `==` \p actor_id2 and `1` if
/// \p actor_id1 `>` \p actor_id2.
/// \pre \p actor_id1 `!= NULL`.
/// \pre \p actor_id2 `!= NULL`.
/// \internal
///
/// #Safety
/// actor_id1 must be a valid pointer to an AMactorId
/// actor_id2 must be a valid pointer to an AMactorId
#[no_mangle]
pub unsafe extern "C" fn AMactorIdCmp(
actor_id1: *const AMactorId,
actor_id2: *const AMactorId,
) -> isize {
match (actor_id1.as_ref(), actor_id2.as_ref()) {
(Some(actor_id1), Some(actor_id2)) => match actor_id1.as_ref().cmp(actor_id2.as_ref()) {
Ordering::Less => -1,
Ordering::Equal => 0,
Ordering::Greater => 1,
},
(None, Some(_)) => -1,
(Some(_), None) => 1,
(None, None) => 0,
}
}
/// \memberof AMactorId
/// \brief Allocates a new actor identifier and initializes it with a random
/// UUID.
///
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMactorId` struct.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
#[no_mangle]
pub unsafe extern "C" fn AMactorIdInit() -> *mut AMresult {
to_result(Ok::<am::ActorId, am::AutomergeError>(am::ActorId::random()))
}
/// \memberof AMactorId
/// \brief Allocates a new actor identifier and initializes it from a sequence
/// of bytes.
///
/// \param[in] src A pointer to a contiguous sequence of bytes.
/// \param[in] count The number of bytes to copy from \p src.
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMactorId` struct.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// src must be a byte array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMactorIdInitBytes(src: *const u8, count: usize) -> *mut AMresult {
let slice = std::slice::from_raw_parts(src, count);
to_result(Ok::<am::ActorId, am::InvalidActorId>(am::ActorId::from(
slice,
)))
}
/// \memberof AMactorId
/// \brief Allocates a new actor identifier and initializes it from a
/// hexadecimal string.
///
/// \param[in] hex_str A UTF-8 string.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMactorId` struct.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// hex_str must be a null-terminated array of `c_char`
#[no_mangle]
pub unsafe extern "C" fn AMactorIdInitStr(hex_str: *const c_char) -> *mut AMresult {
to_result(am::ActorId::from_str(
CStr::from_ptr(hex_str).to_str().unwrap(),
))
}
/// \memberof AMactorId
/// \brief Gets the value of an actor identifier as a hexadecimal string.
///
/// \param[in] actor_id A pointer to an `AMactorId` struct.
/// \pre \p actor_id `!= NULL`.
/// \return A UTF-8 string.
/// \internal
///
/// # Safety
/// actor_id must be a valid pointer to an AMactorId
#[no_mangle]
pub unsafe extern "C" fn AMactorIdStr(actor_id: *const AMactorId) -> *const c_char {
match actor_id.as_ref() {
Some(actor_id) => actor_id.as_c_str(),
None => std::ptr::null::<c_char>(),
}
}

View file

@ -1,64 +0,0 @@
use automerge as am;
/// \struct AMbyteSpan
/// \installed_headerfile
/// \brief A view onto a contiguous sequence of bytes.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMbyteSpan {
/// A pointer to an array of bytes.
/// \attention <b>NEVER CALL `free()` ON \p src!</b>
/// \warning \p src is only valid until the `AMfree()` function is called
/// on the `AMresult` struct that stores the array of bytes to
/// which it points.
pub src: *const u8,
/// The number of bytes in the array.
pub count: usize,
}
impl Default for AMbyteSpan {
fn default() -> Self {
Self {
src: std::ptr::null(),
count: 0,
}
}
}
impl From<&am::ActorId> for AMbyteSpan {
fn from(actor: &am::ActorId) -> Self {
let slice = actor.to_bytes();
Self {
src: slice.as_ptr(),
count: slice.len(),
}
}
}
impl From<&mut am::ActorId> for AMbyteSpan {
fn from(actor: &mut am::ActorId) -> Self {
let slice = actor.to_bytes();
Self {
src: slice.as_ptr(),
count: slice.len(),
}
}
}
impl From<&am::ChangeHash> for AMbyteSpan {
fn from(change_hash: &am::ChangeHash) -> Self {
Self {
src: change_hash.0.as_ptr(),
count: change_hash.0.len(),
}
}
}
impl From<&[u8]> for AMbyteSpan {
fn from(slice: &[u8]) -> Self {
Self {
src: slice.as_ptr(),
count: slice.len(),
}
}
}

View file

@ -1,399 +0,0 @@
use automerge as am;
use std::cmp::Ordering;
use std::ffi::c_void;
use std::mem::size_of;
use crate::byte_span::AMbyteSpan;
use crate::result::{to_result, AMresult};
#[repr(C)]
struct Detail {
len: usize,
offset: isize,
ptr: *const c_void,
}
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
/// propagate the name of a constant initialized from it so if the
/// constant's name is a symbolic representation of the value it can be
/// converted into a number by post-processing the header it generated.
pub const USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
impl Detail {
fn new(change_hashes: &[am::ChangeHash], offset: isize) -> Self {
Self {
len: change_hashes.len(),
offset,
ptr: change_hashes.as_ptr() as *const c_void,
}
}
pub fn advance(&mut self, n: isize) {
if n == 0 {
return;
}
let len = self.len as isize;
self.offset = if self.offset < 0 {
// It's reversed.
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
if unclipped >= 0 {
// Clip it to the forward stop.
len
} else {
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
}
} else {
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
if unclipped < 0 {
// Clip it to the reverse stop.
-(len + 1)
} else {
std::cmp::max(0, std::cmp::min(unclipped, len))
}
}
}
pub fn get_index(&self) -> usize {
(self.offset
+ if self.offset < 0 {
self.len as isize
} else {
0
}) as usize
}
pub fn next(&mut self, n: isize) -> Option<&am::ChangeHash> {
if self.is_stopped() {
return None;
}
let slice: &[am::ChangeHash] =
unsafe { std::slice::from_raw_parts(self.ptr as *const am::ChangeHash, self.len) };
let value = &slice[self.get_index()];
self.advance(n);
Some(value)
}
pub fn is_stopped(&self) -> bool {
let len = self.len as isize;
self.offset < -len || self.offset == len
}
pub fn prev(&mut self, n: isize) -> Option<&am::ChangeHash> {
self.advance(-n);
if self.is_stopped() {
return None;
}
let slice: &[am::ChangeHash] =
unsafe { std::slice::from_raw_parts(self.ptr as *const am::ChangeHash, self.len) };
Some(&slice[self.get_index()])
}
pub fn reversed(&self) -> Self {
Self {
len: self.len,
offset: -(self.offset + 1),
ptr: self.ptr,
}
}
pub fn rewound(&self) -> Self {
Self {
len: self.len,
offset: if self.offset < 0 { -1 } else { 0 },
ptr: self.ptr,
}
}
}
impl From<Detail> for [u8; USIZE_USIZE_USIZE_] {
fn from(detail: Detail) -> Self {
unsafe {
std::slice::from_raw_parts((&detail as *const Detail) as *const u8, USIZE_USIZE_USIZE_)
.try_into()
.unwrap()
}
}
}
/// \struct AMchangeHashes
/// \installed_headerfile
/// \brief A random-access iterator over a sequence of change hashes.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMchangeHashes {
/// An implementation detail that is intentionally opaque.
/// \warning Modifying \p detail will cause undefined behavior.
/// \note The actual size of \p detail will vary by platform, this is just
/// the one for the platform this documentation was built on.
detail: [u8; USIZE_USIZE_USIZE_],
}
impl AMchangeHashes {
pub fn new(change_hashes: &[am::ChangeHash]) -> Self {
Self {
detail: Detail::new(change_hashes, 0).into(),
}
}
pub fn advance(&mut self, n: isize) {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.advance(n);
}
pub fn len(&self) -> usize {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
detail.len
}
pub fn next(&mut self, n: isize) -> Option<&am::ChangeHash> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<&am::ChangeHash> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.prev(n)
}
pub fn reversed(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.reversed().into(),
}
}
pub fn rewound(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.rewound().into(),
}
}
}
impl AsRef<[am::ChangeHash]> for AMchangeHashes {
fn as_ref(&self) -> &[am::ChangeHash] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const am::ChangeHash, detail.len) }
}
}
impl Default for AMchangeHashes {
fn default() -> Self {
Self {
detail: [0; USIZE_USIZE_USIZE_],
}
}
}
/// \memberof AMchangeHashes
/// \brief Advances an iterator over a sequence of change hashes by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in,out] change_hashes A pointer to an `AMchangeHashes` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \pre \p change_hashes `!= NULL`.
/// \internal
///
/// #Safety
/// change_hashes must be a valid pointer to an AMchangeHashes
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesAdvance(change_hashes: *mut AMchangeHashes, n: isize) {
if let Some(change_hashes) = change_hashes.as_mut() {
change_hashes.advance(n);
};
}
/// \memberof AMchangeHashes
/// \brief Compares the sequences of change hashes underlying a pair of
/// iterators.
///
/// \param[in] change_hashes1 A pointer to an `AMchangeHashes` struct.
/// \param[in] change_hashes2 A pointer to an `AMchangeHashes` struct.
/// \return `-1` if \p change_hashes1 `<` \p change_hashes2, `0` if
/// \p change_hashes1 `==` \p change_hashes2 and `1` if
/// \p change_hashes1 `>` \p change_hashes2.
/// \pre \p change_hashes1 `!= NULL`.
/// \pre \p change_hashes2 `!= NULL`.
/// \internal
///
/// #Safety
/// change_hashes1 must be a valid pointer to an AMchangeHashes
/// change_hashes2 must be a valid pointer to an AMchangeHashes
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesCmp(
change_hashes1: *const AMchangeHashes,
change_hashes2: *const AMchangeHashes,
) -> isize {
match (change_hashes1.as_ref(), change_hashes2.as_ref()) {
(Some(change_hashes1), Some(change_hashes2)) => {
match change_hashes1.as_ref().cmp(change_hashes2.as_ref()) {
Ordering::Less => -1,
Ordering::Equal => 0,
Ordering::Greater => 1,
}
}
(None, Some(_)) => -1,
(Some(_), None) => 1,
(None, None) => 0,
}
}
/// \memberof AMchangeHashes
/// \brief Allocates an iterator over a sequence of change hashes and
/// initializes it from a sequence of byte spans.
///
/// \param[in] src A pointer to an array of `AMbyteSpan` structs.
/// \param[in] count The number of `AMbyteSpan` structs to copy from \p src.
/// \return A pointer to an `AMresult` struct containing an `AMchangeHashes`
/// struct.
/// \pre \p src `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`) / sizeof(AMbyteSpan)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// src must be an AMbyteSpan array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesInit(src: *const AMbyteSpan, count: usize) -> *mut AMresult {
let mut change_hashes = Vec::<am::ChangeHash>::new();
for n in 0..count {
let byte_span = &*src.add(n);
let slice = std::slice::from_raw_parts(byte_span.src, byte_span.count);
match slice.try_into() {
Ok(change_hash) => {
change_hashes.push(change_hash);
}
Err(e) => {
return to_result(Err(e));
}
}
}
to_result(Ok::<Vec<am::ChangeHash>, am::InvalidChangeHashSlice>(
change_hashes,
))
}
/// \memberof AMchangeHashes
/// \brief Gets the change hash at the current position of an iterator over a
/// sequence of change hashes and then advances it by at most \p |n|
/// positions where the sign of \p n is relative to the iterator's
/// direction.
///
/// \param[in,out] change_hashes A pointer to an `AMchangeHashes` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return An `AMbyteSpan` struct with `.src == NULL` when \p change_hashes
/// was previously advanced past its forward/reverse limit.
/// \pre \p change_hashes `!= NULL`.
/// \internal
///
/// #Safety
/// change_hashes must be a valid pointer to an AMchangeHashes
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesNext(
change_hashes: *mut AMchangeHashes,
n: isize,
) -> AMbyteSpan {
if let Some(change_hashes) = change_hashes.as_mut() {
if let Some(change_hash) = change_hashes.next(n) {
return change_hash.into();
}
}
AMbyteSpan::default()
}
/// \memberof AMchangeHashes
/// \brief Advances an iterator over a sequence of change hashes by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction and then gets the change hash at its new
/// position.
///
/// \param[in,out] change_hashes A pointer to an `AMchangeHashes` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return An `AMbyteSpan` struct with `.src == NULL` when \p change_hashes is
/// presently advanced past its forward/reverse limit.
/// \pre \p change_hashes `!= NULL`.
/// \internal
///
/// #Safety
/// change_hashes must be a valid pointer to an AMchangeHashes
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesPrev(
change_hashes: *mut AMchangeHashes,
n: isize,
) -> AMbyteSpan {
if let Some(change_hashes) = change_hashes.as_mut() {
if let Some(change_hash) = change_hashes.prev(n) {
return change_hash.into();
}
}
AMbyteSpan::default()
}
/// \memberof AMchangeHashes
/// \brief Gets the size of the sequence of change hashes underlying an
/// iterator.
///
/// \param[in] change_hashes A pointer to an `AMchangeHashes` struct.
/// \return The count of values in \p change_hashes.
/// \pre \p change_hashes `!= NULL`.
/// \internal
///
/// #Safety
/// change_hashes must be a valid pointer to an AMchangeHashes
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesSize(change_hashes: *const AMchangeHashes) -> usize {
if let Some(change_hashes) = change_hashes.as_ref() {
change_hashes.len()
} else {
0
}
}
/// \memberof AMchangeHashes
/// \brief Creates an iterator over the same sequence of change hashes as the
/// given one but with the opposite position and direction.
///
/// \param[in] change_hashes A pointer to an `AMchangeHashes` struct.
/// \return An `AMchangeHashes` struct
/// \pre \p change_hashes `!= NULL`.
/// \internal
///
/// #Safety
/// change_hashes must be a valid pointer to an AMchangeHashes
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesReversed(
change_hashes: *const AMchangeHashes,
) -> AMchangeHashes {
if let Some(change_hashes) = change_hashes.as_ref() {
change_hashes.reversed()
} else {
AMchangeHashes::default()
}
}
/// \memberof AMchangeHashes
/// \brief Creates an iterator at the starting position over the same sequence
/// of change hashes as the given one.
///
/// \param[in] change_hashes A pointer to an `AMchangeHashes` struct.
/// \return An `AMchangeHashes` struct
/// \pre \p change_hashes `!= NULL`.
/// \internal
///
/// #Safety
/// change_hashes must be a valid pointer to an AMchangeHashes
#[no_mangle]
pub unsafe extern "C" fn AMchangeHashesRewound(
change_hashes: *const AMchangeHashes,
) -> AMchangeHashes {
if let Some(change_hashes) = change_hashes.as_ref() {
change_hashes.rewound()
} else {
AMchangeHashes::default()
}
}

View file

@ -1,398 +0,0 @@
use automerge as am;
use std::collections::BTreeMap;
use std::ffi::c_void;
use std::mem::size_of;
use crate::byte_span::AMbyteSpan;
use crate::change::AMchange;
use crate::result::{to_result, AMresult};
#[repr(C)]
struct Detail {
len: usize,
offset: isize,
ptr: *const c_void,
storage: *mut c_void,
}
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
/// propagate the name of a constant initialized from it so if the
/// constant's name is a symbolic representation of the value it can be
/// converted into a number by post-processing the header it generated.
pub const USIZE_USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
impl Detail {
fn new(changes: &[am::Change], offset: isize, storage: &mut BTreeMap<usize, AMchange>) -> Self {
let storage: *mut BTreeMap<usize, AMchange> = storage;
Self {
len: changes.len(),
offset,
ptr: changes.as_ptr() as *const c_void,
storage: storage as *mut c_void,
}
}
pub fn advance(&mut self, n: isize) {
if n == 0 {
return;
}
let len = self.len as isize;
self.offset = if self.offset < 0 {
// It's reversed.
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
if unclipped >= 0 {
// Clip it to the forward stop.
len
} else {
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
}
} else {
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
if unclipped < 0 {
// Clip it to the reverse stop.
-(len + 1)
} else {
std::cmp::max(0, std::cmp::min(unclipped, len))
}
}
}
pub fn get_index(&self) -> usize {
(self.offset
+ if self.offset < 0 {
self.len as isize
} else {
0
}) as usize
}
pub fn next(&mut self, n: isize) -> Option<*const AMchange> {
if self.is_stopped() {
return None;
}
let slice: &mut [am::Change] =
unsafe { std::slice::from_raw_parts_mut(self.ptr as *mut am::Change, self.len) };
let storage = unsafe { &mut *(self.storage as *mut BTreeMap<usize, AMchange>) };
let index = self.get_index();
let value = match storage.get_mut(&index) {
Some(value) => value,
None => {
storage.insert(index, AMchange::new(&mut slice[index]));
storage.get_mut(&index).unwrap()
}
};
self.advance(n);
Some(value)
}
pub fn is_stopped(&self) -> bool {
let len = self.len as isize;
self.offset < -len || self.offset == len
}
pub fn prev(&mut self, n: isize) -> Option<*const AMchange> {
self.advance(-n);
if self.is_stopped() {
return None;
}
let slice: &mut [am::Change] =
unsafe { std::slice::from_raw_parts_mut(self.ptr as *mut am::Change, self.len) };
let storage = unsafe { &mut *(self.storage as *mut BTreeMap<usize, AMchange>) };
let index = self.get_index();
Some(match storage.get_mut(&index) {
Some(value) => value,
None => {
storage.insert(index, AMchange::new(&mut slice[index]));
storage.get_mut(&index).unwrap()
}
})
}
pub fn reversed(&self) -> Self {
Self {
len: self.len,
offset: -(self.offset + 1),
ptr: self.ptr,
storage: self.storage,
}
}
pub fn rewound(&self) -> Self {
Self {
len: self.len,
offset: if self.offset < 0 { -1 } else { 0 },
ptr: self.ptr,
storage: self.storage,
}
}
}
impl From<Detail> for [u8; USIZE_USIZE_USIZE_USIZE_] {
fn from(detail: Detail) -> Self {
unsafe {
std::slice::from_raw_parts(
(&detail as *const Detail) as *const u8,
USIZE_USIZE_USIZE_USIZE_,
)
.try_into()
.unwrap()
}
}
}
/// \struct AMchanges
/// \installed_headerfile
/// \brief A random-access iterator over a sequence of changes.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMchanges {
/// An implementation detail that is intentionally opaque.
/// \warning Modifying \p detail will cause undefined behavior.
/// \note The actual size of \p detail will vary by platform, this is just
/// the one for the platform this documentation was built on.
detail: [u8; USIZE_USIZE_USIZE_USIZE_],
}
impl AMchanges {
pub fn new(changes: &[am::Change], storage: &mut BTreeMap<usize, AMchange>) -> Self {
Self {
detail: Detail::new(changes, 0, &mut *storage).into(),
}
}
pub fn advance(&mut self, n: isize) {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.advance(n);
}
pub fn len(&self) -> usize {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
detail.len
}
pub fn next(&mut self, n: isize) -> Option<*const AMchange> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<*const AMchange> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.prev(n)
}
pub fn reversed(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.reversed().into(),
}
}
pub fn rewound(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.rewound().into(),
}
}
}
impl AsRef<[am::Change]> for AMchanges {
fn as_ref(&self) -> &[am::Change] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const am::Change, detail.len) }
}
}
impl Default for AMchanges {
fn default() -> Self {
Self {
detail: [0; USIZE_USIZE_USIZE_USIZE_],
}
}
}
/// \memberof AMchanges
/// \brief Advances an iterator over a sequence of changes by at most \p |n|
/// positions where the sign of \p n is relative to the iterator's
/// direction.
///
/// \param[in,out] changes A pointer to an `AMchanges` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \pre \p changes `!= NULL`.
/// \internal
///
/// #Safety
/// changes must be a valid pointer to an AMchanges
#[no_mangle]
pub unsafe extern "C" fn AMchangesAdvance(changes: *mut AMchanges, n: isize) {
if let Some(changes) = changes.as_mut() {
changes.advance(n);
};
}
/// \memberof AMchanges
/// \brief Tests the equality of two sequences of changes underlying a pair of
/// iterators.
///
/// \param[in] changes1 A pointer to an `AMchanges` struct.
/// \param[in] changes2 A pointer to an `AMchanges` struct.
/// \return `true` if \p changes1 `==` \p changes2 and `false` otherwise.
/// \pre \p changes1 `!= NULL`.
/// \pre \p changes2 `!= NULL`.
/// \internal
///
/// #Safety
/// changes1 must be a valid pointer to an AMchanges
/// changes2 must be a valid pointer to an AMchanges
#[no_mangle]
pub unsafe extern "C" fn AMchangesEqual(
changes1: *const AMchanges,
changes2: *const AMchanges,
) -> bool {
match (changes1.as_ref(), changes2.as_ref()) {
(Some(changes1), Some(changes2)) => changes1.as_ref() == changes2.as_ref(),
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \memberof AMchanges
/// \brief Allocates an iterator over a sequence of changes and initializes it
/// from a sequence of byte spans.
///
/// \param[in] src A pointer to an array of `AMbyteSpan` structs.
/// \param[in] count The number of `AMbyteSpan` structs to copy from \p src.
/// \return A pointer to an `AMresult` struct containing an `AMchanges` struct.
/// \pre \p src `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`) / sizeof(AMbyteSpan)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// src must be an AMbyteSpan array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMchangesInit(src: *const AMbyteSpan, count: usize) -> *mut AMresult {
let mut changes = Vec::<am::Change>::new();
for n in 0..count {
let byte_span = &*src.add(n);
let slice = std::slice::from_raw_parts(byte_span.src, byte_span.count);
match slice.try_into() {
Ok(change) => {
changes.push(change);
}
Err(e) => {
return to_result(Err::<Vec<am::Change>, am::LoadChangeError>(e));
}
}
}
to_result(Ok::<Vec<am::Change>, am::LoadChangeError>(changes))
}
/// \memberof AMchanges
/// \brief Gets the change at the current position of an iterator over a
/// sequence of changes and then advances it by at most \p |n| positions
/// where the sign of \p n is relative to the iterator's direction.
///
/// \param[in,out] changes A pointer to an `AMchanges` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMchange` struct that's `NULL` when \p changes was
/// previously advanced past its forward/reverse limit.
/// \pre \p changes `!= NULL`.
/// \internal
///
/// #Safety
/// changes must be a valid pointer to an AMchanges
#[no_mangle]
pub unsafe extern "C" fn AMchangesNext(changes: *mut AMchanges, n: isize) -> *const AMchange {
if let Some(changes) = changes.as_mut() {
if let Some(change) = changes.next(n) {
return change;
}
}
std::ptr::null()
}
/// \memberof AMchanges
/// \brief Advances an iterator over a sequence of changes by at most \p |n|
/// positions where the sign of \p n is relative to the iterator's
/// direction and then gets the change at its new position.
///
/// \param[in,out] changes A pointer to an `AMchanges` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMchange` struct that's `NULL` when \p changes is
/// presently advanced past its forward/reverse limit.
/// \pre \p changes `!= NULL`.
/// \internal
///
/// #Safety
/// changes must be a valid pointer to an AMchanges
#[no_mangle]
pub unsafe extern "C" fn AMchangesPrev(changes: *mut AMchanges, n: isize) -> *const AMchange {
if let Some(changes) = changes.as_mut() {
if let Some(change) = changes.prev(n) {
return change;
}
}
std::ptr::null()
}
/// \memberof AMchanges
/// \brief Gets the size of the sequence of changes underlying an iterator.
///
/// \param[in] changes A pointer to an `AMchanges` struct.
/// \return The count of values in \p changes.
/// \pre \p changes `!= NULL`.
/// \internal
///
/// #Safety
/// changes must be a valid pointer to an AMchanges
#[no_mangle]
pub unsafe extern "C" fn AMchangesSize(changes: *const AMchanges) -> usize {
if let Some(changes) = changes.as_ref() {
changes.len()
} else {
0
}
}
/// \memberof AMchanges
/// \brief Creates an iterator over the same sequence of changes as the given
/// one but with the opposite position and direction.
///
/// \param[in] changes A pointer to an `AMchanges` struct.
/// \return An `AMchanges` struct.
/// \pre \p changes `!= NULL`.
/// \internal
///
/// #Safety
/// changes must be a valid pointer to an AMchanges
#[no_mangle]
pub unsafe extern "C" fn AMchangesReversed(changes: *const AMchanges) -> AMchanges {
if let Some(changes) = changes.as_ref() {
changes.reversed()
} else {
AMchanges::default()
}
}
/// \memberof AMchanges
/// \brief Creates an iterator at the starting position over the same sequence
/// of changes as the given one.
///
/// \param[in] changes A pointer to an `AMchanges` struct.
/// \return An `AMchanges` struct
/// \pre \p changes `!= NULL`.
/// \internal
///
/// #Safety
/// changes must be a valid pointer to an AMchanges
#[no_mangle]
pub unsafe extern "C" fn AMchangesRewound(changes: *const AMchanges) -> AMchanges {
if let Some(changes) = changes.as_ref() {
changes.rewound()
} else {
AMchanges::default()
}
}

View file

@ -1,833 +0,0 @@
use automerge as am;
use automerge::transaction::{CommitOptions, Transactable};
use std::ops::{Deref, DerefMut};
use std::os::raw::c_char;
use crate::actor_id::AMactorId;
use crate::change_hashes::AMchangeHashes;
use crate::obj::AMobjId;
use crate::result::{to_result, AMresult, AMvalue};
use crate::sync::{to_sync_message, AMsyncMessage, AMsyncState};
pub mod list;
pub mod map;
pub mod utils;
use crate::changes::AMchanges;
use crate::doc::utils::to_str;
use crate::doc::utils::{to_actor_id, to_doc, to_doc_mut, to_obj_id};
macro_rules! to_changes {
($handle:expr) => {{
let handle = $handle.as_ref();
match handle {
Some(b) => b,
None => return AMresult::err("Invalid AMchanges pointer").into(),
}
}};
}
macro_rules! to_index {
($index:expr, $len:expr, $param_name:expr) => {{
if $index > $len && $index != usize::MAX {
return AMresult::err(&format!("Invalid {} {}", $param_name, $index)).into();
}
std::cmp::min($index, $len)
}};
}
macro_rules! to_sync_state_mut {
($handle:expr) => {{
let handle = $handle.as_mut();
match handle {
Some(b) => b,
None => return AMresult::err("Invalid AMsyncState pointer").into(),
}
}};
}
/// \struct AMdoc
/// \installed_headerfile
/// \brief A JSON-like CRDT.
#[derive(Clone)]
pub struct AMdoc(am::AutoCommit);
impl AMdoc {
pub fn new(auto_commit: am::AutoCommit) -> Self {
Self(auto_commit)
}
}
impl AsRef<am::AutoCommit> for AMdoc {
fn as_ref(&self) -> &am::AutoCommit {
&self.0
}
}
impl Deref for AMdoc {
type Target = am::AutoCommit;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for AMdoc {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
/// \memberof AMdoc
/// \brief Applies a sequence of changes to a document.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] changes A pointer to an `AMchanges` struct.
/// \pre \p doc `!= NULL`.
/// \pre \p changes `!= NULL`.
/// \return A pointer to an `AMresult` struct containing a void.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// changes must be a valid pointer to an AMchanges.
#[no_mangle]
pub unsafe extern "C" fn AMapplyChanges(
doc: *mut AMdoc,
changes: *const AMchanges,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let changes = to_changes!(changes);
to_result(doc.apply_changes(changes.as_ref().to_vec()))
}
/// \memberof AMdoc
/// \brief Allocates storage for a document and initializes it by duplicating
/// the given document.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMdoc` struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMclone(doc: *const AMdoc) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(doc.as_ref().clone())
}
/// \memberof AMdoc
/// \brief Allocates a new document and initializes it with defaults.
///
/// \param[in] actor_id A pointer to an `AMactorId` struct or `NULL` for a
/// random one.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMdoc` struct.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
///
/// # Safety
/// actor_id must be a valid pointer to an AMactorId or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMcreate(actor_id: *const AMactorId) -> *mut AMresult {
to_result(match actor_id.as_ref() {
Some(actor_id) => am::AutoCommit::new().with_actor(actor_id.as_ref().clone()),
None => am::AutoCommit::new(),
})
}
/// \memberof AMdoc
/// \brief Commits the current operations on a document with an optional
/// message and/or time override as seconds since the epoch.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] message A UTF-8 string or `NULL`.
/// \param[in] time A pointer to a `time_t` value or `NULL`.
/// \return A pointer to an `AMresult` struct containing an `AMchangeHashes`
/// with one element.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMcommit(
doc: *mut AMdoc,
message: *const c_char,
time: *const libc::time_t,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let mut options = CommitOptions::default();
if !message.is_null() {
options.set_message(to_str(message));
}
if let Some(time) = time.as_ref() {
options.set_time(*time);
}
to_result(doc.commit_with(options))
}
/// \memberof AMdoc
/// \brief Tests the equality of two documents after closing their respective
/// transactions.
///
/// \param[in,out] doc1 An `AMdoc` struct.
/// \param[in,out] doc2 An `AMdoc` struct.
/// \return `true` if \p doc1 `==` \p doc2 and `false` otherwise.
/// \pre \p doc1 `!= NULL`.
/// \pre \p doc2 `!= NULL`.
/// \internal
///
/// #Safety
/// doc1 must be a valid pointer to an AMdoc
/// doc2 must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMequal(doc1: *mut AMdoc, doc2: *mut AMdoc) -> bool {
match (doc1.as_mut(), doc2.as_mut()) {
(Some(doc1), Some(doc2)) => doc1.document().get_heads() == doc2.document().get_heads(),
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \memberof AMdoc
/// \brief Forks this document at the current or a historical point for use by
/// a different actor.
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for a historical
/// point or `NULL` for the current point.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMdoc` struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMfork(doc: *mut AMdoc, heads: *const AMchangeHashes) -> *mut AMresult {
let doc = to_doc_mut!(doc);
match heads.as_ref() {
None => to_result(doc.fork()),
Some(heads) => to_result(doc.fork_at(heads.as_ref())),
}
}
/// \memberof AMdoc
/// \brief Generates a synchronization message for a peer based upon the given
/// synchronization state.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in,out] sync_state A pointer to an `AMsyncState` struct.
/// \return A pointer to an `AMresult` struct containing either a pointer to an
/// `AMsyncMessage` struct or a void.
/// \pre \p doc `!= NULL`.
/// \pre \p sync_state `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// sync_state must be a valid pointer to an AMsyncState
#[no_mangle]
pub unsafe extern "C" fn AMgenerateSyncMessage(
doc: *mut AMdoc,
sync_state: *mut AMsyncState,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let sync_state = to_sync_state_mut!(sync_state);
to_result(doc.generate_sync_message(sync_state.as_mut()))
}
/// \memberof AMdoc
/// \brief Gets a document's actor identifier.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMactorId` struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMgetActorId(doc: *const AMdoc) -> *mut AMresult {
let doc = to_doc!(doc);
to_result(Ok::<am::ActorId, am::AutomergeError>(
doc.get_actor().clone(),
))
}
/// \memberof AMdoc
/// \brief Gets the change added to a document by its respective hash.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The number of bytes in \p src.
/// \return A pointer to an `AMresult` struct containing an `AMchanges` struct.
/// \pre \p doc `!= NULL`.
/// \pre \p src `!= NULL`.
/// \pre \p count `>= AM_CHANGE_HASH_SIZE`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// src must be a byte array of size `>= automerge::types::HASH_SIZE`
#[no_mangle]
pub unsafe extern "C" fn AMgetChangeByHash(
doc: *mut AMdoc,
src: *const u8,
count: usize,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let slice = std::slice::from_raw_parts(src, count);
match slice.try_into() {
Ok(change_hash) => to_result(doc.get_change_by_hash(&change_hash)),
Err(e) => AMresult::err(&e.to_string()).into(),
}
}
/// \memberof AMdoc
/// \brief Gets the changes added to a document by their respective hashes.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] have_deps A pointer to an `AMchangeHashes` struct or `NULL`.
/// \return A pointer to an `AMresult` struct containing an `AMchanges` struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMgetChanges(
doc: *mut AMdoc,
have_deps: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let empty_deps = Vec::<am::ChangeHash>::new();
let have_deps = match have_deps.as_ref() {
Some(have_deps) => have_deps.as_ref(),
None => &empty_deps,
};
to_result(doc.get_changes(have_deps))
}
/// \memberof AMdoc
/// \brief Gets the changes added to a second document that weren't added to
/// a first document.
///
/// \param[in,out] doc1 An `AMdoc` struct.
/// \param[in,out] doc2 An `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing an `AMchanges` struct.
/// \pre \p doc1 `!= NULL`.
/// \pre \p doc2 `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc1 must be a valid pointer to an AMdoc
/// doc2 must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMgetChangesAdded(doc1: *mut AMdoc, doc2: *mut AMdoc) -> *mut AMresult {
let doc1 = to_doc_mut!(doc1);
let doc2 = to_doc_mut!(doc2);
to_result(doc1.get_changes_added(doc2))
}
/// \memberof AMdoc
/// \brief Gets the current heads of a document.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing an `AMchangeHashes`
/// struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMgetHeads(doc: *mut AMdoc) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(Ok::<Vec<am::ChangeHash>, am::AutomergeError>(
doc.get_heads(),
))
}
/// \memberof AMdoc
/// \brief Gets the hashes of the changes in a document that aren't transitive
/// dependencies of the given hashes of changes.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] heads A pointer to an `AMchangeHashes` struct or `NULL`.
/// \return A pointer to an `AMresult` struct containing an `AMchangeHashes`
/// struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMgetMissingDeps(
doc: *mut AMdoc,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let empty_heads = Vec::<am::ChangeHash>::new();
let heads = match heads.as_ref() {
Some(heads) => heads.as_ref(),
None => &empty_heads,
};
to_result(doc.get_missing_deps(heads))
}
/// \memberof AMdoc
/// \brief Gets the last change made to a document.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing either an `AMchange`
/// struct or a void.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMgetLastLocalChange(doc: *mut AMdoc) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.get_last_local_change())
}
/// \memberof AMdoc
/// \brief Gets the current or historical keys of a map object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
/// keys or `NULL` for current keys.
/// \return A pointer to an `AMresult` struct containing an `AMstrs` struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMkeys(
doc: *const AMdoc,
obj_id: *const AMobjId,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
match heads.as_ref() {
None => to_result(doc.keys(obj_id)),
Some(heads) => to_result(doc.keys_at(obj_id, heads.as_ref())),
}
}
/// \memberof AMdoc
/// \brief Allocates storage for a document and initializes it with the compact
/// form of an incremental save.
///
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The number of bytes in \p src to load.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMdoc` struct.
/// \pre \p src `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// src must be a byte array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMload(src: *const u8, count: usize) -> *mut AMresult {
let mut data = Vec::new();
data.extend_from_slice(std::slice::from_raw_parts(src, count));
to_result(am::AutoCommit::load(&data))
}
/// \memberof AMdoc
/// \brief Loads the compact form of an incremental save into a document.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The number of bytes in \p src to load.
/// \return A pointer to an `AMresult` struct containing the number of
/// operations loaded from \p src.
/// \pre \p doc `!= NULL`.
/// \pre \p src `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// src must be a byte array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMloadIncremental(
doc: *mut AMdoc,
src: *const u8,
count: usize,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let mut data = Vec::new();
data.extend_from_slice(std::slice::from_raw_parts(src, count));
to_result(doc.load_incremental(&data))
}
/// \memberof AMdoc
/// \brief Applies all of the changes in \p src which are not in \p dest to
/// \p dest.
///
/// \param[in,out] dest A pointer to an `AMdoc` struct.
/// \param[in,out] src A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing an `AMchangeHashes`
/// struct.
/// \pre \p dest `!= NULL`.
/// \pre \p src `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// dest must be a valid pointer to an AMdoc
/// src must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMmerge(dest: *mut AMdoc, src: *mut AMdoc) -> *mut AMresult {
let dest = to_doc_mut!(dest);
to_result(dest.merge(to_doc_mut!(src)))
}
/// \memberof AMdoc
/// \brief Gets the current or historical size of an object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
/// size or `NULL` for current size.
/// \return A 64-bit unsigned integer.
/// \pre \p doc `!= NULL`.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMobjSize(
doc: *const AMdoc,
obj_id: *const AMobjId,
heads: *const AMchangeHashes,
) -> usize {
if let Some(doc) = doc.as_ref() {
let obj_id = to_obj_id!(obj_id);
match heads.as_ref() {
None => doc.length(obj_id),
Some(heads) => doc.length_at(obj_id, heads.as_ref()),
}
} else {
0
}
}
/// \memberof AMdoc
/// \brief Gets the current or historical values of an object within its entire
/// range.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
/// items or `NULL` for current items.
/// \return A pointer to an `AMresult` struct containing an `AMobjItems` struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMobjValues(
doc: *const AMdoc,
obj_id: *const AMobjId,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
match heads.as_ref() {
None => to_result(doc.values(obj_id)),
Some(heads) => to_result(doc.values_at(obj_id, heads.as_ref())),
}
}
/// \memberof AMdoc
/// \brief Gets the number of pending operations added during a document's
/// current transaction.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \return The count of pending operations for \p doc.
/// \pre \p doc `!= NULL`.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMpendingOps(doc: *const AMdoc) -> usize {
if let Some(doc) = doc.as_ref() {
doc.pending_ops()
} else {
0
}
}
/// \memberof AMdoc
/// \brief Receives a synchronization message from a peer based upon a given
/// synchronization state.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in,out] sync_state A pointer to an `AMsyncState` struct.
/// \param[in] sync_message A pointer to an `AMsyncMessage` struct.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p sync_state `!= NULL`.
/// \pre \p sync_message `!= NULL`.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// sync_state must be a valid pointer to an AMsyncState
/// sync_message must be a valid pointer to an AMsyncMessage
#[no_mangle]
pub unsafe extern "C" fn AMreceiveSyncMessage(
doc: *mut AMdoc,
sync_state: *mut AMsyncState,
sync_message: *const AMsyncMessage,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let sync_state = to_sync_state_mut!(sync_state);
let sync_message = to_sync_message!(sync_message);
to_result(doc.receive_sync_message(sync_state.as_mut(), sync_message.as_ref().clone()))
}
/// \memberof AMdoc
/// \brief Cancels the pending operations added during a document's current
/// transaction and gets the number of cancellations.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \return The count of pending operations for \p doc that were cancelled.
/// \pre \p doc `!= NULL`.
/// \internal
///
/// # Safety
/// doc must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMrollback(doc: *mut AMdoc) -> usize {
if let Some(doc) = doc.as_mut() {
doc.rollback()
} else {
0
}
}
/// \memberof AMdoc
/// \brief Saves the entirety of a document into a compact form.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing an array of bytes as
/// an `AMbyteSpan` struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMsave(doc: *mut AMdoc) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(Ok(doc.save()))
}
/// \memberof AMdoc
/// \brief Saves the changes to a document since its last save into a compact
/// form.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \return A pointer to an `AMresult` struct containing an array of bytes as
/// an `AMbyteSpan` struct.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
#[no_mangle]
pub unsafe extern "C" fn AMsaveIncremental(doc: *mut AMdoc) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(Ok(doc.save_incremental()))
}
/// \memberof AMdoc
/// \brief Puts the actor identifier of a document.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] actor_id A pointer to an `AMactorId` struct.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p actor_id `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// actor_id must be a valid pointer to an AMactorId
#[no_mangle]
pub unsafe extern "C" fn AMsetActorId(
doc: *mut AMdoc,
actor_id: *const AMactorId,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let actor_id = to_actor_id!(actor_id);
doc.set_actor(actor_id.as_ref().clone());
to_result(Ok(()))
}
/// \memberof AMdoc
/// \brief Splices values into and/or removes values from the identified object
/// at a given position within it.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] pos A position in the object identified by \p obj_id or
/// `SIZE_MAX` to indicate one past its end.
/// \param[in] del The number of characters to delete or `SIZE_MAX` to indicate
/// all of them.
/// \param[in] src A pointer to an array of `AMvalue` structs.
/// \param[in] count The number of `AMvalue` structs in \p src to load.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id`)` or \p pos `== SIZE_MAX`.
/// \pre `0 <=` \p del `<= AMobjSize(`\p obj_id`)` or \p del `== SIZE_MAX`.
/// \pre `(`\p src `!= NULL and 1 <=` \p count `<= sizeof(`\p src`)/
/// sizeof(AMvalue)) or `\p src `== NULL or `\p count `== 0`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// src must be an AMvalue array of size `>= count` or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMsplice(
doc: *mut AMdoc,
obj_id: *const AMobjId,
pos: usize,
del: usize,
src: *const AMvalue,
count: usize,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let len = doc.length(obj_id);
let pos = to_index!(pos, len, "pos");
let del = to_index!(del, len, "del");
let mut vals: Vec<am::ScalarValue> = vec![];
if !(src.is_null() || count == 0) {
let c_vals = std::slice::from_raw_parts(src, count);
for c_val in c_vals {
match c_val.try_into() {
Ok(s) => {
vals.push(s);
}
Err(e) => {
return AMresult::err(&e.to_string()).into();
}
}
}
}
to_result(doc.splice(obj_id, pos, del, vals))
}
/// \memberof AMdoc
/// \brief Splices characters into and/or removes characters from the
/// identified object at a given position within it.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] pos A position in the text object identified by \p obj_id or
/// `SIZE_MAX` to indicate one past its end.
/// \param[in] del The number of characters to delete or `SIZE_MAX` to indicate
/// all of them.
/// \param[in] text A UTF-8 string.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p pos `<= AMobjSize(`\p obj_id`)` or \p pos `== SIZE_MAX`.
/// \pre `0 <=` \p del `<= AMobjSize(`\p obj_id`)` or \p del `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// text must be a null-terminated array of `c_char` or NULL.
#[no_mangle]
pub unsafe extern "C" fn AMspliceText(
doc: *mut AMdoc,
obj_id: *const AMobjId,
pos: usize,
del: usize,
text: *const c_char,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let len = doc.length(obj_id);
let pos = to_index!(pos, len, "pos");
let del = to_index!(del, len, "del");
to_result(doc.splice_text(obj_id, pos, del, &to_str(text)))
}
/// \memberof AMdoc
/// \brief Gets the current or historical string represented by a text object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
/// keys or `NULL` for current keys.
/// \return A pointer to an `AMresult` struct containing a UTF-8 string.
/// \pre \p doc `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMtext(
doc: *const AMdoc,
obj_id: *const AMobjId,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
match heads.as_ref() {
None => to_result(doc.text(obj_id)),
Some(heads) => to_result(doc.text_at(obj_id, heads.as_ref())),
}
}

View file

@ -1,604 +0,0 @@
use automerge as am;
use automerge::transaction::Transactable;
use std::os::raw::c_char;
use crate::change_hashes::AMchangeHashes;
use crate::doc::{to_doc, to_doc_mut, to_obj_id, to_str, AMdoc};
use crate::obj::{AMobjId, AMobjType};
use crate::result::{to_result, AMresult};
pub mod item;
pub mod items;
macro_rules! adjust {
($index:expr, $insert:expr, $len:expr) => {{
// An empty object can only be inserted into.
let insert = $insert || $len == 0;
let end = if insert { $len } else { $len - 1 };
if $index > end && $index != usize::MAX {
return AMresult::err(&format!("Invalid index {}", $index)).into();
}
(std::cmp::min($index, end), insert)
}};
}
macro_rules! to_range {
($begin:expr, $end:expr) => {{
if $begin > $end {
return AMresult::err(&format!("Invalid range [{}-{})", $begin, $end)).into();
};
($begin..$end)
}};
}
/// \memberof AMdoc
/// \brief Deletes an index in a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistDelete(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, _) = adjust!(index, false, doc.length(obj_id));
to_result(doc.delete(obj_id, index))
}
/// \memberof AMdoc
/// \brief Gets the current or historical value at an index in a list object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for a historical
/// value or `NULL` for the current value.
/// \return A pointer to an `AMresult` struct that doesn't contain a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistGet(
doc: *const AMdoc,
obj_id: *const AMobjId,
index: usize,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, _) = adjust!(index, false, doc.length(obj_id));
match heads.as_ref() {
None => to_result(doc.get(obj_id, index)),
Some(heads) => to_result(doc.get_at(obj_id, index, heads.as_ref())),
}
}
/// \memberof AMdoc
/// \brief Gets all of the historical values at an index in a list object until
/// its current one or a specific one.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for a historical
/// last value or `NULL` for the current last value.
/// \return A pointer to an `AMresult` struct containing an `AMobjItems` struct.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistGetAll(
doc: *const AMdoc,
obj_id: *const AMobjId,
index: usize,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, _) = adjust!(index, false, doc.length(obj_id));
match heads.as_ref() {
None => to_result(doc.get_all(obj_id, index)),
Some(heads) => to_result(doc.get_all_at(obj_id, index, heads.as_ref())),
}
}
/// \memberof AMdoc
/// \brief Increments a counter at an index in a list object by the given
/// value.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistIncrement(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
value: i64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, _) = adjust!(index, false, doc.length(obj_id));
to_result(doc.increment(obj_id, index, value))
}
/// \memberof AMdoc
/// \brief Puts a boolean as the value at an index in a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] value A boolean.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistPutBool(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
value: bool,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
let value = am::ScalarValue::Boolean(value);
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts a sequence of bytes as the value at an index in a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \param[in] insert A flag to insert \p src before \p index instead of
/// writing \p src over \p index.
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The number of bytes to copy from \p src.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \pre \p src `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// src must be a byte array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMlistPutBytes(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
src: *const u8,
count: usize,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
let mut value = Vec::new();
value.extend_from_slice(std::slice::from_raw_parts(src, count));
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts a CRDT counter as the value at an index in a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistPutCounter(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
value: i64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
let value = am::ScalarValue::Counter(value.into());
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts a float as the value at an index in a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] value A 64-bit float.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistPutF64(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
value: f64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts a signed integer as the value at an index in a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistPutInt(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
value: i64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts null as the value at an index in a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistPutNull(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
to_result(if insert {
doc.insert(obj_id, index, ())
} else {
doc.put(obj_id, index, ())
})
}
/// \memberof AMdoc
/// \brief Puts an empty object as the value at an index in a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] obj_type An `AMobjIdType` enum tag.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMobjId` struct.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistPutObject(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
obj_type: AMobjType,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
let object = obj_type.into();
to_result(if insert {
doc.insert_object(obj_id, index, object)
} else {
doc.put_object(obj_id, index, object)
})
}
/// \memberof AMdoc
/// \brief Puts a UTF-8 string as the value at an index in a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] value A UTF-8 string.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \pre \p value `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// value must be a null-terminated array of `c_char`
#[no_mangle]
pub unsafe extern "C" fn AMlistPutStr(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
value: *const c_char,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
let value = to_str(value);
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts a Lamport timestamp as the value at an index in a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistPutTimestamp(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
value: i64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
let value = am::ScalarValue::Timestamp(value);
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Puts an unsigned integer as the value at an index in a list object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] index An index in the list object identified by \p obj_id or
/// `SIZE_MAX` to indicate its last index if \p insert
/// `== false` or one past its last index if \p insert
/// `== true`.
/// \param[in] insert A flag to insert \p value before \p index instead of
/// writing \p value over \p index.
/// \param[in] value A 64-bit unsigned integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre `0 <=` \p index `<= AMobjSize(`\p obj_id`)` or \p index `== SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistPutUint(
doc: *mut AMdoc,
obj_id: *const AMobjId,
index: usize,
insert: bool,
value: u64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let obj_id = to_obj_id!(obj_id);
let (index, insert) = adjust!(index, insert, doc.length(obj_id));
to_result(if insert {
doc.insert(obj_id, index, value)
} else {
doc.put(obj_id, index, value)
})
}
/// \memberof AMdoc
/// \brief Gets the current or historical indices and values of the list object
/// within the given range.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] begin The first index in a range of indices.
/// \param[in] end At least one past the last index in a range of indices.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
/// indices and values or `NULL` for current indices and
/// values.
/// \return A pointer to an `AMresult` struct containing an `AMlistItems`
/// struct.
/// \pre \p doc `!= NULL`.
/// \pre \p begin `<=` \p end `<= SIZE_MAX`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMlistRange(
doc: *const AMdoc,
obj_id: *const AMobjId,
begin: usize,
end: usize,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
let range = to_range!(begin, end);
match heads.as_ref() {
None => to_result(doc.list_range(obj_id, range)),
Some(heads) => to_result(doc.list_range_at(obj_id, range, heads.as_ref())),
}
}

View file

@ -1,100 +0,0 @@
use automerge as am;
use std::cell::RefCell;
use std::ffi::CString;
use crate::obj::AMobjId;
use crate::result::AMvalue;
/// \struct AMlistItem
/// \installed_headerfile
/// \brief An item in a list object.
#[repr(C)]
pub struct AMlistItem {
/// The index of an item in a list object.
index: usize,
/// The object identifier of an item in a list object.
obj_id: AMobjId,
/// The value of an item in a list object.
value: (am::Value<'static>, RefCell<Option<CString>>),
}
impl AMlistItem {
pub fn new(index: usize, value: am::Value<'static>, obj_id: am::ObjId) -> Self {
Self {
index,
obj_id: AMobjId::new(obj_id),
value: (value, Default::default()),
}
}
}
impl PartialEq for AMlistItem {
fn eq(&self, other: &Self) -> bool {
self.index == other.index && self.obj_id == other.obj_id && self.value.0 == other.value.0
}
}
/*
impl From<&AMlistItem> for (usize, am::Value<'static>, am::ObjId) {
fn from(list_item: &AMlistItem) -> Self {
(list_item.index, list_item.value.0.clone(), list_item.obj_id.as_ref().clone())
}
}
*/
/// \memberof AMlistItem
/// \brief Gets the index of an item in a list object.
///
/// \param[in] list_item A pointer to an `AMlistItem` struct.
/// \return A 64-bit unsigned integer.
/// \pre \p list_item `!= NULL`.
/// \internal
///
/// # Safety
/// list_item must be a valid pointer to an AMlistItem
#[no_mangle]
pub unsafe extern "C" fn AMlistItemIndex(list_item: *const AMlistItem) -> usize {
if let Some(list_item) = list_item.as_ref() {
list_item.index
} else {
usize::MAX
}
}
/// \memberof AMlistItem
/// \brief Gets the object identifier of an item in a list object.
///
/// \param[in] list_item A pointer to an `AMlistItem` struct.
/// \return A pointer to an `AMobjId` struct.
/// \pre \p list_item `!= NULL`.
/// \internal
///
/// # Safety
/// list_item must be a valid pointer to an AMlistItem
#[no_mangle]
pub unsafe extern "C" fn AMlistItemObjId(list_item: *const AMlistItem) -> *const AMobjId {
if let Some(list_item) = list_item.as_ref() {
&list_item.obj_id
} else {
std::ptr::null()
}
}
/// \memberof AMlistItem
/// \brief Gets the value of an item in a list object.
///
/// \param[in] list_item A pointer to an `AMlistItem` struct.
/// \return An `AMvalue` struct.
/// \pre \p list_item `!= NULL`.
/// \internal
///
/// # Safety
/// list_item must be a valid pointer to an AMlistItem
#[no_mangle]
pub unsafe extern "C" fn AMlistItemValue<'a>(list_item: *const AMlistItem) -> AMvalue<'a> {
if let Some(list_item) = list_item.as_ref() {
(&list_item.value.0, &list_item.value.1).into()
} else {
AMvalue::Void
}
}

View file

@ -1,348 +0,0 @@
use std::ffi::c_void;
use std::mem::size_of;
use crate::doc::list::item::AMlistItem;
#[repr(C)]
struct Detail {
len: usize,
offset: isize,
ptr: *const c_void,
}
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
/// propagate the name of a constant initialized from it so if the
/// constant's name is a symbolic representation of the value it can be
/// converted into a number by post-processing the header it generated.
pub const USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
impl Detail {
fn new(list_items: &[AMlistItem], offset: isize) -> Self {
Self {
len: list_items.len(),
offset,
ptr: list_items.as_ptr() as *const c_void,
}
}
pub fn advance(&mut self, n: isize) {
if n == 0 {
return;
}
let len = self.len as isize;
self.offset = if self.offset < 0 {
// It's reversed.
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
if unclipped >= 0 {
// Clip it to the forward stop.
len
} else {
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
}
} else {
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
if unclipped < 0 {
// Clip it to the reverse stop.
-(len + 1)
} else {
std::cmp::max(0, std::cmp::min(unclipped, len))
}
}
}
pub fn get_index(&self) -> usize {
(self.offset
+ if self.offset < 0 {
self.len as isize
} else {
0
}) as usize
}
pub fn next(&mut self, n: isize) -> Option<&AMlistItem> {
if self.is_stopped() {
return None;
}
let slice: &[AMlistItem] =
unsafe { std::slice::from_raw_parts(self.ptr as *const AMlistItem, self.len) };
let value = &slice[self.get_index()];
self.advance(n);
Some(value)
}
pub fn is_stopped(&self) -> bool {
let len = self.len as isize;
self.offset < -len || self.offset == len
}
pub fn prev(&mut self, n: isize) -> Option<&AMlistItem> {
self.advance(-n);
if self.is_stopped() {
return None;
}
let slice: &[AMlistItem] =
unsafe { std::slice::from_raw_parts(self.ptr as *const AMlistItem, self.len) };
Some(&slice[self.get_index()])
}
pub fn reversed(&self) -> Self {
Self {
len: self.len,
offset: -(self.offset + 1),
ptr: self.ptr,
}
}
pub fn rewound(&self) -> Self {
Self {
len: self.len,
offset: if self.offset < 0 { -1 } else { 0 },
ptr: self.ptr,
}
}
}
impl From<Detail> for [u8; USIZE_USIZE_USIZE_] {
fn from(detail: Detail) -> Self {
unsafe {
std::slice::from_raw_parts((&detail as *const Detail) as *const u8, USIZE_USIZE_USIZE_)
.try_into()
.unwrap()
}
}
}
/// \struct AMlistItems
/// \installed_headerfile
/// \brief A random-access iterator over a sequence of list object items.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMlistItems {
/// An implementation detail that is intentionally opaque.
/// \warning Modifying \p detail will cause undefined behavior.
/// \note The actual size of \p detail will vary by platform, this is just
/// the one for the platform this documentation was built on.
detail: [u8; USIZE_USIZE_USIZE_],
}
impl AMlistItems {
pub fn new(list_items: &[AMlistItem]) -> Self {
Self {
detail: Detail::new(list_items, 0).into(),
}
}
pub fn advance(&mut self, n: isize) {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.advance(n);
}
pub fn len(&self) -> usize {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
detail.len
}
pub fn next(&mut self, n: isize) -> Option<&AMlistItem> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<&AMlistItem> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.prev(n)
}
pub fn reversed(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.reversed().into(),
}
}
pub fn rewound(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.rewound().into(),
}
}
}
impl AsRef<[AMlistItem]> for AMlistItems {
fn as_ref(&self) -> &[AMlistItem] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const AMlistItem, detail.len) }
}
}
impl Default for AMlistItems {
fn default() -> Self {
Self {
detail: [0; USIZE_USIZE_USIZE_],
}
}
}
/// \memberof AMlistItems
/// \brief Advances an iterator over a sequence of list object items by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in,out] list_items A pointer to an `AMlistItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \pre \p list_items `!= NULL`.
/// \internal
///
/// #Safety
/// list_items must be a valid pointer to an AMlistItems
#[no_mangle]
pub unsafe extern "C" fn AMlistItemsAdvance(list_items: *mut AMlistItems, n: isize) {
if let Some(list_items) = list_items.as_mut() {
list_items.advance(n);
};
}
/// \memberof AMlistItems
/// \brief Tests the equality of two sequences of list object items underlying
/// a pair of iterators.
///
/// \param[in] list_items1 A pointer to an `AMlistItems` struct.
/// \param[in] list_items2 A pointer to an `AMlistItems` struct.
/// \return `true` if \p list_items1 `==` \p list_items2 and `false` otherwise.
/// \pre \p list_items1 `!= NULL`.
/// \pre \p list_items2 `!= NULL`.
/// \internal
///
/// #Safety
/// list_items1 must be a valid pointer to an AMlistItems
/// list_items2 must be a valid pointer to an AMlistItems
#[no_mangle]
pub unsafe extern "C" fn AMlistItemsEqual(
list_items1: *const AMlistItems,
list_items2: *const AMlistItems,
) -> bool {
match (list_items1.as_ref(), list_items2.as_ref()) {
(Some(list_items1), Some(list_items2)) => list_items1.as_ref() == list_items2.as_ref(),
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \memberof AMlistItems
/// \brief Gets the list object item at the current position of an iterator
/// over a sequence of list object items and then advances it by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in,out] list_items A pointer to an `AMlistItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMlistItem` struct that's `NULL` when
/// \p list_items was previously advanced past its forward/reverse
/// limit.
/// \pre \p list_items `!= NULL`.
/// \internal
///
/// #Safety
/// list_items must be a valid pointer to an AMlistItems
#[no_mangle]
pub unsafe extern "C" fn AMlistItemsNext(
list_items: *mut AMlistItems,
n: isize,
) -> *const AMlistItem {
if let Some(list_items) = list_items.as_mut() {
if let Some(list_item) = list_items.next(n) {
return list_item;
}
}
std::ptr::null()
}
/// \memberof AMlistItems
/// \brief Advances an iterator over a sequence of list object items by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction and then gets the list object item at its new
/// position.
///
/// \param[in,out] list_items A pointer to an `AMlistItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMlistItem` struct that's `NULL` when
/// \p list_items is presently advanced past its forward/reverse limit.
/// \pre \p list_items `!= NULL`.
/// \internal
///
/// #Safety
/// list_items must be a valid pointer to an AMlistItems
#[no_mangle]
pub unsafe extern "C" fn AMlistItemsPrev(
list_items: *mut AMlistItems,
n: isize,
) -> *const AMlistItem {
if let Some(list_items) = list_items.as_mut() {
if let Some(list_item) = list_items.prev(n) {
return list_item;
}
}
std::ptr::null()
}
/// \memberof AMlistItems
/// \brief Gets the size of the sequence of list object items underlying an
/// iterator.
///
/// \param[in] list_items A pointer to an `AMlistItems` struct.
/// \return The count of values in \p list_items.
/// \pre \p list_items `!= NULL`.
/// \internal
///
/// #Safety
/// list_items must be a valid pointer to an AMlistItems
#[no_mangle]
pub unsafe extern "C" fn AMlistItemsSize(list_items: *const AMlistItems) -> usize {
if let Some(list_items) = list_items.as_ref() {
list_items.len()
} else {
0
}
}
/// \memberof AMlistItems
/// \brief Creates an iterator over the same sequence of list object items as
/// the given one but with the opposite position and direction.
///
/// \param[in] list_items A pointer to an `AMlistItems` struct.
/// \return An `AMlistItems` struct
/// \pre \p list_items `!= NULL`.
/// \internal
///
/// #Safety
/// list_items must be a valid pointer to an AMlistItems
#[no_mangle]
pub unsafe extern "C" fn AMlistItemsReversed(list_items: *const AMlistItems) -> AMlistItems {
if let Some(list_items) = list_items.as_ref() {
list_items.reversed()
} else {
AMlistItems::default()
}
}
/// \memberof AMlistItems
/// \brief Creates an iterator at the starting position over the same sequence
/// of list object items as the given one.
///
/// \param[in] list_items A pointer to an `AMlistItems` struct.
/// \return An `AMlistItems` struct
/// \pre \p list_items `!= NULL`.
/// \internal
///
/// #Safety
/// list_items must be a valid pointer to an AMlistItems
#[no_mangle]
pub unsafe extern "C" fn AMlistItemsRewound(list_items: *const AMlistItems) -> AMlistItems {
if let Some(list_items) = list_items.as_ref() {
list_items.rewound()
} else {
AMlistItems::default()
}
}

View file

@ -1,506 +0,0 @@
use automerge as am;
use automerge::transaction::Transactable;
use std::os::raw::c_char;
use crate::change_hashes::AMchangeHashes;
use crate::doc::utils::to_str;
use crate::doc::{to_doc, to_doc_mut, to_obj_id, AMdoc};
use crate::obj::{AMobjId, AMobjType};
use crate::result::{to_result, AMresult};
pub mod item;
pub mod items;
/// \memberof AMdoc
/// \brief Deletes a key in a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapDelete(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.delete(to_obj_id!(obj_id), to_str(key)))
}
/// \memberof AMdoc
/// \brief Gets the current or historical value for a key in a map object.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by
/// \p obj_id.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for a historical
/// value or `NULL` for the current value.
/// \return A pointer to an `AMresult` struct that doesn't contain a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMmapGet(
doc: *const AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
match heads.as_ref() {
None => to_result(doc.get(obj_id, to_str(key))),
Some(heads) => to_result(doc.get_at(obj_id, to_str(key), heads.as_ref())),
}
}
/// \memberof AMdoc
/// \brief Gets all of the historical values for a key in a map object until
/// its current one or a specific one.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by
/// \p obj_id.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for a historical
/// last value or `NULL` for the current last value.
/// \return A pointer to an `AMresult` struct containing an `AMobjItems` struct.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMmapGetAll(
doc: *const AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
match heads.as_ref() {
None => to_result(doc.get_all(obj_id, to_str(key))),
Some(heads) => to_result(doc.get_all_at(obj_id, to_str(key), heads.as_ref())),
}
}
/// \memberof AMdoc
/// \brief Increments a counter for a key in a map object by the given value.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapIncrement(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: i64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.increment(to_obj_id!(obj_id), to_str(key), value))
}
/// \memberof AMdoc
/// \brief Puts a boolean as the value of a key in a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A boolean.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutBool(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: bool,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.put(to_obj_id!(obj_id), to_str(key), value))
}
/// \memberof AMdoc
/// \brief Puts a sequence of bytes as the value of a key in a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] src A pointer to an array of bytes.
/// \param[in] count The number of bytes to copy from \p src.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \pre \p src `!= NULL`.
/// \pre `0 <` \p count `<= sizeof(`\p src`)`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
/// src must be a byte array of size `>= count`
#[no_mangle]
pub unsafe extern "C" fn AMmapPutBytes(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
src: *const u8,
count: usize,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
let mut vec = Vec::new();
vec.extend_from_slice(std::slice::from_raw_parts(src, count));
to_result(doc.put(to_obj_id!(obj_id), to_str(key), vec))
}
/// \memberof AMdoc
/// \brief Puts a CRDT counter as the value of a key in a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutCounter(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: i64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.put(
to_obj_id!(obj_id),
to_str(key),
am::ScalarValue::Counter(value.into()),
))
}
/// \memberof AMdoc
/// \brief Puts null as the value of a key in a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutNull(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.put(to_obj_id!(obj_id), to_str(key), ()))
}
/// \memberof AMdoc
/// \brief Puts an empty object as the value of a key in a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] obj_type An `AMobjIdType` enum tag.
/// \return A pointer to an `AMresult` struct containing a pointer to an
/// `AMobjId` struct.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutObject(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
obj_type: AMobjType,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.put_object(to_obj_id!(obj_id), to_str(key), obj_type.into()))
}
/// \memberof AMdoc
/// \brief Puts a float as the value of a key in a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A 64-bit float.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutF64(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: f64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.put(to_obj_id!(obj_id), to_str(key), value))
}
/// \memberof AMdoc
/// \brief Puts a signed integer as the value of a key in a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutInt(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: i64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.put(to_obj_id!(obj_id), to_str(key), value))
}
/// \memberof AMdoc
/// \brief Puts a UTF-8 string as the value of a key in a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A UTF-8 string.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \pre \p value `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
/// value must be a null-terminated array of `c_char`
#[no_mangle]
pub unsafe extern "C" fn AMmapPutStr(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: *const c_char,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.put(to_obj_id!(obj_id), to_str(key), to_str(value)))
}
/// \memberof AMdoc
/// \brief Puts a Lamport timestamp as the value of a key in a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A 64-bit signed integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutTimestamp(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: i64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.put(
to_obj_id!(obj_id),
to_str(key),
am::ScalarValue::Timestamp(value),
))
}
/// \memberof AMdoc
/// \brief Puts an unsigned integer as the value of a key in a map object.
///
/// \param[in,out] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] key A UTF-8 string key for the map object identified by \p obj_id.
/// \param[in] value A 64-bit unsigned integer.
/// \return A pointer to an `AMresult` struct containing a void.
/// \pre \p doc `!= NULL`.
/// \pre \p key `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// key must be a c string of the map key to be used
#[no_mangle]
pub unsafe extern "C" fn AMmapPutUint(
doc: *mut AMdoc,
obj_id: *const AMobjId,
key: *const c_char,
value: u64,
) -> *mut AMresult {
let doc = to_doc_mut!(doc);
to_result(doc.put(to_obj_id!(obj_id), to_str(key), value))
}
/// \memberof AMdoc
/// \brief Gets the current or historical keys and values of the map object
/// within the given range.
///
/// \param[in] doc A pointer to an `AMdoc` struct.
/// \param[in] obj_id A pointer to an `AMobjId` struct or `AM_ROOT`.
/// \param[in] begin The first key in a subrange or `NULL` to indicate the
/// absolute first key.
/// \param[in] end The key one past the last key in a subrange or `NULL` to
/// indicate one past the absolute last key.
/// \param[in] heads A pointer to an `AMchangeHashes` struct for historical
/// keys and values or `NULL` for current keys and values.
/// \return A pointer to an `AMresult` struct containing an `AMmapItems`
/// struct.
/// \pre \p doc `!= NULL`.
/// \pre `strcmp(`\p begin, \p end`) != 1` if \p begin `!= NULL` and \p end `!= NULL`.
/// \warning The returned `AMresult` struct must be deallocated with `AMfree()`
/// in order to prevent a memory leak.
/// \internal
/// # Safety
/// doc must be a valid pointer to an AMdoc
/// obj_id must be a valid pointer to an AMobjId or std::ptr::null()
/// heads must be a valid pointer to an AMchangeHashes or std::ptr::null()
#[no_mangle]
pub unsafe extern "C" fn AMmapRange(
doc: *const AMdoc,
obj_id: *const AMobjId,
begin: *const c_char,
end: *const c_char,
heads: *const AMchangeHashes,
) -> *mut AMresult {
let doc = to_doc!(doc);
let obj_id = to_obj_id!(obj_id);
match (begin.as_ref(), end.as_ref()) {
(Some(_), Some(_)) => {
let (begin, end) = (to_str(begin), to_str(end));
if begin > end {
return AMresult::err(&format!("Invalid range [{}-{})", begin, end)).into();
};
let bounds = begin..end;
if let Some(heads) = heads.as_ref() {
to_result(doc.map_range_at(obj_id, bounds, heads.as_ref()))
} else {
to_result(doc.map_range(obj_id, bounds))
}
}
(Some(_), None) => {
let bounds = to_str(begin)..;
if let Some(heads) = heads.as_ref() {
to_result(doc.map_range_at(obj_id, bounds, heads.as_ref()))
} else {
to_result(doc.map_range(obj_id, bounds))
}
}
(None, Some(_)) => {
let bounds = ..to_str(end);
if let Some(heads) = heads.as_ref() {
to_result(doc.map_range_at(obj_id, bounds, heads.as_ref()))
} else {
to_result(doc.map_range(obj_id, bounds))
}
}
(None, None) => {
let bounds = ..;
if let Some(heads) = heads.as_ref() {
to_result(doc.map_range_at(obj_id, bounds, heads.as_ref()))
} else {
to_result(doc.map_range(obj_id, bounds))
}
}
}
}

View file

@ -1,101 +0,0 @@
use automerge as am;
use std::cell::RefCell;
use std::ffi::CString;
use std::os::raw::c_char;
use crate::obj::AMobjId;
use crate::result::AMvalue;
/// \struct AMmapItem
/// \installed_headerfile
/// \brief An item in a map object.
#[repr(C)]
pub struct AMmapItem {
/// The key of an item in a map object.
key: CString,
/// The object identifier of an item in a map object.
obj_id: AMobjId,
/// The value of an item in a map object.
value: (am::Value<'static>, RefCell<Option<CString>>),
}
impl AMmapItem {
pub fn new(key: &'static str, value: am::Value<'static>, obj_id: am::ObjId) -> Self {
Self {
key: CString::new(key).unwrap(),
obj_id: AMobjId::new(obj_id),
value: (value, Default::default()),
}
}
}
impl PartialEq for AMmapItem {
fn eq(&self, other: &Self) -> bool {
self.key == other.key && self.obj_id == other.obj_id && self.value.0 == other.value.0
}
}
/*
impl From<&AMmapItem> for (String, am::Value<'static>, am::ObjId) {
fn from(map_item: &AMmapItem) -> Self {
(map_item.key.into_string().unwrap(), map_item.value.0.clone(), map_item.obj_id.as_ref().clone())
}
}
*/
/// \memberof AMmapItem
/// \brief Gets the key of an item in a map object.
///
/// \param[in] map_item A pointer to an `AMmapItem` struct.
/// \return A 64-bit unsigned integer.
/// \pre \p map_item `!= NULL`.
/// \internal
///
/// # Safety
/// map_item must be a valid pointer to an AMmapItem
#[no_mangle]
pub unsafe extern "C" fn AMmapItemKey(map_item: *const AMmapItem) -> *const c_char {
if let Some(map_item) = map_item.as_ref() {
map_item.key.as_ptr()
} else {
std::ptr::null()
}
}
/// \memberof AMmapItem
/// \brief Gets the object identifier of an item in a map object.
///
/// \param[in] map_item A pointer to an `AMmapItem` struct.
/// \return A pointer to an `AMobjId` struct.
/// \pre \p map_item `!= NULL`.
/// \internal
///
/// # Safety
/// map_item must be a valid pointer to an AMmapItem
#[no_mangle]
pub unsafe extern "C" fn AMmapItemObjId(map_item: *const AMmapItem) -> *const AMobjId {
if let Some(map_item) = map_item.as_ref() {
&map_item.obj_id
} else {
std::ptr::null()
}
}
/// \memberof AMmapItem
/// \brief Gets the value of an item in a map object.
///
/// \param[in] map_item A pointer to an `AMmapItem` struct.
/// \return An `AMvalue` struct.
/// \pre \p map_item `!= NULL`.
/// \internal
///
/// # Safety
/// map_item must be a valid pointer to an AMmapItem
#[no_mangle]
pub unsafe extern "C" fn AMmapItemValue<'a>(map_item: *const AMmapItem) -> AMvalue<'a> {
if let Some(map_item) = map_item.as_ref() {
(&map_item.value.0, &map_item.value.1).into()
} else {
AMvalue::Void
}
}

View file

@ -1,340 +0,0 @@
use std::ffi::c_void;
use std::mem::size_of;
use crate::doc::map::item::AMmapItem;
#[repr(C)]
struct Detail {
len: usize,
offset: isize,
ptr: *const c_void,
}
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
/// propagate the name of a constant initialized from it so if the
/// constant's name is a symbolic representation of the value it can be
/// converted into a number by post-processing the header it generated.
pub const USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
impl Detail {
fn new(map_items: &[AMmapItem], offset: isize) -> Self {
Self {
len: map_items.len(),
offset,
ptr: map_items.as_ptr() as *const c_void,
}
}
pub fn advance(&mut self, n: isize) {
if n == 0 {
return;
}
let len = self.len as isize;
self.offset = if self.offset < 0 {
// It's reversed.
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
if unclipped >= 0 {
// Clip it to the forward stop.
len
} else {
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
}
} else {
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
if unclipped < 0 {
// Clip it to the reverse stop.
-(len + 1)
} else {
std::cmp::max(0, std::cmp::min(unclipped, len))
}
}
}
pub fn get_index(&self) -> usize {
(self.offset
+ if self.offset < 0 {
self.len as isize
} else {
0
}) as usize
}
pub fn next(&mut self, n: isize) -> Option<&AMmapItem> {
if self.is_stopped() {
return None;
}
let slice: &[AMmapItem] =
unsafe { std::slice::from_raw_parts(self.ptr as *const AMmapItem, self.len) };
let value = &slice[self.get_index()];
self.advance(n);
Some(value)
}
pub fn is_stopped(&self) -> bool {
let len = self.len as isize;
self.offset < -len || self.offset == len
}
pub fn prev(&mut self, n: isize) -> Option<&AMmapItem> {
self.advance(-n);
if self.is_stopped() {
return None;
}
let slice: &[AMmapItem] =
unsafe { std::slice::from_raw_parts(self.ptr as *const AMmapItem, self.len) };
Some(&slice[self.get_index()])
}
pub fn reversed(&self) -> Self {
Self {
len: self.len,
offset: -(self.offset + 1),
ptr: self.ptr,
}
}
pub fn rewound(&self) -> Self {
Self {
len: self.len,
offset: if self.offset < 0 { -1 } else { 0 },
ptr: self.ptr,
}
}
}
impl From<Detail> for [u8; USIZE_USIZE_USIZE_] {
fn from(detail: Detail) -> Self {
unsafe {
std::slice::from_raw_parts((&detail as *const Detail) as *const u8, USIZE_USIZE_USIZE_)
.try_into()
.unwrap()
}
}
}
/// \struct AMmapItems
/// \installed_headerfile
/// \brief A random-access iterator over a sequence of map object items.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMmapItems {
/// An implementation detail that is intentionally opaque.
/// \warning Modifying \p detail will cause undefined behavior.
/// \note The actual size of \p detail will vary by platform, this is just
/// the one for the platform this documentation was built on.
detail: [u8; USIZE_USIZE_USIZE_],
}
impl AMmapItems {
pub fn new(map_items: &[AMmapItem]) -> Self {
Self {
detail: Detail::new(map_items, 0).into(),
}
}
pub fn advance(&mut self, n: isize) {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.advance(n);
}
pub fn len(&self) -> usize {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
detail.len
}
pub fn next(&mut self, n: isize) -> Option<&AMmapItem> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<&AMmapItem> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.prev(n)
}
pub fn reversed(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.reversed().into(),
}
}
pub fn rewound(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.rewound().into(),
}
}
}
impl AsRef<[AMmapItem]> for AMmapItems {
fn as_ref(&self) -> &[AMmapItem] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const AMmapItem, detail.len) }
}
}
impl Default for AMmapItems {
fn default() -> Self {
Self {
detail: [0; USIZE_USIZE_USIZE_],
}
}
}
/// \memberof AMmapItems
/// \brief Advances an iterator over a sequence of map object items by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in,out] map_items A pointer to an `AMmapItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \pre \p map_items `!= NULL`.
/// \internal
///
/// #Safety
/// map_items must be a valid pointer to an AMmapItems
#[no_mangle]
pub unsafe extern "C" fn AMmapItemsAdvance(map_items: *mut AMmapItems, n: isize) {
if let Some(map_items) = map_items.as_mut() {
map_items.advance(n);
};
}
/// \memberof AMmapItems
/// \brief Tests the equality of two sequences of map object items underlying
/// a pair of iterators.
///
/// \param[in] map_items1 A pointer to an `AMmapItems` struct.
/// \param[in] map_items2 A pointer to an `AMmapItems` struct.
/// \return `true` if \p map_items1 `==` \p map_items2 and `false` otherwise.
/// \pre \p map_items1 `!= NULL`.
/// \pre \p map_items2 `!= NULL`.
/// \internal
///
/// #Safety
/// map_items1 must be a valid pointer to an AMmapItems
/// map_items2 must be a valid pointer to an AMmapItems
#[no_mangle]
pub unsafe extern "C" fn AMmapItemsEqual(
map_items1: *const AMmapItems,
map_items2: *const AMmapItems,
) -> bool {
match (map_items1.as_ref(), map_items2.as_ref()) {
(Some(map_items1), Some(map_items2)) => map_items1.as_ref() == map_items2.as_ref(),
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \memberof AMmapItems
/// \brief Gets the map object item at the current position of an iterator
/// over a sequence of map object items and then advances it by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in,out] map_items A pointer to an `AMmapItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMmapItem` struct that's `NULL` when \p map_items
/// was previously advanced past its forward/reverse limit.
/// \pre \p map_items `!= NULL`.
/// \internal
///
/// #Safety
/// map_items must be a valid pointer to an AMmapItems
#[no_mangle]
pub unsafe extern "C" fn AMmapItemsNext(map_items: *mut AMmapItems, n: isize) -> *const AMmapItem {
if let Some(map_items) = map_items.as_mut() {
if let Some(map_item) = map_items.next(n) {
return map_item;
}
}
std::ptr::null()
}
/// \memberof AMmapItems
/// \brief Advances an iterator over a sequence of map object items by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction and then gets the map object item at its new
/// position.
///
/// \param[in,out] map_items A pointer to an `AMmapItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMmapItem` struct that's `NULL` when \p map_items
/// is presently advanced past its forward/reverse limit.
/// \pre \p map_items `!= NULL`.
/// \internal
///
/// #Safety
/// map_items must be a valid pointer to an AMmapItems
#[no_mangle]
pub unsafe extern "C" fn AMmapItemsPrev(map_items: *mut AMmapItems, n: isize) -> *const AMmapItem {
if let Some(map_items) = map_items.as_mut() {
if let Some(map_item) = map_items.prev(n) {
return map_item;
}
}
std::ptr::null()
}
/// \memberof AMmapItems
/// \brief Gets the size of the sequence of map object items underlying an
/// iterator.
///
/// \param[in] map_items A pointer to an `AMmapItems` struct.
/// \return The count of values in \p map_items.
/// \pre \p map_items `!= NULL`.
/// \internal
///
/// #Safety
/// map_items must be a valid pointer to an AMmapItems
#[no_mangle]
pub unsafe extern "C" fn AMmapItemsSize(map_items: *const AMmapItems) -> usize {
if let Some(map_items) = map_items.as_ref() {
map_items.len()
} else {
0
}
}
/// \memberof AMmapItems
/// \brief Creates an iterator over the same sequence of map object items as
/// the given one but with the opposite position and direction.
///
/// \param[in] map_items A pointer to an `AMmapItems` struct.
/// \return An `AMmapItems` struct
/// \pre \p map_items `!= NULL`.
/// \internal
///
/// #Safety
/// map_items must be a valid pointer to an AMmapItems
#[no_mangle]
pub unsafe extern "C" fn AMmapItemsReversed(map_items: *const AMmapItems) -> AMmapItems {
if let Some(map_items) = map_items.as_ref() {
map_items.reversed()
} else {
AMmapItems::default()
}
}
/// \memberof AMmapItems
/// \brief Creates an iterator at the starting position over the same sequence of map object items as the given one.
///
/// \param[in] map_items A pointer to an `AMmapItems` struct.
/// \return An `AMmapItems` struct
/// \pre \p map_items `!= NULL`.
/// \internal
///
/// #Safety
/// map_items must be a valid pointer to an AMmapItems
#[no_mangle]
pub unsafe extern "C" fn AMmapItemsRewound(map_items: *const AMmapItems) -> AMmapItems {
if let Some(map_items) = map_items.as_ref() {
map_items.rewound()
} else {
AMmapItems::default()
}
}

View file

@ -1,57 +0,0 @@
use std::ffi::CStr;
use std::os::raw::c_char;
macro_rules! to_actor_id {
($handle:expr) => {{
let handle = $handle.as_ref();
match handle {
Some(b) => b,
None => return AMresult::err("Invalid AMactorId pointer").into(),
}
}};
}
pub(crate) use to_actor_id;
macro_rules! to_doc {
($handle:expr) => {{
let handle = $handle.as_ref();
match handle {
Some(b) => b,
None => return AMresult::err("Invalid AMdoc pointer").into(),
}
}};
}
pub(crate) use to_doc;
macro_rules! to_doc_mut {
($handle:expr) => {{
let handle = $handle.as_mut();
match handle {
Some(b) => b,
None => return AMresult::err("Invalid AMdoc pointer").into(),
}
}};
}
pub(crate) use to_doc_mut;
macro_rules! to_obj_id {
($handle:expr) => {{
match $handle.as_ref() {
Some(obj_id) => obj_id,
None => &automerge::ROOT,
}
}};
}
pub(crate) use to_obj_id;
pub(crate) unsafe fn to_str(c: *const c_char) -> String {
if !c.is_null() {
CStr::from_ptr(c).to_string_lossy().to_string()
} else {
String::default()
}
}

View file

@ -1,11 +0,0 @@
mod actor_id;
mod byte_span;
mod change;
mod change_hashes;
mod changes;
mod doc;
mod obj;
mod result;
mod result_stack;
mod strs;
mod sync;

View file

@ -1,76 +0,0 @@
use automerge as am;
use std::cell::RefCell;
use std::ffi::CString;
use crate::obj::AMobjId;
use crate::result::AMvalue;
/// \struct AMobjItem
/// \installed_headerfile
/// \brief An item in an object.
#[repr(C)]
pub struct AMobjItem {
/// The object identifier of an item in an object.
obj_id: AMobjId,
/// The value of an item in an object.
value: (am::Value<'static>, RefCell<Option<CString>>),
}
impl AMobjItem {
pub fn new(value: am::Value<'static>, obj_id: am::ObjId) -> Self {
Self {
obj_id: AMobjId::new(obj_id),
value: (value, Default::default()),
}
}
}
impl PartialEq for AMobjItem {
fn eq(&self, other: &Self) -> bool {
self.obj_id == other.obj_id && self.value.0 == other.value.0
}
}
impl From<&AMobjItem> for (am::Value<'static>, am::ObjId) {
fn from(obj_item: &AMobjItem) -> Self {
(obj_item.value.0.clone(), obj_item.obj_id.as_ref().clone())
}
}
/// \memberof AMobjItem
/// \brief Gets the object identifier of an item in an object.
///
/// \param[in] obj_item A pointer to an `AMobjItem` struct.
/// \return A pointer to an `AMobjId` struct.
/// \pre \p obj_item `!= NULL`.
/// \internal
///
/// # Safety
/// obj_item must be a valid pointer to an AMobjItem
#[no_mangle]
pub unsafe extern "C" fn AMobjItemObjId(obj_item: *const AMobjItem) -> *const AMobjId {
if let Some(obj_item) = obj_item.as_ref() {
&obj_item.obj_id
} else {
std::ptr::null()
}
}
/// \memberof AMobjItem
/// \brief Gets the value of an item in an object.
///
/// \param[in] obj_item A pointer to an `AMobjItem` struct.
/// \return An `AMvalue` struct.
/// \pre \p obj_item `!= NULL`.
/// \internal
///
/// # Safety
/// obj_item must be a valid pointer to an AMobjItem
#[no_mangle]
pub unsafe extern "C" fn AMobjItemValue<'a>(obj_item: *const AMobjItem) -> AMvalue<'a> {
if let Some(obj_item) = obj_item.as_ref() {
(&obj_item.value.0, &obj_item.value.1).into()
} else {
AMvalue::Void
}
}

View file

@ -1,341 +0,0 @@
use std::ffi::c_void;
use std::mem::size_of;
use crate::obj::item::AMobjItem;
#[repr(C)]
struct Detail {
len: usize,
offset: isize,
ptr: *const c_void,
}
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
/// propagate the name of a constant initialized from it so if the
/// constant's name is a symbolic representation of the value it can be
/// converted into a number by post-processing the header it generated.
pub const USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
impl Detail {
fn new(obj_items: &[AMobjItem], offset: isize) -> Self {
Self {
len: obj_items.len(),
offset,
ptr: obj_items.as_ptr() as *const c_void,
}
}
pub fn advance(&mut self, n: isize) {
if n == 0 {
return;
}
let len = self.len as isize;
self.offset = if self.offset < 0 {
// It's reversed.
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
if unclipped >= 0 {
// Clip it to the forward stop.
len
} else {
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
}
} else {
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
if unclipped < 0 {
// Clip it to the reverse stop.
-(len + 1)
} else {
std::cmp::max(0, std::cmp::min(unclipped, len))
}
}
}
pub fn get_index(&self) -> usize {
(self.offset
+ if self.offset < 0 {
self.len as isize
} else {
0
}) as usize
}
pub fn next(&mut self, n: isize) -> Option<&AMobjItem> {
if self.is_stopped() {
return None;
}
let slice: &[AMobjItem] =
unsafe { std::slice::from_raw_parts(self.ptr as *const AMobjItem, self.len) };
let value = &slice[self.get_index()];
self.advance(n);
Some(value)
}
pub fn is_stopped(&self) -> bool {
let len = self.len as isize;
self.offset < -len || self.offset == len
}
pub fn prev(&mut self, n: isize) -> Option<&AMobjItem> {
self.advance(-n);
if self.is_stopped() {
return None;
}
let slice: &[AMobjItem] =
unsafe { std::slice::from_raw_parts(self.ptr as *const AMobjItem, self.len) };
Some(&slice[self.get_index()])
}
pub fn reversed(&self) -> Self {
Self {
len: self.len,
offset: -(self.offset + 1),
ptr: self.ptr,
}
}
pub fn rewound(&self) -> Self {
Self {
len: self.len,
offset: if self.offset < 0 { -1 } else { 0 },
ptr: self.ptr,
}
}
}
impl From<Detail> for [u8; USIZE_USIZE_USIZE_] {
fn from(detail: Detail) -> Self {
unsafe {
std::slice::from_raw_parts((&detail as *const Detail) as *const u8, USIZE_USIZE_USIZE_)
.try_into()
.unwrap()
}
}
}
/// \struct AMobjItems
/// \installed_headerfile
/// \brief A random-access iterator over a sequence of object items.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMobjItems {
/// An implementation detail that is intentionally opaque.
/// \warning Modifying \p detail will cause undefined behavior.
/// \note The actual size of \p detail will vary by platform, this is just
/// the one for the platform this documentation was built on.
detail: [u8; USIZE_USIZE_USIZE_],
}
impl AMobjItems {
pub fn new(obj_items: &[AMobjItem]) -> Self {
Self {
detail: Detail::new(obj_items, 0).into(),
}
}
pub fn advance(&mut self, n: isize) {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.advance(n);
}
pub fn len(&self) -> usize {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
detail.len
}
pub fn next(&mut self, n: isize) -> Option<&AMobjItem> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<&AMobjItem> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.prev(n)
}
pub fn reversed(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.reversed().into(),
}
}
pub fn rewound(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.rewound().into(),
}
}
}
impl AsRef<[AMobjItem]> for AMobjItems {
fn as_ref(&self) -> &[AMobjItem] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const AMobjItem, detail.len) }
}
}
impl Default for AMobjItems {
fn default() -> Self {
Self {
detail: [0; USIZE_USIZE_USIZE_],
}
}
}
/// \memberof AMobjItems
/// \brief Advances an iterator over a sequence of object items by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in,out] obj_items A pointer to an `AMobjItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \pre \p obj_items `!= NULL`.
/// \internal
///
/// #Safety
/// obj_items must be a valid pointer to an AMobjItems
#[no_mangle]
pub unsafe extern "C" fn AMobjItemsAdvance(obj_items: *mut AMobjItems, n: isize) {
if let Some(obj_items) = obj_items.as_mut() {
obj_items.advance(n);
};
}
/// \memberof AMobjItems
/// \brief Tests the equality of two sequences of object items underlying a
/// pair of iterators.
///
/// \param[in] obj_items1 A pointer to an `AMobjItems` struct.
/// \param[in] obj_items2 A pointer to an `AMobjItems` struct.
/// \return `true` if \p obj_items1 `==` \p obj_items2 and `false` otherwise.
/// \pre \p obj_items1 `!= NULL`.
/// \pre \p obj_items2 `!= NULL`.
/// \internal
///
/// #Safety
/// obj_items1 must be a valid pointer to an AMobjItems
/// obj_items2 must be a valid pointer to an AMobjItems
#[no_mangle]
pub unsafe extern "C" fn AMobjItemsEqual(
obj_items1: *const AMobjItems,
obj_items2: *const AMobjItems,
) -> bool {
match (obj_items1.as_ref(), obj_items2.as_ref()) {
(Some(obj_items1), Some(obj_items2)) => obj_items1.as_ref() == obj_items2.as_ref(),
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \memberof AMobjItems
/// \brief Gets the object item at the current position of an iterator over a
/// sequence of object items and then advances it by at most \p |n|
/// positions where the sign of \p n is relative to the iterator's
/// direction.
///
/// \param[in,out] obj_items A pointer to an `AMobjItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMobjItem` struct that's `NULL` when \p obj_items
/// was previously advanced past its forward/reverse limit.
/// \pre \p obj_items `!= NULL`.
/// \internal
///
/// #Safety
/// obj_items must be a valid pointer to an AMobjItems
#[no_mangle]
pub unsafe extern "C" fn AMobjItemsNext(obj_items: *mut AMobjItems, n: isize) -> *const AMobjItem {
if let Some(obj_items) = obj_items.as_mut() {
if let Some(obj_item) = obj_items.next(n) {
return obj_item;
}
}
std::ptr::null()
}
/// \memberof AMobjItems
/// \brief Advances an iterator over a sequence of object items by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction and then gets the object item at its new
/// position.
///
/// \param[in,out] obj_items A pointer to an `AMobjItems` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMobjItem` struct that's `NULL` when \p obj_items
/// is presently advanced past its forward/reverse limit.
/// \pre \p obj_items `!= NULL`.
/// \internal
///
/// #Safety
/// obj_items must be a valid pointer to an AMobjItems
#[no_mangle]
pub unsafe extern "C" fn AMobjItemsPrev(obj_items: *mut AMobjItems, n: isize) -> *const AMobjItem {
if let Some(obj_items) = obj_items.as_mut() {
if let Some(obj_item) = obj_items.prev(n) {
return obj_item;
}
}
std::ptr::null()
}
/// \memberof AMobjItems
/// \brief Gets the size of the sequence of object items underlying an
/// iterator.
///
/// \param[in] obj_items A pointer to an `AMobjItems` struct.
/// \return The count of values in \p obj_items.
/// \pre \p obj_items `!= NULL`.
/// \internal
///
/// #Safety
/// obj_items must be a valid pointer to an AMobjItems
#[no_mangle]
pub unsafe extern "C" fn AMobjItemsSize(obj_items: *const AMobjItems) -> usize {
if let Some(obj_items) = obj_items.as_ref() {
obj_items.len()
} else {
0
}
}
/// \memberof AMobjItems
/// \brief Creates an iterator over the same sequence of object items as the
/// given one but with the opposite position and direction.
///
/// \param[in] obj_items A pointer to an `AMobjItems` struct.
/// \return An `AMobjItems` struct
/// \pre \p obj_items `!= NULL`.
/// \internal
///
/// #Safety
/// obj_items must be a valid pointer to an AMobjItems
#[no_mangle]
pub unsafe extern "C" fn AMobjItemsReversed(obj_items: *const AMobjItems) -> AMobjItems {
if let Some(obj_items) = obj_items.as_ref() {
obj_items.reversed()
} else {
AMobjItems::default()
}
}
/// \memberof AMobjItems
/// \brief Creates an iterator at the starting position over the same sequence
/// of object items as the given one.
///
/// \param[in] obj_items A pointer to an `AMobjItems` struct.
/// \return An `AMobjItems` struct
/// \pre \p obj_items `!= NULL`.
/// \internal
///
/// #Safety
/// obj_items must be a valid pointer to an AMobjItems
#[no_mangle]
pub unsafe extern "C" fn AMobjItemsRewound(obj_items: *const AMobjItems) -> AMobjItems {
if let Some(obj_items) = obj_items.as_ref() {
obj_items.rewound()
} else {
AMobjItems::default()
}
}

View file

@ -1,914 +0,0 @@
use automerge as am;
use libc::strcmp;
use smol_str::SmolStr;
use std::any::type_name;
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::ffi::CString;
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
use std::os::raw::c_char;
use crate::actor_id::AMactorId;
use crate::byte_span::AMbyteSpan;
use crate::change::AMchange;
use crate::change_hashes::AMchangeHashes;
use crate::changes::AMchanges;
use crate::doc::list::{item::AMlistItem, items::AMlistItems};
use crate::doc::map::{item::AMmapItem, items::AMmapItems};
use crate::doc::utils::to_str;
use crate::doc::AMdoc;
use crate::obj::item::AMobjItem;
use crate::obj::items::AMobjItems;
use crate::obj::AMobjId;
use crate::strs::AMstrs;
use crate::sync::{AMsyncMessage, AMsyncState};
/// \struct AMvalue
/// \installed_headerfile
/// \brief A discriminated union of value type variants for a result.
///
/// \enum AMvalueVariant
/// \brief A value type discriminant.
///
/// \var AMvalue::actor_id
/// An actor identifier as a pointer to an `AMactorId` struct.
///
/// \var AMvalue::boolean
/// A boolean.
///
/// \var AMvalue::bytes
/// A sequence of bytes as an `AMbyteSpan` struct.
///
/// \var AMvalue::change_hashes
/// A sequence of change hashes as an `AMchangeHashes` struct.
///
/// \var AMvalue::changes
/// A sequence of changes as an `AMchanges` struct.
///
/// \var AMvalue::counter
/// A CRDT counter.
///
/// \var AMvalue::doc
/// A document as a pointer to an `AMdoc` struct.
///
/// \var AMvalue::f64
/// A 64-bit float.
///
/// \var AMvalue::int_
/// A 64-bit signed integer.
///
/// \var AMvalue::list_items
/// A sequence of list object items as an `AMlistItems` struct.
///
/// \var AMvalue::map_items
/// A sequence of map object items as an `AMmapItems` struct.
///
/// \var AMvalue::obj_id
/// An object identifier as a pointer to an `AMobjId` struct.
///
/// \var AMvalue::obj_items
/// A sequence of object items as an `AMobjItems` struct.
///
/// \var AMvalue::str
/// A UTF-8 string.
///
/// \var AMvalue::strs
/// A sequence of UTF-8 strings as an `AMstrs` struct.
///
/// \var AMvalue::sync_message
/// A synchronization message as a pointer to an `AMsyncMessage` struct.
///
/// \var AMvalue::sync_state
/// A synchronization state as a pointer to an `AMsyncState` struct.
///
/// \var AMvalue::tag
/// The variant discriminator.
///
/// \var AMvalue::timestamp
/// A Lamport timestamp.
///
/// \var AMvalue::uint
/// A 64-bit unsigned integer.
///
/// \var AMvalue::unknown
/// A value of unknown type as an `AMunknownValue` struct.
#[repr(u8)]
pub enum AMvalue<'a> {
/// A void variant.
/// \note This tag is unalphabetized so that a zeroed struct will have it.
Void,
/// An actor identifier variant.
ActorId(&'a AMactorId),
/// A boolean variant.
Boolean(bool),
/// A byte array variant.
Bytes(AMbyteSpan),
/// A change hashes variant.
ChangeHashes(AMchangeHashes),
/// A changes variant.
Changes(AMchanges),
/// A CRDT counter variant.
Counter(i64),
/// A document variant.
Doc(*mut AMdoc),
/// A 64-bit float variant.
F64(f64),
/// A 64-bit signed integer variant.
Int(i64),
/// A list items variant.
ListItems(AMlistItems),
/// A map items variant.
MapItems(AMmapItems),
/// A null variant.
Null,
/// An object identifier variant.
ObjId(&'a AMobjId),
/// An object items variant.
ObjItems(AMobjItems),
/// A UTF-8 string variant.
Str(*const libc::c_char),
/// A UTF-8 strings variant.
Strs(AMstrs),
/// A synchronization message variant.
SyncMessage(&'a AMsyncMessage),
/// A synchronization state variant.
SyncState(&'a mut AMsyncState),
/// A Lamport timestamp variant.
Timestamp(i64),
/// A 64-bit unsigned integer variant.
Uint(u64),
/// An unknown type of scalar value variant.
Unknown(AMunknownValue),
}
impl<'a> PartialEq for AMvalue<'a> {
fn eq(&self, other: &Self) -> bool {
use AMvalue::*;
match (self, other) {
(ActorId(lhs), ActorId(rhs)) => *lhs == *rhs,
(Boolean(lhs), Boolean(rhs)) => lhs == rhs,
(Bytes(lhs), Bytes(rhs)) => lhs == rhs,
(ChangeHashes(lhs), ChangeHashes(rhs)) => lhs == rhs,
(Changes(lhs), Changes(rhs)) => lhs == rhs,
(Counter(lhs), Counter(rhs)) => lhs == rhs,
(Doc(lhs), Doc(rhs)) => *lhs == *rhs,
(F64(lhs), F64(rhs)) => lhs == rhs,
(Int(lhs), Int(rhs)) => lhs == rhs,
(ListItems(lhs), ListItems(rhs)) => lhs == rhs,
(MapItems(lhs), MapItems(rhs)) => lhs == rhs,
(ObjId(lhs), ObjId(rhs)) => *lhs == *rhs,
(ObjItems(lhs), ObjItems(rhs)) => lhs == rhs,
(Str(lhs), Str(rhs)) => unsafe { strcmp(*lhs, *rhs) == 0 },
(Strs(lhs), Strs(rhs)) => lhs == rhs,
(SyncMessage(lhs), SyncMessage(rhs)) => *lhs == *rhs,
(SyncState(lhs), SyncState(rhs)) => *lhs == *rhs,
(Timestamp(lhs), Timestamp(rhs)) => lhs == rhs,
(Uint(lhs), Uint(rhs)) => lhs == rhs,
(Unknown(lhs), Unknown(rhs)) => lhs == rhs,
(Null, Null) | (Void, Void) => true,
_ => false,
}
}
}
impl From<(&am::Value<'_>, &RefCell<Option<CString>>)> for AMvalue<'_> {
fn from((value, c_str): (&am::Value<'_>, &RefCell<Option<CString>>)) -> Self {
match value {
am::Value::Scalar(scalar) => match scalar.as_ref() {
am::ScalarValue::Boolean(flag) => AMvalue::Boolean(*flag),
am::ScalarValue::Bytes(bytes) => AMvalue::Bytes(bytes.as_slice().into()),
am::ScalarValue::Counter(counter) => AMvalue::Counter(counter.into()),
am::ScalarValue::F64(float) => AMvalue::F64(*float),
am::ScalarValue::Int(int) => AMvalue::Int(*int),
am::ScalarValue::Null => AMvalue::Null,
am::ScalarValue::Str(smol_str) => {
let mut c_str = c_str.borrow_mut();
AMvalue::Str(match c_str.as_mut() {
None => {
let value_str = CString::new(smol_str.to_string()).unwrap();
c_str.insert(value_str).as_ptr()
}
Some(value_str) => value_str.as_ptr(),
})
}
am::ScalarValue::Timestamp(timestamp) => AMvalue::Timestamp(*timestamp),
am::ScalarValue::Uint(uint) => AMvalue::Uint(*uint),
am::ScalarValue::Unknown { bytes, type_code } => AMvalue::Unknown(AMunknownValue {
bytes: bytes.as_slice().into(),
type_code: *type_code,
}),
},
// \todo Confirm that an object variant should be ignored
// when there's no object ID variant.
am::Value::Object(_) => AMvalue::Void,
}
}
}
impl From<&AMvalue<'_>> for u8 {
fn from(value: &AMvalue) -> Self {
use AMvalue::*;
// \warning These numbers must correspond to the order in which the
// variants of an AMvalue are declared within it.
match value {
ActorId(_) => 1,
Boolean(_) => 2,
Bytes(_) => 3,
ChangeHashes(_) => 4,
Changes(_) => 5,
Counter(_) => 6,
Doc(_) => 7,
F64(_) => 8,
Int(_) => 9,
ListItems(_) => 10,
MapItems(_) => 11,
Null => 12,
ObjId(_) => 13,
ObjItems(_) => 14,
Str(_) => 15,
Strs(_) => 16,
SyncMessage(_) => 17,
SyncState(_) => 18,
Timestamp(_) => 19,
Uint(_) => 20,
Unknown(..) => 21,
Void => 0,
}
}
}
impl TryFrom<&AMvalue<'_>> for am::ScalarValue {
type Error = am::AutomergeError;
fn try_from(c_value: &AMvalue) -> Result<Self, Self::Error> {
use am::AutomergeError::InvalidValueType;
use AMvalue::*;
let expected = type_name::<am::ScalarValue>().to_string();
match c_value {
Boolean(b) => Ok(am::ScalarValue::Boolean(*b)),
Bytes(span) => {
let slice = unsafe { std::slice::from_raw_parts(span.src, span.count) };
Ok(am::ScalarValue::Bytes(slice.to_vec()))
}
Counter(c) => Ok(am::ScalarValue::Counter(c.into())),
F64(f) => Ok(am::ScalarValue::F64(*f)),
Int(i) => Ok(am::ScalarValue::Int(*i)),
Str(c_str) => {
let smol_str = unsafe { SmolStr::new(to_str(*c_str)) };
Ok(am::ScalarValue::Str(smol_str))
}
Timestamp(t) => Ok(am::ScalarValue::Timestamp(*t)),
Uint(u) => Ok(am::ScalarValue::Uint(*u)),
Null => Ok(am::ScalarValue::Null),
Unknown(AMunknownValue { bytes, type_code }) => {
let slice = unsafe { std::slice::from_raw_parts(bytes.src, bytes.count) };
Ok(am::ScalarValue::Unknown {
bytes: slice.to_vec(),
type_code: *type_code,
})
}
ActorId(_) => Err(InvalidValueType {
expected,
unexpected: type_name::<AMactorId>().to_string(),
}),
ChangeHashes(_) => Err(InvalidValueType {
expected,
unexpected: type_name::<AMchangeHashes>().to_string(),
}),
Changes(_) => Err(InvalidValueType {
expected,
unexpected: type_name::<AMchanges>().to_string(),
}),
Doc(_) => Err(InvalidValueType {
expected,
unexpected: type_name::<AMdoc>().to_string(),
}),
ListItems(_) => Err(InvalidValueType {
expected,
unexpected: type_name::<AMlistItems>().to_string(),
}),
MapItems(_) => Err(InvalidValueType {
expected,
unexpected: type_name::<AMmapItems>().to_string(),
}),
ObjId(_) => Err(InvalidValueType {
expected,
unexpected: type_name::<AMobjId>().to_string(),
}),
ObjItems(_) => Err(InvalidValueType {
expected,
unexpected: type_name::<AMobjItems>().to_string(),
}),
Strs(_) => Err(InvalidValueType {
expected,
unexpected: type_name::<AMstrs>().to_string(),
}),
SyncMessage(_) => Err(InvalidValueType {
expected,
unexpected: type_name::<AMsyncMessage>().to_string(),
}),
SyncState(_) => Err(InvalidValueType {
expected,
unexpected: type_name::<AMsyncState>().to_string(),
}),
Void => Err(InvalidValueType {
expected,
unexpected: type_name::<()>().to_string(),
}),
}
}
}
/// \memberof AMvalue
/// \brief Tests the equality of two values.
///
/// \param[in] value1 A pointer to an `AMvalue` struct.
/// \param[in] value2 A pointer to an `AMvalue` struct.
/// \return `true` if \p value1 `==` \p value2 and `false` otherwise.
/// \pre \p value1 `!= NULL`.
/// \pre \p value2 `!= NULL`.
/// \internal
///
/// #Safety
/// value1 must be a valid AMvalue pointer
/// value2 must be a valid AMvalue pointer
#[no_mangle]
pub unsafe extern "C" fn AMvalueEqual(value1: *const AMvalue, value2: *const AMvalue) -> bool {
match (value1.as_ref(), value2.as_ref()) {
(Some(value1), Some(value2)) => *value1 == *value2,
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \struct AMresult
/// \installed_headerfile
/// \brief A discriminated union of result variants.
pub enum AMresult {
ActorId(am::ActorId, Option<AMactorId>),
ChangeHashes(Vec<am::ChangeHash>),
Changes(Vec<am::Change>, Option<BTreeMap<usize, AMchange>>),
Doc(Box<AMdoc>),
Error(CString),
ListItems(Vec<AMlistItem>),
MapItems(Vec<AMmapItem>),
ObjId(AMobjId),
ObjItems(Vec<AMobjItem>),
String(CString),
Strings(Vec<CString>),
SyncMessage(AMsyncMessage),
SyncState(Box<AMsyncState>),
Value(am::Value<'static>, RefCell<Option<CString>>),
Void,
}
impl AMresult {
pub(crate) fn err(s: &str) -> Self {
AMresult::Error(CString::new(s).unwrap())
}
}
impl From<am::AutoCommit> for AMresult {
fn from(auto_commit: am::AutoCommit) -> Self {
AMresult::Doc(Box::new(AMdoc::new(auto_commit)))
}
}
impl From<am::ChangeHash> for AMresult {
fn from(change_hash: am::ChangeHash) -> Self {
AMresult::ChangeHashes(vec![change_hash])
}
}
impl From<am::Keys<'_, '_>> for AMresult {
fn from(keys: am::Keys<'_, '_>) -> Self {
let cstrings: Vec<CString> = keys.map(|s| CString::new(s).unwrap()).collect();
AMresult::Strings(cstrings)
}
}
impl From<am::KeysAt<'_, '_>> for AMresult {
fn from(keys: am::KeysAt<'_, '_>) -> Self {
let cstrings: Vec<CString> = keys.map(|s| CString::new(s).unwrap()).collect();
AMresult::Strings(cstrings)
}
}
impl From<am::ListRange<'static, Range<usize>>> for AMresult {
fn from(list_range: am::ListRange<'static, Range<usize>>) -> Self {
AMresult::ListItems(
list_range
.map(|(i, v, o)| AMlistItem::new(i, v.clone(), o))
.collect(),
)
}
}
impl From<am::ListRangeAt<'static, Range<usize>>> for AMresult {
fn from(list_range: am::ListRangeAt<'static, Range<usize>>) -> Self {
AMresult::ListItems(
list_range
.map(|(i, v, o)| AMlistItem::new(i, v.clone(), o))
.collect(),
)
}
}
impl From<am::MapRange<'static, Range<String>>> for AMresult {
fn from(map_range: am::MapRange<'static, Range<String>>) -> Self {
let map_items: Vec<AMmapItem> = map_range
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
.collect();
AMresult::MapItems(map_items)
}
}
impl From<am::MapRangeAt<'static, Range<String>>> for AMresult {
fn from(map_range: am::MapRangeAt<'static, Range<String>>) -> Self {
let map_items: Vec<AMmapItem> = map_range
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
.collect();
AMresult::MapItems(map_items)
}
}
impl From<am::MapRange<'static, RangeFrom<String>>> for AMresult {
fn from(map_range: am::MapRange<'static, RangeFrom<String>>) -> Self {
let map_items: Vec<AMmapItem> = map_range
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
.collect();
AMresult::MapItems(map_items)
}
}
impl From<am::MapRangeAt<'static, RangeFrom<String>>> for AMresult {
fn from(map_range: am::MapRangeAt<'static, RangeFrom<String>>) -> Self {
let map_items: Vec<AMmapItem> = map_range
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
.collect();
AMresult::MapItems(map_items)
}
}
impl From<am::MapRange<'static, RangeFull>> for AMresult {
fn from(map_range: am::MapRange<'static, RangeFull>) -> Self {
let map_items: Vec<AMmapItem> = map_range
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
.collect();
AMresult::MapItems(map_items)
}
}
impl From<am::MapRangeAt<'static, RangeFull>> for AMresult {
fn from(map_range: am::MapRangeAt<'static, RangeFull>) -> Self {
let map_items: Vec<AMmapItem> = map_range
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
.collect();
AMresult::MapItems(map_items)
}
}
impl From<am::MapRange<'static, RangeTo<String>>> for AMresult {
fn from(map_range: am::MapRange<'static, RangeTo<String>>) -> Self {
let map_items: Vec<AMmapItem> = map_range
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
.collect();
AMresult::MapItems(map_items)
}
}
impl From<am::MapRangeAt<'static, RangeTo<String>>> for AMresult {
fn from(map_range: am::MapRangeAt<'static, RangeTo<String>>) -> Self {
let map_items: Vec<AMmapItem> = map_range
.map(|(k, v, o): (&'_ str, am::Value<'_>, am::ObjId)| AMmapItem::new(k, v.clone(), o))
.collect();
AMresult::MapItems(map_items)
}
}
impl From<am::sync::State> for AMresult {
fn from(state: am::sync::State) -> Self {
AMresult::SyncState(Box::new(AMsyncState::new(state)))
}
}
impl From<am::Values<'static>> for AMresult {
fn from(pairs: am::Values<'static>) -> Self {
AMresult::ObjItems(pairs.map(|(v, o)| AMobjItem::new(v.clone(), o)).collect())
}
}
impl From<Result<Vec<(am::Value<'static>, am::ObjId)>, am::AutomergeError>> for AMresult {
fn from(maybe: Result<Vec<(am::Value<'static>, am::ObjId)>, am::AutomergeError>) -> Self {
match maybe {
Ok(pairs) => AMresult::ObjItems(
pairs
.into_iter()
.map(|(v, o)| AMobjItem::new(v, o))
.collect(),
),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<AMresult> for *mut AMresult {
fn from(b: AMresult) -> Self {
Box::into_raw(Box::new(b))
}
}
impl From<Option<&am::Change>> for AMresult {
fn from(maybe: Option<&am::Change>) -> Self {
match maybe {
Some(change) => AMresult::Changes(vec![change.clone()], None),
None => AMresult::Void,
}
}
}
impl From<Option<am::sync::Message>> for AMresult {
fn from(maybe: Option<am::sync::Message>) -> Self {
match maybe {
Some(message) => AMresult::SyncMessage(AMsyncMessage::new(message)),
None => AMresult::Void,
}
}
}
impl From<Result<(), am::AutomergeError>> for AMresult {
fn from(maybe: Result<(), am::AutomergeError>) -> Self {
match maybe {
Ok(()) => AMresult::Void,
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<am::ActorId, am::AutomergeError>> for AMresult {
fn from(maybe: Result<am::ActorId, am::AutomergeError>) -> Self {
match maybe {
Ok(actor_id) => AMresult::ActorId(actor_id, None),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<am::ActorId, am::InvalidActorId>> for AMresult {
fn from(maybe: Result<am::ActorId, am::InvalidActorId>) -> Self {
match maybe {
Ok(actor_id) => AMresult::ActorId(actor_id, None),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<am::AutoCommit, am::AutomergeError>> for AMresult {
fn from(maybe: Result<am::AutoCommit, am::AutomergeError>) -> Self {
match maybe {
Ok(auto_commit) => AMresult::Doc(Box::new(AMdoc::new(auto_commit))),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<am::Change, am::LoadChangeError>> for AMresult {
fn from(maybe: Result<am::Change, am::LoadChangeError>) -> Self {
match maybe {
Ok(change) => AMresult::Changes(vec![change], None),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<am::ObjId, am::AutomergeError>> for AMresult {
fn from(maybe: Result<am::ObjId, am::AutomergeError>) -> Self {
match maybe {
Ok(obj_id) => AMresult::ObjId(AMobjId::new(obj_id)),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<am::sync::Message, am::sync::ReadMessageError>> for AMresult {
fn from(maybe: Result<am::sync::Message, am::sync::ReadMessageError>) -> Self {
match maybe {
Ok(message) => AMresult::SyncMessage(AMsyncMessage::new(message)),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<am::sync::State, am::sync::DecodeStateError>> for AMresult {
fn from(maybe: Result<am::sync::State, am::sync::DecodeStateError>) -> Self {
match maybe {
Ok(state) => AMresult::SyncState(Box::new(AMsyncState::new(state))),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<am::Value<'static>, am::AutomergeError>> for AMresult {
fn from(maybe: Result<am::Value<'static>, am::AutomergeError>) -> Self {
match maybe {
Ok(value) => AMresult::Value(value, Default::default()),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<Option<(am::Value<'static>, am::ObjId)>, am::AutomergeError>> for AMresult {
fn from(maybe: Result<Option<(am::Value<'static>, am::ObjId)>, am::AutomergeError>) -> Self {
match maybe {
Ok(Some((value, obj_id))) => match value {
am::Value::Object(_) => AMresult::ObjId(AMobjId::new(obj_id)),
_ => AMresult::Value(value, Default::default()),
},
Ok(None) => AMresult::Void,
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<String, am::AutomergeError>> for AMresult {
fn from(maybe: Result<String, am::AutomergeError>) -> Self {
match maybe {
Ok(string) => AMresult::String(CString::new(string).unwrap()),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<usize, am::AutomergeError>> for AMresult {
fn from(maybe: Result<usize, am::AutomergeError>) -> Self {
match maybe {
Ok(size) => AMresult::Value(am::Value::uint(size as u64), Default::default()),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<Vec<am::Change>, am::AutomergeError>> for AMresult {
fn from(maybe: Result<Vec<am::Change>, am::AutomergeError>) -> Self {
match maybe {
Ok(changes) => AMresult::Changes(changes, None),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<Vec<am::Change>, am::LoadChangeError>> for AMresult {
fn from(maybe: Result<Vec<am::Change>, am::LoadChangeError>) -> Self {
match maybe {
Ok(changes) => AMresult::Changes(changes, None),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<Vec<&am::Change>, am::AutomergeError>> for AMresult {
fn from(maybe: Result<Vec<&am::Change>, am::AutomergeError>) -> Self {
match maybe {
Ok(changes) => {
let changes: Vec<am::Change> =
changes.iter().map(|&change| change.clone()).collect();
AMresult::Changes(changes, None)
}
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<Vec<am::ChangeHash>, am::AutomergeError>> for AMresult {
fn from(maybe: Result<Vec<am::ChangeHash>, am::AutomergeError>) -> Self {
match maybe {
Ok(change_hashes) => AMresult::ChangeHashes(change_hashes),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<Vec<am::ChangeHash>, am::InvalidChangeHashSlice>> for AMresult {
fn from(maybe: Result<Vec<am::ChangeHash>, am::InvalidChangeHashSlice>) -> Self {
match maybe {
Ok(change_hashes) => AMresult::ChangeHashes(change_hashes),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Result<Vec<u8>, am::AutomergeError>> for AMresult {
fn from(maybe: Result<Vec<u8>, am::AutomergeError>) -> Self {
match maybe {
Ok(bytes) => AMresult::Value(am::Value::bytes(bytes), Default::default()),
Err(e) => AMresult::err(&e.to_string()),
}
}
}
impl From<Vec<&am::Change>> for AMresult {
fn from(changes: Vec<&am::Change>) -> Self {
let changes: Vec<am::Change> = changes.iter().map(|&change| change.clone()).collect();
AMresult::Changes(changes, None)
}
}
impl From<Vec<am::ChangeHash>> for AMresult {
fn from(change_hashes: Vec<am::ChangeHash>) -> Self {
AMresult::ChangeHashes(change_hashes)
}
}
impl From<Vec<u8>> for AMresult {
fn from(bytes: Vec<u8>) -> Self {
AMresult::Value(am::Value::bytes(bytes), Default::default())
}
}
pub fn to_result<R: Into<AMresult>>(r: R) -> *mut AMresult {
(r.into()).into()
}
/// \ingroup enumerations
/// \enum AMstatus
/// \brief The status of an API call.
#[derive(Debug)]
#[repr(u8)]
pub enum AMstatus {
/// Success.
/// \note This tag is unalphabetized so that `0` indicates success.
Ok,
/// Failure due to an error.
Error,
/// Failure due to an invalid result.
InvalidResult,
}
/// \memberof AMresult
/// \brief Gets a result's error message string.
///
/// \param[in] result A pointer to an `AMresult` struct.
/// \return A UTF-8 string value or `NULL`.
/// \pre \p result `!= NULL`.
/// \internal
///
/// # Safety
/// result must be a valid pointer to an AMresult
#[no_mangle]
pub unsafe extern "C" fn AMerrorMessage(result: *const AMresult) -> *const c_char {
match result.as_ref() {
Some(AMresult::Error(s)) => s.as_ptr(),
_ => std::ptr::null::<c_char>(),
}
}
/// \memberof AMresult
/// \brief Deallocates the storage for a result.
///
/// \param[in,out] result A pointer to an `AMresult` struct.
/// \pre \p result `!= NULL`.
/// \internal
///
/// # Safety
/// result must be a valid pointer to an AMresult
#[no_mangle]
pub unsafe extern "C" fn AMfree(result: *mut AMresult) {
if !result.is_null() {
let result: AMresult = *Box::from_raw(result);
drop(result)
}
}
/// \memberof AMresult
/// \brief Gets the size of a result's value.
///
/// \param[in] result A pointer to an `AMresult` struct.
/// \return The count of values in \p result.
/// \pre \p result `!= NULL`.
/// \internal
///
/// # Safety
/// result must be a valid pointer to an AMresult
#[no_mangle]
pub unsafe extern "C" fn AMresultSize(result: *const AMresult) -> usize {
if let Some(result) = result.as_ref() {
use AMresult::*;
match result {
Error(_) | Void => 0,
ActorId(_, _)
| Doc(_)
| ObjId(_)
| String(_)
| SyncMessage(_)
| SyncState(_)
| Value(_, _) => 1,
ChangeHashes(change_hashes) => change_hashes.len(),
Changes(changes, _) => changes.len(),
ListItems(list_items) => list_items.len(),
MapItems(map_items) => map_items.len(),
ObjItems(obj_items) => obj_items.len(),
Strings(cstrings) => cstrings.len(),
}
} else {
0
}
}
/// \memberof AMresult
/// \brief Gets the status code of a result.
///
/// \param[in] result A pointer to an `AMresult` struct.
/// \return An `AMstatus` enum tag.
/// \pre \p result `!= NULL`.
/// \internal
///
/// # Safety
/// result must be a valid pointer to an AMresult
#[no_mangle]
pub unsafe extern "C" fn AMresultStatus(result: *const AMresult) -> AMstatus {
match result.as_ref() {
Some(AMresult::Error(_)) => AMstatus::Error,
None => AMstatus::InvalidResult,
_ => AMstatus::Ok,
}
}
/// \memberof AMresult
/// \brief Gets a result's value.
///
/// \param[in] result A pointer to an `AMresult` struct.
/// \return An `AMvalue` struct.
/// \pre \p result `!= NULL`.
/// \internal
///
/// # Safety
/// result must be a valid pointer to an AMresult
#[no_mangle]
pub unsafe extern "C" fn AMresultValue<'a>(result: *mut AMresult) -> AMvalue<'a> {
let mut content = AMvalue::Void;
if let Some(result) = result.as_mut() {
match result {
AMresult::ActorId(actor_id, c_actor_id) => match c_actor_id {
None => {
content = AMvalue::ActorId(&*c_actor_id.insert(AMactorId::new(&*actor_id)));
}
Some(c_actor_id) => {
content = AMvalue::ActorId(&*c_actor_id);
}
},
AMresult::ChangeHashes(change_hashes) => {
content = AMvalue::ChangeHashes(AMchangeHashes::new(change_hashes));
}
AMresult::Changes(changes, storage) => {
content = AMvalue::Changes(AMchanges::new(
changes,
storage.get_or_insert(BTreeMap::new()),
));
}
AMresult::Doc(doc) => content = AMvalue::Doc(&mut **doc),
AMresult::Error(_) => {}
AMresult::ListItems(list_items) => {
content = AMvalue::ListItems(AMlistItems::new(list_items));
}
AMresult::MapItems(map_items) => {
content = AMvalue::MapItems(AMmapItems::new(map_items));
}
AMresult::ObjId(obj_id) => {
content = AMvalue::ObjId(obj_id);
}
AMresult::ObjItems(obj_items) => {
content = AMvalue::ObjItems(AMobjItems::new(obj_items));
}
AMresult::String(cstring) => content = AMvalue::Str(cstring.as_ptr()),
AMresult::Strings(cstrings) => {
content = AMvalue::Strs(AMstrs::new(cstrings));
}
AMresult::SyncMessage(sync_message) => {
content = AMvalue::SyncMessage(sync_message);
}
AMresult::SyncState(sync_state) => {
content = AMvalue::SyncState(&mut *sync_state);
}
AMresult::Value(value, value_str) => {
content = (&*value, &*value_str).into();
}
AMresult::Void => {}
}
};
content
}
/// \struct AMunknownValue
/// \installed_headerfile
/// \brief A value (typically for a `set` operation) whose type is unknown.
///
#[derive(Eq, PartialEq)]
#[repr(C)]
pub struct AMunknownValue {
/// The value's raw bytes.
bytes: AMbyteSpan,
/// The value's encoded type identifier.
type_code: u8,
}

View file

@ -1,156 +0,0 @@
use crate::result::{AMfree, AMresult, AMresultStatus, AMresultValue, AMstatus, AMvalue};
/// \struct AMresultStack
/// \installed_headerfile
/// \brief A node in a singly-linked list of result pointers.
///
/// \note Using this data structure is purely optional because its only purpose
/// is to make memory management tolerable for direct usage of this API
/// in C, C++ and Objective-C.
#[repr(C)]
pub struct AMresultStack {
/// A result to be deallocated.
pub result: *mut AMresult,
/// The next node in the singly-linked list or `NULL`.
pub next: *mut AMresultStack,
}
impl AMresultStack {
pub fn new(result: *mut AMresult, next: *mut AMresultStack) -> Self {
Self { result, next }
}
}
/// \memberof AMresultStack
/// \brief Deallocates the storage for a stack of results.
///
/// \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
/// \return The number of `AMresult` structs freed.
/// \pre \p stack `!= NULL`.
/// \post `*stack == NULL`.
/// \note Calling this function is purely optional because its only purpose is
/// to make memory management tolerable for direct usage of this API in
/// C, C++ and Objective-C.
/// \internal
///
/// # Safety
/// stack must be a valid AMresultStack pointer pointer
#[no_mangle]
pub unsafe extern "C" fn AMfreeStack(stack: *mut *mut AMresultStack) -> usize {
if stack.is_null() {
return 0;
}
let mut count: usize = 0;
while !(*stack).is_null() {
AMfree(AMpop(stack));
count += 1;
}
count
}
/// \memberof AMresultStack
/// \brief Gets the topmost result from the stack after removing it.
///
/// \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
/// \return A pointer to an `AMresult` struct or `NULL`.
/// \pre \p stack `!= NULL`.
/// \post `*stack == NULL`.
/// \note Calling this function is purely optional because its only purpose is
/// to make memory management tolerable for direct usage of this API in
/// C, C++ and Objective-C.
/// \internal
///
/// # Safety
/// stack must be a valid AMresultStack pointer pointer
#[no_mangle]
pub unsafe extern "C" fn AMpop(stack: *mut *mut AMresultStack) -> *mut AMresult {
if stack.is_null() || (*stack).is_null() {
return std::ptr::null_mut();
}
let top = Box::from_raw(*stack);
*stack = top.next;
let result = top.result;
drop(top);
result
}
/// \memberof AMresultStack
/// \brief The prototype of a function to be called when a value matching the
/// given discriminant cannot be extracted from the result at the top of
/// the given stack.
///
/// \note Implementing this function is purely optional because its only purpose
/// is to make memory management tolerable for direct usage of this API
/// in C, C++ and Objective-C.
pub type AMpushCallback =
Option<extern "C" fn(stack: *mut *mut AMresultStack, discriminant: u8) -> ()>;
/// \memberof AMresultStack
/// \brief Pushes the given result onto the given stack and then either extracts
/// a value matching the given discriminant from that result or,
/// failing that, calls the given function and gets a void value instead.
///
/// \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
/// \param[in] result A pointer to an `AMresult` struct.
/// \param[in] discriminant An `AMvalue` variant's corresponding enum tag.
/// \param[in] callback A pointer to a function with the same signature as
/// `AMpushCallback()` or `NULL`.
/// \return An `AMvalue` struct.
/// \pre \p stack `!= NULL`.
/// \pre \p result `!= NULL`.
/// \warning If \p stack `== NULL` then \p result is deallocated in order to
/// prevent a memory leak.
/// \note Calling this function is purely optional because its only purpose is
/// to make memory management tolerable for direct usage of this API in
/// C, C++ and Objective-C.
/// \internal
///
/// # Safety
/// stack must be a valid AMresultStack pointer pointer
/// result must be a valid AMresult pointer
#[no_mangle]
pub unsafe extern "C" fn AMpush<'a>(
stack: *mut *mut AMresultStack,
result: *mut AMresult,
discriminant: u8,
callback: AMpushCallback,
) -> AMvalue<'a> {
if stack.is_null() {
// There's no stack to push the result onto so it has to be freed in
// order to prevent a memory leak.
AMfree(result);
if let Some(callback) = callback {
callback(stack, discriminant);
}
return AMvalue::Void;
} else if result.is_null() {
if let Some(callback) = callback {
callback(stack, discriminant);
}
return AMvalue::Void;
}
// Always push the result onto the stack, even if it's wrong, so that the
// given callback can retrieve it.
let node = Box::new(AMresultStack::new(result, *stack));
let top = Box::into_raw(node);
*stack = top;
// Test that the result contains a value.
match AMresultStatus(result) {
AMstatus::Ok => {}
_ => {
if let Some(callback) = callback {
callback(stack, discriminant);
}
return AMvalue::Void;
}
}
// Test that the result's value matches the given discriminant.
let value = AMresultValue(result);
if discriminant != u8::from(&value) {
if let Some(callback) = callback {
callback(stack, discriminant);
}
return AMvalue::Void;
}
value
}

View file

@ -1,344 +0,0 @@
use std::cmp::Ordering;
use std::ffi::{c_void, CString};
use std::mem::size_of;
use std::os::raw::c_char;
#[repr(C)]
struct Detail {
len: usize,
offset: isize,
ptr: *const c_void,
}
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
/// propagate the name of a constant initialized from it so if the
/// constant's name is a symbolic representation of the value it can be
/// converted into a number by post-processing the header it generated.
pub const USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
impl Detail {
fn new(c_strings: &[CString], offset: isize) -> Self {
Self {
len: c_strings.len(),
offset,
ptr: c_strings.as_ptr() as *const c_void,
}
}
pub fn advance(&mut self, n: isize) {
if n == 0 {
return;
}
let len = self.len as isize;
self.offset = if self.offset < 0 {
// It's reversed.
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
if unclipped >= 0 {
// Clip it to the forward stop.
len
} else {
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
}
} else {
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
if unclipped < 0 {
// Clip it to the reverse stop.
-(len + 1)
} else {
std::cmp::max(0, std::cmp::min(unclipped, len))
}
}
}
pub fn get_index(&self) -> usize {
(self.offset
+ if self.offset < 0 {
self.len as isize
} else {
0
}) as usize
}
pub fn next(&mut self, n: isize) -> Option<*const c_char> {
if self.is_stopped() {
return None;
}
let slice: &[CString] =
unsafe { std::slice::from_raw_parts(self.ptr as *const CString, self.len) };
let value = slice[self.get_index()].as_ptr();
self.advance(n);
Some(value)
}
pub fn is_stopped(&self) -> bool {
let len = self.len as isize;
self.offset < -len || self.offset == len
}
pub fn prev(&mut self, n: isize) -> Option<*const c_char> {
self.advance(-n);
if self.is_stopped() {
return None;
}
let slice: &[CString] =
unsafe { std::slice::from_raw_parts(self.ptr as *const CString, self.len) };
Some(slice[self.get_index()].as_ptr())
}
pub fn reversed(&self) -> Self {
Self {
len: self.len,
offset: -(self.offset + 1),
ptr: self.ptr,
}
}
pub fn rewound(&self) -> Self {
Self {
len: self.len,
offset: if self.offset < 0 { -1 } else { 0 },
ptr: self.ptr,
}
}
}
impl From<Detail> for [u8; USIZE_USIZE_USIZE_] {
fn from(detail: Detail) -> Self {
unsafe {
std::slice::from_raw_parts((&detail as *const Detail) as *const u8, USIZE_USIZE_USIZE_)
.try_into()
.unwrap()
}
}
}
/// \struct AMstrs
/// \installed_headerfile
/// \brief A random-access iterator over a sequence of UTF-8 strings.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMstrs {
/// An implementation detail that is intentionally opaque.
/// \warning Modifying \p detail will cause undefined behavior.
/// \note The actual size of \p detail will vary by platform, this is just
/// the one for the platform this documentation was built on.
detail: [u8; USIZE_USIZE_USIZE_],
}
impl AMstrs {
pub fn new(c_strings: &[CString]) -> Self {
Self {
detail: Detail::new(c_strings, 0).into(),
}
}
pub fn advance(&mut self, n: isize) {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.advance(n);
}
pub fn len(&self) -> usize {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
detail.len
}
pub fn next(&mut self, n: isize) -> Option<*const c_char> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<*const c_char> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.prev(n)
}
pub fn reversed(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.reversed().into(),
}
}
pub fn rewound(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.rewound().into(),
}
}
}
impl AsRef<[CString]> for AMstrs {
fn as_ref(&self) -> &[CString] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const CString, detail.len) }
}
}
impl Default for AMstrs {
fn default() -> Self {
Self {
detail: [0; USIZE_USIZE_USIZE_],
}
}
}
/// \memberof AMstrs
/// \brief Advances an iterator over a sequence of UTF-8 strings by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in,out] strs A pointer to an `AMstrs` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \pre \p strs `!= NULL`.
/// \internal
///
/// #Safety
/// strs must be a valid pointer to an AMstrs
#[no_mangle]
pub unsafe extern "C" fn AMstrsAdvance(strs: *mut AMstrs, n: isize) {
if let Some(strs) = strs.as_mut() {
strs.advance(n);
};
}
/// \memberof AMstrs
/// \brief Compares the sequences of UTF-8 strings underlying a pair of
/// iterators.
///
/// \param[in] strs1 A pointer to an `AMstrs` struct.
/// \param[in] strs2 A pointer to an `AMstrs` struct.
/// \return `-1` if \p strs1 `<` \p strs2, `0` if
/// \p strs1 `==` \p strs2 and `1` if
/// \p strs1 `>` \p strs2.
/// \pre \p strs1 `!= NULL`.
/// \pre \p strs2 `!= NULL`.
/// \internal
///
/// #Safety
/// strs1 must be a valid pointer to an AMstrs
/// strs2 must be a valid pointer to an AMstrs
#[no_mangle]
pub unsafe extern "C" fn AMstrsCmp(strs1: *const AMstrs, strs2: *const AMstrs) -> isize {
match (strs1.as_ref(), strs2.as_ref()) {
(Some(strs1), Some(strs2)) => match strs1.as_ref().cmp(strs2.as_ref()) {
Ordering::Less => -1,
Ordering::Equal => 0,
Ordering::Greater => 1,
},
(None, Some(_)) => -1,
(Some(_), None) => 1,
(None, None) => 0,
}
}
/// \memberof AMstrs
/// \brief Gets the key at the current position of an iterator over a sequence
/// of UTF-8 strings and then advances it by at most \p |n| positions
/// where the sign of \p n is relative to the iterator's direction.
///
/// \param[in,out] strs A pointer to an `AMstrs` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A UTF-8 string that's `NULL` when \p strs was previously advanced
/// past its forward/reverse limit.
/// \pre \p strs `!= NULL`.
/// \internal
///
/// #Safety
/// strs must be a valid pointer to an AMstrs
#[no_mangle]
pub unsafe extern "C" fn AMstrsNext(strs: *mut AMstrs, n: isize) -> *const c_char {
if let Some(strs) = strs.as_mut() {
if let Some(key) = strs.next(n) {
return key;
}
}
std::ptr::null()
}
/// \memberof AMstrs
/// \brief Advances an iterator over a sequence of UTF-8 strings by at most
/// \p |n| positions where the sign of \p n is relative to the
/// iterator's direction and then gets the key at its new position.
///
/// \param[in,out] strs A pointer to an `AMstrs` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A UTF-8 string that's `NULL` when \p strs is presently advanced
/// past its forward/reverse limit.
/// \pre \p strs `!= NULL`.
/// \internal
///
/// #Safety
/// strs must be a valid pointer to an AMstrs
#[no_mangle]
pub unsafe extern "C" fn AMstrsPrev(strs: *mut AMstrs, n: isize) -> *const c_char {
if let Some(strs) = strs.as_mut() {
if let Some(key) = strs.prev(n) {
return key;
}
}
std::ptr::null()
}
/// \memberof AMstrs
/// \brief Gets the size of the sequence of UTF-8 strings underlying an
/// iterator.
///
/// \param[in] strs A pointer to an `AMstrs` struct.
/// \return The count of values in \p strs.
/// \pre \p strs `!= NULL`.
/// \internal
///
/// #Safety
/// strs must be a valid pointer to an AMstrs
#[no_mangle]
pub unsafe extern "C" fn AMstrsSize(strs: *const AMstrs) -> usize {
if let Some(strs) = strs.as_ref() {
strs.len()
} else {
0
}
}
/// \memberof AMstrs
/// \brief Creates an iterator over the same sequence of UTF-8 strings as the
/// given one but with the opposite position and direction.
///
/// \param[in] strs A pointer to an `AMstrs` struct.
/// \return An `AMstrs` struct.
/// \pre \p strs `!= NULL`.
/// \internal
///
/// #Safety
/// strs must be a valid pointer to an AMstrs
#[no_mangle]
pub unsafe extern "C" fn AMstrsReversed(strs: *const AMstrs) -> AMstrs {
if let Some(strs) = strs.as_ref() {
strs.reversed()
} else {
AMstrs::default()
}
}
/// \memberof AMstrs
/// \brief Creates an iterator at the starting position over the same sequence
/// of UTF-8 strings as the given one.
///
/// \param[in] strs A pointer to an `AMstrs` struct.
/// \return An `AMstrs` struct
/// \pre \p strs `!= NULL`.
/// \internal
///
/// #Safety
/// strs must be a valid pointer to an AMstrs
#[no_mangle]
pub unsafe extern "C" fn AMstrsRewound(strs: *const AMstrs) -> AMstrs {
if let Some(strs) = strs.as_ref() {
strs.rewound()
} else {
AMstrs::default()
}
}

View file

@ -1,378 +0,0 @@
use automerge as am;
use std::collections::BTreeMap;
use std::ffi::c_void;
use std::mem::size_of;
use crate::sync::have::AMsyncHave;
#[repr(C)]
struct Detail {
len: usize,
offset: isize,
ptr: *const c_void,
storage: *mut c_void,
}
/// \note cbindgen won't propagate the value of a `std::mem::size_of<T>()` call
/// (https://github.com/eqrion/cbindgen/issues/252) but it will
/// propagate the name of a constant initialized from it so if the
/// constant's name is a symbolic representation of the value it can be
/// converted into a number by post-processing the header it generated.
pub const USIZE_USIZE_USIZE_USIZE_: usize = size_of::<Detail>();
impl Detail {
fn new(
haves: &[am::sync::Have],
offset: isize,
storage: &mut BTreeMap<usize, AMsyncHave>,
) -> Self {
let storage: *mut BTreeMap<usize, AMsyncHave> = storage;
Self {
len: haves.len(),
offset,
ptr: haves.as_ptr() as *const c_void,
storage: storage as *mut c_void,
}
}
pub fn advance(&mut self, n: isize) {
if n == 0 {
return;
}
let len = self.len as isize;
self.offset = if self.offset < 0 {
// It's reversed.
let unclipped = self.offset.checked_sub(n).unwrap_or(isize::MIN);
if unclipped >= 0 {
// Clip it to the forward stop.
len
} else {
std::cmp::min(std::cmp::max(-(len + 1), unclipped), -1)
}
} else {
let unclipped = self.offset.checked_add(n).unwrap_or(isize::MAX);
if unclipped < 0 {
// Clip it to the reverse stop.
-(len + 1)
} else {
std::cmp::max(0, std::cmp::min(unclipped, len))
}
}
}
pub fn get_index(&self) -> usize {
(self.offset
+ if self.offset < 0 {
self.len as isize
} else {
0
}) as usize
}
pub fn next(&mut self, n: isize) -> Option<*const AMsyncHave> {
if self.is_stopped() {
return None;
}
let slice: &[am::sync::Have] =
unsafe { std::slice::from_raw_parts(self.ptr as *const am::sync::Have, self.len) };
let storage = unsafe { &mut *(self.storage as *mut BTreeMap<usize, AMsyncHave>) };
let index = self.get_index();
let value = match storage.get_mut(&index) {
Some(value) => value,
None => {
storage.insert(index, AMsyncHave::new(&slice[index]));
storage.get_mut(&index).unwrap()
}
};
self.advance(n);
Some(value)
}
pub fn is_stopped(&self) -> bool {
let len = self.len as isize;
self.offset < -len || self.offset == len
}
pub fn prev(&mut self, n: isize) -> Option<*const AMsyncHave> {
self.advance(-n);
if self.is_stopped() {
return None;
}
let slice: &[am::sync::Have] =
unsafe { std::slice::from_raw_parts(self.ptr as *const am::sync::Have, self.len) };
let storage = unsafe { &mut *(self.storage as *mut BTreeMap<usize, AMsyncHave>) };
let index = self.get_index();
Some(match storage.get_mut(&index) {
Some(value) => value,
None => {
storage.insert(index, AMsyncHave::new(&slice[index]));
storage.get_mut(&index).unwrap()
}
})
}
pub fn reversed(&self) -> Self {
Self {
len: self.len,
offset: -(self.offset + 1),
ptr: self.ptr,
storage: self.storage,
}
}
pub fn rewound(&self) -> Self {
Self {
len: self.len,
offset: if self.offset < 0 { -1 } else { 0 },
ptr: self.ptr,
storage: self.storage,
}
}
}
impl From<Detail> for [u8; USIZE_USIZE_USIZE_USIZE_] {
fn from(detail: Detail) -> Self {
unsafe {
std::slice::from_raw_parts(
(&detail as *const Detail) as *const u8,
USIZE_USIZE_USIZE_USIZE_,
)
.try_into()
.unwrap()
}
}
}
/// \struct AMsyncHaves
/// \installed_headerfile
/// \brief A random-access iterator over a sequence of synchronization haves.
#[repr(C)]
#[derive(Eq, PartialEq)]
pub struct AMsyncHaves {
/// An implementation detail that is intentionally opaque.
/// \warning Modifying \p detail will cause undefined behavior.
/// \note The actual size of \p detail will vary by platform, this is just
/// the one for the platform this documentation was built on.
detail: [u8; USIZE_USIZE_USIZE_USIZE_],
}
impl AMsyncHaves {
pub fn new(haves: &[am::sync::Have], storage: &mut BTreeMap<usize, AMsyncHave>) -> Self {
Self {
detail: Detail::new(haves, 0, storage).into(),
}
}
pub fn advance(&mut self, n: isize) {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.advance(n);
}
pub fn len(&self) -> usize {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
detail.len
}
pub fn next(&mut self, n: isize) -> Option<*const AMsyncHave> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.next(n)
}
pub fn prev(&mut self, n: isize) -> Option<*const AMsyncHave> {
let detail = unsafe { &mut *(self.detail.as_mut_ptr() as *mut Detail) };
detail.prev(n)
}
pub fn reversed(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.reversed().into(),
}
}
pub fn rewound(&self) -> Self {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
Self {
detail: detail.rewound().into(),
}
}
}
impl AsRef<[am::sync::Have]> for AMsyncHaves {
fn as_ref(&self) -> &[am::sync::Have] {
let detail = unsafe { &*(self.detail.as_ptr() as *const Detail) };
unsafe { std::slice::from_raw_parts(detail.ptr as *const am::sync::Have, detail.len) }
}
}
impl Default for AMsyncHaves {
fn default() -> Self {
Self {
detail: [0; USIZE_USIZE_USIZE_USIZE_],
}
}
}
/// \memberof AMsyncHaves
/// \brief Advances an iterator over a sequence of synchronization haves by at
/// most \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in,out] sync_haves A pointer to an `AMsyncHaves` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \pre \p sync_haves `!= NULL`.
/// \internal
///
/// #Safety
/// sync_haves must be a valid pointer to an AMsyncHaves
#[no_mangle]
pub unsafe extern "C" fn AMsyncHavesAdvance(sync_haves: *mut AMsyncHaves, n: isize) {
if let Some(sync_haves) = sync_haves.as_mut() {
sync_haves.advance(n);
};
}
/// \memberof AMsyncHaves
/// \brief Tests the equality of two sequences of synchronization haves
/// underlying a pair of iterators.
///
/// \param[in] sync_haves1 A pointer to an `AMsyncHaves` struct.
/// \param[in] sync_haves2 A pointer to an `AMsyncHaves` struct.
/// \return `true` if \p sync_haves1 `==` \p sync_haves2 and `false` otherwise.
/// \pre \p sync_haves1 `!= NULL`.
/// \pre \p sync_haves2 `!= NULL`.
/// \internal
///
/// #Safety
/// sync_haves1 must be a valid pointer to an AMsyncHaves
/// sync_haves2 must be a valid pointer to an AMsyncHaves
#[no_mangle]
pub unsafe extern "C" fn AMsyncHavesEqual(
sync_haves1: *const AMsyncHaves,
sync_haves2: *const AMsyncHaves,
) -> bool {
match (sync_haves1.as_ref(), sync_haves2.as_ref()) {
(Some(sync_haves1), Some(sync_haves2)) => sync_haves1.as_ref() == sync_haves2.as_ref(),
(None, Some(_)) | (Some(_), None) | (None, None) => false,
}
}
/// \memberof AMsyncHaves
/// \brief Gets the synchronization have at the current position of an iterator
/// over a sequence of synchronization haves and then advances it by at
/// most \p |n| positions where the sign of \p n is relative to the
/// iterator's direction.
///
/// \param[in,out] sync_haves A pointer to an `AMsyncHaves` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMsyncHave` struct that's `NULL` when
/// \p sync_haves was previously advanced past its forward/reverse
/// limit.
/// \pre \p sync_haves `!= NULL`.
/// \internal
///
/// #Safety
/// sync_haves must be a valid pointer to an AMsyncHaves
#[no_mangle]
pub unsafe extern "C" fn AMsyncHavesNext(
sync_haves: *mut AMsyncHaves,
n: isize,
) -> *const AMsyncHave {
if let Some(sync_haves) = sync_haves.as_mut() {
if let Some(sync_have) = sync_haves.next(n) {
return sync_have;
}
}
std::ptr::null()
}
/// \memberof AMsyncHaves
/// \brief Advances an iterator over a sequence of synchronization haves by at
/// most \p |n| positions where the sign of \p n is relative to the
/// iterator's direction and then gets the synchronization have at its
/// new position.
///
/// \param[in,out] sync_haves A pointer to an `AMsyncHaves` struct.
/// \param[in] n The direction (\p -n -> opposite, \p n -> same) and maximum
/// number of positions to advance.
/// \return A pointer to an `AMsyncHave` struct that's `NULL` when
/// \p sync_haves is presently advanced past its forward/reverse limit.
/// \pre \p sync_haves `!= NULL`.
/// \internal
///
/// #Safety
/// sync_haves must be a valid pointer to an AMsyncHaves
#[no_mangle]
pub unsafe extern "C" fn AMsyncHavesPrev(
sync_haves: *mut AMsyncHaves,
n: isize,
) -> *const AMsyncHave {
if let Some(sync_haves) = sync_haves.as_mut() {
if let Some(sync_have) = sync_haves.prev(n) {
return sync_have;
}
}
std::ptr::null()
}
/// \memberof AMsyncHaves
/// \brief Gets the size of the sequence of synchronization haves underlying an
/// iterator.
///
/// \param[in] sync_haves A pointer to an `AMsyncHaves` struct.
/// \return The count of values in \p sync_haves.
/// \pre \p sync_haves `!= NULL`.
/// \internal
///
/// #Safety
/// sync_haves must be a valid pointer to an AMsyncHaves
#[no_mangle]
pub unsafe extern "C" fn AMsyncHavesSize(sync_haves: *const AMsyncHaves) -> usize {
if let Some(sync_haves) = sync_haves.as_ref() {
sync_haves.len()
} else {
0
}
}
/// \memberof AMsyncHaves
/// \brief Creates an iterator over the same sequence of synchronization haves
/// as the given one but with the opposite position and direction.
///
/// \param[in] sync_haves A pointer to an `AMsyncHaves` struct.
/// \return An `AMsyncHaves` struct
/// \pre \p sync_haves `!= NULL`.
/// \internal
///
/// #Safety
/// sync_haves must be a valid pointer to an AMsyncHaves
#[no_mangle]
pub unsafe extern "C" fn AMsyncHavesReversed(sync_haves: *const AMsyncHaves) -> AMsyncHaves {
if let Some(sync_haves) = sync_haves.as_ref() {
sync_haves.reversed()
} else {
AMsyncHaves::default()
}
}
/// \memberof AMsyncHaves
/// \brief Creates an iterator at the starting position over the same sequence
/// of synchronization haves as the given one.
///
/// \param[in] sync_haves A pointer to an `AMsyncHaves` struct.
/// \return An `AMsyncHaves` struct
/// \pre \p sync_haves `!= NULL`.
/// \internal
///
/// #Safety
/// sync_haves must be a valid pointer to an AMsyncHaves
#[no_mangle]
pub unsafe extern "C" fn AMsyncHavesRewound(sync_haves: *const AMsyncHaves) -> AMsyncHaves {
if let Some(sync_haves) = sync_haves.as_ref() {
sync_haves.rewound()
} else {
AMsyncHaves::default()
}
}

View file

@ -1,57 +0,0 @@
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
find_package(cmocka REQUIRED)
add_executable(
test_${LIBRARY_NAME}
actor_id_tests.c
doc_tests.c
group_state.c
list_tests.c
macro_utils.c
main.c
map_tests.c
stack_utils.c
str_utils.c
ported_wasm/basic_tests.c
ported_wasm/suite.c
ported_wasm/sync_tests.c
)
set_target_properties(test_${LIBRARY_NAME} PROPERTIES LINKER_LANGUAGE C)
# \note An imported library's INTERFACE_INCLUDE_DIRECTORIES property can't
# contain a non-existent path so its build-time include directory
# must be specified for all of its dependent targets instead.
target_include_directories(
test_${LIBRARY_NAME}
PRIVATE "$<BUILD_INTERFACE:${CBINDGEN_INCLUDEDIR}>"
)
target_link_libraries(test_${LIBRARY_NAME} PRIVATE cmocka ${LIBRARY_NAME})
add_dependencies(test_${LIBRARY_NAME} ${LIBRARY_NAME}_artifacts)
if(BUILD_SHARED_LIBS AND WIN32)
add_custom_command(
TARGET test_${LIBRARY_NAME}
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_SHARED_LIBRARY_SUFFIX}
${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Copying the DLL built by Cargo into the test directory..."
VERBATIM
)
endif()
add_test(NAME test_${LIBRARY_NAME} COMMAND test_${LIBRARY_NAME})
add_custom_command(
TARGET test_${LIBRARY_NAME}
POST_BUILD
COMMAND
${CMAKE_CTEST_COMMAND} --config $<CONFIG> --output-on-failure
COMMENT
"Running the test(s)..."
VERBATIM
)

View file

@ -1,105 +0,0 @@
#include <math.h>
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
/* third-party */
#include <cmocka.h>
/* local */
#include <automerge-c/automerge.h>
#include "str_utils.h"
typedef struct {
uint8_t* src;
char const* str;
size_t count;
} GroupState;
static int group_setup(void** state) {
GroupState* group_state = test_calloc(1, sizeof(GroupState));
group_state->str = "000102030405060708090a0b0c0d0e0f";
group_state->count = strlen(group_state->str) / 2;
group_state->src = test_malloc(group_state->count);
hex_to_bytes(group_state->str, group_state->src, group_state->count);
*state = group_state;
return 0;
}
static int group_teardown(void** state) {
GroupState* group_state = *state;
test_free(group_state->src);
test_free(group_state);
return 0;
}
static void test_AMactorIdInit() {
AMresult* prior_result = NULL;
AMbyteSpan prior_bytes;
char const* prior_str = NULL;
AMresult* result = NULL;
for (size_t i = 0; i != 11; ++i) {
result = AMactorIdInit();
if (AMresultStatus(result) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(result));
}
assert_int_equal(AMresultSize(result), 1);
AMvalue const value = AMresultValue(result);
assert_int_equal(value.tag, AM_VALUE_ACTOR_ID);
AMbyteSpan const bytes = AMactorIdBytes(value.actor_id);
char const* const str = AMactorIdStr(value.actor_id);
if (prior_result) {
size_t const min_count = fmax(bytes.count, prior_bytes.count);
assert_memory_not_equal(bytes.src, prior_bytes.src, min_count);
assert_string_not_equal(str, prior_str);
AMfree(prior_result);
}
prior_result = result;
prior_bytes = bytes;
prior_str = str;
}
AMfree(result);
}
static void test_AMactorIdInitBytes(void **state) {
GroupState* group_state = *state;
AMresult* const result = AMactorIdInitBytes(group_state->src, group_state->count);
if (AMresultStatus(result) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(result));
}
assert_int_equal(AMresultSize(result), 1);
AMvalue const value = AMresultValue(result);
assert_int_equal(value.tag, AM_VALUE_ACTOR_ID);
AMbyteSpan const bytes = AMactorIdBytes(value.actor_id);
assert_int_equal(bytes.count, group_state->count);
assert_memory_equal(bytes.src, group_state->src, bytes.count);
AMfree(result);
}
static void test_AMactorIdInitStr(void **state) {
GroupState* group_state = *state;
AMresult* const result = AMactorIdInitStr(group_state->str);
if (AMresultStatus(result) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage(result));
}
assert_int_equal(AMresultSize(result), 1);
AMvalue const value = AMresultValue(result);
assert_int_equal(value.tag, AM_VALUE_ACTOR_ID);
char const* const str = AMactorIdStr(value.actor_id);
assert_int_equal(strlen(str), group_state->count * 2);
assert_string_equal(str, group_state->str);
AMfree(result);
}
int run_actor_id_tests(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_AMactorIdInit),
cmocka_unit_test(test_AMactorIdInitBytes),
cmocka_unit_test(test_AMactorIdInitStr),
};
return cmocka_run_group_tests(tests, group_setup, group_teardown);
}

View file

@ -1,202 +0,0 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
/* third-party */
#include <cmocka.h>
/* local */
#include <automerge-c/automerge.h>
#include "group_state.h"
#include "stack_utils.h"
#include "str_utils.h"
typedef struct {
GroupState* group_state;
char const* actor_id_str;
uint8_t* actor_id_bytes;
size_t actor_id_size;
} TestState;
static int setup(void** state) {
TestState* test_state = test_calloc(1, sizeof(TestState));
group_setup((void**)&test_state->group_state);
test_state->actor_id_str = "000102030405060708090a0b0c0d0e0f";
test_state->actor_id_size = strlen(test_state->actor_id_str) / 2;
test_state->actor_id_bytes = test_malloc(test_state->actor_id_size);
hex_to_bytes(test_state->actor_id_str, test_state->actor_id_bytes, test_state->actor_id_size);
*state = test_state;
return 0;
}
static int teardown(void** state) {
TestState* test_state = *state;
group_teardown((void**)&test_state->group_state);
test_free(test_state->actor_id_bytes);
test_free(test_state);
return 0;
}
static void test_AMkeys_empty() {
AMresultStack* stack = NULL;
AMdoc* const doc = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
AMstrs forward = AMpush(&stack,
AMkeys(doc, AM_ROOT, NULL),
AM_VALUE_STRS,
cmocka_cb).strs;
assert_int_equal(AMstrsSize(&forward), 0);
AMstrs reverse = AMstrsReversed(&forward);
assert_int_equal(AMstrsSize(&reverse), 0);
assert_null(AMstrsNext(&forward, 1));
assert_null(AMstrsPrev(&forward, 1));
assert_null(AMstrsNext(&reverse, 1));
assert_null(AMstrsPrev(&reverse, 1));
AMfreeStack(&stack);
}
static void test_AMkeys_list() {
AMresultStack* stack = NULL;
AMdoc* const doc = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
AMfree(AMlistPutInt(doc, AM_ROOT, 0, true, 1));
AMfree(AMlistPutInt(doc, AM_ROOT, 1, true, 2));
AMfree(AMlistPutInt(doc, AM_ROOT, 2, true, 3));
AMstrs forward = AMpush(&stack,
AMkeys(doc, AM_ROOT, NULL),
AM_VALUE_STRS,
cmocka_cb).strs;
assert_int_equal(AMstrsSize(&forward), 3);
AMstrs reverse = AMstrsReversed(&forward);
assert_int_equal(AMstrsSize(&reverse), 3);
/* Forward iterator forward. */
char const* str = AMstrsNext(&forward, 1);
assert_ptr_equal(strstr(str, "1@"), str);
str = AMstrsNext(&forward, 1);
assert_ptr_equal(strstr(str, "2@"), str);
str = AMstrsNext(&forward, 1);
assert_ptr_equal(strstr(str, "3@"), str);
assert_null(AMstrsNext(&forward, 1));
/* Forward iterator reverse. */
str = AMstrsPrev(&forward, 1);
assert_ptr_equal(strstr(str, "3@"), str);
str = AMstrsPrev(&forward, 1);
assert_ptr_equal(strstr(str, "2@"), str);
str = AMstrsPrev(&forward, 1);
assert_ptr_equal(strstr(str, "1@"), str);
assert_null(AMstrsPrev(&forward, 1));
/* Reverse iterator forward. */
str = AMstrsNext(&reverse, 1);
assert_ptr_equal(strstr(str, "3@"), str);
str = AMstrsNext(&reverse, 1);
assert_ptr_equal(strstr(str, "2@"), str);
str = AMstrsNext(&reverse, 1);
assert_ptr_equal(strstr(str, "1@"), str);
/* Reverse iterator reverse. */
assert_null(AMstrsNext(&reverse, 1));
str = AMstrsPrev(&reverse, 1);
assert_ptr_equal(strstr(str, "1@"), str);
str = AMstrsPrev(&reverse, 1);
assert_ptr_equal(strstr(str, "2@"), str);
str = AMstrsPrev(&reverse, 1);
assert_ptr_equal(strstr(str, "3@"), str);
assert_null(AMstrsPrev(&reverse, 1));
AMfreeStack(&stack);
}
static void test_AMkeys_map() {
AMresultStack* stack = NULL;
AMdoc* const doc = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
AMfree(AMmapPutInt(doc, AM_ROOT, "one", 1));
AMfree(AMmapPutInt(doc, AM_ROOT, "two", 2));
AMfree(AMmapPutInt(doc, AM_ROOT, "three", 3));
AMstrs forward = AMpush(&stack,
AMkeys(doc, AM_ROOT, NULL),
AM_VALUE_STRS,
cmocka_cb).strs;
assert_int_equal(AMstrsSize(&forward), 3);
AMstrs reverse = AMstrsReversed(&forward);
assert_int_equal(AMstrsSize(&reverse), 3);
/* Forward iterator forward. */
assert_string_equal(AMstrsNext(&forward, 1), "one");
assert_string_equal(AMstrsNext(&forward, 1), "three");
assert_string_equal(AMstrsNext(&forward, 1), "two");
assert_null(AMstrsNext(&forward, 1));
/* Forward iterator reverse. */
assert_string_equal(AMstrsPrev(&forward, 1), "two");
assert_string_equal(AMstrsPrev(&forward, 1), "three");
assert_string_equal(AMstrsPrev(&forward, 1), "one");
assert_null(AMstrsPrev(&forward, 1));
/* Reverse iterator forward. */
assert_string_equal(AMstrsNext(&reverse, 1), "two");
assert_string_equal(AMstrsNext(&reverse, 1), "three");
assert_string_equal(AMstrsNext(&reverse, 1), "one");
assert_null(AMstrsNext(&reverse, 1));
/* Reverse iterator reverse. */
assert_string_equal(AMstrsPrev(&reverse, 1), "one");
assert_string_equal(AMstrsPrev(&reverse, 1), "three");
assert_string_equal(AMstrsPrev(&reverse, 1), "two");
assert_null(AMstrsPrev(&reverse, 1));
AMfreeStack(&stack);
}
static void test_AMputActor_bytes(void **state) {
TestState* test_state = *state;
AMactorId const* actor_id = AMpush(&test_state->group_state->stack,
AMactorIdInitBytes(
test_state->actor_id_bytes,
test_state->actor_id_size),
AM_VALUE_ACTOR_ID,
cmocka_cb).actor_id;
AMfree(AMsetActorId(test_state->group_state->doc, actor_id));
actor_id = AMpush(&test_state->group_state->stack,
AMgetActorId(test_state->group_state->doc),
AM_VALUE_ACTOR_ID,
cmocka_cb).actor_id;
AMbyteSpan const bytes = AMactorIdBytes(actor_id);
assert_int_equal(bytes.count, test_state->actor_id_size);
assert_memory_equal(bytes.src, test_state->actor_id_bytes, bytes.count);
}
static void test_AMputActor_str(void **state) {
TestState* test_state = *state;
AMactorId const* actor_id = AMpush(&test_state->group_state->stack,
AMactorIdInitStr(test_state->actor_id_str),
AM_VALUE_ACTOR_ID,
cmocka_cb).actor_id;
AMfree(AMsetActorId(test_state->group_state->doc, actor_id));
actor_id = AMpush(&test_state->group_state->stack,
AMgetActorId(test_state->group_state->doc),
AM_VALUE_ACTOR_ID,
cmocka_cb).actor_id;
char const* const str = AMactorIdStr(actor_id);
assert_int_equal(strlen(str), test_state->actor_id_size * 2);
assert_string_equal(str, test_state->actor_id_str);
}
static void test_AMspliceText() {
AMresultStack* stack = NULL;
AMdoc* const doc = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
AMfree(AMspliceText(doc, AM_ROOT, 0, 0, "one + "));
AMfree(AMspliceText(doc, AM_ROOT, 4, 2, "two = "));
AMfree(AMspliceText(doc, AM_ROOT, 8, 2, "three"));
char const* const text = AMpush(&stack,
AMtext(doc, AM_ROOT, NULL),
AM_VALUE_STR,
cmocka_cb).str;
assert_string_equal(text, "one two three");
AMfreeStack(&stack);
}
int run_doc_tests(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_AMkeys_empty),
cmocka_unit_test(test_AMkeys_list),
cmocka_unit_test(test_AMkeys_map),
cmocka_unit_test_setup_teardown(test_AMputActor_bytes, setup, teardown),
cmocka_unit_test_setup_teardown(test_AMputActor_str, setup, teardown),
cmocka_unit_test(test_AMspliceText),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}

View file

@ -1,27 +0,0 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stdlib.h>
/* third-party */
#include <cmocka.h>
/* local */
#include "group_state.h"
#include "stack_utils.h"
int group_setup(void** state) {
GroupState* group_state = test_calloc(1, sizeof(GroupState));
group_state->doc = AMpush(&group_state->stack,
AMcreate(NULL),
AM_VALUE_DOC,
cmocka_cb).doc;
*state = group_state;
return 0;
}
int group_teardown(void** state) {
GroupState* group_state = *state;
AMfreeStack(&group_state->stack);
test_free(group_state);
return 0;
}

View file

@ -1,16 +0,0 @@
#ifndef GROUP_STATE_H
#define GROUP_STATE_H
/* local */
#include <automerge-c/automerge.h>
typedef struct {
AMresultStack* stack;
AMdoc* doc;
} GroupState;
int group_setup(void** state);
int group_teardown(void** state);
#endif /* GROUP_STATE_H */

View file

@ -1,379 +0,0 @@
#include <float.h>
#include <limits.h>
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
/* third-party */
#include <cmocka.h>
/* local */
#include <automerge-c/automerge.h>
#include "group_state.h"
#include "macro_utils.h"
#include "stack_utils.h"
static void test_AMlistIncrement(void** state) {
GroupState* group_state = *state;
AMfree(AMlistPutCounter(group_state->doc, AM_ROOT, 0, true, 0));
assert_int_equal(AMpush(&group_state->stack,
AMlistGet(group_state->doc, AM_ROOT, 0, NULL),
AM_VALUE_COUNTER,
cmocka_cb).counter, 0);
AMfree(AMpop(&group_state->stack));
AMfree(AMlistIncrement(group_state->doc, AM_ROOT, 0, 3));
assert_int_equal(AMpush(&group_state->stack,
AMlistGet(group_state->doc, AM_ROOT, 0, NULL),
AM_VALUE_COUNTER,
cmocka_cb).counter, 3);
AMfree(AMpop(&group_state->stack));
}
#define test_AMlistPut(suffix, mode) test_AMlistPut ## suffix ## _ ## mode
#define static_void_test_AMlistPut(suffix, mode, member, scalar_value) \
static void test_AMlistPut ## suffix ## _ ## mode(void **state) { \
GroupState* group_state = *state; \
AMfree(AMlistPut ## suffix(group_state->doc, \
AM_ROOT, \
0, \
!strcmp(#mode, "insert"), \
scalar_value)); \
assert_true(AMpush( \
&group_state->stack, \
AMlistGet(group_state->doc, AM_ROOT, 0, NULL), \
AMvalue_discriminant(#suffix), \
cmocka_cb).member == scalar_value); \
AMfree(AMpop(&group_state->stack)); \
}
#define test_AMlistPutBytes(mode) test_AMlistPutBytes ## _ ## mode
#define static_void_test_AMlistPutBytes(mode, bytes_value) \
static void test_AMlistPutBytes_ ## mode(void **state) { \
static size_t const BYTES_SIZE = sizeof(bytes_value) / sizeof(uint8_t); \
\
GroupState* group_state = *state; \
AMfree(AMlistPutBytes(group_state->doc, \
AM_ROOT, \
0, \
!strcmp(#mode, "insert"), \
bytes_value, \
BYTES_SIZE)); \
AMbyteSpan const bytes = AMpush( \
&group_state->stack, \
AMlistGet(group_state->doc, AM_ROOT, 0, NULL), \
AM_VALUE_BYTES, \
cmocka_cb).bytes; \
assert_int_equal(bytes.count, BYTES_SIZE); \
assert_memory_equal(bytes.src, bytes_value, BYTES_SIZE); \
AMfree(AMpop(&group_state->stack)); \
}
#define test_AMlistPutNull(mode) test_AMlistPutNull_ ## mode
#define static_void_test_AMlistPutNull(mode) \
static void test_AMlistPutNull_ ## mode(void **state) { \
GroupState* group_state = *state; \
AMfree(AMlistPutNull(group_state->doc, \
AM_ROOT, \
0, \
!strcmp(#mode, "insert"))); \
AMresult* const result = AMlistGet(group_state->doc, AM_ROOT, 0, NULL); \
if (AMresultStatus(result) != AM_STATUS_OK) { \
fail_msg("%s", AMerrorMessage(result)); \
} \
assert_int_equal(AMresultSize(result), 1); \
assert_int_equal(AMresultValue(result).tag, AM_VALUE_NULL); \
AMfree(result); \
}
#define test_AMlistPutObject(label, mode) test_AMlistPutObject_ ## label ## _ ## mode
#define static_void_test_AMlistPutObject(label, mode) \
static void test_AMlistPutObject_ ## label ## _ ## mode(void **state) { \
GroupState* group_state = *state; \
AMobjId const* const obj_id = AMpush( \
&group_state->stack, \
AMlistPutObject(group_state->doc, \
AM_ROOT, \
0, \
!strcmp(#mode, "insert"), \
AMobjType_tag(#label)), \
AM_VALUE_OBJ_ID, \
cmocka_cb).obj_id; \
assert_non_null(obj_id); \
assert_int_equal(AMobjSize(group_state->doc, obj_id, NULL), 0); \
AMfree(AMpop(&group_state->stack)); \
}
#define test_AMlistPutStr(mode) test_AMlistPutStr ## _ ## mode
#define static_void_test_AMlistPutStr(mode, str_value) \
static void test_AMlistPutStr_ ## mode(void **state) { \
GroupState* group_state = *state; \
AMfree(AMlistPutStr(group_state->doc, \
AM_ROOT, \
0, \
!strcmp(#mode, "insert"), \
str_value)); \
assert_string_equal(AMpush( \
&group_state->stack, \
AMlistGet(group_state->doc, AM_ROOT, 0, NULL), \
AM_VALUE_STR, \
cmocka_cb).str, str_value); \
AMfree(AMpop(&group_state->stack)); \
}
static_void_test_AMlistPut(Bool, insert, boolean, true)
static_void_test_AMlistPut(Bool, update, boolean, true)
static uint8_t const BYTES_VALUE[] = {INT8_MIN, INT8_MAX / 2, INT8_MAX};
static_void_test_AMlistPutBytes(insert, BYTES_VALUE)
static_void_test_AMlistPutBytes(update, BYTES_VALUE)
static_void_test_AMlistPut(Counter, insert, counter, INT64_MAX)
static_void_test_AMlistPut(Counter, update, counter, INT64_MAX)
static_void_test_AMlistPut(F64, insert, f64, DBL_MAX)
static_void_test_AMlistPut(F64, update, f64, DBL_MAX)
static_void_test_AMlistPut(Int, insert, int_, INT64_MAX)
static_void_test_AMlistPut(Int, update, int_, INT64_MAX)
static_void_test_AMlistPutNull(insert)
static_void_test_AMlistPutNull(update)
static_void_test_AMlistPutObject(List, insert)
static_void_test_AMlistPutObject(List, update)
static_void_test_AMlistPutObject(Map, insert)
static_void_test_AMlistPutObject(Map, update)
static_void_test_AMlistPutObject(Text, insert)
static_void_test_AMlistPutObject(Text, update)
static_void_test_AMlistPutStr(insert, "Hello, world!")
static_void_test_AMlistPutStr(update, "Hello, world!")
static_void_test_AMlistPut(Timestamp, insert, timestamp, INT64_MAX)
static_void_test_AMlistPut(Timestamp, update, timestamp, INT64_MAX)
static_void_test_AMlistPut(Uint, insert, uint, UINT64_MAX)
static_void_test_AMlistPut(Uint, update, uint, UINT64_MAX)
static void test_insert_at_index(void** state) {
AMresultStack* stack = *state;
AMdoc* const doc = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
AMobjId const* const list = AMpush(
&stack,
AMlistPutObject(doc, AM_ROOT, 0, true, AM_OBJ_TYPE_LIST),
AM_VALUE_OBJ_ID,
cmocka_cb).obj_id;
/* Insert both at the same index. */
AMfree(AMlistPutUint(doc, list, 0, true, 0));
AMfree(AMlistPutUint(doc, list, 0, true, 1));
assert_int_equal(AMobjSize(doc, list, NULL), 2);
AMstrs const keys = AMpush(&stack,
AMkeys(doc, list, NULL),
AM_VALUE_STRS,
cmocka_cb).strs;
assert_int_equal(AMstrsSize(&keys), 2);
AMlistItems const range = AMpush(&stack,
AMlistRange(doc, list, 0, SIZE_MAX, NULL),
AM_VALUE_LIST_ITEMS,
cmocka_cb).list_items;
assert_int_equal(AMlistItemsSize(&range), 2);
}
static void test_get_list_values(void** state) {
AMresultStack* stack = *state;
AMdoc* const doc1 = AMpush(&stack, AMcreate(NULL), AM_VALUE_DOC, cmocka_cb).doc;
AMobjId const* const list = AMpush(
&stack,
AMmapPutObject(doc1, AM_ROOT, "list", AM_OBJ_TYPE_LIST),
AM_VALUE_OBJ_ID,
cmocka_cb).obj_id;
/* Insert elements. */
AMfree(AMlistPutStr(doc1, list, 0, true, "First"));
AMfree(AMlistPutStr(doc1, list, 0, true, "Second"));
AMfree(AMlistPutStr(doc1, list, 0, true, "Third"));
AMfree(AMlistPutStr(doc1, list, 0, true, "Fourth"));
AMfree(AMlistPutStr(doc1, list, 0, true, "Fifth"));
AMfree(AMlistPutStr(doc1, list, 0, true, "Sixth"));
AMfree(AMlistPutStr(doc1, list, 0, true, "Seventh"));
AMfree(AMlistPutStr(doc1, list, 0, true, "Eighth"));
AMfree(AMcommit(doc1, NULL, NULL));
AMchangeHashes const v1 = AMpush(&stack,
AMgetHeads(doc1),
AM_VALUE_CHANGE_HASHES,
cmocka_cb).change_hashes;
AMdoc* const doc2 = AMpush(&stack,
AMfork(doc1, NULL),
AM_VALUE_DOC,
cmocka_cb).doc;
AMfree(AMlistPutStr(doc1, list, 2, false, "Third V2"));
AMfree(AMcommit(doc1, NULL, NULL));
AMfree(AMlistPutStr(doc2, list, 2, false, "Third V3"));
AMfree(AMcommit(doc2, NULL, NULL));
AMfree(AMmerge(doc1, doc2));
AMlistItems range = AMpush(&stack,
AMlistRange(doc1, list, 0, SIZE_MAX, NULL),
AM_VALUE_LIST_ITEMS,
cmocka_cb).list_items;
assert_int_equal(AMlistItemsSize(&range), 8);
AMlistItem const* list_item = NULL;
while ((list_item = AMlistItemsNext(&range, 1)) != NULL) {
AMvalue const val1 = AMlistItemValue(list_item);
AMresult* result = AMlistGet(doc1, list, AMlistItemIndex(list_item), NULL);
AMvalue const val2 = AMresultValue(result);
assert_true(AMvalueEqual(&val1, &val2));
assert_non_null(AMlistItemObjId(list_item));
AMfree(result);
}
range = AMpush(&stack,
AMlistRange(doc1, list, 3, 6, NULL),
AM_VALUE_LIST_ITEMS,
cmocka_cb).list_items;
AMlistItems range_back = AMlistItemsReversed(&range);
assert_int_equal(AMlistItemsSize(&range), 3);
assert_int_equal(AMlistItemIndex(AMlistItemsNext(&range, 1)), 3);
assert_int_equal(AMlistItemIndex(AMlistItemsNext(&range_back, 1)), 5);
range = AMlistItemsRewound(&range);
while ((list_item = AMlistItemsNext(&range, 1)) != NULL) {
AMvalue const val1 = AMlistItemValue(list_item);
AMresult* result = AMlistGet(doc1, list, AMlistItemIndex(list_item), NULL);
AMvalue const val2 = AMresultValue(result);
assert_true(AMvalueEqual(&val1, &val2));
assert_non_null(AMlistItemObjId(list_item));
AMfree(result);
}
range = AMpush(&stack,
AMlistRange(doc1, list, 0, SIZE_MAX, &v1),
AM_VALUE_LIST_ITEMS,
cmocka_cb).list_items;
assert_int_equal(AMlistItemsSize(&range), 8);
while ((list_item = AMlistItemsNext(&range, 1)) != NULL) {
AMvalue const val1 = AMlistItemValue(list_item);
AMresult* result = AMlistGet(doc1, list, AMlistItemIndex(list_item), &v1);
AMvalue const val2 = AMresultValue(result);
assert_true(AMvalueEqual(&val1, &val2));
assert_non_null(AMlistItemObjId(list_item));
AMfree(result);
}
range = AMpush(&stack,
AMlistRange(doc1, list, 3, 6, &v1),
AM_VALUE_LIST_ITEMS,
cmocka_cb).list_items;
range_back = AMlistItemsReversed(&range);
assert_int_equal(AMlistItemsSize(&range), 3);
assert_int_equal(AMlistItemIndex(AMlistItemsNext(&range, 1)), 3);
assert_int_equal(AMlistItemIndex(AMlistItemsNext(&range_back, 1)), 5);
range = AMlistItemsRewound(&range);
while ((list_item = AMlistItemsNext(&range, 1)) != NULL) {
AMvalue const val1 = AMlistItemValue(list_item);
AMresult* result = AMlistGet(doc1, list, AMlistItemIndex(list_item), &v1);
AMvalue const val2 = AMresultValue(result);
assert_true(AMvalueEqual(&val1, &val2));
assert_non_null(AMlistItemObjId(list_item));
AMfree(result);
}
range = AMpush(&stack,
AMlistRange(doc1, list, 0, SIZE_MAX, NULL),
AM_VALUE_LIST_ITEMS,
cmocka_cb).list_items;
AMobjItems values = AMpush(&stack,
AMobjValues(doc1, list, NULL),
AM_VALUE_OBJ_ITEMS,
cmocka_cb).obj_items;
assert_int_equal(AMlistItemsSize(&range), AMobjItemsSize(&values));
AMobjItem const* value = NULL;
while ((list_item = AMlistItemsNext(&range, 1)) != NULL &&
(value = AMobjItemsNext(&values, 1)) != NULL) {
AMvalue const val1 = AMlistItemValue(list_item);
AMvalue const val2 = AMobjItemValue(value);
assert_true(AMvalueEqual(&val1, &val2));
assert_true(AMobjIdEqual(AMlistItemObjId(list_item), AMobjItemObjId(value)));
}
range = AMpush(&stack,
AMlistRange(doc1, list, 0, SIZE_MAX, &v1),
AM_VALUE_LIST_ITEMS,
cmocka_cb).list_items;
values = AMpush(&stack,
AMobjValues(doc1, list, &v1),
AM_VALUE_OBJ_ITEMS,
cmocka_cb).obj_items;
assert_int_equal(AMlistItemsSize(&range), AMobjItemsSize(&values));
while ((list_item = AMlistItemsNext(&range, 1)) != NULL &&
(value = AMobjItemsNext(&values, 1)) != NULL) {
AMvalue const val1 = AMlistItemValue(list_item);
AMvalue const val2 = AMobjItemValue(value);
assert_true(AMvalueEqual(&val1, &val2));
assert_true(AMobjIdEqual(AMlistItemObjId(list_item), AMobjItemObjId(value)));
}
}
int run_list_tests(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_AMlistIncrement),
cmocka_unit_test(test_AMlistPut(Bool, insert)),
cmocka_unit_test(test_AMlistPut(Bool, update)),
cmocka_unit_test(test_AMlistPutBytes(insert)),
cmocka_unit_test(test_AMlistPutBytes(update)),
cmocka_unit_test(test_AMlistPut(Counter, insert)),
cmocka_unit_test(test_AMlistPut(Counter, update)),
cmocka_unit_test(test_AMlistPut(F64, insert)),
cmocka_unit_test(test_AMlistPut(F64, update)),
cmocka_unit_test(test_AMlistPut(Int, insert)),
cmocka_unit_test(test_AMlistPut(Int, update)),
cmocka_unit_test(test_AMlistPutNull(insert)),
cmocka_unit_test(test_AMlistPutNull(update)),
cmocka_unit_test(test_AMlistPutObject(List, insert)),
cmocka_unit_test(test_AMlistPutObject(List, update)),
cmocka_unit_test(test_AMlistPutObject(Map, insert)),
cmocka_unit_test(test_AMlistPutObject(Map, update)),
cmocka_unit_test(test_AMlistPutObject(Text, insert)),
cmocka_unit_test(test_AMlistPutObject(Text, update)),
cmocka_unit_test(test_AMlistPutStr(insert)),
cmocka_unit_test(test_AMlistPutStr(update)),
cmocka_unit_test(test_AMlistPut(Timestamp, insert)),
cmocka_unit_test(test_AMlistPut(Timestamp, update)),
cmocka_unit_test(test_AMlistPut(Uint, insert)),
cmocka_unit_test(test_AMlistPut(Uint, update)),
cmocka_unit_test_setup_teardown(test_insert_at_index, setup_stack, teardown_stack),
cmocka_unit_test_setup_teardown(test_get_list_values, setup_stack, teardown_stack),
};
return cmocka_run_group_tests(tests, group_setup, group_teardown);
}

View file

@ -1,24 +0,0 @@
#include <string.h>
/* local */
#include "macro_utils.h"
AMvalueVariant AMvalue_discriminant(char const* suffix) {
if (!strcmp(suffix, "Bool")) return AM_VALUE_BOOLEAN;
else if (!strcmp(suffix, "Bytes")) return AM_VALUE_BYTES;
else if (!strcmp(suffix, "Counter")) return AM_VALUE_COUNTER;
else if (!strcmp(suffix, "F64")) return AM_VALUE_F64;
else if (!strcmp(suffix, "Int")) return AM_VALUE_INT;
else if (!strcmp(suffix, "Null")) return AM_VALUE_NULL;
else if (!strcmp(suffix, "Str")) return AM_VALUE_STR;
else if (!strcmp(suffix, "Timestamp")) return AM_VALUE_TIMESTAMP;
else if (!strcmp(suffix, "Uint")) return AM_VALUE_UINT;
else return AM_VALUE_VOID;
}
AMobjType AMobjType_tag(char const* obj_type_label) {
if (!strcmp(obj_type_label, "List")) return AM_OBJ_TYPE_LIST;
else if (!strcmp(obj_type_label, "Map")) return AM_OBJ_TYPE_MAP;
else if (!strcmp(obj_type_label, "Text")) return AM_OBJ_TYPE_TEXT;
else return 0;
}

View file

@ -1,24 +0,0 @@
#ifndef MACRO_UTILS_H
#define MACRO_UTILS_H
/* local */
#include <automerge-c/automerge.h>
/**
* \brief Gets the result value discriminant corresponding to a function name
* suffix.
*
* \param[in] suffix A string.
* \return An `AMvalue` struct discriminant.
*/
AMvalueVariant AMvalue_discriminant(char const* suffix);
/**
* \brief Gets the object type tag corresponding to an object type label.
*
* \param[in] obj_type_label A string.
* \return An `AMobjType` enum tag.
*/
AMobjType AMobjType_tag(char const* obj_type_label);
#endif /* MACRO_UTILS_H */

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,30 +0,0 @@
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
/* third-party */
#include <cmocka.h>
/* local */
#include "stack_utils.h"
void cmocka_cb(AMresultStack** stack, uint8_t discriminant) {
assert_non_null(stack);
assert_non_null(*stack);
assert_non_null((*stack)->result);
if (AMresultStatus((*stack)->result) != AM_STATUS_OK) {
fail_msg("%s", AMerrorMessage((*stack)->result));
}
assert_int_equal(AMresultValue((*stack)->result).tag, discriminant);
}
int setup_stack(void** state) {
*state = NULL;
return 0;
}
int teardown_stack(void** state) {
AMresultStack* stack = *state;
AMfreeStack(&stack);
return 0;
}

View file

@ -1,38 +0,0 @@
#ifndef STACK_UTILS_H
#define STACK_UTILS_H
#include <stdint.h>
/* local */
#include <automerge-c/automerge.h>
/**
* \brief Reports an error through a cmocka assertion.
*
* \param[in,out] stack A pointer to a pointer to an `AMresultStack` struct.
* \param[in] discriminant An `AMvalueVariant` enum tag.
* \pre \p stack` != NULL`.
*/
void cmocka_cb(AMresultStack** stack, uint8_t discriminant);
/**
* \brief Allocates a result stack for storing the results allocated during one
* or more test cases.
*
* \param[in,out] state A pointer to a pointer to an `AMresultStack` struct.
* \pre \p state` != NULL`.
* \warning The `AMresultStack` struct returned through \p state must be
* deallocated with `teardown_stack()` in order to prevent memory leaks.
*/
int setup_stack(void** state);
/**
* \brief Deallocates a result stack after deallocating any results that were
* stored in it by one or more test cases.
*
* \param[in] state A pointer to a pointer to an `AMresultStack` struct.
* \pre \p state` != NULL`.
*/
int teardown_stack(void** state);
#endif /* STACK_UTILS_H */

View file

@ -1,14 +0,0 @@
#ifndef STR_UTILS_H
#define STR_UTILS_H
/**
* \brief Converts a hexadecimal string into a sequence of bytes.
*
* \param[in] hex_str A string.
* \param[in] src A pointer to a contiguous sequence of bytes.
* \param[in] count The number of bytes to copy to \p src.
* \pre \p count `<=` length of \p src.
*/
void hex_to_bytes(char const* hex_str, uint8_t* src, size_t const count);
#endif /* STR_UTILS_H */

857
automerge-cli/Cargo.lock generated
View file

@ -1,857 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi",
]
[[package]]
name = "anyhow"
version = "1.0.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "159bb86af3a200e19a068f4224eae4c8bb2d0fa054c7e5d1cacd5cef95e684cd"
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "automerge"
version = "0.1.0"
dependencies = [
"flate2",
"fxhash",
"hex",
"itertools",
"js-sys",
"leb128",
"nonzero_ext",
"rand",
"serde",
"sha2",
"smol_str",
"thiserror",
"tinyvec",
"tracing",
"unicode-segmentation",
"uuid",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "automerge-cli"
version = "0.1.0"
dependencies = [
"anyhow",
"atty",
"automerge",
"clap",
"colored_json",
"combine",
"duct",
"maplit",
"serde_json",
"thiserror",
"tracing-subscriber",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "block-buffer"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "3.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced1892c55c910c1219e98d6fc8d71f6bddba7905866ce740066d8bfea859312"
dependencies = [
"atty",
"bitflags",
"clap_derive",
"indexmap",
"lazy_static",
"os_str_bytes",
"strsim",
"termcolor",
"textwrap",
]
[[package]]
name = "clap_derive"
version = "3.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da95d038ede1a964ce99f49cbe27a7fb538d1da595e4b4f70b8c8f338d17bf16"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "colored_json"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd32eb54d016e203b7c2600e3a7802c75843a92e38ccc4869aefeca21771a64"
dependencies = [
"ansi_term",
"atty",
"libc",
"serde",
"serde_json",
]
[[package]]
name = "combine"
version = "4.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50b727aacc797f9fc28e355d21f34709ac4fc9adecfe470ad07b8f4464f53062"
dependencies = [
"bytes",
"memchr",
]
[[package]]
name = "cpufeatures"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
"cfg-if",
]
[[package]]
name = "crypto-common"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "duct"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fc6a0a59ed0888e0041cf708e66357b7ae1a82f1c67247e1f93b5e0818f7d8d"
dependencies = [
"libc",
"once_cell",
"os_pipe",
"shared_child",
]
[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "flate2"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f"
dependencies = [
"cfg-if",
"crc32fast",
"libc",
"miniz_oxide",
]
[[package]]
name = "fxhash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
dependencies = [
"byteorder",
]
[[package]]
name = "generic-array"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]]
name = "heck"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "indexmap"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "itertools"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
[[package]]
name = "js-sys"
version = "0.3.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "leb128"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
[[package]]
name = "libc"
version = "0.2.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4"
[[package]]
name = "log"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
]
[[package]]
name = "maplit"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]]
name = "memchr"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "miniz_oxide"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
dependencies = [
"adler",
"autocfg",
]
[[package]]
name = "nonzero_ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44a1290799eababa63ea60af0cbc3f03363e328e58f32fb0294798ed3e85f444"
[[package]]
name = "once_cell"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
[[package]]
name = "os_pipe"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb233f06c2307e1f5ce2ecad9f8121cffbbee2c95428f44ea85222e460d0d213"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "os_str_bytes"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
dependencies = [
"memchr",
]
[[package]]
name = "pin-project-lite"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
[[package]]
name = "ppv-lite86"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
dependencies = [
"getrandom",
]
[[package]]
name = "ryu"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
[[package]]
name = "serde"
version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "sha2"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
dependencies = [
"lazy_static",
]
[[package]]
name = "shared_child"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6be9f7d5565b1483af3e72975e2dee33879b3b86bd48c0929fccf6585d79e65a"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "smallvec"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
[[package]]
name = "smol_str"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61d15c83e300cce35b7c8cd39ff567c1ef42dde6d4a1a38dbdbf9a59902261bd"
dependencies = [
"serde",
]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "termcolor"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
[[package]]
name = "thiserror"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
dependencies = [
"once_cell",
]
[[package]]
name = "tinyvec"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tracing"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6c650a8ef0cd2dd93736f033d21cbd1224c5a967aa0c258d00fcf7dafef9b9f"
dependencies = [
"cfg-if",
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8276d9a4a3a558d7b7ad5303ad50b53d58264641b82914b7ada36bd762e7a716"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03cfcb51380632a72d3111cb8d3447a8d908e577d31beeac006f836383d29a23"
dependencies = [
"lazy_static",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3"
dependencies = [
"lazy_static",
"log",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e0ab7bdc962035a87fba73f3acca9b8a8d0034c2e6f60b84aeaaddddc155dce"
dependencies = [
"ansi_term",
"sharded-slab",
"smallvec",
"thread_local",
"tracing-core",
"tracing-log",
]
[[package]]
name = "typenum"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "unicode-segmentation"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "uuid"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [
"getrandom",
"serde",
]
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "wasm-bindgen"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca"
dependencies = [
"bumpalo",
"lazy_static",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2"
[[package]]
name = "web-sys"
version = "0.3.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

View file

@ -1,18 +0,0 @@
## Automerge
Automerge is a library of data structures for building collaborative
applications, this package is the javascript implementation.
Please see [automerge.org](http://automerge.org/) for documentation.
## Setup
This package is a wrapper around a core library which is written in rust and
compiled to WASM. In `node` this should be transparent to you, but in the
browser you will need a bundler to include the WASM blob as part of your module
hierarchy. There are examples of doing this with common bundlers in `./examples`.
## Meta
Copyright 20172021, the Automerge contributors. Released under the terms of the
MIT license (see `LICENSE`).

View file

@ -1,6 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../dist/cjs"
}
}

View file

@ -1,8 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"target": "es6",
"module": "es6",
"outDir": "../dist/mjs"
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,15 +0,0 @@
import { defineConfig } from "vite"
import wasm from "vite-plugin-wasm"
import topLevelAwait from "vite-plugin-top-level-await"
export default defineConfig({
plugins: [topLevelAwait(), wasm()],
optimizeDeps: {
// This is necessary because otherwise `vite dev` includes two separate
// versions of the JS wrapper. This causes problems because the JS
// wrapper has a module level variable to track JS side heap
// allocations, initializing this twice causes horrible breakage
exclude: ["@automerge/automerge-wasm"]
}
})

View file

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

View file

@ -1,63 +0,0 @@
{
"name": "@automerge/automerge",
"collaborators": [
"Orion Henry <orion@inkandswitch.com>",
"Martin Kleppmann"
],
"version": "2.0.0-alpha.4",
"description": "Javascript implementation of automerge, backed by @automerge/automerge-wasm",
"homepage": "https://github.com/automerge/automerge-rs/tree/main/automerge-js",
"repository": "github:automerge/automerge-rs",
"files": [
"README.md",
"LICENSE",
"package.json",
"index.d.ts",
"dist/*.d.ts",
"dist/cjs/constants.js",
"dist/cjs/types.js",
"dist/cjs/numbers.js",
"dist/cjs/index.js",
"dist/cjs/uuid.js",
"dist/cjs/counter.js",
"dist/cjs/low_level.js",
"dist/cjs/text.js",
"dist/cjs/proxies.js",
"dist/mjs/constants.js",
"dist/mjs/types.js",
"dist/mjs/numbers.js",
"dist/mjs/index.js",
"dist/mjs/uuid.js",
"dist/mjs/counter.js",
"dist/mjs/low_level.js",
"dist/mjs/text.js",
"dist/mjs/proxies.js"
],
"types": "./dist/index.d.ts",
"module": "./dist/mjs/index.js",
"main": "./dist/cjs/index.js",
"license": "MIT",
"scripts": {
"lint": "eslint src",
"build": "tsc -p config/mjs.json && tsc -p config/cjs.json && tsc --emitDeclarationOnly",
"test": "ts-mocha test/*.ts"
},
"devDependencies": {
"@types/expect": "^24.3.0",
"@types/mocha": "^9.1.1",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.25.0",
"@typescript-eslint/parser": "^5.25.0",
"eslint": "^8.15.0",
"fast-sha256": "^1.3.0",
"mocha": "^10.0.0",
"pako": "^2.0.4",
"ts-mocha": "^10.0.0",
"ts-node": "^10.9.1",
"typescript": "^4.6.4"
},
"dependencies": {
"@automerge/automerge-wasm": "0.1.9",
"uuid": "^8.3"
}
}

View file

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

View file

@ -1,460 +0,0 @@
export { uuid } from './uuid'
import { rootProxy, listProxy, textProxy, mapProxy } from "./proxies"
import { STATE, HEADS, TRACE, OBJECT_ID, READ_ONLY, FROZEN } from "./constants"
import { AutomergeValue, Text, Counter } from "./types"
export { AutomergeValue, Text, Counter, Int, Uint, Float64 } from "./types"
import { type API, type Patch } from "@automerge/automerge-wasm";
import { ApiHandler, UseApi } from "./low_level"
import { Actor as ActorId, Prop, ObjID, Change, DecodedChange, Heads, Automerge, MaterializeValue } from "@automerge/automerge-wasm"
import { JsSyncState as SyncState, SyncMessage, DecodedSyncMessage } from "@automerge/automerge-wasm"
export type ChangeOptions<T> = { message?: string, time?: number, patchCallback?: PatchCallback<T> }
export type ApplyOptions<T> = { patchCallback?: PatchCallback<T> }
export type Doc<T> = { readonly [P in keyof T]: T[P] }
export type ChangeFn<T> = (doc: T) => void
export type PatchCallback<T> = (patch: Patch, before: Doc<T>, after: Doc<T>) => void
export interface State<T> {
change: DecodedChange
snapshot: T
}
export function use(api: API) {
UseApi(api)
}
import * as wasm from "@automerge/automerge-wasm"
use(wasm)
export type InitOptions<T> = {
actor?: ActorId,
freeze?: boolean,
patchCallback?: PatchCallback<T>,
};
interface InternalState<T> {
handle: Automerge,
heads: Heads | undefined,
freeze: boolean,
patchCallback?: PatchCallback<T>
}
export function getBackend<T>(doc: Doc<T>) : Automerge {
return _state(doc).handle
}
function _state<T>(doc: Doc<T>, checkroot = true) : InternalState<T> {
const state = Reflect.get(doc,STATE)
if (state === undefined || (checkroot && _obj(doc) !== "_root")) {
throw new RangeError("must be the document root")
}
return state
}
function _frozen<T>(doc: Doc<T>) : boolean {
return Reflect.get(doc,FROZEN) === true
}
function _trace<T>(doc: Doc<T>) : string | undefined {
return Reflect.get(doc,TRACE)
}
function _set_heads<T>(doc: Doc<T>, heads: Heads) {
_state(doc).heads = heads
}
function _clear_heads<T>(doc: Doc<T>) {
Reflect.set(doc,HEADS,undefined)
Reflect.set(doc,TRACE,undefined)
}
function _obj<T>(doc: Doc<T>) : ObjID {
let proxy_objid = Reflect.get(doc,OBJECT_ID)
if (proxy_objid) {
return proxy_objid
}
if (Reflect.get(doc,STATE)) {
return "_root"
}
throw new RangeError("invalid document passed to _obj()")
}
function _readonly<T>(doc: Doc<T>) : boolean {
return Reflect.get(doc,READ_ONLY) !== false
}
function importOpts<T>(_actor?: ActorId | InitOptions<T>) : InitOptions<T> {
if (typeof _actor === 'object') {
return _actor
} else {
return { actor: _actor }
}
}
export function init<T>(_opts?: ActorId | InitOptions<T>) : Doc<T>{
let opts = importOpts(_opts)
let freeze = !!opts.freeze
let patchCallback = opts.patchCallback
const handle = ApiHandler.create(opts.actor)
handle.enablePatches(true)
//@ts-ignore
handle.registerDatatype("counter", (n) => new Counter(n))
//@ts-ignore
handle.registerDatatype("text", (n) => new Text(n))
//@ts-ignore
const doc = handle.materialize("/", undefined, { handle, heads: undefined, freeze, patchCallback })
//@ts-ignore
return doc
}
export function clone<T>(doc: Doc<T>) : Doc<T> {
const state = _state(doc)
const handle = state.heads ? state.handle.forkAt(state.heads) : state.handle.fork()
//@ts-ignore
const clonedDoc : any = handle.materialize("/", undefined, { ... state, handle })
return clonedDoc
}
export function free<T>(doc: Doc<T>) {
return _state(doc).handle.free()
}
export function from<T extends Record<string, unknown>>(initialState: T | Doc<T>, actor?: ActorId): Doc<T> {
return change(init(actor), (d) => Object.assign(d, initialState))
}
export function change<T>(doc: Doc<T>, options: string | ChangeOptions<T> | ChangeFn<T>, callback?: ChangeFn<T>): Doc<T> {
if (typeof options === 'function') {
return _change(doc, {}, options)
} else if (typeof callback === 'function') {
if (typeof options === "string") {
options = { message: options }
}
return _change(doc, options, callback)
} else {
throw RangeError("Invalid args for change")
}
}
function progressDocument<T>(doc: Doc<T>, heads: Heads, callback?: PatchCallback<T>): Doc<T> {
let state = _state(doc)
let nextState = { ... state, heads: undefined };
// @ts-ignore
let nextDoc = state.handle.applyPatches(doc, nextState, callback)
state.heads = heads
if (nextState.freeze) { Object.freeze(nextDoc) }
return nextDoc
}
function _change<T>(doc: Doc<T>, options: ChangeOptions<T>, callback: ChangeFn<T>): Doc<T> {
if (typeof callback !== "function") {
throw new RangeError("invalid change function");
}
const state = _state(doc)
if (doc === undefined || state === undefined) {
throw new RangeError("must be the document root");
}
if (state.heads) {
throw new RangeError("Attempting to use an outdated Automerge document")
}
if (_readonly(doc) === false) {
throw new RangeError("Calls to Automerge.change cannot be nested")
}
const heads = state.handle.getHeads()
try {
state.heads = heads
const root : T = rootProxy(state.handle);
callback(root)
if (state.handle.pendingOps() === 0) {
state.heads = undefined
return doc
} else {
state.handle.commit(options.message, options.time)
return progressDocument(doc, heads, options.patchCallback || state.patchCallback);
}
} catch (e) {
//console.log("ERROR: ",e)
state.heads = undefined
state.handle.rollback()
throw e
}
}
export function emptyChange<T>(doc: Doc<T>, options: ChangeOptions<T>) {
if (options === undefined) {
options = {}
}
if (typeof options === "string") {
options = { message: options }
}
const state = _state(doc)
if (state.heads) {
throw new RangeError("Attempting to use an outdated Automerge document")
}
if (_readonly(doc) === false) {
throw new RangeError("Calls to Automerge.change cannot be nested")
}
const heads = state.handle.getHeads()
state.handle.commit(options.message, options.time)
return progressDocument(doc, heads)
}
export function load<T>(data: Uint8Array, _opts?: ActorId | InitOptions<T>) : Doc<T> {
const opts = importOpts(_opts)
const actor = opts.actor
const patchCallback = opts.patchCallback
const handle = ApiHandler.load(data, actor)
handle.enablePatches(true)
//@ts-ignore
handle.registerDatatype("counter", (n) => new Counter(n))
//@ts-ignore
handle.registerDatatype("text", (n) => new Text(n))
//@ts-ignore
const doc : any = handle.materialize("/", undefined, { handle, heads: undefined, patchCallback })
return doc
}
export function loadIncremental<T>(doc: Doc<T>, data: Uint8Array, opts?: ApplyOptions<T>) : Doc<T> {
if (!opts) { opts = {} }
const state = _state(doc)
if (state.heads) {
throw new RangeError("Attempting to change an out of date document - set at: " + _trace(doc));
}
if (_readonly(doc) === false) {
throw new RangeError("Calls to Automerge.change cannot be nested")
}
const heads = state.handle.getHeads()
state.handle.loadIncremental(data)
return progressDocument(doc, heads, opts.patchCallback || state.patchCallback)
}
export function save<T>(doc: Doc<T>) : Uint8Array {
return _state(doc).handle.save()
}
export function merge<T>(local: Doc<T>, remote: Doc<T>) : Doc<T> {
const localState = _state(local)
if (localState.heads) {
throw new RangeError("Attempting to change an out of date document - set at: " + _trace(local));
}
const heads = localState.handle.getHeads()
const remoteState = _state(remote)
const changes = localState.handle.getChangesAdded(remoteState.handle)
localState.handle.applyChanges(changes)
return progressDocument(local, heads, localState.patchCallback)
}
export function getActorId<T>(doc: Doc<T>) : ActorId {
const state = _state(doc)
return state.handle.getActorId()
}
type Conflicts = { [key: string]: AutomergeValue }
function conflictAt(context : Automerge, objectId: ObjID, prop: Prop) : Conflicts | undefined {
const values = context.getAll(objectId, prop)
if (values.length <= 1) {
return
}
const result : Conflicts = {}
for (const fullVal of values) {
switch (fullVal[0]) {
case "map":
result[fullVal[1]] = mapProxy(context, fullVal[1], [ prop ], true)
break;
case "list":
result[fullVal[1]] = listProxy(context, fullVal[1], [ prop ], true)
break;
case "text":
result[fullVal[1]] = textProxy(context, fullVal[1], [ prop ], true)
break;
//case "table":
//case "cursor":
case "str":
case "uint":
case "int":
case "f64":
case "boolean":
case "bytes":
case "null":
result[fullVal[2]] = fullVal[1]
break;
case "counter":
result[fullVal[2]] = new Counter(fullVal[1])
break;
case "timestamp":
result[fullVal[2]] = new Date(fullVal[1])
break;
default:
throw RangeError(`datatype ${fullVal[0]} unimplemented`)
}
}
return result
}
export function getConflicts<T>(doc: Doc<T>, prop: Prop) : Conflicts | undefined {
const state = _state(doc, false)
const objectId = _obj(doc)
return conflictAt(state.handle, objectId, prop)
}
export function getLastLocalChange<T>(doc: Doc<T>) : Change | undefined {
const state = _state(doc)
return state.handle.getLastLocalChange() || undefined
}
export function getObjectId<T>(doc: Doc<T>) : ObjID {
return _obj(doc)
}
export function getChanges<T>(oldState: Doc<T>, newState: Doc<T>) : Change[] {
const o = _state(oldState)
const n = _state(newState)
return n.handle.getChanges(getHeads(oldState))
}
export function getAllChanges<T>(doc: Doc<T>) : Change[] {
const state = _state(doc)
return state.handle.getChanges([])
}
export function applyChanges<T>(doc: Doc<T>, changes: Change[], opts?: ApplyOptions<T>) : [Doc<T>] {
const state = _state(doc)
if (!opts) { opts = {} }
if (state.heads) {
throw new RangeError("Attempting to use an outdated Automerge document")
}
if (_readonly(doc) === false) {
throw new RangeError("Calls to Automerge.change cannot be nested")
}
const heads = state.handle.getHeads();
state.handle.applyChanges(changes)
state.heads = heads;
return [progressDocument(doc, heads, opts.patchCallback || state.patchCallback )]
}
export function getHistory<T>(doc: Doc<T>) : State<T>[] {
const history = getAllChanges(doc)
return history.map((change, index) => ({
get change () {
return decodeChange(change)
},
get snapshot () {
const [state] = applyChanges(init(), history.slice(0, index + 1))
return <T>state
}
})
)
}
// FIXME : no tests
// FIXME can we just use deep equals now?
export function equals(val1: unknown, val2: unknown) : boolean {
if (!isObject(val1) || !isObject(val2)) return val1 === val2
const keys1 = Object.keys(val1).sort(), keys2 = Object.keys(val2).sort()
if (keys1.length !== keys2.length) return false
for (let i = 0; i < keys1.length; i++) {
if (keys1[i] !== keys2[i]) return false
if (!equals(val1[keys1[i]], val2[keys2[i]])) return false
}
return true
}
export function encodeSyncState(state: SyncState) : Uint8Array {
return ApiHandler.encodeSyncState(ApiHandler.importSyncState(state))
}
export function decodeSyncState(state: Uint8Array) : SyncState {
return ApiHandler.exportSyncState(ApiHandler.decodeSyncState(state))
}
export function generateSyncMessage<T>(doc: Doc<T>, inState: SyncState) : [ SyncState, SyncMessage | null ] {
const state = _state(doc)
const syncState = ApiHandler.importSyncState(inState)
const message = state.handle.generateSyncMessage(syncState)
const outState = ApiHandler.exportSyncState(syncState)
return [ outState, message ]
}
export function receiveSyncMessage<T>(doc: Doc<T>, inState: SyncState, message: SyncMessage, opts?: ApplyOptions<T>) : [ Doc<T>, SyncState, null ] {
const syncState = ApiHandler.importSyncState(inState)
if (!opts) { opts = {} }
const state = _state(doc)
if (state.heads) {
throw new RangeError("Attempting to change an out of date document - set at: " + _trace(doc));
}
if (_readonly(doc) === false) {
throw new RangeError("Calls to Automerge.change cannot be nested")
}
const heads = state.handle.getHeads()
state.handle.receiveSyncMessage(syncState, message)
const outSyncState = ApiHandler.exportSyncState(syncState)
return [progressDocument(doc, heads, opts.patchCallback || state.patchCallback), outSyncState, null];
}
export function initSyncState() : SyncState {
return ApiHandler.exportSyncState(ApiHandler.initSyncState())
}
export function encodeChange(change: DecodedChange) : Change {
return ApiHandler.encodeChange(change)
}
export function decodeChange(data: Change) : DecodedChange {
return ApiHandler.decodeChange(data)
}
export function encodeSyncMessage(message: DecodedSyncMessage) : SyncMessage {
return ApiHandler.encodeSyncMessage(message)
}
export function decodeSyncMessage(message: SyncMessage) : DecodedSyncMessage {
return ApiHandler.decodeSyncMessage(message)
}
export function getMissingDeps<T>(doc: Doc<T>, heads: Heads) : Heads {
const state = _state(doc)
return state.handle.getMissingDeps(heads)
}
export function getHeads<T>(doc: Doc<T>) : Heads {
const state = _state(doc)
return state.heads || state.handle.getHeads()
}
export function dump<T>(doc: Doc<T>) {
const state = _state(doc)
state.handle.dump()
}
// FIXME - return T?
export function toJS<T>(doc: Doc<T>) : MaterializeValue {
const state = _state(doc)
// @ts-ignore
return state.handle.materialize("_root", state.heads, state)
}
function isObject(obj: unknown) : obj is Record<string,unknown> {
return typeof obj === 'object' && obj !== null
}
export type { API, SyncState, ActorId, Conflicts, Prop, Change, ObjID, DecodedChange, DecodedSyncMessage, Heads, MaterializeValue }

View file

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

View file

@ -1,711 +0,0 @@
import { Automerge, Heads, ObjID } from "@automerge/automerge-wasm"
import { Prop } from "@automerge/automerge-wasm"
import { AutomergeValue, ScalarValue, MapValue, ListValue, TextValue } from "./types"
import { Counter, getWriteableCounter } from "./counter"
import { Text } from "./text"
import { STATE, HEADS, TRACE, FROZEN, OBJECT_ID, READ_ONLY, COUNTER, INT, UINT, F64, TEXT } from "./constants"
function parseListIndex(key) {
if (typeof key === 'string' && /^[0-9]+$/.test(key)) key = parseInt(key, 10)
if (typeof key !== 'number') {
// throw new TypeError('A list index must be a number, but you passed ' + JSON.stringify(key))
return key
}
if (key < 0 || isNaN(key) || key === Infinity || key === -Infinity) {
throw new RangeError('A list index must be positive, but you passed ' + key)
}
return key
}
function valueAt(target, prop: Prop) : AutomergeValue | undefined {
const { context, objectId, path, readonly, heads} = target
const value = context.getWithType(objectId, prop, heads)
if (value === null) {
return
}
const datatype = value[0]
const val = value[1]
switch (datatype) {
case undefined: return;
case "map": return mapProxy(context, val, [ ... path, prop ], readonly, heads);
case "list": return listProxy(context, val, [ ... path, prop ], readonly, heads);
case "text": return textProxy(context, val, [ ... path, prop ], readonly, heads);
//case "table":
//case "cursor":
case "str": return val;
case "uint": return val;
case "int": return val;
case "f64": return val;
case "boolean": return val;
case "null": return null;
case "bytes": return val;
case "timestamp": return val;
case "counter": {
if (readonly) {
return new Counter(val);
} else {
return getWriteableCounter(val, context, path, objectId, prop)
}
}
default:
throw RangeError(`datatype ${datatype} unimplemented`)
}
}
function import_value(value) {
switch (typeof value) {
case 'object':
if (value == null) {
return [ null, "null"]
} else if (value[UINT]) {
return [ value.value, "uint" ]
} else if (value[INT]) {
return [ value.value, "int" ]
} else if (value[F64]) {
return [ value.value, "f64" ]
} else if (value[COUNTER]) {
return [ value.value, "counter" ]
} else if (value[TEXT]) {
return [ value, "text" ]
} else if (value instanceof Date) {
return [ value.getTime(), "timestamp" ]
} else if (value instanceof Uint8Array) {
return [ value, "bytes" ]
} else if (value instanceof Array) {
return [ value, "list" ]
} else if (Object.getPrototypeOf(value) === Object.getPrototypeOf({})) {
return [ value, "map" ]
} else if (value[OBJECT_ID]) {
throw new RangeError('Cannot create a reference to an existing document object')
} else {
throw new RangeError(`Cannot assign unknown object: ${value}`)
}
break;
case 'boolean':
return [ value, "boolean" ]
case 'number':
if (Number.isInteger(value)) {
return [ value, "int" ]
} else {
return [ value, "f64" ]
}
break;
case 'string':
return [ value ]
break;
default:
throw new RangeError(`Unsupported type of value: ${typeof value}`)
}
}
const MapHandler = {
get (target, key) : AutomergeValue {
const { context, objectId, readonly, frozen, heads, cache } = target
if (key === Symbol.toStringTag) { return target[Symbol.toStringTag] }
if (key === OBJECT_ID) return objectId
if (key === READ_ONLY) return readonly
if (key === FROZEN) return frozen
if (key === HEADS) return heads
if (key === TRACE) return target.trace
if (key === STATE) return context;
if (!cache[key]) {
cache[key] = valueAt(target, key)
}
return cache[key]
},
set (target, key, val) {
const { context, objectId, path, readonly, frozen} = target
target.cache = {} // reset cache on set
if (val && val[OBJECT_ID]) {
throw new RangeError('Cannot create a reference to an existing document object')
}
if (key === FROZEN) {
target.frozen = val
return true
}
if (key === HEADS) {
target.heads = val
return true
}
if (key === TRACE) {
target.trace = val
return true
}
const [ value, datatype ] = import_value(val)
if (frozen) {
throw new RangeError("Attempting to use an outdated Automerge document")
}
if (readonly) {
throw new RangeError(`Object property "${key}" cannot be modified`)
}
switch (datatype) {
case "list": {
const list = context.putObject(objectId, key, [])
const proxyList = listProxy(context, list, [ ... path, key ], readonly );
for (let i = 0; i < value.length; i++) {
proxyList[i] = value[i]
}
break
}
case "text": {
const text = context.putObject(objectId, key, "", "text")
const proxyText = textProxy(context, text, [ ... path, key ], readonly );
for (let i = 0; i < value.length; i++) {
proxyText[i] = value.get(i)
}
break
}
case "map": {
const map = context.putObject(objectId, key, {})
const proxyMap = mapProxy(context, map, [ ... path, key ], readonly );
for (const key in value) {
proxyMap[key] = value[key]
}
break;
}
default:
context.put(objectId, key, value, datatype)
}
return true
},
deleteProperty (target, key) {
const { context, objectId, readonly } = target
target.cache = {} // reset cache on delete
if (readonly) {
throw new RangeError(`Object property "${key}" cannot be modified`)
}
context.delete(objectId, key)
return true
},
has (target, key) {
const value = this.get(target, key)
return value !== undefined
},
getOwnPropertyDescriptor (target, key) {
// const { context, objectId } = target
const value = this.get(target, key)
if (typeof value !== 'undefined') {
return {
configurable: true, enumerable: true, value
}
}
},
ownKeys (target) {
const { context, objectId, heads} = target
// FIXME - this is a tmp workaround until fix the dupe key bug in keys()
const keys = context.keys(objectId, heads)
return [...new Set<string>(keys)]
},
}
const ListHandler = {
get (target, index) {
const {context, objectId, readonly, frozen, heads } = target
index = parseListIndex(index)
if (index === Symbol.hasInstance) { return (instance) => { return Array.isArray(instance) } }
if (index === Symbol.toStringTag) { return target[Symbol.toStringTag] }
if (index === OBJECT_ID) return objectId
if (index === READ_ONLY) return readonly
if (index === FROZEN) return frozen
if (index === HEADS) return heads
if (index === TRACE) return target.trace
if (index === STATE) return context;
if (index === 'length') return context.length(objectId, heads);
if (typeof index === 'number') {
return valueAt(target, index)
} else {
return listMethods(target)[index]
}
},
set (target, index, val) {
const {context, objectId, path, readonly, frozen } = target
index = parseListIndex(index)
if (val && val[OBJECT_ID]) {
throw new RangeError('Cannot create a reference to an existing document object')
}
if (index === FROZEN) {
target.frozen = val
return true
}
if (index === HEADS) {
target.heads = val
return true
}
if (index === TRACE) {
target.trace = val
return true
}
if (typeof index == "string") {
throw new RangeError('list index must be a number')
}
const [ value, datatype] = import_value(val)
if (frozen) {
throw new RangeError("Attempting to use an outdated Automerge document")
}
if (readonly) {
throw new RangeError(`Object property "${index}" cannot be modified`)
}
switch (datatype) {
case "list": {
let list
if (index >= context.length(objectId)) {
list = context.insertObject(objectId, index, [])
} else {
list = context.putObject(objectId, index, [])
}
const proxyList = listProxy(context, list, [ ... path, index ], readonly);
proxyList.splice(0,0,...value)
break;
}
case "text": {
let text
if (index >= context.length(objectId)) {
text = context.insertObject(objectId, index, "", "text")
} else {
text = context.putObject(objectId, index, "", "text")
}
const proxyText = textProxy(context, text, [ ... path, index ], readonly);
proxyText.splice(0,0,...value)
break;
}
case "map": {
let map
if (index >= context.length(objectId)) {
map = context.insertObject(objectId, index, {})
} else {
map = context.putObject(objectId, index, {})
}
const proxyMap = mapProxy(context, map, [ ... path, index ], readonly);
for (const key in value) {
proxyMap[key] = value[key]
}
break;
}
default:
if (index >= context.length(objectId)) {
context.insert(objectId, index, value, datatype)
} else {
context.put(objectId, index, value, datatype)
}
}
return true
},
deleteProperty (target, index) {
const {context, objectId} = target
index = parseListIndex(index)
if (context.get(objectId, index)[0] == "counter") {
throw new TypeError('Unsupported operation: deleting a counter from a list')
}
context.delete(objectId, index)
return true
},
has (target, index) {
const {context, objectId, heads} = target
index = parseListIndex(index)
if (typeof index === 'number') {
return index < context.length(objectId, heads)
}
return index === 'length'
},
getOwnPropertyDescriptor (target, index) {
const {context, objectId, heads} = target
if (index === 'length') return {writable: true, value: context.length(objectId, heads) }
if (index === OBJECT_ID) return {configurable: false, enumerable: false, value: objectId}
index = parseListIndex(index)
const value = valueAt(target, index)
return { configurable: true, enumerable: true, value }
},
getPrototypeOf(target) { return Object.getPrototypeOf(target) },
ownKeys (/*target*/) : string[] {
const keys : string[] = []
// uncommenting this causes assert.deepEqual() to fail when comparing to a pojo array
// but not uncommenting it causes for (i in list) {} to not enumerate values properly
//const {context, objectId, heads } = target
//for (let i = 0; i < target.context.length(objectId, heads); i++) { keys.push(i.toString()) }
keys.push("length");
return keys
}
}
const TextHandler = Object.assign({}, ListHandler, {
get (target, index) {
// FIXME this is a one line change from ListHandler.get()
const {context, objectId, readonly, frozen, heads } = target
index = parseListIndex(index)
if (index === Symbol.toStringTag) { return target[Symbol.toStringTag] }
if (index === Symbol.hasInstance) { return (instance) => { return Array.isArray(instance) } }
if (index === OBJECT_ID) return objectId
if (index === READ_ONLY) return readonly
if (index === FROZEN) return frozen
if (index === HEADS) return heads
if (index === TRACE) return target.trace
if (index === STATE) return context;
if (index === 'length') return context.length(objectId, heads);
if (typeof index === 'number') {
return valueAt(target, index)
} else {
return textMethods(target)[index] || listMethods(target)[index]
}
},
getPrototypeOf(/*target*/) {
return Object.getPrototypeOf(new Text())
},
})
export function mapProxy(context: Automerge, objectId: ObjID, path?: Prop[], readonly?: boolean, heads?: Heads) : MapValue {
return new Proxy({context, objectId, path, readonly: !!readonly, frozen: false, heads, cache: {}}, MapHandler)
}
export function listProxy(context: Automerge, objectId: ObjID, path?: Prop[], readonly?: boolean, heads?: Heads) : ListValue {
const target = []
Object.assign(target, {context, objectId, path, readonly: !!readonly, frozen: false, heads, cache: {}})
return new Proxy(target, ListHandler)
}
export function textProxy(context: Automerge, objectId: ObjID, path?: Prop[], readonly?: boolean, heads?: Heads) : TextValue {
const target = []
Object.assign(target, {context, objectId, path, readonly: !!readonly, frozen: false, heads, cache: {}})
return new Proxy(target, TextHandler)
}
export function rootProxy<T>(context: Automerge, readonly?: boolean) : T {
/* eslint-disable-next-line */
return <any>mapProxy(context, "_root", [], !!readonly)
}
function listMethods(target) {
const {context, objectId, path, readonly, frozen, heads} = target
const methods = {
deleteAt(index, numDelete) {
if (typeof numDelete === 'number') {
context.splice(objectId, index, numDelete)
} else {
context.delete(objectId, index)
}
return this
},
fill(val: ScalarValue, start: number, end: number) {
const [value, datatype] = import_value(val)
const length = context.length(objectId)
start = parseListIndex(start || 0)
end = parseListIndex(end || length)
for (let i = start; i < Math.min(end, length); i++) {
context.put(objectId, i, value, datatype)
}
return this
},
indexOf(o, start = 0) {
const length = context.length(objectId)
for (let i = start; i < length; i++) {
const value = context.getWithType(objectId, i, heads)
if (value && value[1] === o[OBJECT_ID] || value[1] === o) {
return i
}
}
return -1
},
insertAt(index, ...values) {
this.splice(index, 0, ...values)
return this
},
pop() {
const length = context.length(objectId)
if (length == 0) {
return undefined
}
const last = valueAt(target, length - 1)
context.delete(objectId, length - 1)
return last
},
push(...values) {
const len = context.length(objectId)
this.splice(len, 0, ...values)
return context.length(objectId)
},
shift() {
if (context.length(objectId) == 0) return
const first = valueAt(target, 0)
context.delete(objectId, 0)
return first
},
splice(index, del, ...vals) {
index = parseListIndex(index)
del = parseListIndex(del)
for (const val of vals) {
if (val && val[OBJECT_ID]) {
throw new RangeError('Cannot create a reference to an existing document object')
}
}
if (frozen) {
throw new RangeError("Attempting to use an outdated Automerge document")
}
if (readonly) {
throw new RangeError("Sequence object cannot be modified outside of a change block")
}
const result : AutomergeValue[] = []
for (let i = 0; i < del; i++) {
const value = valueAt(target, index)
if (value !== undefined) {
result.push(value)
}
context.delete(objectId, index)
}
const values = vals.map((val) => import_value(val))
for (const [value,datatype] of values) {
switch (datatype) {
case "list": {
const list = context.insertObject(objectId, index, [])
const proxyList = listProxy(context, list, [ ... path, index ], readonly);
proxyList.splice(0,0,...value)
break;
}
case "text": {
const text = context.insertObject(objectId, index, "", "text")
const proxyText = textProxy(context, text, [ ... path, index ], readonly);
proxyText.splice(0,0,...value)
break;
}
case "map": {
const map = context.insertObject(objectId, index, {})
const proxyMap = mapProxy(context, map, [ ... path, index ], readonly);
for (const key in value) {
proxyMap[key] = value[key]
}
break;
}
default:
context.insert(objectId, index, value, datatype)
}
index += 1
}
return result
},
unshift(...values) {
this.splice(0, 0, ...values)
return context.length(objectId)
},
entries() {
const i = 0;
const iterator = {
next: () => {
const value = valueAt(target, i)
if (value === undefined) {
return { value: undefined, done: true }
} else {
return { value: [ i, value ], done: false }
}
}
}
return iterator
},
keys() {
let i = 0;
const len = context.length(objectId, heads)
const iterator = {
next: () => {
let value : undefined | number = undefined
if (i < len) { value = i; i++ }
return { value, done: true }
}
}
return iterator
},
values() {
const i = 0;
const iterator = {
next: () => {
const value = valueAt(target, i)
if (value === undefined) {
return { value: undefined, done: true }
} else {
return { value, done: false }
}
}
}
return iterator
},
toArray() : AutomergeValue[] {
const list : AutomergeValue = []
let value
do {
value = valueAt(target, list.length)
if (value !== undefined) {
list.push(value)
}
} while (value !== undefined)
return list
},
map<T>(f: (AutomergeValue, number) => T) : T[] {
return this.toArray().map(f)
},
toString() : string {
return this.toArray().toString()
},
toLocaleString() : string {
return this.toArray().toLocaleString()
},
forEach(f: (AutomergeValue, number) => undefined ) {
return this.toArray().forEach(f)
},
// todo: real concat function is different
concat(other: AutomergeValue[]) : AutomergeValue[] {
return this.toArray().concat(other)
},
every(f: (AutomergeValue, number) => boolean) : boolean {
return this.toArray().every(f)
},
filter(f: (AutomergeValue, number) => boolean) : AutomergeValue[] {
return this.toArray().filter(f)
},
find(f: (AutomergeValue, number) => boolean) : AutomergeValue | undefined {
let index = 0
for (let v of this) {
if (f(v, index)) {
return v
}
index += 1
}
},
findIndex(f: (AutomergeValue, number) => boolean) : number {
let index = 0
for (let v of this) {
if (f(v, index)) {
return index
}
index += 1
}
return -1
},
includes(elem: AutomergeValue) : boolean {
return this.find((e) => e === elem) !== undefined
},
join(sep?: string) : string {
return this.toArray().join(sep)
},
// todo: remove the any
reduce<T>(f: (any, AutomergeValue) => T, initalValue?: T) : T | undefined {
return this.toArray().reduce(f,initalValue)
},
// todo: remove the any
reduceRight<T>(f: (any, AutomergeValue) => T, initalValue?: T) : T | undefined{
return this.toArray().reduceRight(f,initalValue)
},
lastIndexOf(search: AutomergeValue, fromIndex = +Infinity) : number {
// this can be faster
return this.toArray().lastIndexOf(search,fromIndex)
},
slice(index?: number, num?: number) : AutomergeValue[] {
return this.toArray().slice(index,num)
},
some(f: (AutomergeValue, number) => boolean) : boolean {
let index = 0;
for (let v of this) {
if (f(v,index)) {
return true
}
index += 1
}
return false
},
[Symbol.iterator]: function *() {
let i = 0;
let value = valueAt(target, i)
while (value !== undefined) {
yield value
i += 1
value = valueAt(target, i)
}
}
}
return methods
}
function textMethods(target) {
const {context, objectId, heads } = target
const methods = {
set (index: number, value) {
return this[index] = value
},
get (index: number) : AutomergeValue {
return this[index]
},
toString () : string {
return context.text(objectId, heads).replace(//g,'')
},
toSpans () : AutomergeValue[] {
const spans : AutomergeValue[] = []
let chars = ''
const length = context.length(objectId)
for (let i = 0; i < length; i++) {
const value = this[i]
if (typeof value === 'string') {
chars += value
} else {
if (chars.length > 0) {
spans.push(chars)
chars = ''
}
spans.push(value)
}
}
if (chars.length > 0) {
spans.push(chars)
}
return spans
},
toJSON () : string {
return this.toString()
},
indexOf(o, start = 0) {
const text = context.text(objectId)
return text.indexOf(o,start)
}
}
return methods
}

View file

@ -1,12 +0,0 @@
export { Text } from "./text"
export { Counter } from "./counter"
export { Int, Uint, Float64 } from "./numbers"
import { Counter } from "./counter"
export type AutomergeValue = ScalarValue | { [key: string]: AutomergeValue } | Array<AutomergeValue>
export type MapValue = { [key: string]: AutomergeValue }
export type ListValue = Array<AutomergeValue>
export type TextValue = Array<AutomergeValue>
export type ScalarValue = string | number | null | boolean | Date | Counter | Uint8Array

View file

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

View file

@ -1,233 +0,0 @@
import * as assert from 'assert'
import * as Automerge from '../src'
describe('Automerge', () => {
describe('basics', () => {
it('should init clone and free', () => {
let doc1 = Automerge.init()
let doc2 = Automerge.clone(doc1);
})
it('handle basic set and read on root object', () => {
let doc1 = Automerge.init()
let doc2 = Automerge.change(doc1, (d) => {
d.hello = "world"
d.big = "little"
d.zip = "zop"
d.app = "dap"
assert.deepEqual(d, { hello: "world", big: "little", zip: "zop", app: "dap" })
})
assert.deepEqual(doc2, { hello: "world", big: "little", zip: "zop", app: "dap" })
})
it('handle basic sets over many changes', () => {
let doc1 = Automerge.init()
let timestamp = new Date();
let counter = new Automerge.Counter(100);
let bytes = new Uint8Array([10,11,12]);
let doc2 = Automerge.change(doc1, (d) => {
d.hello = "world"
})
let doc3 = Automerge.change(doc2, (d) => {
d.counter1 = counter
})
let doc4 = Automerge.change(doc3, (d) => {
d.timestamp1 = timestamp
})
let doc5 = Automerge.change(doc4, (d) => {
d.app = null
})
let doc6 = Automerge.change(doc5, (d) => {
d.bytes1 = bytes
})
let doc7 = Automerge.change(doc6, (d) => {
d.uint = new Automerge.Uint(1)
d.int = new Automerge.Int(-1)
d.float64 = new Automerge.Float64(5.5)
d.number1 = 100
d.number2 = -45.67
d.true = true
d.false = false
})
assert.deepEqual(doc7, { hello: "world", true: true, false: false, int: -1, uint: 1, float64: 5.5, number1: 100, number2: -45.67, counter1: counter, timestamp1: timestamp, bytes1: bytes, app: null })
let changes = Automerge.getAllChanges(doc7)
let t1 = Automerge.init()
;let [t2] = Automerge.applyChanges(t1, changes)
assert.deepEqual(doc7,t2)
})
it('handle overwrites to values', () => {
let doc1 = Automerge.init()
let doc2 = Automerge.change(doc1, (d) => {
d.hello = "world1"
})
let doc3 = Automerge.change(doc2, (d) => {
d.hello = "world2"
})
let doc4 = Automerge.change(doc3, (d) => {
d.hello = "world3"
})
let doc5 = Automerge.change(doc4, (d) => {
d.hello = "world4"
})
assert.deepEqual(doc5, { hello: "world4" } )
})
it('handle set with object value', () => {
let doc1 = Automerge.init()
let doc2 = Automerge.change(doc1, (d) => {
d.subobj = { hello: "world", subsubobj: { zip: "zop" } }
})
assert.deepEqual(doc2, { subobj: { hello: "world", subsubobj: { zip: "zop" } } })
})
it('handle simple list creation', () => {
let doc1 = Automerge.init()
let doc2 = Automerge.change(doc1, (d) => d.list = [])
assert.deepEqual(doc2, { list: []})
})
it('handle simple lists', () => {
let doc1 = Automerge.init()
let doc2 = Automerge.change(doc1, (d) => {
d.list = [ 1, 2, 3 ]
})
assert.deepEqual(doc2.list.length, 3)
assert.deepEqual(doc2.list[0], 1)
assert.deepEqual(doc2.list[1], 2)
assert.deepEqual(doc2.list[2], 3)
assert.deepEqual(doc2, { list: [1,2,3] })
// assert.deepStrictEqual(Automerge.toJS(doc2), { list: [1,2,3] })
let doc3 = Automerge.change(doc2, (d) => {
d.list[1] = "a"
})
assert.deepEqual(doc3.list.length, 3)
assert.deepEqual(doc3.list[0], 1)
assert.deepEqual(doc3.list[1], "a")
assert.deepEqual(doc3.list[2], 3)
assert.deepEqual(doc3, { list: [1,"a",3] })
})
it('handle simple lists', () => {
let doc1 = Automerge.init()
let doc2 = Automerge.change(doc1, (d) => {
d.list = [ 1, 2, 3 ]
})
let changes = Automerge.getChanges(doc1, doc2)
let docB1 = Automerge.init()
;let [docB2] = Automerge.applyChanges(docB1, changes)
assert.deepEqual(docB2, doc2);
})
it('handle text', () => {
let doc1 = Automerge.init()
let tmp = new Automerge.Text("hello")
let doc2 = Automerge.change(doc1, (d) => {
d.list = new Automerge.Text("hello")
d.list.insertAt(2,"Z")
})
let changes = Automerge.getChanges(doc1, doc2)
let docB1 = Automerge.init()
;let [docB2] = Automerge.applyChanges(docB1, changes)
assert.deepEqual(docB2, doc2);
})
it('have many list methods', () => {
let doc1 = Automerge.from({ list: [1,2,3] })
assert.deepEqual(doc1, { list: [1,2,3] });
let doc2 = Automerge.change(doc1, (d) => {
d.list.splice(1,1,9,10)
})
assert.deepEqual(doc2, { list: [1,9,10,3] });
let doc3 = Automerge.change(doc2, (d) => {
d.list.push(11,12)
})
assert.deepEqual(doc3, { list: [1,9,10,3,11,12] });
let doc4 = Automerge.change(doc3, (d) => {
d.list.unshift(2,2)
})
assert.deepEqual(doc4, { list: [2,2,1,9,10,3,11,12] });
let doc5 = Automerge.change(doc4, (d) => {
d.list.shift()
})
assert.deepEqual(doc5, { list: [2,1,9,10,3,11,12] });
let doc6 = Automerge.change(doc5, (d) => {
d.list.insertAt(3,100,101)
})
assert.deepEqual(doc6, { list: [2,1,9,100,101,10,3,11,12] });
})
it('allows access to the backend', () => {
let doc = Automerge.init()
assert.deepEqual(Object.keys(Automerge.getBackend(doc)), ["ptr"])
})
it('lists and text have indexof', () => {
let doc = Automerge.from({ list: [0,1,2,3,4,5,6], text: new Automerge.Text("hello world") })
console.log(doc.list.indexOf(5))
console.log(doc.text.indexOf("world"))
})
})
describe('proxy lists', () => {
it('behave like arrays', () => {
let doc = Automerge.from({
chars: ["a","b","c"],
numbers: [20,3,100],
repeats: [20,20,3,3,3,3,100,100]
})
let r1 = []
doc = Automerge.change(doc, (d) => {
assert.deepEqual(d.chars.concat([1,2]), ["a","b","c",1,2])
assert.deepEqual(d.chars.map((n) => n + "!"), ["a!", "b!", "c!"])
assert.deepEqual(d.numbers.map((n) => n + 10), [30, 13, 110])
assert.deepEqual(d.numbers.toString(), "20,3,100")
assert.deepEqual(d.numbers.toLocaleString(), "20,3,100")
assert.deepEqual(d.numbers.forEach((n) => r1.push(n)), undefined)
assert.deepEqual(d.numbers.every((n) => n > 1), true)
assert.deepEqual(d.numbers.every((n) => n > 10), false)
assert.deepEqual(d.numbers.filter((n) => n > 10), [20,100])
assert.deepEqual(d.repeats.find((n) => n < 10), 3)
assert.deepEqual(d.repeats.toArray().find((n) => n < 10), 3)
assert.deepEqual(d.repeats.find((n) => n < 0), undefined)
assert.deepEqual(d.repeats.findIndex((n) => n < 10), 2)
assert.deepEqual(d.repeats.findIndex((n) => n < 0), -1)
assert.deepEqual(d.repeats.toArray().findIndex((n) => n < 10), 2)
assert.deepEqual(d.repeats.toArray().findIndex((n) => n < 0), -1)
assert.deepEqual(d.numbers.includes(3), true)
assert.deepEqual(d.numbers.includes(-3), false)
assert.deepEqual(d.numbers.join("|"), "20|3|100")
assert.deepEqual(d.numbers.join(), "20,3,100")
assert.deepEqual(d.numbers.some((f) => f === 3), true)
assert.deepEqual(d.numbers.some((f) => f < 0), false)
assert.deepEqual(d.numbers.reduce((sum,n) => sum + n, 100), 223)
assert.deepEqual(d.repeats.reduce((sum,n) => sum + n, 100), 352)
assert.deepEqual(d.chars.reduce((sum,n) => sum + n, "="), "=abc")
assert.deepEqual(d.chars.reduceRight((sum,n) => sum + n, "="), "=cba")
assert.deepEqual(d.numbers.reduceRight((sum,n) => sum + n, 100), 223)
assert.deepEqual(d.repeats.lastIndexOf(3), 5)
assert.deepEqual(d.repeats.lastIndexOf(3,3), 3)
})
doc = Automerge.change(doc, (d) => {
assert.deepEqual(d.numbers.fill(-1,1,2), [20,-1,100])
assert.deepEqual(d.chars.fill("z",1,100), ["a","z","z"])
})
assert.deepEqual(r1, [20,3,100])
assert.deepEqual(doc.numbers, [20,-1,100])
assert.deepEqual(doc.chars, ["a","z","z"])
})
})
it('should obtain the same conflicts, regardless of merge order', () => {
let s1 = Automerge.init()
let s2 = Automerge.init()
s1 = Automerge.change(s1, doc => { doc.x = 1; doc.y = 2 })
s2 = Automerge.change(s2, doc => { doc.x = 3; doc.y = 4 })
const m1 = Automerge.merge(Automerge.clone(s1), Automerge.clone(s2))
const m2 = Automerge.merge(Automerge.clone(s2), Automerge.clone(s1))
assert.deepStrictEqual(Automerge.getConflicts(m1, 'x'), Automerge.getConflicts(m2, 'x'))
})
})

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -1,11 +0,0 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
};

View file

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

View file

@ -1,58 +0,0 @@
{
"collaborators": [
"Orion Henry <orion@inkandswitch.com>",
"Alex Good <alex@memoryandthought.me>",
"Martin Kleppmann"
],
"name": "@automerge/automerge-wasm",
"description": "wasm-bindgen bindings to the automerge rust implementation",
"homepage": "https://github.com/automerge/automerge-rs/tree/main/automerge-wasm",
"repository": "github:automerge/automerge-rs",
"version": "0.1.9",
"license": "MIT",
"files": [
"README.md",
"LICENSE",
"package.json",
"index.d.ts",
"nodejs/bindgen.js",
"nodejs/bindgen_bg.wasm",
"bundler/bindgen.js",
"bundler/bindgen_bg.js",
"bundler/bindgen_bg.wasm"
],
"private": false,
"types": "index.d.ts",
"module": "./bundler/bindgen.js",
"main": "./nodejs/bindgen.js",
"scripts": {
"lint": "eslint test/*.ts index.d.ts",
"debug": "cross-env PROFILE=dev yarn buildall",
"build": "cross-env PROFILE=dev FEATURES='' yarn buildall",
"release": "cross-env PROFILE=release yarn buildall",
"buildall": "cross-env TARGET=nodejs yarn target && cross-env TARGET=bundler yarn target",
"target": "rimraf ./$TARGET && wasm-pack build --target $TARGET --$PROFILE --out-name bindgen -d $TARGET -- $FEATURES",
"test": "ts-mocha -p tsconfig.json --type-check --bail --full-trace test/*.ts"
},
"devDependencies": {
"@types/expect": "^24.3.0",
"@types/jest": "^27.4.0",
"@types/mocha": "^9.1.0",
"@types/node": "^17.0.13",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.25.0",
"@typescript-eslint/parser": "^5.25.0",
"cross-env": "^7.0.3",
"eslint": "^8.16.0",
"fast-sha256": "^1.3.0",
"mocha": "^9.1.3",
"pako": "^2.0.4",
"rimraf": "^3.0.2",
"ts-mocha": "^9.0.2",
"typescript": "^4.6.4"
},
"exports": {
"browser": "./bundler/bindgen.js",
"require": "./nodejs/bindgen.js"
}
}

View file

@ -1,720 +0,0 @@
use crate::value::Datatype;
use crate::Automerge;
use automerge as am;
use automerge::transaction::Transactable;
use automerge::{Change, ChangeHash, ObjType, Prop};
use js_sys::{Array, Function, Object, Reflect, Symbol, Uint8Array};
use std::collections::{BTreeSet, HashSet};
use std::fmt::Display;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use crate::{observer::Patch, ObjId, Value};
const RAW_DATA_SYMBOL: &str = "_am_raw_value_";
const DATATYPE_SYMBOL: &str = "_am_datatype_";
const RAW_OBJECT_SYMBOL: &str = "_am_objectId";
const META_SYMBOL: &str = "_am_meta";
pub(crate) struct JS(pub(crate) JsValue);
pub(crate) struct AR(pub(crate) Array);
impl From<AR> for JsValue {
fn from(ar: AR) -> Self {
ar.0.into()
}
}
impl From<JS> for JsValue {
fn from(js: JS) -> Self {
js.0
}
}
impl From<am::sync::State> for JS {
fn from(state: am::sync::State) -> Self {
let shared_heads: JS = state.shared_heads.into();
let last_sent_heads: JS = state.last_sent_heads.into();
let their_heads: JS = state.their_heads.into();
let their_need: JS = state.their_need.into();
let sent_hashes: JS = state.sent_hashes.into();
let their_have = if let Some(have) = &state.their_have {
JsValue::from(AR::from(have.as_slice()).0)
} else {
JsValue::null()
};
let result: JsValue = Object::new().into();
// we can unwrap here b/c we made the object and know its not frozen
Reflect::set(&result, &"sharedHeads".into(), &shared_heads.0).unwrap();
Reflect::set(&result, &"lastSentHeads".into(), &last_sent_heads.0).unwrap();
Reflect::set(&result, &"theirHeads".into(), &their_heads.0).unwrap();
Reflect::set(&result, &"theirNeed".into(), &their_need.0).unwrap();
Reflect::set(&result, &"theirHave".into(), &their_have).unwrap();
Reflect::set(&result, &"sentHashes".into(), &sent_hashes.0).unwrap();
JS(result)
}
}
impl From<Vec<ChangeHash>> for JS {
fn from(heads: Vec<ChangeHash>) -> Self {
JS(heads
.iter()
.map(|h| JsValue::from_str(&h.to_string()))
.collect::<Array>()
.into())
}
}
impl From<HashSet<ChangeHash>> for JS {
fn from(heads: HashSet<ChangeHash>) -> Self {
let result: JsValue = Object::new().into();
for key in &heads {
Reflect::set(&result, &key.to_string().into(), &true.into()).unwrap();
}
JS(result)
}
}
impl From<BTreeSet<ChangeHash>> for JS {
fn from(heads: BTreeSet<ChangeHash>) -> Self {
let result: JsValue = Object::new().into();
for key in &heads {
Reflect::set(&result, &key.to_string().into(), &true.into()).unwrap();
}
JS(result)
}
}
impl From<Option<Vec<ChangeHash>>> for JS {
fn from(heads: Option<Vec<ChangeHash>>) -> Self {
if let Some(v) = heads {
let v: Array = v
.iter()
.map(|h| JsValue::from_str(&h.to_string()))
.collect();
JS(v.into())
} else {
JS(JsValue::null())
}
}
}
impl TryFrom<JS> for HashSet<ChangeHash> {
type Error = JsValue;
fn try_from(value: JS) -> Result<Self, Self::Error> {
let mut result = HashSet::new();
for key in Reflect::own_keys(&value.0)?.iter() {
if let Some(true) = Reflect::get(&value.0, &key)?.as_bool() {
result.insert(serde_wasm_bindgen::from_value(key).map_err(to_js_err)?);
}
}
Ok(result)
}
}
impl TryFrom<JS> for BTreeSet<ChangeHash> {
type Error = JsValue;
fn try_from(value: JS) -> Result<Self, Self::Error> {
let mut result = BTreeSet::new();
for key in Reflect::own_keys(&value.0)?.iter() {
if let Some(true) = Reflect::get(&value.0, &key)?.as_bool() {
result.insert(serde_wasm_bindgen::from_value(key).map_err(to_js_err)?);
}
}
Ok(result)
}
}
impl TryFrom<JS> for Vec<ChangeHash> {
type Error = JsValue;
fn try_from(value: JS) -> Result<Self, Self::Error> {
let value = value.0.dyn_into::<Array>()?;
let value: Result<Vec<ChangeHash>, _> =
value.iter().map(serde_wasm_bindgen::from_value).collect();
let value = value.map_err(to_js_err)?;
Ok(value)
}
}
impl From<JS> for Option<Vec<ChangeHash>> {
fn from(value: JS) -> Self {
let value = value.0.dyn_into::<Array>().ok()?;
let value: Result<Vec<ChangeHash>, _> =
value.iter().map(serde_wasm_bindgen::from_value).collect();
let value = value.ok()?;
Some(value)
}
}
impl TryFrom<JS> for Vec<Change> {
type Error = JsValue;
fn try_from(value: JS) -> Result<Self, Self::Error> {
let value = value.0.dyn_into::<Array>()?;
let changes: Result<Vec<Uint8Array>, _> = value.iter().map(|j| j.dyn_into()).collect();
let changes = changes?;
let changes = changes.iter().try_fold(Vec::new(), |mut acc, arr| {
match automerge::Change::try_from(arr.to_vec().as_slice()) {
Ok(c) => acc.push(c),
Err(e) => return Err(to_js_err(e)),
}
Ok(acc)
})?;
Ok(changes)
}
}
impl TryFrom<JS> for am::sync::State {
type Error = JsValue;
fn try_from(value: JS) -> Result<Self, Self::Error> {
let value = value.0;
let shared_heads = js_get(&value, "sharedHeads")?.try_into()?;
let last_sent_heads = js_get(&value, "lastSentHeads")?.try_into()?;
let their_heads = js_get(&value, "theirHeads")?.into();
let their_need = js_get(&value, "theirNeed")?.into();
let their_have = js_get(&value, "theirHave")?.try_into()?;
let sent_hashes = js_get(&value, "sentHashes")?.try_into()?;
Ok(am::sync::State {
shared_heads,
last_sent_heads,
their_heads,
their_need,
their_have,
sent_hashes,
})
}
}
impl TryFrom<JS> for Option<Vec<am::sync::Have>> {
type Error = JsValue;
fn try_from(value: JS) -> Result<Self, Self::Error> {
if value.0.is_null() {
Ok(None)
} else {
Ok(Some(value.try_into()?))
}
}
}
impl TryFrom<JS> for Vec<am::sync::Have> {
type Error = JsValue;
fn try_from(value: JS) -> Result<Self, Self::Error> {
let value = value.0.dyn_into::<Array>()?;
let have: Result<Vec<am::sync::Have>, JsValue> = value
.iter()
.map(|s| {
let last_sync = js_get(&s, "lastSync")?.try_into()?;
let bloom = js_get(&s, "bloom")?.try_into()?;
Ok(am::sync::Have { last_sync, bloom })
})
.collect();
let have = have?;
Ok(have)
}
}
impl TryFrom<JS> for am::sync::BloomFilter {
type Error = JsValue;
fn try_from(value: JS) -> Result<Self, Self::Error> {
let value: Uint8Array = value.0.dyn_into()?;
let value = value.to_vec();
let value = value.as_slice().try_into().map_err(to_js_err)?;
Ok(value)
}
}
impl From<&[ChangeHash]> for AR {
fn from(value: &[ChangeHash]) -> Self {
AR(value
.iter()
.map(|h| JsValue::from_str(&hex::encode(&h.0)))
.collect())
}
}
impl From<&[Change]> for AR {
fn from(value: &[Change]) -> Self {
let changes: Array = value
.iter()
.map(|c| Uint8Array::from(c.raw_bytes()))
.collect();
AR(changes)
}
}
impl From<&[am::sync::Have]> for AR {
fn from(value: &[am::sync::Have]) -> Self {
AR(value
.iter()
.map(|have| {
let last_sync: Array = have
.last_sync
.iter()
.map(|h| JsValue::from_str(&hex::encode(&h.0)))
.collect();
// FIXME - the clone and the unwrap here shouldnt be needed - look at into_bytes()
let bloom = Uint8Array::from(have.bloom.to_bytes().as_slice());
let obj: JsValue = Object::new().into();
// we can unwrap here b/c we created the object and know its not frozen
Reflect::set(&obj, &"lastSync".into(), &last_sync.into()).unwrap();
Reflect::set(&obj, &"bloom".into(), &bloom.into()).unwrap();
obj
})
.collect())
}
}
pub(crate) fn to_js_err<T: Display>(err: T) -> JsValue {
js_sys::Error::new(&std::format!("{}", err)).into()
}
pub(crate) fn js_get<J: Into<JsValue>>(obj: J, prop: &str) -> Result<JS, JsValue> {
Ok(JS(Reflect::get(&obj.into(), &prop.into())?))
}
pub(crate) fn js_set<V: Into<JsValue>>(obj: &JsValue, prop: &str, val: V) -> Result<bool, JsValue> {
Reflect::set(obj, &prop.into(), &val.into())
}
pub(crate) fn to_prop(p: JsValue) -> Result<Prop, JsValue> {
if let Some(s) = p.as_string() {
Ok(Prop::Map(s))
} else if let Some(n) = p.as_f64() {
Ok(Prop::Seq(n as usize))
} else {
Err(to_js_err("prop must me a string or number"))
}
}
pub(crate) fn to_objtype(
value: &JsValue,
datatype: &Option<String>,
) -> Option<(ObjType, Vec<(Prop, JsValue)>)> {
match datatype.as_deref() {
Some("map") => {
let map = value.clone().dyn_into::<js_sys::Object>().ok()?;
let map = js_sys::Object::keys(&map)
.iter()
.zip(js_sys::Object::values(&map).iter())
.map(|(key, val)| (key.as_string().unwrap().into(), val))
.collect();
Some((ObjType::Map, map))
}
Some("list") => {
let list = value.clone().dyn_into::<js_sys::Array>().ok()?;
let list = list
.iter()
.enumerate()
.map(|(i, e)| (i.into(), e))
.collect();
Some((ObjType::List, list))
}
Some("text") => {
let text = value.as_string()?;
let text = text
.chars()
.enumerate()
.map(|(i, ch)| (i.into(), ch.to_string().into()))
.collect();
Some((ObjType::Text, text))
}
Some(_) => None,
None => {
if let Ok(list) = value.clone().dyn_into::<js_sys::Array>() {
let list = list
.iter()
.enumerate()
.map(|(i, e)| (i.into(), e))
.collect();
Some((ObjType::List, list))
} else if let Ok(map) = value.clone().dyn_into::<js_sys::Object>() {
// FIXME unwrap
let map = js_sys::Object::keys(&map)
.iter()
.zip(js_sys::Object::values(&map).iter())
.map(|(key, val)| (key.as_string().unwrap().into(), val))
.collect();
Some((ObjType::Map, map))
} else if let Some(text) = value.as_string() {
let text = text
.chars()
.enumerate()
.map(|(i, ch)| (i.into(), ch.to_string().into()))
.collect();
Some((ObjType::Text, text))
} else {
None
}
}
}
}
pub(crate) fn get_heads(heads: Option<Array>) -> Option<Vec<ChangeHash>> {
let heads = heads?;
let heads: Result<Vec<ChangeHash>, _> =
heads.iter().map(serde_wasm_bindgen::from_value).collect();
heads.ok()
}
impl Automerge {
pub(crate) fn export_object(
&self,
obj: &ObjId,
datatype: Datatype,
heads: Option<&Vec<ChangeHash>>,
meta: &JsValue,
) -> Result<JsValue, JsValue> {
let result = if datatype.is_sequence() {
self.wrap_object(
self.export_list(obj, heads, meta)?,
datatype,
&obj.to_string().into(),
meta,
)?
} else {
self.wrap_object(
self.export_map(obj, heads, meta)?,
datatype,
&obj.to_string().into(),
meta,
)?
};
Ok(result.into())
}
pub(crate) fn export_map(
&self,
obj: &ObjId,
heads: Option<&Vec<ChangeHash>>,
meta: &JsValue,
) -> Result<Object, JsValue> {
let keys = self.doc.keys(obj);
let map = Object::new();
for k in keys {
let val_and_id = if let Some(heads) = heads {
self.doc.get_at(obj, &k, heads)
} else {
self.doc.get(obj, &k)
};
if let Ok(Some((val, id))) = val_and_id {
let subval = match val {
Value::Object(o) => self.export_object(&id, o.into(), heads, meta)?,
Value::Scalar(_) => self.export_value(alloc(&val))?,
};
Reflect::set(&map, &k.into(), &subval)?;
};
}
Ok(map)
}
pub(crate) fn export_list(
&self,
obj: &ObjId,
heads: Option<&Vec<ChangeHash>>,
meta: &JsValue,
) -> Result<Object, JsValue> {
let len = self.doc.length(obj);
let array = Array::new();
for i in 0..len {
let val_and_id = if let Some(heads) = heads {
self.doc.get_at(obj, i as usize, heads)
} else {
self.doc.get(obj, i as usize)
};
if let Ok(Some((val, id))) = val_and_id {
let subval = match val {
Value::Object(o) => self.export_object(&id, o.into(), heads, meta)?,
Value::Scalar(_) => self.export_value(alloc(&val))?,
};
array.push(&subval);
};
}
Ok(array.into())
}
pub(crate) fn export_value(
&self,
(datatype, raw_value): (Datatype, JsValue),
) -> Result<JsValue, JsValue> {
if let Some(function) = self.external_types.get(&datatype) {
let wrapped_value = function.call1(&JsValue::undefined(), &raw_value)?;
if let Ok(o) = wrapped_value.dyn_into::<Object>() {
let key = Symbol::for_(RAW_DATA_SYMBOL);
set_hidden_value(&o, &key, &raw_value)?;
let key = Symbol::for_(DATATYPE_SYMBOL);
set_hidden_value(&o, &key, datatype)?;
Ok(o.into())
} else {
Err(to_js_err(format!(
"data handler for type {} did not return a valid object",
datatype
)))
}
} else {
Ok(raw_value)
}
}
pub(crate) fn unwrap_object(
&self,
ext_val: &Object,
) -> Result<(Object, Datatype, JsValue), JsValue> {
let inner = Reflect::get(ext_val, &Symbol::for_(RAW_DATA_SYMBOL))?;
let datatype = Reflect::get(ext_val, &Symbol::for_(DATATYPE_SYMBOL))?.try_into();
let mut id = Reflect::get(ext_val, &Symbol::for_(RAW_OBJECT_SYMBOL))?;
if id.is_undefined() {
id = "_root".into();
}
let inner = inner
.dyn_into::<Object>()
.unwrap_or_else(|_| ext_val.clone());
let datatype = datatype.unwrap_or_else(|_| {
if Array::is_array(&inner) {
Datatype::List
} else {
Datatype::Map
}
});
Ok((inner, datatype, id))
}
pub(crate) fn unwrap_scalar(&self, ext_val: JsValue) -> Result<JsValue, JsValue> {
let inner = Reflect::get(&ext_val, &Symbol::for_(RAW_DATA_SYMBOL))?;
if !inner.is_undefined() {
Ok(inner)
} else {
Ok(ext_val)
}
}
fn maybe_wrap_object(
&self,
(datatype, raw_value): (Datatype, JsValue),
id: &ObjId,
meta: &JsValue,
) -> Result<JsValue, JsValue> {
if let Ok(obj) = raw_value.clone().dyn_into::<Object>() {
let result = self.wrap_object(obj, datatype, &id.to_string().into(), meta)?;
Ok(result.into())
} else {
self.export_value((datatype, raw_value))
}
}
pub(crate) fn wrap_object(
&self,
value: Object,
datatype: Datatype,
id: &JsValue,
meta: &JsValue,
) -> Result<Object, JsValue> {
let value = if let Some(function) = self.external_types.get(&datatype) {
let wrapped_value = function.call1(&JsValue::undefined(), &value)?;
let wrapped_object = wrapped_value.dyn_into::<Object>().map_err(|_| {
to_js_err(format!(
"data handler for type {} did not return a valid object",
datatype
))
})?;
set_hidden_value(&wrapped_object, &Symbol::for_(RAW_DATA_SYMBOL), value)?;
wrapped_object
} else {
value
};
set_hidden_value(&value, &Symbol::for_(DATATYPE_SYMBOL), datatype)?;
set_hidden_value(&value, &Symbol::for_(RAW_OBJECT_SYMBOL), id)?;
set_hidden_value(&value, &Symbol::for_(META_SYMBOL), meta)?;
Ok(value)
}
pub(crate) fn apply_patch_to_array(
&self,
array: &Object,
patch: &Patch,
meta: &JsValue,
) -> Result<Object, JsValue> {
let result = Array::from(array); // shallow copy
match patch {
Patch::PutSeq { index, value, .. } => {
let sub_val = self.maybe_wrap_object(alloc(&value.0), &value.1, meta)?;
Reflect::set(&result, &(*index as f64).into(), &sub_val)?;
Ok(result.into())
}
Patch::DeleteSeq { index, .. } => self.sub_splice(result, *index, 1, &[], meta),
Patch::Insert { index, values, .. } => self.sub_splice(result, *index, 0, values, meta),
Patch::Increment { prop, value, .. } => {
if let Prop::Seq(index) = prop {
let index = (*index as f64).into();
let old_val = Reflect::get(&result, &index)?;
let old_val = self.unwrap_scalar(old_val)?;
if let Some(old) = old_val.as_f64() {
let new_value: Value<'_> =
am::ScalarValue::counter(old as i64 + *value).into();
Reflect::set(&result, &index, &self.export_value(alloc(&new_value))?)?;
Ok(result.into())
} else {
Err(to_js_err("cant increment a non number value"))
}
} else {
Err(to_js_err("cant increment a key on a seq"))
}
}
Patch::DeleteMap { .. } => Err(to_js_err("cannot delete from a seq")),
Patch::PutMap { .. } => Err(to_js_err("cannot set key in seq")),
}
}
pub(crate) fn apply_patch_to_map(
&self,
map: &Object,
patch: &Patch,
meta: &JsValue,
) -> Result<Object, JsValue> {
let result = Object::assign(&Object::new(), map); // shallow copy
match patch {
Patch::PutMap { key, value, .. } => {
let sub_val = self.maybe_wrap_object(alloc(&value.0), &value.1, meta)?;
Reflect::set(&result, &key.into(), &sub_val)?;
Ok(result)
}
Patch::DeleteMap { key, .. } => {
Reflect::delete_property(&result, &key.into())?;
Ok(result)
}
Patch::Increment { prop, value, .. } => {
if let Prop::Map(key) = prop {
let key = key.into();
let old_val = Reflect::get(&result, &key)?;
let old_val = self.unwrap_scalar(old_val)?;
if let Some(old) = old_val.as_f64() {
let new_value: Value<'_> =
am::ScalarValue::counter(old as i64 + *value).into();
Reflect::set(&result, &key, &self.export_value(alloc(&new_value))?)?;
Ok(result)
} else {
Err(to_js_err("cant increment a non number value"))
}
} else {
Err(to_js_err("cant increment an index on a map"))
}
}
Patch::Insert { .. } => Err(to_js_err("cannot insert into map")),
Patch::DeleteSeq { .. } => Err(to_js_err("cannot splice a map")),
Patch::PutSeq { .. } => Err(to_js_err("cannot array index a map")),
}
}
pub(crate) fn apply_patch(
&self,
obj: Object,
patch: &Patch,
depth: usize,
meta: &JsValue,
) -> Result<Object, JsValue> {
let (inner, datatype, id) = self.unwrap_object(&obj)?;
let prop = patch.path().get(depth).map(|p| prop_to_js(&p.1));
let result = if let Some(prop) = prop {
if let Ok(sub_obj) = Reflect::get(&inner, &prop)?.dyn_into::<Object>() {
let new_value = self.apply_patch(sub_obj, patch, depth + 1, meta)?;
let result = shallow_copy(&inner);
Reflect::set(&result, &prop, &new_value)?;
Ok(result)
} else {
// if a patch is trying to access a deleted object make no change
// short circuit the wrap process
return Ok(obj);
}
} else if Array::is_array(&inner) {
self.apply_patch_to_array(&inner, patch, meta)
} else {
self.apply_patch_to_map(&inner, patch, meta)
}?;
self.wrap_object(result, datatype, &id, meta)
}
fn sub_splice(
&self,
o: Array,
index: usize,
num_del: usize,
values: &[(Value<'_>, ObjId)],
meta: &JsValue,
) -> Result<Object, JsValue> {
let args: Array = values
.iter()
.map(|v| self.maybe_wrap_object(alloc(&v.0), &v.1, meta))
.collect::<Result<_, _>>()?;
args.unshift(&(num_del as u32).into());
args.unshift(&(index as u32).into());
let method = Reflect::get(&o, &"splice".into())?.dyn_into::<Function>()?;
Reflect::apply(&method, &o, &args)?;
Ok(o.into())
}
}
pub(crate) fn alloc(value: &Value<'_>) -> (Datatype, JsValue) {
match value {
am::Value::Object(o) => match o {
ObjType::Map => (Datatype::Map, Object::new().into()),
ObjType::Table => (Datatype::Table, Object::new().into()),
ObjType::List => (Datatype::List, Array::new().into()),
ObjType::Text => (Datatype::Text, Array::new().into()),
},
am::Value::Scalar(s) => match s.as_ref() {
am::ScalarValue::Bytes(v) => (Datatype::Bytes, Uint8Array::from(v.as_slice()).into()),
am::ScalarValue::Str(v) => (Datatype::Str, v.to_string().into()),
am::ScalarValue::Int(v) => (Datatype::Int, (*v as f64).into()),
am::ScalarValue::Uint(v) => (Datatype::Uint, (*v as f64).into()),
am::ScalarValue::F64(v) => (Datatype::F64, (*v).into()),
am::ScalarValue::Counter(v) => (Datatype::Counter, (f64::from(v)).into()),
am::ScalarValue::Timestamp(v) => (
Datatype::Timestamp,
js_sys::Date::new(&(*v as f64).into()).into(),
),
am::ScalarValue::Boolean(v) => (Datatype::Boolean, (*v).into()),
am::ScalarValue::Null => (Datatype::Null, JsValue::null()),
am::ScalarValue::Unknown { bytes, type_code } => (
Datatype::Unknown(*type_code),
Uint8Array::from(bytes.as_slice()).into(),
),
},
}
}
fn set_hidden_value<V: Into<JsValue>>(o: &Object, key: &Symbol, value: V) -> Result<(), JsValue> {
let definition = Object::new();
js_set(&definition, "value", &value.into())?;
js_set(&definition, "writable", false)?;
js_set(&definition, "enumerable", false)?;
js_set(&definition, "configurable", false)?;
Object::define_property(o, &key.into(), &definition);
Ok(())
}
fn shallow_copy(obj: &Object) -> Object {
if Array::is_array(obj) {
Array::from(obj).into()
} else {
Object::assign(&Object::new(), obj)
}
}
fn prop_to_js(prop: &Prop) -> JsValue {
match prop {
Prop::Map(key) => key.into(),
Prop::Seq(index) => (*index as f64).into(),
}
}

View file

@ -1,874 +0,0 @@
#![doc(
html_logo_url = "https://raw.githubusercontent.com/automerge/automerge-rs/main/img/brandmark.svg",
html_favicon_url = "https:///raw.githubusercontent.com/automerge/automerge-rs/main/img/favicon.ico"
)]
#![warn(
missing_debug_implementations,
// missing_docs, // TODO: add documentation!
rust_2021_compatibility,
rust_2018_idioms,
unreachable_pub,
bad_style,
const_err,
dead_code,
improper_ctypes,
non_shorthand_field_patterns,
no_mangle_generic_items,
overflowing_literals,
path_statements,
patterns_in_fns_without_body,
private_in_public,
unconditional_recursion,
unused,
unused_allocation,
unused_comparisons,
unused_parens,
while_true
)]
#![allow(clippy::unused_unit)]
use am::transaction::CommitOptions;
use am::transaction::Transactable;
use automerge as am;
use automerge::{Change, ObjId, ObjType, Prop, Value, ROOT};
use js_sys::{Array, Function, Object, Uint8Array};
use serde::ser::Serialize;
use std::collections::HashMap;
use std::convert::TryInto;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
mod interop;
mod observer;
mod sync;
mod value;
use observer::Observer;
use interop::{alloc, get_heads, js_get, js_set, to_js_err, to_objtype, to_prop, AR, JS};
use sync::SyncState;
use value::Datatype;
#[allow(unused_macros)]
macro_rules! log {
( $( $t:tt )* ) => {
web_sys::console::log_1(&format!( $( $t )* ).into());
};
}
type AutoCommit = am::AutoCommitWithObs<Observer>;
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen]
#[derive(Debug)]
pub struct Automerge {
doc: AutoCommit,
external_types: HashMap<Datatype, Function>,
}
#[wasm_bindgen]
impl Automerge {
pub fn new(actor: Option<String>) -> Result<Automerge, JsValue> {
let mut doc = AutoCommit::default();
if let Some(a) = actor {
let a = automerge::ActorId::from(hex::decode(a).map_err(to_js_err)?.to_vec());
doc.set_actor(a);
}
Ok(Automerge {
doc,
external_types: HashMap::default(),
})
}
#[allow(clippy::should_implement_trait)]
pub fn clone(&mut self, actor: Option<String>) -> Result<Automerge, JsValue> {
let mut automerge = Automerge {
doc: self.doc.clone(),
external_types: self.external_types.clone(),
};
if let Some(s) = actor {
let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec());
automerge.doc.set_actor(actor);
}
Ok(automerge)
}
pub fn fork(&mut self, actor: Option<String>) -> Result<Automerge, JsValue> {
let mut automerge = Automerge {
doc: self.doc.fork(),
external_types: self.external_types.clone(),
};
if let Some(s) = actor {
let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec());
automerge.doc.set_actor(actor);
}
Ok(automerge)
}
#[wasm_bindgen(js_name = forkAt)]
pub fn fork_at(&mut self, heads: JsValue, actor: Option<String>) -> Result<Automerge, JsValue> {
let deps: Vec<_> = JS(heads).try_into()?;
let mut automerge = Automerge {
doc: self.doc.fork_at(&deps)?,
external_types: self.external_types.clone(),
};
if let Some(s) = actor {
let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec());
automerge.doc.set_actor(actor);
}
Ok(automerge)
}
pub fn free(self) {}
#[wasm_bindgen(js_name = pendingOps)]
pub fn pending_ops(&self) -> JsValue {
(self.doc.pending_ops() as u32).into()
}
pub fn commit(&mut self, message: Option<String>, time: Option<f64>) -> JsValue {
let mut commit_opts = CommitOptions::default();
if let Some(message) = message {
commit_opts.set_message(message);
}
if let Some(time) = time {
commit_opts.set_time(time as i64);
}
let hash = self.doc.commit_with(commit_opts);
JsValue::from_str(&hex::encode(&hash.0))
}
pub fn merge(&mut self, other: &mut Automerge) -> Result<Array, JsValue> {
let heads = self.doc.merge(&mut other.doc)?;
let heads: Array = heads
.iter()
.map(|h| JsValue::from_str(&hex::encode(&h.0)))
.collect();
Ok(heads)
}
pub fn rollback(&mut self) -> f64 {
self.doc.rollback() as f64
}
pub fn keys(&self, obj: JsValue, heads: Option<Array>) -> Result<Array, JsValue> {
let obj = self.import(obj)?;
let result = if let Some(heads) = get_heads(heads) {
self.doc
.keys_at(&obj, &heads)
.map(|s| JsValue::from_str(&s))
.collect()
} else {
self.doc.keys(&obj).map(|s| JsValue::from_str(&s)).collect()
};
Ok(result)
}
pub fn text(&self, obj: JsValue, heads: Option<Array>) -> Result<String, JsValue> {
let obj = self.import(obj)?;
if let Some(heads) = get_heads(heads) {
Ok(self.doc.text_at(&obj, &heads)?)
} else {
Ok(self.doc.text(&obj)?)
}
}
pub fn splice(
&mut self,
obj: JsValue,
start: f64,
delete_count: f64,
text: JsValue,
) -> Result<(), JsValue> {
let obj = self.import(obj)?;
let start = start as usize;
let delete_count = delete_count as usize;
let mut vals = vec![];
if let Some(t) = text.as_string() {
self.doc.splice_text(&obj, start, delete_count, &t)?;
} else {
if let Ok(array) = text.dyn_into::<Array>() {
for i in array.iter() {
let value = self
.import_scalar(&i, &None)
.ok_or_else(|| to_js_err("expected scalar"))?;
vals.push(value);
}
}
self.doc
.splice(&obj, start, delete_count, vals.into_iter())?;
}
Ok(())
}
pub fn push(&mut self, obj: JsValue, value: JsValue, datatype: JsValue) -> Result<(), JsValue> {
let obj = self.import(obj)?;
let value = self
.import_scalar(&value, &datatype.as_string())
.ok_or_else(|| to_js_err("invalid scalar value"))?;
let index = self.doc.length(&obj);
self.doc.insert(&obj, index, value)?;
Ok(())
}
#[wasm_bindgen(js_name = pushObject)]
pub fn push_object(&mut self, obj: JsValue, value: JsValue) -> Result<Option<String>, JsValue> {
let obj = self.import(obj)?;
let (value, subvals) =
to_objtype(&value, &None).ok_or_else(|| to_js_err("expected object"))?;
let index = self.doc.length(&obj);
let opid = self.doc.insert_object(&obj, index, value)?;
self.subset(&opid, subvals)?;
Ok(opid.to_string().into())
}
pub fn insert(
&mut self,
obj: JsValue,
index: f64,
value: JsValue,
datatype: JsValue,
) -> Result<(), JsValue> {
let obj = self.import(obj)?;
let index = index as f64;
let value = self
.import_scalar(&value, &datatype.as_string())
.ok_or_else(|| to_js_err("expected scalar value"))?;
self.doc.insert(&obj, index as usize, value)?;
Ok(())
}
#[wasm_bindgen(js_name = insertObject)]
pub fn insert_object(
&mut self,
obj: JsValue,
index: f64,
value: JsValue,
) -> Result<Option<String>, JsValue> {
let obj = self.import(obj)?;
let index = index as f64;
let (value, subvals) =
to_objtype(&value, &None).ok_or_else(|| to_js_err("expected object"))?;
let opid = self.doc.insert_object(&obj, index as usize, value)?;
self.subset(&opid, subvals)?;
Ok(opid.to_string().into())
}
pub fn put(
&mut self,
obj: JsValue,
prop: JsValue,
value: JsValue,
datatype: JsValue,
) -> Result<(), JsValue> {
let obj = self.import(obj)?;
let prop = self.import_prop(prop)?;
let value = self
.import_scalar(&value, &datatype.as_string())
.ok_or_else(|| to_js_err("expected scalar value"))?;
self.doc.put(&obj, prop, value)?;
Ok(())
}
#[wasm_bindgen(js_name = putObject)]
pub fn put_object(
&mut self,
obj: JsValue,
prop: JsValue,
value: JsValue,
) -> Result<JsValue, JsValue> {
let obj = self.import(obj)?;
let prop = self.import_prop(prop)?;
let (value, subvals) =
to_objtype(&value, &None).ok_or_else(|| to_js_err("expected object"))?;
let opid = self.doc.put_object(&obj, prop, value)?;
self.subset(&opid, subvals)?;
Ok(opid.to_string().into())
}
fn subset(&mut self, obj: &am::ObjId, vals: Vec<(am::Prop, JsValue)>) -> Result<(), JsValue> {
for (p, v) in vals {
let (value, subvals) = self.import_value(&v, None)?;
//let opid = self.0.set(id, p, value)?;
let opid = match (p, value) {
(Prop::Map(s), Value::Object(objtype)) => {
Some(self.doc.put_object(obj, s, objtype)?)
}
(Prop::Map(s), Value::Scalar(scalar)) => {
self.doc.put(obj, s, scalar.into_owned())?;
None
}
(Prop::Seq(i), Value::Object(objtype)) => {
Some(self.doc.insert_object(obj, i, objtype)?)
}
(Prop::Seq(i), Value::Scalar(scalar)) => {
self.doc.insert(obj, i, scalar.into_owned())?;
None
}
};
if let Some(opid) = opid {
self.subset(&opid, subvals)?;
}
}
Ok(())
}
pub fn increment(
&mut self,
obj: JsValue,
prop: JsValue,
value: JsValue,
) -> Result<(), JsValue> {
let obj = self.import(obj)?;
let prop = self.import_prop(prop)?;
let value: f64 = value
.as_f64()
.ok_or_else(|| to_js_err("increment needs a numeric value"))?;
self.doc.increment(&obj, prop, value as i64)?;
Ok(())
}
#[wasm_bindgen(js_name = get)]
pub fn get(
&self,
obj: JsValue,
prop: JsValue,
heads: Option<Array>,
) -> Result<JsValue, JsValue> {
let obj = self.import(obj)?;
let prop = to_prop(prop);
let heads = get_heads(heads);
if let Ok(prop) = prop {
let value = if let Some(h) = heads {
self.doc.get_at(&obj, prop, &h)?
} else {
self.doc.get(&obj, prop)?
};
if let Some((value, id)) = value {
match alloc(&value) {
(datatype, js_value) if datatype.is_scalar() => Ok(js_value),
_ => Ok(id.to_string().into()),
}
} else {
Ok(JsValue::undefined())
}
} else {
Ok(JsValue::undefined())
}
}
#[wasm_bindgen(js_name = getWithType)]
pub fn get_with_type(
&self,
obj: JsValue,
prop: JsValue,
heads: Option<Array>,
) -> Result<JsValue, JsValue> {
let obj = self.import(obj)?;
let prop = to_prop(prop);
let heads = get_heads(heads);
if let Ok(prop) = prop {
let value = if let Some(h) = heads {
self.doc.get_at(&obj, prop, &h)?
} else {
self.doc.get(&obj, prop)?
};
if let Some(value) = value {
match &value {
(Value::Object(obj_type), obj_id) => {
let result = Array::new();
result.push(&obj_type.to_string().into());
result.push(&obj_id.to_string().into());
Ok(result.into())
}
(Value::Scalar(_), _) => {
let result = Array::new();
let (datatype, value) = alloc(&value.0);
result.push(&datatype.into());
result.push(&value);
Ok(result.into())
}
}
} else {
Ok(JsValue::null())
}
} else {
Ok(JsValue::null())
}
}
#[wasm_bindgen(js_name = getAll)]
pub fn get_all(
&self,
obj: JsValue,
arg: JsValue,
heads: Option<Array>,
) -> Result<Array, JsValue> {
let obj = self.import(obj)?;
let result = Array::new();
let prop = to_prop(arg);
if let Ok(prop) = prop {
let values = if let Some(heads) = get_heads(heads) {
self.doc.get_all_at(&obj, prop, &heads)
} else {
self.doc.get_all(&obj, prop)
}
.map_err(to_js_err)?;
for (value, id) in values {
let sub = Array::new();
let (datatype, js_value) = alloc(&value);
sub.push(&datatype.into());
if value.is_scalar() {
sub.push(&js_value);
}
sub.push(&id.to_string().into());
result.push(&JsValue::from(&sub));
}
}
Ok(result)
}
#[wasm_bindgen(js_name = enablePatches)]
pub fn enable_patches(&mut self, enable: JsValue) -> Result<(), JsValue> {
let enable = enable
.as_bool()
.ok_or_else(|| to_js_err("must pass a bool to enable_patches"))?;
self.doc.observer().enable(enable);
Ok(())
}
#[wasm_bindgen(js_name = registerDatatype)]
pub fn register_datatype(
&mut self,
datatype: JsValue,
function: JsValue,
) -> Result<(), JsValue> {
let datatype = Datatype::try_from(datatype)?;
if let Ok(function) = function.dyn_into::<Function>() {
self.external_types.insert(datatype, function);
} else {
self.external_types.remove(&datatype);
}
Ok(())
}
#[wasm_bindgen(js_name = applyPatches)]
pub fn apply_patches(
&mut self,
object: JsValue,
meta: JsValue,
callback: JsValue,
) -> Result<JsValue, JsValue> {
let mut object = object.dyn_into::<Object>()?;
let patches = self.doc.observer().take_patches();
let callback = callback.dyn_into::<Function>().ok();
// even if there are no patches we may need to update the meta object
// which requires that we update the object too
if patches.is_empty() && !meta.is_undefined() {
let (obj, datatype, id) = self.unwrap_object(&object)?;
object = Object::assign(&Object::new(), &obj);
object = self.wrap_object(object, datatype, &id, &meta)?;
}
for p in patches {
if let Some(c) = &callback {
let before = object.clone();
object = self.apply_patch(object, &p, 0, &meta)?;
c.call3(&JsValue::undefined(), &p.try_into()?, &before, &object)?;
} else {
object = self.apply_patch(object, &p, 0, &meta)?;
}
}
Ok(object.into())
}
#[wasm_bindgen(js_name = popPatches)]
pub fn pop_patches(&mut self) -> Result<Array, JsValue> {
// transactions send out observer updates as they occur, not waiting for them to be
// committed.
// If we pop the patches then we won't be able to revert them.
let patches = self.doc.observer().take_patches();
let result = Array::new();
for p in patches {
result.push(&p.try_into()?);
}
Ok(result)
}
pub fn length(&self, obj: JsValue, heads: Option<Array>) -> Result<f64, JsValue> {
let obj = self.import(obj)?;
if let Some(heads) = get_heads(heads) {
Ok(self.doc.length_at(&obj, &heads) as f64)
} else {
Ok(self.doc.length(&obj) as f64)
}
}
pub fn delete(&mut self, obj: JsValue, prop: JsValue) -> Result<(), JsValue> {
let obj = self.import(obj)?;
let prop = to_prop(prop)?;
self.doc.delete(&obj, prop).map_err(to_js_err)?;
Ok(())
}
pub fn save(&mut self) -> Uint8Array {
Uint8Array::from(self.doc.save().as_slice())
}
#[wasm_bindgen(js_name = saveIncremental)]
pub fn save_incremental(&mut self) -> Uint8Array {
let bytes = self.doc.save_incremental();
Uint8Array::from(bytes.as_slice())
}
#[wasm_bindgen(js_name = loadIncremental)]
pub fn load_incremental(&mut self, data: Uint8Array) -> Result<f64, JsValue> {
let data = data.to_vec();
let len = self.doc.load_incremental(&data).map_err(to_js_err)?;
Ok(len as f64)
}
#[wasm_bindgen(js_name = applyChanges)]
pub fn apply_changes(&mut self, changes: JsValue) -> Result<(), JsValue> {
let changes: Vec<_> = JS(changes).try_into()?;
self.doc.apply_changes(changes).map_err(to_js_err)?;
Ok(())
}
#[wasm_bindgen(js_name = getChanges)]
pub fn get_changes(&mut self, have_deps: JsValue) -> Result<Array, JsValue> {
let deps: Vec<_> = JS(have_deps).try_into()?;
let changes = self.doc.get_changes(&deps)?;
let changes: Array = changes
.iter()
.map(|c| Uint8Array::from(c.raw_bytes()))
.collect();
Ok(changes)
}
#[wasm_bindgen(js_name = getChangeByHash)]
pub fn get_change_by_hash(&mut self, hash: JsValue) -> Result<JsValue, JsValue> {
let hash = serde_wasm_bindgen::from_value(hash).map_err(to_js_err)?;
let change = self.doc.get_change_by_hash(&hash);
if let Some(c) = change {
Ok(Uint8Array::from(c.raw_bytes()).into())
} else {
Ok(JsValue::null())
}
}
#[wasm_bindgen(js_name = getChangesAdded)]
pub fn get_changes_added(&mut self, other: &mut Automerge) -> Result<Array, JsValue> {
let changes = self.doc.get_changes_added(&mut other.doc);
let changes: Array = changes
.iter()
.map(|c| Uint8Array::from(c.raw_bytes()))
.collect();
Ok(changes)
}
#[wasm_bindgen(js_name = getHeads)]
pub fn get_heads(&mut self) -> Array {
let heads = self.doc.get_heads();
let heads: Array = heads
.iter()
.map(|h| JsValue::from_str(&hex::encode(&h.0)))
.collect();
heads
}
#[wasm_bindgen(js_name = getActorId)]
pub fn get_actor_id(&self) -> String {
let actor = self.doc.get_actor();
actor.to_string()
}
#[wasm_bindgen(js_name = getLastLocalChange)]
pub fn get_last_local_change(&mut self) -> Result<JsValue, JsValue> {
if let Some(change) = self.doc.get_last_local_change() {
Ok(Uint8Array::from(change.raw_bytes()).into())
} else {
Ok(JsValue::null())
}
}
pub fn dump(&mut self) {
self.doc.dump()
}
#[wasm_bindgen(js_name = getMissingDeps)]
pub fn get_missing_deps(&mut self, heads: Option<Array>) -> Result<Array, JsValue> {
let heads = get_heads(heads).unwrap_or_default();
let deps = self.doc.get_missing_deps(&heads);
let deps: Array = deps
.iter()
.map(|h| JsValue::from_str(&hex::encode(&h.0)))
.collect();
Ok(deps)
}
#[wasm_bindgen(js_name = receiveSyncMessage)]
pub fn receive_sync_message(
&mut self,
state: &mut SyncState,
message: Uint8Array,
) -> Result<(), JsValue> {
let message = message.to_vec();
let message = am::sync::Message::decode(message.as_slice()).map_err(to_js_err)?;
self.doc
.receive_sync_message(&mut state.0, message)
.map_err(to_js_err)?;
Ok(())
}
#[wasm_bindgen(js_name = generateSyncMessage)]
pub fn generate_sync_message(&mut self, state: &mut SyncState) -> Result<JsValue, JsValue> {
if let Some(message) = self.doc.generate_sync_message(&mut state.0) {
Ok(Uint8Array::from(message.encode().as_slice()).into())
} else {
Ok(JsValue::null())
}
}
#[wasm_bindgen(js_name = toJS)]
pub fn to_js(&self, meta: JsValue) -> Result<JsValue, JsValue> {
self.export_object(&ROOT, Datatype::Map, None, &meta)
}
pub fn materialize(
&mut self,
obj: JsValue,
heads: Option<Array>,
meta: JsValue,
) -> Result<JsValue, JsValue> {
let obj = self.import(obj).unwrap_or(ROOT);
let heads = get_heads(heads);
let obj_type = self
.doc
.object_type(&obj)
.ok_or_else(|| to_js_err(format!("invalid obj {}", obj)))?;
let _patches = self.doc.observer().take_patches(); // throw away patches
self.export_object(&obj, obj_type.into(), heads.as_ref(), &meta)
}
fn import(&self, id: JsValue) -> Result<ObjId, JsValue> {
if let Some(s) = id.as_string() {
if let Some(post) = s.strip_prefix('/') {
let mut obj = ROOT;
let mut is_map = true;
let parts = post.split('/');
for prop in parts {
if prop.is_empty() {
break;
}
let val = if is_map {
self.doc.get(obj, prop)?
} else {
self.doc.get(obj, am::Prop::Seq(prop.parse().unwrap()))?
};
match val {
Some((am::Value::Object(ObjType::Map), id)) => {
is_map = true;
obj = id;
}
Some((am::Value::Object(ObjType::Table), id)) => {
is_map = true;
obj = id;
}
Some((am::Value::Object(_), id)) => {
is_map = false;
obj = id;
}
None => return Err(to_js_err(format!("invalid path '{}'", s))),
_ => return Err(to_js_err(format!("path '{}' is not an object", s))),
};
}
Ok(obj)
} else {
Ok(self.doc.import(&s)?)
}
} else {
Err(to_js_err("invalid objid"))
}
}
fn import_prop(&self, prop: JsValue) -> Result<Prop, JsValue> {
if let Some(s) = prop.as_string() {
Ok(s.into())
} else if let Some(n) = prop.as_f64() {
Ok((n as usize).into())
} else {
Err(to_js_err(format!("invalid prop {:?}", prop)))
}
}
fn import_scalar(&self, value: &JsValue, datatype: &Option<String>) -> Option<am::ScalarValue> {
match datatype.as_deref() {
Some("boolean") => value.as_bool().map(am::ScalarValue::Boolean),
Some("int") => value.as_f64().map(|v| am::ScalarValue::Int(v as i64)),
Some("uint") => value.as_f64().map(|v| am::ScalarValue::Uint(v as u64)),
Some("str") => value.as_string().map(|v| am::ScalarValue::Str(v.into())),
Some("f64") => value.as_f64().map(am::ScalarValue::F64),
Some("bytes") => Some(am::ScalarValue::Bytes(
value.clone().dyn_into::<Uint8Array>().unwrap().to_vec(),
)),
Some("counter") => value.as_f64().map(|v| am::ScalarValue::counter(v as i64)),
Some("timestamp") => {
if let Some(v) = value.as_f64() {
Some(am::ScalarValue::Timestamp(v as i64))
} else if let Ok(d) = value.clone().dyn_into::<js_sys::Date>() {
Some(am::ScalarValue::Timestamp(d.get_time() as i64))
} else {
None
}
}
Some("null") => Some(am::ScalarValue::Null),
Some(_) => None,
None => {
if value.is_null() {
Some(am::ScalarValue::Null)
} else if let Some(b) = value.as_bool() {
Some(am::ScalarValue::Boolean(b))
} else if let Some(s) = value.as_string() {
Some(am::ScalarValue::Str(s.into()))
} else if let Some(n) = value.as_f64() {
if (n.round() - n).abs() < f64::EPSILON {
Some(am::ScalarValue::Int(n as i64))
} else {
Some(am::ScalarValue::F64(n))
}
} else if let Ok(d) = value.clone().dyn_into::<js_sys::Date>() {
Some(am::ScalarValue::Timestamp(d.get_time() as i64))
} else if let Ok(o) = &value.clone().dyn_into::<Uint8Array>() {
Some(am::ScalarValue::Bytes(o.to_vec()))
} else {
None
}
}
}
}
fn import_value(
&self,
value: &JsValue,
datatype: Option<String>,
) -> Result<(Value<'static>, Vec<(Prop, JsValue)>), JsValue> {
match self.import_scalar(value, &datatype) {
Some(val) => Ok((val.into(), vec![])),
None => {
if let Some((o, subvals)) = to_objtype(value, &datatype) {
Ok((o.into(), subvals))
} else {
web_sys::console::log_2(&"Invalid value".into(), value);
Err(to_js_err("invalid value"))
}
}
}
}
}
#[wasm_bindgen(js_name = create)]
pub fn init(actor: Option<String>) -> Result<Automerge, JsValue> {
console_error_panic_hook::set_once();
Automerge::new(actor)
}
#[wasm_bindgen(js_name = load)]
pub fn load(data: Uint8Array, actor: Option<String>) -> Result<Automerge, JsValue> {
let data = data.to_vec();
let mut doc = AutoCommit::load(&data).map_err(to_js_err)?;
if let Some(s) = actor {
let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec());
doc.set_actor(actor);
}
Ok(Automerge {
doc,
external_types: HashMap::default(),
})
}
#[wasm_bindgen(js_name = encodeChange)]
pub fn encode_change(change: JsValue) -> Result<Uint8Array, JsValue> {
// Alex: Technically we should be using serde_wasm_bindgen::from_value instead of into_serde.
// Unfortunately serde_wasm_bindgen::from_value fails for some inscrutable reason, so instead
// we use into_serde (sorry to future me).
#[allow(deprecated)]
let change: am::ExpandedChange = change.into_serde().map_err(to_js_err)?;
let change: Change = change.into();
Ok(Uint8Array::from(change.raw_bytes()))
}
#[wasm_bindgen(js_name = decodeChange)]
pub fn decode_change(change: Uint8Array) -> Result<JsValue, JsValue> {
let change = Change::from_bytes(change.to_vec()).map_err(to_js_err)?;
let change: am::ExpandedChange = change.decode();
let serializer = serde_wasm_bindgen::Serializer::json_compatible();
change.serialize(&serializer).map_err(to_js_err)
}
#[wasm_bindgen(js_name = initSyncState)]
pub fn init_sync_state() -> SyncState {
SyncState(am::sync::State::new())
}
// this is needed to be compatible with the automerge-js api
#[wasm_bindgen(js_name = importSyncState)]
pub fn import_sync_state(state: JsValue) -> Result<SyncState, JsValue> {
Ok(SyncState(JS(state).try_into()?))
}
// this is needed to be compatible with the automerge-js api
#[wasm_bindgen(js_name = exportSyncState)]
pub fn export_sync_state(state: SyncState) -> JsValue {
JS::from(state.0).into()
}
#[wasm_bindgen(js_name = encodeSyncMessage)]
pub fn encode_sync_message(message: JsValue) -> Result<Uint8Array, JsValue> {
let heads = js_get(&message, "heads")?.try_into()?;
let need = js_get(&message, "need")?.try_into()?;
let changes = js_get(&message, "changes")?.try_into()?;
let have = js_get(&message, "have")?.try_into()?;
Ok(Uint8Array::from(
am::sync::Message {
heads,
need,
have,
changes,
}
.encode()
.as_slice(),
))
}
#[wasm_bindgen(js_name = decodeSyncMessage)]
pub fn decode_sync_message(msg: Uint8Array) -> Result<JsValue, JsValue> {
let data = msg.to_vec();
let msg = am::sync::Message::decode(&data).map_err(to_js_err)?;
let heads = AR::from(msg.heads.as_slice());
let need = AR::from(msg.need.as_slice());
let changes = AR::from(msg.changes.as_slice());
let have = AR::from(msg.have.as_slice());
let obj = Object::new().into();
js_set(&obj, "heads", heads)?;
js_set(&obj, "need", need)?;
js_set(&obj, "have", have)?;
js_set(&obj, "changes", changes)?;
Ok(obj)
}
#[wasm_bindgen(js_name = encodeSyncState)]
pub fn encode_sync_state(state: SyncState) -> Result<Uint8Array, JsValue> {
let state = state.0;
Ok(Uint8Array::from(state.encode().as_slice()))
}
#[wasm_bindgen(js_name = decodeSyncState)]
pub fn decode_sync_state(data: Uint8Array) -> Result<SyncState, JsValue> {
SyncState::decode(data)
}

View file

@ -1,313 +0,0 @@
#![allow(dead_code)]
use crate::interop::{alloc, js_set};
use automerge::{ObjId, OpObserver, Parents, Prop, Value};
use js_sys::{Array, Object};
use wasm_bindgen::prelude::*;
#[derive(Debug, Clone, Default)]
pub(crate) struct Observer {
enabled: bool,
patches: Vec<Patch>,
}
impl Observer {
pub(crate) fn take_patches(&mut self) -> Vec<Patch> {
std::mem::take(&mut self.patches)
}
pub(crate) fn enable(&mut self, enable: bool) {
if self.enabled && !enable {
self.patches.truncate(0)
}
self.enabled = enable;
}
}
#[derive(Debug, Clone)]
pub(crate) enum Patch {
PutMap {
obj: ObjId,
path: Vec<(ObjId, Prop)>,
key: String,
value: (Value<'static>, ObjId),
conflict: bool,
},
PutSeq {
obj: ObjId,
path: Vec<(ObjId, Prop)>,
index: usize,
value: (Value<'static>, ObjId),
conflict: bool,
},
Insert {
obj: ObjId,
path: Vec<(ObjId, Prop)>,
index: usize,
values: Vec<(Value<'static>, ObjId)>,
},
Increment {
obj: ObjId,
path: Vec<(ObjId, Prop)>,
prop: Prop,
value: i64,
},
DeleteMap {
obj: ObjId,
path: Vec<(ObjId, Prop)>,
key: String,
},
DeleteSeq {
obj: ObjId,
path: Vec<(ObjId, Prop)>,
index: usize,
length: usize,
},
}
impl OpObserver for Observer {
fn insert(
&mut self,
mut parents: Parents<'_>,
obj: ObjId,
index: usize,
tagged_value: (Value<'_>, ObjId),
) {
if self.enabled {
let value = (tagged_value.0.to_owned(), tagged_value.1);
if let Some(Patch::Insert {
obj: tail_obj,
index: tail_index,
values,
..
}) = self.patches.last_mut()
{
if tail_obj == &obj && *tail_index + values.len() == index {
values.push(value);
return;
}
}
let path = parents.path();
let patch = Patch::Insert {
path,
obj,
index,
values: vec![value],
};
self.patches.push(patch);
}
}
fn put(
&mut self,
mut parents: Parents<'_>,
obj: ObjId,
prop: Prop,
tagged_value: (Value<'_>, ObjId),
conflict: bool,
) {
if self.enabled {
let path = parents.path();
let value = (tagged_value.0.to_owned(), tagged_value.1);
let patch = match prop {
Prop::Map(key) => Patch::PutMap {
path,
obj,
key,
value,
conflict,
},
Prop::Seq(index) => Patch::PutSeq {
path,
obj,
index,
value,
conflict,
},
};
self.patches.push(patch);
}
}
fn increment(
&mut self,
mut parents: Parents<'_>,
obj: ObjId,
prop: Prop,
tagged_value: (i64, ObjId),
) {
if self.enabled {
let path = parents.path();
let value = tagged_value.0;
self.patches.push(Patch::Increment {
path,
obj,
prop,
value,
})
}
}
fn delete(&mut self, mut parents: Parents<'_>, obj: ObjId, prop: Prop) {
if self.enabled {
let path = parents.path();
let patch = match prop {
Prop::Map(key) => Patch::DeleteMap { path, obj, key },
Prop::Seq(index) => Patch::DeleteSeq {
path,
obj,
index,
length: 1,
},
};
self.patches.push(patch)
}
}
fn merge(&mut self, other: &Self) {
self.patches.extend_from_slice(other.patches.as_slice())
}
fn branch(&self) -> Self {
Observer {
patches: vec![],
enabled: self.enabled,
}
}
}
fn prop_to_js(p: &Prop) -> JsValue {
match p {
Prop::Map(key) => JsValue::from_str(key),
Prop::Seq(index) => JsValue::from_f64(*index as f64),
}
}
fn export_path(path: &[(ObjId, Prop)], end: &Prop) -> Array {
let result = Array::new();
for p in path {
result.push(&prop_to_js(&p.1));
}
result.push(&prop_to_js(end));
result
}
impl Patch {
pub(crate) fn path(&self) -> &[(ObjId, Prop)] {
match &self {
Self::PutMap { path, .. } => path.as_slice(),
Self::PutSeq { path, .. } => path.as_slice(),
Self::Increment { path, .. } => path.as_slice(),
Self::Insert { path, .. } => path.as_slice(),
Self::DeleteMap { path, .. } => path.as_slice(),
Self::DeleteSeq { path, .. } => path.as_slice(),
}
}
pub(crate) fn obj(&self) -> &ObjId {
match &self {
Self::PutMap { obj, .. } => obj,
Self::PutSeq { obj, .. } => obj,
Self::Increment { obj, .. } => obj,
Self::Insert { obj, .. } => obj,
Self::DeleteMap { obj, .. } => obj,
Self::DeleteSeq { obj, .. } => obj,
}
}
}
impl TryFrom<Patch> for JsValue {
type Error = JsValue;
fn try_from(p: Patch) -> Result<Self, Self::Error> {
let result = Object::new();
match p {
Patch::PutMap {
path,
key,
value,
conflict,
..
} => {
js_set(&result, "action", "put")?;
js_set(
&result,
"path",
export_path(path.as_slice(), &Prop::Map(key)),
)?;
js_set(&result, "value", alloc(&value.0).1)?;
js_set(&result, "conflict", &JsValue::from_bool(conflict))?;
Ok(result.into())
}
Patch::PutSeq {
path,
index,
value,
conflict,
..
} => {
js_set(&result, "action", "put")?;
js_set(
&result,
"path",
export_path(path.as_slice(), &Prop::Seq(index)),
)?;
js_set(&result, "value", alloc(&value.0).1)?;
js_set(&result, "conflict", &JsValue::from_bool(conflict))?;
Ok(result.into())
}
Patch::Insert {
path,
index,
values,
..
} => {
js_set(&result, "action", "splice")?;
js_set(
&result,
"path",
export_path(path.as_slice(), &Prop::Seq(index)),
)?;
js_set(
&result,
"values",
values.iter().map(|v| alloc(&v.0).1).collect::<Array>(),
)?;
Ok(result.into())
}
Patch::Increment {
path, prop, value, ..
} => {
js_set(&result, "action", "inc")?;
js_set(&result, "path", export_path(path.as_slice(), &prop))?;
js_set(&result, "value", &JsValue::from_f64(value as f64))?;
Ok(result.into())
}
Patch::DeleteMap { path, key, .. } => {
js_set(&result, "action", "del")?;
js_set(
&result,
"path",
export_path(path.as_slice(), &Prop::Map(key)),
)?;
Ok(result.into())
}
Patch::DeleteSeq {
path,
index,
length,
..
} => {
js_set(&result, "action", "del")?;
js_set(
&result,
"path",
export_path(path.as_slice(), &Prop::Seq(index)),
)?;
if length > 1 {
js_set(&result, "length", length)?;
}
Ok(result.into())
}
}
}
}

View file

@ -1,44 +0,0 @@
use crate::{
clock::{Clock, ClockData},
Change, ChangeHash,
};
use std::collections::HashMap;
pub(crate) struct Clocks(HashMap<ChangeHash, Clock>);
#[derive(Debug, thiserror::Error)]
#[error("attempted to derive a clock for a change with dependencies we don't have")]
pub struct MissingDep(ChangeHash);
impl Clocks {
pub(crate) fn new() -> Self {
Self(HashMap::new())
}
pub(crate) fn add_change(
&mut self,
change: &Change,
actor_index: usize,
) -> Result<(), MissingDep> {
let mut clock = Clock::new();
for hash in change.deps() {
let c = self.0.get(hash).ok_or(MissingDep(*hash))?;
clock.merge(c);
}
clock.include(
actor_index,
ClockData {
max_op: change.max_op(),
seq: change.seq(),
},
);
self.0.insert(change.hash(), clock);
Ok(())
}
}
impl From<Clocks> for HashMap<ChangeHash, Clock> {
fn from(c: Clocks) -> Self {
c.0
}
}

View file

@ -1,82 +0,0 @@
use crate::ActorId;
use serde::Serialize;
use serde::Serializer;
use std::cmp::{Ord, Ordering};
use std::fmt;
use std::hash::{Hash, Hasher};
#[derive(Debug, Clone)]
pub enum ExId {
Root,
Id(u64, ActorId, usize),
}
impl PartialEq for ExId {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(ExId::Root, ExId::Root) => true,
(ExId::Id(ctr1, actor1, _), ExId::Id(ctr2, actor2, _))
if ctr1 == ctr2 && actor1 == actor2 =>
{
true
}
_ => false,
}
}
}
impl Eq for ExId {}
impl fmt::Display for ExId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ExId::Root => write!(f, "_root"),
ExId::Id(ctr, actor, _) => write!(f, "{}@{}", ctr, actor),
}
}
}
impl Hash for ExId {
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
ExId::Root => 0.hash(state),
ExId::Id(ctr, actor, _) => {
ctr.hash(state);
actor.hash(state);
}
}
}
}
impl Ord for ExId {
fn cmp(&self, other: &Self) -> Ordering {
match (self, other) {
(ExId::Root, ExId::Root) => Ordering::Equal,
(ExId::Root, _) => Ordering::Less,
(_, ExId::Root) => Ordering::Greater,
(ExId::Id(c1, a1, _), ExId::Id(c2, a2, _)) if c1 == c2 => a2.cmp(a1),
(ExId::Id(c1, _, _), ExId::Id(c2, _, _)) => c1.cmp(c2),
}
}
}
impl PartialOrd for ExId {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Serialize for ExId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_str())
}
}
impl AsRef<ExId> for ExId {
fn as_ref(&self) -> &ExId {
self
}
}

View file

@ -1,112 +0,0 @@
#![doc(
html_logo_url = "https://raw.githubusercontent.com/automerge/automerge-rs/main/img/brandmark.svg",
html_favicon_url = "https:///raw.githubusercontent.com/automerge/automerge-rs/main/img/favicon.ico"
)]
#![warn(
missing_debug_implementations,
// missing_docs, // TODO: add documentation!
rust_2018_idioms,
unreachable_pub,
bad_style,
const_err,
dead_code,
improper_ctypes,
non_shorthand_field_patterns,
no_mangle_generic_items,
overflowing_literals,
path_statements,
patterns_in_fns_without_body,
private_in_public,
unconditional_recursion,
unused,
unused_allocation,
unused_comparisons,
unused_parens,
while_true
)]
#[doc(hidden)]
#[macro_export]
macro_rules! log {
( $( $t:tt )* ) => {
{
use $crate::__log;
__log!( $( $t )* );
}
}
}
#[cfg(all(feature = "wasm", target_family = "wasm"))]
#[doc(hidden)]
#[macro_export]
macro_rules! __log {
( $( $t:tt )* ) => {
web_sys::console::log_1(&format!( $( $t )* ).into());
}
}
#[cfg(not(all(feature = "wasm", target_family = "wasm")))]
#[doc(hidden)]
#[macro_export]
macro_rules! __log {
( $( $t:tt )* ) => {
println!( $( $t )* );
}
}
mod autocommit;
mod automerge;
mod autoserde;
mod change;
mod clock;
mod clocks;
mod columnar;
mod convert;
mod error;
mod exid;
mod indexed_cache;
mod keys;
mod keys_at;
mod legacy;
mod list_range;
mod list_range_at;
mod map_range;
mod map_range_at;
mod op_observer;
mod op_set;
mod op_tree;
mod parents;
mod query;
mod storage;
pub mod sync;
pub mod transaction;
mod types;
mod value;
mod values;
#[cfg(feature = "optree-visualisation")]
mod visualisation;
pub use crate::automerge::Automerge;
pub use autocommit::{AutoCommit, AutoCommitWithObs};
pub use autoserde::AutoSerde;
pub use change::{Change, LoadError as LoadChangeError};
pub use error::AutomergeError;
pub use error::InvalidActorId;
pub use error::InvalidChangeHashSlice;
pub use exid::ExId as ObjId;
pub use keys::Keys;
pub use keys_at::KeysAt;
pub use legacy::Change as ExpandedChange;
pub use list_range::ListRange;
pub use list_range_at::ListRangeAt;
pub use map_range::MapRange;
pub use map_range_at::MapRangeAt;
pub use op_observer::OpObserver;
pub use op_observer::Patch;
pub use op_observer::VecOpObserver;
pub use parents::Parents;
pub use types::{ActorId, ChangeHash, ObjType, OpType, Prop};
pub use value::{ScalarValue, Value};
pub use values::Values;
pub const ROOT: ObjId = ObjId::Root;

View file

@ -1,236 +0,0 @@
use crate::exid::ExId;
use crate::Parents;
use crate::Prop;
use crate::Value;
/// An observer of operations applied to the document.
pub trait OpObserver: Default + Clone {
/// A new value has been inserted into the given object.
///
/// - `parents`: A parents iterator that can be used to collect path information
/// - `objid`: the object that has been inserted into.
/// - `index`: the index the new value has been inserted at.
/// - `tagged_value`: the value that has been inserted and the id of the operation that did the
/// insert.
fn insert(
&mut self,
parents: Parents<'_>,
objid: ExId,
index: usize,
tagged_value: (Value<'_>, ExId),
);
/// A new value has been put into the given object.
///
/// - `parents`: A parents iterator that can be used to collect path information
/// - `objid`: the object that has been put into.
/// - `prop`: the prop that the value as been put at.
/// - `tagged_value`: the value that has been put into the object and the id of the operation
/// that did the put.
/// - `conflict`: whether this put conflicts with other operations.
fn put(
&mut self,
parents: Parents<'_>,
objid: ExId,
prop: Prop,
tagged_value: (Value<'_>, ExId),
conflict: bool,
);
/// A counter has been incremented.
///
/// - `parents`: A parents iterator that can be used to collect path information
/// - `objid`: the object that contains the counter.
/// - `prop`: they prop that the chounter is at.
/// - `tagged_value`: the amount the counter has been incremented by, and the the id of the
/// increment operation.
fn increment(
&mut self,
parents: Parents<'_>,
objid: ExId,
prop: Prop,
tagged_value: (i64, ExId),
);
/// A value has beeen deleted.
///
/// - `parents`: A parents iterator that can be used to collect path information
/// - `objid`: the object that has been deleted in.
/// - `prop`: the prop of the value that has been deleted.
fn delete(&mut self, parents: Parents<'_>, objid: ExId, prop: Prop);
/// Branch of a new op_observer later to be merged
///
/// Called by AutoCommit when creating a new transaction. Observer branch
/// will be merged on `commit()` or thrown away on `rollback()`
///
fn branch(&self) -> Self {
Self::default()
}
/// Merge observed information from a transaction.
///
/// Called by AutoCommit on `commit()`
///
/// - `other`: Another Op Observer of the same type
fn merge(&mut self, other: &Self);
}
impl OpObserver for () {
fn insert(
&mut self,
_parents: Parents<'_>,
_objid: ExId,
_index: usize,
_tagged_value: (Value<'_>, ExId),
) {
}
fn put(
&mut self,
_parents: Parents<'_>,
_objid: ExId,
_prop: Prop,
_tagged_value: (Value<'_>, ExId),
_conflict: bool,
) {
}
fn increment(
&mut self,
_parents: Parents<'_>,
_objid: ExId,
_prop: Prop,
_tagged_value: (i64, ExId),
) {
}
fn delete(&mut self, _parents: Parents<'_>, _objid: ExId, _prop: Prop) {}
fn merge(&mut self, _other: &Self) {}
}
/// Capture operations into a [`Vec`] and store them as patches.
#[derive(Default, Debug, Clone)]
pub struct VecOpObserver {
patches: Vec<Patch>,
}
impl VecOpObserver {
/// Take the current list of patches, leaving the internal list empty and ready for new
/// patches.
pub fn take_patches(&mut self) -> Vec<Patch> {
std::mem::take(&mut self.patches)
}
}
impl OpObserver for VecOpObserver {
fn insert(
&mut self,
mut parents: Parents<'_>,
obj: ExId,
index: usize,
(value, id): (Value<'_>, ExId),
) {
let path = parents.path();
self.patches.push(Patch::Insert {
obj,
path,
index,
value: (value.into_owned(), id),
});
}
fn put(
&mut self,
mut parents: Parents<'_>,
obj: ExId,
prop: Prop,
(value, id): (Value<'_>, ExId),
conflict: bool,
) {
let path = parents.path();
self.patches.push(Patch::Put {
obj,
path,
prop,
value: (value.into_owned(), id),
conflict,
});
}
fn increment(
&mut self,
mut parents: Parents<'_>,
obj: ExId,
prop: Prop,
tagged_value: (i64, ExId),
) {
let path = parents.path();
self.patches.push(Patch::Increment {
obj,
path,
prop,
value: tagged_value,
});
}
fn delete(&mut self, mut parents: Parents<'_>, obj: ExId, prop: Prop) {
let path = parents.path();
self.patches.push(Patch::Delete { obj, path, prop })
}
fn merge(&mut self, other: &Self) {
self.patches.extend_from_slice(other.patches.as_slice())
}
}
/// A notification to the application that something has changed in a document.
#[derive(Debug, Clone, PartialEq)]
pub enum Patch {
/// Associating a new value with a prop in a map, or an existing list element
Put {
/// path to the object
path: Vec<(ExId, Prop)>,
/// The object that was put into.
obj: ExId,
/// The prop that the new value was put at.
prop: Prop,
/// The value that was put, and the id of the operation that put it there.
value: (Value<'static>, ExId),
/// Whether this put conflicts with another.
conflict: bool,
},
/// Inserting a new element into a list/text
Insert {
/// path to the object
path: Vec<(ExId, Prop)>,
/// The object that was inserted into.
obj: ExId,
/// The index that the new value was inserted at.
index: usize,
/// The value that was inserted, and the id of the operation that inserted it there.
value: (Value<'static>, ExId),
},
/// Incrementing a counter.
Increment {
/// path to the object
path: Vec<(ExId, Prop)>,
/// The object that was incremented in.
obj: ExId,
/// The prop that was incremented.
prop: Prop,
/// The amount that the counter was incremented by, and the id of the operation that
/// did the increment.
value: (i64, ExId),
},
/// Deleting an element from a list/text
Delete {
/// path to the object
path: Vec<(ExId, Prop)>,
/// The object that was deleted from.
obj: ExId,
/// The prop that was deleted.
prop: Prop,
},
}

View file

@ -1,809 +0,0 @@
use std::{
cmp::{min, Ordering},
fmt::Debug,
mem,
ops::RangeBounds,
};
pub(crate) use crate::op_set::OpSetMetadata;
use crate::{
clock::Clock,
query::{self, Index, QueryResult, ReplaceArgs, TreeQuery},
};
use crate::{
types::{ObjId, Op, OpId},
ObjType,
};
use std::collections::HashSet;
pub(crate) const B: usize = 16;
mod iter;
pub(crate) use iter::OpTreeIter;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct OpTree {
pub(crate) internal: OpTreeInternal,
pub(crate) objtype: ObjType,
/// The id of the parent object, root has no parent.
pub(crate) parent: Option<ObjId>,
}
impl OpTree {
pub(crate) fn new() -> Self {
Self {
internal: Default::default(),
objtype: ObjType::Map,
parent: None,
}
}
pub(crate) fn iter(&self) -> OpTreeIter<'_> {
self.internal.iter()
}
pub(crate) fn len(&self) -> usize {
self.internal.len()
}
}
#[derive(Clone, Debug)]
pub(crate) struct OpTreeInternal {
pub(crate) root_node: Option<OpTreeNode>,
}
#[derive(Clone, Debug)]
pub(crate) struct OpTreeNode {
pub(crate) children: Vec<OpTreeNode>,
pub(crate) elements: Vec<Op>,
pub(crate) index: Index,
length: usize,
}
impl OpTreeInternal {
/// Construct a new, empty, sequence.
pub(crate) fn new() -> Self {
Self { root_node: None }
}
/// Get the length of the sequence.
pub(crate) fn len(&self) -> usize {
self.root_node.as_ref().map_or(0, |n| n.len())
}
pub(crate) fn keys(&self) -> Option<query::Keys<'_>> {
self.root_node.as_ref().map(query::Keys::new)
}
pub(crate) fn keys_at(&self, clock: Clock) -> Option<query::KeysAt<'_>> {
self.root_node
.as_ref()
.map(|root| query::KeysAt::new(root, clock))
}
pub(crate) fn map_range<'a, R: RangeBounds<String>>(
&'a self,
range: R,
meta: &'a OpSetMetadata,
) -> Option<query::MapRange<'a, R>> {
self.root_node
.as_ref()
.map(|node| query::MapRange::new(range, node, meta))
}
pub(crate) fn map_range_at<'a, R: RangeBounds<String>>(
&'a self,
range: R,
meta: &'a OpSetMetadata,
clock: Clock,
) -> Option<query::MapRangeAt<'a, R>> {
self.root_node
.as_ref()
.map(|node| query::MapRangeAt::new(range, node, meta, clock))
}
pub(crate) fn list_range<R: RangeBounds<usize>>(
&self,
range: R,
) -> Option<query::ListRange<'_, R>> {
self.root_node
.as_ref()
.map(|node| query::ListRange::new(range, node))
}
pub(crate) fn list_range_at<R: RangeBounds<usize>>(
&self,
range: R,
clock: Clock,
) -> Option<query::ListRangeAt<'_, R>> {
self.root_node
.as_ref()
.map(|node| query::ListRangeAt::new(range, clock, node))
}
pub(crate) fn search<'a, 'b: 'a, Q>(&'b self, mut query: Q, m: &OpSetMetadata) -> Q
where
Q: TreeQuery<'a>,
{
self.root_node
.as_ref()
.map(|root| match query.query_node_with_metadata(root, m) {
QueryResult::Descend => root.search(&mut query, m, None),
QueryResult::Skip(skip) => root.search(&mut query, m, Some(skip)),
_ => true,
});
query
}
/// Create an iterator through the sequence.
pub(crate) fn iter(&self) -> OpTreeIter<'_> {
iter::OpTreeIter::new(self)
}
/// Insert the `element` into the sequence at `index`.
///
/// # Panics
///
/// Panics if `index > len`.
pub(crate) fn insert(&mut self, index: usize, element: Op) {
assert!(
index <= self.len(),
"tried to insert at {} but len is {}",
index,
self.len()
);
let old_len = self.len();
if let Some(root) = self.root_node.as_mut() {
#[cfg(debug_assertions)]
root.check();
if root.is_full() {
let original_len = root.len();
let new_root = OpTreeNode::new();
// move new_root to root position
let old_root = mem::replace(root, new_root);
root.length += old_root.len();
root.index = old_root.index.clone();
root.children.push(old_root);
root.split_child(0);
assert_eq!(original_len, root.len());
// after splitting the root has one element and two children, find which child the
// index is in
let first_child_len = root.children[0].len();
let (child, insertion_index) = if first_child_len < index {
(&mut root.children[1], index - (first_child_len + 1))
} else {
(&mut root.children[0], index)
};
root.length += 1;
root.index.insert(&element);
child.insert_into_non_full_node(insertion_index, element)
} else {
root.insert_into_non_full_node(index, element)
}
} else {
let mut root = OpTreeNode::new();
root.insert_into_non_full_node(index, element);
self.root_node = Some(root)
}
assert_eq!(self.len(), old_len + 1, "{:#?}", self);
}
/// Get the `element` at `index` in the sequence.
pub(crate) fn get(&self, index: usize) -> Option<&Op> {
self.root_node.as_ref().and_then(|n| n.get(index))
}
// this replaces get_mut() because it allows the indexes to update correctly
pub(crate) fn update<F>(&mut self, index: usize, f: F)
where
F: FnMut(&mut Op),
{
if self.len() > index {
self.root_node.as_mut().unwrap().update(index, f);
}
}
/// Removes the element at `index` from the sequence.
///
/// # Panics
///
/// Panics if `index` is out of bounds.
pub(crate) fn remove(&mut self, index: usize) -> Op {
if let Some(root) = self.root_node.as_mut() {
#[cfg(debug_assertions)]
let len = root.check();
let old = root.remove(index);
if root.elements.is_empty() {
if root.is_leaf() {
self.root_node = None;
} else {
self.root_node = Some(root.children.remove(0));
}
}
#[cfg(debug_assertions)]
debug_assert_eq!(len, self.root_node.as_ref().map_or(0, |r| r.check()) + 1);
old
} else {
panic!("remove from empty tree")
}
}
}
impl OpTreeNode {
fn new() -> Self {
Self {
elements: Vec::new(),
children: Vec::new(),
index: Default::default(),
length: 0,
}
}
pub(crate) fn search<'a, 'b: 'a, Q>(
&'b self,
query: &mut Q,
m: &OpSetMetadata,
skip: Option<usize>,
) -> bool
where
Q: TreeQuery<'a>,
{
if self.is_leaf() {
let skip = skip.unwrap_or(0);
for e in self.elements.iter().skip(skip) {
if query.query_element_with_metadata(e, m) == QueryResult::Finish {
return true;
}
}
false
} else {
let mut skip = skip.unwrap_or(0);
for (child_index, child) in self.children.iter().enumerate() {
match skip.cmp(&child.len()) {
Ordering::Greater => {
// not in this child at all
// take off the number of elements in the child as well as the next element
skip -= child.len() + 1;
}
Ordering::Equal => {
// just try the element
skip -= child.len();
if let Some(e) = self.elements.get(child_index) {
if query.query_element_with_metadata(e, m) == QueryResult::Finish {
return true;
}
}
}
Ordering::Less => {
// descend and try find it
match query.query_node_with_metadata(child, m) {
QueryResult::Descend => {
// search in the child node, passing in the number of items left to
// skip
if child.search(query, m, Some(skip)) {
return true;
}
}
QueryResult::Finish => return true,
QueryResult::Next => (),
QueryResult::Skip(_) => panic!("had skip from non-root node"),
}
if let Some(e) = self.elements.get(child_index) {
if query.query_element_with_metadata(e, m) == QueryResult::Finish {
return true;
}
}
// reset the skip to zero so we continue iterating normally
skip = 0;
}
}
}
false
}
}
pub(crate) fn len(&self) -> usize {
self.length
}
fn reindex(&mut self) {
let mut index = Index::new();
for c in &self.children {
index.merge(&c.index);
}
for e in &self.elements {
index.insert(e);
}
self.index = index
}
fn is_leaf(&self) -> bool {
self.children.is_empty()
}
fn is_full(&self) -> bool {
self.elements.len() >= 2 * B - 1
}
/// Returns the child index and the given index adjusted for the cumulative index before that
/// child.
fn find_child_index(&self, index: usize) -> (usize, usize) {
let mut cumulative_len = 0;
for (child_index, child) in self.children.iter().enumerate() {
if cumulative_len + child.len() >= index {
return (child_index, index - cumulative_len);
} else {
cumulative_len += child.len() + 1;
}
}
panic!("index {} not found in node with len {}", index, self.len())
}
fn insert_into_non_full_node(&mut self, index: usize, element: Op) {
assert!(!self.is_full());
self.index.insert(&element);
if self.is_leaf() {
self.length += 1;
self.elements.insert(index, element);
} else {
let (child_index, sub_index) = self.find_child_index(index);
let child = &mut self.children[child_index];
if child.is_full() {
self.split_child(child_index);
// child structure has changed so we need to find the index again
let (child_index, sub_index) = self.find_child_index(index);
let child = &mut self.children[child_index];
child.insert_into_non_full_node(sub_index, element);
} else {
child.insert_into_non_full_node(sub_index, element);
}
self.length += 1;
}
}
// A utility function to split the child `full_child_index` of this node
// Note that `full_child_index` must be full when this function is called.
fn split_child(&mut self, full_child_index: usize) {
let original_len_self = self.len();
let full_child = &mut self.children[full_child_index];
// Create a new node which is going to store (B-1) keys
// of the full child.
let mut successor_sibling = OpTreeNode::new();
let original_len = full_child.len();
assert!(full_child.is_full());
successor_sibling.elements = full_child.elements.split_off(B);
if !full_child.is_leaf() {
successor_sibling.children = full_child.children.split_off(B);
}
let middle = full_child.elements.pop().unwrap();
full_child.length =
full_child.elements.len() + full_child.children.iter().map(|c| c.len()).sum::<usize>();
successor_sibling.length = successor_sibling.elements.len()
+ successor_sibling
.children
.iter()
.map(|c| c.len())
.sum::<usize>();
let z_len = successor_sibling.len();
let full_child_len = full_child.len();
full_child.reindex();
successor_sibling.reindex();
self.children
.insert(full_child_index + 1, successor_sibling);
self.elements.insert(full_child_index, middle);
assert_eq!(full_child_len + z_len + 1, original_len, "{:#?}", self);
assert_eq!(original_len_self, self.len());
}
fn remove_from_leaf(&mut self, index: usize) -> Op {
self.length -= 1;
self.elements.remove(index)
}
fn remove_element_from_non_leaf(&mut self, index: usize, element_index: usize) -> Op {
self.length -= 1;
if self.children[element_index].elements.len() >= B {
let total_index = self.cumulative_index(element_index);
// recursively delete index - 1 in predecessor_node
let predecessor = self.children[element_index].remove(index - 1 - total_index);
// replace element with that one
mem::replace(&mut self.elements[element_index], predecessor)
} else if self.children[element_index + 1].elements.len() >= B {
// recursively delete index + 1 in successor_node
let total_index = self.cumulative_index(element_index + 1);
let successor = self.children[element_index + 1].remove(index + 1 - total_index);
// replace element with that one
mem::replace(&mut self.elements[element_index], successor)
} else {
let middle_element = self.elements.remove(element_index);
let successor_child = self.children.remove(element_index + 1);
self.children[element_index].merge(middle_element, successor_child);
let total_index = self.cumulative_index(element_index);
self.children[element_index].remove(index - total_index)
}
}
fn cumulative_index(&self, child_index: usize) -> usize {
self.children[0..child_index]
.iter()
.map(|c| c.len() + 1)
.sum()
}
fn remove_from_internal_child(&mut self, index: usize, mut child_index: usize) -> Op {
if self.children[child_index].elements.len() < B
&& if child_index > 0 {
self.children[child_index - 1].elements.len() < B
} else {
true
}
&& if child_index + 1 < self.children.len() {
self.children[child_index + 1].elements.len() < B
} else {
true
}
{
// if the child and its immediate siblings have B-1 elements merge the child
// with one sibling, moving an element from this node into the new merged node
// to be the median
if child_index > 0 {
let middle = self.elements.remove(child_index - 1);
// use the predessor sibling
let successor = self.children.remove(child_index);
child_index -= 1;
self.children[child_index].merge(middle, successor);
} else {
let middle = self.elements.remove(child_index);
// use the sucessor sibling
let successor = self.children.remove(child_index + 1);
self.children[child_index].merge(middle, successor);
}
} else if self.children[child_index].elements.len() < B {
if child_index > 0
&& self
.children
.get(child_index - 1)
.map_or(false, |c| c.elements.len() >= B)
{
let last_element = self.children[child_index - 1].elements.pop().unwrap();
assert!(!self.children[child_index - 1].elements.is_empty());
self.children[child_index - 1].length -= 1;
self.children[child_index - 1].index.remove(&last_element);
let parent_element =
mem::replace(&mut self.elements[child_index - 1], last_element);
self.children[child_index].index.insert(&parent_element);
self.children[child_index]
.elements
.insert(0, parent_element);
self.children[child_index].length += 1;
if let Some(last_child) = self.children[child_index - 1].children.pop() {
self.children[child_index - 1].length -= last_child.len();
self.children[child_index - 1].reindex();
self.children[child_index].length += last_child.len();
self.children[child_index].children.insert(0, last_child);
self.children[child_index].reindex();
}
} else if self
.children
.get(child_index + 1)
.map_or(false, |c| c.elements.len() >= B)
{
let first_element = self.children[child_index + 1].elements.remove(0);
self.children[child_index + 1].index.remove(&first_element);
self.children[child_index + 1].length -= 1;
assert!(!self.children[child_index + 1].elements.is_empty());
let parent_element = mem::replace(&mut self.elements[child_index], first_element);
self.children[child_index].length += 1;
self.children[child_index].index.insert(&parent_element);
self.children[child_index].elements.push(parent_element);
if !self.children[child_index + 1].is_leaf() {
let first_child = self.children[child_index + 1].children.remove(0);
self.children[child_index + 1].length -= first_child.len();
self.children[child_index + 1].reindex();
self.children[child_index].length += first_child.len();
self.children[child_index].children.push(first_child);
self.children[child_index].reindex();
}
}
}
self.length -= 1;
let total_index = self.cumulative_index(child_index);
self.children[child_index].remove(index - total_index)
}
fn check(&self) -> usize {
let l = self.elements.len() + self.children.iter().map(|c| c.check()).sum::<usize>();
assert_eq!(self.len(), l, "{:#?}", self);
l
}
pub(crate) fn remove(&mut self, index: usize) -> Op {
let original_len = self.len();
if self.is_leaf() {
let v = self.remove_from_leaf(index);
self.index.remove(&v);
assert_eq!(original_len, self.len() + 1);
debug_assert_eq!(self.check(), self.len());
v
} else {
let mut total_index = 0;
for (child_index, child) in self.children.iter().enumerate() {
match (total_index + child.len()).cmp(&index) {
Ordering::Less => {
// should be later on in the loop
total_index += child.len() + 1;
continue;
}
Ordering::Equal => {
let v = self.remove_element_from_non_leaf(
index,
min(child_index, self.elements.len() - 1),
);
self.index.remove(&v);
assert_eq!(original_len, self.len() + 1);
debug_assert_eq!(self.check(), self.len());
return v;
}
Ordering::Greater => {
let v = self.remove_from_internal_child(index, child_index);
self.index.remove(&v);
assert_eq!(original_len, self.len() + 1);
debug_assert_eq!(self.check(), self.len());
return v;
}
}
}
panic!(
"index not found to remove {} {} {} {}",
index,
total_index,
self.len(),
self.check()
);
}
}
fn merge(&mut self, middle: Op, successor_sibling: OpTreeNode) {
self.index.insert(&middle);
self.index.merge(&successor_sibling.index);
self.elements.push(middle);
self.elements.extend(successor_sibling.elements);
self.children.extend(successor_sibling.children);
self.length += successor_sibling.length + 1;
assert!(self.is_full());
}
/// Update the operation at the given index using the provided function.
///
/// This handles updating the indices after the update.
pub(crate) fn update<F>(&mut self, index: usize, f: F) -> ReplaceArgs
where
F: FnOnce(&mut Op),
{
if self.is_leaf() {
let new_element = self.elements.get_mut(index).unwrap();
let old_id = new_element.id;
let old_visible = new_element.visible();
f(new_element);
let replace_args = ReplaceArgs {
old_id,
new_id: new_element.id,
old_visible,
new_visible: new_element.visible(),
new_key: new_element.elemid_or_key(),
};
self.index.replace(&replace_args);
replace_args
} else {
let mut cumulative_len = 0;
let len = self.len();
for (child_index, child) in self.children.iter_mut().enumerate() {
match (cumulative_len + child.len()).cmp(&index) {
Ordering::Less => {
cumulative_len += child.len() + 1;
}
Ordering::Equal => {
let new_element = self.elements.get_mut(child_index).unwrap();
let old_id = new_element.id;
let old_visible = new_element.visible();
f(new_element);
let replace_args = ReplaceArgs {
old_id,
new_id: new_element.id,
old_visible,
new_visible: new_element.visible(),
new_key: new_element.elemid_or_key(),
};
self.index.replace(&replace_args);
return replace_args;
}
Ordering::Greater => {
let replace_args = child.update(index - cumulative_len, f);
self.index.replace(&replace_args);
return replace_args;
}
}
}
panic!("Invalid index to set: {} but len was {}", index, len)
}
}
pub(crate) fn last(&self) -> &Op {
if self.is_leaf() {
// node is never empty so this is safe
self.elements.last().unwrap()
} else {
// if not a leaf then there is always at least one child
self.children.last().unwrap().last()
}
}
pub(crate) fn get(&self, index: usize) -> Option<&Op> {
if self.is_leaf() {
return self.elements.get(index);
} else {
let mut cumulative_len = 0;
for (child_index, child) in self.children.iter().enumerate() {
match (cumulative_len + child.len()).cmp(&index) {
Ordering::Less => {
cumulative_len += child.len() + 1;
}
Ordering::Equal => return self.elements.get(child_index),
Ordering::Greater => {
return child.get(index - cumulative_len);
}
}
}
}
None
}
}
impl Default for OpTreeInternal {
fn default() -> Self {
Self::new()
}
}
impl PartialEq for OpTreeInternal {
fn eq(&self, other: &Self) -> bool {
self.len() == other.len() && self.iter().zip(other.iter()).all(|(a, b)| a == b)
}
}
impl<'a> IntoIterator for &'a OpTreeInternal {
type Item = &'a Op;
type IntoIter = Iter<'a>;
fn into_iter(self) -> Self::IntoIter {
Iter {
inner: self,
index: 0,
}
}
}
pub(crate) struct Iter<'a> {
inner: &'a OpTreeInternal,
index: usize,
}
impl<'a> Iterator for Iter<'a> {
type Item = &'a Op;
fn next(&mut self) -> Option<Self::Item> {
self.index += 1;
self.inner.get(self.index - 1)
}
fn nth(&mut self, n: usize) -> Option<Self::Item> {
self.index += n + 1;
self.inner.get(self.index - 1)
}
}
#[derive(Debug, Clone, PartialEq)]
struct CounterData {
pos: usize,
val: i64,
succ: HashSet<OpId>,
op: Op,
}
#[cfg(test)]
mod tests {
use crate::legacy as amp;
use crate::types::{Op, OpId};
use super::*;
fn op() -> Op {
let zero = OpId(0, 0);
Op {
id: zero,
action: amp::OpType::Put(0.into()),
key: zero.into(),
succ: Default::default(),
pred: Default::default(),
insert: false,
}
}
#[test]
fn insert() {
let mut t: OpTree = OpTree::new();
t.internal.insert(0, op());
t.internal.insert(1, op());
t.internal.insert(0, op());
t.internal.insert(0, op());
t.internal.insert(0, op());
t.internal.insert(3, op());
t.internal.insert(4, op());
}
#[test]
fn insert_book() {
let mut t: OpTree = OpTree::new();
for i in 0..100 {
t.internal.insert(i % 2, op());
}
}
#[test]
fn insert_book_vec() {
let mut t: OpTree = OpTree::new();
let mut v = Vec::new();
for i in 0..100 {
t.internal.insert(i % 3, op());
v.insert(i % 3, op());
assert_eq!(v, t.internal.iter().cloned().collect::<Vec<_>>())
}
}
}

View file

@ -1,35 +0,0 @@
use crate::op_set::OpSet;
use crate::types::ObjId;
use crate::{exid::ExId, Prop};
#[derive(Debug)]
pub struct Parents<'a> {
pub(crate) obj: ObjId,
pub(crate) ops: &'a OpSet,
}
impl<'a> Parents<'a> {
pub fn path(&mut self) -> Vec<(ExId, Prop)> {
let mut path = self.collect::<Vec<_>>();
path.reverse();
path
}
}
impl<'a> Iterator for Parents<'a> {
type Item = (ExId, Prop);
fn next(&mut self) -> Option<Self::Item> {
if self.obj.is_root() {
None
} else if let Some((obj, key)) = self.ops.parent_object(&self.obj) {
self.obj = obj;
Some((
self.ops.id_to_exid(self.obj.0),
self.ops.export_key(self.obj, key),
))
} else {
None
}
}
}

View file

@ -1,56 +0,0 @@
use crate::{
op_tree::OpTreeNode,
types::{ElemId, Key},
};
use super::{QueryResult, TreeQuery};
/// Lookup the index in the list that this elemid occupies.
pub(crate) struct ElemIdPos {
elemid: ElemId,
pos: usize,
found: bool,
}
impl ElemIdPos {
pub(crate) fn new(elemid: ElemId) -> Self {
Self {
elemid,
pos: 0,
found: false,
}
}
pub(crate) fn index(&self) -> Option<usize> {
if self.found {
Some(self.pos)
} else {
None
}
}
}
impl<'a> TreeQuery<'a> for ElemIdPos {
fn query_node(&mut self, child: &OpTreeNode) -> QueryResult {
// if index has our element then we can continue
if child.index.has_visible(&Key::Seq(self.elemid)) {
// element is in this node somewhere
QueryResult::Descend
} else {
// not in this node, try the next one
self.pos += child.index.visible_len();
QueryResult::Next
}
}
fn query_element(&mut self, element: &crate::types::Op) -> QueryResult {
if element.elemid() == Some(self.elemid) {
// this is it
self.found = true;
return QueryResult::Finish;
} else if element.visible() {
self.pos += 1;
}
QueryResult::Next
}
}

View file

@ -1,21 +0,0 @@
use crate::op_tree::OpTreeNode;
use crate::query::{QueryResult, TreeQuery};
use std::fmt::Debug;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Len {
pub(crate) len: usize,
}
impl Len {
pub(crate) fn new() -> Self {
Len { len: 0 }
}
}
impl<'a> TreeQuery<'a> for Len {
fn query_node(&mut self, child: &OpTreeNode) -> QueryResult {
self.len = child.index.visible_len();
QueryResult::Finish
}
}

View file

@ -1,87 +0,0 @@
use crate::op_tree::{OpSetMetadata, OpTreeNode};
use crate::query::{binary_search_by, QueryResult, TreeQuery};
use crate::types::{Key, Op};
use std::fmt::Debug;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Prop<'a> {
key: Key,
pub(crate) ops: Vec<&'a Op>,
pub(crate) ops_pos: Vec<usize>,
pub(crate) pos: usize,
start: Option<Start>,
}
#[derive(Debug, Clone, PartialEq)]
struct Start {
/// The index to start searching for in the optree
idx: usize,
/// The total length of the optree
optree_len: usize,
}
impl<'a> Prop<'a> {
pub(crate) fn new(prop: usize) -> Self {
Prop {
key: Key::Map(prop),
ops: vec![],
ops_pos: vec![],
pos: 0,
start: None,
}
}
}
impl<'a> TreeQuery<'a> for Prop<'a> {
fn query_node_with_metadata(
&mut self,
child: &'a OpTreeNode,
m: &OpSetMetadata,
) -> QueryResult {
if let Some(Start {
idx: start,
optree_len,
}) = self.start
{
if self.pos + child.len() >= start {
// skip empty nodes
if child.index.visible_len() == 0 {
if self.pos + child.len() >= optree_len {
self.pos = optree_len;
QueryResult::Finish
} else {
self.pos += child.len();
QueryResult::Next
}
} else {
QueryResult::Descend
}
} else {
self.pos += child.len();
QueryResult::Next
}
} else {
// in the root node find the first op position for the key
let start = binary_search_by(child, |op| m.key_cmp(&op.key, &self.key));
self.start = Some(Start {
idx: start,
optree_len: child.len(),
});
self.pos = start;
QueryResult::Skip(start)
}
}
fn query_element(&mut self, op: &'a Op) -> QueryResult {
// don't bother looking at things past our key
if op.key != self.key {
return QueryResult::Finish;
}
if op.visible() {
self.ops.push(op);
self.ops_pos.push(self.pos);
}
self.pos += 1;
QueryResult::Next
}
}

View file

@ -1,145 +0,0 @@
use crate::op_tree::{OpSetMetadata, OpTreeNode};
use crate::query::{binary_search_by, QueryResult, TreeQuery};
use crate::types::{Key, Op, HEAD};
use std::cmp::Ordering;
use std::fmt::Debug;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct SeekOp<'a> {
/// the op we are looking for
op: &'a Op,
/// The position to insert at
pub(crate) pos: usize,
/// The indices of ops that this op overwrites
pub(crate) succ: Vec<usize>,
/// whether a position has been found
found: bool,
/// The found start position of the key if there is one yet (for map objects).
start: Option<usize>,
}
impl<'a> SeekOp<'a> {
pub(crate) fn new(op: &'a Op) -> Self {
SeekOp {
op,
succ: vec![],
pos: 0,
found: false,
start: None,
}
}
fn lesser_insert(&self, op: &Op, m: &OpSetMetadata) -> bool {
op.insert && m.lamport_cmp(op.id, self.op.id) == Ordering::Less
}
fn greater_opid(&self, op: &Op, m: &OpSetMetadata) -> bool {
m.lamport_cmp(op.id, self.op.id) == Ordering::Greater
}
fn is_target_insert(&self, op: &Op) -> bool {
op.insert && op.elemid() == self.op.key.elemid()
}
}
impl<'a> TreeQuery<'a> for SeekOp<'a> {
fn query_node_with_metadata(&mut self, child: &OpTreeNode, m: &OpSetMetadata) -> QueryResult {
if self.found {
return QueryResult::Descend;
}
match self.op.key {
Key::Seq(HEAD) => {
while self.pos < child.len() {
let op = child.get(self.pos).unwrap();
if op.insert && m.lamport_cmp(op.id, self.op.id) == Ordering::Less {
break;
}
self.pos += 1;
}
QueryResult::Finish
}
Key::Seq(e) => {
if child.index.ops.contains(&e.0) {
QueryResult::Descend
} else {
self.pos += child.len();
QueryResult::Next
}
}
Key::Map(_) => {
if let Some(start) = self.start {
if self.pos + child.len() >= start {
// skip empty nodes
if child.index.visible_len() == 0 {
self.pos += child.len();
QueryResult::Next
} else {
QueryResult::Descend
}
} else {
self.pos += child.len();
QueryResult::Next
}
} else {
// in the root node find the first op position for the key
let start = binary_search_by(child, |op| m.key_cmp(&op.key, &self.op.key));
self.start = Some(start);
self.pos = start;
QueryResult::Skip(start)
}
}
}
}
fn query_element_with_metadata(&mut self, e: &Op, m: &OpSetMetadata) -> QueryResult {
match self.op.key {
Key::Map(_) => {
// don't bother looking at things past our key
if e.key != self.op.key {
return QueryResult::Finish;
}
if self.op.overwrites(e) {
self.succ.push(self.pos);
}
if m.lamport_cmp(e.id, self.op.id) == Ordering::Greater {
return QueryResult::Finish;
}
self.pos += 1;
QueryResult::Next
}
Key::Seq(_) => {
if !self.found {
if self.is_target_insert(e) {
self.found = true;
if self.op.overwrites(e) {
self.succ.push(self.pos);
}
}
self.pos += 1;
QueryResult::Next
} else {
// we have already found the target
if self.op.overwrites(e) {
self.succ.push(self.pos);
}
if self.op.insert {
if self.lesser_insert(e, m) {
QueryResult::Finish
} else {
self.pos += 1;
QueryResult::Next
}
} else if e.insert || self.greater_opid(e, m) {
QueryResult::Finish
} else {
self.pos += 1;
QueryResult::Next
}
}
}
}
}
}

View file

@ -1,118 +0,0 @@
use core::mem::size_of;
use std::num::NonZeroU64;
use super::{take1, Input, ParseError, ParseResult};
#[derive(PartialEq, thiserror::Error, Debug, Clone)]
pub(crate) enum Error {
#[error("leb128 was too large for the destination type")]
Leb128TooLarge,
#[error("leb128 was zero when it was expected to be nonzero")]
UnexpectedZero,
}
macro_rules! impl_leb {
($parser_name: ident, $ty: ty) => {
#[allow(dead_code)]
pub(crate) fn $parser_name<'a, E>(input: Input<'a>) -> ParseResult<'a, $ty, E>
where
E: From<Error>,
{
let mut res = 0;
let mut shift = 0;
let mut input = input;
let mut pos = 0;
loop {
let (i, byte) = take1(input)?;
input = i;
if (byte & 0x80) == 0 {
res |= (byte as $ty) << shift;
return Ok((input, res));
} else if pos == leb128_size::<$ty>() - 1 {
return Err(ParseError::Error(Error::Leb128TooLarge.into()));
} else {
res |= ((byte & 0x7F) as $ty) << shift;
}
pos += 1;
shift += 7;
}
}
};
}
impl_leb!(leb128_u64, u64);
impl_leb!(leb128_u32, u32);
impl_leb!(leb128_i64, i64);
impl_leb!(leb128_i32, i32);
/// Parse a LEB128 encoded u64 from the input, throwing an error if it is `0`
pub(crate) fn nonzero_leb128_u64<E>(input: Input<'_>) -> ParseResult<'_, NonZeroU64, E>
where
E: From<Error>,
{
let (input, num) = leb128_u64(input)?;
let result =
NonZeroU64::new(num).ok_or_else(|| ParseError::Error(Error::UnexpectedZero.into()))?;
Ok((input, result))
}
/// Maximum LEB128-encoded size of an integer type
const fn leb128_size<T>() -> usize {
let bits = size_of::<T>() * 8;
(bits + 6) / 7 // equivalent to ceil(bits/7) w/o floats
}
#[cfg(test)]
mod tests {
use super::super::Needed;
use super::*;
use std::{convert::TryFrom, num::NonZeroUsize};
const NEED_ONE: Needed = Needed::Size(unsafe { NonZeroUsize::new_unchecked(1) });
#[test]
fn leb_128_unsigned() {
let one = &[0b00000001_u8];
let one_two_nine = &[0b10000001, 0b00000001];
let one_and_more = &[0b00000001, 0b00000011];
let scenarios: Vec<(&'static [u8], ParseResult<'_, u64, Error>)> = vec![
(one, Ok((Input::with_position(one, 1), 1))),
(&[0b10000001_u8], Err(ParseError::Incomplete(NEED_ONE))),
(
one_two_nine,
Ok((Input::with_position(one_two_nine, 2), 129)),
),
(one_and_more, Ok((Input::with_position(one_and_more, 1), 1))),
(
&[129, 129, 129, 129, 129, 129, 129, 129, 129, 129, 129, 129],
Err(ParseError::Error(Error::Leb128TooLarge)),
),
];
for (index, (input, expected)) in scenarios.clone().into_iter().enumerate() {
let result = leb128_u64(Input::new(input));
if result != expected {
panic!(
"Scenario {} failed for u64: expected {:?} got {:?}",
index + 1,
expected,
result
);
}
}
for (index, (input, expected)) in scenarios.into_iter().enumerate() {
let u32_expected = expected.map(|(i, e)| (i, u32::try_from(e).unwrap()));
let result = leb128_u32(Input::new(input));
if result != u32_expected {
panic!(
"Scenario {} failed for u32: expected {:?} got {:?}",
index + 1,
u32_expected,
result
);
}
}
}
}

View file

@ -1,530 +0,0 @@
use itertools::Itertools;
use serde::ser::SerializeMap;
use std::collections::{HashMap, HashSet};
use crate::{
storage::{parse, Change as StoredChange, ReadChangeOpError},
Automerge, AutomergeError, Change, ChangeHash, OpObserver,
};
mod bloom;
mod state;
pub use bloom::BloomFilter;
pub use state::DecodeError as DecodeStateError;
pub use state::{Have, State};
const MESSAGE_TYPE_SYNC: u8 = 0x42; // first byte of a sync message, for identification
impl Automerge {
pub fn generate_sync_message(&self, sync_state: &mut State) -> Option<Message> {
let our_heads = self.get_heads();
let our_need = self.get_missing_deps(sync_state.their_heads.as_ref().unwrap_or(&vec![]));
let their_heads_set = if let Some(ref heads) = sync_state.their_heads {
heads.iter().collect::<HashSet<_>>()
} else {
HashSet::new()
};
let our_have = if our_need.iter().all(|hash| their_heads_set.contains(hash)) {
vec![self.make_bloom_filter(sync_state.shared_heads.clone())]
} else {
Vec::new()
};
if let Some(ref their_have) = sync_state.their_have {
if let Some(first_have) = their_have.first().as_ref() {
if !first_have
.last_sync
.iter()
.all(|hash| self.get_change_by_hash(hash).is_some())
{
let reset_msg = Message {
heads: our_heads,
need: Vec::new(),
have: vec![Have::default()],
changes: Vec::new(),
};
return Some(reset_msg);
}
}
}
let changes_to_send = if let (Some(their_have), Some(their_need)) = (
sync_state.their_have.as_ref(),
sync_state.their_need.as_ref(),
) {
self.get_changes_to_send(their_have, their_need)
.expect("Should have only used hashes that are in the document")
} else {
Vec::new()
};
let heads_unchanged = sync_state.last_sent_heads == our_heads;
let heads_equal = if let Some(their_heads) = sync_state.their_heads.as_ref() {
their_heads == &our_heads
} else {
false
};
if heads_unchanged && heads_equal && changes_to_send.is_empty() {
return None;
}
// deduplicate the changes to send with those we have already sent and clone it now
let changes_to_send = changes_to_send
.into_iter()
.filter_map(|change| {
if !sync_state.sent_hashes.contains(&change.hash()) {
Some(change.clone())
} else {
None
}
})
.collect::<Vec<_>>();
sync_state.last_sent_heads = our_heads.clone();
sync_state
.sent_hashes
.extend(changes_to_send.iter().map(|c| c.hash()));
let sync_message = Message {
heads: our_heads,
have: our_have,
need: our_need,
changes: changes_to_send,
};
Some(sync_message)
}
pub fn receive_sync_message(
&mut self,
sync_state: &mut State,
message: Message,
) -> Result<(), AutomergeError> {
self.receive_sync_message_with::<()>(sync_state, message, None)
}
pub fn receive_sync_message_with<Obs: OpObserver>(
&mut self,
sync_state: &mut State,
message: Message,
op_observer: Option<&mut Obs>,
) -> Result<(), AutomergeError> {
let before_heads = self.get_heads();
let Message {
heads: message_heads,
changes: message_changes,
need: message_need,
have: message_have,
} = message;
let changes_is_empty = message_changes.is_empty();
if !changes_is_empty {
self.apply_changes_with(message_changes, op_observer)?;
sync_state.shared_heads = advance_heads(
&before_heads.iter().collect(),
&self.get_heads().into_iter().collect(),
&sync_state.shared_heads,
);
}
// trim down the sent hashes to those that we know they haven't seen
self.filter_changes(&message_heads, &mut sync_state.sent_hashes)?;
if changes_is_empty && message_heads == before_heads {
sync_state.last_sent_heads = message_heads.clone();
}
let known_heads = message_heads
.iter()
.filter(|head| self.get_change_by_hash(head).is_some())
.collect::<Vec<_>>();
if known_heads.len() == message_heads.len() {
sync_state.shared_heads = message_heads.clone();
// If the remote peer has lost all its data, reset our state to perform a full resync
if message_heads.is_empty() {
sync_state.last_sent_heads = Default::default();
sync_state.sent_hashes = Default::default();
}
} else {
sync_state.shared_heads = sync_state
.shared_heads
.iter()
.chain(known_heads)
.copied()
.unique()
.sorted()
.collect::<Vec<_>>();
}
sync_state.their_have = Some(message_have);
sync_state.their_heads = Some(message_heads);
sync_state.their_need = Some(message_need);
Ok(())
}
fn make_bloom_filter(&self, last_sync: Vec<ChangeHash>) -> Have {
let new_changes = self
.get_changes(&last_sync)
.expect("Should have only used hashes that are in the document");
let hashes = new_changes.iter().map(|change| change.hash());
Have {
last_sync,
bloom: BloomFilter::from_hashes(hashes),
}
}
fn get_changes_to_send(
&self,
have: &[Have],
need: &[ChangeHash],
) -> Result<Vec<&Change>, AutomergeError> {
if have.is_empty() {
Ok(need
.iter()
.filter_map(|hash| self.get_change_by_hash(hash))
.collect())
} else {
let mut last_sync_hashes = HashSet::new();
let mut bloom_filters = Vec::with_capacity(have.len());
for h in have {
let Have { last_sync, bloom } = h;
last_sync_hashes.extend(last_sync);
bloom_filters.push(bloom);
}
let last_sync_hashes = last_sync_hashes.into_iter().copied().collect::<Vec<_>>();
let changes = self.get_changes(&last_sync_hashes)?;
let mut change_hashes = HashSet::with_capacity(changes.len());
let mut dependents: HashMap<ChangeHash, Vec<ChangeHash>> = HashMap::new();
let mut hashes_to_send = HashSet::new();
for change in &changes {
change_hashes.insert(change.hash());
for dep in change.deps() {
dependents.entry(*dep).or_default().push(change.hash());
}
if bloom_filters
.iter()
.all(|bloom| !bloom.contains_hash(&change.hash()))
{
hashes_to_send.insert(change.hash());
}
}
let mut stack = hashes_to_send.iter().copied().collect::<Vec<_>>();
while let Some(hash) = stack.pop() {
if let Some(deps) = dependents.get(&hash) {
for dep in deps {
if hashes_to_send.insert(*dep) {
stack.push(*dep);
}
}
}
}
let mut changes_to_send = Vec::new();
for hash in need {
hashes_to_send.insert(*hash);
if !change_hashes.contains(hash) {
let change = self.get_change_by_hash(hash);
if let Some(change) = change {
changes_to_send.push(change);
}
}
}
for change in changes {
if hashes_to_send.contains(&change.hash()) {
changes_to_send.push(change);
}
}
Ok(changes_to_send)
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ReadMessageError {
#[error("expected {expected_one_of:?} but found {found}")]
WrongType { expected_one_of: Vec<u8>, found: u8 },
#[error("{0}")]
Parse(String),
#[error(transparent)]
ReadChangeOps(#[from] ReadChangeOpError),
#[error("not enough input")]
NotEnoughInput,
}
impl From<parse::leb128::Error> for ReadMessageError {
fn from(e: parse::leb128::Error) -> Self {
ReadMessageError::Parse(e.to_string())
}
}
impl From<bloom::ParseError> for ReadMessageError {
fn from(e: bloom::ParseError) -> Self {
ReadMessageError::Parse(e.to_string())
}
}
impl From<crate::storage::change::ParseError> for ReadMessageError {
fn from(e: crate::storage::change::ParseError) -> Self {
ReadMessageError::Parse(format!("error parsing changes: {}", e))
}
}
impl From<ReadMessageError> for parse::ParseError<ReadMessageError> {
fn from(e: ReadMessageError) -> Self {
parse::ParseError::Error(e)
}
}
impl From<parse::ParseError<ReadMessageError>> for ReadMessageError {
fn from(p: parse::ParseError<ReadMessageError>) -> Self {
match p {
parse::ParseError::Error(e) => e,
parse::ParseError::Incomplete(..) => Self::NotEnoughInput,
}
}
}
/// The sync message to be sent.
#[derive(Clone, Debug, PartialEq)]
pub struct Message {
/// The heads of the sender.
pub heads: Vec<ChangeHash>,
/// The hashes of any changes that are being explicitly requested from the recipient.
pub need: Vec<ChangeHash>,
/// A summary of the changes that the sender already has.
pub have: Vec<Have>,
/// The changes for the recipient to apply.
pub changes: Vec<Change>,
}
impl serde::Serialize for Message {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = serializer.serialize_map(Some(4))?;
map.serialize_entry("heads", &self.heads)?;
map.serialize_entry("need", &self.need)?;
map.serialize_entry("have", &self.have)?;
map.serialize_entry(
"changes",
&self
.changes
.iter()
.map(crate::ExpandedChange::from)
.collect::<Vec<_>>(),
)?;
map.end()
}
}
fn parse_have(input: parse::Input<'_>) -> parse::ParseResult<'_, Have, ReadMessageError> {
let (i, last_sync) = parse::length_prefixed(parse::change_hash)(input)?;
let (i, bloom_bytes) = parse::length_prefixed_bytes(i)?;
let (_, bloom) = BloomFilter::parse(parse::Input::new(bloom_bytes)).map_err(|e| e.lift())?;
Ok((i, Have { last_sync, bloom }))
}
impl Message {
pub fn decode(input: &[u8]) -> Result<Self, ReadMessageError> {
let input = parse::Input::new(input);
match Self::parse(input) {
Ok((_, msg)) => Ok(msg),
Err(parse::ParseError::Error(e)) => Err(e),
Err(parse::ParseError::Incomplete(_)) => Err(ReadMessageError::NotEnoughInput),
}
}
pub(crate) fn parse(input: parse::Input<'_>) -> parse::ParseResult<'_, Self, ReadMessageError> {
let (i, message_type) = parse::take1(input)?;
if message_type != MESSAGE_TYPE_SYNC {
return Err(parse::ParseError::Error(ReadMessageError::WrongType {
expected_one_of: vec![MESSAGE_TYPE_SYNC],
found: message_type,
}));
}
let (i, heads) = parse::length_prefixed(parse::change_hash)(i)?;
let (i, need) = parse::length_prefixed(parse::change_hash)(i)?;
let (i, have) = parse::length_prefixed(parse_have)(i)?;
let change_parser = |i| {
let (i, bytes) = parse::length_prefixed_bytes(i)?;
let (_, change) =
StoredChange::parse(parse::Input::new(bytes)).map_err(|e| e.lift())?;
Ok((i, change))
};
let (i, stored_changes) = parse::length_prefixed(change_parser)(i)?;
let changes_len = stored_changes.len();
let changes: Vec<Change> = stored_changes
.into_iter()
.try_fold::<_, _, Result<_, ReadMessageError>>(
Vec::with_capacity(changes_len),
|mut acc, stored| {
let change = Change::new_from_unverified(stored.into_owned(), None)
.map_err(ReadMessageError::ReadChangeOps)?;
acc.push(change);
Ok(acc)
},
)?;
Ok((
i,
Message {
heads,
need,
have,
changes,
},
))
}
pub fn encode(mut self) -> Vec<u8> {
let mut buf = vec![MESSAGE_TYPE_SYNC];
encode_hashes(&mut buf, &self.heads);
encode_hashes(&mut buf, &self.need);
encode_many(&mut buf, self.have.iter(), |buf, h| {
encode_hashes(buf, &h.last_sync);
leb128::write::unsigned(buf, h.bloom.to_bytes().len() as u64).unwrap();
buf.extend(h.bloom.to_bytes());
});
encode_many(&mut buf, self.changes.iter_mut(), |buf, change| {
leb128::write::unsigned(buf, change.raw_bytes().len() as u64).unwrap();
buf.extend(change.raw_bytes().as_ref())
});
buf
}
}
fn encode_many<'a, I, It, F>(out: &mut Vec<u8>, data: I, f: F)
where
I: Iterator<Item = It> + ExactSizeIterator + 'a,
F: Fn(&mut Vec<u8>, It),
{
leb128::write::unsigned(out, data.len() as u64).unwrap();
for datum in data {
f(out, datum)
}
}
fn encode_hashes(buf: &mut Vec<u8>, hashes: &[ChangeHash]) {
debug_assert!(
hashes.windows(2).all(|h| h[0] <= h[1]),
"hashes were not sorted"
);
encode_many(buf, hashes.iter(), |buf, hash| buf.extend(hash.as_bytes()))
}
fn advance_heads(
my_old_heads: &HashSet<&ChangeHash>,
my_new_heads: &HashSet<ChangeHash>,
our_old_shared_heads: &[ChangeHash],
) -> Vec<ChangeHash> {
let new_heads = my_new_heads
.iter()
.filter(|head| !my_old_heads.contains(head))
.copied()
.collect::<Vec<_>>();
let common_heads = our_old_shared_heads
.iter()
.filter(|head| my_new_heads.contains(head))
.copied()
.collect::<Vec<_>>();
let mut advanced_heads = HashSet::with_capacity(new_heads.len() + common_heads.len());
for head in new_heads.into_iter().chain(common_heads) {
advanced_heads.insert(head);
}
let mut advanced_heads = advanced_heads.into_iter().collect::<Vec<_>>();
advanced_heads.sort();
advanced_heads
}
#[cfg(test)]
mod tests {
use super::*;
use crate::change::gen::gen_change;
use crate::storage::parse::Input;
use crate::types::gen::gen_hash;
use proptest::prelude::*;
prop_compose! {
fn gen_bloom()(hashes in gen_sorted_hashes(0..10)) -> BloomFilter {
BloomFilter::from_hashes(hashes.into_iter())
}
}
prop_compose! {
fn gen_have()(bloom in gen_bloom(), last_sync in gen_sorted_hashes(0..10)) -> Have {
Have {
bloom,
last_sync,
}
}
}
fn gen_sorted_hashes(size: std::ops::Range<usize>) -> impl Strategy<Value = Vec<ChangeHash>> {
proptest::collection::vec(gen_hash(), size).prop_map(|mut h| {
h.sort();
h
})
}
prop_compose! {
fn gen_sync_message()(
heads in gen_sorted_hashes(0..10),
need in gen_sorted_hashes(0..10),
have in proptest::collection::vec(gen_have(), 0..10),
changes in proptest::collection::vec(gen_change(), 0..10),
) -> Message {
Message {
heads,
need,
have,
changes,
}
}
}
#[test]
fn encode_decode_empty_message() {
let msg = Message {
heads: vec![],
need: vec![],
have: vec![],
changes: vec![],
};
let encoded = msg.encode();
Message::parse(Input::new(&encoded)).unwrap();
}
proptest! {
#[test]
fn encode_decode_message(msg in gen_sync_message()) {
let encoded = msg.clone().encode();
let (i, decoded) = Message::parse(Input::new(&encoded)).unwrap();
assert!(i.is_empty());
assert_eq!(msg, decoded);
}
}
}

View file

@ -1,462 +0,0 @@
use std::num::NonZeroU64;
use crate::automerge::Actor;
use crate::exid::ExId;
use crate::query::{self, OpIdSearch};
use crate::storage::Change as StoredChange;
use crate::types::{Key, ObjId, OpId};
use crate::{op_tree::OpSetMetadata, types::Op, Automerge, Change, ChangeHash, OpObserver, Prop};
use crate::{AutomergeError, ObjType, OpType, ScalarValue};
#[derive(Debug, Clone)]
pub(crate) struct TransactionInner {
pub(crate) actor: usize,
pub(crate) seq: u64,
pub(crate) start_op: NonZeroU64,
pub(crate) time: i64,
pub(crate) message: Option<String>,
pub(crate) deps: Vec<ChangeHash>,
pub(crate) operations: Vec<(ObjId, Prop, Op)>,
}
impl TransactionInner {
pub(crate) fn pending_ops(&self) -> usize {
self.operations.len()
}
/// Commit the operations performed in this transaction, returning the hashes corresponding to
/// the new heads.
#[tracing::instrument(skip(self, doc))]
pub(crate) fn commit(
mut self,
doc: &mut Automerge,
message: Option<String>,
time: Option<i64>,
) -> ChangeHash {
if message.is_some() {
self.message = message;
}
if let Some(t) = time {
self.time = t;
}
let num_ops = self.pending_ops();
let change = self.export(&doc.ops.m);
let hash = change.hash();
#[cfg(not(debug_assertions))]
tracing::trace!(commit=?hash, deps=?change.deps(), "committing transaction");
#[cfg(debug_assertions)]
{
let ops = change.iter_ops().collect::<Vec<_>>();
tracing::trace!(commit=?hash, ?ops, deps=?change.deps(), "committing transaction");
}
doc.update_history(change, num_ops);
debug_assert_eq!(doc.get_heads(), vec![hash]);
hash
}
#[tracing::instrument(skip(self, metadata))]
pub(crate) fn export(self, metadata: &OpSetMetadata) -> Change {
use crate::storage::{change::PredOutOfOrder, convert::op_as_actor_id};
let actor = metadata.actors.get(self.actor).clone();
let ops = self.operations.iter().map(|o| (&o.0, &o.2));
//let (ops, other_actors) = encode_change_ops(ops, actor.clone(), actors, props);
let deps = self.deps.clone();
let stored = match StoredChange::builder()
.with_actor(actor)
.with_seq(self.seq)
.with_start_op(self.start_op)
.with_message(self.message.clone())
.with_dependencies(deps)
.with_timestamp(self.time)
.build(
ops.into_iter()
.map(|(obj, op)| op_as_actor_id(obj, op, metadata)),
) {
Ok(s) => s,
Err(PredOutOfOrder) => {
// SAFETY: types::Op::preds is `types::OpIds` which ensures ops are always sorted
panic!("preds out of order");
}
};
#[cfg(debug_assertions)]
{
let realized_ops = self.operations.iter().collect::<Vec<_>>();
tracing::trace!(?stored, ops=?realized_ops, "committing change");
}
#[cfg(not(debug_assertions))]
tracing::trace!(?stored, "committing change");
Change::new(stored)
}
/// Undo the operations added in this transaction, returning the number of cancelled
/// operations.
pub(crate) fn rollback(self, doc: &mut Automerge) -> usize {
let num = self.pending_ops();
// remove in reverse order so sets are removed before makes etc...
for (obj, _prop, op) in self.operations.into_iter().rev() {
for pred_id in &op.pred {
if let Some(p) = doc.ops.search(&obj, OpIdSearch::new(*pred_id)).index() {
doc.ops.replace(&obj, p, |o| o.remove_succ(&op));
}
}
if let Some(pos) = doc.ops.search(&obj, OpIdSearch::new(op.id)).index() {
doc.ops.remove(&obj, pos);
}
}
// remove the actor from the cache so that it doesn't end up in the saved document
if doc.states.get(&self.actor).is_none() && doc.ops.m.actors.len() > 0 {
let actor = doc.ops.m.actors.remove_last();
doc.actor = Actor::Unused(actor);
}
num
}
/// Set the value of property `P` to value `V` in object `obj`.
///
/// # Returns
///
/// The opid of the operation which was created, or None if this operation doesn't change the
/// document
///
/// # Errors
///
/// This will return an error if
/// - The object does not exist
/// - The key is the wrong type for the object
/// - The key does not exist in the object
pub(crate) fn put<P: Into<Prop>, V: Into<ScalarValue>, Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
ex_obj: &ExId,
prop: P,
value: V,
) -> Result<(), AutomergeError> {
let obj = doc.exid_to_obj(ex_obj)?;
let value = value.into();
let prop = prop.into();
self.local_op(doc, op_observer, obj, prop, value.into())?;
Ok(())
}
/// Set the value of property `P` to value `V` in object `obj`.
///
/// # Returns
///
/// The opid of the operation which was created, or None if this operation doesn't change the
/// document
///
/// # Errors
///
/// This will return an error if
/// - The object does not exist
/// - The key is the wrong type for the object
/// - The key does not exist in the object
pub(crate) fn put_object<P: Into<Prop>, Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
ex_obj: &ExId,
prop: P,
value: ObjType,
) -> Result<ExId, AutomergeError> {
let obj = doc.exid_to_obj(ex_obj)?;
let prop = prop.into();
let id = self
.local_op(doc, op_observer, obj, prop, value.into())?
.unwrap();
let id = doc.id_to_exid(id);
Ok(id)
}
fn next_id(&mut self) -> OpId {
OpId(self.start_op.get() + self.pending_ops() as u64, self.actor)
}
#[allow(clippy::too_many_arguments)]
fn insert_local_op<Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
prop: Prop,
op: Op,
pos: usize,
obj: ObjId,
succ_pos: &[usize],
) {
doc.ops.add_succ(&obj, succ_pos.iter().copied(), &op);
if !op.is_delete() {
doc.ops.insert(pos, &obj, op.clone());
}
self.finalize_op(doc, op_observer, obj, prop, op);
}
pub(crate) fn insert<V: Into<ScalarValue>, Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
ex_obj: &ExId,
index: usize,
value: V,
) -> Result<(), AutomergeError> {
let obj = doc.exid_to_obj(ex_obj)?;
let value = value.into();
tracing::trace!(obj=?obj, value=?value, "inserting value");
self.do_insert(doc, op_observer, obj, index, value.into())?;
Ok(())
}
pub(crate) fn insert_object<Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
ex_obj: &ExId,
index: usize,
value: ObjType,
) -> Result<ExId, AutomergeError> {
let obj = doc.exid_to_obj(ex_obj)?;
let id = self.do_insert(doc, op_observer, obj, index, value.into())?;
let id = doc.id_to_exid(id);
Ok(id)
}
fn do_insert<Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
obj: ObjId,
index: usize,
action: OpType,
) -> Result<OpId, AutomergeError> {
let id = self.next_id();
let query = doc.ops.search(&obj, query::InsertNth::new(index));
let key = query.key()?;
let op = Op {
id,
action,
key,
succ: Default::default(),
pred: Default::default(),
insert: true,
};
doc.ops.insert(query.pos(), &obj, op.clone());
self.finalize_op(doc, op_observer, obj, Prop::Seq(index), op);
Ok(id)
}
pub(crate) fn local_op<Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
obj: ObjId,
prop: Prop,
action: OpType,
) -> Result<Option<OpId>, AutomergeError> {
match prop {
Prop::Map(s) => self.local_map_op(doc, op_observer, obj, s, action),
Prop::Seq(n) => self.local_list_op(doc, op_observer, obj, n, action),
}
}
fn local_map_op<Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
obj: ObjId,
prop: String,
action: OpType,
) -> Result<Option<OpId>, AutomergeError> {
if prop.is_empty() {
return Err(AutomergeError::EmptyStringKey);
}
let id = self.next_id();
let prop_index = doc.ops.m.props.cache(prop.clone());
let query = doc.ops.search(&obj, query::Prop::new(prop_index));
// no key present to delete
if query.ops.is_empty() && action == OpType::Delete {
return Ok(None);
}
if query.ops.len() == 1 && query.ops[0].is_noop(&action) {
return Ok(None);
}
// increment operations are only valid against counter values.
// if there are multiple values (from conflicts) then we just need one of them to be a counter.
if matches!(action, OpType::Increment(_)) && query.ops.iter().all(|op| !op.is_counter()) {
return Err(AutomergeError::MissingCounter);
}
let pred = doc.ops.m.sorted_opids(query.ops.iter().map(|o| o.id));
let op = Op {
id,
action,
key: Key::Map(prop_index),
succ: Default::default(),
pred,
insert: false,
};
let pos = query.pos;
let ops_pos = query.ops_pos;
self.insert_local_op(doc, op_observer, Prop::Map(prop), op, pos, obj, &ops_pos);
Ok(Some(id))
}
fn local_list_op<Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
obj: ObjId,
index: usize,
action: OpType,
) -> Result<Option<OpId>, AutomergeError> {
let query = doc.ops.search(&obj, query::Nth::new(index));
let id = self.next_id();
let pred = doc.ops.m.sorted_opids(query.ops.iter().map(|o| o.id));
let key = query.key()?;
if query.ops.len() == 1 && query.ops[0].is_noop(&action) {
return Ok(None);
}
// increment operations are only valid against counter values.
// if there are multiple values (from conflicts) then we just need one of them to be a counter.
if matches!(action, OpType::Increment(_)) && query.ops.iter().all(|op| !op.is_counter()) {
return Err(AutomergeError::MissingCounter);
}
let op = Op {
id,
action,
key,
succ: Default::default(),
pred,
insert: false,
};
let pos = query.pos;
let ops_pos = query.ops_pos;
self.insert_local_op(doc, op_observer, Prop::Seq(index), op, pos, obj, &ops_pos);
Ok(Some(id))
}
pub(crate) fn increment<P: Into<Prop>, Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
obj: &ExId,
prop: P,
value: i64,
) -> Result<(), AutomergeError> {
let obj = doc.exid_to_obj(obj)?;
self.local_op(doc, op_observer, obj, prop.into(), OpType::Increment(value))?;
Ok(())
}
pub(crate) fn delete<P: Into<Prop>, Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
ex_obj: &ExId,
prop: P,
) -> Result<(), AutomergeError> {
let obj = doc.exid_to_obj(ex_obj)?;
let prop = prop.into();
self.local_op(doc, op_observer, obj, prop, OpType::Delete)?;
Ok(())
}
/// Splice new elements into the given sequence. Returns a vector of the OpIds used to insert
/// the new elements
pub(crate) fn splice<Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
ex_obj: &ExId,
mut pos: usize,
del: usize,
vals: impl IntoIterator<Item = ScalarValue>,
) -> Result<(), AutomergeError> {
let obj = doc.exid_to_obj(ex_obj)?;
for _ in 0..del {
// del()
self.local_op(doc, op_observer, obj, pos.into(), OpType::Delete)?;
}
for v in vals {
// insert()
self.do_insert(doc, op_observer, obj, pos, v.clone().into())?;
pos += 1;
}
Ok(())
}
fn finalize_op<Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
obj: ObjId,
prop: Prop,
op: Op,
) {
// TODO - id_to_exid should be a noop if not used - change type to Into<ExId>?
let ex_obj = doc.ops.id_to_exid(obj.0);
let parents = doc.ops.parents(obj);
if op.insert {
let value = (op.value(), doc.ops.id_to_exid(op.id));
match prop {
Prop::Map(_) => panic!("insert into a map"),
Prop::Seq(index) => op_observer.insert(parents, ex_obj, index, value),
}
} else if op.is_delete() {
op_observer.delete(parents, ex_obj, prop.clone());
} else if let Some(value) = op.get_increment_value() {
op_observer.increment(
parents,
ex_obj,
prop.clone(),
(value, doc.ops.id_to_exid(op.id)),
);
} else {
let value = (op.value(), doc.ops.id_to_exid(op.id));
op_observer.put(parents, ex_obj, prop.clone(), value, false);
}
self.operations.push((obj, prop, op));
}
}
#[cfg(test)]
mod tests {
use crate::{transaction::Transactable, ROOT};
use super::*;
#[test]
fn map_rollback_doesnt_panic() {
let mut doc = Automerge::new();
let mut tx = doc.transaction();
let a = tx.put_object(ROOT, "a", ObjType::Map).unwrap();
tx.put(&a, "b", 1).unwrap();
assert!(tx.get(&a, "b").unwrap().is_some());
}
}

View file

@ -1,200 +0,0 @@
use std::ops::RangeBounds;
use crate::exid::ExId;
use crate::{
AutomergeError, ChangeHash, Keys, KeysAt, ListRange, ListRangeAt, MapRange, MapRangeAt,
ObjType, Parents, Prop, ScalarValue, Value, Values,
};
/// A way of mutating a document within a single change.
pub trait Transactable {
/// Get the number of pending operations in this transaction.
fn pending_ops(&self) -> usize;
/// Set the value of property `P` to value `V` in object `obj`.
///
/// # Errors
///
/// This will return an error if
/// - The object does not exist
/// - The key is the wrong type for the object
/// - The key does not exist in the object
fn put<O: AsRef<ExId>, P: Into<Prop>, V: Into<ScalarValue>>(
&mut self,
obj: O,
prop: P,
value: V,
) -> Result<(), AutomergeError>;
/// Set the value of property `P` to the new object `V` in object `obj`.
///
/// # Returns
///
/// The id of the object which was created.
///
/// # Errors
///
/// This will return an error if
/// - The object does not exist
/// - The key is the wrong type for the object
/// - The key does not exist in the object
fn put_object<O: AsRef<ExId>, P: Into<Prop>>(
&mut self,
obj: O,
prop: P,
object: ObjType,
) -> Result<ExId, AutomergeError>;
/// Insert a value into a list at the given index.
fn insert<O: AsRef<ExId>, V: Into<ScalarValue>>(
&mut self,
obj: O,
index: usize,
value: V,
) -> Result<(), AutomergeError>;
/// Insert an object into a list at the given index.
fn insert_object<O: AsRef<ExId>>(
&mut self,
obj: O,
index: usize,
object: ObjType,
) -> Result<ExId, AutomergeError>;
/// Increment the counter at the prop in the object by `value`.
fn increment<O: AsRef<ExId>, P: Into<Prop>>(
&mut self,
obj: O,
prop: P,
value: i64,
) -> Result<(), AutomergeError>;
/// Delete the value at prop in the object.
fn delete<O: AsRef<ExId>, P: Into<Prop>>(
&mut self,
obj: O,
prop: P,
) -> Result<(), AutomergeError>;
fn splice<O: AsRef<ExId>, V: IntoIterator<Item = ScalarValue>>(
&mut self,
obj: O,
pos: usize,
del: usize,
vals: V,
) -> Result<(), AutomergeError>;
/// Like [`Self::splice`] but for text.
fn splice_text<O: AsRef<ExId>>(
&mut self,
obj: O,
pos: usize,
del: usize,
text: &str,
) -> Result<(), AutomergeError> {
let vals = text.chars().map(|c| c.into());
self.splice(obj, pos, del, vals)
}
/// Get the keys of the given object, it should be a map.
fn keys<O: AsRef<ExId>>(&self, obj: O) -> Keys<'_, '_>;
/// Get the keys of the given object at a point in history.
fn keys_at<O: AsRef<ExId>>(&self, obj: O, heads: &[ChangeHash]) -> KeysAt<'_, '_>;
fn map_range<O: AsRef<ExId>, R: RangeBounds<String>>(
&self,
obj: O,
range: R,
) -> MapRange<'_, R>;
fn map_range_at<O: AsRef<ExId>, R: RangeBounds<String>>(
&self,
obj: O,
range: R,
heads: &[ChangeHash],
) -> MapRangeAt<'_, R>;
fn list_range<O: AsRef<ExId>, R: RangeBounds<usize>>(
&self,
obj: O,
range: R,
) -> ListRange<'_, R>;
fn list_range_at<O: AsRef<ExId>, R: RangeBounds<usize>>(
&self,
obj: O,
range: R,
heads: &[ChangeHash],
) -> ListRangeAt<'_, R>;
fn values<O: AsRef<ExId>>(&self, obj: O) -> Values<'_>;
fn values_at<O: AsRef<ExId>>(&self, obj: O, heads: &[ChangeHash]) -> Values<'_>;
/// Get the length of the given object.
fn length<O: AsRef<ExId>>(&self, obj: O) -> usize;
/// Get the length of the given object at a point in history.
fn length_at<O: AsRef<ExId>>(&self, obj: O, heads: &[ChangeHash]) -> usize;
/// Get type for object
fn object_type<O: AsRef<ExId>>(&self, obj: O) -> Option<ObjType>;
/// Get the string that this text object represents.
fn text<O: AsRef<ExId>>(&self, obj: O) -> Result<String, AutomergeError>;
/// Get the string that this text object represents at a point in history.
fn text_at<O: AsRef<ExId>>(
&self,
obj: O,
heads: &[ChangeHash],
) -> Result<String, AutomergeError>;
/// Get the value at this prop in the object.
fn get<O: AsRef<ExId>, P: Into<Prop>>(
&self,
obj: O,
prop: P,
) -> Result<Option<(Value<'_>, ExId)>, AutomergeError>;
/// Get the value at this prop in the object at a point in history.
fn get_at<O: AsRef<ExId>, P: Into<Prop>>(
&self,
obj: O,
prop: P,
heads: &[ChangeHash],
) -> Result<Option<(Value<'_>, ExId)>, AutomergeError>;
fn get_all<O: AsRef<ExId>, P: Into<Prop>>(
&self,
obj: O,
prop: P,
) -> Result<Vec<(Value<'_>, ExId)>, AutomergeError>;
fn get_all_at<O: AsRef<ExId>, P: Into<Prop>>(
&self,
obj: O,
prop: P,
heads: &[ChangeHash],
) -> Result<Vec<(Value<'_>, ExId)>, AutomergeError>;
/// Get the parents of an object in the document tree.
///
/// ### Errors
///
/// Returns an error when the id given is not the id of an object in this document.
/// This function does not get the parents of scalar values contained within objects.
///
/// ### Experimental
///
/// This function may in future be changed to allow getting the parents from the id of a scalar
/// value.
fn parents<O: AsRef<ExId>>(&self, obj: O) -> Result<Parents<'_>, AutomergeError>;
fn path_to_object<O: AsRef<ExId>>(&self, obj: O) -> Result<Vec<(ExId, Prop)>, AutomergeError> {
let mut path = self.parents(obj.as_ref().clone())?.collect::<Vec<_>>();
path.reverse();
Ok(path)
}
}

View file

@ -1,31 +0,0 @@
// this assumes that the automerge-rs folder is checked out along side this repo
// and someone has run
// # cd automerge-rs/automerge-backend-wasm
// # yarn release
const { edits, finalText } = require('./editing-trace')
const Automerge = require('../../automerge')
const path = require('path')
const wasmBackend = require(path.resolve("../../automerge-rs/automerge-backend-wasm"))
Automerge.setDefaultBackend(wasmBackend)
const start = new Date()
let state = Automerge.from({text: new Automerge.Text()})
state = Automerge.change(state, doc => {
for (let i = 0; i < edits.length; i++) {
if (i % 10000 === 0) {
console.log(`Processed ${i} edits in ${new Date() - start} ms`)
}
if (edits[i][1] > 0) doc.text.deleteAt(edits[i][0], edits[i][1])
if (edits[i].length > 2) doc.text.insertAt(edits[i][0], ...edits[i].slice(2))
}
})
console.log(`Done in ${new Date() - start} ms`)
if (state.text.join('') !== finalText) {
throw new RangeError('ERROR: final text did not match expectation')
}

View file

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

View file

@ -3,39 +3,42 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils = {
url = "github:numtide/flake-utils";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
};
outputs = { self, nixpkgs, flake-utils, rust-overlay }:
outputs = {
self,
nixpkgs,
flake-utils,
rust-overlay,
}:
flake-utils.lib.eachDefaultSystem
(system:
let
(system: let
pkgs = import nixpkgs {
overlays = [ rust-overlay.overlay ];
overlays = [rust-overlay.overlays.default];
inherit system;
};
lib = pkgs.lib;
rust = pkgs.rust-bin.stable.latest.default;
cargoNix = pkgs.callPackage ./Cargo.nix {
inherit pkgs;
release = true;
in {
formatter = pkgs.alejandra;
packages = {
deadnix = pkgs.runCommand "deadnix" {} ''
${pkgs.deadnix}/bin/deadnix --fail ${./.}
mkdir $out
'';
};
debugCargoNix = pkgs.callPackage ./Cargo.nix {
inherit pkgs;
release = false;
checks = {
inherit (self.packages.${system}) deadnix;
};
in
{
devShell = pkgs.mkShell {
buildInputs = with pkgs;
[
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
(rust.override {
extensions = [ "rust-src" ];
targets = [ "wasm32-unknown-unknown" ];
extensions = ["rust-src"];
targets = ["wasm32-unknown-unknown"];
})
cargo-edit
cargo-watch
@ -51,6 +54,7 @@
nodejs
yarn
deno
# c deps
cmake

View file

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

15
javascript/.eslintrc.cjs Normal file
View file

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

View file

@ -1,3 +1,6 @@
/node_modules
/yarn.lock
dist
docs/
.vim
deno_dist/

View file

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

4
javascript/.prettierrc Normal file
View file

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

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