Provider
The Provider is the client-side API that manages Y.js document synchronization, awareness, offline persistence, and RPC operations. It wraps a Connection and handles the higher-level document synchronization protocol.
Overview
Section titled “Overview”The provider system is built on two main abstractions:
Connection: Manages the low-level network connection, handles reconnection logic, message buffering, transport selection, and connection state. The default implementation isDirectConnection, which accepts an ordered list of pluggable transports.Provider: Manages Yjs document synchronization, awareness, offline persistence, and RPC operations. It uses aConnectionfor network communication.
Why the Split?
Section titled “Why the Split?”The Connection and Provider are separate classes because they have fundamentally different lifecycles and responsibilities:
Connection handles network concerns: selecting a transport (WebSocket, HTTP, or custom), reconnecting with exponential backoff when the network drops, buffering and batching messages while disconnected, and tracking heartbeats. It knows nothing about Y.js documents.
Provider handles document concerns: running the Y.js sync protocol, managing awareness (cursor/presence) state, coordinating offline persistence with IndexedDB, and exposing RPC operations like milestones and attribution. It relies on a Connection for transport but doesn’t manage the connection itself.
This separation enables connection reuse – multiple Providers can share a single Connection, which is how Teleportal multiplexes many documents over one WebSocket. It also enables independent lifecycle management: you can switch documents (destroying one Provider and creating another) without tearing down and re-establishing the network connection. The switchDocument() method exploits this directly. And when using a SharedWorker, the Connection lives in the worker thread while Providers remain in the main thread, a split that would be impossible if they were a single object.
Basic Usage
Section titled “Basic Usage”import { Provider } from "teleportal/providers";import { createEncryptionKey } from "teleportal/encryption-key";
// Create a provider with automatic connection.// By default creates a DirectConnection with [websocketTransport(), httpTransport()]const provider = await Provider.create({ url: "wss://example.com", document: "my-document-id", encryptionKey: createEncryptionKey(),});
// Wait for document to be syncedawait provider.synced;
// Access the Yjs documentconst ymap = provider.doc.getMap("data");ymap.set("key", "value");
// Listen to connection stateprovider.on("update", (state) => { console.log("Connection state:", state.type);});Connection and Transports
Section titled “Connection and Transports”DirectConnection with Pluggable Transports
Section titled “DirectConnection with Pluggable Transports”DirectConnection is the single connection class. Instead of separate connection classes for WebSocket and HTTP, it takes an ordered array of ConnectionTransport instances and manages transport selection, fallback, and auto-upgrade internally:
import { DirectConnection, websocketTransport, httpTransport } from "teleportal/providers";
const connection = new DirectConnection({ url: "wss://example.com", transports: [websocketTransport(), httpTransport()],});How it works:
- Transport fallback: On connect, transports are tried in order. If WebSocket fails (e.g. blocked by a corporate firewall), the HTTP transport is tried automatically.
- Upgrade probing: When connected on a non-preferred transport (e.g. HTTP),
DirectConnectionperiodically probes the preferred transport (WebSocket). If the probe succeeds, it transparently upgrades back – no reconnection logic needed from the application. - Manual override: Call
connection.switchTransport("http")to force a specific transport and disable automatic upgrade probing.
Provider.create() uses this by default with [websocketTransport({ timeout: 5000 }), httpTransport()]:
// These are equivalent:const provider = await Provider.create({ url: "wss://example.com", document: "my-document", encryptionKey: key,});
const provider = await Provider.create({ url: "wss://example.com", document: "my-document", encryptionKey: key, transports: [websocketTransport({ timeout: 5000 }), httpTransport()],});Built-in Transport Factories
Section titled “Built-in Transport Factories”Two transport factories are provided:
websocketTransport(options?) – WebSocket-based transport. Supports probe() for upgrade detection.
import { websocketTransport } from "teleportal/providers";
websocketTransport({ timeout: 5000, // Connection timeout (ms) protocols: [], // WebSocket sub-protocols WebSocket: WebSocket, // WebSocket implementation override});httpTransport(options?) – HTTP/SSE-based transport. Uses Server-Sent Events for server-to-client messages and HTTP POST for client-to-server messages.
import { httpTransport } from "teleportal/providers";
httpTransport({ timeout: 10000, // SSE connection timeout (ms) fetch: fetch, // fetch implementation override EventSource: EventSource, // EventSource implementation override httpBatchingOptions: { maxBatchSize: 10, maxBatchDelay: 50 },});WebSocket-only or HTTP-only
Section titled “WebSocket-only or HTTP-only”// WebSocket only (no fallback)const connection = new DirectConnection({ url: "wss://example.com", transports: [websocketTransport()],});
// HTTP onlyconst connection = new DirectConnection({ url: "https://example.com", transports: [httpTransport()],});SharedWorker Connection
Section titled “SharedWorker Connection”Offloads the network connection to a SharedWorker so all open tabs share a single underlying transport. Falls back to a direct in-thread connection when SharedWorker is unavailable.
import { WorkerProvider } from "teleportal/providers/worker";import { websocketTransport, httpTransport } from "teleportal/providers";
const provider = await WorkerProvider.create({ workerUrl: new URL("./worker.ts", import.meta.url), url: "wss://example.com/sync", transports: [websocketTransport(), httpTransport()], document: "my-document", encryptionKey: key,});
await provider.synced;See the SharedWorker Connection guide for setup details, connection pooling, and configuration options.
Document Operations
Section titled “Document Operations”Accessing the Document
Section titled “Accessing the Document”// Access the Y.js documentconst ydoc = provider.doc;
// Create Y.js typesconst ytext = ydoc.getText("content");const ymap = ydoc.getMap("data");const yarray = ydoc.getArray("items");
// Make changesytext.insert(0, "Hello, world!");ymap.set("key", "value");yarray.push([1, 2, 3]);Listening to Updates
Section titled “Listening to Updates”// Listen to document updatesydoc.on("update", (update, origin) => { console.log("Document updated"); // Changes are automatically synced to other clients});Awareness
Section titled “Awareness”The provider includes an Awareness instance for user presence and cursor information:
// Access awarenessconst awareness = provider.awareness;
// Set local stateawareness.setLocalStateField("user", { name: "John Doe", color: "#ff0000",});
// Listen to awareness updatesawareness.on("update", ({ added, updated, removed }) => { console.log("Users changed:", { added, updated, removed });});Subdocuments
Section titled “Subdocuments”The provider supports Y.js sub-documents and properly lets the ydoc know that it is synced:
// Listen to subdocument eventsprovider.on("load-subdoc", ({ subdoc, provider: subdocProvider }) => { console.log("Subdocument loaded:", subdoc.guid); // subdocProvider is a Provider instance for the subdocument});
// Access subdocumentsconst subdocProvider = provider.subdocs.get("subdoc-guid");if (subdocProvider) { await subdocProvider.synced;}Document Switching
Section titled “Document Switching”Efficiently switch between documents while maintaining the same connection:
// Switch to a new document (reuses connection)const newProvider = provider.switchDocument({ document: "new-document-id", encryptionKey: key,});
// Old provider is destroyed, new provider is readyawait newProvider.synced;Offline Persistence
Section titled “Offline Persistence”The provider automatically enables offline persistence by default using IndexedDB:
const provider = await Provider.create({ url: "wss://example.com", document: "my-document-id", encryptionKey: createEncryptionKey(), enableOfflinePersistence: true, // default indexedDBPrefix: "my-app-", // custom prefix});
// Document will be loaded from IndexedDB if availableawait provider.loaded; // Resolves when local data is loadedawait provider.synced; // Resolves when synced with serverConnection State
Section titled “Connection State”Monitor connection state:
// Get current connection stateconst state = provider.state;
if (state.type === "connected") { console.log("Connected via:", state.transport);} else if (state.type === "errored") { console.error("Connection error:", state.error);}
// Check active transportconsole.log("Active transport:", provider.connection.activeTransport);console.log("Available transports:", provider.connection.availableTransports);
// Wait for connectiontry { await provider.synced; console.log("Fully synced!");} catch (error) { console.error("Sync failed:", error);}Connection Sharing
Section titled “Connection Sharing”Multiple providers can share the same connection:
import { DirectConnection, websocketTransport, httpTransport } from "teleportal/providers";import { createEncryptionKey } from "teleportal/encryption-key";
// Create a connectionconst connection = new DirectConnection({ url: "wss://example.com", transports: [websocketTransport(), httpTransport()],});
const key = createEncryptionKey();
// Create multiple providers with the same connectionconst provider1 = new Provider({ connection, document: "doc-1", encryptionKey: key,});
const provider2 = new Provider({ connection, document: "doc-2", encryptionKey: key,});Lifecycle Management
Section titled “Lifecycle Management”The provider supports explicit resource management:
// Using explicit resource managementusing provider = await Provider.create({ url: "wss://example.com", document: "my-document", encryptionKey: key,});
// Provider automatically disposes when exiting scope
// Or manually destroyprovider.destroy({ destroyConnection: true, // default: true destroyDoc: true, // default: true});Events
Section titled “Events”The provider extends Observable and emits events:
// Subdocument eventsprovider.on("load-subdoc", ({ subdoc, provider }) => { console.log("Subdocument loaded");});
provider.on("unload-subdoc", ({ subdoc, provider }) => { console.log("Subdocument unloaded");});
// Connection state events (delegated from connection)provider.on("update", (state) => { console.log("Connection state changed:", state.type);});
// Peer presence eventsprovider.on("peer-join", (peer) => { console.log("Peer joined:", peer.userId);});
provider.on("peer-leave", (peer) => { console.log("Peer left:", peer.userId);});Next Steps
Section titled “Next Steps”- Server - Learn how the server handles provider connections
- Transport - Understand the transport layer
- SharedWorker Connection - Offload connections to a SharedWorker
- Milestones - Learn about document versioning