Skip to content

test: delete#1122

Closed
janthurau wants to merge 33 commits into
mainfrom
claude/hocuspocus-rust-rewrite-q7xr1s
Closed

test: delete#1122
janthurau wants to merge 33 commits into
mainfrom
claude/hocuspocus-rust-rewrite-q7xr1s

Conversation

@janthurau

@janthurau janthurau commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

deleted

claude added 30 commits July 10, 2026 11:05
RFC 0001 lays out the architecture for rewriting the hocuspocus server
in Rust on top of yrs: per-document actor concurrency, a trait-based
extension system mirroring the 25 TS hooks, byte-compatible wire
protocol and Redis interop, a webhook contract for auth/persistence,
and a conformance strategy that reuses the existing TS test suite as
the product spec.

PORTING.md pins the explicit TS-to-Rust construct mapping, the hook
semantics table, and the byte-exact / semantic / allowed-to-differ
classification that gates every milestone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
Eight crates per RFC 0001:

- hocuspocus-protocol: lib0-compatible varint/varstring codec, message
  envelope with session-aware routing keys, auth submessages incl. the
  optional trailing providerVersion, bare ping/pong frames, close-code
  constants, and the Redis pub/sub frame codec - all with unit tests
  against hand-derived byte layouts from the TS implementation
- hocuspocus-core: the Extension trait (all 25 hooks, default no-ops),
  HookChain with priority ordering and SkipFurtherHooks/Abort
  semantics, Context (JSON + typed halves), Storage and Authenticator
  traits, Origin, and the document actor type skeletons
  (DocMessage/DocHandle/DocRegistry)
- hocuspocus-webhook: HMAC-SHA256 request signing byte-compatible with
  @hocuspocus/extension-webhook
- hocuspocus-storage: Storage trait impl surface with InMemoryStorage
- hocuspocus-axum, hocuspocus-redis, hocuspocus-metrics: config and
  constant surfaces for M2-M5
- hocuspocus-server: binary that loads TOML+env config (defaults
  mirror the TS defaultConfiguration), binds, prints a machine-readable
  ready line for the conformance harness, serves / and /healthz, and
  shuts down gracefully on SIGTERM/SIGINT

