feat(ocapn-simulator): browser playground for op:flush experiments#3250
Draft
kumavis wants to merge 3 commits into
Draft
feat(ocapn-simulator): browser playground for op:flush experiments#3250kumavis wants to merge 3 commits into
kumavis wants to merge 3 commits into
Conversation
Implement the receiver-initiated flush handshake that preserves per-reference FIFO order when a peer is about to shorten a promise (typically as part of a third-party handoff). When the receiver sends op:flush for an exported promise position, the exporter mints a fresh local promise, swaps it in at the same export position (keeping the slot's refcount), and replies with op:flush-done. Further deliveries the receiver had already sent for that position arrive before the ack and dispatch on the original promise; deliveries sent after ack are serialized through the new promise and naturally buffer until the shortened reference resolves it. Also exposes _debug.flushExport on Ocapn for tests, and adds replaceExportValue on the pairwise table to support in-place value replacement at a slot.
Move op:flush / op:flush-done into the existing OcapnMessageUnionCodec snapshot table so the wire form is captured by the same convention as the other operations, and update the snapshots. Drop the now-redundant manual round-trip tests. The flush behavior tests previously had B return an unresolved promise directly, which blocks the deliver-fulfill until the promise settles and prevented the export from being registered. Wrap the promise in an array so the array (a copyArray) is the immediate fulfillment value and the embedded promise is serialized as desc:import-promise on the wire. Also satisfy lint: thread the void/Promise<void> conversion of quietReject through unknown, and silence no-use-before-define on captures of send, didUnplug, and quietReject inside the new handler / flushExport.
🦋 Changeset detectedLatest commit: d9aa01c The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
a3a9d1e to
accc55f
Compare
accc55f to
d9aa01c
Compare
ee674ad to
19c0bbf
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a private workspace package
@endo/ocapn-simulator: a Vite-builtbrowser playground that runs N OCapN clients in web workers, brokered
by a custom
MessagePortnetlayer with simulated latency. The intentis to make the
op:flush/op:flush-doneproposal concrete enoughto poke at by hand — kick off forwarder chains, observe the export
tables fill with
desc:import-promiseslots, and trigger flushes thatmirror what an automatic shortening implementation would do as part of
a 3PH.
This was originally prototyped on
kumavis/ocapn#7, where the
simulator vendored the upstream package source and patched it at fetch
time. This PR brings the simulator into the monorepo so it imports
@endo/ocapn(workspace:^) directly. Two of the three upstreampatches are folded into the package itself; the third (
node:crypto→ WebCrypto) is handled in the simulator's Vite config without
modifying source.
What's in the simulator
Each worker hosts one
@endo/ocapnclient and exposes a single sturdyref
forward.forward(N)sleeps a random fraction of a second,picks a random peer, sleeps again, calls that peer's
forward(N - 1),and returns
harden([answerPromise]). The array wrap is the sametrick used in
flush.test.js— a bare returned promise gets awaitedand serialized as the settled value, collapsing the chain. Wrapping it
forces the marshal layer to emit it as
desc:import-promiseso thechain stays observable.
Wire protocol with the worker
After that, the two ports carry application bytes
(
{ kind: 'data', bytes }) directly, with each side's netlayerapplying the user-configured per-write latency.
Interactive UI features (current)
Controls
ends of every connection.
forward(N)kickoff.forward(<length>). The drainer walks the[innerPromise]wrap upto N+4 levels deep, so you eventually see the leaf string
done@<short-designator>.positions
p-0..p-63of every active session's table fordesc:import-promiseslots, and call the debugflushExport()onone. The exporter swaps in a fresh local promise at the same slot
(preserving refcount), replies with
op:flush-done, and subsequentdeliveries queue on the new promise so per-reference FIFO order is
preserved.
Visualization
that edge.
forward(N).forwardcallis dispatched.
Logs
console.logreports.currently has live sessions with.
Why this is useful for op:flush
The flush primitive is the receive-side mechanism that promise
shortening would use as part of a third-party handoff: when the
exporter learns a chain has resolved and wants to drop the import
table entry, it sends
op:flushfor the slot, mints a fresh localpromise as the new export at that position, and routes any in-flight
deliveries through the replacement so per-slot order is preserved.
Today the simulator exposes that primitive manually so you can:
losing in-flight messages or breaking refcounts.
Suggested improvements / future additions
These would make the simulator a much richer testbed for op:flush —
not in this PR; calling them out as ideas the next iteration could
pick up.
Observability of the export/import tables
active export and import slot, with refcounts and the value's
pass-style. Right now
flushExportprobesp-0..p-63becauseOcapnTabledoesn't expose iteration; an iteration API on thedebug surface would let the simulator render real-time table
contents instead of approximating from session activity.
— i.e., the answer-promise has already resolved on the exporter
but the importer hasn't been told yet.
op:flushissued,op:flush-donereceived,messages re-routed onto the replacement promise.
Manual chain construction
the user pick
A → B → C → Dexplicitly so you can reason abouta specific 3PH scenario.
forwardcall at a chosen worker so youcan inspect the table state before letting it continue.
from the page so it doesn't get GC'd between kickoffs.
Automatic shortening, hooked to op:flush
desc:import-promise,schedules a flush-then-3PH attempt once the answer has resolved
on the exporter side. This is the natural next experiment: the
simulator already has the primitives, what's missing is the
policy that decides when to invoke them.
you can visualise the chain depth shrinking as flushes land.
Stress and correctness scenarios
single global slider.
a controllable reorder window would help test that flush's
per-slot FIFO guarantee survives churn.
flush-done" path. Today the simulator never kills nodes once
spawned.
multiple slots are in-flight at once and flushes interleave.
Conformance scaffolding
the topology, the kickoffs, and the expected post-flush table
shape, runnable headlessly in Playwright. The package already
has Playwright as a devDep ancestor in the original PR; we
intentionally dropped it here to keep the dependency footprint
small, but a CI-runnable scenario harness is the obvious next
step once the spec text lands.
ocapn-pass-styleof every value crossing eachport, so the recorded transcript can be diffed against a fixture
to detect protocol regressions.
Spec exploration
op:flushfrom the UI (e.g.refcount-preserving vs. refcount-resetting; promise-only vs.
any export) so the proposal can be A/B-tested against a live
network.
replaceExportValueevent on the pairwise table(added in the flush feature commit) as a viz event, so you can
literally watch the slot swap.
Test plan
yarn installresolves without warningsyarn workspace @endo/ocapn-simulator devserves the page;kickoff produces leaf-string responses end-to-end
yarn workspace @endo/ocapn-simulator buildsucceeds; thebuilt worker bundle contains no
node:cryptoreference (onlygetRandomValues)yarn workspace @endo/ocapn run test— all 288 tests passyarn workspace @endo/goblin-chat run test— interop testspass (depends on the same
@endo/ocapnindex.js shape)random imported promise, confirm at least one
flush done: …line appears in the event logNotes for reviewers
The two changes to
@endo/ocapnare deliberate, not just simulatorplumbing:
encodeSwissnum/locationToLocationIdare clearly part of theexternal surface any embedder of OCapN needs (mint a sturdyref,
key on a session). The internal subpath was always an awkward
workaround.
deposit-giftrelaxation is a real protocol question for theflush proposal: if 3PH is going to ship answer-promises across
links as gifts, the exporter has to be willing to receive a
promise gift. Happy to break this out into its own PR if you'd
rather merge it independently of the simulator.
https://claude.ai/code/session_011bc6mdcwawQ6cisWED843c
Generated by Claude Code