From 80f2c05b439536da0aa31b67787551095cd31ae4 Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Wed, 23 Jul 2025 16:03:55 +0100 Subject: [PATCH 1/3] Add object-level write API spec for RealtimeObjects Adds spec for: - `RealtimeObjects.createMap` - `RealtimeObjects.createCounter` Resolves PUB-1829 --- textile/objects-features.textile | 115 ++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 2 deletions(-) diff --git a/textile/objects-features.textile b/textile/objects-features.textile index 90908c73..7b3dbc18 100644 --- a/textile/objects-features.textile +++ b/textile/objects-features.textile @@ -20,6 +20,77 @@ h3(#realtime-objects). RealtimeObjects ** @(RTO1b)@ If the channel is in the @DETACHED@ or @FAILED@ state, the library should throw an @ErrorInfo@ error with @statusCode@ 400 and @code@ 90001 ** @(RTO1c)@ Waits for the objects sync sequence to complete and for "RTO5c":#RTO5c to finish ** @(RTO1d)@ Returns the object with id @root@ from the internal @ObjectsPool@ as a @LiveMap@ +* @(RTO11)@ @RealtimeObjects#createMap@ function: +** @(RTO11a)@ Expects the following arguments: +*** @(RTO11a1)@ @entries@ @Dict@ (optional) - the initial entries for the new @LiveMap@ object +** @(RTO11b)@ The return type is a @LiveMap@, which is returned once the required I/O has successfully completed +** @(RTO11c)@ Requires the @OBJECT_PUBLISH@ channel mode to be granted per "RTO2":#RTO2 +** @(RTO11d)@ If the channel is in the @DETACHED@, @FAILED@ or @SUSPENDED@ state, the library should throw an @ErrorInfo@ error with @statusCode@ 400 and @code@ 90001 +** @(RTO11e)@ If "@echoMessages@":../features#TO3h client option is @false@, the library should throw an @ErrorInfo@ error with @statusCode@ 400 and @code@ 40000, indicating that @echoMessages@ must be enabled for this operation +** @(RTO11f)@ Creates an @ObjectMessage@ for a @MAP_CREATE@ action in the following way: +*** @(RTO11f1)@ If @entries@ is null or not of type @Dict@, the library should throw an @ErrorInfo@ error with @statusCode@ 400 and @code@ 40003, indicating that @entries@ must be a @Dict@. Note that @entries@ is an optional argument, and if omitted, this error must not be thrown +*** @(RTO11f2)@ If any of the keys provided in @entries@ are not of type @String@, the library should throw an @ErrorInfo@ error with @statusCode@ 400 and @code@ 40003, indicating that keys must be @String@ +*** @(RTO11f3)@ If any of the values provided in @entries@ are not of an expected type, the library should throw an @ErrorInfo@ error with @statusCode@ 400 and @code@ 40013, indicating that such data type is unsupported +*** @(RTO11f4)@ Create a partial @ObjectOperation@ with the initial value for the new @LiveMap@: +**** @(RTO11f4a)@ Set @ObjectOperation.map.semantics@ to @ObjectsMapSemantics.LWW@ +**** @(RTO11f4b)@ Set @ObjectOperation.map.entries@ to an empty map if @entries@ is omitted +**** @(RTO11f4c)@ Otherwise, set @ObjectOperation.map.entries@ based on the provided @entries@. For each key-value pair in @entries@: +***** @(RTO11f4c1)@ Create an @ObjectsMapEntry@ for the current value: +****** @(RTO11f4c1a)@ If the value is of type @LiveCounter@ or @LiveMap@, set @ObjectsMapEntry.data.objectId@ to the @objectId@ of that object +****** @(RTO11f4c1b)@ If the value is of type @JsonArray@ or @JsonObject@, set @ObjectsMapEntry.data.json@ to that value +****** @(RTO11f4c1c)@ If the value is of type @String@, set @ObjectsMapEntry.data.string@ to that value +****** @(RTO11f4c1d)@ If the value is of type @Number@, set @ObjectsMapEntry.data.number@ to that value +****** @(RTO11f4c1e)@ If the value is of type @Boolean@, set @ObjectsMapEntry.data.boolean@ to that value +****** @(RTO11f4c1f)@ If the value is of type @Binary@, set @ObjectsMapEntry.data.bytes@ to that value +***** @(RTO11f4c2)@ Add a new entry to @ObjectOperation.map.entries@ with the current key and the created @ObjectsMapEntry@ as the value +*** @(RTO11f5)@ Create an initial value JSON string as described in "RTO13":#RTO13, passing in the partial @ObjectOperation@ from "RTO11f4":#RTO11f4 +*** @(RTO11f6)@ Create a unique nonce as a random string +*** @(RTO11f7)@ Get the current server time as described in "RTO16":#RTO16 +*** @(RTO11f8)@ Create an @objectId@ for the new @LiveMap@ object as described in "RTO14":#RTO14, passing in @map@ string as the @type@, the initial value JSON string from "RTO11f5":#RTO11f5, the nonce from "RTO11f6":#RTO11f6, and the server time from "RTO11f7":#RTO11f7 +*** @(RTO11f9)@ Set @ObjectMessage.operation.action@ to @ObjectOperationAction.MAP_CREATE@ +*** @(RTO11f10)@ Set @ObjectMessage.operation.objectId@ to the @objectId@ created in "RTO11f8":#RTO11f8 +*** @(RTO11f11)@ Set @ObjectMessage.operation.nonce@ to the nonce value created in "RTO11f6":#RTO11f6 +*** @(RTO11f12)@ Set @ObjectMessage.operation.initialValue@ to the JSON string created in "RTO11f5":#RTO11f5 +*** @(RTO11f13)@ Merge values from the partial @ObjectOperation@ created in "RTO11f4":#RTO11f4 into @ObjectMessage.operation@ +** @(RTO11g)@ Publishes the @ObjectMessage@ from "RTO11f":#RTO11f using "@RealtimeObjects.publish@":#RTO15, passing the @ObjectMessage@ as a single element in the array +*** @(RTO11g1)@ The client library waits for the publish operation I/O to complete. On failure, an error is returned to the caller; on success, the @createMap@ operation continues +** @(RTO11h)@ Returns a @LiveMap@ instance: +*** @(RTO11h1)@ While waiting for the publish operation to complete in "RTO11g1":#RTO11g1, the client library may have already received the echoed @ObjectMessage@ operation, as it could arrive before the @ACK@ for the publish operation. Depending on the threading and/or asynchronous model of the client library, this could mean that the @ObjectMessage@ for the @MAP_CREATE@ operation has already been processed, and the new @LiveMap@ instance already exists in the internal @ObjectsPool@. As such, the following checks are performed to determine whether the instance already exists +*** @(RTO11h2)@ If an object with the @ObjectMessage.operation.objectId@ exists in the internal @ObjectsPool@, return it +*** @(RTO11h3)@ Otherwise, if the object does not exist in the internal @ObjectsPool@: +**** @(RTO11h3a)@ Create a zero-value @LiveMap@ (per "RTLM4":#RTLM4), set its @objectId@ to @ObjectMessage.operation.objectId@, set its @semantics@ to @ObjectMessage.operation.map.semantics@, and merge the initial value as described in "RTLM17":#RTLM17, passing in @ObjectMessage.operation@ +**** @(RTO11h3b)@ Add the created @LiveMap@ instance to the internal @ObjectsPool@ +**** @(RTO11h3c)@ Return the created @LiveMap@ instance +* @(RTO12)@ @RealtimeObjects#createCounter@ function: +** @(RTO12a)@ Expects the following arguments: +*** @(RTO12a1)@ @count@ @Number@ (optional) - the initial count for the new @LiveCounter@ object +** @(RTO12b)@ The return type is a @LiveCounter@, which is returned once the required I/O has successfully completed +** @(RTO12c)@ Requires the @OBJECT_PUBLISH@ channel mode to be granted per "RTO2":#RTO2 +** @(RTO12d)@ If the channel is in the @DETACHED@, @FAILED@ or @SUSPENDED@ state, the library should throw an @ErrorInfo@ error with @statusCode@ 400 and @code@ 90001 +** @(RTO12e)@ If "@echoMessages@":../features#TO3h client option is @false@, the library should throw an @ErrorInfo@ error with @statusCode@ 400 and @code@ 40000, indicating that @echoMessages@ must be enabled for this operation +** @(RTO12f)@ Creates an @ObjectMessage@ for a @COUNTER_CREATE@ action in the following way: +*** @(RTO12f1)@ If @count@ is null, not of type @Number@, or not a finite number, the library should throw an @ErrorInfo@ error with @statusCode@ 400 and @code@ 40003, indicating that @count@ must be a valid number. Note that @count@ is an optional argument, and if omitted, this error must not be thrown +*** @(RTO12f2)@ Create a partial @ObjectOperation@ with the initial value for the new @LiveCounter@: +**** @(RTO12f2a)@ Set @ObjectOperation.counter.count@ to 0 if @count@ is omitted +**** @(RTO12f2b)@ Otherwise, set @ObjectOperation.counter.count@ to the provided @count@ value +*** @(RTO12f3)@ Create an initial value JSON string as described in "RTO13":#RTO13, passing in the partial @ObjectOperation@ from "RTO12f2":#RTO12f2 +*** @(RTO12f4)@ Create a unique nonce as a random string +*** @(RTO12f5)@ Get the current server time as described in "RTO16":#RTO16 +*** @(RTO12f6)@ Create an @objectId@ for the new @LiveCounter@ object as described in "RTO14":#RTO14, passing in @counter@ string as the @type@, the initial value JSON string from "RTO12f3":#RTO12f3, the nonce from "RTO12f4":#RTO12f4, and the server time from "RTO12f5":#RTO12f5 +*** @(RTO12f7)@ Set @ObjectMessage.operation.action@ to @ObjectOperationAction.COUNTER_CREATE@ +*** @(RTO12f8)@ Set @ObjectMessage.operation.objectId@ to the @objectId@ created in "RTO12f6":#RTO12f6 +*** @(RTO12f9)@ Set @ObjectMessage.operation.nonce@ to the nonce value created in "RTO12f4":#RTO12f4 +*** @(RTO12f10)@ Set @ObjectMessage.operation.initialValue@ to the JSON string created in "RTO12f3":#RTO12f3 +*** @(RTO12f11)@ Merge values from the partial @ObjectOperation@ created in "RTO12f2":#RTO12f2 into @ObjectMessage.operation@ +** @(RTO12g)@ Publishes the @ObjectMessage@ from "RTO12f":#RTO12f using "@RealtimeObjects.publish@":#RTO15, passing the @ObjectMessage@ as a single element in the array +*** @(RTO12g1)@ The client library waits for the publish operation I/O to complete. On failure, an error is returned to the caller; on success, the @createCounter@ operation continues +** @(RTO12h)@ Returns a @LiveCounter@ instance: +*** @(RTO12h1)@ While waiting for the publish operation to complete in "RTO12g1":#RTO12g1, the client library may have already received the echoed @ObjectMessage@ operation, as it could arrive before the @ACK@ for the publish operation. Depending on the threading and/or asynchronous model of the client library, this could mean that the @ObjectMessage@ for the @COUNTER_CREATE@ operation has already been processed, and the new @LiveCounter@ instance already exists in the internal @ObjectsPool@. As such, the following checks are performed to determine whether the instance already exists +*** @(RTO12h2)@ If an object with the @ObjectMessage.operation.objectId@ exists in the internal @ObjectsPool@, return it +*** @(RTO12h3)@ Otherwise, if the object does not exist in the internal @ObjectsPool@: +**** @(RTO12h3a)@ Create a zero-value @LiveCounter@ (per "RTLC4":#RTLC4), set its @objectId@ to @ObjectMessage.operation.objectId@, and merge the initial value as described in "RTLC10":#RTLC10, passing in @ObjectMessage.operation@ +**** @(RTO12h3b)@ Add the created @LiveCounter@ instance to the internal @ObjectsPool@ +**** @(RTO12h3c)@ Return the created @LiveCounter@ instance * @(RTO2)@ Certain object operations may require a specific channel mode to be set on a channel in order to be performed. If a specific channel mode is required by an operation, then: ** @(RTO2a)@ If the channel is in the @ATTACHED@ state, the presence of the required channel mode is checked against the set of channel modes granted by the server per "RTL4m":../features#RTL4m : *** @(RTO2a1)@ If the channel mode is in the set, the operation is allowed @@ -71,7 +142,7 @@ h3(#realtime-objects). RealtimeObjects * @(RTO6)@ Certain object operations may require creating a zero-value object if one does not already exist in the internal @ObjectsPool@ for the given @objectId@. This can be done as follows: ** @(RTO6a)@ If an object with @objectId@ exists in @ObjectsPool@, do not create a new object ** @(RTO6b)@ The expected type of the object can be inferred from the provided @objectId@: -*** @(RTO6b1)@ Split the @objectId@ (formatted as @type:hash@timestamp@) on the separator @:@ and parse the first part as the type string +*** @(RTO6b1)@ Split the @objectId@ (formatted as @[type]:[hash]@[timestamp]@, see "RTO14c":#RTO14c) on the separator @:@ and parse the first part as the type string *** @(RTO6b2)@ If the parsed type is @map@, create a zero-value @LiveMap@ per "RTLM4":#RTLM4 in the @ObjectsPool@ *** @(RTO6b3)@ If the parsed type is @counter@, create a zero-value @LiveCounter@ per "RTLC4":#RTLC4 in the @ObjectsPool@ * @(RTO7)@ The client library may receive @OBJECT@ @ProtocolMessages@ in realtime over the channel concurrently with @OBJECT_SYNC@ @ProtocolMessages@ during the object sync sequence ("RTO5":#RTO5). Some of the incoming @OBJECT@ messages may have already been applied to the objects described in the sync sequence, while others may not. Therefore, the client must buffer @OBJECT@ messages during the sync sequence so that it can determine which of them should be applied to the objects once the sync is complete. See "RTO8":#RTO8 @@ -99,6 +170,35 @@ h3(#realtime-objects). RealtimeObjects *** @(RTO10c1)@ For each @LiveObject@ in the @ObjectsPool@: **** @(RTO10c1a)@ Check if the @LiveObject@ needs to release any resources, see "RTLM19":#RTLM19 **** @(RTO10c1b)@ If @LiveObject.isTombstone@ is @true@, and the difference between the current time and @LiveObject.tombstonedAt@ is greater than or equal to the "grace period":#RTO10b, remove the object from the @ObjectsPool@ and release resources for the corresponding object entity to allow it to be garbage collected +* @(RTO13)@ The initial value JSON string can be created from a partial @ObjectOperation@ in the following way: +** @(RTO13a)@ Expects the following arguments: +*** @(RTO13a1)@ @ObjectOperation@ +** @(RTO13b)@ The @ObjectOperation@ may contain user-provided @ObjectData@ that requires encoding. For example, binary data must be encoded to ensure it can be correctly represented in a JSON string and parsed by the Ably server. Therefore, create an encoded instance of @ObjectOperation@ using the same encoding procedure described for the @ObjectMessage@ in "OM4":../features#OM4 +** @(RTO13c)@ Return a JSON string representation of the encoded @ObjectOperation@ +* @(RTO14)@ An Object ID can be created in the client library for a new @LiveObject@ instance in the following way: +** @(RTO14a)@ Expects the following arguments: +*** @(RTO14a1)@ @type@ @String@ - the type of object this Object ID is generated for. Must be one of @map@ or @counter@ +*** @(RTO14a2)@ @initialValue@ @String@ - a JSON string representation of the initial value for the object, based on the encoded @ObjectOperation@. This protects against Object IDs being reused for create operations with differing content +*** @(RTO14a3)@ @nonce@ @String@ - a random string to ensure uniqueness across clients +*** @(RTO14a4)@ @timestamp@ @Time@ - the current server time. This protects against Object IDs being reused across time +** @(RTO14b)@ Generate a @hash@ string for the Object ID: +*** @(RTO14b1)@ Generate a SHA-256 digest from a UTF-8 encoded string in the format @[initialValue]:[nonce]@ +*** @(RTO14b2)@ Base64URL-encode the generated digest. This must follow the URL-safe Base64 encoding as described in "RFC 4648 s.5":https://datatracker.ietf.org/doc/html/rfc4648#section-5, not standard Base64 encoding +** @(RTO14c)@ Return an Object ID in the format @[type]:[hash]@[timestamp]@, where @timestamp@ is represented as milliseconds since the epoch +* @(RTO15)@ Internal @RealtimeObjects.publish@ function: +** @(RTO15a)@ Expects the following arguments: +*** @(RTO15a1)@ @ObjectMessage[]@ - an array of @ObjectMessage@ to be published on a channel +** @(RTO15b)@ Must adhere to the same connection and channel state conditions as message publishing, see "RTL6c":../features#RTL6c +** @(RTO15c)@ Must encode the provided @ObjectMessages@ as described in "OM4":../features#OM4 +** @(RTO15d)@ Should validate that the total size of the encoded @ObjectMessages@, calculated as per "OM3":../features#OM3, does not exceed "@maxMessageSize@":#TO3l8. If it does, the client library must reject the publish and throw an @ErrorInfo@ error with @statusCode@ 400 and @code@ 40009 +** @(RTO15e)@ Must construct the following @ProtocolMessage@: +*** @(RTO15e1)@ Set @ProtocolMessage.action@ to @OBJECT@ +*** @(RTO15e2)@ Set @ProtocolMessage.channel@ to the channel name +*** @(RTO15e3)@ Set @ProtocolMessage.state@ to the encoded @ObjectMessages@ +** @(RTO15f)@ Must send the @ProtocolMessage@ to the connection +** @(RTO15g)@ Must indicate success or failure of the publish (once @ACKed@ or @NACKed@) in the same way as @RealtimeChannel#publish@ +* @(RTO16)@ Server time can be retrieved using "@RestClient#time@":../features#RSC16 +** @(RTO16a)@ The server time offset can be persisted by the client library and used to calculate the server time without making a request, in a similar way to how it is described in "RSA10k":../features#RSA10k. The persisted offset from either operation can be used interchangeably h3(#liveobject). LiveObject @@ -176,6 +276,8 @@ h3(#livecounter). LiveCounter ** @(RTLC5a)@ Requires the @OBJECT_SUBSCRIBE@ channel mode to be granted per "RTO2":#RTO2 ** @(RTLC5b)@ If the channel is in the @DETACHED@ or @FAILED@ state, the library should throw an @ErrorInfo@ error with @statusCode@ 400 and @code@ 90001 ** @(RTLC5c)@ Returns the current @data@ value +* @(RTLC12)@ TODO: @LiveCounter#increment@ function: +* @(RTLC13)@ TODO: @LiveCounter#decrement@ function: * @(RTLC6)@ @LiveCounter@'s internal @data@ can be replaced with the provided @ObjectState@ in the following way: ** @(RTLC6a)@ Replace the private @siteTimeserials@ of the @LiveCounter@ with the value from @ObjectState.siteTimeserials@ ** @(RTLC6e)@ If @LiveCounter.isTombstone@ is @true@, finish processing the @ObjectState@ @@ -269,6 +371,8 @@ h3(#livemap). LiveMap * @(RTLM13)@ @LiveMap#values@: ** @(RTLM13a)@ A method or property, depending on what is more idiomatic for the platform to use for a Map/Dictionary interface. For example, in JavaScript, this is a method similar to @Map.values()@ for the native @Map@ class ** @(RTLM13b)@ The implementation is identical to @LiveMap#entries@, except that it returns only the values from the internal @data@ map +* @(RTLM20)@ TODO: @LiveMap#set@ function: +* @(RTLM21)@ TODO: @LiveMap#remove@ function: * @(RTLM14)@ An @ObjectsMapEntry@ in the internal @data@ map can be checked for being tombstoned using the convenience method: ** @(RTLM14a)@ The method returns true if @ObjectsMapEntry.tombstone@ is true ** @(RTLM14c)@ The method returns true if @ObjectsMapEntry.data.objectId@ exists, there is an object in the local @ObjectsPool@ with that id, and that @LiveObject.isTombstone@ property is @true@ @@ -306,7 +410,7 @@ h3(#livemap). LiveMap **** @(RTLM15d5a)@ Emit a @LiveMapUpdate@ object with @LiveMapUpdate.update@ consisting of entries for the keys that were removed as a result of applying the @OBJECT_DELETE@ operation, each set to @removed@ *** @(RTLM15d4)@ Otherwise, log a warning that an object operation message with an unsupported action has been received, and discard the current @ObjectMessage@ without taking any further action. No data update event is emitted * @(RTLM16)@ A @MAP_CREATE@ operation can be applied to a @LiveMap@ in the following way: -** @(RTLM16a)@ Expects the following argument: +** @(RTLM16a)@ Expects the following arguments: *** @(RTLM16a1)@ @ObjectOperation@ ** @(RTLM16e)@ The return type is a @LiveMapUpdate@ object, which indicates the data update for this @LiveMap@ ** @(RTLM16b)@ If the private flag @createOperationIsMerged@ is @true@, log a debug or trace message indicating that the operation will not be applied because a @MAP_CREATE@ operation has already been applied to this @LiveMap@. Discard the operation without taking any further action, and return a @LiveMapUpdate@ object with @LiveMapUpdate.noop@ set to @true@, indicating that no update was made to the object @@ -378,6 +482,9 @@ Types and their properties/methods are public and exposed to users by default. A
 class RealtimeObjects: // RTO*
   getRoot() => io LiveMap // RTO1
+  createMap(Dict entries?) => io LiveMap // RTO11
+  createCounter(Number count?) => io LiveCounter // RTO12
+  publish(ObjectMessage[]) => io // RTO15, internal
 
 class LiveObject: // RTLO*
   objectId: String // RTLO3a, internal
@@ -400,6 +507,8 @@ interface LiveObjectUpdate: // RTLO4b4
 
 class LiveCounter extends LiveObject: // RTLC*, RTLC1
   value() -> Number // RTLC5
+  increment(Number) => io // RTLC12
+  decrement(Number) => io // RTLC13
 
 interface LiveCounterUpdate extends LiveObjectUpdate: // RTLC11, RTLC11a
   update: { amount: Number } // RTLC11b, RTLC11b1
@@ -410,6 +519,8 @@ class LiveMap extends LiveObject: // RTLM*, RTLM1
   entries() -> [String, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap)?][] // RTLM11
   keys() -> String[] // RTLM12
   values() -> (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap)?[] // RTLM13
