Skip to content

@dualscreen/core

Framework-agnostic. No React dependency.

ts
import { createLink } from 'dualscreen'
ts
function createLink(options: LinkOptions): Link
ts
const link = createLink({ channel: 'my-app' })

link.set('selected', 'EXP-102')
link.subscribeKey('selected', (id) => render(id))

await link.whenReady()
link.close()

LinkOptions

TypeDefault
channelstringRequired. Namespace.
rolestringfrom ?ds=Surface this window renders.
transportTransportBroadcastChannelMessage channel.
metaRecord<string, unknown>Advertised in presence.
initialStateRecord<string, unknown>Defaults, applied after the handshake.
snapshotTimeoutnumber250Ms to wait for a snapshot before assuming we're alone.
debugbooleanfalseTrace the protocol.

Identity

idThis window's peer id.
roleSurface name. 'main' for the primary window.
channelThe channel namespace.
isLeaderWhether this window holds leadership.
isReadyWhether the handshake settled.
peersEvery connected window, oldest first.
othersPeers other than this one.
diagnostics{ transport, leaderStrategy, protocol, clock, sent, received, ready }

State

ts
link.get<T>(key): T | undefined
link.getAll(): Record<string, unknown>
link.set(key, value, options?: { ephemeral?: boolean }): void
link.subscribe((state, changedKeys) => void): () => void
link.subscribeKey<T>(key, (value) => void): () => void

Commands

ts
link.send(name, args?, options?: { to?: string }): void
link.command(name, (args, from) => void): () => void
ts
link.navigate(surface, to, options?: { replace?: boolean }): void

Emits a one-shot nav event. For durable routing that survives reloads, prefer useSurface().navigate() in React, which writes to shared state instead.

Events and lifecycle

ts
link.on('peers' | 'leader' | 'state' | 'nav' | 'ready', handler): () => void
link.tap((envelope, direction) => void): () => void
link.whenReady(): Promise<void>
link.close(): void

tap observes every envelope in and out — it's what the devtools overlay uses. It runs on the hot path, so keep handlers cheap. close() is idempotent and is called automatically on pagehide.

Transports

ts
createBroadcastChannelTransport(channel: string): Transport
createMemoryTransport(channel: string): Transport
resetMemoryTransports(): void
isBroadcastChannelSupported(): boolean

See Custom transports for writing your own.

Surface helpers

ts
readSurface(href?): string            // 'main' or the ?ds= value
isSecondarySurface(href?): boolean
surfaceUrl(name, { base?, route? }): string
SURFACE_PARAM   // 'ds'
MAIN_SURFACE    // 'main'

Lower-level building blocks

Exported for advanced use and for testing.

ts
createStateStore(selfId): StateStore
createLeaderElection({ name, peerId, onChange }): LeaderElection
createPresence({ channel, self, onChange, staleMs? }): Presence
isWebLocksSupported(): boolean

Utilities

ts
rafThrottle<T>(fn): ((value: T) => void) & { cancel(): void }
throttle<T>(fn, ms): ((value: T) => void) & { cancel(): void }
shallowEqual(a, b): boolean
uid(prefix?): string
Emitter<Events>

Types

Envelope, MessageType, Transport, PeerInfo, StateEntry, StateMap, LinkOptions, LinkEvents, TapHandler, TapDirection, HelloPayload, WelcomePayload, PatchPayload, NavPayload, CmdPayload, PROTOCOL_VERSION.

Released under the MIT License.