@nocha/multiplayer-flow
v0.2.0
Published
Bounded ephemeral publication queues and timestamped multiplayer snapshot interpolation.
Readme
@nocha/multiplayer-flow
Extracted from the identical Canva Ultra and Canevas 2000 ephemeral queues. Create one flow per experience and share its publishers across features:
import { createMultiplayerFlow } from '@nocha/multiplayer-flow';
const flow = createMultiplayerFlow({ liveTransport: !import.meta.env.DEV });
const publisher = flow.createPublisher(send);
// Enable only after a connected event from the live transport.
flow.enableLiveTransport();
publisher.publish({ type: 'snapshot', position });
publisher.clear();Preserves the existing 18 sends/1050 ms shared window, 750 ms legacy spacing,
32-entry queue, replacement of queued snapshots, 450 ms action expiry,
2500 ms snapshot expiry and 1500 ms delay after an uncertain send. A failed
send is not replayed. clear() invalidates queued and not-yet-started sends;
an already running send cannot be cancelled. It does not reset the shared budget.
SDK calls, connection health, snapshot vocabulary, capability checks and billing consent remain the experience's responsibility. This is neither delivery acknowledgment nor authoritative multiplayer state. Importing has no DOM effect. The built npm package has no runtime dependencies or sibling imports.
Latest values and observable queues (0.2.0)
const poses = flow.createLatestPublisher(sendPose, {
key: pose => pose.entityId,
maxAgeMs: 500,
onOutcome: ({ outcome, error }) => recordPublication(outcome, error),
});
poses.publish({ entityId: 'avatar', x: 1, z: 2 });
poses.pause();
poses.resume();Keys are local to one publisher; all publishers in one flow share the budget.
createPublisher(send, { coalesceKey, maxAgeMs, onOutcome }) exposes the same
policy for mixed traffic. Omit the policy to retain the existing Canevas defaults.
An undefined key preserves an action instead of replacing it. Never coalesce
commands whose effects must be acknowledged individually.
Outcomes are sent, replaced, overflow, expired, cleared, or uncertain.
sent means only that the supplied send promise resolved, not that another player
received or applied anything. Observers run in a microtask; observer failures do
not interrupt the queue. Uncertain sends are never replayed. Pause clears pending
work and inhibits dispatch; publications made while paused remain bounded and
expire normally before dispatch on resume. Publish a fresh pose on reconnection.
An in-flight send cannot be cancelled. clear() does not change pause state.
Timestamped interpolation (0.2.0)
import { createSnapshotBuffer } from '@nocha/multiplayer-flow';
const positions = createSnapshotBuffer<{ x: number; z: number }>({
delayMs: 120,
capacity: 32,
interpolate: (previous, next, alpha) => ({
x: previous.x + (next.x - previous.x) * alpha,
z: previous.z + (next.z - previous.z) * alpha,
}),
});
positions.push({ sequence: 1, time: performance.now(), value: { x: 0, z: 0 } });
const displayed = positions.sample(performance.now());
positions.clear();Create one buffer per entity and sender incarnation. Sequences must be safe,
nonnegative integers, and both sequence and timestamp must strictly increase.
Duplicate, late or invalid samples are rejected (push returns false). Validate
the payload before insertion and do not mutate stored values. Clear on a known
scope/room/incarnation change; do not reset simply because a late sequence arrives.
The buffer does not authenticate senders or infer authority from their payload.
sample(now) displays now - delayMs, interpolating two surrounding snapshots.
The playback clock never moves backwards until clear. With too few snapshots,
or a gap, it holds the nearest pose. Optional extrapolate(previous, latest,
elapsedMs, intervalMs) receives elapsed time capped by maxExtrapolationMs
(default zero). Extrapolation stays frozen at the cap, not indefinitely advancing.
Capacity defaults to 32 and is constrained to 2–1024 snapshots.
All timestamps and sampling times must use the same clock. Monotonic local receipt times work without trusting a peer's wall clock, though transport bunching still affects that timeline. A server-timestamp timeline needs a separately estimated clock offset. A fixed delay is explicit configuration, not adaptive jitter control or proof of network latency. Start with a modest delay and tune using real frame and snapshot-age observations. Use shortest-arc interpolation for angles; never linearly interpolate wrapped yaw across a full turn. This package does not supply prediction, a room authority, reliable commands, or a persistence store.
