diff --git a/specifications/features.md b/specifications/features.md index 0282218b..916f08d5 100644 --- a/specifications/features.md +++ b/specifications/features.md @@ -939,7 +939,7 @@ The threading and/or asynchronous model for each realtime library will vary by l ### RealtimeObject {#realtime-objects} -Reserved for `RealtimeObject` feature specification, see [objects-features](../objects-features). Reserved spec points: `RTO`, `RTLO`, `RTLC`, `RTLM`, `RTPO`, `RTINS`, `RTLCV`, `RTLMV` +Reserved for `RealtimeObject` feature specification, see [objects-features](../objects-features). Reserved spec points: `RTO`, `RTLO`, `RTLC`, `RTLM`, `RTPO`, `RTINS`, `RTLCV`, `RTLMV`, `RTBC` ### RealtimeAnnotations {#realtime-annotations} diff --git a/specifications/objects-features.md b/specifications/objects-features.md index dbd63b5f..337e0b09 100644 --- a/specifications/objects-features.md +++ b/specifications/objects-features.md @@ -994,6 +994,14 @@ A `PathObject` is obtained from `RealtimeObject#get` ([RTO23](#RTO23)), which re - `(RTPO19e2)` `message` `PublicAPI::ObjectMessage` (optional) - if `LiveObjectUpdate.objectMessage` from the [RTLO4b4](#RTLO4b4) emission that triggered this event is populated and its `operation` field is populated, a `PublicAPI::ObjectMessage` ([PAOM1](#PAOM1)) derived from it per [PAOM3](#PAOM3); otherwise omitted - `(RTPO19f)` Adds a subscription to the `RealtimeObject`'s `PathObjectSubscriptionRegister` ([RTO24](#RTO24)) with subscribed path equal to this `PathObject`'s `path` (per [RTPO2a](#RTPO2a)), the provided `listener`, and the provided `options.depth` - `(RTPO19g)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status +- `(RTPO20)` `PathObject#batch` function: + - `(RTPO20a)` Expects a synchronous function `fn` that receives a `BatchContext` as its argument + - `(RTPO20b)` Checks the write API preconditions per [RTO26](#RTO26) + - `(RTPO20c)` Resolves the path to a `LiveObject` using the internal path resolution procedure. If the path does not resolve to a `LiveObject`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 + - `(RTPO20d)` Creates a `RootBatchContext` ([RTBC16](#RTBC16)) wrapping the resolved `Instance` + - `(RTPO20e)` Executes `fn`, passing the `BatchContext` as argument + - `(RTPO20f)` After `fn` returns, flushes the `RootBatchContext` ([RTBC16d](#RTBC16d)) to publish all queued operations atomically + - `(RTPO20g)` The `RootBatchContext` is closed after flush completes, regardless of success or failure ### Instance @@ -1076,6 +1084,14 @@ An `Instance` holds a direct reference to a specific resolved `LiveObject` or pr - `(RTINS16f)` Returns a [`Subscription`](../features#SUB1) object - `(RTINS16g)` The subscription is identity-based: it follows the specific `LiveObject` instance, regardless of where it sits in the graph - `(RTINS16h)` This operation must not have any side effects on `RealtimeObject`, the underlying channel, or their status +- `(RTINS17)` `Instance#batch` function: + - `(RTINS17a)` Expects a synchronous function `fn` that receives a `BatchContext` as its argument + - `(RTINS17b)` Checks the write API preconditions per [RTO26](#RTO26) + - `(RTINS17c)` If the wrapped value is not a `LiveObject`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 + - `(RTINS17d)` Creates a `RootBatchContext` ([RTBC16](#RTBC16)) wrapping this `Instance` + - `(RTINS17e)` Executes `fn`, passing the `BatchContext` as argument + - `(RTINS17f)` After `fn` returns, flushes the `RootBatchContext` ([RTBC16d](#RTBC16d)) to publish all queued operations atomically + - `(RTINS17g)` The `RootBatchContext` is closed after flush completes, regardless of success or failure ### PublicAPI::ObjectMessage @@ -1122,6 +1138,88 @@ An `Instance` holds a direct reference to a specific resolved `LiveObject` or pr - `(PAOOP3c2)` Else if `counterCreateWithObjectId` ([OOP3q](../features#OOP3q)) is present on the source, set `counterCreate` to the `CounterCreate` from which it was derived (retained per [RTLCV4g5](#RTLCV4g5)) - `(PAOOP3c3)` Otherwise omit `counterCreate` +### BatchContext + +A `BatchContext` wraps an `Instance` with synchronous write methods that queue operations instead of sending them immediately. All queued operations are published atomically as a single channel message when the batch function returns. This allows multiple mutations to be grouped into a single publish call. + +- `(RTBC1)` The `BatchContext` class provides a synchronous write interface for grouping multiple mutations into a single atomic publish + - `(RTBC1a)` A specific SDK implementation may choose to expose a subset of the methods available on the `BatchContext` class based on the known underlying type, in a similar manner to `Instance` ([RTINS1a](#RTINS1a)) +- `(RTBC2)` `BatchContext` has the following internal properties: + - `(RTBC2a)` `instance` - the underlying `Instance` that this `BatchContext` wraps + - `(RTBC2b)` `rootContext` - a reference to the `RootBatchContext` ([RTBC16](#RTBC16)) that manages the batch operation +- `(RTBC3)` `BatchContext#id` property: + - `(RTBC3a)` Returns the `objectId` of the underlying `Instance` ([RTINS3](#RTINS3)) + - `(RTBC3b)` If the batch is closed, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 40000, indicating that the batch is closed +- `(RTBC4)` `BatchContext#get` function: + - `(RTBC4a)` Expects a `key` `String` argument + - `(RTBC4b)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTBC4c)` If the batch is closed, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 40000 + - `(RTBC4d)` Delegates to `Instance#get` ([RTINS5](#RTINS5)) on the underlying `Instance`. If the result is undefined, returns undefined + - `(RTBC4e)` Otherwise, wraps the resulting `Instance` in a `BatchContext` via the `RootBatchContext#wrapInstance` ([RTBC16c](#RTBC16c)) and returns it +- `(RTBC5)` `BatchContext#value` function: + - `(RTBC5a)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTBC5b)` If the batch is closed, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 40000 + - `(RTBC5c)` Delegates to `Instance#value` ([RTINS4](#RTINS4)) on the underlying `Instance` +- `(RTBC6)` `BatchContext#entries` function: + - `(RTBC6a)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTBC6b)` If the batch is closed, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 40000 + - `(RTBC6c)` Delegates to `Instance#entries` ([RTINS6](#RTINS6)) on the underlying `Instance`, wrapping each yielded `Instance` value in a `BatchContext` via `RootBatchContext#wrapInstance` ([RTBC16c](#RTBC16c)) + - `(RTBC6d)` Yields `[String, BatchContext]` pairs +- `(RTBC7)` `BatchContext#keys` function: + - `(RTBC7a)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTBC7b)` If the batch is closed, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 40000 + - `(RTBC7c)` Delegates to `Instance#keys` ([RTINS7](#RTINS7)) on the underlying `Instance` +- `(RTBC8)` `BatchContext#values` function: + - `(RTBC8a)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTBC8b)` If the batch is closed, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 40000 + - `(RTBC8c)` Delegates to `Instance#values` ([RTINS8](#RTINS8)) on the underlying `Instance`, wrapping each yielded `Instance` in a `BatchContext` via `RootBatchContext#wrapInstance` ([RTBC16c](#RTBC16c)) +- `(RTBC9)` `BatchContext#size` function: + - `(RTBC9a)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTBC9b)` If the batch is closed, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 40000 + - `(RTBC9c)` Delegates to `Instance#size` ([RTINS9](#RTINS9)) on the underlying `Instance` +- `(RTBC10)` `BatchContext#compact` function: + - `(RTBC10a)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTBC10b)` If the batch is closed, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 40000 + - `(RTBC10c)` Delegates to `Instance#compact` ([RTINS10](#RTINS10)) on the underlying `Instance` +- `(RTBC11)` `BatchContext#compactJson` function: + - `(RTBC11a)` Checks the access API preconditions per [RTO25](#RTO25) + - `(RTBC11b)` If the batch is closed, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 40000 + - `(RTBC11c)` Delegates to `Instance#compactJson` ([RTINS11](#RTINS11)) on the underlying `Instance` +- `(RTBC12)` `BatchContext#set` function. This method is synchronous: + - `(RTBC12a)` Expects the following arguments: + - `(RTBC12a1)` `key` `String` - the key to set the value for + - `(RTBC12a2)` `value` `Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounterValueType | LiveMapValueType` - the value to assign to the key + - `(RTBC12b)` Checks the write API preconditions per [RTO26](#RTO26) + - `(RTBC12c)` If the batch is closed, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 40000 + - `(RTBC12d)` If the wrapped value is not a `LiveMap`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 + - `(RTBC12e)` Queues a message constructor on the `RootBatchContext` that, when executed, creates `ObjectMessages` for a `MAP_SET` operation in the same manner as `LiveMap#set` ([RTLM20e](#RTLM20e)) +- `(RTBC13)` `BatchContext#remove` function. This method is synchronous: + - `(RTBC13a)` Expects a `key` `String` argument + - `(RTBC13b)` Checks the write API preconditions per [RTO26](#RTO26) + - `(RTBC13c)` If the batch is closed, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 40000 + - `(RTBC13d)` If the wrapped value is not a `LiveMap`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 + - `(RTBC13e)` Queues a message constructor on the `RootBatchContext` that, when executed, creates an `ObjectMessage` for a `MAP_REMOVE` operation in the same manner as `LiveMap#remove` ([RTLM21e](#RTLM21e)) +- `(RTBC14)` `BatchContext#increment` function. This method is synchronous: + - `(RTBC14a)` Expects the following arguments: + - `(RTBC14a1)` `amount` `Number` (optional) - the amount by which to increment the counter value. Defaults to 1 + - `(RTBC14b)` Checks the write API preconditions per [RTO26](#RTO26) + - `(RTBC14c)` If the batch is closed, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 40000 + - `(RTBC14d)` If the wrapped value is not a `LiveCounter`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 + - `(RTBC14e)` Queues a message constructor on the `RootBatchContext` that, when executed, creates an `ObjectMessage` for a `COUNTER_INC` operation in the same manner as `LiveCounter#increment` ([RTLC12](#RTLC12)) +- `(RTBC15)` `BatchContext#decrement` function. This method is synchronous: + - `(RTBC15a)` Expects the following arguments: + - `(RTBC15a1)` `amount` `Number` (optional) - the amount by which to decrement the counter value. Defaults to 1 + - `(RTBC15b)` Checks the write API preconditions per [RTO26](#RTO26) + - `(RTBC15c)` If the batch is closed, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 40000 + - `(RTBC15d)` If the wrapped value is not a `LiveCounter`, the library must throw an `ErrorInfo` error with `statusCode` 400 and `code` 92007 + - `(RTBC15e)` Delegates to `BatchContext#increment` ([RTBC14](#RTBC14)) with the negated `amount` +- `(RTBC16)` Internal `RootBatchContext` - manages the lifecycle and message queue for a batch operation: + - `(RTBC16a)` Maintains an internal `wrappedInstances` map that memoizes `BatchContext` wrappers by `objectId` + - `(RTBC16b)` Maintains an internal `queuedMessageConstructors` list of deferred message constructor functions. Some `ObjectMessages` require asynchronous I/O during construction (e.g. generating an `objectId` for nested value types), so message constructors are queued during synchronous batch method calls and executed on flush + - `(RTBC16c)` `wrapInstance` function: wraps an `Instance` in a `BatchContext`. If the `Instance` has an `objectId` and a wrapper for that `objectId` already exists in `wrappedInstances`, the existing wrapper is returned. Otherwise, a new `BatchContext` is created and stored in `wrappedInstances` + - `(RTBC16d)` `flush` function: closes the batch context, executes all queued message constructors, flattens the resulting `ObjectMessages` into a single array, and publishes them using `RealtimeObject#publish` ([RTO15](#RTO15)). If there are no queued messages, no publish is performed + - `(RTBC16e)` After the batch is closed, any method call on the `BatchContext` or its children must throw an `ErrorInfo` error with `statusCode` 400 and `code` 40000, indicating that the batch is closed + ## Interface Definition {#idl} Describes types for RealtimeObject.\ @@ -1250,6 +1348,7 @@ Types and their properties/methods are public and exposed to users by default. A increment(Number amount?) => io // RTPO17 decrement(Number amount?) => io // RTPO18 subscribe((PathObjectSubscriptionEvent) -> listener, PathObjectSubscriptionOptions? options) -> Subscription // RTPO19 + batch(((BatchContext) ->) fn) => io // RTPO20 class Instance: // RTINS* id: String? // RTINS3 @@ -1266,3 +1365,19 @@ Types and their properties/methods are public and exposed to users by default. A increment(Number amount?) => io // RTINS14 decrement(Number amount?) => io // RTINS15 subscribe((InstanceSubscriptionEvent) -> listener) -> Subscription // RTINS16 + batch(((BatchContext) ->) fn) => io // RTINS17 + + class BatchContext: // RTBC* + id: String? // RTBC3 + get(String key) -> BatchContext? // RTBC4 + value() -> (Boolean | Binary | Number | String | JsonArray | JsonObject)? // RTBC5 + entries() -> Iterator<[String, BatchContext]> // RTBC6 + keys() -> Iterator // RTBC7 + values() -> Iterator // RTBC8 + size() -> Number? // RTBC9 + compact() -> Object? // RTBC10 + compactJson() -> Object? // RTBC11 + set(String key, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounterValueType | LiveMapValueType) value) // RTBC12 + remove(String key) // RTBC13 + increment(Number amount?) // RTBC14 + decrement(Number amount?) // RTBC15