Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions specifications/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -1048,6 +1048,9 @@ 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 persisted activation state is only valid if the `LocalDevice` details it depends on are available. When the state machine is initialised, it must load the `LocalDevice` details from persisted state and verify their integrity, before processing any events:
- `(RSH3h1)` If the `LocalDevice` details fail to load — either because the storage reports an error, or because [`RSH8a2`](#RSH8a2) reports an error — then all `LocalDevice` details must be discarded in memory, deleted from persistent storage where possible, and the state machine must start in the `NotActivated` state, regardless of the persisted activation state.
- `(RSH3h2)` If [`RSH8a2a`](#RSH8a2a) indicates that the `deviceIdentityToken` needs to be validated against the server, the state machine must start in the `ValidatingDeviceIdentityToken` state, regardless of the persisted activation state.
- `(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`.
Expand Down Expand Up @@ -1133,6 +1136,12 @@ The core SDK provides an API for wrapper SDKs to supply Ably with analytics info
- `(RSH3g3)` On event `DeregistrationFailed`:
- `(RSH3g3a)` Makes `Push#deactivate` return or call its callback with the error.
- `(RSH3g3b)` Transitions to the previous state, which is either `WaitingForNewPushDeviceDetails` or `AfterRegistrationSyncFailed` (so, in purity, `WaitingForDeregistration` are two separate states, one for each previous state).
- `(RSH3i)` State `ValidatingDeviceIdentityToken` (entered only when legacy ably-cocoa persisted data needs to be validated against the server; see [`RSH3h2`](#RSH3h2)). Implementations that have always had the atomicity mechanism of [`RSH8a2`](#RSH8a2) do not need this state.
- `(RSH3i1)` On event `CalledActivate`:
- `(RSH3i1a)` Makes an asynchronous HTTP GET request to `/push/deviceRegistrations/:deviceId`, authenticating with the `deviceIdentityToken` (via the `X-Ably-DeviceToken` header). This validates that the token is bound to the current device `id`.
- `(RSH3i1b)` If the request succeeds, the (`id`, `deviceSecret`, `deviceIdentityToken`) tuple is re-persisted with the atomicity mechanism of [`RSH8a2`](#RSH8a2). Makes `Push#activate` return or call its callback with no error. Transitions to `WaitingForNewPushDeviceDetails`.
- `(RSH3i1c)` If the server rejects the request (e.g. 401 or 404), the `deviceIdentityToken` is invalid for this device. All `LocalDevice` details must be discarded in memory and deleted from persistent storage where possible. Makes `Push#activate` return or call its callback with the error. Transitions to `NotActivated`. <!-- TODO: specify exact status codes that constitute a rejection vs a transient error --> <!-- TODO: investigate whether it's possible to regenerate the token using the deviceSecret (which we know is valid for this id) instead of discarding the entire registration -->
- `(RSH3i1d)` If the request fails due to a transient error (e.g. network failure), the state machine remains in `ValidatingDeviceIdentityToken`.
- `(RSH4)` When an event is fired and a transition from the current state is not defined for such event, the event is put into a queue. Then, whenever a transition happens, an event is dequeued from the queue. If a transition from the new current state is defined for the dequeued event, such transition happens. If not, the event is put back in its place in the queue. E. g. we're `WaitingForDeregistration`, and an event `CalledActivate` happens. This event will be put in the queue, since there's no transition defined for it. Then, an event `Deregistered` arrives, causing a transition to `NotActivated`. Now we peek the next item on the queue: `CalledActivate`. Because `NotActivated` transitions on `CalledActivate`, the event is consumed and the machine transitions.
- `(RSH5)` Event handling is atomic and sequential: while an event is being handled, the next one should be handled only after the current one has caused a state transition or has been put into the pending events queue.

Expand Down Expand Up @@ -1166,8 +1175,13 @@ 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.)
- `(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.
- `(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 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)` This clause has been replaced by [`RSH3h`](#RSH3h).
- `(RSH8a2)` The `deviceSecret` and `deviceIdentityToken` are each bound to the device `id` for which they were issued and are not valid for any other device `id`. Thus, the (`id`, `deviceSecret`, `deviceIdentityToken`) tuple must be persisted and loaded atomically. If an implementation chooses to store any of the sensitive components of this tuple (i.e. `deviceSecret` or `deviceIdentityToken`) in a more secure storage mechanism (e.g. a platform keychain) than the rest of the `LocalDevice` details, then it must do so in a way that provides a mechanism for checking that the tuple which is loaded is equal to that which was originally persisted. If upon loading the tuple this mechanism indicates that the loaded tuple diverges from that which was saved, it must be treated as a load failure per [`RSH3h1`](#RSH3h1).
- `(RSH8a2a)` The only implementation that predates [`RSH8a2`](#RSH8a2) and for which there may exist legacy persisted data that does not provide the atomicity guarantee is ably-cocoa. It guarantees that if a `deviceSecret` is present then it belongs to the current `id`, but it does not make the same guarantee about the persisted `deviceIdentityToken`, which may have been issued for a previous device `id`. The following spec points describe how to handle legacy ably-cocoa persisted data that does not provide the atomicity guarantee.
- `(RSH8a2a1)` When loading such data, the implementation must check the following invariants: `id` and `deviceSecret` must either both be present or both be absent, and `deviceIdentityToken` must only be present if `id` and `deviceSecret` are also present. A violation must be treated as a load failure per [`RSH3h1`](#RSH3h1).
- `(RSH8a2a2)` If these invariants hold and `id`, `deviceSecret`, and `deviceIdentityToken` are all present, it must indicate to [`RSH3h2`](#RSH3h2) that the `deviceIdentityToken` needs to be validated against the server.
- `(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.
Expand All @@ -1176,7 +1190,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

Expand Down
Loading