feat(realtime): v3 client implementation#995
Draft
grdsdev wants to merge 82 commits into
Draft
Conversation
Coverage Report for CI Build 28446934485Coverage decreased (-0.03%) to 80.743%Details
Uncovered ChangesNo uncovered changes found. Coverage Regressions3 previously-covered lines in 1 file lost coverage.
Coverage Stats
💛 - Coveralls |
Greenfield Swift API redesign for the Realtime module, targeting Swift 6.2+. Captures the locked design after grill-through review: - realtime-v3.md — full API specification: explicit lifecycle, typed throws throughout, AsyncSequence as the canonical surface, register-then-subscribe pattern for postgres_changes (reflecting the Phoenix wire constraint that filters must be in the join payload), shared-by-topic channel identity, pluggable transport + clock for deterministic testing, 45+ locked decisions with rationale, and a V2 → V3 migration table. - realtime-v3-questions-for-backend.md — assumption-vs-question pairs across 16 topics (connection, channel lifecycle, broadcast WS + HTTP, replay, presence, postgres changes, auth, errors, rate limits, ordering, app lifecycle, protocol limits) for the Realtime backend team to validate before implementation begins. Tracked in Linear: https://linear.app/supabase/project/realtime-v3-idiomatic-swift-api-rfc-044c5935314f Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the two-type Channel + ActiveChannel split with a single post-join type that conforms to AsyncSequence and owns broadcast send. - subscribe() returns ChannelSubscription (was ActiveChannel) - ChannelSubscription: AsyncSequence with Element = PhoenixMessage - typed views: broadcasts(of:event:), events(for: token), presence - broadcast send moves from Channel to ChannelSubscription — type-level gate replaces the runtime .channelNotJoined error - presence accessor moves from Channel to ChannelSubscription - subscribe() is now the only join path; no iteration-driven lazy-join - 30-second tour, §2-§7, §11 migration table, §12 decisions, Appendix A all updated end-to-end The new shape collapses asymmetry: Channel exposes registration + the join verb; ChannelSubscription exposes everything else. Sending without a live subscription is unrepresentable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…te accessor - PhoenixMessage gains joinRef and ref fields for request/reply correlation visibility; raw iteration now includes internal phx_reply/phx_close/phx_error frames so advanced consumers can observe everything the SDK sees. - Document raw iteration as the unfiltered escape hatch (the SDK still consumes these frames internally for ack correlation and lifecycle). - Lock decisions 14k (raw PhoenixMessage shape) and 14l (defer ChannelSubscription.isAlive / state accessor — additive if needed). - subscribe() remains async throws (no change); confirmed lock. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Including topic on the raw frame so consumers that pass PhoenixMessage values across logging, debugging, or multi-topic aggregation surfaces keep the routing key without threading it separately. Always matches the ChannelSubscription's topic for in-iteration consumers. Confirmed locks (no spec change needed): - Socket-level RTT via ConnectionStatus.latency is sufficient (no per-channel / per-broadcast latency stream). - ChannelSubscription stays as the type name. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…lidates-subscription
Three improvements after consistency sweep:
1. ChangeRegistration generics simplified.
- Variants (Insert/Update/Delete/AnyEvent) are themselves generic over T
and conform to a ChangeEventVariant protocol declaring Element.
- ChangeRegistration drops to a single generic parameter (the variant
carries T): ChangeRegistration<Insert<Message>> instead of
ChangeRegistration<Message, Insert>.
- sub.events(for:) becomes a single overload dispatched on the variant.
- Fixes a real type-system bug in the previous shape, where
ChangeEventVariant.Element referenced T but T wasn't in scope.
2. Filter split into Filter<T: RealtimeTable> + UntypedFilter.
- Untyped path no longer requires JSONValue to conform to RealtimeTable.
- Untyped factories (channel.changes(schema:table:filter:), etc.) return
ChangeRegistration<E<JSONValue>>; identical type to typed registrations
just with a different variant T. Mix freely.
3. Tighten subscription lifecycle.
- §2.1 invariants now state that manual leave() invalidates the
subscription value; methods throw .channelClosed(.userRequested);
iteration terminates; reconnects do NOT invalidate.
- Decision 14m captures this; 14n/14o capture the type split.
Also drops .channelNotJoined from §7 references (already removed earlier;
Decision 26 still mentioned it — fixed).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bugs fixed: - §1.2 Configuration: replace fictional .iso8601 / .automaticDefault statics with proper SDK-provided constants (JSONDecoder.realtimeDefault, LifecyclePolicy.automaticDefault). Add `& Sendable` to the clock type. - §1.2: rename `handleAppLifecycle: Bool` to `lifecycle: LifecyclePolicy` so the §9.3 enum is actually wired in (was dead code before). - §6.1: declare realtime.connect() — was referenced but never declared. - §4: define PresenceKey (typealias for String). - §5.2: add RealtimePostgresFilterValue constraint to .in factory. - Appendix A: pass `me: UUID` through init (Self.currentUserID was undefined); rename ChatMessage to ChatBroadcast and define the type. Inconsistencies fixed: - §2.1: drop "joins lazily on first subscribe" (subscribe IS the join). - §2.3: pipelined re-acquire returns the same Channel actor in unsubscribed state, not a "fresh Channel" (was contradicting Decision 1). - §4: docstring referenced channel.leave(); generalize to "leave on any holder of the topic". - §9.2: stale channel.broadcast(...) → sub.broadcast(...). - §6.2: split disconnect()'s close reason from policy giveup — add CloseReason.clientDisconnected. Reconnection policy applies only to UNEXPECTED closes. - §12: refresh Decisions 12, 13, 22 to reflect the typed/untyped filter split and the broadcast Data overload. Decision 14g merged into 26. Ambiguities tightened: - §6.3: clarify "same token" = byte-equal returned string, no rotation attempted; propagate .authenticationFailed. - §6.4: document `since` (state-entry timestamp) and `latency` semantics. - §7: explain .disconnected vs .channelClosed distinction. - §7: add .unknownToken for events(for:) called with cross-channel tokens. - §2.1 PhoenixMessage.joinRef: document v1 (always nil) vs v2 behavior. No semantic changes — all locked decisions stand. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ad44789 to
d2d0987
Compare
…ain actors) Final review pass focused on Swift 6 idiom, grounded against the repo (Swift tools 6.1, StrictConcurrency complete) and house style. - Plain `public actor Realtime` / `public actor Channel` — drop `final` (an error on actors) and redundant `: Sendable`. Matches AuthClient / FunctionsClient house style. - Explicit isolation contract: immutable metadata (topic, options), stream factories (broadcasts, postgresChanges, messages, state, presence, status), and postgres registration are `nonisolated` over a Sendable lock-protected hub; channel(_:), subscribe, leave, broadcast-send, httpBroadcast, and presence.track are isolated `async`. Resolves call sites in the examples that read members without `await`. - channel(_:) is isolated (touches the actor's topic registry); callers `await`. Appendix A restructured to acquire the channel async in start(). - Raw feed is now `nonisolated var messages: AsyncStream<PhoenixMessage>` instead of an AsyncSequence conformance on the actor (avoids the awkward synchronous makeAsyncIterator() requirement and a public iterator type). - Document AsyncStream single-consumer semantics: fan-out is per call; two consumers = two calls. - New Decisions 8a (isolation contract) and updated 8, 14f. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… actor Per review: remove the "nonisolated over a Sendable lock-protected hub" concept. Channel is now a straightforward actor that is the single source of truth for its state. - `state` is an isolated property (stored in the actor), not nonisolated. - Raw feed is `func messages() -> AsyncStream<PhoenixMessage>` — a method, since each call mints a fresh stream; isolated. - broadcasts(of:event:), postgresChanges(for:), and the postgres registration factories (changes/inserts/updates/deletes) are isolated — they register a consumer / pending registration in the actor, so callers `await` them. - realtime.status is isolated too (same actor-state-backed-stream situation). - Only `topic`/`options` (immutable lets) and the `presence` accessor (wraps self; its stream methods register lazily) remain nonisolated. - Rewrote the Isolation contract, Decisions 8a/14f, and updated the 30-second tour, §5.3/§5.4 usage, and Appendix A to `await` the factories. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Aligns the design with the project's actual floor (AGENTS.md: iOS 16+; package: swift-tools 6.1). Audited every higher-floor dependency: - Swift 6.2-only: removed the isolated-deinit claim (Appendix C). The leaked-channel warning reads a nonisolated flag from the synchronous deinit, so no isolated deinit is needed. Typed throws is Swift 6.0, fine. - iOS 17 withThrowingDiscardingTaskGroup → withThrowingTaskGroup(of: Void.self) + waitForAll() in both task-group examples (§5.3 usage, Appendix A). - iOS 17 @observable → ObservableObject + @published in Appendix A (with a note that iOS 17+ uses @observable, and Perception back-ports below 17). Design Principle 4 reworded: the SDK surface is AsyncSequence-based and does not depend on Observation. - Appendix C floor: iOS 16 / macOS 13 / tvOS 16 / watchOS 9 / visionOS 1. macOS pairs at 13 (not 12) because Duration/Clock/ContinuousClock — used for timeouts, backoff, and heartbeat RTT — are iOS 16 / macOS 13. The core design (actors, AsyncStream, typed throws, macros, the isolation contract) is unchanged — all of it is available on Swift 6.1 / iOS 16. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…annelState) - JSONValue: typealias to AnyJSON for SDK interoperability - RealtimeError: flat enum with 20 error cases (spec §7) + LocalizedError - ChannelState + CloseReason: connection state machine (spec §2.4)
- Refactor InMemoryConnection.frames to use AsyncThrowingStream.makeStream() and store the bridging Task so close(code:reason:) can cancel it, preventing leaks when the consumer drops early. - Remove the unused fileprivate yieldClientSent(_:) method from TransportServer. - Remove the no-op nonisolated annotation from TransportServer.init. - Add a NOTE comment at the connection-construction site explaining shared streams across reconnects.
- RefGenerator: LockIsolated monotonic counter producing "1", "2", ... refs - InflightPushRegistry: actor with LockIsolated pending dict to race reply vs timeout - RealtimeError: add Equatable conformance for #expect(throws:) in tests
…licy, ConnectionStatus
Clamp jitter result to non-negative in exponentialBackoff, add an explanatory comment on State about missing Equatable synthesis, and add three new tests covering max-clamp, fixed with limit, and fixed unlimited.
…lazy connect Add ChannelOptions/BroadcastOptions/PresenceOptions/ReplayOption (§2.2), minimal Channel actor stub, and Realtime actor with idempotent/coalescing connect(), topic→Channel registry (first-call-wins, Decision 33), and status AsyncStream broadcast. Transport parameter is required (no default) pending Task 11's URLSessionTransport.
…altimeError Equatable; fix connect task lifecycle
…on reconnect After a successful reconnect, all channels that were previously joined (shouldRejoin=true) automatically re-send their phx_join frame with the same baked postgres_changes registrations. On success, presence state is re-tracked if isPresenceTracked was true (Decision 18). Open messages(), broadcasts, postgres, and presence streams survive the transport gap without interruption (Decision 6). On policy give-up, eligible channels transition to .closed(.transportFailure), terminating all streams with the appropriate error/finish, and are evicted from the channel registry.
Observes app background/foreground transitions when Configuration.lifecycle == .automatic (the default on iOS/macOS/tvOS/visionOS). On returning to foreground after a background drop, reconnects only if the socket is down and no intentional disconnect was requested. Manual lifecycle policy and explicit disconnect() both suppress the foreground reconnect. The lifecycle source is injectable via an internal init parameter so tests drive it deterministically without a real app.
Add RealtimeLogger protocol, LogEvent, LogLevel, Category types per spec §10. Ship OSLogLogger (macOS 11+/iOS 14+ availability guard) and StdoutLogger. Add Configuration.logger field (was TODO-deferred). Emit log events at key lifecycle sites: - connection: connect, connected, disconnect, reconnect (with attempt metric) - channel: join, joined, leave, left, rejection - presence: track, untrack - postgres: subscription errors via system events Metrics-as-logs: heartbeat.rtt_ms on every heartbeat reply, reconnect.attempt on every reconnect loop iteration. Realtime.log() is nonisolated (backed by nonisolated let logger) so Channel can call it synchronously without await, keeping logging non-throwing and non-async as required.
Tracks which channel topics have joined but not been explicitly left in a nonisolated LockIsolated<Set<String>> on Realtime. A synchronous deinit reads this set (no actor hop, no Swift 6.2 isolated deinit) and fires reportIssue via IssueReporting if any topics remain joined. Channel.transition(to:) wires _markJoined/_markLeft at join/close boundaries. disconnect() intentionally does NOT clear joinedTopics (transport-level, not leave). Tests use withExpectedIssue (Swift Testing) for the leak case and plain pass for the no-warn case; deinit is made deterministic by scoping Realtime in an inner function and calling disconnect() to cancel background task captures first.
…uration.encoder, send vsn Fix #1: Channel.receive(_:) now routes phx_close, phx_error, and non-postgres system error frames to terminal close transitions. Guards on current state so a trailing phx_close after our own leave() does not overwrite .userRequested. Clears shouldRejoin on server-initiated close to prevent auto-rejoin loops. The existing transition(to: .closed) cascade finishes all broadcast, postgres, presence, and messages streams so consumers receive the error cleanly. Fix #2: All user-payload Encodable encode sites now use realtime.configuration.encoder (Channel+Broadcast.swift broadcast<T>, Channel.sendPresenceTrack, HttpBroadcast.swift _httpBroadcastBatch). PhoenixSerializer retains bare JSONEncoder — it encodes protocol frame arrays (not user Codable types) and a comment documents the intentional distinction. Fix #3: _openConnection appends vsn=<configuration.protocolVersion.rawValue> to the connect URL so the backend can negotiate the binary serializer format. Tests: ServerCloseTests (serverPhxCloseTerminatesChannel, serverSystemAuthErrorClosesUnauthorized, ownLeaveNotOverwrittenByTrailingPhxClose, serverPhxErrorTerminatesChannel), ConfiguredEncoderTests (configuredEncoderUsedForBroadcast), VsnConnectTests (vsnSentOnConnect, vsnRespectsProtocolVersionConfig). All 129 RealtimeV3Tests pass, 0 warnings.
Wire the `Configuration.disconnectOnEmptyChannelsAfter` knob (Decision 39): when the last live channel leaves, arm a timer on `configuration.clock`; after the duration elapses with no new channels joining, close the socket and transition status to `.idle`. Key design choices: - Live-channel count is tracked via the existing `joinedTopics` LockIsolated set (already updated by `_markJoined`/`_markLeft`); reusing it avoids a parallel counter and is semantically correct — a terminally-closed channel is `_markLeft`, so it is no longer counted. - Idle close uses a separate `idleClosed` flag (not `intentionalDisconnect`) so the reconnect suppression is scoped to the idle close only. `connect()` clears `idleClosed` on entry, making the next `subscribe()` fully recover. - `idleCloseTask` is cancelled on `_markJoined` (new channel joins), on `disconnect()` (avoid double teardown), and superseded on re-arm. - Status after idle close is `.idle` (not `.closed(.clientDisconnected)`) to signal "recoverable idle" vs "explicit user disconnect".
…inst live supabase
…annel()
_openConnection() now appends `/websocket` to the base URL before dialling
the transport, guarding against double-append when the caller already ends
in `/websocket`. The HTTP broadcast client uses a separate httpBaseURL that
is derived from the original url (before path mutation) so it continues to
POST to `.../realtime/v1/api/broadcast` unchanged.
channel(_:configure:) now computes fullTopic = "realtime:<topic>" once, with
a hasPrefix guard to prevent double-prefix. The prefixed string IS channel.topic
from creation onward; no strip/dual-topic logic was added anywhere else.
Tests updated: all server-injected frames that carry a hardcoded topic string
now use the prefixed form ("realtime:room:1" etc.). UpdateTokenTests assertion
updated to match the prefixed topic. Integration tests switched to base URL
(no /websocket) and short topics — the SDK adds the prefix automatically.
…live supabase IE-3 BroadcastE2ETests: WS round-trip between two clients, HTTP broadcast delivery via httpBroadcastBatch (with SDK gap documented), ack mode. IE-4 PresenceE2ETests: two-client presence sync — track, observe join, cancel handle, observe leave. IE-5 PostgresChangesE2ETests: INSERT with room_id filter (server-id routing validated), UPDATE with old_record (REPLICA IDENTITY FULL confirmed), DELETE with PK-only old_record (server security behavior documented). IE-6 ReconnectionE2ETests: leave → disconnect → connect → subscribe cycle; broadcast stream resumes post-reconnect. SDK gap documented: intentional disconnect without prior leave() leaves channel in .joined state, making subscribe() idempotent. Forced-drop reconnect covered by unit RejoinTests. SDK gaps found: - Channel.httpBroadcast sends full "realtime:<topic>" — server needs short topic - HTTP broadcast endpoint requires service-role JWT, not anon apikey - disconnect() without leave() strands channel in .joined state
…nd disconnect channel-state semantics
…vent feed Replace the seven per-type consumer/finisher registries on Channel (broadcast/presence/postgres consumers + finishers + registrationConsumers) with a single ChannelEvent feed. messages(), broadcasts(of:event:), presence.observe/diffs, and postgresChanges(for:) are now transforms over a fresh subscription to that feed, each filtering and decoding the frames it cares about. The close reason is carried in-band (.terminated(reason)) so throwing streams finish with .channelClosed(reason) race-free. Per-consumer presence state becomes task-local (drops LockIsolated), decoding moves off the actor's critical path, and postgres server-id routing is read live per frame via a new postgresServerIDs(for:) helper.
…elpers Extract the repeated subscribe + drain-task + onTermination + terminal-close handling into two generic Channel helpers, _makeStream and _makeThrowingStream, parameterized by an inout per-subscription State. messages(), broadcasts, presence observe/diffs, and postgresChanges now each reduce to a small body closure over .message frames; the helper owns task lifecycle and translates a terminal close to a clean finish (non-throwing) or .channelClosed(reason) (throwing). Presence's running roster threads through State, so no external storage or lock. No public API or behavior change.
… primitive All nine frame-sending paths (broadcast x2, presence track/untrack/retrack, join, rejoin, leave, access_token) re-implemented the same wire mechanics: ref generation, text/binary encoding with error mapping, lazy-connect send, and ack correlation. Extract that into Channel._push(event:body:ref:joinRef:ack:), with a PushBody enum (text / broadcastJSON / broadcastData) and an AckPolicy (none / require). Add _requireJoinedForSend() for the shared state gate and _encodeToJSON() for the encoder round-trip. Collapses the two divergent ack patterns into one: presence no longer pre-registers a manual replyTask before sending — awaiting after send is race-free thanks to the registry's early-reply buffering. No public API or behavior change.
Drop _retrackPresence and the rejoin block that re-sent the last tracked presence payload after a transparent reconnect, along with the now-dead lastTrackedPresencePayload storage. isPresenceTracked stays (it keeps untrack idempotent). After a rejoin, presence is no longer automatically re-tracked; callers re-track explicitly if desired.
… funnel Move the push primitive from Channel to Realtime (parameterized by topic) and route every framed send through it, including the heartbeat. Channel._push is now a thin wrapper supplying topic + the channel's joinRef. Add _rawSend (the one place a frame reaches the socket) and a lazyConnect flag so the heartbeat sends on the current connection without triggering a reconnect (a dead socket surfaces as .disconnected/timeout instead). Replace sendText/sendBinary with _rawSend. Heartbeater drops its encode/send/awaitReply/serializer plumbing and becomes a loop that calls Realtime._sendHeartbeat (which routes through _push and measures round-trip latency). No public API or behavior change.
Two quick wins enabled by the _push funnel: - Wire the documented broadcast.ack_latency_ms metric: _push measures the send-to-ack round trip and logs it for acked broadcasts (heartbeat keeps its own heartbeat.rtt_ms; other acks have no defined metric). Factor the Duration-to-ms math into a Duration.inMilliseconds helper reused by both. - Move _encodeToJSON to Realtime (nonisolated; reads the Sendable config) so HTTP broadcast and channel broadcast/presence share one encoder round-trip instead of three copies. Channel._encodeToJSON is now a thin wrapper.
…hannel joined A join whose payload carries postgres_changes registrations is not actually subscribed when the phx_reply arrives — the server sets up replication asynchronously and confirms with a system event (extension postgres_changes, status ok). Transitioning to .joined on the reply alone drops the first changes (the recurring IE cold-start flake). _performJoin and rejoin now subscribe to the channel event feed before sending the join and, when registrations are present, await the system confirmation (via _awaitPostgresSubscribed, racing joinTimeout) before .joined: status ok proceeds, status error throws .postgresSubscriptionFailed, timeout throws .channelJoinTimeout. No-registration joins keep reply-only behavior. The test transport now emits the system ok after a join carrying a non-empty postgres_changes set, mirroring the real server.
…n files Both Realtime.swift (~1150) and Channel.swift (~1050) were over the 800-line house soft-max. Carve out cohesive extensions (no behavior change): - Realtime+Push.swift — nextRef/_rawSend/_push/_encodeToJSON/awaitReply/accessTokenForJoin - Realtime+Reconnection.swift — handleConnectionLost + reconnection loop + rejoin/give-up - Realtime+Heartbeat.swift — startHeartbeat/_sendHeartbeat/updateLatency - Channel+Routing.swift — receive + server-close/error + system routing + server-id routing - Channel+Presence.swift — gains the presence track/untrack send seam Stored properties stay in the primary declarations; members reached across the new file boundaries are widened from private to internal (Swift private does not cross files), each annotated with why. Realtime.swift → 778, Channel.swift → 786.
… rejoin _performJoin (subscribe path) and rejoin (reconnect path) were ~80% parallel — generate ref, subscribe the postgres feed, build payload, push join, await reply, build server-id routing, await the postgres system confirmation — and had already drifted once (the postgres-system wait was added to both by hand). Extract _sendJoin(ref:realtime:) for that shared wire handshake, returning a JoinOutcome (.joined / .rejected) and throwing on wire failures. Each caller keeps its own error policy: _performJoin throws (rejection -> .unauthorized + .channelJoinRejected); rejoin maps outcome/error to state transitions (auth -> .unauthorized, transport -> recoverable .transportFailure). Behavior preserved, including the shouldRejoin=true ordering before the postgres wait.
…dcaster from Realtime First step in decomposing the Realtime god-actor (Tier 1). Pull two concerns that are not part of the socket state machine into focused value types held as actor-isolated stored properties (no separate actors, so no new hops or reentrancy windows, and no Sendable gymnastics): - ChannelRegistry — the topic→Channel collection: lookup, insert, evict, snapshot. Replaces the raw dict scattered across connect/reconnect/routing/token. - ConnectionStatusBroadcaster — current status + subscriber fan-out. Backs the status stream and transition()/updateLatency. transition(), status, and channel(_:) stay as thin wrappers, so call sites are unchanged. The socket mechanism (connect/reconnect/heartbeat/idle/routing/push) stays cohesive in Realtime by design — splitting it across actors would reintroduce the connection-state coordination the single-actor model prevents. No behavior change.
…tionTasks disconnect(), _performIdleClose(), and handleConnectionLost() each repeated the same sequence: cancel the heartbeat + frame-routing tasks and failAll in-flight pushes. Extract it into _teardownConnectionTasks(failPushesWith:). Each caller keeps what's genuinely its own — claiming/clearing connection, the socket close (distinct code/reason), caller-specific task/flag cancellation, and the status transition. No behavior change.
- connect(): preserve a specific RealtimeError from the connect task instead of flattening every failure into .transportFailure(underlying:). - Gate the _test_* shims behind #if DEBUG so they're compiled out of release. - Extract _connectURL(base:apiKey:vsn:) as a pure, nonisolated static helper and cover it with ConnectURLTests (websocket append idempotency, query dedup). - Replace the four invariant-justified as! casts in postgresChanges with a throwing postgresElement helper (never-crash policy). - Introduce SystemEventPayload to centralize the postgres_changes/system wire keys + parse shape shared by _awaitPostgresSubscribed, _routeSystemEvent, and the postgres transform. No behavior change.
…annelHost Channel held a weak reference to the concrete Realtime actor and reached into its internals. Introduce a ChannelHost protocol capturing exactly the surface Channel consumes (configuration, nextRef, log, _markJoined/_markLeft, _encodeToJSON, accessTokenForJoin, _push, _httpBroadcastBatch) and depend on `any ChannelHost` instead. Realtime conforms declaratively — every member already has matching isolation (the earlier _push/_encodeToJSON consolidation is what shrank the surface to 9 members). Decouples the two types: Channel can be exercised against a test double, and the host's identity becomes an implementation detail (de-risking a future connection-controller split). No behavior change.
Contributor
|
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.
Implements the Realtime v3 client in a new, isolated
RealtimeV3target (renamed toRealtimeat cutover), per the v3 design proposal.AsyncStream-based broadcast / presence / postgres_changes.Base:
release/v3.