You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[finding] realtime-protocol.mdx: six type: "event" fences carry field shapes EventMessageSchema does not declare (subscription_id/event/data/changes/reason) #17749
Found while landing the second half of #17184 (the subscribe/unsubscribe fences on the same page) in PR #17747. That round's declared face was the subscribe/unsubscribe fences only, so this neighbouring family was measured and fenced out rather than widened into. It is not addressed by that PR.
Measured on origin/main at 9bd4344e4b. Line numbers are from the tree after PR #17747's edit.
The declared contract
EventMessageSchema (packages/spec/src/api/websocket.zod.ts:300–:310) declares, on top of the three BaseWebSocketMessage fields (messageId, type, timestamp):
subscription_id vs subscriptionId — snake_case for a camelCase field, and the schema declares z.string().uuid(), so the literal "sub_1" in every fence does not parse either.
event vs eventName — a different key name, not a spelling variant.
data vs payload — a different key name.
changes and reason — declared nowhere. reason: "entered_query" in particular names a query-membership transition nothing tracks.
object is the one key the fences get right; it is genuinely declared and optional.
Why this is a defect and not a style note
Same reasoning as #17184, and it is class (a) — copy the example and it fails against the declared contract. WebSocketMessageSchema is a discriminated union, and an event message that reaches its event branch is then checked against EventMessageSchema: a payload with event / data and no eventName / payload / messageId / timestamp does not parse. A client written from these fences reads msg.data and msg.subscription_id and gets undefined on both.
Nothing produces these messages, so the fork lands on (a) the page is wrong, by the same measurement PR #17747 reports: SubscribeMessageSchema / EventSubscriptionSchema have zero consumers outside packages/spec's own declaration, unit test and type pin; IRealtimeService.handleUpgrade is deliberately unimplemented platform-wide, with service-realtime/src/no-channel-route.pin.test.ts:49 asserting it is not a function; and subscription_id returns 0 across 6120 tracked packages/** source files (no dist/) with four lit controls on the same corpus and the same call — subscriptionId 40, eventTypes 27, handleUpgrade 18, EventSubscriptionSchema 13.
⚠️ One coupling worth flagging for whoever takes this. PR #17747 established that no query field is declared on a subscription and no runtime tracks query membership, so the two reason: "entered_query" / "exited_query" fences at :496 and :512 describe a signal the contract cannot send. That PR's callout says so in prose but deliberately left those two fences untouched, because they are event messages. Fixing them belongs here.
Not in scope here
The eventNamevocabulary question is separate and already tracked: the page's bare verbs, RealtimeEventType's members and what the engine actually publishes (DataEventType's data.record.*) are three different sets, recorded in packages/spec/liveness/realtime_subscription.json. This card is about the message envelope's field shapes, not about which event names are correct.
Found while landing the second half of #17184 (the subscribe/unsubscribe fences on the same page) in PR #17747. That round's declared face was the subscribe/unsubscribe fences only, so this neighbouring family was measured and fenced out rather than widened into. It is not addressed by that PR.
Measured on
origin/mainat9bd4344e4b. Line numbers are from the tree after PR #17747's edit.The declared contract
EventMessageSchema(packages/spec/src/api/websocket.zod.ts:300–:310) declares, on top of the threeBaseWebSocketMessagefields (messageId,type,timestamp):What the page teaches instead
Six
type: "event"fences oncontent/docs/protocol/kernel/realtime-protocol.mdx::347–:361(Receiving Events, created)type,subscription_id,event,object,data:364–:382(Receiving Events, updated)type,subscription_id,event,object,data,changes:385–:396(Receiving Events, deleted)type,subscription_id,event,object,data:496–:509(Query Results, entered)type,subscription_id,event,object,data,reason:512–:524(Query Results, exited)type,subscription_id,event,object,data,reason:734–:746(Permission Enforcement, JS)type,event,object,data,reasonAxis by axis, and they are not one thing:
subscription_idvssubscriptionId— snake_case for a camelCase field, and the schema declaresz.string().uuid(), so the literal"sub_1"in every fence does not parse either.eventvseventName— a different key name, not a spelling variant.datavspayload— a different key name.changesandreason— declared nowhere.reason: "entered_query"in particular names a query-membership transition nothing tracks.messageIdandtimestampabsent from all six, thoughBaseWebSocketMessagerequires both — and this page's own Declared message vocabulary section (added by docs(protocol): drop the five WebSocket message typesWebSocketMessageTypedoes not declare #17700) already says every message carries all three base fields.objectis the one key the fences get right; it is genuinely declared and optional.Why this is a defect and not a style note
Same reasoning as #17184, and it is class (a) — copy the example and it fails against the declared contract.
WebSocketMessageSchemais a discriminated union, and aneventmessage that reaches itseventbranch is then checked againstEventMessageSchema: a payload withevent/dataand noeventName/payload/messageId/timestampdoes not parse. A client written from these fences readsmsg.dataandmsg.subscription_idand getsundefinedon both.Nothing produces these messages, so the fork lands on (a) the page is wrong, by the same measurement PR #17747 reports:
SubscribeMessageSchema/EventSubscriptionSchemahave zero consumers outsidepackages/spec's own declaration, unit test and type pin;IRealtimeService.handleUpgradeis deliberately unimplemented platform-wide, withservice-realtime/src/no-channel-route.pin.test.ts:49asserting it is not a function; andsubscription_idreturns 0 across 6120 trackedpackages/**source files (nodist/) with four lit controls on the same corpus and the same call —subscriptionId40,eventTypes27,handleUpgrade18,EventSubscriptionSchema13.queryfield is declared on a subscription and no runtime tracks query membership, so the tworeason: "entered_query"/"exited_query"fences at:496and:512describe a signal the contract cannot send. That PR's callout says so in prose but deliberately left those two fences untouched, because they areeventmessages. Fixing them belongs here.Not in scope here
The
eventNamevocabulary question is separate and already tracked: the page's bare verbs,RealtimeEventType's members and what the engine actually publishes (DataEventType'sdata.record.*) are three different sets, recorded inpackages/spec/liveness/realtime_subscription.json. This card is about the message envelope's field shapes, not about which event names are correct.Related: #17184, #17700, #17127.
Generated by Claude Code