knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = FALSE # Set to FALSE since examples require Node.js and filesystem operations )
This vignette includes executable JavaScript and R code examples. To run them:
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.R
This script will:
@automerge/automerge)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 install
Run individual examples:
# Create a document in JavaScript node inst/js/create-shared-doc.js shared_doc.automerge # Then load in R Rscript -e 'doc <- automerge::am_load(readBin("shared_doc.automerge", "raw", 1e7)); print(doc)'
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) }
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.
Automerge uses a standardized binary format (see automerge.org/automerge-binary-format-spec) that is identical across all implementations. This means:
You'll need:
@automerge/automerge package (npm install)automerge package (this package)For these examples, we'll use file-based exchange with Node.js on the 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))
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" )
# 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")
// 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))
This example shows how to use the sync protocol for real-time synchronization between JavaScript and R.
# 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")
// 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') }
# 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") }
This demonstrates Automerge's CRDT capabilities with concurrent edits in both platforms.
# Create a shared document shared <- am_create() |> am_put(AM_ROOT, "document", "Shared Document") |> am_put(AM_ROOT, "sections", am_list()) |> am_commit("Initialize document") # Save for both platforms shared_bytes <- am_save(shared) writeBin(shared_bytes, "concurrent_doc.automerge")
// 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:
# From installed package JS_DIR=$(Rscript -e "cat(system.file('js', package='automerge'))") node $JS_DIR/concurrent-edit.js concurrent_doc.automerge js_concurrent.automerge # Or from source node inst/js/concurrent-edit.js concurrent_doc.automerge js_concurrent.automerge
# 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")
# 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")
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:
# From installed package JS_DIR=$(Rscript -e "cat(system.file('js', package='automerge'))") node $JS_DIR/verify-merge.js r_concurrent.automerge # Or from source node inst/js/verify-merge.js r_concurrent.automerge
Text objects are particularly interesting as they demonstrate character-level CRDT merge.
let textDoc = Automerge.change(Automerge.init(), doc => { doc.notes = new Automerge.Text('Hello from JavaScript') }) fs.writeFileSync('text_doc.automerge', Automerge.save(textDoc))
# 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")
// Load updated text document const updatedTextDoc = Automerge.load(fs.readFileSync('text_doc.automerge')) console.log('Text content:', updatedTextDoc.notes.toString()) // Output: "Hello from JavaScript and R!"
| 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.
The saved document format includes:
The binary format is deterministic and identical across platforms, enabling:
Both JavaScript and R use UTF-8 for strings. If you encounter encoding issues:
# Ensure UTF-8 encoding when reading from files doc <- am_load(readBin("doc.automerge", "raw", 1e7)) # Check string encoding str_value <- doc[["string_field"]] Encoding(str_value) # Should be "UTF-8"
When transferring files between systems, always use binary mode:
# Correct: binary transfer scp -B doc.automerge server:/path/ # Incorrect: text mode (can corrupt) # Don't use text mode transfer for .automerge files
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")
// JavaScript - specify actor ID let doc = Automerge.init({ actorId: "js-session-456" })
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 char
// JavaScript - lists use 0-based indexing const firstItem = doc.items[0] // First element // Text operations - also 0-based doc.content.insertAt(0, "Start")
All examples in this vignette can be tested using the executable scripts provided in inst/js/:
Run all examples automatically:
Rscript inst/js/run-examples.R
This will execute all JavaScript scripts, verify results in R, and demonstrate complete round-trip interoperability.
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.
The following scripts are available in inst/js/:
create-shared-doc.js - Create documents in JavaScriptverify-r-changes.js - Verify R modifications from JavaScriptconcurrent-edit.js - Make concurrent edits in JavaScriptverify-merge.js - Verify CRDT merge from JavaScriptTo find these scripts after installation:
system.file("js", package = "automerge")
system.file("js", package = "automerge") for executable examplesAny scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.