Capacitor provides bounded, frame-indexed input synchronization primitives for lockstep and rollback simulations. It owns input history, prediction, correction tracking, frame batching, rollback snapshot metadata, and peer frame progress. Games retain transport, authentication, serialization, snapshots, and simulation policy.
npm install https://codeload.github.com/bluehexagons/capacitor/tar.gz/refs/tags/v0.7.2The supported Node.js runtime is 24 or newer. Runtime modules use ordinary ECMAScript values and contain no Node APIs, so they can also be bundled for modern browsers and native shells.
import { Capacitor } from '@bluehexagons/capacitor';
type Input = { buttons: number };
const inputsEqual = (a: Input, b: Input) => a.buttons === b.buttons;
const inputs = new Capacitor<Input>(inputsEqual);
const player = inputs.connect({ historyFrames: 1024 });
player.commit(0, { buttons: 1 });
if (inputs.readConfirmed(0)) {
simulate(player.cache!);
}Predictions are provisional. A mismatching confirmation returns corrected
and records the earliest dirty frame. Confirmed input is immutable by default;
a second, different confirmed value returns conflict without replacing it.
const remote = inputs.connect({
predictor: (previous) => previous ?? { buttons: 0 },
});
const resolved = inputs.resolveFrame(frame, {
predict: true,
maxPredictionLead: 8,
});
if (resolved.complete) {
saveSnapshot(frame);
simulate(resolved.clients.map((result) => result.value!));
}
const dirty = inputs.consumeDirty();
if (dirty !== null) {
inputs.invalidatePredictedFrom(dirty);
restoreSnapshot(dirty);
replay(dirty, frame);
}RollbackRing supplies bounded snapshot indexing and unsafe-boundary metadata
while callbacks keep the snapshot representation game-owned.
| Name | Meaning |
|---|---|
startFrame |
First participating frame, inclusive |
endFrame |
Last participating frame, exclusive |
baseFrame |
Oldest retained frame |
confirmedHead |
First frame not contiguously confirmed |
writtenHead |
First frame beyond every stored value |
Clients before startFrame block lockstep reads because their participation is
pending. Clients at or beyond endFrame are complete and do not block reads or
batch frontiers.
collectFrameBatch selects confirmed input fairly from active sources.
applyFrameBatch classifies decoded input and advances the shared confirmed
frontier. FrameExchangeProgress tracks the per-peer send, acknowledgement,
receive, and rebase cursors around those helpers.
const outgoing = collectFrameBatch({
sources: [player],
originFrame: progress.selectSendOrigin(2),
throughFrame: frame,
maxEntries: 255,
maxFrameSpan: 255,
});
const revision = progress.revision;
queuePacket(outgoing, (error) => {
if (error === null) progress.markSent(outgoing.sentThroughFrame, revision);
});
const incoming = applyFrameBatch({
targets: new Map([[playerID, remote]]),
entries: decodedEntries,
originFrame: decodedOrigin,
receivedThroughFrame: progress.receivedThroughFrame,
maxFrameLead: 255,
});
progress.markReceived(incoming.receivedThroughFrame);Batching invariants:
maxEntriesmust cover every source participating in the requested span.- A target rejects input that would evict unresolved retained history.
- Remove or deactivate participants at their exclusive
endFrame. - Treat
conflictEntriesas protocol errors unless legacy replacement was explicitly enabled. - Capture
revisionwhen queueing transport work and advance sent or acknowledged cursors only from that work's successful completion callback. - Encode authentication, checksums, epochs, and payloads outside this package.
npm install
npm run checkChecked-in output under build/src supports Git-tag installs. See
CHANGELOG.md for release history. Releases update both lockfile
versions, run npm run check, commit generated output, then run
npm run release.
Apache-2.0