Establish watch relay contracts and platform package wiring
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user