Skip to content

Commit 31e7201

Browse files
committed
fix(studio-bridge): fix coroutine resume race and skip play-mode contexts
Use task.defer instead of task.spawn to resume the caller thread in scanPortsAsync, preventing "cannot resume non-suspended coroutine" errors. Skip plugin initialization in client/server play-mode contexts since HttpService is restricted to the game server. Increase session settle timeout from 2.5s to 4s to allow more time for persistent plugins to discover the ephemeral host.
1 parent 17e531b commit 31e7201

2 files changed

Lines changed: 14 additions & 13 deletions

File tree

tools/studio-bridge/src/bridge/bridge-connection.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -451,18 +451,18 @@ export class BridgeConnection extends EventEmitter {
451451
async waitForSessionsToSettleAsync(options?: {
452452
/** Max time to wait for the first session (ms). Default: 5000 */
453453
firstSessionTimeout?: number;
454-
/** How long to wait after the last connection before considering settled (ms). Default: 2500 */
454+
/** How long to wait after the last connection before considering settled (ms). Default: 4000 */
455455
settleMs?: number;
456-
/** Absolute max wait time (ms). Default: 10000 */
456+
/** Absolute max wait time (ms). Default: 15000 */
457457
maxMs?: number;
458458
}): Promise<void> {
459459
if (this._role !== 'host') {
460460
return;
461461
}
462462

463463
const firstTimeout = options?.firstSessionTimeout ?? 5_000;
464-
const settleMs = options?.settleMs ?? 2_500;
465-
const maxMs = options?.maxMs ?? 10_000;
464+
const settleMs = options?.settleMs ?? 4_000;
465+
const maxMs = options?.maxMs ?? 15_000;
466466

467467
// Wait for the first session
468468
try {

tools/studio-bridge/templates/studio-bridge-plugin/src/StudioBridgePlugin.server.lua

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ local PORT = "{{PORT}}"
3333
local SESSION_ID = "{{SESSION_ID}}"
3434
local IS_EPHEMERAL = ("{{EPHEMERAL}}" == "true")
3535

36-
-- Only run inside Studio
37-
if not RunService:IsStudio() then
36+
-- Only run inside Studio edit context. Plugin instances spawned by play
37+
-- mode (client/server) cannot make HTTP requests, so they cannot discover
38+
-- the bridge. The edit-context instance stays alive during play mode.
39+
if not RunService:IsStudio() or RunService:IsRunning() then
3840
return
3941
end
4042

@@ -254,7 +256,8 @@ else
254256
scanPortsAsync = function(ports, timeoutSec)
255257
-- Scan all ports in parallel using task.spawn. The calling thread
256258
-- yields and is resumed as soon as any port succeeds, all fail,
257-
-- or the timeout expires.
259+
-- or the timeout expires. Use task.defer (not task.spawn) to resume
260+
-- the caller so it has time to reach coroutine.yield() first.
258261
local callerThread = coroutine.running()
259262
local foundPort = nil
260263
local foundBody = nil
@@ -270,13 +273,13 @@ else
270273
foundPort = port
271274
foundBody = body
272275
settled = true
273-
task.spawn(callerThread)
276+
task.defer(callerThread)
274277
return
275278
end
276279
remaining -= 1
277280
if remaining <= 0 and not settled then
278281
settled = true
279-
task.spawn(callerThread)
282+
task.defer(callerThread)
280283
end
281284
end)
282285
table.insert(threads, thread)
@@ -286,13 +289,11 @@ else
286289
local timeoutThread = task.delay(timeoutSec, function()
287290
if not settled then
288291
settled = true
289-
task.spawn(callerThread)
292+
task.defer(callerThread)
290293
end
291294
end)
292295

293-
if not settled then
294-
coroutine.yield()
295-
end
296+
coroutine.yield()
296297

297298
-- Cancel remaining HTTP threads and the timeout
298299
pcall(task.cancel, timeoutThread)

0 commit comments

Comments
 (0)