|
| 1 | +# Message Bus |
| 2 | + |
| 3 | +The SDK's typed publish/subscribe channel for inter-feature communication. Features registered to the same core can exchange strongly-typed values without importing each other. |
| 4 | + |
| 5 | +## Core Protocols |
| 6 | + |
| 7 | +| Protocol | Role | |
| 8 | +|----------|------| |
| 9 | +| `BusMessage` | A value type (struct or enum) carried on the bus. Declares a stable `key`. | |
| 10 | +| `BusMessageReceiver` | A class-bound receiver for one `BusMessage` type. Subscribed by identity. | |
| 11 | +| `MessageBus` | The channel. Subscribe, unsubscribe, send. | |
| 12 | +| `MessageBusSubscription` | Opaque handle returned by the closure-based `subscribe(block:)` API. | |
| 13 | + |
| 14 | +All types live in `DatadogInternal/Sources/MessageBus/MessageBus.swift`. The concrete implementation is `CoreMessageBus` in `DatadogCore/Sources/Core/CoreMessageBus.swift`. |
| 15 | + |
| 16 | +## Subscription Patterns |
| 17 | + |
| 18 | +### Receiver-based (long-lived objects) |
| 19 | + |
| 20 | +Implement `BusMessageReceiver` when the subscriber already has a natural lifecycle (a `Feature`, an instrumentation component). The bus retains the receiver until `unsubscribe` is called. |
| 21 | + |
| 22 | +```swift |
| 23 | +final class MyReceiver: BusMessageReceiver { |
| 24 | + typealias Message = RUMSessionState |
| 25 | + |
| 26 | + func receive(message: RUMSessionState, from core: DatadogCoreProtocol) { |
| 27 | + // handle on the bus's serial queue — do not block |
| 28 | + } |
| 29 | +} |
| 30 | + |
| 31 | +let receiver = MyReceiver() |
| 32 | +core.messageBus.subscribe(receiver: receiver) |
| 33 | +// ... |
| 34 | +core.messageBus.unsubscribe(receiver: receiver) |
| 35 | +``` |
| 36 | + |
| 37 | +Subscribe at feature enable time, typically in the module's `enable(with:in:)` function: |
| 38 | + |
| 39 | +```swift |
| 40 | +// DatadogRUM/Sources/RUM.swift |
| 41 | +core.messageBus.subscribe(receiver: rum.crashReportReceiver) |
| 42 | +core.messageBus.subscribe(receiver: rum.telemetryReceiver) |
| 43 | +``` |
| 44 | + |
| 45 | +### Closure-based (ad-hoc subscriptions) |
| 46 | + |
| 47 | +Use `subscribe(block:)` when no natural receiver object exists. The returned `MessageBusSubscription` owns the subscription — store it for the lifetime you need, then pass it to `unsubscribe(_:)`. |
| 48 | + |
| 49 | +```swift |
| 50 | +var subscriptions: [MessageBusSubscription] = [] |
| 51 | + |
| 52 | +subscriptions += [ |
| 53 | + bus.subscribe { [weak self] (message: RUMViewEvent, _) in |
| 54 | + self?.update(viewEvent: message) |
| 55 | + }, |
| 56 | + bus.subscribe { [weak self] (_: RUMViewReset, _) in |
| 57 | + self?.clearViewEvent() |
| 58 | + }, |
| 59 | +] |
| 60 | + |
| 61 | +// cancel all at teardown |
| 62 | +subscriptions.forEach { bus.unsubscribe($0) } |
| 63 | +``` |
| 64 | + |
| 65 | +`CrashContextCoreProvider` uses this pattern to subscribe to multiple message types on one bus, retaining all handles in a `[MessageBusSubscription]` array. See `DatadogCrashReporting/Sources/CrashContextProvider.swift`. |
| 66 | + |
| 67 | +## Sending Messages |
| 68 | + |
| 69 | +```swift |
| 70 | +// Fire-and-forget — no fallback needed |
| 71 | +core.messageBus.send(message: RUMViewReset()) |
| 72 | + |
| 73 | +// With a fallback when no subscriber is registered |
| 74 | +core.messageBus.send(message: WebViewLogMessage(event: event), else: { |
| 75 | + DD.logger.warn("A WebView log is lost because Logging is disabled in the SDK") |
| 76 | +}) |
| 77 | +``` |
| 78 | + |
| 79 | +`send` is asynchronous — it dispatches on the bus's serial queue. Do not assume the message is delivered by the time `send` returns. |
| 80 | + |
| 81 | +## Supported Messages |
| 82 | + |
| 83 | +The table below lists every `BusMessage` type registered across the SDK. |
| 84 | + |
| 85 | +| Type | Key | Sent by | Consumed by | |
| 86 | +|------|-----|---------|-------------| |
| 87 | +| `DatadogContext` | `"core.context"` | `DatadogCore` (on every context update) | `ContextSharingTransformer`, `NetworkContextCoreProvider`, `WatchdogTerminationMonitor`, `RUMContextReceiver` (SR), `ContextMessageReceiver` (Trace), `CrashContextCoreProvider` | |
| 88 | +| `TelemetryMessage` | `"telemetry"` | Any feature via `core.telemetry.*` | `TelemetryReceiver` (RUM) | |
| 89 | +| `LogMessage` | `"log-message"` | `TracingWithLoggingIntegration` (Trace) | `LogMessageReceiver` (Logs) | |
| 90 | +| `LogEventAttributes` | `"log-event-attributes"` | `Logs.enable` (shared global attributes) | `CrashContextCoreProvider` | |
| 91 | +| `Crash` | `"crash-report"` | `CrashReportSender` (CrashReporting) | `CrashReportReceiver` (RUM) | |
| 92 | +| `RUMViewEvent` | `"rum-view-event"` | `FatalErrorContextNotifier` (RUM) | `CrashContextCoreProvider` | |
| 93 | +| `RUMEventAttributes` | `"rum-event-attributes"` | `FatalErrorContextNotifier` (RUM) | `CrashContextCoreProvider` | |
| 94 | +| `RUMViewReset` | `"rum-view-reset"` | `FatalErrorContextNotifier` (RUM) | `CrashContextCoreProvider` | |
| 95 | +| `RUMSessionState` | `"rum-session-state"` | `FatalErrorContextNotifier` (RUM) | `CrashContextCoreProvider` | |
| 96 | +| `RUMErrorMessage` | `"rum-error"` | `RemoteLogger` (Logs) | `ErrorMessageReceiver` (RUM) | |
| 97 | +| `RUMFlagEvaluationMessage` | `"rum-flag-evaluation"` | `RUMFlagEvaluationReporter` (Flags) | `FlagEvaluationReceiver` (RUM) | |
| 98 | +| `WebViewLogMessage` | `"webview-log"` | `MessageEmitter` (WebViewTracking) | `WebViewLogReceiver` (Logs) | |
| 99 | +| `WebViewRUMMessage` | `"webview-rum"` | `MessageEmitter` (WebViewTracking) | `WebViewEventReceiver` (RUM) | |
| 100 | +| `WebViewRecordMessage` | `"webview-record"` | `MessageEmitter` (WebViewTracking) | `WebViewRecordReceiver` (SR) | |
| 101 | + |
| 102 | +### `TelemetryMessage` — special dispatch |
| 103 | + |
| 104 | +`TelemetryMessage.configuration(...)` is intercepted by `CoreMessageBus` and **not** delivered immediately. The bus accumulates configuration updates and dispatches a single merged `TelemetryMessage.configuration` to subscribers 5 seconds after initialization. All other `TelemetryMessage` variants (`.debug`, `.error`, `.metric`, `.usage`) are delivered normally. |
| 105 | + |
| 106 | +## How to Add a New Message |
| 107 | + |
| 108 | +### 1. Define the message type in `DatadogInternal` |
| 109 | + |
| 110 | +Messages live in `DatadogInternal/Sources/Models/` alongside the domain they belong to. Prefer immutable value types. |
| 111 | + |
| 112 | +```swift |
| 113 | +// DatadogInternal/Sources/Models/MyFeature/MyMessage.swift |
| 114 | +public struct MyMessage: BusMessage { |
| 115 | + public static let key = "my-feature.my-message" // globally unique, namespaced |
| 116 | + |
| 117 | + public let value: String |
| 118 | + |
| 119 | + public init(value: String) { |
| 120 | + self.value = value |
| 121 | + } |
| 122 | +} |
| 123 | +``` |
| 124 | + |
| 125 | +Rules for `key`: |
| 126 | +- Must be **globally unique** across the SDK — check the table above before choosing. |
| 127 | +- Use `"<module>.<purpose>"` format (e.g. `"rum-session-state"`, `"webview-log"`). |
| 128 | +- Treat it as **immutable** after the first release — downstream tooling and crash-context serialization may depend on it. |
| 129 | + |
| 130 | +Add the new file to the `DatadogInternal` Xcode target via the `xcode-file-management` skill. |
| 131 | + |
| 132 | +### 2. Implement a receiver in the consuming feature |
| 133 | + |
| 134 | +```swift |
| 135 | +// DatadogMyFeature/Sources/Feature/MyMessageReceiver.swift |
| 136 | +internal final class MyMessageReceiver: BusMessageReceiver { |
| 137 | + func receive(message: MyMessage, from core: DatadogCoreProtocol) { |
| 138 | + // called on the bus's serial queue — do not block |
| 139 | + } |
| 140 | +} |
| 141 | +``` |
| 142 | + |
| 143 | +### 3. Subscribe at feature enable time |
| 144 | + |
| 145 | +```swift |
| 146 | +// DatadogMyFeature/Sources/MyFeature.swift |
| 147 | +core.messageBus.subscribe(receiver: feature.myMessageReceiver) |
| 148 | +``` |
| 149 | + |
| 150 | +If you need multiple subscriptions from a single object without a natural `BusMessageReceiver` conformance, use the closure-based API and retain the handles (see `CrashContextCoreProvider` for the canonical pattern). |
| 151 | + |
| 152 | +### 4. Send the message from the producing feature |
| 153 | + |
| 154 | +```swift |
| 155 | +core.messageBus.send(message: MyMessage(value: "hello"), else: { |
| 156 | + // invoked if no subscriber is registered |
| 157 | +}) |
| 158 | +``` |
| 159 | + |
| 160 | +### 5. Write tests |
| 161 | + |
| 162 | +- Subscribe to `PassthroughCoreMock.messageBus` in unit tests. |
| 163 | +- Use `core.messageBus.send(message:)` to drive receivers in isolation. |
| 164 | +- Assert side effects via the receiver's internal state or the core mock's recorded events. |
| 165 | + |
| 166 | +See `DatadogInternal/Tests/MessageBus/MessageBusTests.swift` for bus-level tests and `DatadogCrashReporting/Tests/CrashContextCoreProviderTests.swift` for a feature-level example. |
| 167 | + |
| 168 | +## Threading |
| 169 | + |
| 170 | +All delivery runs on the bus's internal serial queue (`com.datadoghq.ios-sdk-message-bus`, QoS `.utility`). Receivers must not block — doing so delays every other subscriber. Move work off the queue immediately if it requires significant computation. |
| 171 | + |
| 172 | +`send` and `subscribe`/`unsubscribe` are safe to call from any thread. |
| 173 | + |
| 174 | +## Subscription Lifetime and Retain Semantics |
| 175 | + |
| 176 | +- `subscribe(receiver:)` — the bus **strongly retains** `receiver`. Call `unsubscribe(receiver:)` at teardown, or the receiver (and anything it captures) will leak. |
| 177 | +- `subscribe(block:)` — the bus retains the internal wrapper. The caller owns the `MessageBusSubscription`; dropping it without calling `unsubscribe(_:)` leaks the subscription. |
| 178 | +- Features must **not** retain the `core` reference passed to `receive(message:from:)` — use it transiently within the call. |
0 commit comments