Skip to content

Add WebSocketClient and BonjourService targets#15

Open
luizmb wants to merge 5 commits into
mainfrom
feature/websocket-bonjour-targets
Open

Add WebSocketClient and BonjourService targets#15
luizmb wants to merge 5 commits into
mainfrom
feature/websocket-bonjour-targets

Conversation

@luizmb
Copy link
Copy Markdown
Owner

@luizmb luizmb commented Jun 1, 2026

Summary

  • Removes external CombineWebSocket and CombineBonjour dependencies — NetworkTools is now self-contained
  • Adds WebSocketClient target: Combine (WebSocket, WebSocketPublisher) + async (WebSocketConnection) APIs
  • Adds BonjourService target: Combine (NWBrowserPublisher, NWListenerPublisher, + 3 legacy NetService publishers) + async (bonjourBrowserStream, bonjourListenerStream) APIs
  • Vendors DemandBuffer (from CombineExt) into Core with package access level

Linux / cross-platform design

Both targets compile on Linux even though the functionality is unavailable there. The public API surface never exposes Apple-specific types:

Type Platform Replaces
WebSocketMessage unconditional URLSessionWebSocketTask.Message
WebSocketConnection unconditional — (new)
BonjourServiceInfo unconditional NWEndpoint
BonjourConnection unconditional NWConnection
BonjourBrowseEvent / BonjourListenEvent unconditional had NWEndpoint / NWConnection

Apple implementations are guarded:

  • #if canImport(Darwin)WebSocketConnection+Darwin.swift, NetService+Extensions.swift
  • #if canImport(Network) — all NW* stream files, BonjourConnection+Darwin.swift, IP.swift
  • #if canImport(Combine) — all Combine publishers

A future Linux/NIO implementation would produce the same unconditional types from different factories with no API change for callers.

Test plan

  • swift build succeeds with zero errors and warnings on macOS
  • WebSocketConnection and BonjourConnection structs are referenceable without any platform guard
  • Combine publishers compile and publish WebSocketMessage / BonjourBrowseEvent / BonjourListenEvent
  • bonjourListenerStream yields BonjourListenEvent.newConnection(BonjourConnection)NWConnection is not visible to callers
  • All existing targets (Core, NetworkClient, NetworkServer, Multipeer) still build cleanly

🤖 Generated with Claude Code

Adds two new SPM targets with dual Combine / DeferredTask+DeferredStream APIs,
designed to compile on Linux even where the functionality is unavailable.

WebSocketClient
- WebSocketMessage (unconditional) — platform-agnostic text/data enum
- WebSocketConnection (unconditional) — lazy struct of DeferredTask/DeferredStream
  closures; no Apple types in the public surface
- WebSocketConnection+Darwin.swift (#if canImport(Darwin)) — URLSessionWebSocketTask
  implementation using callback-recursive receive (no async/await in the stream)
- Combine: WebSocket, WebSocketPublisher, URLSessionWebSocketTaskProtocol — all
  under #if canImport(Combine); publisher output is WebSocketMessage for
  consistency with the async API

BonjourService
- BonjourServiceInfo, BonjourConnection (unconditional) — platform-agnostic browse
  result and TCP connection structs; NWEndpoint and NWConnection never appear in
  the public API
- BonjourBrowseEvent, BonjourListenEvent + error types (unconditional) — use
  only BonjourServiceInfo, BonjourConnection, UInt16
- NWBrowserStream, NWListenerStream (#if canImport(Network)) — convert NW types to
  platform-agnostic events; BonjourConnection+Darwin wraps NWConnection with the
  same callback-recursive pattern as WebSocketReceiveDelegate
- Combine: NWBrowserPublisher, NWListenerPublisher, NWEndpointPublisher,
  NetServiceBrowserPublisher, NetServicePublisher — all under #if canImport(Combine)
- Extensions: NWBrowser+Extensions (#if canImport(Network)),
  NetService+Extensions (#if canImport(Darwin)); IP.swift (#if canImport(Network))
  with FoundationExtensions dependency inlined as two private Data helpers

Core
- DemandBuffer vendored from CombineExt with package access level so both
  WebSocketClient and BonjourService Combine publishers can share it without
  making it part of the public API

Package.swift
- Removes external CombineWebSocket and CombineBonjour dependencies;
  NetworkTools is now self-contained
- Adds Core dependency to both new targets

README updated with full Combine and async examples for all four
browse/advertise × Combine/async combinations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@luizmb luizmb changed the base branch from fix/dependabot-track-package-resolved to main June 1, 2026 20:30
luizmb and others added 4 commits June 1, 2026 21:33
- opening_brace: double-space before { in stop() methods → single space
- statement_position: else on new line → same line as closing brace
- multiline_arguments / multiline_parameters: one arg per line
- cyclomatic_complexity: extract browseResultsChangedHandler body into
  handle(change:) method in NWBrowserStreamDelegate
- superfluous_disable_command: remove discouraged_optional_collection and
  type_name disable comments that no longer triggered
- discouraged_optional_collection: use block-level disable/enable in
  NWBrowser+Extensions wrapping the extension
- trailing_newline: remove extra blank line from BonjourBrowseEvent.swift

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both types were structs of closures, which has two problems:
1. Freely copyable — multiple "owners" sharing one resource with no clear
   responsibility for closing it.
2. close was a DeferredTask<Void> stored property — nothing calls it unless
   the caller remembers to .run() it; no RAII, no cleanup on scope exit.

Fix: make both final classes with a cancelAction: () -> Void and deinit
that fires the action. The connection now lives exactly as long as someone
holds a reference, mirroring how Combine's WebSocket closes when its
AnyCancellable is cancelled.

- close() becomes a synchronous func (like AnyCancellable.cancel()) instead
  of a stored DeferredTask — explicit early close before last reference drops.
- deinit provides automatic RAII-style cleanup.
- send/receive/ping remain DeferredTask/DeferredStream stored properties for
  FP composability; only close is imperative.

Also:
- Extract private makeConnection(_:) helper in WebSocketConnection+Darwin to
  eliminate factory code duplication across the three URL overloads.
- Add webSocketConnection(with:protocols:) overload to match the Combine API.
- Update README close() example.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ption bridge

bonjourBrowserStream(browser: NWBrowser):
- Adds a pre-built-browser overload alongside the serviceType convenience.
- Refactors NWBrowserStreamDelegate to take an NWBrowser in its designated
  init; the serviceType variant now delegates to bonjourBrowserStream(browser:).

bonjourResolve(_:timeout:) -> DeferredTask<Result<ResolvedServiceInfo, Error>>:
- New ResolvedServiceInfo (unconditional) carries host, ips, port, txt as
  platform-agnostic types (strings and UInt16).
- Convenience webSocketURL(path:secure:) on ResolvedServiceInfo builds a
  ws:// / wss:// URL from the resolved address, handling IPv6 brackets.
- BonjourResolve+Darwin.swift implements resolution via NetService internally
  (the only API that resolves a name to IP without opening a connection);
  the public API is clean and platform-agnostic.
- NetServiceResolveDelegate uses a self-retention pattern (strongSelf) to
  stay alive across the withCheckedContinuation suspension.

ServiceDescription ↔ BonjourServiceInfo bridge:
- init(_: BonjourServiceInfo) on ServiceDescription for Combine callers.
- init(_: ServiceDescription) on BonjourServiceInfo for async callers.

Fix four new lint violations introduced by class rewrites:
- vertical_whitespace_opening_braces in WebSocketConnection and BonjourConnection.
- opening_brace double-space in stop() methods.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant