Define message-size string accounting as UTF-8 byte length (with explicit string-length exceptions) - #516
Define message-size string accounting as UTF-8 byte length (with explicit string-length exceptions)#516sacOO7 wants to merge 1 commit into
Conversation
…N representations as string length) Disambiguate every message-size string clause in features.md. "length"/"string length" was undefined (UTF-8 bytes vs UTF-16 code units), and the spec was internally contradictory (OM3d string-length vs OD3g byte-length), causing cross-SDK gate divergence for non-ASCII clientIds and map keys. - UTF-8 byte length: OM3f (object clientId), MCR3a1/MST3c/MRM3a/OMP4a1 (all map/operation keys), OD3e (string), OD3g (json; "byte" -> "UTF-8 byte"), TM6f (string data). - Explicit UTF-16 string length: TM6b (object/array data), TM6d and OM3d (extras) reworded from bare "string length" to "string length (the number of UTF-16 code units)". - Canonical umbrella at TO3l8f: a single default (message-size string accounting is UTF-8 byte length, including OD3g) with the two UTF-16 exceptions (extras TM6d/OM3d and object/array data TM6b), matching Ably's published message-size accounting. Supersedes #331.
There was a problem hiding this comment.
Pull request overview
Clarifies the specification’s message-size accounting rules by defining how to measure string sizes (defaulting to UTF-8 byte length) and explicitly documenting the UTF-16 code-unit exceptions, to eliminate cross-SDK divergence and internal inconsistencies.
Changes:
- Defines string
data,clientId, and LiveObjects map key sizing as UTF-8 byte length (e.g.,TM6f,OM3f,MCR3a1,MST3c,MRM3a,OMP4a1,OD3e,OD3g). - Makes the UTF-16 code-unit basis explicit for JSON-stringified
extrasand object/arraydatasizing (TM6b,TM6d,OM3d). - Adds an umbrella rule at
TO3l8fdefining the default and enumerating the exceptions.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| - `(TO3l8d)` This clause has been replaced by [TM6d](#TM6d) | ||
| - `(TO3l8e)` This clause has been replaced by [TM6e](#TM6e) | ||
| - `(TO3l8f)` The size is defined as the sum of all message sizes being published, calculated based on [TM6](#TM6), [TP5](#TP5) and [OM3](#OM3) | ||
| - `(TO3l8f)` The size is defined as the sum of all message sizes being published, calculated based on [TM6](#TM6), [TP5](#TP5) and [OM3](#OM3). Unless a clause explicitly states otherwise, string sizes in message-size accounting are measured as their UTF-8 byte length (this includes the `json` `ObjectData` payload, [OD3g](#OD3g)). The only exceptions are the `extras` property ([TM6d](#TM6d)/[OM3d](#OM3d)) and an `Object` or `Array` `data` property ([TM6b](#TM6b)), whose JSON representations are instead measured by string length (the number of UTF-16 code units), matching the service's published accounting for `extras` |
…a1); align regular-Message extras sizing with the published accounting Message-size accounting now matches Ably's published per-field rule: every plain string field and map key is measured as its UTF-8 byte length, while extras (and JSON-stringified object/array data) keep the documented "string length of its JSON representation" (UTF-16 code units). Sites changed: - ObjectMessage.swift: ObjectsMap.size (OMP4a1) key measurement utf16.count -> utf8.count, so map-state entry keys now match the MapCreate/MapSet/MapRemove operation keys; extras stays utf16.count; the string-measurement comment block is rewritten for the resolved convention. - ARTBaseMessage.m#messageSize (core regular Message): extras changed from UTF-8 byte length to NSString.length (UTF-16 string length) and object/array data from jsonData length (bytes) to jsonString length (UTF-16); clientId and string data stay UTF-8. Tests: +4 non-ASCII ObjectMessage tests (clientIdIsUTF8ByteLength OM3f, mapSetKeyIsUTF8ByteLength MST3c, objectsMapEntryKeyIsUTF8ByteLength OMP4a1, extrasIsUTF16StringLengthOfJSON OM3d); +1 core test test__027__ in UtilitiesTests covering name/clientId/string-data UTF-8 and extras/object-data UTF-16. Spec: ably/specification#516
…(OMP4a1) Message-size accounting matches Ably's published per-field rule: every plain string field and map key is measured as its UTF-8 byte length, while extras keeps the documented "string length of its JSON representation" (UTF-16 code units). Sites changed: - WireObjectMessage.kt: WireObjectsMap.size (OMP4a1) key measurement it.key.length -> it.key.byteSize, so map-state entry keys now match the MapCreate/MapSet/MapRemove operation keys; fixed a duplicated-// comment typo; corrected the WireObjectData json branch comment from OD3e to OD3g; extras keeps gson.toJson(it).length (UTF-16) now with an explanatory comment. Tests: +1 non-ASCII test testObjectMapStateEntryKeyUnicodeSizeIsUtf8 (OMP4a1). Spec: ably/specification#516
| - `(TM6)` The size of the `Message` for [TO3l8](#TO3l8) is calculated as follows: | ||
| - `(TM6a)` The size is the sum of the sizes of the `name`, `data`, `clientId`, and `extras` properties | ||
| - `(TM6b)` The size of an `Object` or `Array` `data` property is its string length after being JSON-stringified | ||
| - `(TM6b)` The size of an `Object` or `Array` `data` property is its string length (the number of UTF-16 code units) after being JSON-stringified |
There was a problem hiding this comment.
Shouldn’t this be all measured in bytes? I’m not sure I understand the logic behind choosing UTF-16/UTF-8 encoding
There was a problem hiding this comment.
It's updated as per https://ably.com/docs/platform/pricing/faqs#concurrent-channels
There was a problem hiding this comment.
PS. More information included in PR description
Problem
The client-side publish-size gate (
RTO15dforObjectMessage,TO3l8/maxMessageSizeforMessage) sums per-field sizes and rejects an over-limit publish before it hits the wire. The spec told SDKs to measure string fields by their "length", but never said whether "length" means UTF-8 bytes or UTF-16 code units. For ASCII these coincide; for non-ASCII they diverge (你= 3 UTF-8 bytes / 1 UTF-16 code unit;😊= 4 UTF-8 bytes / 2 UTF-16 code units).The spec was also internally contradictory:
OM3d(extras): "the string length of its JSON representation" — reads as UTF-16 code units.OD3g(json): "the byte length of its JSON-encoded string representation" — explicitly bytes.OM3f/MCR3a1/MST3c/MRM3a/OMP4a1/OD3e/TM6f: bare "length" — undefined.Because "length" was undefined, the SDKs diverged, and the same map key or
clientIdproduced different gate results across SDKs. Concrete before-state (clientId = "émile", keys ="héllo👍", string data ="你好", extras ={"k":"你"}):clientId(OM3f)émilehéllo👍héllo👍你好{"k":"你"}This surfaces the concrete gate-flip: a boundary
ObjectMessagewhoseclientId/keys push it just over the limit is accepted by js (UTF-16 undercount) but rejected by cocoa/java (UTF-8) — the same publish, two verdicts. Two distinct defects:clientIdand operation keys (UTF-16) while cocoa/java counted them in UTF-8 bytes.MST3c, UTF-8) or via map state (OMP4a1, UTF-16).The authoritative definition already exists in Ably's published billing/limits accounting — How is maximum message size measured? — verbatim:
Note the deliberate asymmetry in the published contract:
name/clientId/stringdataare UTF-8 bytes, butextrasis string length of its JSON representation.Relationship to #331
This supersedes #331, the earlier attempt at the same disambiguation. This work substantially agrees with #331 — its conclusions and these changes converge on the same unit for every field both cover (string
data,name,clientId,OD3e→ UTF-8), and per-point coverage is documented below. Two deltas:extrasdeliberately follows the published docs (UTF-16 string length) where Message/PresenceMessage/Annotation size #331's discussion had converged on UTF-8. Measuringextrasin UTF-8 would over-count (bytes ≥ units) and false-reject documented-valid messages — the worse failure direction. Message/PresenceMessage/Annotation size #331's UTF-8-for-extras was a hedged consistency preference ("technically a bug ... not one that matters much"), with no authoritative server-side statement that the server byte-countsextras. The docs are the currently-published enforced contract.MCR3a1/MST3c/MRM3a/OMP4a1) andOD3g, which Message/PresenceMessage/Annotation size #331 predates, and uses a single canonical umbrella atTO3l8finstead of per-type duplication (addressing Message/PresenceMessage/Annotation size #331's "don't copy-paste size clauses per type" review feedback).#331 is now
CONFLICTING(the repo migratedtextile/features.textile→specifications/features.md, so its diff no longer applies). It is left open for the author to close; this PR notes the supersession.Solution
Match the service's published accounting per field. Every plain string and map key is measured in UTF-8 bytes;
extras(and the regular-Messageobject/arraydataJSON) keep the documented "string length" (UTF-16 code units).name(regular Message)clientId(regular + object)datadata/bytesObjectData.stringObjectData.jsonObjectData.numberObjectData.booleanextras(regular + object)data(regular Message)Per-clause edits to
specifications/features.md:TM6f(string data),OM3f(object clientId),MCR3a1,MST3c,MRM3a,OMP4a1(all map/operation keys),OD3e(string),OD3g(json — clarified "byte" → "UTF-8 byte").TM6b(object/array data),TM6dandOM3d(extras) — reworded from bare "string length" to "string length (the number of UTF-16 code units)".TO3l8f: one sentence establishing the default (all message-size string accounting is UTF-8 byte length unless a clause states otherwise, explicitly including thejsonOD3gpayload) and enumerating the two UTF-16 exceptions (extrasTM6d/OM3d and object/arraydataTM6b). Deliberately one umbrella rule rather than per-field/per-type duplication.Wording-only change; no test additions in this repo.
SDK status
objectmessage.ts(clientId/OM3f, OMP4a1, MCR3a1, MST3c, MRM3a →dataSizeBytes);message.tsgetMessageSize(name+clientId →Utils.dataSizeBytes)extrasstaysJSON.stringify(...).lengthObjectMessage.swiftObjectsMap.size(OMP4a1:utf16.count→utf8.count— the one behavioural change);ARTBaseMessage.mcore-Message alignmentextras/object-arraydataUTF-16feature/liveobjects-implementationWireObjectMessage.ktWireObjectsMap.size(OMP4a1:.length→.byteSize)extrasUTF-16refactor/uts-objects-unit-into-liveobjectsNotes
extras/UTF-16 rationale. The published accounting has an explicit rule forextras("string length of its JSON representation") distinct from the byte rule for other strings. Matching it exactly (UTF-16 code units) makes the gate neither false-accept nor false-rejectextrasrelative to the documented contract. UTF-8 would over-count and false-reject documented-valid messages. This is a deliberate documented exception, not an inconsistency; all three SDKs already agreed on UTF-16 here.UTF-8 byte length ≥ UTF-16 code-unit count(equality only for ASCII). Measuring a key in UTF-8 bytes equals the server's on-the-wire byte count, so the gate never false-accepts a key the server would reject. UTF-8 strictly dominates UTF-16 for keys.name/clientIdfor regularMessagehave no explicit per-field sub-clause inTM6; they are covered by theTO3l8fumbrella default (UTF-8).TP5(PresenceMessage) needed no edit — it already delegates wholesale toTM6, andRSAN1a4(annotations) delegates toTO3l8, so presence and annotation sizing inherit the rule with no separate edits.