Skip to content

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.

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 is DirectConnection, which accepts an ordered list of pluggable transports.
  • Provider: Manages Yjs document synchronization, awareness, offline persistence, and RPC operations. It uses a Connection for network communication.

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.

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 synced
await provider.synced;
// Access the Yjs document
const ymap = provider.doc.getMap("data");
ymap.set("key", "value");
// Listen to connection state
provider.on("update", (state) => {
console.log("Connection state:", state.type);
});

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), DirectConnection periodically 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()],
});

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 (no fallback)
const connection = new DirectConnection({
url: "wss://example.com",
transports: [websocketTransport()],
});
// HTTP only
const connection = new DirectConnection({
url: "https://example.com",
transports: [httpTransport()],
});

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.

// Access the Y.js document
const ydoc = provider.doc;
// Create Y.js types
const ytext = ydoc.getText("content");
const ymap = ydoc.getMap("data");
const yarray = ydoc.getArray("items");
// Make changes
ytext.insert(0, "Hello, world!");
ymap.set("key", "value");
yarray.push([1, 2, 3]);
// Listen to document updates
ydoc.on("update", (update, origin) => {
console.log("Document updated");
// Changes are automatically synced to other clients
});

The provider includes an Awareness instance for user presence and cursor information:

// Access awareness
const awareness = provider.awareness;
// Set local state
awareness.setLocalStateField("user", {
name: "John Doe",
color: "#ff0000",
});
// Listen to awareness updates
awareness.on("update", ({ added, updated, removed }) => {
console.log("Users changed:", { added, updated, removed });
});

The provider supports Y.js sub-documents and properly lets the ydoc know that it is synced:

// Listen to subdocument events
provider.on("load-subdoc", ({ subdoc, provider: subdocProvider }) => {
console.log("Subdocument loaded:", subdoc.guid);
// subdocProvider is a Provider instance for the subdocument
});
// Access subdocuments
const subdocProvider = provider.subdocs.get("subdoc-guid");
if (subdocProvider) {
await subdocProvider.synced;
}

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 ready
await newProvider.synced;

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 available
await provider.loaded; // Resolves when local data is loaded
await provider.synced; // Resolves when synced with server

Monitor connection state:

// Get current connection state
const 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 transport
console.log("Active transport:", provider.connection.activeTransport);
console.log("Available transports:", provider.connection.availableTransports);
// Wait for connection
try {
await provider.synced;
console.log("Fully synced!");
} catch (error) {
console.error("Sync failed:", error);
}

Multiple providers can share the same connection:

import { DirectConnection, websocketTransport, httpTransport } from "teleportal/providers";
import { createEncryptionKey } from "teleportal/encryption-key";
// Create a connection
const connection = new DirectConnection({
url: "wss://example.com",
transports: [websocketTransport(), httpTransport()],
});
const key = createEncryptionKey();
// Create multiple providers with the same connection
const provider1 = new Provider({
connection,
document: "doc-1",
encryptionKey: key,
});
const provider2 = new Provider({
connection,
document: "doc-2",
encryptionKey: key,
});

The provider supports explicit resource management:

// Using explicit resource management
using provider = await Provider.create({
url: "wss://example.com",
document: "my-document",
encryptionKey: key,
});
// Provider automatically disposes when exiting scope
// Or manually destroy
provider.destroy({
destroyConnection: true, // default: true
destroyDoc: true, // default: true
});

The provider extends Observable and emits events:

// Subdocument events
provider.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 events
provider.on("peer-join", (peer) => {
console.log("Peer joined:", peer.userId);
});
provider.on("peer-leave", (peer) => {
console.log("Peer left:", peer.userId);
});