[{"data":1,"prerenderedAt":1598},["ShallowReactive",2],{"docs-dev\u002Fwebsocketapi":3,"navigation":1456,"docs-surround-dev\u002Fwebsocketapi":1594},{"id":4,"title":5,"body":6,"description":1448,"extension":1449,"meta":1450,"navigation":1451,"path":1452,"seo":1453,"stem":1454,"__hash__":1455},"docs\u002F3.dev\u002Fwebsocketapi.md","WebSocket API",{"type":7,"value":8,"toc":1435},"minimark",[9,13,28,31,36,50,59,74,79,82,142,159,162,166,172,215,222,225,350,355,358,400,452,463,467,597,609,619,621,699,713,716,720,746,749,753,1109,1118,1152,1160,1164,1167,1296,1326,1329,1333,1339,1346,1350,1353,1421,1431],[10,11,5],"h1",{"id":12},"websocket-api",[14,15,16,17,22,23,27],"p",{},"The WebSocket API pushes scene changes and viewer presence to connected clients instead of making them poll for it. It exists alongside the ",[18,19,21],"a",{"href":20},"udServerAPI","HTTP API"," rather than replacing it: the same collaborative editing is available over ",[24,25,26],"code",{},"_scene\u002Fsync",", and the two transports write to the same state, so a scene can have polling and WebSocket viewers at once and each sees the other.",[14,29,30],{},"What it removes is the polling interval. An edit is fanned out to the other subscribers as soon as it is recorded, which takes most of the latency out of collaborative editing.",[32,33,35],"h2",{"id":34},"connecting","Connecting",[14,37,38,39,45,46,49],{},"Upgrade at ",[40,41,42],"strong",{},[24,43,44],{},"\u002F_ws"," (or ",[24,47,48],{},"\u002Fapi\u002F_ws",") with a standard RFC 6455 handshake.",[14,51,52,53,58],{},"The socket carries JSON text frames. Every message — in both directions — is an object with a ",[40,54,55],{},[24,56,57],{},"type"," field naming it. Binary frames are accepted at the transport level for future use; the router itself reads JSON.",[14,60,61,62,65,66,69,70,73],{},"A connection begins ",[40,63,64],{},"unauthenticated"," and stays that way until the first message authenticates it. Anything other than ",[24,67,68],{},"auth"," on an unauthenticated connection is answered with ",[24,71,72],{},"noauth"," and the connection is closed.",[75,76,78],"h3",{"id":77},"errors","Errors",[14,80,81],{},"Any message can be answered with:",[83,84,89],"pre",{"className":85,"code":86,"language":87,"meta":88,"style":88},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","{ \"type\": \"error\", \"message\": \"notallowed\" }\n","json","",[24,90,91],{"__ignoreMap":88},[92,93,96,100,103,106,109,112,114,118,120,123,125,128,130,132,134,137,139],"span",{"class":94,"line":95},"line",1,[92,97,99],{"class":98},"sMK4o","{",[92,101,102],{"class":98}," \"",[92,104,57],{"class":105},"spNyl",[92,107,108],{"class":98},"\"",[92,110,111],{"class":98},":",[92,113,102],{"class":98},[92,115,117],{"class":116},"sfazB","error",[92,119,108],{"class":98},[92,121,122],{"class":98},",",[92,124,102],{"class":98},[92,126,127],{"class":105},"message",[92,129,108],{"class":98},[92,131,111],{"class":98},[92,133,102],{"class":98},[92,135,136],{"class":116},"notallowed",[92,138,108],{"class":98},[92,140,141],{"class":98}," }\n",[14,143,144,145,148,149,148,151,154,155,158],{},"An error does not close the connection unless it was a protocol or authorisation failure. Common values are ",[24,146,147],{},"malformedreqs",", ",[24,150,136],{},[24,152,153],{},"notsubscribed"," and ",[24,156,157],{},"serverfault",".",[14,160,161],{},"Fatal failures close the connection with a WebSocket close code and one of the reasons below.",[32,163,165],{"id":164},"authentication","Authentication",[14,167,168,169,171],{},"Send ",[24,170,68],{}," first, carrying an existing session id — the WebSocket does not mint sessions, it adopts one you already hold.",[83,173,175],{"className":85,"code":174,"language":87,"meta":88,"style":88},"{ \"type\": \"auth\", \"sessionid\": \"\u003Cuuid>\" }\n",[24,176,177],{"__ignoreMap":88},[92,178,179,181,183,185,187,189,191,193,195,197,199,202,204,206,208,211,213],{"class":94,"line":95},[92,180,99],{"class":98},[92,182,102],{"class":98},[92,184,57],{"class":105},[92,186,108],{"class":98},[92,188,111],{"class":98},[92,190,102],{"class":98},[92,192,68],{"class":116},[92,194,108],{"class":98},[92,196,122],{"class":98},[92,198,102],{"class":98},[92,200,201],{"class":105},"sessionid",[92,203,108],{"class":98},[92,205,111],{"class":98},[92,207,102],{"class":98},[92,209,210],{"class":116},"\u003Cuuid>",[92,212,108],{"class":98},[92,214,141],{"class":98},[14,216,217,218,221],{},"The session is validated exactly as the HTTP path validates it: the session id, the ",[24,219,220],{},"User-Agent"," from the upgrade request, and the client IP address must all match. A session established from one browser cannot be moved onto a socket opened by something else.",[14,223,224],{},"On success:",[83,226,228],{"className":85,"code":227,"language":87,"meta":88,"style":88},"{\n  \"type\": \"auth-ack\",\n  \"userid\": \"\u003Cuuid>\",\n  \"expires\": 1770000000.0,\n  \"servertime\": 1769000000.0,\n  \"heartbeatMS\": 30000,\n  \"success\": true\n}\n",[24,229,230,235,257,277,295,312,329,344],{"__ignoreMap":88},[92,231,232],{"class":94,"line":95},[92,233,234],{"class":98},"{\n",[92,236,238,241,243,245,247,249,252,254],{"class":94,"line":237},2,[92,239,240],{"class":98},"  \"",[92,242,57],{"class":105},[92,244,108],{"class":98},[92,246,111],{"class":98},[92,248,102],{"class":98},[92,250,251],{"class":116},"auth-ack",[92,253,108],{"class":98},[92,255,256],{"class":98},",\n",[92,258,260,262,265,267,269,271,273,275],{"class":94,"line":259},3,[92,261,240],{"class":98},[92,263,264],{"class":105},"userid",[92,266,108],{"class":98},[92,268,111],{"class":98},[92,270,102],{"class":98},[92,272,210],{"class":116},[92,274,108],{"class":98},[92,276,256],{"class":98},[92,278,280,282,285,287,289,293],{"class":94,"line":279},4,[92,281,240],{"class":98},[92,283,284],{"class":105},"expires",[92,286,108],{"class":98},[92,288,111],{"class":98},[92,290,292],{"class":291},"sbssI"," 1770000000.0",[92,294,256],{"class":98},[92,296,298,300,303,305,307,310],{"class":94,"line":297},5,[92,299,240],{"class":98},[92,301,302],{"class":105},"servertime",[92,304,108],{"class":98},[92,306,111],{"class":98},[92,308,309],{"class":291}," 1769000000.0",[92,311,256],{"class":98},[92,313,315,317,320,322,324,327],{"class":94,"line":314},6,[92,316,240],{"class":98},[92,318,319],{"class":105},"heartbeatMS",[92,321,108],{"class":98},[92,323,111],{"class":98},[92,325,326],{"class":291}," 30000",[92,328,256],{"class":98},[92,330,332,334,337,339,341],{"class":94,"line":331},7,[92,333,240],{"class":98},[92,335,336],{"class":105},"success",[92,338,108],{"class":98},[92,340,111],{"class":98},[92,342,343],{"class":98}," true\n",[92,345,347],{"class":94,"line":346},8,[92,348,349],{"class":98},"}\n",[14,351,352,354],{},[24,353,319],{}," is how often the server pings an idle connection — 30 seconds by default. It is reported rather than assumed so a client can size its own timeouts against the real value.",[14,356,357],{},"On failure:",[83,359,361],{"className":85,"code":360,"language":87,"meta":88,"style":88},"{ \"type\": \"auth-fail\", \"message\": \"noauth\" }\n",[24,362,363],{"__ignoreMap":88},[92,364,365,367,369,371,373,375,377,380,382,384,386,388,390,392,394,396,398],{"class":94,"line":95},[92,366,99],{"class":98},[92,368,102],{"class":98},[92,370,57],{"class":105},[92,372,108],{"class":98},[92,374,111],{"class":98},[92,376,102],{"class":98},[92,378,379],{"class":116},"auth-fail",[92,381,108],{"class":98},[92,383,122],{"class":98},[92,385,102],{"class":98},[92,387,127],{"class":105},[92,389,108],{"class":98},[92,391,111],{"class":98},[92,393,102],{"class":98},[92,395,72],{"class":116},[92,397,108],{"class":98},[92,399,141],{"class":98},[401,402,403,417],"table",{},[404,405,406],"thead",{},[407,408,409,414],"tr",{},[410,411,412],"th",{},[24,413,127],{},[410,415,416],{},"Meaning",[418,419,420,433,442],"tbody",{},[407,421,422,428],{},[423,424,425],"td",{},[24,426,427],{},"malformedauth",[423,429,430,432],{},[24,431,201],{}," was missing or not a UUID.",[407,434,435,439],{},[423,436,437],{},[24,438,72],{},[423,440,441],{},"The session is unknown, expired, or does not match this user agent and address.",[407,443,444,449],{},[423,445,446],{},[24,447,448],{},"toomanyconnections",[423,450,451],{},"This session already holds the maximum number of connections.",[14,453,454,455,458,459,462],{},"A session may hold at most ",[40,456,457],{},"4"," simultaneous connections. One multiplexed client only needs one; the rest is headroom for a second application instance on the same login, and for a reconnect racing its own not-yet-reaped predecessor. Connections that have stopped answering pings are reaped before the count is taken, so a reconnect is not refused by the dead connection it is replacing. When the limit is genuinely reached the new connection is ",[40,460,461],{},"refused"," rather than the oldest being evicted — evicting would let anyone holding the session id displace the real client simply by connecting.",[32,464,466],{"id":465},"scene-subscription","Scene Subscription",[83,468,470],{"className":85,"code":469,"language":87,"meta":88,"style":88},"{\n  \"type\": \"scene-subscribe\",\n  \"orgid\": \"\u003Corgid>\",\n  \"projid\": \"\u003Cprojid>\",\n  \"sceneid\": \"\u003Csceneid>\",\n  \"username\": \"Jo Bloggs\",\n  \"scenesessionid\": \"\u003Cuuid>\"\n}\n",[24,471,472,476,495,515,535,555,575,593],{"__ignoreMap":88},[92,473,474],{"class":94,"line":95},[92,475,234],{"class":98},[92,477,478,480,482,484,486,488,491,493],{"class":94,"line":237},[92,479,240],{"class":98},[92,481,57],{"class":105},[92,483,108],{"class":98},[92,485,111],{"class":98},[92,487,102],{"class":98},[92,489,490],{"class":116},"scene-subscribe",[92,492,108],{"class":98},[92,494,256],{"class":98},[92,496,497,499,502,504,506,508,511,513],{"class":94,"line":259},[92,498,240],{"class":98},[92,500,501],{"class":105},"orgid",[92,503,108],{"class":98},[92,505,111],{"class":98},[92,507,102],{"class":98},[92,509,510],{"class":116},"\u003Corgid>",[92,512,108],{"class":98},[92,514,256],{"class":98},[92,516,517,519,522,524,526,528,531,533],{"class":94,"line":279},[92,518,240],{"class":98},[92,520,521],{"class":105},"projid",[92,523,108],{"class":98},[92,525,111],{"class":98},[92,527,102],{"class":98},[92,529,530],{"class":116},"\u003Cprojid>",[92,532,108],{"class":98},[92,534,256],{"class":98},[92,536,537,539,542,544,546,548,551,553],{"class":94,"line":297},[92,538,240],{"class":98},[92,540,541],{"class":105},"sceneid",[92,543,108],{"class":98},[92,545,111],{"class":98},[92,547,102],{"class":98},[92,549,550],{"class":116},"\u003Csceneid>",[92,552,108],{"class":98},[92,554,256],{"class":98},[92,556,557,559,562,564,566,568,571,573],{"class":94,"line":314},[92,558,240],{"class":98},[92,560,561],{"class":105},"username",[92,563,108],{"class":98},[92,565,111],{"class":98},[92,567,102],{"class":98},[92,569,570],{"class":116},"Jo Bloggs",[92,572,108],{"class":98},[92,574,256],{"class":98},[92,576,577,579,582,584,586,588,590],{"class":94,"line":331},[92,578,240],{"class":98},[92,580,581],{"class":105},"scenesessionid",[92,583,108],{"class":98},[92,585,111],{"class":98},[92,587,102],{"class":98},[92,589,210],{"class":116},[92,591,592],{"class":98},"\"\n",[92,594,595],{"class":94,"line":346},[92,596,349],{"class":98},[14,598,599,601,602,604,605,608],{},[24,600,561],{}," is the display name shown to other viewers. ",[24,603,581],{}," is optional: a client that was polling before its socket came up already has a scene session, and passing it here ",[40,606,607],{},"adopts"," that session rather than minting a second one — otherwise the same person would appear twice in the presence list until the first entry aged out.",[14,610,611,612,615,616,618],{},"Authorisation is checked per message. A valid session does not by itself grant scene access; the ",[40,613,614],{},"SceneAccess"," project permission is required, and ",[24,617,136],{}," is returned without it.",[14,620,224],{},[83,622,624],{"className":85,"code":623,"language":87,"meta":88,"style":88},"{\n  \"type\": \"scene-subscribed\",\n  \"sceneSessionID\": \"\u003Cuuid>\",\n  \"servertime\": 4213,\n  \"success\": true\n}\n",[24,625,626,630,649,668,683,695],{"__ignoreMap":88},[92,627,628],{"class":94,"line":95},[92,629,234],{"class":98},[92,631,632,634,636,638,640,642,645,647],{"class":94,"line":237},[92,633,240],{"class":98},[92,635,57],{"class":105},[92,637,108],{"class":98},[92,639,111],{"class":98},[92,641,102],{"class":98},[92,643,644],{"class":116},"scene-subscribed",[92,646,108],{"class":98},[92,648,256],{"class":98},[92,650,651,653,656,658,660,662,664,666],{"class":94,"line":259},[92,652,240],{"class":98},[92,654,655],{"class":105},"sceneSessionID",[92,657,108],{"class":98},[92,659,111],{"class":98},[92,661,102],{"class":98},[92,663,210],{"class":116},[92,665,108],{"class":98},[92,667,256],{"class":98},[92,669,670,672,674,676,678,681],{"class":94,"line":279},[92,671,240],{"class":98},[92,673,302],{"class":105},[92,675,108],{"class":98},[92,677,111],{"class":98},[92,679,680],{"class":291}," 4213",[92,682,256],{"class":98},[92,684,685,687,689,691,693],{"class":94,"line":297},[92,686,240],{"class":98},[92,688,336],{"class":105},[92,690,108],{"class":98},[92,692,111],{"class":98},[92,694,343],{"class":98},[92,696,697],{"class":94,"line":314},[92,698,349],{"class":98},[14,700,701,703,704,707,708,712],{},[24,702,302],{}," is the subscription's starting ",[40,705,706],{},"sequence cursor",", not a clock. It is a per-scene edit sequence — treat it as opaque, and never compare it against a time or do arithmetic on it. Subscribing starts you at what is settled ",[709,710,711],"em",{},"now",", so you receive what happens from here rather than the scene's history. The cursor never advances past an edit still being written, which is what makes it safe: no change can be committed below a cursor a client has already been given.",[14,714,715],{},"Subscribing again without unsubscribing moves the connection to the new scene and releases the presence entry belonging to the old one.",[75,717,719],{"id":718},"unsubscribing","Unsubscribing",[83,721,723],{"className":85,"code":722,"language":87,"meta":88,"style":88},"{ \"type\": \"scene-unsubscribe\" }\n",[24,724,725],{"__ignoreMap":88},[92,726,727,729,731,733,735,737,739,742,744],{"class":94,"line":95},[92,728,99],{"class":98},[92,730,102],{"class":98},[92,732,57],{"class":105},[92,734,108],{"class":98},[92,736,111],{"class":98},[92,738,102],{"class":98},[92,740,741],{"class":116},"scene-unsubscribe",[92,743,108],{"class":98},[92,745,141],{"class":98},[14,747,748],{},"The presence entry is removed, so other viewers stop seeing you. Closing the socket does the same thing.",[32,750,752],{"id":751},"sending-updates","Sending Updates",[83,754,756],{"className":85,"code":755,"language":87,"meta":88,"style":88},"{\n  \"type\": \"scene-update\",\n  \"flags\": 0,\n  \"head\":      { \"x\": 0, \"y\": 0, \"z\": 0, \"h\": 0, \"p\": 0, \"r\": 0 },\n  \"handLeft\":  { \"...\": \"as head\" },\n  \"handRight\": { \"...\": \"as head\" },\n  \"anchor\":    { \"x\": 0, \"y\": 0, \"z\": 0, \"w\": 1 },\n  \"messages\": [],\n  \"selectedNodes\": [],\n  \"changes\": { \"\u003Cnode-uuid>\": { \"...\": \"values\" } },\n  \"transient\": false\n}\n",[24,757,758,762,781,797,890,922,952,1016,1030,1044,1089,1104],{"__ignoreMap":88},[92,759,760],{"class":94,"line":95},[92,761,234],{"class":98},[92,763,764,766,768,770,772,774,777,779],{"class":94,"line":237},[92,765,240],{"class":98},[92,767,57],{"class":105},[92,769,108],{"class":98},[92,771,111],{"class":98},[92,773,102],{"class":98},[92,775,776],{"class":116},"scene-update",[92,778,108],{"class":98},[92,780,256],{"class":98},[92,782,783,785,788,790,792,795],{"class":94,"line":259},[92,784,240],{"class":98},[92,786,787],{"class":105},"flags",[92,789,108],{"class":98},[92,791,111],{"class":98},[92,793,794],{"class":291}," 0",[92,796,256],{"class":98},[92,798,799,801,804,806,808,811,813,817,819,821,823,825,827,830,832,834,836,838,840,843,845,847,849,851,853,856,858,860,862,864,866,868,870,872,874,876,878,881,883,885,887],{"class":94,"line":279},[92,800,240],{"class":98},[92,802,803],{"class":105},"head",[92,805,108],{"class":98},[92,807,111],{"class":98},[92,809,810],{"class":98},"      {",[92,812,102],{"class":98},[92,814,816],{"class":815},"sBMFI","x",[92,818,108],{"class":98},[92,820,111],{"class":98},[92,822,794],{"class":291},[92,824,122],{"class":98},[92,826,102],{"class":98},[92,828,829],{"class":815},"y",[92,831,108],{"class":98},[92,833,111],{"class":98},[92,835,794],{"class":291},[92,837,122],{"class":98},[92,839,102],{"class":98},[92,841,842],{"class":815},"z",[92,844,108],{"class":98},[92,846,111],{"class":98},[92,848,794],{"class":291},[92,850,122],{"class":98},[92,852,102],{"class":98},[92,854,855],{"class":815},"h",[92,857,108],{"class":98},[92,859,111],{"class":98},[92,861,794],{"class":291},[92,863,122],{"class":98},[92,865,102],{"class":98},[92,867,14],{"class":815},[92,869,108],{"class":98},[92,871,111],{"class":98},[92,873,794],{"class":291},[92,875,122],{"class":98},[92,877,102],{"class":98},[92,879,880],{"class":815},"r",[92,882,108],{"class":98},[92,884,111],{"class":98},[92,886,794],{"class":291},[92,888,889],{"class":98}," },\n",[92,891,892,894,897,899,901,904,906,909,911,913,915,918,920],{"class":94,"line":297},[92,893,240],{"class":98},[92,895,896],{"class":105},"handLeft",[92,898,108],{"class":98},[92,900,111],{"class":98},[92,902,903],{"class":98},"  {",[92,905,102],{"class":98},[92,907,908],{"class":815},"...",[92,910,108],{"class":98},[92,912,111],{"class":98},[92,914,102],{"class":98},[92,916,917],{"class":116},"as head",[92,919,108],{"class":98},[92,921,889],{"class":98},[92,923,924,926,929,931,933,936,938,940,942,944,946,948,950],{"class":94,"line":314},[92,925,240],{"class":98},[92,927,928],{"class":105},"handRight",[92,930,108],{"class":98},[92,932,111],{"class":98},[92,934,935],{"class":98}," {",[92,937,102],{"class":98},[92,939,908],{"class":815},[92,941,108],{"class":98},[92,943,111],{"class":98},[92,945,102],{"class":98},[92,947,917],{"class":116},[92,949,108],{"class":98},[92,951,889],{"class":98},[92,953,954,956,959,961,963,966,968,970,972,974,976,978,980,982,984,986,988,990,992,994,996,998,1000,1002,1004,1007,1009,1011,1014],{"class":94,"line":331},[92,955,240],{"class":98},[92,957,958],{"class":105},"anchor",[92,960,108],{"class":98},[92,962,111],{"class":98},[92,964,965],{"class":98},"    {",[92,967,102],{"class":98},[92,969,816],{"class":815},[92,971,108],{"class":98},[92,973,111],{"class":98},[92,975,794],{"class":291},[92,977,122],{"class":98},[92,979,102],{"class":98},[92,981,829],{"class":815},[92,983,108],{"class":98},[92,985,111],{"class":98},[92,987,794],{"class":291},[92,989,122],{"class":98},[92,991,102],{"class":98},[92,993,842],{"class":815},[92,995,108],{"class":98},[92,997,111],{"class":98},[92,999,794],{"class":291},[92,1001,122],{"class":98},[92,1003,102],{"class":98},[92,1005,1006],{"class":815},"w",[92,1008,108],{"class":98},[92,1010,111],{"class":98},[92,1012,1013],{"class":291}," 1",[92,1015,889],{"class":98},[92,1017,1018,1020,1023,1025,1027],{"class":94,"line":346},[92,1019,240],{"class":98},[92,1021,1022],{"class":105},"messages",[92,1024,108],{"class":98},[92,1026,111],{"class":98},[92,1028,1029],{"class":98}," [],\n",[92,1031,1033,1035,1038,1040,1042],{"class":94,"line":1032},9,[92,1034,240],{"class":98},[92,1036,1037],{"class":105},"selectedNodes",[92,1039,108],{"class":98},[92,1041,111],{"class":98},[92,1043,1029],{"class":98},[92,1045,1047,1049,1052,1054,1056,1058,1060,1063,1065,1067,1069,1071,1073,1075,1077,1079,1082,1084,1087],{"class":94,"line":1046},10,[92,1048,240],{"class":98},[92,1050,1051],{"class":105},"changes",[92,1053,108],{"class":98},[92,1055,111],{"class":98},[92,1057,935],{"class":98},[92,1059,102],{"class":98},[92,1061,1062],{"class":815},"\u003Cnode-uuid>",[92,1064,108],{"class":98},[92,1066,111],{"class":98},[92,1068,935],{"class":98},[92,1070,102],{"class":98},[92,1072,908],{"class":291},[92,1074,108],{"class":98},[92,1076,111],{"class":98},[92,1078,102],{"class":98},[92,1080,1081],{"class":116},"values",[92,1083,108],{"class":98},[92,1085,1086],{"class":98}," }",[92,1088,889],{"class":98},[92,1090,1092,1094,1097,1099,1101],{"class":94,"line":1091},11,[92,1093,240],{"class":98},[92,1095,1096],{"class":105},"transient",[92,1098,108],{"class":98},[92,1100,111],{"class":98},[92,1102,1103],{"class":98}," false\n",[92,1105,1107],{"class":94,"line":1106},12,[92,1108,349],{"class":98},[14,1110,1111,1112,1117],{},"This is the incoming half of ",[18,1113,1115],{"href":1114},"udServerAPI#scenes",[24,1116,26],{}," and carries the same fields. Every part is optional; a message with only positions is a presence update.",[1119,1120,1121,1131,1142],"ul",{},[1122,1123,1124,1127,1128,1130],"li",{},[40,1125,1126],{},"Presence"," — positions, selection and messages are recorded for the other viewers. Sent without ",[24,1129,1051],{},", this is a heartbeat that keeps you in the presence list.",[1122,1132,1133,1137,1138,1141],{},[40,1134,1135],{},[24,1136,1051],{}," — scene edits, keyed by node UUID. Requires the ",[40,1139,1140],{},"SceneEdit"," permission; without it the presence half is still applied and the changes are silently dropped rather than failing the message. Keys that are not valid UUIDs are skipped.",[1122,1143,1144,1148,1149,1151],{},[40,1145,1146],{},[24,1147,1096],{}," — marks frames that are part of an in-progress gesture, such as a drag. Same meaning as on ",[24,1150,26],{},": a transient change is superseded by the one that settles it rather than being kept as a separate step in the scene's history.",[14,1153,1154,1155,1157,1158,158],{},"Sending ",[24,1156,776],{}," before subscribing returns ",[24,1159,153],{},[32,1161,1163],{"id":1162},"receiving-changes","Receiving Changes",[14,1165,1166],{},"The server pushes when something happens, and coalesces presence so a busy scene does not produce a frame per viewer per tick:",[83,1168,1170],{"className":85,"code":1169,"language":87,"meta":88,"style":88},"{\n  \"type\": \"scene-changes\",\n  \"viewers\": [ { \"...\": \"presence rows\" } ],\n  \"changes\": [ { \"id\": \"\u003Cnode-uuid>\", \"values\": { } } ],\n  \"seq\": 4271\n}\n",[24,1171,1172,1176,1195,1231,1278,1292],{"__ignoreMap":88},[92,1173,1174],{"class":94,"line":95},[92,1175,234],{"class":98},[92,1177,1178,1180,1182,1184,1186,1188,1191,1193],{"class":94,"line":237},[92,1179,240],{"class":98},[92,1181,57],{"class":105},[92,1183,108],{"class":98},[92,1185,111],{"class":98},[92,1187,102],{"class":98},[92,1189,1190],{"class":116},"scene-changes",[92,1192,108],{"class":98},[92,1194,256],{"class":98},[92,1196,1197,1199,1202,1204,1206,1209,1211,1213,1215,1217,1219,1221,1224,1226,1228],{"class":94,"line":259},[92,1198,240],{"class":98},[92,1200,1201],{"class":105},"viewers",[92,1203,108],{"class":98},[92,1205,111],{"class":98},[92,1207,1208],{"class":98}," [",[92,1210,935],{"class":98},[92,1212,102],{"class":98},[92,1214,908],{"class":815},[92,1216,108],{"class":98},[92,1218,111],{"class":98},[92,1220,102],{"class":98},[92,1222,1223],{"class":116},"presence rows",[92,1225,108],{"class":98},[92,1227,1086],{"class":98},[92,1229,1230],{"class":98}," ],\n",[92,1232,1233,1235,1237,1239,1241,1243,1245,1247,1250,1252,1254,1256,1258,1260,1262,1264,1266,1268,1270,1272,1274,1276],{"class":94,"line":279},[92,1234,240],{"class":98},[92,1236,1051],{"class":105},[92,1238,108],{"class":98},[92,1240,111],{"class":98},[92,1242,1208],{"class":98},[92,1244,935],{"class":98},[92,1246,102],{"class":98},[92,1248,1249],{"class":815},"id",[92,1251,108],{"class":98},[92,1253,111],{"class":98},[92,1255,102],{"class":98},[92,1257,1062],{"class":116},[92,1259,108],{"class":98},[92,1261,122],{"class":98},[92,1263,102],{"class":98},[92,1265,1081],{"class":815},[92,1267,108],{"class":98},[92,1269,111],{"class":98},[92,1271,935],{"class":98},[92,1273,1086],{"class":98},[92,1275,1086],{"class":98},[92,1277,1230],{"class":98},[92,1279,1280,1282,1285,1287,1289],{"class":94,"line":297},[92,1281,240],{"class":98},[92,1283,1284],{"class":105},"seq",[92,1286,108],{"class":98},[92,1288,111],{"class":98},[92,1290,1291],{"class":291}," 4271\n",[92,1293,1294],{"class":94,"line":314},[92,1295,349],{"class":98},[1119,1297,1298,1312,1319],{},[1122,1299,1300,1304,1305,1308,1309,158],{},[40,1301,1302],{},[24,1303,1201],{}," — every ",[709,1306,1307],{},"other"," viewer on the scene, whichever transport they arrived by. A viewer is considered present if seen within the last ",[40,1310,1311],{},"30 seconds",[1122,1313,1314,1318],{},[40,1315,1316],{},[24,1317,1051],{}," — edits recorded since your cursor, excluding your own. There is no need to filter your own edits out; the server does it by scene session.",[1122,1320,1321,1325],{},[40,1322,1323],{},[24,1324,1284],{}," — where your cursor now stands. Nothing needs to be echoed back: the server tracks the cursor per connection, unlike the polling path where the client carries it.",[14,1327,1328],{},"A push is only sent when there is something in it, so silence means nothing changed.",[32,1330,1332],{"id":1331},"keeping-the-connection-alive","Keeping the Connection Alive",[14,1334,1335,1336,1338],{},"The server pings an idle connection every ",[24,1337,319],{}," (30 seconds by default) and expects a pong. A connection that misses a full heartbeat interval is treated as gone.",[14,1340,1341,1342,1345],{},"If you are deploying behind a load balancer, its idle timeout must be ",[40,1343,1344],{},"longer"," than the heartbeat, or it will close connections the server still considers healthy. The 30-second default is chosen to sit under the 60-second default idle timeout of an AWS ALB. The server's own idle keep-alive reap is 75 seconds, which must likewise stay above the balancer's idle timeout — otherwise the balancer answers a request on a connection the server has already reaped, and the client sees a gateway timeout rather than a clean close.",[32,1347,1349],{"id":1348},"close-codes","Close Codes",[14,1351,1352],{},"Connections are closed with a proper WebSocket close frame and a reason, rather than a bare TCP FIN:",[401,1354,1355,1368],{},[404,1356,1357],{},[407,1358,1359,1362,1365],{},[410,1360,1361],{},"Reason",[410,1363,1364],{},"Close code",[410,1366,1367],{},"Cause",[418,1369,1370,1384,1397,1408],{},[407,1371,1372,1376,1379],{},[423,1373,1374],{},[24,1375,427],{},[423,1377,1378],{},"Policy violation (1008)",[423,1380,1381,1383],{},[24,1382,68],{}," was malformed.",[407,1385,1386,1390,1392],{},[423,1387,1388],{},[24,1389,72],{},[423,1391,1378],{},[423,1393,1394,1395,158],{},"Authentication failed, or a message arrived before ",[24,1396,68],{},[407,1398,1399,1403,1405],{},[423,1400,1401],{},[24,1402,448],{},[423,1404,1378],{},[423,1406,1407],{},"The session's connection limit was reached.",[407,1409,1410,1415,1418],{},[423,1411,1412],{},[24,1413,1414],{},"stale",[423,1416,1417],{},"Going away (1001)",[423,1419,1420],{},"The connection stopped answering pings and was reaped.",[1422,1423,1424],"blockquote",{},[14,1425,1426,1427,1430],{},"Server-initiated closes made ",[709,1428,1429],{},"without"," a close frame surface at the client as an abnormal closure (1006) with no reason. If you see 1006, the disconnection came from the network or an intermediary rather than from the reasons above.",[1432,1433,1434],"style",{},"html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}",{"title":88,"searchDepth":237,"depth":237,"links":1436},[1437,1440,1441,1444,1445,1446,1447],{"id":34,"depth":237,"text":35,"children":1438},[1439],{"id":77,"depth":259,"text":78},{"id":164,"depth":237,"text":165},{"id":465,"depth":237,"text":466,"children":1442},[1443],{"id":718,"depth":259,"text":719},{"id":751,"depth":237,"text":752},{"id":1162,"depth":237,"text":1163},{"id":1331,"depth":237,"text":1332},{"id":1348,"depth":237,"text":1349},"The WebSocket API pushes scene changes and viewer presence to connected clients instead of making them poll for it. It exists alongside the HTTP API rather than replacing it: the same collaborative editing is available over _scene\u002Fsync, and the two transports write to the same state, so a scene can have polling and WebSocket viewers at once and each sees the other.","md",{},true,"\u002Fdev\u002Fwebsocketapi",{"title":5,"description":1448},"3.dev\u002Fwebsocketapi","Lr5aUMLe8pSxOuC_NTeotTL4wOUwEw9m4yqYmSw5-dc",[1457,1461,1522,1583],{"title":1458,"path":1459,"stem":1460},"udServer Manuals","\u002F","0.index",{"title":1462,"path":1463,"stem":1464,"children":1465},"User Guide","\u002Fdocs\u002Fuser","1.user\u002F0.index",[1466,1470,1474,1478,1482,1486,1490,1494,1498,1502,1506,1510,1514,1518],{"title":1467,"path":1468,"stem":1469},"Login","\u002Fdocs\u002Fuser\u002Flogin","1.user\u002F1.login",{"title":1471,"path":1472,"stem":1473},"Account Settings","\u002Fdocs\u002Fuser\u002Faccount-settings","1.user\u002Faccount-settings",{"title":1475,"path":1476,"stem":1477},"Files","\u002Fdocs\u002Fuser\u002Ffiles","1.user\u002Ffiles",{"title":1479,"path":1480,"stem":1481},"Jobs","\u002Fdocs\u002Fuser\u002Fjobs","1.user\u002Fjobs",{"title":1483,"path":1484,"stem":1485},"3rd Party Licenses","\u002Fdocs\u002Fuser\u002Flicenses","1.user\u002Flicenses",{"title":1487,"path":1488,"stem":1489},"Licensing","\u002Fdocs\u002Fuser\u002Flicensing","1.user\u002Flicensing",{"title":1491,"path":1492,"stem":1493},"Organisations","\u002Fdocs\u002Fuser\u002Forganisations","1.user\u002Forganisations",{"title":1495,"path":1496,"stem":1497},"Permissions","\u002Fdocs\u002Fuser\u002Fpermissions","1.user\u002Fpermissions",{"title":1499,"path":1500,"stem":1501},"Connecting an Azure Blob Container to a Project","\u002Fdocs\u002Fuser\u002Fproject-azure-setup","1.user\u002Fproject-azure-setup",{"title":1503,"path":1504,"stem":1505},"Connecting an AWS S3 Bucket to a Project","\u002Fdocs\u002Fuser\u002Fproject-s3-setup","1.user\u002Fproject-s3-setup",{"title":1507,"path":1508,"stem":1509},"Projects","\u002Fdocs\u002Fuser\u002Fprojects","1.user\u002Fprojects",{"title":1511,"path":1512,"stem":1513},"Scenes","\u002Fdocs\u002Fuser\u002Fscenes","1.user\u002Fscenes",{"title":1515,"path":1516,"stem":1517},"Software Downloads","\u002Fdocs\u002Fuser\u002Fsoftware","1.user\u002Fsoftware",{"title":1519,"path":1520,"stem":1521},"Work Items","\u002Fdocs\u002Fuser\u002Fworkitems","1.user\u002Fworkitems",{"title":1523,"path":1524,"stem":1525,"children":1526},"IT Administrators Guide","\u002Fdocs\u002Fadmin","2.admin\u002F0.index",[1527,1531,1535,1539,1543,1547,1551,1555,1559,1563,1567,1571,1575,1579],{"title":1528,"path":1529,"stem":1530},"System Requirements","\u002Fdocs\u002Fadmin\u002Fsystem-requirements","2.admin\u002F1.system-requirements",{"title":1532,"path":1533,"stem":1534},"Publishing Software (Packages)","\u002Fdocs\u002Fadmin\u002Fsoftware-packages","2.admin\u002F10.software-packages",{"title":1536,"path":1537,"stem":1538},"Legal Documents","\u002Fdocs\u002Fadmin\u002Flegal-documents","2.admin\u002F11.legal-documents",{"title":1540,"path":1541,"stem":1542},"Licensing & Purchasing","\u002Fdocs\u002Fadmin\u002Flicensing","2.admin\u002F12.licensing",{"title":1544,"path":1545,"stem":1546},"Egress","\u002Fdocs\u002Fadmin\u002Fegress","2.admin\u002F13.egress",{"title":1548,"path":1549,"stem":1550},"Backups & Restore","\u002Fdocs\u002Fadmin\u002Fbackups","2.admin\u002F14.backups",{"title":1552,"path":1553,"stem":1554},"Generic Installation","\u002Fdocs\u002Fadmin\u002Fgettingstarted","2.admin\u002F2.gettingstarted",{"title":1556,"path":1557,"stem":1558},"Setting up on an Amazon EC2 instance","\u002Fdocs\u002Fadmin\u002Famazon-ec2","2.admin\u002F3.amazon-ec2",{"title":1560,"path":1561,"stem":1562},"Configuring OAuth \u002F OIDC Authentication","\u002Fdocs\u002Fadmin\u002Foauth","2.admin\u002F4.oauth",{"title":1564,"path":1565,"stem":1566},"Admin Console","\u002Fdocs\u002Fadmin\u002Fadmin-console","2.admin\u002F5.admin-console",{"title":1568,"path":1569,"stem":1570},"Server Configuration","\u002Fdocs\u002Fadmin\u002Fserver-configuration","2.admin\u002F6.server-configuration",{"title":1572,"path":1573,"stem":1574},"Users & Organisations","\u002Fdocs\u002Fadmin\u002Fusers-and-organisations","2.admin\u002F7.users-and-organisations",{"title":1576,"path":1577,"stem":1578},"Analytics & System Log","\u002Fdocs\u002Fadmin\u002Fanalytics-and-logs","2.admin\u002F8.analytics-and-logs",{"title":1580,"path":1581,"stem":1582},"Webhooks","\u002Fdocs\u002Fadmin\u002Fwebhooks","2.admin\u002F9.webhooks",{"title":1584,"path":1585,"stem":1586,"children":1587},"Developers Guide","\u002Fdocs\u002Fdev","3.dev\u002F0.index",[1588,1592],{"title":1589,"path":1590,"stem":1591},"udServer API","\u002Fdocs\u002Fdev\u002Fudserverapi","3.dev\u002FudServerAPI",{"title":5,"path":1593,"stem":1454},"\u002Fdocs\u002Fdev\u002Fwebsocketapi",[1595,1597],{"title":1589,"path":1596,"stem":1591,"children":-1},"\u002Fdev\u002Fudserverapi",null,1787483255548]