diff --git a/README.md b/README.md index e1f4b5c..67f0710 100644 --- a/README.md +++ b/README.md @@ -232,9 +232,12 @@ ways over UDP against iroh v1.0.3, and against n0's production relay infrastructure over wss. `endpoint-demo/` is the first consumer, composed via `wac plug` and driven by `host-wasmtime/src/bin/endpoint-demo.rs`. Internally: one detached pump -task per bound endpoint owns all I/O, and resource methods observe its -consequences by bounded polling on the clock import (cross-task wakeups -have no channel that works on every host today; see the issues). The +task per bound endpoint owns all I/O, and wake-ups are event-driven in +both directions — resource methods kick the pump to flush their +mutations and park on wakers the pump fires (cross-task wakeups ride +wit-bindgen's `inter-task-wakeup` channel, delivered by both hosts; +the bench asserts the endpoint handshake stays within a fixed margin +of the single-task spike's). The JS host for this surface is `host-deltic/`: it drives the endpoint component runtime-linked under [deltic](https://github.com/lann/deltic) on stock Deno (no transpile step, no engine flag) — the jco host this diff --git a/endpoint/Cargo.toml b/endpoint/Cargo.toml index fb6f0b2..e7036ab 100644 --- a/endpoint/Cargo.toml +++ b/endpoint/Cargo.toml @@ -16,7 +16,7 @@ rustls = { version = "=0.23.43", default-features = false, features = ["std", "c getrandom = "0.4" bytes = "1" hex = "0.4" -wit-bindgen = { version = "0.59", features = ["async", "async-spawn"] } +wit-bindgen = { version = "0.59", features = ["async", "async-spawn", "inter-task-wakeup"] } futures = { version = "0.3", default-features = false, features = ["async-await", "std"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/endpoint/src/endpoint_impl.rs b/endpoint/src/endpoint_impl.rs index ef238de..e8f1241 100644 --- a/endpoint/src/endpoint_impl.rs +++ b/endpoint/src/endpoint_impl.rs @@ -1,17 +1,29 @@ //! The endpoint surface implementation: noq-proto state shared between //! the exported resources and one detached pump task per bound endpoint. //! -//! Resource methods mutate the shared state directly and wait for the -//! pump's consequences by bounded polling on the clock import — never by -//! parking on a waker another task fires. Cross-task wakeups have no -//! portable channel today: wit-bindgen's `inter-task-wakeup` feature -//! signals through a guest-internal unit stream, which wasmtime delivers -//! and jco does not, and racing a clock future against a waker would -//! cancel an in-flight import subtask, which jco traps on (#6). Bounded -//! polling costs at most one quantum per wake edge and behaves -//! identically on every host. All of it runs on the component-model async -//! ABI's single cooperative thread: the `RefCell` borrows never cross an -//! await. +//! Wake-ups are event-driven in both directions. Resource methods +//! mutate the shared state directly, kick the pump to flush the +//! consequences (`State::kick_pump`), and park on a waker in +//! `State::waiters`; the pump wakes the waiters after every drain that +//! progressed (`State::wake_waiters`). Cross-task wakeups ride +//! wit-bindgen's `inter-task-wakeup` channel — a guest-internal unit +//! stream whose write resumes a task parked in `waitable-set.wait` — +//! which both hosts deliver. (The jco-era bounded-polling pump this +//! replaces is recorded on issues #10 and #42.) +//! +//! The pump's clock tick stays: noq's timers need servicing, and the +//! tick turn wakes all waiters unconditionally, which bounds two things +//! at one tick — how late a waiter's deadline condition (relay-open +//! timeout, signaling deadline) is observed, and how stale a missed +//! wake edge can go. +//! +//! An in-flight import is a component-model subtask and is always +//! awaited to completion, never dropped mid-flight (the teardown +//! discipline; the kick future is guest-local, so re-creating it each +//! select turn cancels nothing). All of it runs on the component-model +//! async ABI's single cooperative thread: the `RefCell` borrows never +//! cross an await, and a fired waker resumes its task through the +//! scheduler, never synchronously. use std::cell::{Cell, RefCell}; use std::collections::{HashMap, HashSet, VecDeque}; @@ -19,6 +31,7 @@ use std::net::{IpAddr, Ipv6Addr, SocketAddr}; use std::pin::pin; use std::rc::Rc; use std::sync::Arc; +use std::task::{Poll, Waker}; use std::time::{Duration, Instant}; @@ -52,13 +65,10 @@ use crate::Component; use iroh_endpoint_core::relay::RelayConn; use wit_bindgen::rt::async_support::StreamReader; -/// The pump's tick: noq's deadlines, and the bound on how stale a -/// resource-method mutation can go unflushed. +/// The pump's tick: noq's deadlines, the waiters' deadline re-check +/// cadence, and the bound on how stale a missed wake edge can go. const TICK_NS: u64 = 10_000_000; -/// Resource methods' polling quantum while waiting on pump consequences. -pub(crate) const POLL_NS: u64 = 5_000_000; - /// Bounded window for final packets after `endpoint.close`. const LINGER: Duration = Duration::from_millis(500); @@ -128,6 +138,13 @@ pub(crate) struct State { /// Set when the relay connection died; every operation fails from /// then on. dead: Option, + /// Wakers parked by `wait_until` futures, drained and fired by + /// `wake_waiters`. + waiters: Vec, + /// The pump's parked waker and the pending-kick flag behind + /// `kick_pump` (consumed by the pump's `kicked` select arm). + pump_waker: Option, + pump_kicked: bool, } /// The wire behind one synthetic peer address. @@ -217,6 +234,9 @@ impl State { closed: false, closed_at: None, dead: None, + waiters: Vec::new(), + pump_waker: None, + pump_kicked: false, } } @@ -240,16 +260,37 @@ impl State { } /// Add a relay connection to the pool under its normalized URL. - /// The first registration is the home relay (`HOME_RELAY`). + /// The first registration is the home relay (`HOME_RELAY`). Kicks + /// the pump to arm the relay's receive. fn register_relay(&mut self, url: &str, conn: Rc) -> u32 { let key = self.next_relay_key; self.next_relay_key += 1; self.relay_pool.insert(key, conn.clone()); self.relay_keys.insert(normalize_relay_url(url), key); self.new_relays.push((key, conn)); + self.kick_pump(); key } + /// Wake every parked `wait_until` future to re-check its condition. + /// A woken task resumes through the scheduler, never synchronously, + /// so calling this under the `RefCell` borrow is sound. + pub(crate) fn wake_waiters(&mut self) { + for waker in self.waiters.drain(..) { + waker.wake(); + } + } + + /// Wake the pump to flush the consequences of a state mutation: + /// queued transmits and signals, new relays and channels, pending + /// upgrades, close. + pub(crate) fn kick_pump(&mut self) { + self.pump_kicked = true; + if let Some(waker) = self.pump_waker.take() { + waker.wake(); + } + } + /// True once no operation can succeed anymore; signaling sessions /// poll this to abandon their dance. pub(crate) fn is_closed_or_dead(&self) -> bool { @@ -280,9 +321,10 @@ impl State { } /// Queue one signaling payload for the pump to relay to `peer` - /// through the session's relay. + /// through the session's relay, kicking the pump to flush it. pub(crate) fn push_signal_outbound(&mut self, relay: u32, peer: [u8; 32], payload: Vec) { self.relay_outbound.push_back((relay, peer, payload)); + self.kick_pump(); } /// The next signaling payload from `peer`, if any. @@ -292,8 +334,8 @@ impl State { /// Register an open channel to `peer` and move the peer's route /// onto it: noq keeps addressing the peer's standin while the - /// packets change wire. The pump arms the channel's receive on its - /// next turn. A closing endpoint refuses and closes the wire. + /// packets change wire. Kicks the pump to arm the channel's + /// receive. A closing endpoint refuses and closes the wire. pub(crate) fn register_channel( &mut self, peer: [u8; 32], @@ -317,6 +359,7 @@ impl State { self.new_channels.push((id, wire)); let synthetic = self.addr_for_peer(peer, fallback_relay); self.routes.insert(synthetic, RouteWire::Channel(id)); + self.kick_pump(); Ok(()) } @@ -345,8 +388,11 @@ impl State { } /// Drain endpoint-bound events, application events, and transmits - /// until quiescent; polling method futures observe the consequences. - fn drain(&mut self) { + /// until quiescent; returns whether anything progressed, so the + /// pump wakes parked method futures exactly when the state they + /// observe may have changed. + fn drain(&mut self) -> bool { + let mut any_progress = false; let now = Instant::now(); let State { noq, @@ -408,11 +454,14 @@ impl State { if !progressed { break; } + any_progress = true; } if !entry.drained && entry.conn.is_drained() { entry.drained = true; + any_progress = true; } } + any_progress } fn handle_relay_datagram(&mut self, via: u32, source: [u8; 32], payload: Vec) { @@ -608,9 +657,18 @@ async fn pump(shared: Shared, udp: Option>) { let mut tick = pin!(monotonic_clock::wait_for(TICK_NS).fuse()); let mut channel_recvs: FuturesUnordered = FuturesUnordered::new(); let mut relay_recvs: FuturesUnordered = FuturesUnordered::new(); + // Set by the tick arm: wake the waiters even without drain progress, + // so deadline conditions are re-checked at tick granularity. + let mut force_wake = false; 'pump: loop { - shared.borrow_mut().drain(); + { + let mut st = shared.borrow_mut(); + if st.drain() || force_wake { + st.wake_waiters(); + } + } + force_wake = false; loop { let item = shared.borrow_mut().relay_outbound.pop_front(); match item { @@ -763,13 +821,22 @@ async fn pump(shared: Shared, udp: Option>) { } } }, + _ = kicked(&shared).fuse() => { + // A method mutated state needing a flush; the loop top + // drains and transmits it. + }, _ = tick => { tick.set(monotonic_clock::wait_for(TICK_NS).fuse()); shared.borrow_mut().handle_timeouts(); + force_wake = true; } } } + // No waiter sleeps past the pump: every break path set its terminal + // state (dead, or closed and drained/lingered) before arriving here. + shared.borrow_mut().wake_waiters(); + // Resolve the pinned imports before the task ends: close every pool // relay and every channel (each pending receive resolves with its // closed error), self-wake the UDP socket (a zero-length datagram @@ -886,6 +953,9 @@ fn handle_signal(shared: &Shared, via: u32, source: [u8; 32], payload: &[u8]) { if inbox.len() < INBOX_CAP { inbox.push_back(payload.to_vec()); } + // Inbox pushes bypass `drain`, so the loop-top wake never sees + // them; wake the session waiters here. + st.wake_waiters(); spawn_answerer }; if spawn_answerer { @@ -933,16 +1003,42 @@ fn normalize_relay_url(url: &str) -> String { url.trim_end_matches('/').to_string() } -/// Poll `check` against the shared state until it produces a value, -/// sleeping one quantum between attempts. Each sleep is a clock import -/// awaited to completion — never cancelled mid-flight. -async fn wait_until(shared: &Shared, mut check: impl FnMut(&mut State) -> Option) -> R { - loop { - if let Some(result) = check(&mut shared.borrow_mut()) { - return result; +/// Run `check` against the shared state until it produces a value, +/// parking a waker in `State::waiters` between attempts. The pump fires +/// the waiters when the state may have changed, and on every tick — a +/// deadline inside `check` is observed at tick granularity. +pub(crate) async fn wait_until( + shared: &Shared, + mut check: impl FnMut(&mut State) -> Option, +) -> R { + std::future::poll_fn(|cx| { + let mut st = shared.borrow_mut(); + match check(&mut st) { + Some(result) => Poll::Ready(result), + None => { + st.waiters.push(cx.waker().clone()); + Poll::Pending + } } - monotonic_clock::wait_for(POLL_NS).await; - } + }) + .await +} + +/// The pump's kick arm: resolves once a resource method kicks the pump, +/// parking the pump's waker meanwhile. Guest-local — re-creating it +/// each select turn cancels no import subtask. +async fn kicked(shared: &Shared) { + std::future::poll_fn(|cx| { + let mut st = shared.borrow_mut(); + if st.pump_kicked { + st.pump_kicked = false; + Poll::Ready(()) + } else { + st.pump_waker = Some(cx.waker().clone()); + Poll::Pending + } + }) + .await } // --- exported resources -------------------------------------------------- @@ -960,6 +1056,8 @@ impl Drop for EndpointRes { st.closed = true; st.closed_at = Some(Instant::now()); st.close_all(b"endpoint dropped"); + st.kick_pump(); + st.wake_waiters(); } } } @@ -1010,6 +1108,8 @@ impl EndpointRes { let opened = RelayConn::connect(url, &self.identity).await; let mut st = self.shared.borrow_mut(); st.relay_opening.remove(&normalized); + // Concurrent dialers of this relay wait on the outcome. + st.wake_waiters(); return match opened { Ok(conn) => Ok(st.register_relay(url, Rc::new(conn))), Err(e) => Err(Error::ConnectFailed(format!("relay {url}: {e}"))), @@ -1242,6 +1342,8 @@ impl GuestEndpoint for EndpointRes { if let Some(key) = upgrade_key { st.pending_upgrades.push((peer, key)); } + // Flush the first flight (and spawn the upgrade) now. + st.kick_pump(); handle }; @@ -1286,6 +1388,10 @@ impl GuestEndpoint for EndpointRes { st.closed = true; st.closed_at = Some(Instant::now()); st.close_all(b"endpoint closed"); + // Flush the CLOSEs and start the linger; wake the accept + // and connect waiters watching `closed`. + st.kick_pump(); + st.wake_waiters(); } } } @@ -1299,12 +1405,24 @@ impl ConnectionRes { async fn open_stream(&self, dir: Dir) -> Result { let handle = self.handle; - wait_until(&self.shared, |st| { + let mut kicked_blocked = false; + wait_until(&self.shared, move |st| { let entry = st.conns.get_mut(&handle).expect("connection entry"); if let Some(err) = &entry.error { return Some(Err(err.clone())); } - entry.conn.streams().open(dir).map(Ok) + match entry.conn.streams().open(dir) { + Some(id) => Some(Ok(id)), + None => { + // Blocked on stream credit: flush the + // STREAMS_BLOCKED once so the peer knows. + if !kicked_blocked { + kicked_blocked = true; + st.kick_pump(); + } + None + } + } }) .await } @@ -1416,7 +1534,7 @@ impl GuestConnection for ConnectionRes { } fn send_datagram(&self, data: Vec) -> Result<(), Error> { - self.with_entry(|e| { + let result = self.with_entry(|e| { if let Some(err) = &e.error { return Err(err.clone()); } @@ -1435,9 +1553,12 @@ impl GuestConnection for ConnectionRes { } other => Error::Other(format!("send-datagram: {other}")), }) - }) - // The pump's next tick flushes the queued datagram, the same - // bound stream writes live under. + }); + if result.is_ok() { + // Flush the queued datagram. + self.shared.borrow_mut().kick_pump(); + } + result } async fn recv_datagram(&self) -> Result, Error> { @@ -1461,6 +1582,8 @@ impl GuestConnection for ConnectionRes { VarInt::from_u32(code), bytes::Bytes::from(reason.into_bytes()), ); + // Flush the CONNECTION_CLOSE. + st.kick_pump(); } } @@ -1474,7 +1597,7 @@ impl GuestConnection for ConnectionRes { } } -/// Write all of `bytes`, polling through flow control as needed. +/// Write all of `bytes`, parking through flow control as needed. async fn write_all( shared: &Shared, handle: ConnectionHandle, @@ -1487,24 +1610,30 @@ async fn write_all( if let Some(err) = &entry.error { return Some(Err(err.clone())); } - loop { + let pass_start = offset; + let outcome = loop { match entry.conn.send_stream(id).write(&bytes[offset..]) { Ok(written) => { offset += written; if offset == bytes.len() { - return Some(Ok(())); + break Some(Ok(())); } if written == 0 { - return None; + break None; } } - Err(WriteError::Blocked) => return None, + Err(WriteError::Blocked) => break None, Err(WriteError::Stopped(code)) => { - return Some(Err(Error::Reset(code.to_string()))); + break Some(Err(Error::Reset(code.to_string()))); } - Err(WriteError::ClosedStream) => return Some(Err(Error::Closed)), + Err(WriteError::ClosedStream) => break Some(Err(Error::Closed)), } + }; + if offset > pass_start { + // Flush what this pass buffered, complete or not. + st.kick_pump(); } + outcome }) .await } @@ -1537,8 +1666,18 @@ async fn read_some( let result = chunks.next(max as usize); let _ = chunks.finalize(); match result { - Ok(Some(chunk)) => Some(Ok(Some(chunk.bytes.to_vec()))), - Ok(None) => Some(Ok(None)), + Ok(Some(chunk)) => { + // Consuming frees receive window; flush the credit + // update so the sender is not left blocked. + st.kick_pump(); + Some(Ok(Some(chunk.bytes.to_vec()))) + } + Ok(None) => { + // The consumed FIN retires the stream; flush the + // credit it releases. + st.kick_pump(); + Some(Ok(None)) + } Err(ReadError::Blocked) => None, Err(ReadError::Reset(code)) => Some(Err(Error::Reset(code.to_string()))), } @@ -1558,7 +1697,11 @@ impl GuestSendStream for SendStreamRes { return Err(err.clone()); } match entry.conn.send_stream(self.id).finish() { - Ok(()) => Ok(()), + Ok(()) => { + // Flush the FIN. + st.kick_pump(); + Ok(()) + } Err(FinishError::Stopped(code)) => Err(Error::Reset(code.to_string())), Err(FinishError::ClosedStream) => Err(Error::Closed), } @@ -1571,6 +1714,8 @@ impl GuestSendStream for SendStreamRes { .conn .send_stream(self.id) .reset(VarInt::from_u32(code)); + // Flush the RESET_STREAM. + st.kick_pump(); } async fn write_via_stream(&self, mut data: StreamReader) -> Result<(), Error> { @@ -1603,6 +1748,8 @@ impl GuestRecvStream for RecvStreamRes { let mut st = self.shared.borrow_mut(); let entry = st.conns.get_mut(&self.handle).expect("connection entry"); let _ = entry.conn.recv_stream(self.id).stop(VarInt::from_u32(code)); + // Flush the STOP_SENDING. + st.kick_pump(); } fn read_via_stream(&self) -> Result, Error> { diff --git a/endpoint/src/webrtc.rs b/endpoint/src/webrtc.rs index 55fd13f..f18dcf4 100644 --- a/endpoint/src/webrtc.rs +++ b/endpoint/src/webrtc.rs @@ -25,8 +25,7 @@ use crate::bindings::polymorph::webrtc_datachannels::connections::{ use crate::bindings::polymorph::webrtc_datachannels::types::{ DataChannelState, IceCandidate, Message as ChannelMessage, SdpType, SessionDescription, }; -use crate::bindings::wasi::clocks::monotonic_clock; -use crate::endpoint_impl::{Shared, POLL_NS}; +use crate::endpoint_impl::{wait_until, Shared}; /// The first byte of every signaling datagram on the relay. pub const SIGNAL_PREFIX: u8 = 0x00; @@ -131,28 +130,31 @@ fn publish(shared: &Shared, peer: [u8; 32], signal: &Signal) -> Result<(), Error Ok(()) } -/// The peer's next signal: polls the inbox the pump fills. `Ok(None)` -/// once the peer sends `done`; errors on the deadline or endpoint close. +/// The peer's next signal, from the inbox the pump fills (arrivals wake +/// the parked waiter; the deadline is observed at the pump's tick). +/// `Ok(None)` once the peer sends `done`; errors on the deadline or +/// endpoint close. async fn next_signal( shared: &Shared, peer: [u8; 32], started: Instant, ) -> Result, Error> { loop { - let payload = { - let mut st = shared.borrow_mut(); + let payload = wait_until(shared, move |st| { if st.is_closed_or_dead() { - return Err(Error::Closed); + return Some(Err(Error::Closed)); + } + if let Some(payload) = st.pop_signal_inbox(peer) { + return Some(Ok(payload)); } - st.pop_signal_inbox(peer) - }; - let Some(payload) = payload else { if started.elapsed() > SIGNAL_DEADLINE { - return Err(Error::ConnectFailed("webrtc signaling timed out".into())); + return Some(Err(Error::ConnectFailed( + "webrtc signaling timed out".into(), + ))); } - monotonic_clock::wait_for(POLL_NS).await; - continue; - }; + None + }) + .await?; let signal: Signal = match serde_json::from_slice(&payload) { Ok(signal) => signal, // A malformed signal is the peer's bug; skip it. diff --git a/scripts/bench.sh b/scripts/bench.sh index 079091a..f5f420f 100755 --- a/scripts/bench.sh +++ b/scripts/bench.sh @@ -12,10 +12,14 @@ cd "$(dirname "$0")/.." # # Time ceilings are deliberately loose: they catch order-of-magnitude # regressions (a lost first flight, a stalled pump) without flaking on -# shared CI runners. - -HANDSHAKE_CEILING_MS=2000 -ROUNDTRIP_CEILING_MS=2000 +# shared CI runners. The event-driven-pump claim (issue #42) is asserted +# separately, as a bound on the spike-to-endpoint handshake delta: +# both rows ride the same wire, relay, and run, so the delta cancels +# runner noise that absolute ceilings must tolerate. + +HANDSHAKE_CEILING_MS=250 +ROUNDTRIP_CEILING_MS=250 +POLLING_TAX_CEILING_MS=10 BULK_FLOOR_MBPS=1.0 RELAY_PORT=3341 @@ -113,10 +117,12 @@ run_once() { grep -m1 "handshake_ms=" "$LOGDIR/$name-client.log" } -# Median handshake/roundtrip over N iterations of a pairing. +# Median handshake/roundtrip over N iterations of a pairing. Leaves the +# handshake median in LAST_HANDSHAKE_MS for cross-row assertions. # bench_latency -- bench_latency() { local row=$1 iters=$2; shift 2 + LAST_HANDSHAKE_MS="" local handshakes=() roundtrips=() for i in $(seq 1 "$iters"); do local line @@ -131,6 +137,7 @@ bench_latency() { emit "$row" roundtrip_ms_median "$rt" [ "$hs" -le "$HANDSHAKE_CEILING_MS" ] || fail "$row handshake ${hs}ms > ${HANDSHAKE_CEILING_MS}ms" [ "$rt" -le "$ROUNDTRIP_CEILING_MS" ] || fail "$row roundtrip ${rt}ms > ${ROUNDTRIP_CEILING_MS}ms" + LAST_HANDSHAKE_MS=$hs } # Median bulk-echo throughput (payload out and back) over N iterations. @@ -155,20 +162,30 @@ bench_bulk() { # --- latency rows ---------------------------------------------------------- # # The spike (single-task, event-driven pump) is the baseline the -# endpoint's bounded-polling pump is compared against: the handshake -# delta between spike-relay and endpoint-relay is the polling tax -# recorded on issue #10. +# composed endpoint is compared against: the handshake delta between +# spike-relay and endpoint-relay is the price of the endpoint's +# resource surface (export-call tasks woken by the pump), asserted +# below against POLLING_TAX_CEILING_MS so the bounded-polling tax +# retired by issue #42 cannot quietly return. bench_latency spike-relay-wasmtime "$LATENCY_ITERS" \ timeout 120 "$HOST" "$SPIKE_WASM" --role server --server "$RELAY_URL" --transport relay -- \ timeout 120 "$HOST" "$SPIKE_WASM" --role client --server "$RELAY_URL" --transport relay \ --message bench --peer +SPIKE_RELAY_HS=$LAST_HANDSHAKE_MS bench_latency endpoint-relay-wasmtime "$LATENCY_ITERS" \ timeout 120 "$EHOST" "$COMPOSED_WASM" --role server --relay "$RELAY_URL" -- \ timeout 120 "$EHOST" "$COMPOSED_WASM" --role client --relay "$RELAY_URL" \ --message bench --peer +if [ -n "$SPIKE_RELAY_HS" ] && [ -n "$LAST_HANDSHAKE_MS" ]; then + TAX=$((LAST_HANDSHAKE_MS - SPIKE_RELAY_HS)) + emit endpoint-relay-wasmtime handshake_tax_ms "$TAX" + [ "$TAX" -le "$POLLING_TAX_CEILING_MS" ] || + fail "endpoint-relay handshake ${LAST_HANDSHAKE_MS}ms exceeds spike ${SPIKE_RELAY_HS}ms by ${TAX}ms > ${POLLING_TAX_CEILING_MS}ms" +fi + bench_latency endpoint-udp-wasmtime "$LATENCY_ITERS" \ timeout 120 "$EHOST" "$COMPOSED_WASM" --role server --relay "$RELAY_URL" \ --udp-bind 127.0.0.1:0 -- \ diff --git a/scripts/setup.sh b/scripts/setup.sh index 14ef86c..af0fbde 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -20,7 +20,7 @@ WEBRTC_PIN=13ddd6b4289e2503cb41fa7680758f2e3ddb08a8 WEBCRYPTO_REPO=https://github.com/polymorph-components/polymorph-webcrypto.git WEBCRYPTO_PIN=8a3de9cdaae901643d906b8d83f47bb797a2dd74 WEBSOCKET_REPO=https://github.com/polymorph-components/polymorph-websocket.git -WEBSOCKET_PIN=0278c0e9dfc13357b4b6d23a20a9b50d8176f51e +WEBSOCKET_PIN=f8fdf6601d251186a42c66b83f228368415d16ff IROH_REPO=https://github.com/n0-computer/iroh.git IROH_PIN=816dd70c056b813dcb5cbfb6a9a15e12d04b72b1 # v1.0.3 TLS_REPO=https://github.com/polymorph-components/polymorph-tls.git