Crandore Hub

automerge

R Bindings for 'Automerge' 'CRDT' Library

Provides R bindings to the 'Automerge' Conflict-free Replicated Data Type ('CRDT') library. 'Automerge' enables automatic merging of concurrent changes without conflicts, making it ideal for distributed systems, collaborative applications, and offline-first architectures. The approach of local-first software was proposed in Kleppmann, M., Wiggins, A., van Hardenberg, P., McGranaghan, M. (2019) <doi:10.1145/3359591.3359737>. This package supports all 'Automerge' data types (maps, lists, text, counters) and provides both low-level and high-level synchronization protocols for seamless interoperability with 'JavaScript' and other 'Automerge' implementations.

README

# JavaScript ↔ R Interoperability Examples

This directory contains executable Node.js scripts that demonstrate and verify cross-platform synchronization between JavaScript and R using Automerge.

## Quick Start

### Automated Testing

The easiest way to verify interoperability:

```bash
# From package root directory
Rscript inst/js/run-examples.R

# Or from this directory (inst/js/)
Rscript run-examples.R
```

This automated script will:
1. Check for Node.js availability
2. Install required npm packages
3. Run all examples
4. Verify results
5. Display colored output showing success/failure

### Setup

```bash
# Install Node.js from https://nodejs.org/
```

The automated test runner (`run-examples.R`) handles npm package installation automatically using R's user cache directory. This means:

- npm packages are installed to `~/.cache/R/automerge/js/` (Unix) or similar OS-specific location
- Packages persist across R sessions and package reinstalls
- No pollution of the package source directory
- Only installed once; subsequent runs reuse the cache

If you want to install npm packages manually:

```bash
# Navigate to this directory
cd inst/js

# Install npm packages
npm install
```

This installs the `@automerge/automerge` package and dependencies locally.

## Available Scripts

### Example 1: Create a document in JavaScript

```bash
node create-shared-doc.js [output-path]
```

Creates an Automerge document in JavaScript and saves it to a binary file that can be loaded in R.

**Example:**
```bash
node create-shared-doc.js shared_doc.automerge
```

**Default output:** `shared_doc.automerge`

### Example 2: Verify R changes

```bash
node verify-r-changes.js [input-path]
```

Loads a document modified by R, verifies the changes, and adds JavaScript changes.

**Example:**
```bash
node verify-r-changes.js shared_doc.automerge
```

### Example 3: Make concurrent edits

```bash
node concurrent-edit.js <base-doc-path> <output-path>
```

Loads a base document and makes concurrent edits (simulating what would happen if a JavaScript user edited the document at the same time as an R user).

**Example:**
```bash
node concurrent-edit.js base.automerge js_edited.automerge
```

### Example 4: Verify merged document

```bash
node verify-merge.js <merged-doc-path>
```

Verifies that a merged document contains changes from both R and JavaScript, demonstrating CRDT conflict-free merge.

**Example:**
```bash
node verify-merge.js merged.automerge
```

## Directory Structure

```
inst/js/                        # Installed JavaScript examples
├── README.md                   # This file
├── run-examples.R              # Automated test runner
├── package.json                # npm dependencies
├── create-shared-doc.js        # Example 1: JS creates doc
├── verify-r-changes.js         # Example 2: JS verifies R changes
├── concurrent-edit.js          # Example 3: JS concurrent edit
└── verify-merge.js             # Example 4: Verify CRDT merge

vignettes/
└── cross-platform.Rmd          # Main vignette with examples
```

## Running from R

### Interactive Use

From R, you can execute these scripts:

```r
# Check if Node.js is available
node_available <- system2("node", "--version", stdout = FALSE, stderr = FALSE) == 0

if (node_available) {
  # Get JavaScript directory (works with installed package or source)
  js_dir <- system.file("js", package = "automerge")

  # Run example 1
  temp_file <- tempfile(fileext = ".automerge")
  system2("node", c(file.path(js_dir, "create-shared-doc.js"), temp_file),
          stdout = TRUE, stderr = TRUE)

  # Load the result
  doc <- automerge::am_load(readBin(temp_file, "raw", 1e7))
  print(doc)
}
```

### Finding Scripts After Installation

When the package is installed:

```r
# Get directory path
js_dir <- system.file("js", package = "automerge")

# List available scripts
list.files(js_dir, pattern = "\\.js$")
```

## Manual Testing Examples

### Example 1: JavaScript → R

```bash
# From installed package
JS_DIR=$(Rscript -e "cat(system.file('js', package='automerge'))")
node $JS_DIR/create-shared-doc.js test.automerge

# Or from source
node inst/js/create-shared-doc.js test.automerge
```

```r
# Load in R
library(automerge)
doc <- am_load(readBin("test.automerge", "raw", 1e7))
print(doc)
```

### Example 2: R → JavaScript

```r
# Modify in R
library(automerge)
doc <- am_load(readBin("test.automerge", "raw", 1e7))
doc[["r_data"]] <- list(value = 42, timestamp = Sys.time())
am_commit(doc, "Add R data")
writeBin(am_save(doc), "test.automerge")
```