+  set(String, Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap) => io // RTLM20
+  remove(String) => io // RTLM21
 
 interface LiveMapUpdate extends LiveObjectUpdate: // RTLM18, RTLM18a
   update: Dict // RTLM18b

From 88342ebae356781f37b86c73dfc7ee35f62012a2 Mon Sep 17 00:00:00 2001
From: Andrew Bulat 
Date: Fri, 25 Jul 2025 04:40:03 +0100
Subject: [PATCH 2/3] Add object-level write API spec for RealtimeObjects

Adds spec for:
- `LiveCounter.increment`
- `LiveCounter.decrement`
- `LiveMap.set`
- `LiveMap.remove`

Resolves PUB-1829
---
 textile/objects-features.textile | 67 ++++++++++++++++++++++++++------
 1 file changed, 56 insertions(+), 11 deletions(-)

diff --git a/textile/objects-features.textile b/textile/objects-features.textile
index 7b3dbc18..0b7f0dc4 100644
--- a/textile/objects-features.textile
+++ b/textile/objects-features.textile
@@ -52,7 +52,7 @@ h3(#realtime-objects). RealtimeObjects
 *** @(RTO11f11)@ Set @ObjectMessage.operation.nonce@ to the nonce value created in "RTO11f6":#RTO11f6
 *** @(RTO11f12)@ Set @ObjectMessage.operation.initialValue@ to the JSON string created in "RTO11f5":#RTO11f5
 *** @(RTO11f13)@ Merge values from the partial @ObjectOperation@ created in "RTO11f4":#RTO11f4 into @ObjectMessage.operation@
-** @(RTO11g)@ Publishes the @ObjectMessage@ from "RTO11f":#RTO11f using "@RealtimeObjects.publish@":#RTO15, passing the @ObjectMessage@ as a single element in the array
+** @(RTO11g)@ Publishes the @ObjectMessage@ from "RTO11f":#RTO11f using "@RealtimeObjects#publish@":#RTO15, passing the @ObjectMessage@ as a single element in the array
 *** @(RTO11g1)@ The client library waits for the publish operation I/O to complete. On failure, an error is returned to the caller; on success, the @createMap@ operation continues
 ** @(RTO11h)@ Returns a @LiveMap@ instance:
 *** @(RTO11h1)@ While waiting for the publish operation to complete in "RTO11g1":#RTO11g1, the client library may have already received the echoed @ObjectMessage@ operation, as it could arrive before the @ACK@ for the publish operation. Depending on the threading and/or asynchronous model of the client library, this could mean that the @ObjectMessage@ for the @MAP_CREATE@ operation has already been processed, and the new @LiveMap@ instance already exists in the internal @ObjectsPool@. As such, the following checks are performed to determine whether the instance already exists
@@ -82,7 +82,7 @@ h3(#realtime-objects). RealtimeObjects
 *** @(RTO12f9)@ Set @ObjectMessage.operation.nonce@ to the nonce value created in "RTO12f4":#RTO12f4
 *** @(RTO12f10)@ Set @ObjectMessage.operation.initialValue@ to the JSON string created in "RTO12f3":#RTO12f3
 *** @(RTO12f11)@ Merge values from the partial @ObjectOperation@ created in "RTO12f2":#RTO12f2 into @ObjectMessage.operation@
-** @(RTO12g)@ Publishes the @ObjectMessage@ from "RTO12f":#RTO12f using "@RealtimeObjects.publish@":#RTO15, passing the @ObjectMessage@ as a single element in the array
+** @(RTO12g)@ Publishes the @ObjectMessage@ from "RTO12f":#RTO12f using "@RealtimeObjects#publish@":#RTO15, passing the @ObjectMessage@ as a single element in the array
 *** @(RTO12g1)@ The client library waits for the publish operation I/O to complete. On failure, an error is returned to the caller; on success, the @createCounter@ operation continues
 ** @(RTO12h)@ Returns a @LiveCounter@ instance:
 *** @(RTO12h1)@ While waiting for the publish operation to complete in "RTO12g1":#RTO12g1, the client library may have already received the echoed @ObjectMessage@ operation, as it could arrive before the @ACK@ for the publish operation. Depending on the threading and/or asynchronous model of the client library, this could mean that the @ObjectMessage@ for the @COUNTER_CREATE@ operation has already been processed, and the new @LiveCounter@ instance already exists in the internal @ObjectsPool@. As such, the following checks are performed to determine whether the instance already exists
@@ -185,7 +185,7 @@ h3(#realtime-objects). RealtimeObjects
 *** @(RTO14b1)@ Generate a SHA-256 digest from a UTF-8 encoded string in the format @[initialValue]:[nonce]@
 *** @(RTO14b2)@ Base64URL-encode the generated digest. This must follow the URL-safe Base64 encoding as described in "RFC 4648 s.5":https://datatracker.ietf.org/doc/html/rfc4648#section-5, not standard Base64 encoding
 ** @(RTO14c)@ Return an Object ID in the format @[type]:[hash]@[timestamp]@, where @timestamp@ is represented as milliseconds since the epoch
-* @(RTO15)@ Internal @RealtimeObjects.publish@ function:
+* @(RTO15)@ Internal @RealtimeObjects#publish@ function:
 ** @(RTO15a)@ Expects the following arguments:
 *** @(RTO15a1)@ @ObjectMessage[]@ - an array of @ObjectMessage@ to be published on a channel
 ** @(RTO15b)@ Must adhere to the same connection and channel state conditions as message publishing, see "RTL6c":../features#RTL6c
@@ -276,8 +276,23 @@ h3(#livecounter). LiveCounter
 ** @(RTLC5a)@ Requires the @OBJECT_SUBSCRIBE@ channel mode to be granted per "RTO2":#RTO2
 ** @(RTLC5b)@ If the channel is in the @DETACHED@ or @FAILED@ state, the library should throw an @ErrorInfo@ error with @statusCode@ 400 and @code@ 90001
 ** @(RTLC5c)@ Returns the current @data@ value
-* @(RTLC12)@ TODO: @LiveCounter#increment@ function:
-* @(RTLC13)@ TODO: @LiveCounter#decrement@ function:
+* @(RTLC12)@ @LiveCounter#increment@ function:
+** @(RTLC12a)@ Expects the following arguments:
+*** @(RTLC12a1)@ @amount@ @Number@ - the amount by which to increment the counter value
+** @(RTLC12b)@ Requires the @OBJECT_PUBLISH@ channel mode to be granted per "RTO2":#RTO2
+** @(RTLC12c)@ If the channel is in the @DETACHED@, @FAILED@ or @SUSPENDED@ state, the library should throw an @ErrorInfo@ error with @statusCode@ 400 and @code@ 90001
+** @(RTLC12d)@ If "@echoMessages@":../features#TO3h client option is @false@, the library should throw an @ErrorInfo@ error with @statusCode@ 400 and @code@ 40000, indicating that @echoMessages@ must be enabled for this operation
+** @(RTLC12e)@ Creates an @ObjectMessage@ for a @COUNTER_INC@ action in the following way:
+*** @(RTLC12e1)@ If @amount@ is null, not of type @Number@, not a finite number, or omitted, the library should throw an @ErrorInfo@ error with @statusCode@ 400 and @code@ 40003, indicating that @amount@ must be a valid number
+*** @(RTLC12e2)@ Set @ObjectMessage.operation.action@ to @ObjectOperationAction.COUNTER_INC@
+*** @(RTLC12e3)@ Set @ObjectMessage.operation.objectId@ to the Object ID of this @LiveCounter@
+*** @(RTLC12e4)@ Set @ObjectMessage.operation.counterOp.amount@ to the provided @amount@ value
+** @(RTLC12f)@ Publishes the @ObjectMessage@ from "RTLC12e":#RTLC12e using "@RealtimeObjects#publish@":#RTO15, passing the @ObjectMessage@ as a single element in the array
+* @(RTLC13)@ @LiveCounter#decrement@ function:
+** @(RTLC13a)@ Expects the following arguments:
+*** @(RTLC13a1)@ @amount@ @Number@ - the amount by which to decrement the counter value
+** @(RTLC13b)@ This is an alias for calling "@LiveCounter#increment@":#RTLC12 with a negative @amount@ and must be implemented with the same behavior
+** @(RTLC13c)@ If the client library chooses to delegate to @LiveCounter#increment@ with a negated @amount@, then in languages where negating a non-number may result in implicit type coercion, the @amount@ argument must first be validated as described in "RTLC12e1":#RTLC12e1 before proceeding
 * @(RTLC6)@ @LiveCounter@'s internal @data@ can be replaced with the provided @ObjectState@ in the following way:
 ** @(RTLC6a)@ Replace the private @siteTimeserials@ of the @LiveCounter@ with the value from @ObjectState.siteTimeserials@
 ** @(RTLC6e)@ If @LiveCounter.isTombstone@ is @true@, finish processing the @ObjectState@
@@ -371,8 +386,38 @@ h3(#livemap). LiveMap
 * @(RTLM13)@ @LiveMap#values@:
 ** @(RTLM13a)@ A method or property, depending on what is more idiomatic for the platform to use for a Map/Dictionary interface. For example, in JavaScript, this is a method similar to @Map.values()@ for the native @Map@ class
 ** @(RTLM13b)@ The implementation is identical to @LiveMap#entries@, except that it returns only the values from the internal @data@ map
-* @(RTLM20)@ TODO: @LiveMap#set@ function:
-* @(RTLM21)@ TODO: @LiveMap#remove@ function:
+* @(RTLM20)@ @LiveMap#set@ function:
+** @(RTLM20a)@ Expects the following arguments:
+*** @(RTLM20a1)@ @key@ @String@ - the key to set the value for
+*** @(RTLM20a2)@ @value@ @Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap@ - the value to assign to the key
+** @(RTLM20b)@ Requires the @OBJECT_PUBLISH@ channel mode to be granted per "RTO2":#RTO2
+** @(RTLM20c)@ If the channel is in the @DETACHED@, @FAILED@ or @SUSPENDED@ state, the library should throw an @ErrorInfo@ error with @statusCode@ 400 and @code@ 90001
+** @(RTLM20d)@ If "@echoMessages@":../features#TO3h client option is @false@, the library should throw an @ErrorInfo@ error with @statusCode@ 400 and @code@ 40000, indicating that @echoMessages@ must be enabled for this operation
+** @(RTLM20e)@ Creates an @ObjectMessage@ for a @MAP_SET@ action in the following way:
+*** @(RTLM20e1)@ Validates the provided @key@ and @value@ in a similar way as described in "RTO11f2":#RTO11f2 and "RTO11f3":#RTO11f3
+*** @(RTLM20e2)@ Set @ObjectMessage.operation.action@ to @ObjectOperationAction.MAP_SET@
+*** @(RTLM20e3)@ Set @ObjectMessage.operation.objectId@ to the Object ID of this @LiveMap@
+*** @(RTLM20e4)@ Set @ObjectMessage.operation.mapOp.key@ to the provided @key@ value
+*** @(RTLM20e5)@ Set @ObjectMessage.operation.mapOp.data@ depending on the type of the provided @value@:
+**** @(RTLM20e5a)@ If the @value@ is of type @LiveCounter@ or @LiveMap@, set @ObjectMessage.operation.mapOp.data.objectId@ to the @objectId@ of that object
+**** @(RTLM20e5b)@ If the @value@ is of type @JsonArray@ or @JsonObject@, set @ObjectMessage.operation.mapOp.data.json@ to that value
+**** @(RTLM20e5c)@ If the @value@ is of type @String@, set @ObjectMessage.operation.mapOp.data.string@ to that value
+**** @(RTLM20e5d)@ If the @value@ is of type @Number@, set @ObjectMessage.operation.mapOp.data.number@ to that value
+**** @(RTLM20e5e)@ If the @value@ is of type @Boolean@, set @ObjectMessage.operation.mapOp.data.boolean@ to that value
+**** @(RTLM20e5f)@ If the @value@ is of type @Binary@, set @ObjectMessage.operation.mapOp.data.bytes@ to that value
+** @(RTLM20f)@ Publishes the @ObjectMessage@ from "RTLM20e":#RTLM20e using "@RealtimeObjects#publish@":#RTO15, passing the @ObjectMessage@ as a single element in the array
+* @(RTLM21)@ @LiveMap#remove@ function:
+** @(RTLM21a)@ Expects the following arguments:
+*** @(RTLM21a1)@ @key@ @String@ - the key to remove the value for
+** @(RTLM21b)@ Requires the @OBJECT_PUBLISH@ channel mode to be granted per "RTO2":#RTO2
+** @(RTLM21c)@ If the channel is in the @DETACHED@, @FAILED@ or @SUSPENDED@ state, the library should throw an @ErrorInfo@ error with @statusCode@ 400 and @code@ 90001
+** @(RTLM21d)@ If "@echoMessages@":../features#TO3h client option is @false@, the library should throw an @ErrorInfo@ error with @statusCode@ 400 and @code@ 40000, indicating that @echoMessages@ must be enabled for this operation
+** @(RTLM21e)@ Creates an @ObjectMessage@ for a @MAP_REMOVE@ action in the following way:
+*** @(RTLM21e1)@ Validates the provided @key@ in a similar way as described in "RTO11f2":#RTO11f2
+*** @(RTLM21e2)@ Set @ObjectMessage.operation.action@ to @ObjectOperationAction.MAP_REMOVE@
+*** @(RTLM21e3)@ Set @ObjectMessage.operation.objectId@ to the Object ID of this @LiveMap@
+*** @(RTLM21e4)@ Set @ObjectMessage.operation.mapOp.key@ to the provided @key@ value
+** @(RTLM21f)@ Publishes the @ObjectMessage@ from "RTLM21e":#RTLM21e using "@RealtimeObjects#publish@":#RTO15, passing the @ObjectMessage@ as a single element in the array
 * @(RTLM14)@ An @ObjectsMapEntry@ in the internal @data@ map can be checked for being tombstoned using the convenience method:
 ** @(RTLM14a)@ The method returns true if @ObjectsMapEntry.tombstone@ is true
 ** @(RTLM14c)@ The method returns true if @ObjectsMapEntry.data.objectId@ exists, there is an object in the local @ObjectsPool@ with that id, and that @LiveObject.isTombstone@ property is @true@
@@ -507,8 +552,8 @@ interface LiveObjectUpdate: // RTLO4b4
 
 class LiveCounter extends LiveObject: // RTLC*, RTLC1
   value() -> Number // RTLC5
-  increment(Number) => io // RTLC12
-  decrement(Number) => io // RTLC13
+  increment(Number amount) => io // RTLC12
+  decrement(Number amount) => io // RTLC13
 
 interface LiveCounterUpdate extends LiveObjectUpdate: // RTLC11, RTLC11a
   update: { amount: Number } // RTLC11b, RTLC11b1
@@ -519,8 +564,8 @@ class LiveMap extends LiveObject: // RTLM*, RTLM1
   entries() -> [String, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap)?][] // RTLM11
   keys() -> String[] // RTLM12
   values() -> (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap)?[] // RTLM13
-  set(String, Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap) => io // RTLM20
-  remove(String) => io // RTLM21
+  set(String key, (Boolean | Binary | Number | String | JsonArray | JsonObject | LiveCounter | LiveMap) value) => io // RTLM20
+  remove(String key) => io // RTLM21
 
 interface LiveMapUpdate extends LiveObjectUpdate: // RTLM18, RTLM18a
   update: Dict // RTLM18b

From 3517453994bc39339d14122dff3ee922644007ec Mon Sep 17 00:00:00 2001
From: Andrew Bulat 
Date: Wed, 3 Sep 2025 17:32:08 +0100
Subject: [PATCH 3/3] Clarify nonce generation for LiveObjects operations

---
 textile/objects-features.textile | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/textile/objects-features.textile b/textile/objects-features.textile
index 0b7f0dc4..44ac6bea 100644
--- a/textile/objects-features.textile
+++ b/textile/objects-features.textile
@@ -44,7 +44,7 @@ h3(#realtime-objects). RealtimeObjects
 ****** @(RTO11f4c1f)@ If the value is of type @Binary@, set @ObjectsMapEntry.data.bytes@ to that value
 ***** @(RTO11f4c2)@ Add a new entry to @ObjectOperation.map.entries@ with the current key and the created @ObjectsMapEntry@ as the value
 *** @(RTO11f5)@ Create an initial value JSON string as described in "RTO13":#RTO13, passing in the partial @ObjectOperation@ from "RTO11f4":#RTO11f4
-*** @(RTO11f6)@ Create a unique nonce as a random string
+*** @(RTO11f6)@ Create a unique string nonce with 16+ characters; the nonce is used to ensure object ID uniqueness across clients
 *** @(RTO11f7)@ Get the current server time as described in "RTO16":#RTO16
 *** @(RTO11f8)@ Create an @objectId@ for the new @LiveMap@ object as described in "RTO14":#RTO14, passing in @map@ string as the @type@, the initial value JSON string from "RTO11f5":#RTO11f5, the nonce from "RTO11f6":#RTO11f6, and the server time from "RTO11f7":#RTO11f7
 *** @(RTO11f9)@ Set @ObjectMessage.operation.action@ to @ObjectOperationAction.MAP_CREATE@
@@ -74,7 +74,7 @@ h3(#realtime-objects). RealtimeObjects
 **** @(RTO12f2a)@ Set @ObjectOperation.counter.count@ to 0 if @count@ is omitted
 **** @(RTO12f2b)@ Otherwise, set @ObjectOperation.counter.count@ to the provided @count@ value
 *** @(RTO12f3)@ Create an initial value JSON string as described in "RTO13":#RTO13, passing in the partial @ObjectOperation@ from "RTO12f2":#RTO12f2
-*** @(RTO12f4)@ Create a unique nonce as a random string
+*** @(RTO12f4)@ Create a unique string nonce with 16+ characters; the nonce is used to ensure object ID uniqueness across clients
 *** @(RTO12f5)@ Get the current server time as described in "RTO16":#RTO16
 *** @(RTO12f6)@ Create an @objectId@ for the new @LiveCounter@ object as described in "RTO14":#RTO14, passing in @counter@ string as the @type@, the initial value JSON string from "RTO12f3":#RTO12f3, the nonce from "RTO12f4":#RTO12f4, and the server time from "RTO12f5":#RTO12f5
 *** @(RTO12f7)@ Set @ObjectMessage.operation.action@ to @ObjectOperationAction.COUNTER_CREATE@