Files
wristivo/Adapters/WatchBridge/INTEGRATION-PROPOSAL.md
T

140 lines
5.8 KiB
Markdown

# WatchBridge integration proposal
Status: accepted with integration decisions below. This proposal is retained as
design history; `docs/CONTRACTS.md` and RemoteCore/WatchSession.swift are the
authoritative final API. The original suggested signatures below are not the
implementation contract.
## Integration decisions
- Shared DTOs are implemented, including RelayFailure conforming to Error.
- RelayJSON centrally enforces millisecond dates and the 4096-byte payload bound.
- PhoneCommandCoordinating is an Actor protocol with explicit request values and
a local WatchPeerGeneration argument on every operation, plus activation and
deactivation callbacks. This resolves stale counterpart/lifecycle events.
- The bounded three-key container remains adapter-owned; RelayMessageKind freezes
its v1 routing vocabulary. Only A's single actor consumes command admission.
- An adjacent local Swift package provides Common/iPhone/Watch source and test
targets. Xcode links its platform products instead of duplicating source files
directly in the app targets. New B source files require no project regeneration.
- B may use a fake injected actor now. A's production conformance and physical
device behavior remain later integration gates, not blockers to B's coding.
## Original proposal (historical)
## Shared RemoteCore additions requested
Add typed, `Codable`, `Sendable`, `Equatable` control-plane values to
`RemoteCore`. Names are suggestions; the wire values and semantics need to be
frozen by integration before B implements them.
```swift
public struct BeginRemoteSessionRequest: Codable, Sendable, Equatable {
public let schemaVersion: Int
}
public struct RemoteSessionGrant: Codable, Sendable, Equatable {
public let schemaVersion: Int
public let sessionID: UUID
public let televisionID: UUID
public let issuedAt: Date
public let maximumCommandLifetimeMilliseconds: UInt64
}
public struct EndRemoteSessionRequest: Codable, Sendable, Equatable {
public let schemaVersion: Int
public let sessionID: UUID
}
public struct RemoteSessionEnded: Codable, Sendable, Equatable {
public let schemaVersion: Int
public let sessionID: UUID
}
public struct RelayFailure: Codable, Sendable, Equatable {
public enum Code: String, Codable, Sendable {
case unsupportedSchema
case malformedRequest
case noSelectedTelevision
case unavailable
case sessionInvalidated
}
public let schemaVersion: Int
public let code: Code
}
```
`maximumCommandLifetimeMilliseconds` must be positive and no greater than
`CommandAdmission.maximumLifetime * 1_000`. `issuedAt` is encoded as JSON
milliseconds since Unix epoch. A grant authorizes no command by itself and must
be bound by the phone actor to the currently activated paired WCSession and the
phone's currently selected TV.
The bridge container remains adapter-owned and has exactly three keys:
```text
schemaVersion: Int == 1
kind: String
payload: Data (maximum 4 KiB)
```
Freeze these v1 `kind` values: `beginSession`, `session`, `endSession`, `ended`,
`command`, `receipt`, and `error`. Every request receives one reply container.
Unsupported versions, unknown kinds, missing/extra or incorrectly typed keys,
oversized payloads, and structural decode failures fail closed with an `error`
reply. Semantic command rejection remains solely in the phone command actor.
## Injected phone actor seam requested
Integration/A should provide one actor conforming to a narrow protocol such as:
```swift
public protocol PhoneCommandCoordinating: Sendable {
func beginWatchSession() async -> Result<RemoteSessionGrant, RelayFailure>
func endWatchSession(_ request: EndRemoteSessionRequest) async
-> Result<RemoteSessionEnded, RelayFailure>
func submitWatchCommand(_ envelope: CommandEnvelope) async -> CommandReceipt
func watchBridgeDidDeactivate() async
}
```
That actor remains the only owner of the authorized session and
`CommandAdmission`. `WatchBridge` will retain only the injected protocol
reference on iPhone. It will not instantiate `CommandAdmission`, perform
semantic admission, select a TV, or issue session UUIDs.
Please confirm whether a changed paired-watch identity can be surfaced to the
actor as `watchBridgeDidDeactivate()` or needs an explicit opaque peer
generation parameter. No persistent hardware identifier should cross this seam.
## Project/test wiring requested
Update the integrator-owned project generator and generated project to:
- compile `Adapters/WatchBridge/Sources/Common` into both app targets;
- compile `Sources/iPhone` only into the iPhone target and `Sources/Watch` only
into the watchOS target;
- link `WatchConnectivity` through normal SDK imports (no third-party runtime);
- add adjacent fake-driven test targets for common, iPhone, and watch behavior;
- keep the adapter outside the pure algorithm package.
The test target must be able to inject a session facade rather than subclassing
or manufacturing `WCSession`. Planned coverage: reachable/unreachable delivery,
activation/deactivation, phone/watch reinstall or counterpart switch,
duplicate callback suppression, reply timeout, lost reply, invalid containers,
4 KiB payload boundary, millisecond date round trips, and application-context
sanitization.
## Migration and impact
This is additive to contract v1 but introduces the first control-plane wire
format. Existing demo apps remain demos until integration explicitly injects the
live adapter. Mixed installations fail closed on schema mismatch and expose an
update-required/unavailable UI state; they do not fall back to queued commands.
No permission string, background mode, credential path, analytics, raw motion,
or third-party dependency is requested. Physical iPhone/Watch testing remains
required for locked-phone wake, relaunch, counterpart switching, and actual
reachability behavior without Xcode attached.