{"id":27936592,"url":"https://github.com/reearth/ygo","last_synced_at":"2026-05-26T07:02:11.817Z","repository":{"id":285831411,"uuid":"959140047","full_name":"reearth/ygo","owner":"reearth","description":"A pure-Go implementation of the Yjs CRDT framework. Binary-compatible with yjs@13.x.","archived":false,"fork":false,"pushed_at":"2026-05-25T10:05:32.000Z","size":1607,"stargazers_count":5,"open_issues_count":19,"forks_count":3,"subscribers_count":3,"default_branch":"main","last_synced_at":"2026-05-25T10:29:05.172Z","etag":null,"topics":["collaborative-editing","crdt","golang","realtime","websocket","yjs"],"latest_commit_sha":null,"homepage":"https://pkg.go.dev/github.com/reearth/ygo","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/reearth.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-04-02T10:30:37.000Z","updated_at":"2026-05-25T09:58:51.000Z","dependencies_parsed_at":null,"dependency_job_id":"4de08fd2-db47-42cc-8fd9-428ae09fea65","html_url":"https://github.com/reearth/ygo","commit_stats":null,"previous_names":["reearth/ygo"],"tags_count":27,"template":false,"template_full_name":null,"purl":"pkg:github/reearth/ygo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reearth%2Fygo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reearth%2Fygo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reearth%2Fygo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reearth%2Fygo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/reearth","download_url":"https://codeload.github.com/reearth/ygo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reearth%2Fygo/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33508317,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T03:12:49.672Z","status":"ssl_error","status_checked_at":"2026-05-26T03:12:47.976Z","response_time":63,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["collaborative-editing","crdt","golang","realtime","websocket","yjs"],"created_at":"2025-05-07T06:58:26.743Z","updated_at":"2026-05-26T07:02:11.804Z","avatar_url":"https://github.com/reearth.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ygo\n\n[![CI](https://github.com/reearth/ygo/actions/workflows/ci.yml/badge.svg)](https://github.com/reearth/ygo/actions/workflows/ci.yml)\n[![Go Reference](https://pkg.go.dev/badge/github.com/reearth/ygo.svg)](https://pkg.go.dev/github.com/reearth/ygo)\n[![Go Report Card](https://goreportcard.com/badge/github.com/reearth/ygo)](https://goreportcard.com/report/github.com/reearth/ygo)\n[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n\n**ygo** is a pure-Go implementation of the [Yjs](https://github.com/yjs/yjs) CRDT (Conflict-free Replicated Data Type) library, enabling real-time collaborative applications in Go backends without CGO or embedded runtimes.\n\nIt is **binary-compatible** with the JavaScript Yjs reference implementation — updates produced by ygo can be applied by Yjs clients, and vice versa.\n\n## API stability\n\nygo follows [semantic versioning](https://semver.org/). The v1.x public API is stable: new functionality lands as minor releases; bug fixes as patch releases; breaking changes are deferred to v2.\n\n## Capabilities\n\nygo is a pure-Go CRDT library that interoperates with Yjs (JavaScript) and yrs (Rust):\n\n- All Y-types: `YText`, `YArray`, `YMap`, `YXmlFragment`, `YXmlElement`, `YXmlText`\n- Both update wire formats (V1 and V2, with V1↔V2 conversion)\n- The y-protocols sync handshake and awareness layer\n- WebSocket and HTTP transport bindings (the core is transport-agnostic)\n- Snapshots, garbage collection, undo manager, persistence adapters\n\nThe current release is **v1.14.0**. See [CHANGELOG.md](CHANGELOG.md) for the per-release detail and [docs/HISTORY.md](docs/HISTORY.md) for the longer arc.\n\n## Features\n\n- **Pure Go** — no CGO, no V8, no embedded JavaScript engine.\n- **Binary-compatible** with Yjs JS and yrs. Updates round-trip across all three implementations.\n- **Full Y-type coverage** — `YText`, `YArray`, `YMap`, `YXmlFragment`, `YXmlElement`, `YXmlText`.\n- **Both update formats** — V1 and V2, with V1↔V2 conversion.\n- **Sync protocol** — implements [y-protocols](https://github.com/yjs/y-protocols) `SyncStep1`, `SyncStep2`, and incremental updates.\n- **Awareness** — presence, cursor sharing, ephemeral state.\n- **Snapshots** — point-in-time document history and restore.\n- **Transport-agnostic** — core logic has no transport dependency; WebSocket and HTTP handlers are addons.\n\nPost-v1.0 hardening:\n\n- **Panic-safe transactions** (v1.1.1). If `fn` inside `Transact` panics, the document lock is still released, observers fire with the partial state that was committed before the panic, and the original panic propagates to the caller. No rollback — that's by design, matching Yjs JS and yrs. For atomic batching, recover above `Transact` and reconcile via sync.\n- **Cooperative cancellation** (v1.1.2). `Doc.TransactContext` accepts a `context.Context` and exposes it inside `fn` via `txn.Ctx()`. `fn` can poll `txn.Ctx().Err()` to bail out early when the request context is cancelled.\n- **Error-returning variants** (v1.3.0, v1.6.0, v1.7.0). Sibling methods that surface errors instead of panicking or silently succeeding: `TransactE`, `TransactContextE`, `Awareness.SetLocalStateContext`, `Awareness.ApplyUpdateContext`, `UndoManager.UndoContext`, `UndoManager.RedoContext`, `Encoder.WriteVarIntE`. All additive; original methods unchanged.\n- **Out-of-order delta convergence** (v1.2.0). When an update references an item whose dependency hasn't arrived yet, the item is parked in a per-doc pending queue and integrated automatically when the missing predecessor arrives. Mirrors `pendingStructs` in Yjs JS and `Store.pending` in yrs.\n- **WebSocket hardening** (v1.4.0). Structured logging via `*slog.Logger`, per-message size cap (`MaxMessageBytes`), bounded per-peer broadcast queue with disconnect-on-overflow (`PeerWriteQueueSize`), and the bounded broadcast pattern itself — replacing the previous goroutine-per-broadcast fan-out.\n- **Operational observability** (v1.5.0). `Doc.PendingStats()` returns counts of items parked waiting for dependencies — useful when monitoring convergence in production.\n- **Semaphore-backed hard caps** (v1.5.0). `MaxConnections` and `MaxPeersPerRoom` are now hard guarantees, not optimistic atomic counters with race windows.\n- **`crypto/rand` ClientID** (v1.5.0). Predictable IDs in multi-tenant deployments are a footgun; the default `ClientID` is now cryptographically random. `crdt.NewClientID()` is exposed for callers who want to generate IDs externally.\n- **Context-aware persistence** (v1.7.0). Adapters can opt into `PersistenceAdapterContext` to receive a context cancelled when `Server.Shutdown` begins, letting them abort in-flight DB calls instead of blocking shutdown.\n- **Security hardening** (v1.8.0–v1.8.1). Pending-items queue cap (`Server.MaxPendingItems`), WebSocket handshake read deadline (`Server.HandshakeTimeout`), CSWSH documentation for `AllowedOrigins`, and a per-room awareness state cap (`Server.MaxAwarenessBytesPerRoom` plus `Awareness.SetMaxBytes`).\n- **lib0 wire-format parity** (v1.8.0, v1.10.0). Float byte-order fixed to big-endian (contributed by @zombiek731), lib0 `Any` tag 122 (BigInt) support, integer dispatch by magnitude matching lib0, lossless float64→float32 narrowing, strict UTF-8 in `ReadVarString` (`ErrInvalidUTF8`), and acceptance of Go's full numeric tower in `WriteAny`.\n- **Cross-reference audit** (v1.9.0–v1.14.0). A systematic comparison of ygo against Yjs JS and yrs reference implementations surfaced ten correctness gaps, tracked under the [`gaps` label](https://github.com/reearth/ygo/issues?label=gaps). Notable fixes: YATA `OriginRight` boundary (#65, #68), awareness self-state protection (#73), `Item.delete` cascade into nested types + `DeleteSet` partial-overlap split (#72), YText format-marker correctness (#71: bleed, accumulation, gap cleanup, current-attribute inheritance), `YText.InsertEmbed` (#76), and `YArray/YMap.ToJSON` recursive unwrap of nested types (#75). See [`gaps` label](https://github.com/reearth/ygo/issues?label=gaps) for the full list.\n- **Sync read-loop resilience** (v1.9.0). `sync.WithErrorHandler` option lets `ApplySyncMessage` route a single malformed update to a caller-supplied handler rather than tearing down the connection.\n- **Awareness heartbeat** (v1.11.0). `Awareness.Heartbeat()` re-emits local state at an incremented clock so peers learn we're still alive without the local state needing to change. Pairs with `StartAutoExpiry` on the peer side.\n\nSee [CHANGELOG.md](CHANGELOG.md) for the full per-release picture.\n\n## Requirements\n\n- Go 1.23 or later\n\n## Installation\n\n```bash\ngo get github.com/reearth/ygo\n```\n\n## Quick Start\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"github.com/reearth/ygo/crdt\"\n)\n\nfunc main() {\n    // Create two peers\n    alice := crdt.New()\n    bob := crdt.New()\n\n    // Obtain the shared type before entering a transaction —\n    // GetText and Transact both acquire the document mutex.\n    text := alice.GetText(\"content\")\n\n    // Alice makes edits\n    alice.Transact(func(txn *crdt.Transaction) {\n        text.Insert(txn, 0, \"Hello, world!\", nil)\n    })\n\n    // Encode Alice's state and send to Bob\n    update := alice.EncodeStateAsUpdate()\n\n    // Bob applies the update — both docs now converge\n    if err := crdt.ApplyUpdateV1(bob, update, nil); err != nil {\n        panic(err)\n    }\n\n    fmt.Println(bob.GetText(\"content\").ToString()) // \"Hello, world!\"\n}\n```\n\n## Examples\n\nThe [`examples/`](examples/) directory contains four runnable programs with detailed inline comments:\n\n| Example | What it shows |\n|---------|---------------|\n| [`examples/peer-sync`](examples/peer-sync/) | In-process two-peer sync via the y-protocols handshake — no network needed |\n| [`examples/http-sync`](examples/http-sync/) | Pull/push sync over HTTP with incremental state-vector diffs |\n| [`examples/collab-editor`](examples/collab-editor/) | Real-time multi-tab collaborative editor with a browser client |\n| [`examples/snapshot-history`](examples/snapshot-history/) | Document versioning — capture, store, and restore past states |\n\nRun any example from the repository root:\n\n```bash\ngo run ./examples/peer-sync\ngo run ./examples/http-sync\ngo run ./examples/snapshot-history\ngo run ./examples/collab-editor/server   # then open http://localhost:8080\n```\n\n**New users**: start with `peer-sync` for the smallest end-to-end demonstration of two docs converging in-process. Jump to `collab-editor` when you want to wire the WebSocket server to a real browser client.\n\n## WebSocket Server\n\n```go\npackage main\n\nimport (\n    \"net/http\"\n    \"github.com/reearth/ygo/provider/websocket\"\n)\n\nfunc main() {\n    server := websocket.NewServer()\n    http.Handle(\"/yjs/{room}\", server)\n    http.ListenAndServe(\":8080\", nil)\n}\n```\n\n## Server-side document injection\n\nBackend services — AI agents, HTTP handlers, content pipelines — can push\nchanges into a live room without simulating a WebSocket peer. Three APIs\nare available on `*websocket.Server`.\n\n### `BroadcastUpdate(ctx, room, update)`\n\nFans a pre-encoded V1 update out to all peers currently connected to a\nroom. Does **not** apply the update to the server's doc — callers who\nwant the server's state to reflect the broadcast must call\n`crdt.ApplyUpdateV1` first (or use `Apply` below).\n\n```go\ndoc := server.GetDoc(\"my-room\")\nif err := crdt.ApplyUpdateV1(doc, update, nil); err != nil {\n    return err\n}\nif err := server.BroadcastUpdate(ctx, \"my-room\", update); err != nil {\n    return err\n}\n```\n\n**Skipping `ApplyUpdateV1` creates divergence.** Live peers see the\nupdate, but peers joining afterwards receive the server's stale state\nvia sync step 2.\n\n### `Apply(ctx, room, fn)`\n\nApplies a callback to the doc and broadcasts the resulting delta atomically.\nAuto-creates the room if needed. Persistence runs via the existing\n`OnUpdate` hook — callers do not need to persist separately.\n\n```go\nerr := server.Apply(ctx, \"my-room\",\n    func(doc *crdt.Doc, transact func(func(*crdt.Transaction))) {\n        frag := doc.GetXmlFragment(\"content\") // OUTSIDE transact — see note\n        transact(func(txn *crdt.Transaction) {\n            elem := crdt.NewYXmlElement(\"p\")\n            frag.InsertElement(txn, 0, elem)\n        })\n    },\n)\n```\n\n**Important:** calls to `doc.GetXmlFragment`, `doc.GetText`, `doc.GetMap`,\nand the other root-type accessors must happen **outside** the `transact`\ncallback. These methods acquire the doc's write lock, which `transact`\nalready holds — calling them inside deadlocks.\n\n`fn` should be fast. It runs inside the doc's write lock and blocks all\npeer reads and writes to the room for the duration.\n\n**On `ErrUpdateTooLarge`, the mutation sticks.** The size check runs\nafter `fn`'s transaction commits and after persistence has enqueued the\nupdate, so the server's doc reflects `fn`'s changes and the update IS\npersisted — but peers do NOT see it. Size-bound `fn`'s effects\nexplicitly or reconcile peers via a sync step 1/2 exchange.\n\n### `CloseRoom(name, force)`\n\nExplicit teardown for rooms created by `Apply` that never accumulated\npeer connections. Without `CloseRoom`, such rooms linger until process\nexit.\n\n```go\nif err := server.CloseRoom(\"my-room\", false); err != nil { /* ... */ }\n// force=true closes connected peers first.\n```\n\n### Access control: `Server.OnInject`\n\nAn optional hook gates all server-side writes:\n\n```go\nserver.OnInject = func(ctx context.Context, info websocket.InjectInfo) error {\n    tenant, _ := ctx.Value(tenantKey{}).(string)\n    if !allowed(tenant, info.Room) {\n        return fmt.Errorf(\"tenant %q may not write to %q\", tenant, info.Room)\n    }\n    if info.Op == websocket.OpBroadcastUpdate \u0026\u0026 info.UpdateSize \u003e 1\u003c\u003c20 {\n        return errors.New(\"update too large for this tenant\")\n    }\n    return nil\n}\n```\n\n`info.Op` is `OpBroadcastUpdate` or `OpApply`. `info.UpdateSize` is the\nlength of the update bytes for `BroadcastUpdate`; zero for `Apply` (the\ndelta has not yet been produced — size capping for `Apply` is handled\nby `MaxUpdateBytes`, post-hoc).\n\nRefusals are returned wrapped with `ErrInjectRefused`, so callers can\nmatch either the sentinel or the hook's own error via `errors.Is`.\n\n### Resource caps\n\n- `Server.MaxUpdateBytes` — per-update size cap, default 64 MiB (matches\n  the peer frame limit).\n- `Server.MaxRooms` — total-room cap applied uniformly to peer upgrades\n  (HTTP 503) and `Apply` (`ErrTooManyRooms`). Default unlimited.\n\n### Trust model\n\n`Server.Apply` and `Server.BroadcastUpdate` grant total write authority\non the document. Treat the `*Server` handle with the same care as a\ndatabase connection — do not expose it directly to untrusted code.\n`OnInject` is defense-in-depth, not a substitute for caller-side\nauthorization. A caller who can reach either API can craft updates that\nspoof any client ID, which is equivalent to the authority already\ngranted by `GetDoc` + `ApplyUpdateV1`.\n\n## Persistence\n\nThe WebSocket server takes an optional `PersistenceAdapter` so room state survives restarts:\n\n```go\ntype PersistenceAdapter interface {\n    LoadDoc(room string) ([]byte, error)\n    StoreUpdate(room string, update []byte) error\n}\n```\n\n`LoadDoc` is called once when the first peer connects to a room; the result seeds the in-memory doc. `StoreUpdate` is called on every committed transaction. Writes run on a per-room worker goroutine — slow storage doesn't block peers. Wire an adapter in via `NewServerWithPersistence(adapter)`.\n\nFor backend examples (Postgres, Redis, file-system) and the v1.7.0 context-aware extension that lets adapters abort in-flight writes during `Server.Shutdown`, see [docs/PERSISTENCE.md](docs/PERSISTENCE.md).\n\n## Running in production\n\nThe library ships several operational hooks. See package godoc for the full reference; here's the short version of what to wire up.\n\n### Logging\n\n`Server.Logger *slog.Logger` defaults to `slog.Default()`. Surfaces slow-peer write failures, sync-dispatch errors, and awareness apply errors at `Warn` level with `room` and `peer` context.\n\n```go\nserver := websocket.NewServer()\nserver.Logger = slog.New(slog.NewJSONHandler(os.Stdout, nil))\n```\n\n### Observability\n\n`Doc.PendingStats()` returns a snapshot of the per-doc pending queue: how many items are parked, how many delete-set ranges are queued, which clients we're blocked on. Cheap (one read-lock). Useful when monitoring out-of-order delivery in production.\n\n```go\nstats := doc.PendingStats()\nmetrics.PendingItems.Set(float64(stats.Items))\nmetrics.PendingDeleteRanges.Set(float64(stats.DeleteRanges))\n```\n\n### Resource limits\n\nAll hard-capped (semaphore-backed for connection counts):\n\n- `Server.MaxConnections` — server-wide cap on simultaneous WebSocket peers.\n- `Server.MaxPeersPerRoom` — per-room cap.\n- `Server.MaxRooms` — total-room cap (applies to peer upgrades and `Server.Apply`).\n- `Server.MaxUpdateBytes` — per-update size cap (default 64 MiB).\n- `Server.MaxMessageBytes` — per-message size on the WebSocket read path (default 64 MiB).\n- `Server.PeerWriteQueueSize` — per-peer broadcast queue depth (default 256). When the queue fills, the peer is disconnected.\n- `Server.MaxPendingItems` — per-document cap on items parked in the out-of-order pending queue (default 100,000). When the cap is reached, updates that would park additional items return `ErrInvalidUpdate`. Defends against a crafted update full of far-future-clock items that would otherwise grow the queue unboundedly. Same cap is available at the doc level via `crdt.WithMaxPendingItems(n)`.\n- `Server.HandshakeTimeout` — first-read deadline applied after WebSocket upgrade (default 30s). Closes connections that complete the handshake but never send a message (slow-loris defense). Cleared after the first successful read.\n- `Server.MaxAwarenessBytesPerRoom` — cap on the cumulative byte size of awareness state held in one room across all remote clients (default unlimited; suggested production value: 100 MiB). Without this cap, a single peer can claim up to 10,000 clientIDs each holding the 1 MiB per-state maximum. Forwarded to each room's `Awareness` via `awareness.Awareness.SetMaxBytes`.\n\nEach defaults to a sensible value or unlimited where noted.\n\n### Auth\n\n`Server.AuthFunc func(*http.Request) bool` runs before the WebSocket upgrade. Return false to reject:\n\n```go\nserver.AuthFunc = func(r *http.Request) bool {\n    return validateBearer(r.Header.Get(\"Authorization\"))\n}\n```\n\n### Graceful shutdown\n\n`Server.Shutdown(ctx)` drains pending writes and closes peers. Adapters that implement `PersistenceAdapterContext` (v1.7.0) receive a context derived from the shutdown signal so they can abort in-flight DB calls instead of waiting for the driver's timeout.\n\n## Performance\n\n### Running the benchmarks\n\n```bash\n# Run all benchmarks with memory allocation stats\ngo test ./... -run='^$' -bench='^Benchmark' -benchmem\n\n# Run a specific package only\ngo test ./crdt/ -run='^$' -bench='^Benchmark' -benchmem\n\n# Run with more iterations for tighter confidence intervals\ngo test ./... -run='^$' -bench='^Benchmark' -benchmem -benchtime=5s -count=3\n```\n\nTo compare two branches (e.g. before and after an optimization), install [`benchstat`](https://pkg.go.dev/golang.org/x/perf/cmd/benchstat):\n\n```bash\ngo install golang.org/x/perf/cmd/benchstat@latest\n\n# Capture baseline\ngit checkout main\ngo test ./... -run='^$' -bench='^Benchmark' -benchmem -count=5 | tee old.txt\n\n# Capture candidate\ngit checkout my-branch\ngo test ./... -run='^$' -bench='^Benchmark' -benchmem -count=5 | tee new.txt\n\n# Compare\nbenchstat old.txt new.txt\n```\n\nThe CI benchmark workflow (`.github/workflows/benchmark.yml`) runs this comparison automatically on every pull request.\n\n### Reference numbers\n\nMeasured on Apple M4 Max (arm64, Go 1.23). Your numbers will vary by hardware.\n\n**Encoding (`encoding/`)** — the codec runs on every item; these are sub-10 ns, zero-alloc:\n\n| Benchmark | ns/op | Allocs |\n|-----------|-------|--------|\n| ReadVarUint (1 byte) | 1.0 | 0 |\n| WriteVarUint (1 byte) | 1.7 | 0 |\n| WriteVarString (1000 chars) | 15 | 0 |\n| ReadVarString (1000 chars) | 89 | 1 (string copy) |\n| Encoder reuse (`Reset`) vs new | 7.7 vs 12.4 | 0 vs 1 |\n\n**CRDT core (`crdt/`)** — realistic document operations:\n\n| Benchmark | ns/op | Notes |\n|-----------|-------|-------|\n| `YText_InsertBulk` (1000 chars) | 2 006 | Single transaction — fast path |\n| `YText_Insert` (1000 × 1 char) | 344 048 | ~344 ns per keystroke |\n| `YText_Delete` (1000 × 1 char) | 891 456 | ~891 ns per delete |\n| `EncodeStateAsUpdateV1` (1000 items) | 21 360 | ~21 µs to serialise a document |\n| `ApplyUpdateV1` (1000 items) | 109 806 | ~110 µs to integrate a full state |\n| `EncodeStateAsUpdateV2` | 33 029 | V2 is ~1.5× larger to encode… |\n| `ApplyUpdateV2` | 679 207 | …and ~6× slower to decode |\n| `TwoPeerConvergence` | 16 284 | Encode + apply incremental sync |\n| `YMap_Set` (100 keys) | 19 557 | |\n| `YArray_Push` (100 elements) | 59 209 | |\n\n**Sync protocol (`sync/`)** — message framing overhead is negligible:\n\n| Benchmark | ns/op |\n|-----------|-------|\n| `EncodeSyncStep1` | 179 |\n| `ApplySyncMessage_Step1` | 631 |\n| `ApplySyncMessage_Update` (1000-item doc) | 1 404 |\n| `FullHandshake` | 1 303 |\n\n**Awareness (`awareness/`)** — per-peer ephemeral state:\n\n| Benchmark | ns/op |\n|-----------|-------|\n| `SetLocalState` | 65 |\n| `EncodeUpdate` (1 client) | 226 |\n| `EncodeUpdate` (50 clients) | 12 901 |\n| `ApplyUpdate` (50 clients) | 19 801 |\n\n## Architecture\n\nSee [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for a detailed explanation of the CRDT algorithm, data model, and package design.\n\n## Compatibility\n\nygo targets compatibility with:\n\n- [Yjs](https://github.com/yjs/yjs) v13.x (JavaScript reference implementation)\n- [y-protocols](https://github.com/yjs/y-protocols) sync and awareness protocol\n- [lib0](https://github.com/dmonad/lib0) binary encoding format\n\nCompatibility is verified by golden-file tests that compare binary output byte-for-byte with Yjs-generated fixtures.\n\nFor a Go-vs-Rust port comparison, see [docs/comparison/ygo-vs-yrs.md](docs/comparison/ygo-vs-yrs.md).\n\n## Gotchas\n\n### No read methods or observer registration inside `Transact`\n\n`Transact` acquires the document **write lock** for the duration of its callback.\nCalling any of the read methods (`Get`, `ToSlice`, `Keys`, `Entries`, `ToString`,\n`ToDelta`) or registering/unregistering observers (`Observe`, `ObserveDeep`) from\n**inside** a `Transact` callback will **deadlock** because those operations try to\nacquire the same lock.\n\n```go\n// ✗ WRONG — deadlocks\ndoc.Transact(func(txn *crdt.Transaction) {\n    arr.Get(0)         // tries to RLock — deadlock\n    arr.Observe(fn)    // tries to Lock  — deadlock\n})\n\n// ✓ CORRECT — acquire references and register observers before Transact\narr.Observe(func(e crdt.YArrayEvent) { /* ... */ })\ndoc.Transact(func(txn *crdt.Transaction) {\n    arr.Push(txn, []any{\"value\"})\n})\nfmt.Println(arr.ToSlice()) // read after Transact returns\n```\n\nThis constraint applies to `YArray`, `YText`, `YMap`, `YXmlFragment`, and\n`YXmlElement`. UndoManager callbacks (`OnStackItemAdded`) also run outside\nthe lock and are safe to use normally.\n\n### `Doc.ClientID` is read-only after creation\n\nUse `crdt.WithClientID(id)` at construction time. Changing the ID after the\ndocument has started accepting operations will corrupt the item store.\n\n## What's changed since v1.0\n\nEighteen minor and patch releases between v1.1.0 and v1.14.0. The early arc (v1.1.x–v1.7.x) focused on production hardening: panic safety, out-of-order convergence, WebSocket hooks, observability, error-returning variants, context-aware persistence. The recent arc (v1.8.x–v1.14.x) delivered a systematic cross-reference audit against Yjs JS and yrs, closing correctness gaps in YATA boundary handling, awareness protocol, delete-path cascade, lib0 wire-format parity, YText format markers, and JSON serialisation of nested shared types — tracked under the [`gaps` label](https://github.com/reearth/ygo/issues?label=gaps). See [CHANGELOG.md](CHANGELOG.md) for the per-release detail and [docs/HISTORY.md](docs/HISTORY.md) for the design narrative.\n\n## Contributing\n\nContributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) before submitting a pull request.\n\nFor significant changes, open an issue first to discuss what you'd like to change.\n\n## Security\n\nygo's security model is **defense-in-depth**, not authentication:\n\n- **`ClientID` is collision-avoidance, not authentication.** The protocol does not validate that incoming updates match a peer's declared `ClientID`. Use `Server.AuthFunc` and/or transport-level auth.\n- **Transport security is the embedder's responsibility.** ygo does not enforce TLS, signed updates, or peer authentication. Wrap the WebSocket server behind your usual reverse proxy.\n- **`Server.Apply` and `Server.BroadcastUpdate` grant total write authority.** Treat the `*Server` handle like a database connection. `OnInject` is defense-in-depth, not a substitute for caller-side authorization.\n- **`ClientID` generation uses `crypto/rand`** (v1.5.0). 32-bit space matches Yjs JS for wire compatibility; collision probability at multi-tenant scale is documented in [SECURITY.md](SECURITY.md).\n\nPlease report vulnerabilities by following the process in [SECURITY.md](SECURITY.md). Do not open public issues for security problems.\n\n## License\n\nMIT License — see [LICENSE](LICENSE).\n\nThis project is not affiliated with the Yjs authors. Yjs is developed by [Kevin Jahns](https://github.com/dmonad) and contributors.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freearth%2Fygo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Freearth%2Fygo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freearth%2Fygo/lists"}