@el4cteo/rbx-studio-mcp 0.2.0 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge/api.js +29 -0
- package/dist/bridge/api.js.map +1 -1
- package/dist/bridge/remote.js +35 -0
- package/dist/bridge/remote.js.map +1 -1
- package/dist/bridge/rpc.js +81 -2
- package/dist/bridge/rpc.js.map +1 -1
- package/dist/bridge/server.js +56 -3
- package/dist/bridge/server.js.map +1 -1
- package/dist/index.js +86 -2
- package/dist/index.js.map +1 -1
- package/dist/tools/input.js +63 -1
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/perf.js +73 -16
- package/dist/tools/perf.js.map +1 -1
- package/dist/tools/session.js +25 -4
- package/dist/tools/session.js.map +1 -1
- package/package.json +2 -2
- package/plugin/src/Config.luau +1 -1
- package/plugin/src/Console.luau +204 -4
- package/plugin/src/Transport.luau +78 -19
- package/plugin/src/Visuals.luau +217 -20
- package/plugin/src/handlers/Input.luau +41 -1
- package/plugin/src/handlers/Perf.luau +662 -645
- package/plugin/src/init.server.luau +48 -11
- package/scripts/check-plugin.mjs +29 -1
- package/scripts/test-bridge.mjs +103 -0
- package/scripts/test-transport.mjs +58 -0
package/plugin/src/Console.luau
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
--!strict
|
|
2
2
|
--[[
|
|
3
|
-
The
|
|
3
|
+
The rbx-studio console.
|
|
4
4
|
|
|
5
5
|
This is the only feedback channel the plugin has. It deliberately never
|
|
6
6
|
writes to Studio's Output window, because that log is also what the agent
|
|
@@ -107,12 +107,22 @@ end
|
|
|
107
107
|
|
|
108
108
|
type State = {
|
|
109
109
|
lines: { string },
|
|
110
|
+
--[[
|
|
111
|
+
Rows written, as opposed to lines rendered.
|
|
112
|
+
|
|
113
|
+
`lines` also holds the indented continuations a long detail wraps onto,
|
|
114
|
+
so counting it would report a number nobody wrote -- "cleared 41 logs"
|
|
115
|
+
for twelve commands. This counts calls to `log`, which is what a person
|
|
116
|
+
means by a log line.
|
|
117
|
+
]]
|
|
118
|
+
entries: number,
|
|
110
119
|
label: TextLabel?,
|
|
111
120
|
scroller: ScrollingFrame?,
|
|
112
121
|
statusDot: TextLabel?,
|
|
113
122
|
statusText: TextLabel?,
|
|
114
123
|
metaText: TextLabel?,
|
|
115
124
|
countersText: TextLabel?,
|
|
125
|
+
clientsChip: TextLabel?,
|
|
116
126
|
pinned: boolean,
|
|
117
127
|
calls: number,
|
|
118
128
|
errors: number,
|
|
@@ -120,23 +130,43 @@ type State = {
|
|
|
120
130
|
-- The command in flight, shown in the footer while it runs. One value that
|
|
121
131
|
-- is replaced, never a list that grows.
|
|
122
132
|
running: string?,
|
|
133
|
+
--[[
|
|
134
|
+
Bumped by every call, so a pending idle timer can tell whether it is still
|
|
135
|
+
the most recent one. Cheaper and less error-prone than cancelling timers:
|
|
136
|
+
`task.delay` has no handle to cancel, and a stale closure that checks a
|
|
137
|
+
counter simply does nothing.
|
|
138
|
+
]]
|
|
139
|
+
generation: number,
|
|
140
|
+
-- How many MCP clients share this bridge. Only ever displayed above one.
|
|
141
|
+
clients: number,
|
|
142
|
+
-- Set while a repaint is already scheduled for the end of this frame.
|
|
143
|
+
dirty: boolean,
|
|
123
144
|
}
|
|
124
145
|
|
|
125
146
|
local state: State = {
|
|
126
147
|
lines = {},
|
|
148
|
+
entries = 0,
|
|
127
149
|
label = nil,
|
|
128
150
|
scroller = nil,
|
|
129
151
|
statusDot = nil,
|
|
130
152
|
statusText = nil,
|
|
131
153
|
metaText = nil,
|
|
132
154
|
countersText = nil,
|
|
155
|
+
clientsChip = nil,
|
|
133
156
|
pinned = true,
|
|
134
157
|
calls = 0,
|
|
135
158
|
errors = 0,
|
|
136
159
|
totalMs = 0,
|
|
137
160
|
running = nil,
|
|
161
|
+
generation = 0,
|
|
162
|
+
clients = 1,
|
|
163
|
+
dirty = false,
|
|
138
164
|
}
|
|
139
165
|
|
|
166
|
+
-- How long the session must be silent before the console says so. Long enough
|
|
167
|
+
-- that an agent pausing to think is not announced as having stopped.
|
|
168
|
+
local QUIET_SECONDS = 20
|
|
169
|
+
|
|
140
170
|
local function hex(color: Color3): string
|
|
141
171
|
return string.format(
|
|
142
172
|
"#%02X%02X%02X",
|
|
@@ -159,7 +189,8 @@ local function span(color: Color3, value: string): string
|
|
|
159
189
|
return string.format('<font color="%s">%s</font>', hex(color), escape(value))
|
|
160
190
|
end
|
|
161
191
|
|
|
162
|
-
local function
|
|
192
|
+
local function paint()
|
|
193
|
+
state.dirty = false
|
|
163
194
|
local label = state.label
|
|
164
195
|
if not label then
|
|
165
196
|
return
|
|
@@ -177,6 +208,24 @@ local function redraw()
|
|
|
177
208
|
end
|
|
178
209
|
end
|
|
179
210
|
|
|
211
|
+
--[[
|
|
212
|
+
Asks for a repaint, at most one per frame.
|
|
213
|
+
|
|
214
|
+
Painting is a concat of up to three hundred strings followed by a RichText
|
|
215
|
+
relayout of the whole label, and it used to run once per appended row. A
|
|
216
|
+
failed command writes three rows -- the failure, its message, sometimes a
|
|
217
|
+
hint -- so a single unlucky call repainted the entire console three times,
|
|
218
|
+
and all of that sat in front of the reply on its way back to the agent.
|
|
219
|
+
Deferring collapses them into the one paint that was always sufficient.
|
|
220
|
+
]]
|
|
221
|
+
local function redraw()
|
|
222
|
+
if state.dirty then
|
|
223
|
+
return
|
|
224
|
+
end
|
|
225
|
+
state.dirty = true
|
|
226
|
+
task.defer(paint)
|
|
227
|
+
end
|
|
228
|
+
|
|
180
229
|
local function refreshCounters()
|
|
181
230
|
local counters = state.countersText
|
|
182
231
|
if not counters then
|
|
@@ -257,6 +306,7 @@ function Console.log(level: Level, message: string, detail: string?)
|
|
|
257
306
|
off beneath it.
|
|
258
307
|
]]
|
|
259
308
|
table.insert(state.lines, line)
|
|
309
|
+
state.entries += 1
|
|
260
310
|
if detail then
|
|
261
311
|
if inlineDetail then
|
|
262
312
|
local padding = math.max(1, DETAIL_COLUMN - bodyWidth)
|
|
@@ -303,6 +353,7 @@ local KIND_LOOK: { [string]: { color: Color3, urgency: number } } = {
|
|
|
303
353
|
|
|
304
354
|
function Console.beginCall(title: string, kind: string)
|
|
305
355
|
state.running = title
|
|
356
|
+
state.generation += 1
|
|
306
357
|
local look = KIND_LOOK[kind] or KIND_LOOK.read
|
|
307
358
|
Visuals.setCaption(title)
|
|
308
359
|
Visuals.setKind(look.color, look.urgency)
|
|
@@ -319,6 +370,9 @@ function Console.recordCall(ok: boolean, milliseconds: number)
|
|
|
319
370
|
if not ok then
|
|
320
371
|
state.errors += 1
|
|
321
372
|
end
|
|
373
|
+
-- Read before it is cleared: the bar wants the same phrase the log row uses,
|
|
374
|
+
-- and this is the only place that still has it.
|
|
375
|
+
local title = state.running or "call"
|
|
322
376
|
state.running = nil
|
|
323
377
|
refreshCounters()
|
|
324
378
|
-- The footer reports totals when idle, so the band shows what just ran
|
|
@@ -327,7 +381,35 @@ function Console.recordCall(ok: boolean, milliseconds: number)
|
|
|
327
381
|
|
|
328
382
|
-- The band plots it: bar height is how long it took, colour is whether it
|
|
329
383
|
-- worked, and a failure knocks the solid off its axis as well.
|
|
330
|
-
Visuals.recordCall(milliseconds, ok)
|
|
384
|
+
Visuals.recordCall(milliseconds, ok, title)
|
|
385
|
+
|
|
386
|
+
--[[
|
|
387
|
+
Says so when the session goes quiet, once per burst.
|
|
388
|
+
|
|
389
|
+
There is no "the agent has finished" message in MCP -- an agent that has
|
|
390
|
+
stopped and one that is thinking are the same silence -- so this reports
|
|
391
|
+
the silence rather than claiming to know what caused it: what ran, how
|
|
392
|
+
much of it, and how fast. The generation check is what makes it once per
|
|
393
|
+
burst: every later call bumps the counter, so all but the newest timer
|
|
394
|
+
wake up, find they are stale, and do nothing.
|
|
395
|
+
]]
|
|
396
|
+
state.generation += 1
|
|
397
|
+
local mine = state.generation
|
|
398
|
+
task.delay(QUIET_SECONDS, function()
|
|
399
|
+
if state.generation ~= mine or state.calls == 0 then
|
|
400
|
+
return
|
|
401
|
+
end
|
|
402
|
+
Console.log(
|
|
403
|
+
"dim",
|
|
404
|
+
string.format(
|
|
405
|
+
"agent idle -- %d call%s, avg %.0fms",
|
|
406
|
+
state.calls,
|
|
407
|
+
if state.calls == 1 then "" else "s",
|
|
408
|
+
state.totalMs / state.calls
|
|
409
|
+
)
|
|
410
|
+
)
|
|
411
|
+
Visuals.setQuiet()
|
|
412
|
+
end)
|
|
331
413
|
end
|
|
332
414
|
|
|
333
415
|
--[[
|
|
@@ -342,13 +424,99 @@ function Console.setCaption(message: string)
|
|
|
342
424
|
Visuals.setCaption(message)
|
|
343
425
|
end
|
|
344
426
|
|
|
427
|
+
--[[
|
|
428
|
+
Announces that an MCP client disconnected.
|
|
429
|
+
|
|
430
|
+
This is the one moment the bridge can be certain a session ended rather than
|
|
431
|
+
paused -- the client's process is gone -- so it is the one place the console
|
|
432
|
+
is allowed to state it outright. Everything else it knows about agent
|
|
433
|
+
activity is inference, and is worded as inference.
|
|
434
|
+
]]
|
|
435
|
+
function Console.agentFinished()
|
|
436
|
+
--[[
|
|
437
|
+
Named for what happened, not for what it might have meant.
|
|
438
|
+
|
|
439
|
+
This said "Agent finished task." and that was overclaiming: the bridge
|
|
440
|
+
sees a client disconnect and nothing more. Quitting the editor, a crash
|
|
441
|
+
and a restart all arrive here identically, and the first time one was
|
|
442
|
+
watched live it announced a completed task for an agent that had simply
|
|
443
|
+
been closed mid-idle.
|
|
444
|
+
]]
|
|
445
|
+
Console.log("ok", "Agent disconnected.", "client left")
|
|
446
|
+
Visuals.setQuiet()
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
--[[
|
|
450
|
+
How many MCP clients are sharing this bridge.
|
|
451
|
+
|
|
452
|
+
Reported only above one. A single client is the ordinary case and needs no
|
|
453
|
+
badge -- a permanent "1 client connected" is a label, not a signal, and the
|
|
454
|
+
header has better uses for the width.
|
|
455
|
+
]]
|
|
456
|
+
function Console.setClients(count: number)
|
|
457
|
+
local previous = state.clients
|
|
458
|
+
state.clients = count
|
|
459
|
+
|
|
460
|
+
local chip = state.clientsChip
|
|
461
|
+
if chip then
|
|
462
|
+
chip.Visible = count > 1
|
|
463
|
+
-- Pluralised even though the badge hides at one. A string that is only
|
|
464
|
+
-- ever correct because nobody can see it is a trap for whoever changes
|
|
465
|
+
-- the visibility rule later.
|
|
466
|
+
chip.Text = string.format(
|
|
467
|
+
"\u{25C6} %d client%s",
|
|
468
|
+
count,
|
|
469
|
+
if count == 1 then "" else "s"
|
|
470
|
+
)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
local meta = state.metaText
|
|
474
|
+
if meta then
|
|
475
|
+
meta.Size = UDim2.new(1, if count > 1 then -486 else -394, 1, 0)
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
--[[
|
|
479
|
+
Only the arrival is worth a row.
|
|
480
|
+
|
|
481
|
+
The departure had one too, and seeing it live made the redundancy plain:
|
|
482
|
+
"one MCP client connected" landed in the same second as "Agent finished
|
|
483
|
+
task.", saying the same thing less well, while the badge disappearing
|
|
484
|
+
said it a third time. The count going up is news because nothing else
|
|
485
|
+
reports it; the count coming down is already covered twice over.
|
|
486
|
+
]]
|
|
487
|
+
if count > 1 and previous <= 1 then
|
|
488
|
+
Console.log("info", string.format("%d MCP clients connected", count))
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
|
|
345
492
|
function Console.clear()
|
|
493
|
+
--[[
|
|
494
|
+
Says what it did, with a timestamp, like everything else in here.
|
|
495
|
+
|
|
496
|
+
A button that empties the screen and leaves no trace is indistinguishable
|
|
497
|
+
from one that crashed the panel. The count is read before the clear and
|
|
498
|
+
written after it, so the first row of the fresh log is the receipt for
|
|
499
|
+
the one that went.
|
|
500
|
+
]]
|
|
501
|
+
local cleared = state.entries
|
|
346
502
|
table.clear(state.lines)
|
|
503
|
+
state.entries = 0
|
|
347
504
|
state.calls = 0
|
|
348
505
|
state.errors = 0
|
|
349
506
|
state.totalMs = 0
|
|
350
507
|
refreshCounters()
|
|
508
|
+
-- The bars belonged to the log that just went. Leaving forty timings from an
|
|
509
|
+
-- erased session on screen while the footer reads "idle" is two answers to
|
|
510
|
+
-- one question.
|
|
511
|
+
Visuals.clearTrace()
|
|
351
512
|
redraw()
|
|
513
|
+
|
|
514
|
+
if cleared > 0 then
|
|
515
|
+
Console.log(
|
|
516
|
+
"dim",
|
|
517
|
+
string.format("cleared %d log%s", cleared, if cleared == 1 then "" else "s")
|
|
518
|
+
)
|
|
519
|
+
end
|
|
352
520
|
end
|
|
353
521
|
|
|
354
522
|
--[[
|
|
@@ -482,6 +650,38 @@ function Console.mount(parent: Instance, handlers: Handlers)
|
|
|
482
650
|
meta.Parent = header
|
|
483
651
|
state.metaText = meta
|
|
484
652
|
|
|
653
|
+
--[[
|
|
654
|
+
The shared-bridge badge, hidden until there is something to share.
|
|
655
|
+
|
|
656
|
+
Sits between the meta line and the buttons rather than in the activity
|
|
657
|
+
band, because it is a fact about the connection and the header is where
|
|
658
|
+
connection facts live. `meta` gives up the width when it appears; see
|
|
659
|
+
`Console.setClients`.
|
|
660
|
+
]]
|
|
661
|
+
local clientsChip = Instance.new("TextLabel")
|
|
662
|
+
clientsChip.Name = "Clients"
|
|
663
|
+
clientsChip.AnchorPoint = Vector2.new(1, 0.5)
|
|
664
|
+
clientsChip.Position = UDim2.new(1, -252, 0.5, 0)
|
|
665
|
+
clientsChip.Size = UDim2.new(0, 84, 0, 18)
|
|
666
|
+
clientsChip.BackgroundColor3 = PALETTE.background
|
|
667
|
+
clientsChip.BorderSizePixel = 0
|
|
668
|
+
clientsChip.Font = Enum.Font.Code
|
|
669
|
+
clientsChip.TextSize = 11
|
|
670
|
+
clientsChip.TextColor3 = PALETTE.cyan
|
|
671
|
+
clientsChip.Text = ""
|
|
672
|
+
clientsChip.Visible = false
|
|
673
|
+
clientsChip.Parent = header
|
|
674
|
+
state.clientsChip = clientsChip
|
|
675
|
+
|
|
676
|
+
local chipCorner = Instance.new("UICorner")
|
|
677
|
+
chipCorner.CornerRadius = UDim.new(0, 3)
|
|
678
|
+
chipCorner.Parent = clientsChip
|
|
679
|
+
|
|
680
|
+
local chipStroke = Instance.new("UIStroke")
|
|
681
|
+
chipStroke.Color = PALETTE.cyan
|
|
682
|
+
chipStroke.Transparency = 0.6
|
|
683
|
+
chipStroke.Parent = clientsChip
|
|
684
|
+
|
|
485
685
|
local buttons = Instance.new("Frame")
|
|
486
686
|
buttons.AnchorPoint = Vector2.new(1, 0.5)
|
|
487
687
|
buttons.Position = UDim2.new(1, 0, 0.5, 0)
|
|
@@ -571,7 +771,7 @@ function Console.mount(parent: Instance, handlers: Handlers)
|
|
|
571
771
|
footerPadding.Parent = footer
|
|
572
772
|
|
|
573
773
|
local prompt = Instance.new("TextLabel")
|
|
574
|
-
prompt.Text = "
|
|
774
|
+
prompt.Text = "rbx\u{00B7}studio"
|
|
575
775
|
prompt.Font = Enum.Font.Code
|
|
576
776
|
prompt.TextSize = 11
|
|
577
777
|
prompt.TextColor3 = PALETTE.violet
|
|
@@ -28,6 +28,19 @@ export type Status = "disconnected" | "connecting" | "connected"
|
|
|
28
28
|
type Callbacks = {
|
|
29
29
|
onCommand: (id: string, op: string, params: { [string]: any }?) -> (),
|
|
30
30
|
onStatus: (status: Status, detail: string?) -> (),
|
|
31
|
+
--[[
|
|
32
|
+
Anything the bridge wants to tell this plugin that is not a command.
|
|
33
|
+
|
|
34
|
+
The stream was one-way in practice -- commands down, results back over a
|
|
35
|
+
separate POST -- and there was no way for the server to say something the
|
|
36
|
+
plugin should merely know, like how many agents are now sharing it. A
|
|
37
|
+
frame carrying `event` instead of `id`/`op` is that channel.
|
|
38
|
+
|
|
39
|
+
No protocol bump: a plugin older than this drops any frame without an id
|
|
40
|
+
and an op, which is exactly the right behaviour for a frame it does not
|
|
41
|
+
understand.
|
|
42
|
+
]]
|
|
43
|
+
onEvent: (event: { [string]: any }) -> (),
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
local running = false
|
|
@@ -87,23 +100,53 @@ local function identity(transport: string): { [string]: any }
|
|
|
87
100
|
end
|
|
88
101
|
|
|
89
102
|
--[[
|
|
90
|
-
Parses one
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
Parses one delivery from the stream, which may hold more than one event.
|
|
104
|
+
|
|
105
|
+
This used to assume one event per `MessageReceived` and decode the whole
|
|
106
|
+
payload as a single JSON object. That held only because the bridge had never
|
|
107
|
+
written two frames in the same tick -- and the moment it did, both were lost:
|
|
108
|
+
the two writes arrived in one chunk, so the decode saw
|
|
109
|
+
`{...}<blank>data: {...}` and returned nil for the pair. The visible symptom
|
|
110
|
+
was a client-count badge that went up and never came back down. The invisible
|
|
111
|
+
one is why this is written properly rather than patched around: nothing stops
|
|
112
|
+
a *command* from sharing a chunk with anything else, and a dropped command is
|
|
113
|
+
a tool call that disappears with no error on either side.
|
|
114
|
+
|
|
115
|
+
So the payload is split into events on blank lines, and each event's `data:`
|
|
116
|
+
lines are concatenated the way the SSE spec says they should be. Comments --
|
|
117
|
+
the `: connected` greeting and the `: ping` keepalives -- are skipped rather
|
|
118
|
+
than being allowed to poison the block they arrive with.
|
|
93
119
|
]]
|
|
94
|
-
local function
|
|
95
|
-
local
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
120
|
+
local function parseFrames(message: string): { { [string]: any } }
|
|
121
|
+
local frames: { { [string]: any } } = {}
|
|
122
|
+
|
|
123
|
+
-- Normalised so one split handles either line ending.
|
|
124
|
+
local normalised = (string.gsub(message, "\r\n", "\n"))
|
|
125
|
+
|
|
126
|
+
-- The appended separator gives every event, the last one included, a
|
|
127
|
+
-- trailing blank line, so a single pattern finds them all.
|
|
128
|
+
for block in string.gmatch(normalised .. "\n\n", "(.-)\n\n") do
|
|
129
|
+
local payload: string? = nil
|
|
130
|
+
for line in string.gmatch(block, "[^\n]+") do
|
|
131
|
+
if string.sub(line, 1, 1) ~= ":" then
|
|
132
|
+
local value = line
|
|
133
|
+
if string.sub(value, 1, 5) == "data:" then
|
|
134
|
+
value = (string.gsub(string.sub(value, 6), "^%s+", ""))
|
|
135
|
+
end
|
|
136
|
+
-- Several data lines in one event concatenate, per the spec.
|
|
137
|
+
payload = if payload == nil then value else payload .. "\n" .. value
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
if payload ~= nil and payload ~= "" then
|
|
142
|
+
local decoded = Net.decode(payload)
|
|
143
|
+
if typeof(decoded) == "table" then
|
|
144
|
+
table.insert(frames, decoded)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
105
147
|
end
|
|
106
|
-
|
|
148
|
+
|
|
149
|
+
return frames
|
|
107
150
|
end
|
|
108
151
|
|
|
109
152
|
local function handleFrame(callbacks: Callbacks, frame: { [string]: any })
|
|
@@ -111,6 +154,10 @@ local function handleFrame(callbacks: Callbacks, frame: { [string]: any })
|
|
|
111
154
|
local op = frame.op
|
|
112
155
|
if typeof(id) == "string" and typeof(op) == "string" then
|
|
113
156
|
callbacks.onCommand(id, op, frame.params)
|
|
157
|
+
return
|
|
158
|
+
end
|
|
159
|
+
if typeof(frame.event) == "string" then
|
|
160
|
+
callbacks.onEvent(frame)
|
|
114
161
|
end
|
|
115
162
|
end
|
|
116
163
|
|
|
@@ -151,8 +198,7 @@ local function runStream(callbacks: Callbacks): boolean
|
|
|
151
198
|
table.insert(
|
|
152
199
|
connections,
|
|
153
200
|
client.MessageReceived:Connect(function(message: string)
|
|
154
|
-
|
|
155
|
-
if frame then
|
|
201
|
+
for _, frame in parseFrames(message) do
|
|
156
202
|
handleFrame(callbacks, frame)
|
|
157
203
|
end
|
|
158
204
|
end)
|
|
@@ -224,8 +270,21 @@ local function runPolling(callbacks: Callbacks): boolean
|
|
|
224
270
|
return false
|
|
225
271
|
else
|
|
226
272
|
local payload = Net.decode(response.body)
|
|
227
|
-
if typeof(payload) == "table"
|
|
228
|
-
|
|
273
|
+
if typeof(payload) == "table" then
|
|
274
|
+
if typeof(payload.command) == "table" then
|
|
275
|
+
handleFrame(callbacks, payload.command)
|
|
276
|
+
end
|
|
277
|
+
--[[
|
|
278
|
+
Poll sessions get the same news, riding on the answer they were
|
|
279
|
+
already waiting for. The bridge has nowhere to push to here, so
|
|
280
|
+
every poll response carries the current value rather than only
|
|
281
|
+
the moments it changed -- the console filters out the repeats,
|
|
282
|
+
and a poll session that missed a change while it was handling a
|
|
283
|
+
command would otherwise never hear about it.
|
|
284
|
+
]]
|
|
285
|
+
if typeof(payload.clients) == "number" then
|
|
286
|
+
callbacks.onEvent({ event = "clients", count = payload.clients })
|
|
287
|
+
end
|
|
229
288
|
end
|
|
230
289
|
end
|
|
231
290
|
|