From 4f1f843578a1c4ffe5f7884cca217e34af90be64 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Fri, 20 Mar 2026 10:08:19 -0300 Subject: [PATCH 1/3] RSH8k2: link remaining RSH3a2b references The note in RSH8k2 mentioned RSH3a2b three times but only linked the first reference. Link the other two for consistency. Co-Authored-By: Claude Opus 4.6 (1M context) --- specifications/features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifications/features.md b/specifications/features.md index e30f80ef1..56381e539 100644 --- a/specifications/features.md +++ b/specifications/features.md @@ -1169,7 +1169,7 @@ The core SDK provides an API for wrapper SDKs to supply Ably with analytics info - `(RSH8)` In platforms that support receiving push notifications, the `device` method on the `RestClient` or `RealtimeClient` interfaces returns an instance of `LocalDevice` that represents the current state of the device in respect of it being a target for push notifications. - `(RSH8k)` `LocalDevice` has the following attributes: - `(RSH8k1)` `deviceIdentityToken` string? -- populated as described in [RSH8c](#RSH8c) - - `(RSH8k2)` `deviceSecret` string -- populated as described in [RSH8b](#RSH8b). (Note: This property being non-nullable is not actually consistent with [`RSH3a2b`](#RSH3a2b); that spec point implies that `id` and `deviceSecret` both start off unset and are only set upon a `CalledActivate` event. However, since `deviceSecret` needs to have the same nullability as `id` --- since per `RSH3a2b` either both or neither should be set --- to reflect the behaviour described in the spec we would have to make `LocalDevice#id` nullable, but this is incompatible with the superclass `DeviceDetails`. In reality, our implementations of `LocalDevice` actually generate `id` and `deviceSecret` when the device is fetched, i.e. not following `RSH3a2b`. What we *should* do is either make `LocalDevice` stop inheriting from `DeviceDetails`, or change the specified behaviour for when to generate `id` and `deviceSecret` to match our implementations, or both. See spec issues [#180](https://github.com/ably/specification/issues/180) and [#25](https://github.com/ably/specification/issues/25). For now, this note exists to reduce confusion.) + - `(RSH8k2)` `deviceSecret` string -- populated as described in [RSH8b](#RSH8b). (Note: This property being non-nullable is not actually consistent with [`RSH3a2b`](#RSH3a2b); that spec point implies that `id` and `deviceSecret` both start off unset and are only set upon a `CalledActivate` event. However, since `deviceSecret` needs to have the same nullability as `id` --- since per [`RSH3a2b`](#RSH3a2b) either both or neither should be set --- to reflect the behaviour described in the spec we would have to make `LocalDevice#id` nullable, but this is incompatible with the superclass `DeviceDetails`. In reality, our implementations of `LocalDevice` actually generate `id` and `deviceSecret` when the device is fetched, i.e. not following [`RSH3a2b`](#RSH3a2b). What we *should* do is either make `LocalDevice` stop inheriting from `DeviceDetails`, or change the specified behaviour for when to generate `id` and `deviceSecret` to match our implementations, or both. See spec issues [#180](https://github.com/ably/specification/issues/180) and [#25](https://github.com/ably/specification/issues/25). For now, this note exists to reduce confusion.) - `(RSH8a)` The `LocalDevice` is initialised when first required, either as a result of a call to `RestClient#device` or `RealtimeClient#device`, or as a result of an operation involving the Activation State Machine. The `LocalDevice` `id`, `clientId`, `deviceSecret` and `deviceIdentityToken` attributes are populated, together with any `recipient`-related attributes, to the extent that they exist, from the persisted state. - `(RSH8b)` The `LocalDevice` `id` and `deviceSecret` attributes are generated, and persisted as part of the `LocalDevice` state, when required by step [`RSH3a2b`](#RSH3a2b) in the Activation State Machine. At that time, the `clientId` attribute is also initialised, if the client is identified according to [`RSA7`](#RSA7). - `(RSH8c)` Following successful registration of a `LocalDevice`, following the procedure in [`RSH3c2a`](#RSH3c2a), the now known `deviceIdentityToken` is set and persisted. From 9871e20fe304bb09fe35ee922e469ee0824cce46 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Fri, 20 Mar 2026 10:16:54 -0300 Subject: [PATCH 2/3] RSH8k2: clarify per-SDK behaviour for id/deviceSecret generation The note previously said "our implementations" all generate eagerly, but this isn't accurate. ably-java follows RSH3a2b (lazy generation on CalledActivate); ably-cocoa and ably-js generate eagerly at device fetch time. Checked against: - ably-cocoa 745e7b7 - ably-java da4c60f - ably-js 17be43e Co-Authored-By: Claude Opus 4.6 (1M context) --- specifications/features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifications/features.md b/specifications/features.md index 56381e539..614fb8e1b 100644 --- a/specifications/features.md +++ b/specifications/features.md @@ -1169,7 +1169,7 @@ The core SDK provides an API for wrapper SDKs to supply Ably with analytics info - `(RSH8)` In platforms that support receiving push notifications, the `device` method on the `RestClient` or `RealtimeClient` interfaces returns an instance of `LocalDevice` that represents the current state of the device in respect of it being a target for push notifications. - `(RSH8k)` `LocalDevice` has the following attributes: - `(RSH8k1)` `deviceIdentityToken` string? -- populated as described in [RSH8c](#RSH8c) - - `(RSH8k2)` `deviceSecret` string -- populated as described in [RSH8b](#RSH8b). (Note: This property being non-nullable is not actually consistent with [`RSH3a2b`](#RSH3a2b); that spec point implies that `id` and `deviceSecret` both start off unset and are only set upon a `CalledActivate` event. However, since `deviceSecret` needs to have the same nullability as `id` --- since per [`RSH3a2b`](#RSH3a2b) either both or neither should be set --- to reflect the behaviour described in the spec we would have to make `LocalDevice#id` nullable, but this is incompatible with the superclass `DeviceDetails`. In reality, our implementations of `LocalDevice` actually generate `id` and `deviceSecret` when the device is fetched, i.e. not following [`RSH3a2b`](#RSH3a2b). What we *should* do is either make `LocalDevice` stop inheriting from `DeviceDetails`, or change the specified behaviour for when to generate `id` and `deviceSecret` to match our implementations, or both. See spec issues [#180](https://github.com/ably/specification/issues/180) and [#25](https://github.com/ably/specification/issues/25). For now, this note exists to reduce confusion.) + - `(RSH8k2)` `deviceSecret` string -- populated as described in [RSH8b](#RSH8b). (Note: This property being non-nullable is not actually consistent with [`RSH3a2b`](#RSH3a2b); that spec point implies that `id` and `deviceSecret` both start off unset and are only set upon a `CalledActivate` event. However, since `deviceSecret` needs to have the same nullability as `id` --- since per [`RSH3a2b`](#RSH3a2b) either both or neither should be set --- to reflect the behaviour described in the spec we would have to make `LocalDevice#id` nullable, but this is incompatible with the superclass `DeviceDetails`. In reality, ably-cocoa and ably-js actually generate `id` and `deviceSecret` when the device is fetched, i.e. not following [`RSH3a2b`](#RSH3a2b). ably-java follows [`RSH3a2b`](#RSH3a2b), allowing `id` to be `null` (this doesn't affect the public API because nullability is not a concept in Java's type system). What we *should* do is either make `LocalDevice` stop inheriting from `DeviceDetails`, or change the specified behaviour for when to generate `id` and `deviceSecret` to match our implementations, or both. See spec issues [#180](https://github.com/ably/specification/issues/180) and [#25](https://github.com/ably/specification/issues/25). For now, this note exists to reduce confusion.) - `(RSH8a)` The `LocalDevice` is initialised when first required, either as a result of a call to `RestClient#device` or `RealtimeClient#device`, or as a result of an operation involving the Activation State Machine. The `LocalDevice` `id`, `clientId`, `deviceSecret` and `deviceIdentityToken` attributes are populated, together with any `recipient`-related attributes, to the extent that they exist, from the persisted state. - `(RSH8b)` The `LocalDevice` `id` and `deviceSecret` attributes are generated, and persisted as part of the `LocalDevice` state, when required by step [`RSH3a2b`](#RSH3a2b) in the Activation State Machine. At that time, the `clientId` attribute is also initialised, if the client is identified according to [`RSA7`](#RSA7). - `(RSH8c)` Following successful registration of a `LocalDevice`, following the procedure in [`RSH3c2a`](#RSH3c2a), the now known `deviceIdentityToken` is set and persisted. From 8ce3ee1df11c1814dd96fceb7ae671c3135e5154 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Fri, 20 Mar 2026 10:27:21 -0300 Subject: [PATCH 3/3] Clear deviceIdentityToken when we fail to load id or deviceSecret Otherwise, the old token will survive and we'll end up trying to validate it with an absent or non-matching device ID in RSH3a2a. Given that most of our SDKs have not yet implemented RSH8j, it seemed like a good moment to also tighten up the language before we start trying to implement it. The spec provides no mechanism for the state machine to perform a state transition outside of the context of receiving an event. So instead of talking about a transition, we make it clear that LocalDevice load failure should always be handled before the state machine is hydrated. Co-Authored-By: Claude Opus 4.6 (1M context) --- specifications/features.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/specifications/features.md b/specifications/features.md index 614fb8e1b..b881951c2 100644 --- a/specifications/features.md +++ b/specifications/features.md @@ -1051,6 +1051,7 @@ The core SDK provides an API for wrapper SDKs to supply Ably with analytics info ### Activation State Machine - `(RSH3)` In platforms that support receiving push notifications, in order to connect the device's push features with Ably's, the library must perform the process described in the following abstract state machine. While this process should be implemented in whatever way better fits the concrete platform, it should be taken into account that its lifetime is that of the *app* that runs it, which outlives that of the `RestClient` instance or (typically) the process running the app. This typically forces some kind of on-disk storage to which the state machine's state must be persisted, so that it can be recovered later by new instances and processes running the app triggered by external events. + - `(RSH3h)` The Activation State Machine is initialised when first required (i.e. when an event first needs to be delivered to it). Initialisation comprises, in order: (1) the `LocalDevice` is initialised per [`RSH8a`](#RSH8a), which may, per [`RSH8a1`](#RSH8a1), discard the persisted Activation State Machine data; (2) the in-memory state machine is then constructed from the persisted Activation State Machine data, or starts in `NotActivated` if no such data is persisted. The state machine must not process any events before this initialisation has completed. - `(RSH3a)` State `NotActivated` (the initial one). - `(RSH3a1)` On event `CalledDeactivate`: - `(RSH3a1a)` This clause has been deleted. It was valid up to and including specification version `3.0.0`. @@ -1170,7 +1171,8 @@ The core SDK provides an API for wrapper SDKs to supply Ably with analytics info - `(RSH8k)` `LocalDevice` has the following attributes: - `(RSH8k1)` `deviceIdentityToken` string? -- populated as described in [RSH8c](#RSH8c) - `(RSH8k2)` `deviceSecret` string -- populated as described in [RSH8b](#RSH8b). (Note: This property being non-nullable is not actually consistent with [`RSH3a2b`](#RSH3a2b); that spec point implies that `id` and `deviceSecret` both start off unset and are only set upon a `CalledActivate` event. However, since `deviceSecret` needs to have the same nullability as `id` --- since per [`RSH3a2b`](#RSH3a2b) either both or neither should be set --- to reflect the behaviour described in the spec we would have to make `LocalDevice#id` nullable, but this is incompatible with the superclass `DeviceDetails`. In reality, ably-cocoa and ably-js actually generate `id` and `deviceSecret` when the device is fetched, i.e. not following [`RSH3a2b`](#RSH3a2b). ably-java follows [`RSH3a2b`](#RSH3a2b), allowing `id` to be `null` (this doesn't affect the public API because nullability is not a concept in Java's type system). What we *should* do is either make `LocalDevice` stop inheriting from `DeviceDetails`, or change the specified behaviour for when to generate `id` and `deviceSecret` to match our implementations, or both. See spec issues [#180](https://github.com/ably/specification/issues/180) and [#25](https://github.com/ably/specification/issues/25). For now, this note exists to reduce confusion.) - - `(RSH8a)` The `LocalDevice` is initialised when first required, either as a result of a call to `RestClient#device` or `RealtimeClient#device`, or as a result of an operation involving the Activation State Machine. The `LocalDevice` `id`, `clientId`, `deviceSecret` and `deviceIdentityToken` attributes are populated, together with any `recipient`-related attributes, to the extent that they exist, from the persisted state. + - `(RSH8a)` The `LocalDevice` is initialised when first required, either as a result of a call to `RestClient#device` or `RealtimeClient#device`, or as a result of the Activation State Machine being initialised (see [`RSH3h`](#RSH3h)). The `LocalDevice` `id`, `clientId`, `deviceSecret` and `deviceIdentityToken` attributes are populated, together with any `recipient`-related attributes, to the extent that they exist, from the persisted state. + - `(RSH8a1)` If loading the `LocalDevice` `id` or `deviceSecret` attributes fails, then: (1) all persisted `LocalDevice` attributes must be discarded; (2) all persisted Activation State Machine data must be discarded — combined with [`RSH3h`](#RSH3h), this ensures that the state machine starts in `NotActivated`. (Non-normative: this clause imposes no requirement on when new `id` and `deviceSecret` attributes are generated; their generation is governed by [`RSH8b`](#RSH8b). SDKs that exhibit the non-conforming eager generation behaviour noted in [`RSH8k2`](#RSH8k2) will instead regenerate them as part of the same `LocalDevice` initialisation.) - `(RSH8b)` The `LocalDevice` `id` and `deviceSecret` attributes are generated, and persisted as part of the `LocalDevice` state, when required by step [`RSH3a2b`](#RSH3a2b) in the Activation State Machine. At that time, the `clientId` attribute is also initialised, if the client is identified according to [`RSA7`](#RSA7). - `(RSH8c)` Following successful registration of a `LocalDevice`, following the procedure in [`RSH3c2a`](#RSH3c2a), the now known `deviceIdentityToken` is set and persisted. - `(RSH8d)` If the `LocalDevice` is created by an unidentified client (see [`RSA7`](#RSA7) ) and therefore has no `clientId` set, but the client subsequently becomes identified (as a result of [`RSA7b2`](#RSA7b2) or [`RSA7b3`](#RSA7b3) ), then the `LocalDevice` `clientId` is set and persisted. @@ -1179,7 +1181,7 @@ The core SDK provides an API for wrapper SDKs to supply Ably with analytics info - `(RSH8g)` Whenever any change arises of the push transport details for local device (eg an FCM registration token update triggered by the platform), a `GotPushDeviceDetails` event is sent to [the state machine](#RSH3). - `(RSH8h)` If an attempt to obtain the push transport details for local device (eg an FCM registration token) fails, a `GettingPushDeviceDetailsFailed` event containing the indicated error is sent to [the state machine](#RSH3). - `(RSH8i)` Each time the library is instantiated, if the LocalDevice has push device details (eg an APNS deviceToken), and if the platform supports it, it must verify the validity of those details (eg by requesting a token from the platform and comparing that with the already-known token). If as a result there are updated details, then an update to the Ably server is triggered by sending a `GotPushDeviceDetails` event to [the state machine](#RSH3). - - `(RSH8j)` If during library initialisation the `LocalDevice` `id` or `deviceSecret` attributes are not able to be loaded then those LocalDevice details must be discarded and the ActivationStateMachine machine should transition to the `NotActivated` state. New `LocalDevice` `id` and `deviceSecret` attributes should be generated on the next activation event. + - `(RSH8j)` This clause has been replaced by [`RSH8a1`](#RSH8a1). ## Types