Cross-Platform Synchronization: JavaScript ↔ R
Source:vignettes/cross-platform.Rmd
cross-platform.RmdRunning the Examples
This vignette includes executable JavaScript and R code examples. To run them:
Option 1: Automated Test Script
The easiest way to verify JavaScript ↔︎ R interoperability:
# From package root directory
Rscript inst/js/run-examples.R
# Or from inst/js/ directory
cd inst/js
Rscript run-examples.RThis script will:
- Check for Node.js availability
- Install required npm packages
(
@automerge/automerge) - Execute all examples and verify results
- Display output showing success/failure
Option 2: Manual Execution
Prerequisites:
# Install Node.js from https://nodejs.org/
# Get JavaScript directory
# From installed package:
R -e "cat(system.file('js', package = 'automerge'))"
# From source: inst/js/
cd inst/js
npm installRun individual examples:
Option 3: Interactive Verification
library(automerge)
# Check if Node.js is available
if (system2("node", "--version", stdout = FALSE, stderr = FALSE) == 0) {
# Get JavaScript directory
js_dir <- system.file("js", package = "automerge")
# Run JavaScript example
temp_file <- tempfile(fileext = ".automerge")
system2("node", c(file.path(js_dir, "create-shared-doc.js"), temp_file))
# Load in R
doc <- am_load(readBin(temp_file, "raw", 1e7))
print(doc)
}Overview
One of Automerge’s key strengths is seamless synchronization across different platforms and programming languages. This vignette demonstrates how documents created in JavaScript can be synced with R and vice versa, enabling collaborative workflows across different technology stacks.
Binary Format Compatibility
Automerge uses a standardized binary format (see automerge.org/automerge-binary-format-spec) that is identical across all implementations. This means:
- Documents saved in JavaScript can be loaded in R
- Changes made in R can be merged back into JavaScript
- The sync protocol works seamlessly between platforms
- All CRDT types (maps, lists, text, counters) are fully compatible
Prerequisites
You’ll need:
-
JavaScript:
@automerge/automergepackage (npm install) -
R:
automergepackage (this package) - A way to exchange binary data between environments (files, HTTP, WebSockets, etc.)
For these examples, we’ll use file-based exchange with Node.js on the JavaScript side.
Example 1: Creating a Document in JavaScript, Loading in R
JavaScript Side
// Node.js or browser
import * as Automerge from '@automerge/automerge'
const fs = require('fs')
// Create a document
let doc = Automerge.init()
// Add some data
doc = Automerge.change(doc, 'Initial data', doc => {
doc.title = 'Collaborative Analysis'
doc.datasets = []
doc.datasets.push({ name: 'sales_2024', rows: 1000 })
doc.datasets.push({ name: 'customers', rows: 5000 })
doc.metadata = {
created_by: 'javascript',
created_at: new Date().toISOString(),
version: '1.0'
}
})
// Save to binary format
const bytes = Automerge.save(doc)
// Write to file (Node.js)
fs.writeFileSync('shared_doc.automerge', bytes)
console.log('Document created and saved')
console.log('Actor ID:', Automerge.getActorId(doc))R Side
library(automerge)
# Load the document created in JavaScript
doc_bytes <- readBin("shared_doc.automerge", "raw", 1e7)
doc <- am_load(doc_bytes)
# Examine the document
print(doc)
# Access data created in JavaScript
cat("Title:", doc[["title"]], "\n")
cat("Created by:", doc[["metadata"]][["created_by"]], "\n")
# Show datasets
datasets <- doc[["datasets"]]
cat("Number of datasets:", am_length(doc, datasets), "\n")
# Examine first dataset (R uses 1-based indexing)
dataset1 <- am_get(doc, datasets, 1)
cat(
"First dataset:",
am_get(doc, dataset1, "name"),
"with",
am_get(doc, dataset1, "rows"),
"rows\n"
)Example 2: Modifying in R, Syncing Back to JavaScript
R Side - Make Changes
# Continue from previous example
# Add analysis results from R
am_put(
doc,
AM_ROOT,
"r_analysis",
list(
performed_by = "R",
timestamp = Sys.time(),
R_version = paste(R.version$major, R.version$minor, sep = "."),
summary_stats = list(
mean_sales = 45231.5,
median_sales = 38900.0,
total_customers = 5000L
)
)
)
# Commit changes
am_commit(doc, "Added R analysis results")
# Save back to file
writeBin(am_save(doc), "shared_doc.automerge")
cat("Document updated by R and saved\n")
cat("R Actor ID:", am_get_actor_hex(doc), "\n")JavaScript Side - Load Updated Document
// Load the updated document
const updatedBytes = fs.readFileSync('shared_doc.automerge')
let updatedDoc = Automerge.load(updatedBytes)
console.log('Document loaded with R changes')
console.log('Title:', updatedDoc.title)
console.log('R Analysis:', updatedDoc.r_analysis)
console.log('Mean sales:', updatedDoc.r_analysis.summary_stats.mean_sales)
console.log('Analysis performed by:', updatedDoc.r_analysis.performed_by)
// View change history
const changes = Automerge.getAllChanges(updatedDoc)
console.log(`Total changes: ${changes.length}`)
// Make additional changes in JavaScript
updatedDoc = Automerge.change(updatedDoc, 'Add JS visualization', doc => {
doc.visualizations = []
doc.visualizations.push({
type: 'bar_chart',
data_source: 'r_analysis.summary_stats',
created_in: 'javascript'
})
})
// Save for next R session
fs.writeFileSync('shared_doc.automerge', Automerge.save(updatedDoc))Example 3: Real-Time Sync Protocol
This example shows how to use the sync protocol for real-time synchronization between JavaScript and R.
R Side - Set Up Sync
# Initial R document
r_doc <- am_create() |>
am_put(AM_ROOT, "source", "R") |>
am_put(
AM_ROOT,
"data",
list(
r_value = 123,
timestamp = Sys.time()
)
) |>
am_commit("Initial R doc")
# Create sync state
r_sync <- am_sync_state_new()
# Generate sync message to send to JavaScript
sync_msg_to_js <- am_sync_encode(r_doc, r_sync)
# Save sync message to file (in practice, send over network)
writeBin(sync_msg_to_js, "r_to_js_sync.bin")
cat("R sync message ready:", length(sync_msg_to_js), "bytes\n")JavaScript Side - Receive and Respond
// Initial JavaScript document
let jsDoc = Automerge.change(Automerge.init(), 'Initial', doc => {
doc.source = 'JavaScript'
doc.data = {
js_value: 456,
timestamp: Date.now()
}
})
// Create sync state
let jsSyncState = Automerge.initSyncState()
// Load sync message from R
const syncMsgFromR = fs.readFileSync('r_to_js_sync.bin')
// Receive sync message and update document
;[jsDoc, jsSyncState] = Automerge.receiveSyncMessage(
jsDoc,
jsSyncState,
syncMsgFromR
)
console.log('Received sync from R')
console.log('Document now has:', Object.keys(jsDoc))
// Generate response sync message
const syncMsgToR = Automerge.generateSyncMessage(jsDoc, jsSyncState)
if (syncMsgToR) {
fs.writeFileSync('js_to_r_sync.bin', syncMsgToR)
console.log('JS sync message ready:', syncMsgToR.length, 'bytes')
}R Side - Complete Sync
# Load sync message from JavaScript
sync_msg_from_js <- readBin("js_to_r_sync.bin", "raw", 1e7)
# Apply sync message
am_sync_decode(r_doc, r_sync, sync_msg_from_js)
# Documents are now synchronized
cat("Sync complete!\n")
cat("R document now contains:\n")
print(names(r_doc))
# Verify we have data from JavaScript
if (!is.null(r_doc[["data"]][["js_value"]])) {
cat("JavaScript value:", r_doc[["data"]][["js_value"]], "\n")
}Example 4: Concurrent Edits and Automatic Merge
This demonstrates Automerge’s CRDT capabilities with concurrent edits in both platforms.
JavaScript - Concurrent Edit 1
// Load shared document
let jsDoc = Automerge.load(fs.readFileSync('concurrent_doc.automerge'))
// JavaScript makes changes
jsDoc = Automerge.change(jsDoc, 'Add JS section', doc => {
doc.sections.push({
title: 'JavaScript Analysis',
content: 'Web visualization results',
author: 'JS Team'
})
doc.js_edit_time = Date.now()
})
// Save changes
fs.writeFileSync('js_concurrent.automerge', Automerge.save(jsDoc))Or run the provided script:
R - Concurrent Edit 2
# Load the same original document
r_doc <- am_load(shared_bytes)
# R makes different changes to the same document
sections <- r_doc[["sections"]]
am_insert(
r_doc,
sections,
1,
list(
title = "R Statistical Analysis",
content = "Regression model results",
author = "R Team"
)
)
am_put(r_doc, AM_ROOT, "r_edit_time", Sys.time())
am_commit(r_doc, "Add R section")
# Save R changes
writeBin(am_save(r_doc), "r_concurrent.automerge")Merge Concurrent Changes (R Side)
# Load JavaScript version
js_doc_bytes <- readBin("js_concurrent.automerge", "raw", 1e7)
js_doc <- am_load(js_doc_bytes)
# Merge JavaScript changes into R document
am_merge(r_doc, js_doc)
# Verify merge - should have both sections
sections_merged <- r_doc[["sections"]]
cat(
"After merge, document has",
am_length(r_doc, sections_merged),
"sections\n"
)
# Section 1 (from R)
section1 <- am_get(r_doc, sections_merged, 1)
cat("Section 1:", am_get(r_doc, section1, "title"), "\n")
# Section 2 (from JavaScript)
section2 <- am_get(r_doc, sections_merged, 2)
cat("Section 2:", am_get(r_doc, section2, "title"), "\n")
# Both timestamps preserved
cat("R edit time:", r_doc[["r_edit_time"]], "\n")
cat("JS edit time:", r_doc[["js_edit_time"]], "\n")Merge Concurrent Changes (JavaScript Side)
The same merge can be done on the JavaScript side:
// JavaScript loads R version and merges
const rDocBytes = fs.readFileSync('r_concurrent.automerge')
const rDoc = Automerge.load(rDocBytes)
// Merge R changes into JS document
jsDoc = Automerge.merge(jsDoc, rDoc)
// Verify - both sections present
console.log('After merge, sections:', jsDoc.sections.length)
console.log('Section 0:', jsDoc.sections[0].title, '(from R)')
console.log('Section 1:', jsDoc.sections[1].title, '(from JS)')
// Both timestamps preserved
console.log('R edit time:', jsDoc.r_edit_time)
console.log('JS edit time:', jsDoc.js_edit_time)Or verify using the provided script:
Example 5: Text CRDT Synchronization
Text objects are particularly interesting as they demonstrate character-level CRDT merge.
R - Load and Edit Text
# Load text document
text_doc <- am_load(readBin("text_doc.automerge", "raw", 1e7))
# Get text object
notes <- am_get(text_doc, AM_ROOT, "notes")
# Append text in R (0-based position indexing)
current_length <- am_length(text_doc, notes)
am_text_splice(notes, current_length, 0, " and R!")
am_commit(text_doc, "R appended text")
# Get full text
full_text <- am_text_content(notes)
cat("Text after R edit:", full_text, "\n")
# Output: "Hello from JavaScript and R!"
# Save back
writeBin(am_save(text_doc), "text_doc.automerge")Type Compatibility Matrix
| Automerge | JavaScript | R | Notes |
|---|---|---|---|
| Map | Object {}
|
Named list | Root is always a map |
| List | Array []
|
Unnamed list | R uses 1-based indexing |
| Text | Automerge.Text |
Text object (am_text) | Character-level CRDT |
| String | string |
character(1) |
UTF-8 encoding |
| Number (int) | number |
integer / double
|
32-bit int if in range, else double |
| Number (uint64) | BigInt |
am_uint64 |
Unsigned 64-bit integer |
| Number (float) | number |
double |
Double precision (64-bit) |
| Boolean | boolean |
logical |
TRUE/FALSE |
| Null | null |
NULL |
Absence of value |
| Bytes | Uint8Array |
raw |
Binary data |
| Timestamp |
Date / number
|
POSIXct |
Milliseconds since epoch |
| Counter | CRDT counter | am_counter |
Conflict-free counter |
Important Notes:
Integer Sizes: Automerge stores 64-bit signed integers internally. R integers are 32-bit, so values outside the range ±2,147,483,647 are automatically converted to
numeric(double). JavaScript uses 64-bit floats for all numbers (safe integers up to ±9,007,199,254,740,991).List Indexing: JavaScript uses 0-based indexing (
array[0]), R uses 1-based indexing (am_get(doc, list_obj, 1))Text Operations: Both use 0-based positions for text operations (splice, cursors, marks)
UTF-32 vs UTF-16: R bindings use UTF-32 character indexing by default, JavaScript uses UTF-16. Positions may differ for emoji and some Unicode characters.
Binary Format Details
The saved document format includes:
- Change history: All operations since document creation
- Actor IDs: Unique identifiers for each editing session
- Dependencies: Causal relationships between changes
- Compressed storage: Columnar format with RLE compression
The binary format is deterministic and identical across platforms, enabling:
- File-based collaboration (Dropbox, Git LFS, etc.)
- Network synchronization (HTTP, WebSockets)
- Conflict-free merging regardless of edit order
Troubleshooting
Character Encoding Issues
Both JavaScript and R use UTF-8 for strings. If you encounter encoding issues:
Actor ID Collisions
Each platform generates random actor IDs. To use custom IDs:
# R - specify actor ID as raw bytes or hex string
doc <- am_create(actor_id = "r-session-123")Indexing Differences
Remember the indexing conventions:
# Lists: R uses 1-based indexing
list_obj <- doc[["items"]]
first_item <- am_get(doc, list_obj, 1) # First element
# Text operations: 0-based positions (same as JavaScript)
text_obj <- doc[["content"]]
am_text_splice(text_obj, 0, 0, "Start") # Position 0 = before first charTesting Cross-Platform Interoperability
All examples in this vignette can be tested using the executable
scripts provided in inst/js/:
Automated Testing
Run all examples automatically:
This will execute all JavaScript scripts, verify results in R, and demonstrate complete round-trip interoperability.
Manual Testing
See the documentation in inst/js/README.md (or after
installation, use
system.file("js/README.md", package = "automerge")) for
detailed instructions on running individual examples and integrating
with your own tests.
Available Scripts
The following scripts are available in inst/js/:
-
create-shared-doc.js- Create documents in JavaScript -
verify-r-changes.js- Verify R modifications from JavaScript -
concurrent-edit.js- Make concurrent edits in JavaScript -
verify-merge.js- Verify CRDT merge from JavaScript
To find these scripts after installation:
system.file("js", package = "automerge")Further Reading
- Automerge Website
- Binary Format Specification
- CRDT Research Papers
- Sync Protocol Vignette - Details on the sync protocol
- CRDT Concepts Vignette - Understanding CRDTs
- JavaScript Interoperability Scripts - See
system.file("js", package = "automerge")for executable examples