cargo build/test/clippy -D warnings/fmt all green; 31 unit tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
Path-gated to crates/** and the Cargo manifests so JS-only PRs are
unaffected; reuses the Redis service-container pattern from build.yml
for the multi-node interop tests landing in M4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
A Node script that drives the real TypeScript implementations
(OutgoingMessage, @hocuspocus/common auth encoding, lib0, y-protocols,
yjs) to produce the committed fixture corpus consumed by the Rust
protocol crate's conformance tests: every message type, session-aware
addresses, auth frames with and without the trailing providerVersion,
real yjs sync steps and state vectors, awareness updates including
null-state removals, and the Redis pub/sub frame layout.

Adds y-protocols as a tests-workspace dev dependency.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
… (M1)

- SyncMessage codec: y-protocols sync subtypes (Step1/Step2/Update)
  over opaque yjs update bytes
- AwarenessUpdate codec: structured clientId/clock/state-JSON entries,
  enabling before_handle_awareness hooks to mutate states without the
  scratch Y.Doc the Node server allocates per frame; allocation bounded
  against absurd entry counts
- Golden tests over the TS-generated fixture corpus: all 24 fixtures
  decode, semantic content matches, envelopes/awareness/redis frames
  re-encode byte-exact, and - the key compatibility check - a full
  update produced by real yjs applies cleanly to a yrs document, yields
  identical content, and round-trips through yrs re-encoding

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
Three targets covering attacker-controlled inputs: the full inbound
frame path (envelope + type-specific payload decoders), the Redis
pub/sub frame decoder, and the awareness update decoder with re-encode.
Corpus seeded from the golden-vector fixtures. Smoke runs: ~22M total
executions across the targets with zero crashes.

Run with: cargo +nightly fuzz run <target> from crates/hocuspocus-protocol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
Real @hocuspocus/provider clients now sync against the Rust server:

- hocuspocus-core engine: per-document actor tasks exclusively owning
  the yrs::Doc, replicating MessageReceiver.ts semantics - SyncStep1
  gets Step2 + the server's own Step1, applied updates broadcast to all
  subscribers and ack with SyncStatus, SyncReply suppresses the
  reciprocal first sync step (server-internal, never sent to clients)
- server-side awareness store wire-compatible with y-protocols clock
  rules, per-connection client-id tracking, and removal broadcast on
  disconnect - without the per-frame scratch Y.Doc the Node server
  allocates
- per-socket connection task: pre-auth queueing with the exact TS
  limits (5 MiB / 1000 messages / 100 pending documents), the
  non-refreshable pre-auth deadline, post-auth idle timeout (4408),
  PermissionDenied + 4401 on auth failure, per-document multiplexing
  over raw routing keys
- debounced persistence (debounce/maxDebounce semantics) through the
  Storage trait; documents unload when empty and reload from storage
- hocuspocus-axum: WebSocket transport pumping engine channels
- hocuspocus-server: WS upgrade on any path, control API
  (/control/stats, /control/close-connections), graceful shutdown that
  closes clients with 4205 and flushes pending stores

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
…erver

Covers the auth handshake, initial sync, bidirectional propagation,
SyncStatus acks (hasUnsyncedChanges), awareness propagation and
disconnect cleanup, late-joiner full sync, document isolation, the
control API, and unload/reload through storage. All checks pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
- HOCUSPOCUS_TEST_TARGET=rust switches newHocuspocus() to spawn the
  Rust server binary; the shim exposes the surface Tier-1 tests use
  (webSocketURL, closeConnections via the control API) and fails fast
  with a clear message for in-process hook options until the M3 webhook
  transport lands
- tests/conformance/rust-target.json is the skip-map: 6 applicable
  files today (21 tests, all passing), everything else annotated with
  the milestone that unlocks it
- pnpm test:rust prints the conformance dashboard; a new CI job builds
  the binary and runs it on every Rust or conformance change
- the server binary no longer rejects unrelated HOCUSPOCUS_* env vars
  (HOCUSPOCUS_TEST_TARGET leaked into the figment namespace)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
- WebhookAuthenticator POSTs signed auth events ({event, payload} with
  X-Hocuspocus-Signature-256) and maps 200 {context, scope} / 403
  {reason} to the auth decision
- server binary: [auth] mode = "none"|"webhook" + [webhook] url/secret
  config (env-mappable for the test harness)
- auth failure now matches TS exactly: PermissionDenied is sent and the
  socket STAYS OPEN with that document's state reset, so provider token
  retries work on the same connection (previously closed with 4401)
- test shim serves onAuthenticate closures through a per-test HTTP
  webhook receiver - the production contract doubles as the test
  transport; onAuthenticated/onAuthenticationFailed/retry files moved
  to the conformance run list

Dashboard: 9 files / 28 tests passing against the Rust binary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
onConnect, onDisconnect, onClose, onAwarenessChange, onAwarenessUpdate
and onMessage use provider-side callbacks only (no server hooks) and
pass against the Rust binary unmodified. Re-annotate the remaining
skips with the precise blocking feature (webhook load/store, server
onStateless hook).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
…read-only acks

- WebhookStorage: GET/PUT {base}/documents/{name} binary endpoints
  (signed), selected via [storage] backend = "webhook"
- EventHooks trait: connect (awaited before auth, may reject) and
  disconnect notifications; WebhookEvents delivers them per the
  [webhook] events config
- read-only Step2 acks now match TS snapshotContainsUpdate behavior:
  saved=true when the document's state vector covers the update
- config env nesting switched to double underscore
  (HOCUSPOCUS_SERVER__LISTEN) so multi-word fields are addressable;
  scalar test options (debounce, timeout, unloadImmediately) map to env
- test shim serves onLoadDocument/onStoreDocument/onConnect/onDisconnect
  closures through the per-test webhook receiver

Dashboard: 17 files / 61 tests passing against the Rust binary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
- stateless webhook event: the actor delivers client Stateless payloads
  to the EventHooks trait (awaited inline, preserving the per-document
  ordering of the TS message queue); WebhookEvents posts them per the
  [webhook] events config; test shim serves onStateless closures
- SqliteStorage: byte-compatible with @hocuspocus/extension-sqlite
  (identical documents(name, data) schema and upsert), so Node and Rust
  fleets can share one database file during migration
- PostgresStorage: same storage contract over bytea rows
- both behind cargo features (sqlx, rustls); [storage] backend =
  memory|webhook|sqlite|postgres in the binary config

Dashboard: 18 files / 62 tests - the ENTIRE tests/provider/ and
tests/providerwebsocket/ suites now pass against the Rust binary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
- Scaler trait in core: attach/detach per loaded document plus try-once
  store-lock coordination (lost lock = another instance persists,
  matching the SkipFurtherHooksError semantics)
- document actors gain a pub/sub relay: on attach they publish their
  first sync step and query peer awareness (the Node extension's
  afterLoadDocument bootstrap); peer Step1 gets Step2 + own Step1 as
  SyncReply; non-redis-origin changes publish the server's Step1
  (Node's onChange); awareness changes relay while local connections
  exist; redis-origin changes never schedule stores
- hocuspocus-redis: redis-rs RESP3 push connection with per-channel
  routing into actors, identifier self-echo filtering, per-document
  publish forwarders, SET NX PX + compare-and-delete Lua store lock on
  the same keys as the Node Redlock usage
- [redis] url/prefix/identifier config on the binary

Verified by a three-topology smoke suite against a real Redis: two
Rust instances, and - the migration path - a REAL Node
@hocuspocus/server with extension-redis sharing Redis with the Rust
binary in both directions. Edits, awareness, and late-joiner full
state all propagate cross-instance in all topologies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
- the shim maps Redis extension instances from test options onto the
  binary's [redis] env config, so the multi-server extension-redis AVA
  tests now run against Rust-Rust topologies unmodified
- server-side broadcast_stateless: engine API + control endpoint
  (/control/broadcast-stateless), relayed to peer instances unless the
  message itself arrived via the relay (no ping-pong)
- stateless webhook responses can carry a respond payload, delivered
  back to the originating connection - the webhook equivalent of the
  onStateless hook's connection.sendStateless()
- CI conformance job gains the Redis service container

Dashboard: 21 files / 68 tests passing (extension-redis onChange,
onAwarenessChange, onStateless included; onStoreDocument stays skipped
- two of its tests inject custom in-process TS extension classes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
The tests/server/unauthenticatedQueueLimit suite exposed three real
gaps, all fixed:

- authentication now runs on a spawned task per document (TS behavior:
  a slow or never-resolving onAuthenticate must not stall the frame
  loop), with an Authenticating state that queues frames under the
  same byte/message limits and counts toward maxPendingDocuments -
  including in-flight auths
- pre-auth limit violations close with ResetConnection 4205 (was 1009),
  matching the TS server
- the axum transport drains the socket (bounded, 3s) after an
  engine-initiated close while the client is mid-flood: dropping a
  socket with unread inbound data sends TCP RST, destroying the
  in-flight close frame (clients saw 1006 instead of the close code)
- maxUnauthenticatedQueueSize/Messages and maxPendingDocuments are now
  configurable on the binary and mapped from test options

Conformance run list is now strictly exit-code-verified: 23 files / 76
tests. tests/server files mixing wire tests with in-process-API tests
are annotated in the skip map pending per-test granularity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
- rust-target.json gains a 'partial' section: files mixing wire tests
  with in-process-API tests run in their own ava invocation with the
  in-process tests excluded via negative --match patterns
- getConnectionsCount()/getDocumentsCount() are now shimmed as sync
  getters backed by a 50ms control-API stats poller (tests read them
  inside retryable assertions, so eventual consistency suffices)
- stats 'connections' now reports the TS semantic - established
  document connections, counted by the actors - instead of open
  sockets (a socket with failed auth counts zero, which the
  auth-failure tests assert); sockets are reported separately
- shim passes providerVersion through to onAuthenticate payloads

Dashboard: 28 files / 97 tests passing (76 full + 21 partial),
strictly exit-code verified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
…n load failure

Two behavioral differences surfaced by tests/server/onStoreDocument
and onLoadDocument:

- a failed store no longer unloads the document: the actor stays
  loaded with the state re-marked dirty so the debounce timer retries,
  and unload only happens once the final store succeeds (TS keeps
  documents in memory when onStoreDocument throws - no data loss)
- a failed document load sends PermissionDenied and resets the
  per-document state with the socket kept open (TS setUpNewConnection
  catch path), instead of closing with 4403

Full dashboard (28 files / 97 tests) verified unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
- both Redis connections use ConnectionManager: exponential-backoff
  reconnects, and the pub/sub connection re-subscribes all document
  channels automatically after a reconnect - a silently dropped
  subscription would partition a document across the fleet
- the per-document publish forwarder retries briefly (5 x 500ms) when
  a publish races a reconnect, instead of silently dropping the frame
- topology smoke gains a restart-resilience section: Redis is
  SIGKILLed and restarted mid-session; edits made after the restart
  must still propagate between instances (verified passing), with
  bounded teardown so a wedged Node extension can't mask results

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
ObjectStoreStorage works over any object_store backend (S3, GCS,
Azure, MinIO, filesystem, in-memory) with the exact key layout of
@hocuspocus/extension-s3 ({prefix}{documentName}.bin, full yjs update
v1 per object), so Node and Rust fleets can share one bucket during
migration. Verified by an in-memory roundtrip test asserting the
Node-compatible object key.

Binary config: [storage] backend = "s3" with bucket/region/endpoint/
prefix; MinIO-style endpoints get path-style addressing and http is
allowed for local endpoints. Credentials come from the standard AWS
environment variables.

Full conformance dashboard verified green after the change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
The actor fires a change event on the debounced store cadence carrying
the incremental yjs update (base64) since the previous event - apps
apply it to their own Y.Doc copy (the documented transformer-sidecar
pattern replaces the in-process Tiptap transformer). Redis-origin
changes never fire on the receiving instance. Enabled via [webhook]
events = "change"; verified by a committed smoke that decodes the
payload and asserts the document content.

This completes the TS webhook extension's event surface
(auth/connect/disconnect/change/stateless) on the Rust binary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
Measures the failure mode that motivated the rewrite: edit propagation
on a small document while loader processes hammer an 8M-char document
with fresh initial syncs (each a full-state encode server-side).

Results (16-core container, release build): Node's probe latency
degrades 16x at p50 (2ms -> 33ms; p99 116ms) because the single thread
couples every document to the busiest one. The Rust server's p50 is
UNCHANGED under load (1ms; p99 12ms) while serving 76% more initial
syncs (648 vs 368 in 12s). Recorded in the RFC.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
Text exposition of the engine gauges: established document connections
(TS getConnectionsCount semantic), open sockets, and loaded documents.
Message/store counters follow with the metrics-facade instrumentation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
The connection context established at auth time now flows everywhere the
TS server delivers it:

- Storage::fetch/store carry the triggering connection's context;
  WebhookStorage sends it as an X-Hocuspocus-Context JSON header on the
  GET/PUT persistence requests (TS onLoadDocument/onStoreDocument
  `context` payload field).
- EventHooks::connect may now contribute context: the connect webhook's
  `{"context": {...}}` response merges into the connection context under
  the auth response's keys (TS order: onConnect first, onAuthenticate on
  top). disconnect/change/stateless event payloads carry the context of
  the connection that caused them.
- The document actor tracks the last change's context for store/change
  events; DocMessage::Join and the socket's Active state carry the
  connection context.

Conformance shim: the per-test webhook receiver now runs unconditionally
(auth, storage, and connect/disconnect/stateless events always routed
through it), which also makes `server.configure({...})` after
construction work — hook closures merge into the live dispatch. Two
previously excluded context tests now run and pass:

- onAuthenticate.ts "passes context from onAuthenticate to onLoadDocument"
- onDisconnect.ts "passes the context to the onLoadDocument callback"

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
When a test fails inside a bare timer (the hasUnsyncedChanges pattern),
the ava worker dies from the uncaught exception WITHOUT running
teardowns; any ref'd handle the shim holds then keeps the worker's event
loop alive and the whole batch hangs after its summary instead of
reporting the failure. Unref everything the shim owns: the webhook
receiver and its sockets, the spawned binary's process watch, and its
stdout pipe (nothing is read after the ready line).

Also scope the auth and persistence webhook wiring back to spawn-time
hooks: the extra webhook hop on every connection/document load added
enough latency under full-batch load to flake timing-sensitive tests.
Lifecycle events (connect/disconnect/stateless) stay always-on — that is
all `configure()`-added hooks need.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
The always-on connect event added a second sequential webhook round trip
through the (batch-saturated) test process's event loop before every
authentication, which deterministically blew the suite's tightest fixed
deadline — hasUnsyncedChanges' 200 ms readonly test — in two consecutive
full-dashboard runs. Enable connect/disconnect/stateless events only when
the test passed lifecycle hooks up front, or when the server was created
bare (the configure()-after-construction pattern that relies on them).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
…tics

Server fixes surfaced by triaging the two remaining tests/server files
into the conformance run:

- unloadImmediately=false now defers the final store to its natural
  debounce deadline after the last disconnect (TS lets the debouncer run;
  only unloadImmediately=true flushes at once).
- Failed document loads propagate an explicit denial reason: a new typed
  `Refused` error carries the webhook 403 body's `reason` into the
  client's PermissionDenied message (TS `error.reason ??
  "permission-denied"`); other errors stay generic.

Conformance shim robustness (root cause of the batch hangs found):
- The spawned binary no longer inherits the ava worker's stderr pipe —
  an orphaned server (worker crashed before teardowns) held that pipe
  open and ava's main process waited on its EOF forever. Server logs are
  opt-in via HOCUSPOCUS_RUST_STDERR=inherit.
- The stats poller uses plain node:http with agent:false instead of
  fetch(), whose global undici pool keeps ref'd keep-alive sockets alive.
- onLoadDocument return values are only adopted when they are a Y.Doc
  (TS ignores anything else, e.g. `return true`).
- Denial responses carry `error.reason`, not Error#message.
- server.documents gains a poller-backed `size`.

tests/server/onLoadDocument.ts (7 wire tests) and onStoreDocument.ts
(10 wire tests) move from skip to partial — extension-instance and
in-process-API tests stay excluded via --match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
claude added 3 commits July 12, 2026 12:56
- New EngineStats (lock-free atomics on hocuspocus-core): inbound
  messages by type, outbound frames, store outcomes
  (success/failure/skipped-on-lock), auth denials; absorbs the existing
  document-connection gauge. Counted in the socket task, document actor,
  and the axum transport writer; embedders read Engine::stats().
- /metrics renders the counters alongside the gauges (verified
  end-to-end against a live provider session).
- rust.yml: nightly-scheduled ~1h fuzz soak over the three protocol
  decoders (20 min each), corpora seeded from the TS golden fixtures,
  crash artifacts uploaded; conformance job gains a 30-minute timeout.
- Conformance: lifecycle webhook events are now opt-in per file via
  rust-target.json `env` (HOCUSPOCUS_RUST_FORCE_EVENTS for
  onDisconnect.ts's server.configure tests) instead of on for every
  bare server — the connect-event round trip was inflating
  hasUnsyncedChanges' flake rate under batch load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
…ne, migration guide

- crates/npm/: @hocuspocus/server-rust launcher (bin shim resolving the
  platform binary, HOCUSPOCUS_RUST_BIN override, musl detection) plus six
  platform-binary packages wired as optionalDependencies (the esbuild
  distribution pattern). Verified end-to-end against a local build.
- crates/Dockerfile: multi-stage musl build on Alpine → minimal non-root
  runtime with /healthz healthcheck; linux/amd64 + linux/arm64.
- .github/workflows/rust-release.yml: on server-rust-v* tags — six-platform
  binary matrix, packed GitHub release assets, npm publish (platform
  packages then launcher, versions stamped from the tag), multi-arch GHCR
  image via buildx.
- crates/MIGRATION.md: Node→Rust config mapping, hooks→webhook contract
  table, extension mapping, zero-downtime mixed-fleet rollout via the
  interoperable Redis integration, transformer sidecar example, ops notes.
- hocuspocus-webhook: reqwest gains rustls-tls — https webhook URLs
  previously had no TLS backend at all and failed at runtime.
- RFC milestone table updated: M5/M6 done.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
Mounts the real HocuspocusProviderWebsocketComponent + HocuspocusRoom in
jsdom, asserts the room syncs, that an independent plain provider observes
a change made through the React-managed document, and that unmounting
closes the server-side connection. provider-react previously had no test
coverage against either server implementation.

Adds jsdom/react/react-dom/tsx as tests devDependencies (react pinned to
the instance provider-react resolves, to avoid a duplicate-React tree).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuJw4JrwBzqKtW3ZBuRdkP
@janthurau janthurau closed this Jul 12, 2026
@janthurau
janthurau deleted the claude/hocuspocus-rust-rewrite-q7xr1s branch July 12, 2026 13:25
@janthurau janthurau changed the title Hocuspocus server rewrite in Rust with full wire protocol compatibility deleted Jul 12, 2026
@janthurau janthurau changed the title deleted test: delete Jul 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants