Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
^\.automerge$
^codecov\.yml$
^cran-comments\.md$
^srcjs$
41 changes: 40 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ devtools::install() # install locally

Single test file: `testthat::test_file("tests/testthat/test-server.R")`.

```bash
# Rebuild the React frontend bundle (after editing anything under srcjs/)
npm --prefix srcjs ci # first time / lockfile changes
npm --prefix srcjs run build # -> inst/www/amsync.{js,css}
```

`shinyreact` (Suggests; only for the interactive `sync_app()` / `$edit()` UI)
is installed from posit-dev/shinyreact (see `Remotes` in DESCRIPTION).

## Related packages

Interactive project browsing and live editing (`project_open()`, `project_app()`, `project_edit()`) live in the sibling `shinysync` package (`../shinysync`), which depends on autosync; autosync itself has no `shiny`/`bslib` dependency.
Expand All @@ -37,7 +46,11 @@ Interactive project browsing and live editing (`project_open()`, `project_app()`

**Auth (R/auth.R)**: Optional OAuth2 authentication via `auth_config()`. Validates Google OAuth2 tokens, supports email/domain allowlists and custom validators. TLS is mandatory when auth is enabled. Uses `later::later()` for auth timeout enforcement. `sync_token()` obtains an ID token interactively by delegating the Authorization Code + PKCE flow to httr2 (`oauth_server_metadata()` for discovery, `oauth_flow_auth_code()` for the browser handshake and token exchange).

**Client (R/client.R)**: `sync_fetch()` implements the client-side protocol for fetching documents from any automerge-repo server.
**Client (R/client.R)**: `sync_fetch()` implements the client-side protocol for fetching documents from any automerge-repo server. `sync_client()` opens a persistent connection whose `$open_doc()` returns `autosync_doc` handles sharing one socket; a handle's `$edit()` opens a live editor.

**Project (R/project.R)**: `sync_project()` browses a project document's `files` map (path -> file doc ID) over a single connection, opening files on demand.

**Interactive UI (R/app.R, R/edit.R)**: `sync_app()` is a single-window gadget (connect / browse / edit) and `$edit()` is the standalone live editor. Both render a **React frontend via shinyreact** (not bslib) and keep R as the sole owner of the Automerge documents — the browser is pure UI. `install_editor_sync()` (edit.R) wires the bidirectional editor<->document sync: an outgoing observer reads `input$content` and writes the minimal diff into the live doc; an incoming poll reflects remote changes back via a pluggable `set_editor()` callback (which bumps the `editor_doc` reactive_output revision the React CodeMirror editor watches).

**Storage (R/storage.R)**: Persistence layer using `.automerge` files in a configurable data directory.

Expand Down Expand Up @@ -68,6 +81,32 @@ Document IDs are Base58Check-encoded 16-byte random values. Peer IDs are Base64-
- `secretbase` - CBOR encoding (cborenc/cbordec) and Base58/Base64
- `later` - Event loop integration (run_now for async recv)

### JS frontend (`srcjs/` -> `inst/www/`)

The `sync_app()` / `$edit()` UI is a React app built with Vite.

- **Source**: `srcjs/src/` (TypeScript/TSX). `index.tsx` mounts `<App/>` into the
`#root` div it appends to `<body>` itself (`page_react()` serves a bare
page with no mount container). `App.tsx` routes on
`output$view`. Components: `ConnectScreen`, `BrowseScreen`, `FileTree` (the
`@pierre/trees` / trees.software file tree), `Editor` (CodeMirror 6), `Toast`
(notifications via `send_message`). `shiny.ts` is a typed facade over the
global `window.shinyreact` hooks; `languages.ts` maps file extensions to
CodeMirror language modes.
- **Build**: `npm --prefix srcjs run build` emits a self-contained IIFE to
`inst/www/amsync.js` + `amsync.css`. React/ReactDOM are **externalized** to
`window.shinyreact.{React,ReactDOM}` (vite.config.ts) so the bundle shares
shinyreact's single React 19 instance — never bundle a second React (it breaks
hooks). `@pierre/trees` and CodeMirror are bundled in.
- **Shipping**: the built `inst/www/*` is committed so the installed package
needs no Node; `srcjs/` is `.Rbuildignore`d. Rebuild and commit after editing
any `srcjs/` source.
- **R<->JS contract**: R reads `input$*` (url, proj_id, client_id/secret/issuer,
authenticate, connect, file, content, refresh, disconnect, exit, close) and
publishes `reactive_output`s (`view`, `init`, `authed`, `paths`, `selected`,
`editor_doc`). `paths` is emitted via `as.list()` so a length-1 vector still
serialises as a JSON array.

## Testing

Tests use port 0 (OS-assigned) by default, retrieving the actual URL via `server$url`. Test files cover server, client, handlers, storage, and integration scenarios.
Expand Down
4 changes: 4 additions & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ Imports:
secretbase (>= 1.3.0)
Suggests:
openssl,
shiny,
shinyreact (>= 0.1.0),
testthat (>= 3.0.0)
Remotes:
posit-dev/shinyreact
Config/Needs/website: tidyverse/tidytemplate
Config/roxygen2/markdown: TRUE
Config/roxygen2/version: 8.1.0
Expand Down
8 changes: 7 additions & 1 deletion NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@

S3method(print,autosync_client)
S3method(print,autosync_doc)
S3method(print,autosync_project)
S3method(print,autosync_server)
export(auth_config)
export(create_document)
export(generate_document_id)
export(get_document)
export(list_documents)
export(sync_app)
export(sync_client)
export(sync_fetch)
export(sync_project)
export(sync_server)
export(sync_token)
importFrom(automerge,
Expand All @@ -24,7 +27,9 @@ importFrom(automerge,
am_sync_encode,
am_sync_state,
am_sync_state_decode,
am_sync_state_encode
am_sync_state_encode,
am_text_content,
am_text_update
)
importFrom(httr2,
oauth_client,
Expand Down Expand Up @@ -66,4 +71,5 @@ importFrom(secretbase,
jsondec,
jsonenc
)
importFrom(tools,file_ext)
importFrom(utils,str)
Loading
Loading