```bash
# Verify in JavaScript
node inst/js/verify-r-changes.js test.automerge
```

### Example 3: Concurrent Edits with CRDT Merge

```r
# Create base document
library(automerge)
base <- am_create() |>
  am_put(AM_ROOT, "document", "Shared") |>
  am_put(AM_ROOT, "sections", am_list())
writeBin(am_save(base), "base.automerge")

# R makes concurrent edit
r_doc <- am_load(readBin("base.automerge", "raw", 1e7))
sections <- r_doc[["sections"]]
am_insert(r_doc, sections, 1, list(title = "R Section", author = "R Team"))
writeBin(am_save(r_doc), "r_edit.automerge")
```

```bash
# JavaScript makes concurrent edit (from same base)
node inst/js/concurrent-edit.js base.automerge js_edit.automerge
```

```r
# Merge in R
js_doc <- am_load(readBin("js_edit.automerge", "raw", 1e7))
am_merge(r_doc, js_doc)

# Verify both changes present
sections_merged <- r_doc[["sections"]]
am_length(r_doc, sections_merged)  # Should be 2

# Save merged document
writeBin(am_save(r_doc), "merged.automerge")
```

```bash
# Verify merge contains both changes
node inst/js/verify-merge.js merged.automerge
```

## Integration with Package Tests

These scripts can be used to create test fixtures:

```r
# tests/testthat/helper-js-fixtures.R
create_js_fixtures <- function() {
  if (system2("node", "--version", stdout = FALSE, stderr = FALSE) != 0) {
    skip("Node.js not available")
  }

  js_dir <- system.file("js", package = "automerge")
  if (js_dir == "") {
    js_dir <- file.path(getwd(), "..", "..", "inst", "js")
  }

  fixture_dir <- test_path("fixtures")
  dir.create(fixture_dir, showWarnings = FALSE, recursive = TRUE)

  system2("node", c(file.path(js_dir, "create-shared-doc.js"),
                    file.path(fixture_dir, "js_document.automerge")))
}
```

Example test:

```r
# tests/testthat/test-js-interop.R

test_that("JavaScript interoperability works", {
  skip_if_not(system2("node", "--version", stdout = FALSE, stderr = FALSE) == 0,
              "Node.js not available")

  # Get JavaScript directory (works with installed package or source)
  js_dir <- system.file("js", package = "automerge")
  if (js_dir == "") {
    js_dir <- file.path(getwd(), "..", "..", "inst", "js")
  }

  # Run JavaScript to create fixture
  js_script <- file.path(js_dir, "create-shared-doc.js")
  temp_file <- tempfile(fileext = ".automerge")

  system2("node", c(js_script, temp_file))

  # Load in R
  doc <- am_load(readBin(temp_file, "raw", 1e7))

  # Verify
  expect_equal(doc[["title"]], "Collaborative Analysis")
  expect_equal(doc[["metadata"]][["created_by"]], "javascript")
})
```

## CI/CD Integration

For automated testing in CI/CD:

```yaml
# .github/workflows/test.yml
steps:
  - name: Install Node.js
    uses: actions/setup-node@v3
    with:
      node-version: '18'

  - name: Install npm packages
    run: |
      cd inst/js
      npm install

  - name: Run interoperability tests
    run: Rscript inst/js/run-examples.R
```

## Troubleshooting

### "Node.js not available"

Install Node.js from https://nodejs.org/

### "Cannot find module '@automerge/automerge'"

The automated test runner should install packages to the cache automatically. If you see this error:

1. Check cache location:
   ```r
   tools::R_user_dir("automerge", which = "cache")
   ```

2. Clear cache and retry:
   ```r
   unlink(tools::R_user_dir("automerge", which = "cache"), recursive = TRUE)
   ```

3. Or install manually:
   ```bash
   cd inst/js
   npm install
   ```

### Cleaning the Cache

To remove cached npm packages:

```r
# Clean automerge cache
cache_dir <- tools::R_user_dir("automerge", which = "cache")
unlink(cache_dir, recursive = TRUE)
```

This will force reinstallation on next run.

### Different actor IDs

Each session generates a random actor ID. This is expected behavior.

### Timestamp precision

JavaScript uses milliseconds since epoch, R uses seconds. Small differences (< 1ms) are expected.

## What Gets Verified

The test suite verifies:

- ✓ JavaScript → R document loading
- ✓ R → JavaScript document modifications
- ✓ Bidirectional sync and merge
- ✓ Type compatibility (strings, numbers, lists, maps, timestamps)
- ✓ CRDT conflict-free merging
- ✓ Binary format compatibility
- ✓ Change history preservation

## Further Reading

- Main vignette: `vignette("cross-platform", package = "automerge")`
- [Automerge Website](https://automerge.org)
- [Binary Format Specification](https://automerge.org/automerge-binary-format-spec)
- [JavaScript API Documentation](https://automerge.org/docs/api/)

Versions across snapshots

VersionRepositoryFileSize
0.4.0 2026-04-09 windows/windows R-4.5 automerge_0.4.0.zip 1.3 MiB

Dependencies (latest)

Suggests