API Reference¶
This reference lists the Python surface exposed by transports.
Session¶
- class transports.Session(*, log_cap: int = 512, log_bytes: int = 67108864, blob_threshold: int = 65536, command_cap: int = 1024)[source]¶
Bases:
object- reject_reason: str | None¶
why the last submit was rejected (None after a successful submit) — the model’s validation message where available, so callers can put it on the wire (the reject frame)
- host(model: Any, *, list_keys: list[dict] | None = None, owner: str = 'server') int[source]¶
Host and watch a model. Optional list_keys records define exact list and item-key paths.
- on_patch(fn: Callable[[int, dict], None]) None[source]¶
Register a callback invoked as fn(model_id, patch) for each emitted patch.
- flush() list[tuple[int, dict]][source]¶
Diff every dirty model against the core and emit the minimal patches. Returns them.
- update(mid: int) list[tuple[int, dict]][source]¶
Force a diff+emit for one hosted model and flush.
Automatic emission needs bigbrother to observe the model; models without a
__dict__(msgspec.Structand other__slots__types) can’t be watched, so mutate them and then callupdate(id)explicitly.
- snapshot(mid: int) dict[source]¶
{“type_name”:.., “rev”:.., “value”:..} for a hosted model (flushes pending changes).
- apply_patch(mid: int, patch: dict) bool[source]¶
Apply an authoritative remote patch to a hosted/mirrored model (adopts the patch’s rev).
Also refreshes the hosted Python object so the caller’s own reference stays in sync — the update is made under observation suppression so it doesn’t echo back as a new patch.
- submit(mid: int, patch: dict) dict | None[source]¶
Apply a client-proposed patch as the server: the server owns rev.
The proposal’s ops are applied to the hosted value, the server’s rev is bumped (not the client’s guess), the hosted Python object is refreshed, and the authoritative patch (with the server rev) is returned to broadcast to every connection. None if the id is unknown or the proposal is rejected — then reject_reason says why.
- command(mid: int, field: str, value: Any) int[source]¶
Queue an ordered command for the field’s owner, including before any client connects.
Delivery is at least once across ownership handover. The owner acknowledges after applying the command; commands remain queued until then. A full queue raises instead of dropping work.
- since(mid: int, since_rev: int) list[dict] | None[source]¶
Patches to advance a mirror from since_rev to current, or None if the log can’t bridge the gap (the next needed patch was evicted) — then the caller should send a fresh snapshot instead.
Model bridge¶
List identity policy¶
Session.host(model, list_keys=...) and Hub.share(model, list_keys=...) accept a list of
{"path": [...], "key": [...]} records. path addresses a list from the model root;
key addresses its identity within each item. Segments are {"Key": "field"} or
{"Index": 0}. Empty paths address the root. Paths are exact; wildcards are not supported.
rules = [{"path": [{"Key": "rows"}], "key": [{"Key": "id"}]}]
model_id = session.host(model, list_keys=rules)
The low-level Python diff(old, new, list_keys=json_string) and
Store.host(type_name, value_json, list_keys=json_string) take JSON-encoded policy records.
JavaScript’s diff(old, new, listKeys) and Store.host(typeName, valueJson, listKeys)
take the same optional JSON string as their third argument.
Keys are unique strings or integers; string "1" and integer 1 are distinct.
Use strings for integer identifiers outside JavaScript’s safe integer range when mirroring to a browser.
Missing, duplicate, or unsupported item keys select the existing default list diff for that list.
Duplicate policy paths, unknown policy fields, and malformed paths raise an error.
The default policy remains unchanged when the argument is omitted.
Retained identities emit existing move/reorder operations, followed by field updates at their new
positions. Added and removed identities emit insert/remove operations. Policy metadata is host
configuration and is not included in snapshots or patches; configure it again when restoring a host.
Client proposals are still positional and server-authoritative, not concurrency-safe keyed intents.
list_keys cannot be combined with crdt_spec; CRDT identity remains governed by its schema.
- transports.to_value(model: Any, *, include: set[str] | None = None) Any[source]¶
Convert model data to a core Value, preserving JSON serializers and native bytes.
- transports.from_value(value: Any, cls: type[M]) M[source]¶
Reconstruct a model of type cls from a core Value.
Connections¶
- class transports.Server(session: Session, *, default_codec: str = 'json')[source]¶
Bases:
objectServes a Session to connected clients: sends a snapshot on connect, broadcasts patches, and relays a client’s patches to the other clients (a hub). Transport-agnostic — its methods return the messages to send; an adapter such as ws_endpoint performs the I/O.
Each connection has its own negotiated codec, so outbound messages are encoded per connection.
- open(conn: Any, codec: str | None = None, since: dict[int, int] | None = None, batch: bool = False, *, ownership: bool = True) list[str | bytes][source]¶
Register a connection; return the messages that bring it up to date.
Fresh connect (
since=None) → a snapshot per model. Resume (since={mid: last_rev}) → only the patches each model emitted afterlast_rev, falling back to a snapshot for any model whose replay log can’t bridge the gap. So a reconnecting client replays the delta, not the whole model.
- recv(conn: Any, data: str | bytes) dict[Any, list[str | bytes]][source]¶
Handle an inbound message (text or binary frame); returns messages to send, keyed by conn.
A client patch is a proposal: the server applies it, bumps its own authoritative rev, and echoes the resulting patch to every connection (including the origin), each in that connection’s codec. Models are server-authoritative — a client’s mirror updates when this echo arrives, not optimistically. Pending host patches are returned before the proposal reply.
- flush() dict[Any, list[str | bytes]][source]¶
Drain the session and return the patch messages to broadcast, encoded once per codec.
Encoding depends only on the codec, so a broadcast to N same-codec connections shares one encoded copy instead of re-encoding per connection — the fan-out cost is O(messages x distinct codecs), not O(messages x connections).
- class transports.Client(codec: str = 'json', *, update_interval: float = 0.25, ownership: bool = True)[source]¶
Bases:
objectMirrors a remote Session — applies snapshot/patch messages to a local copy of each model.
Read values with value(id) or materialize them with model(id, cls). Drive it with a live connection via connect(url), attach an application-owned duplex sender, or feed it messages directly with recv(data). The codec (“json”, “msgpack”, or “cbor”) frames outbound edits and decodes inbound frames.
- property connected: bool¶
Whether a managed duplex connection is open right now.
- async send(frame: str | bytes) bool[source]¶
Send a frame over the active managed duplex connection.
Returns
Truewhen handed to an open connection,Falsewhen none is active (never connected, in a reconnect gap, or receive-only connect_sse) — the frame is dropped, like a browser WebSocket’s send on a closed socket, so an adapter can passclient.sendas a fire-and-forget callback; check connected (or the return) when it matters.
- async propose(mid: int, new_value: Any, proposal: str | None = None) bool[source]¶
Propose an edit over the active connection:
send(edit(mid, new_value)).Server-authoritative — the mirror updates when the authoritative patch echoes back (or on_reject fires with why it was refused). Returns
False(dropped) when not connected.
- async propose_ops(mid: int, ops: list[dict], proposal: str | None = None) bool[source]¶
Propose explicit patch operations over the active connection.
- async propose_crdt(mid: int, mutations: list[dict]) bool[source]¶
Apply local CRDT mutations immediately and retain them until an authoritative echo.
- async set_awareness(mid: int, state: Any | None, *, scope: str | None = None) bool[source]¶
Publish ephemeral state for one model, or clear it with
None.The latest non-null state is retained locally and republished when a managed connection opens. A named
scopepublishes independently of unscoped state and other scopes; None withdraws only the selected scope. Awareness requires a shared Hub model.
- on_change(callback: Callable[[dict], None]) Callable[[], None][source]¶
Register a callback fired after each accepted snapshot or patch — the same change dict recv returns — so connect/run/connect_sse consumers get path-level changes without managing the socket themselves. Not fired for ignored frames (stale revision, unknown message type). Returns an unsubscribe function.
- on_reject(callback: Callable[[dict], None]) Callable[[], None][source]¶
Register a callback fired when the server refuses a proposed edit — with the decoded
rejectmessage ({"t": "reject", "id", "rev", "error"}). The mirror itself reverts via the authoritative snapshot the server sends alongside. Returns an unsubscribe function.
- on_awareness(callback: Callable[[dict], None], *, scope: str | None = None) Callable[[], None][source]¶
Observe updates/removals for one scope (unscoped by default); unsubscribe does not withdraw state.
- on_ack(callback: Callable[[dict], None]) Callable[[], None][source]¶
Register a callback fired when the server accepts a tagged proposal.
The callback receives an authoritative patch or no-op acknowledgement, including its opaque
proposalidentifier. Returns an unsubscribe function.
- on_disconnect(callback: Callable[[], None]) Callable[[], None][source]¶
Register a callback fired when an active managed duplex connection disconnects.
- on_connect(callback: Callable[[], None]) Callable[[], None][source]¶
Register a callback fired when a managed duplex connection opens, including reconnects.
- on_abandon(callback: Callable[[list[str]], None]) Callable[[], None][source]¶
Register a callback fired with unsettled proposal identifiers on disconnect.
- async attach(sender: Callable[[str | bytes], Any]) None[source]¶
Attach an open duplex channel and flush queued CRDT operations and awareness.
Feed inbound frames to recv. When the channel closes, pass the same sender object to detach; sender identity prevents a late close from detaching a replacement channel. An existing channel is disconnected before its replacement is attached.
- detach(sender: Callable[[str | bytes], Any]) bool[source]¶
Detach sender if it is still active, returning whether the client disconnected.
- recv(data: str | bytes) dict | list[dict] | None[source]¶
Apply an inbound snapshot or patch message (text or binary frame) to the local mirror.
Returns the accepted change so reactive consumers can update only its paths —
{"t": "snapshot", "id", "rev"}for a snapshot, the decoded patch message for a patch — orNonefor a patch whose revision was already applied, anack, areject(dispatched to on_reject), and an unrecognized message type, which is ignored so a newer server can add message types without breaking older clients. A connection that negotiated?batch=1may receive batch frames, which apply in order and return the accepted changes as alist. Invalid frames raise without changing the mirror or its accepted revision.
- crdt_diff(mid: int, value: Any, path: list[str] | None = None) list[dict][source]¶
Plan value edits against the current CRDT replica without sending or mutating.
- crdt_spec(mid: int) CrdtSpec | None[source]¶
The canonical merge specification for a CRDT-backed model, else
None.
- type_name(mid: int) str | None[source]¶
Type from the last accepted snapshot, retained across reconnects.
- pending_proposals() list[str][source]¶
Identifiers for proposals that have not settled or been abandoned.
- pending_crdt_ops(mid: int | None = None) int[source]¶
Number of local CRDT operations awaiting an authoritative echo.
- awareness(mid: int, *, scope: str | None = None) dict[str, Any][source]¶
Snapshot remote ephemeral state keyed by server-assigned peer id.
- abandon_proposal(proposal: str) bool[source]¶
Stop tracking one proposal that the caller did not send.
Returns whether the proposal was pending. This does not call on_abandon; those callbacks report proposals abandoned by a managed connection closing.
- on_ownership(callback: Callable[[dict], None]) Callable[[], None][source]¶
Observe grants and handover; a disconnected client owns no fields.
- on_command(callback: Callable[[dict], None]) Callable[[], None][source]¶
Receive ordered commands, including commands received before registering this listener.
Apply the command and report resulting state with propose_ops(), then acknowledge with ack_command(). Values use the tagged wire representation. Delivery after handover may repeat.
- command_ack(command: dict) str | bytes[source]¶
Build an acknowledgement after applying a received command (manual transport API).
- async ack_command(command: dict) bool[source]¶
Acknowledge a completed command over the managed connection.
- async flush_owned() bool[source]¶
Flush coalesced status now; normally driven by update_interval (seconds).
- edit(mid: int, new_value: Any, proposal: str | None = None) str | bytes[source]¶
Propose an edit to a mirrored model; returns the patch frame to send (encoded in this codec).
Server-owned fields update on the authoritative echo. Client-owned fields update locally immediately; the server acknowledges their revision without replacing local state.
- edit_ops(mid: int, ops: list[dict], proposal: str | None = None) str | bytes[source]¶
Propose explicit patch operations, optionally under a caller-supplied identifier.
When
proposalis omitted, a client-local monotonic identifier is assigned. Use this form when a new optimistic value equals the current mirror and a whole-value diff would be empty while an older proposal is still pending.
- edit_crdt(mid: int, mutations: list[dict]) str | bytes[source]¶
Apply local CRDT mutations and return an idempotent operation frame.
Operations remain queued until the server echoes their causal dots. Reconnects restore the server snapshot, reapply queued operations locally, and resend them.
- async connect(url: str) None[source]¶
Connect to a transports server and mirror it until the connection closes (one connection).
Under Pyodide (
sys.platform == "emscripten") the browser gives Python no raw sockets, so this rides the browser’s nativeWebSocketthrough thejsFFI instead of thewebsocketslibrary — same API, same wire.
- async connect_data_channel(channel: Any) None[source]¶
Mirror over an externally negotiated browser
RTCDataChanneluntil it closes.Signaling and peer-connection ownership stay with the application. Under Pyodide the channel rides the browser’s
jsFFI; native Python deliberately has noaiortcdependency.
- async run(url: str, *, authority: str = 'server', retry: float = 1.0) None[source]¶
Connect and mirror, reconnecting whenever the connection drops — so the client survives a server restart or a network blip.
authoritydecides reconciliation on each (re)connect:"server"(default): the server is canonical; the client adopts its state (resuming from?since=when it can, else a fresh snapshot). This is the “refetch on refresh” behavior."client": the client is canonical; after the server’s snapshot it pushes its last-known state back as an edit, so a server that came back stale or empty is rectified from the client. With a CRDT model the push merges (newer stamps win); otherwise it overwrites.
Runs until cancelled. The choice of where the authoritative state lives is yours — pair this with the server-side durability hooks (Hub.on_shared_write) as your use case needs. Under Pyodide this rides the browser’s native
WebSocket(like connect), same semantics.
- async connect_sse(url: str) None[source]¶
Mirror a transports server over Server-Sent Events (receive-only) until the stream closes.
SSE is a one-way server→client channel, so this only receives snapshots and patches; use connect() (WebSocket) when the client also needs to send edits. Under Pyodide this rides the browser’s native
EventSource(no raw sockets for httpx).
- transports.ws_endpoint(server: Broadcaster)[source]¶
Build a Starlette WebSocket endpoint that serves server (a Server or Hub).
The connection’s codec is read from a
?codec=query param, falling back to the broadcaster’s default_codec. Wire it into an app, e.g.WebSocketRoute("/ws", ws_endpoint(server)), and run autosync(server) as a background task to stream server-side model changes to clients.
- transports.sse_endpoint(server: Broadcaster)[source]¶
Build a Starlette endpoint that streams a Server/Hub to a client over SSE.
Wire it into an app as a normal route (
Route("/sse", sse_endpoint(server))) and run autosync(server) as a background task to stream server-side model changes to connected clients.
- transports.serve_comm(server: Broadcaster, comm: Any, codec: str = 'json', *, ownership: bool = False) _CommConn[source]¶
Wire a comm to a Server/Hub: send the opening snapshots and relay inbound messages.
The comm is registered as a connection; its messages (
msg["content"]["data"]) are fed toserver.recvand any resulting messages are sent back over the relevant connections. Returns the connection handle. Call sync(server) after host-side mutations to push changes.
- transports.serve_anywidget(server: Broadcaster, widget: Any, codec: str = 'json', *, view_timeout: float = 180) _WidgetConn[source]¶
Wire a widget to a Server/Hub. Returns the connection handle; call sync(server) to push.
The widget’s custom messages drive the protocol:
{"ready": true}triggers the opening snapshots, and any{"wire": <wire>}is relayed toserver.recv(a client’s edits), with results sent back over the relevant widgets.
- async transports.autosync(server: Broadcaster, interval: float = 0.01, *, max_queue: int = 1024, max_queue_bytes: int = 16777216, max_interval: float = 0.25, shards: int = 32, stall_timeout: float = 0.5) None[source]¶
Background task: periodically flush and broadcast patches to all connections.
Run exactly one of these per Server/Hub (not per connection), so a single drain feeds every client. The async counterpart of sync — use this for socket backends (WebSocket/SSE) driven by an event loop, and sync for the synchronous ones (Jupyter comm/anywidget).
Each connection’s undelivered state patches live in a per-model map, and state coalesces: a newer revision is composed with that model’s undelivered patch into one frame, so no delta is lost while a slow consumer’s patch frame count stays bounded by its model count. CRDT operations, snapshots, batch envelopes, and direct replies do not coalesce;
max_queuebounds their count. Composed patches can still grow one frame, somax_queue_bytesalso bounds each connection’s queued encoded payload.Delivery runs on a fixed pool of
shardswriter tasks, each serially draining its share of connections — serial-loop economics (per-connection writer tasks were measured as a multiple-x CPU regression at 1000 connections: task-scheduling churn, and no drain-rate feedback). The flush loop itself never awaits a socket. Direct replies from ws_endpoint enter the same queue, preserving revision order without making the receive loop wait on every recipient. Two slow-consumer policies bound the pathological cases: after writers have had a drain cycle, a connection still holding more thanmax_queueundelivered messages or more thanmax_queue_bytesof encoded payload is disconnected. This lets one ready-to-send flush exceed either threshold without rejecting a healthy connection. A connection whose send makes no progress forstall_timeoutseconds (a wedged socket, which would stall its shard) is cut by a watchdog. A disconnected client’s reconnect resumes from its last revision viaopen(since=...).
- transports.sync(server: Broadcaster) None[source]¶
Drain host-side changes and deliver the patches over every connection, synchronously.
The manual counterpart of autosync, for backends driven by a synchronous loop (a Jupyter comm or anywidget): call it after mutating hosted models — e.g. at the end of a cell, or from a kernel timer. Each connection handle exposes send(wire).
Multi-tenancy and sharing¶
- transports.READ = 'read'¶
str(object=’’) -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to ‘strict’.
- transports.WRITE = 'write'¶
str(object=’’) -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to ‘strict’.
- class transports.Hub(key: Callable[[Any], Any], *, default_codec: str = 'json', tenant_idle_timeout: float | None = None)[source]¶
Bases:
objectRoute connections to per-tenant Session objects and fan shared data structures to subscribers.
Construct with key, a function mapping a connection handle to its tenant key. Register shared models with share() and connect tenants to them with subscribe(). Like Server, the methods return the messages to send keyed by connection; an adapter such as ws_endpoint(hub) performs I/O. Set tenant_idle_timeout (seconds, positive) to expire inactive tenants during flush()/autosync(); None retains them until explicitly retired.
- tenant(key: Any) Session[source]¶
Get or create a tenant’s private Session, first expiring it if its idle window elapsed.
- retire_tenant(key: Any) bool[source]¶
Remove an inactive tenant’s Session and subscriptions; return whether it existed.
Raises ValueError while any connection remains. Shared models and their CRDT replica ownership remain intact. Reusing a key creates a fresh Session with no subscriptions. After a retirement, private reconnects use snapshots because revision numbers alone cannot distinguish the old Session from a replacement.
- prune_tenants() list[Any][source]¶
Retire tenants whose configured idle timeout elapsed; return their keys.
flush()/autosync() also sweep. None disables expiration. Creation starts the initial idle window; the last connection closing starts a new one. Host-side access does not renew it.
Register a shared data structure; returns its shared id.
Pass a model instance (pydantic/dataclass/msgspec) to capture its value and type name, or a core Value dict together with type_name. merge is a MergeStrategy subclass (each shared model gets its own instance) or an instance to reuse.
replay=Truekeeps a bounded patch log so a joining worker can catch up by delta (seesince_shared()) instead of a full snapshot. To restore a model from a durable checkpoint on startup, passrevandmerge_state(from a priorsnapshot_shared()) alongside the saved value. Passcrdt_specto use the shared schema-directed reducer instead ofmerge;crdt_staterestores its reducer metadata andreplicaoverrides this hub’s unique operation identity.list_keysconfigures ordinary list diff identity using exact list and item-key paths; it cannot be combined withcrdt_specand must be configured again on host restore.
- subscribe(tenant_key: Any, sid: int, mode: str = 'read') None[source]¶
Subscribe a tenant to a shared model with READ or WRITE access.
- open(conn: Any, codec: str | None = None, since: dict[int, int] | None = None, batch: bool = False, *, ownership: bool = True) list[str | bytes][source]¶
Register a connection; return the messages to bring it up to date — its tenant’s private models and its subscribed shared models. With
since={mid: last_rev}a reconnecting client resumes its private models from the delta until the first tenant retirement; afterwards private reconnects use snapshots to avoid reusing a retired Session’s revisions. Shared models re-snapshot.
- recv(conn: Any, data: str | bytes) dict[Any, list[str | bytes]][source]¶
Handle an inbound patch; returns messages to send, keyed by connection.
A patch to a private model is applied as the server (the tenant’s session owns rev) and the authoritative patch is broadcast to all of that tenant’s connections. A patch to a shared model (from a WRITE subscriber) is merged into the authoritative value and broadcast to every subscriber connection. Both paths are server-authoritative (origin included), with pending host patches returned before the proposal reply.
- flush() dict[Any, list[str | bytes]][source]¶
Drain every tenant session and any host-side shared writes; route the patches per tenant/subscription.
Write to a shared model from the host side; the change is broadcast on the next sync/autosync.
Merge a shared-model write that happened on another worker (delivered over a backplane), and queue the resulting authoritative fan for this worker’s subscribers on the next flush. Do not re-publish — the originating worker already broadcast it. When the model’s MergeStrategy is a CRDT this is convergent: applying the same set of writes in any order yields the same value, so concurrent edits from clients on different workers reconcile identically everywhere.
Apply host-side CRDT mutations and queue their operations for subscriber fan-out.
Apply CRDT operations received from another worker and queue local subscriber fan-out.
Compact causally stable reducer metadata for a shared CRDT model.
Register a callback fired after each authoritative shared write, with
(sid, type_name, value, rev, change, merge_state). For CRDT-backed models,changecontainscrdt_opsandeffect, orcrdt_compactedfor a same-revision compaction, whilemerge_statecontains the CRDT specification and reducer state. transports stores nothing durably; persist these to survive a full-cluster restart, then restore withshare(value=…, rev=…, merge_state=…). Gate on a single writer (e.g. the relay’s leader) if you don’t want every worker persisting the same change.
The full transferable/persistable state of a shared model:
value,revand the merge clock (merge_state). Used by the relay to catch up a joining worker, and by users to checkpoint for durability.
Patches after since_rev for a delta catch-up, or
Noneif it is outside the kept log (the caller should fall back to a snapshot). Requiresshare(replay=True). Mirrors Session.since.
Adopt a peer’s snapshot of a shared model: set value/rev and restore the merge clock, so later merges respect the transferred causal stamps.
Catch up by applying replay patches onto the current (restored) value, then restoring the merge clock — cheaper than a snapshot when a recent checkpoint is held.
- class transports.MergeStrategy[source]¶
Bases:
objectHow a write to a shared model is reconciled into its authoritative value.
merge(current, patch, origin) returns the new core Value. Implementations may be stateful; pass the class (not an instance) to Hub.share(merge=…) so each shared model gets its own instance and its own state.
- class transports.LastWriteWins[source]¶
Bases:
MergeStrategyApply each write in arrival order (today’s Store semantics). Order-dependent.
- class transports.LwwMapCrdt[source]¶
Bases:
MergeStrategyConflict-free per-top-level-key last-writer-wins register map.
Each top-level map key carries a logical stamp (patch rev, origin); a key’s write is accepted only if its stamp is at least the stored one. Two consequences: concurrent edits to different keys both survive, and conflicting edits to the same key converge to the same value regardless of the order the hub happens to receive them in (the stamp is intrinsic to the write, not its arrival order). Nested or list ops fall back to a direct apply, stamped by their top-level key.
CRDT specifications¶
- class transports.CrdtSpec(root: RegisterPolicy | MapPolicy | SetPolicy | SequencePolicy, *, version: int | None = None)[source]¶
Bases:
objectValidated, canonical merge semantics for one model.
- classmethod from_dict(value: Mapping[str, object]) CrdtSpec[source]¶
Validate a complete serialized specification.
- property hash: str¶
SHA-256 of the deterministic serialized representation.
- class transports.CrdtDocument(spec: CrdtSpec, value: Any, replica: str)[source]¶
Bases:
objectOne schema-directed CRDT replica backed by the shared Rust reducer.
- classmethod from_state(spec: CrdtSpec, state: Mapping[str, Any], replica: str) CrdtDocument[source]¶
Restore a transferred reducer state under a new local replica identifier.
- property value: Any¶
Current ordinary Python value.
- property state: dict[str, Any]¶
Detached, JSON-compatible reducer state for transfer or persistence.
- mutate(mutations: Iterable[dict[str, Any]]) dict[str, Any][source]¶
Create and apply local operations, returning operations and materialized effects.
- apply(ops: Iterable[dict[str, Any]]) dict[str, Any][source]¶
Apply remote operations idempotently and return their materialized effect.
- diff(value: Any, path: list[str] | None = None) list[dict[str, Any]][source]¶
Plan edits without mutating this replica. Path selects map keys; set order is ignored.
RegisterPolicy, MapPolicy, SetPolicy, SequencePolicy, and their CrdtPolicy union describe
the accepted dictionary shapes for type checkers. Policy validation, canonical serialization, and
hashing run in the shared Rust core.
CrdtDocument accepts ordinary Python values. mutate() creates causally identified operations and
applies them locally; apply() accepts the same operations from another replica. Both return a
positional patch for existing model consumers and identity-preserving deltas for CRDT-aware
consumers. For sequences, a local sequence_splice mutation converts a positional range into the
stable-ID delete and insert operations used on the wire. Transfer state to another process with
from_state() when a patch history is not available.
After a CRDT snapshot, Client.crdt_spec(id) returns that model’s canonical specification. The
JavaScript spelling is client.crdtSpec(id). Plain and unknown models return None / undefined.
CrdtDocument.diff(value, path=None) returns mutation descriptors for replacing the materialized
value at path. JavaScript uses document.diff(value, path = []). The path contains map keys;
it cannot traverse set members or sequence indexes. Missing map ancestors are created according
to the specification. Planning does not change the replica, assign operation IDs, or send messages.
Maps produce per-key additions, removals, and nested edits. Sets compare canonical member keys: retained members receive nested edits, missing members are removed, and new members are added. Changing an identity is a removal plus an addition. Set order has no effect; duplicate identities are rejected. Sequences produce one prefix/suffix splice, with string offsets in Unicode scalars. Register values are replaced. Invalid paths or values raise an error without changing state.
Client.crdt_diff(id, value, path=None) / client.crdtDiff(id, value, path = []) plans against the
managed replica, including pending local operations. Unknown or non-CRDT model IDs raise an error.
The result can be passed to edit_crdt / editCrdt or propose_crdt / proposeCrdt immediately.
An unchanged value returns an empty list. Plans are relative to the observed state: applying one
after intervening edits can overwrite newer intent. A stale replacement value can also remove
observed keys or members absent from that value. Explicit mutations support intent that a final
value cannot represent, such as precise editor ranges or an intentional remove-and-readd.
Protocol helpers¶
- transports.protocol.normalize_codec(name: str | None) str[source]¶
Map a codec name or content-type to a canonical name (
JSON,MSGPACK,CBOR, or a registered custom content type).
- transports.protocol.crdt_snapshot_msg(model_id: int, type_name: str, rev: int, value: Any, spec: dict, state: dict) str[source]¶
- transports.protocol.crdt_msg(model_id: int, ops: list[dict], *, rev: int = 0, effect: dict | None = None, proposal: str | None = None) str[source]¶
- transports.protocol.encode(msg_json: str, codec: str = 'json') str | bytes[source]¶
Encode a JSON message string into the wire form for
codec.Returns the string unchanged for JSON, MessagePack
bytesfor the msgpack codec, or whatever a registered custom codec produces — so the caller sends a text or binary frame accordingly.
- transports.protocol.decode(data: str | bytes, codec: str | None = None) dict[source]¶
Parse an inbound frame to a message dict.
Pass the connection’s
codecto select the decoder (required for custom codecs). With nocodecthe built-ins are inferred from the frame type (str=JSON, bytes=msgpack).
The codec registry functions (encode_as, decode_as, register_codec,
unregister_codec, registered_codecs) are re-exported at the top level and listed under
Core below.
Core¶
- transports.diff(old, new, *, list_keys=None)¶
Diff two JSON-encoded models, returning the JSON-encoded patch.
- transports.apply(value, patch)¶
Apply a JSON-encoded patch to a JSON-encoded model, returning the JSON-encoded result.
- transports.encode(value)¶
Encode a JSON-encoded model to codec bytes.
- transports.decode(data)¶
Decode codec bytes back to a JSON-encoded model string.
- transports.encode_as(value_json: str, content_type: str) bytes[source]¶
Encode a model
Value(JSON string) to bytes with the named codec (built-in or registered).
- transports.decode_as(data: str | bytes, content_type: str) str[source]¶
Decode bytes back to a model
Value(JSON string) with the named codec (built-in or registered).
- transports.json_to_msgpack(json)¶
Convert an arbitrary JSON document to MessagePack bytes (for encoding whole protocol messages).
- transports.msgpack_to_json(data)¶
Convert MessagePack bytes back to a JSON document.
- transports.register_codec(content_type: str, encode: Callable[[Any], str | bytes], decode: Callable[[str | bytes], Any]) None[source]¶
Register a custom wire codec under
content_type.encodeturns a JSON-able object (a protocol message or a modelValue) into wire bytes (or a str);decodeis its inverse. Once registered,content_typeworks anywhere a codec name is accepted —Client(codec=content_type), a?codec=query param, orencode_as/decode_as. Register a matching implementation in every binding that needs it (the JS binding has its ownregisterCodec). The built-injson/msgpackcodecs cannot be overridden.
- transports.unregister_codec(content_type: str) None[source]¶
Remove a previously registered custom codec.
- transports.registered_codecs() tuple[str, ...][source]¶
The content types of the currently registered custom codecs.
- class transports.Store¶
Bases:
objectIn-process model store: host / mutate → patch / apply / snapshot.
- apply(id, patch_json)¶
Apply a JSON patch to a mirrored model; returns whether the id was known.
- field(id, name)¶
- host(type_name, value_json, *, list_keys=None)¶
Host a model from its JSON; returns the assigned id.
- mutate(id, value_json)¶
Replace a hosted model from JSON; returns the JSON patch (or None if id unknown).
- revision(id)¶
- snapshot(id)¶
{“type_name”:..,”rev”:..,”value”:..} for a hosted model, or None.
Client model metadata¶
Client.type_name(id) in Python and Client.typeName(id) in JavaScript return the type name from the
last accepted snapshot, or None / undefined for an unknown ID. Ordinary and CRDT snapshots update
this metadata through the shared Rust client state before change listeners run. Metadata survives
disconnect and delta-only resume. ids() lists mirrored models; several IDs can have the same type.
The accepted-change notification shape is unchanged.