@open-ot/client
v0.3.0
Published
The client-side state machine for OpenOT, handling optimistic updates, operation buffering, and server synchronization.
Readme
@open-ot/client
The client-side state machine for OpenOT, handling optimistic updates, operation buffering, and server synchronization.
Overview
@open-ot/client implements the standard OT client state machine, ensuring your UI remains responsive even on slow or unreliable networks. It manages three distinct states to handle the complexity of real-time collaboration:
- Synchronized: Client is up-to-date with the server.
- AwaitingConfirm: Waiting for the server to acknowledge a sent operation.
- AwaitingWithBuffer: User continues editing while waiting for acknowledgment. New edits are buffered and composed.
Installation
npm install @open-ot/client @open-ot/coreQuick Start
import { OTClient } from '@open-ot/client';
import { TextType } from '@open-ot/core';
const client = new OTClient({
type: TextType,
initialSnapshot: "Hello World",
initialRevision: 0,
});
// User types " Alice"
client.applyLocal([
{ r: 5 }, // Retain "Hello"
{ i: " Alice" }, // Insert " Alice"
{ r: 6 } // Retain " World"
]);
console.log(client.getSnapshot());
// => "Hello Alice World"How It Works
The State Machine
The client operates as a finite state machine to handle the asynchronous nature of network communication:
┌─────────────┐
│ Synchronized │ ◄──── Initial state
└──────┬──────┘
│ User edits
│ (send to server)
▼
┌────────────────┐
│ AwaitingConfirm │ ◄──── Waiting for server ACK
└────────┬───────┘
│ User continues editing
│ (buffer locally)
▼
┌──────────────────────┐
│ AwaitingWithBuffer │ ◄──── Composing buffered ops
└──────────────────────┘Optimistic UI Updates
When a user makes a change, the client:
- Immediately applies the operation to the local snapshot (optimistic update).
- Sends the operation to the server.
- Transitions to
AwaitingConfirmstate.
This ensures the UI never locks up waiting for the server.
Operation Buffering
If the user continues typing while waiting for the server:
- New operations are buffered locally.
- The client composes consecutive operations into a single efficient operation.
- Once the server acknowledges the pending operation, the buffered operation is sent.
This minimizes network traffic and reduces serverless invocation costs.
Handling Remote Operations
When a remote operation arrives from the server:
- The client transforms its pending/buffered operations against the remote operation.
- The remote operation is applied to the local snapshot.
- The client remains in sync with the server.
API Reference
OTClient<Snapshot, Op>
Constructor
new OTClient(options: OTClientOptions<Snapshot, Op>)Options:
interface OTClientOptions<Snapshot, Op> {
type: OTType<Snapshot, Op>;
initialRevision: number;
initialSnapshot: Snapshot;
transport?: TransportAdapter;
}type: The OT type (e.g.,TextType,JsonType).initialRevision: The starting revision number (usually0).initialSnapshot: The initial document state.transport(optional): A transport adapter for automatic server communication.
Methods
applyLocal(op: Op): Op | null
Apply a local operation generated by the user.
Returns:
- The operation to send to the server, or
nullif buffering.
Example:
const opToSend = client.applyLocal([{ i: "Hello" }]);
if (opToSend) {
// Send to server manually if not using a transport
}serverAck(): Op | null
Handle a server acknowledgment.
Returns:
- The next operation to send (if buffered), or
null.
Example:
// When server confirms receipt
const nextOp = client.serverAck();
if (nextOp) {
// Send the buffered operation
}applyRemote(op: Op): Op
Apply a remote operation from the server.
Returns:
- The transformed operation that was applied to the local snapshot.
Example:
// When receiving an operation from another user
const transformedOp = client.applyRemote(remoteOp);
// Update UI with transformedOp if neededgetSnapshot(): Snapshot
Get the current document state.
const currentState = client.getSnapshot();getRevision(): number
Get the current revision number.
const rev = client.getRevision();Using with a Transport
The client can automatically handle server communication when provided with a TransportAdapter:
import { OTClient } from '@open-ot/client';
import { WebSocketTransport } from '@open-ot/transport-websocket';
import { TextType } from '@open-ot/core';
const transport = new WebSocketTransport('ws://localhost:3000');
const client = new OTClient({
type: TextType,
initialSnapshot: "",
initialRevision: 0,
transport: transport, // Automatic sync!
});
// Just apply local changes, transport handles the rest
client.applyLocal([{ i: "Hello" }]);Manual Transport Integration
If you're building a custom transport, implement the message protocol:
interface MessageProtocol<Op> {
type: 'op' | 'ack';
op?: Op;
revision?: number;
}Client → Server (operation):
{
"type": "op",
"op": [{ "i": "Hello" }],
"revision": 5
}Server → Client (acknowledgment):
{
"type": "ack"
}Server → Client (remote operation):
{
"type": "op",
"op": [{ "i": "World" }]
}Advanced: Offline-First Applications
The client works seamlessly offline. Operations are queued locally and automatically synced when the connection returns:
// User edits while offline
client.applyLocal(op1);
client.applyLocal(op2);
client.applyLocal(op3);
// Operations are composed: op1 ∘ op2 ∘ op3
// When connection returns, transport sends the composed operationState Transitions
| Current State | Event | Next State | Action | |----------------------|--------------------|----------------------|---------------------------------| | Synchronized | User edits | AwaitingConfirm | Send operation to server | | AwaitingConfirm | User edits | AwaitingWithBuffer | Buffer operation locally | | AwaitingWithBuffer | User edits | AwaitingWithBuffer | Compose with buffered operation | | AwaitingConfirm | Server ACK | Synchronized | Clear pending operation | | AwaitingWithBuffer | Server ACK | AwaitingConfirm | Send buffered operation | | Any state | Remote operation | Same state | Transform and apply |
License
MIT
