fix(lua): Signal.Fire через очередь handler'ов в tickScheduler

Прошлый фикс с __rbxl_run_in_coroutine падал внутри wasmoon с
'Cannot read properties of null (reading then)' — wasmoon
PromiseTypeExtension тщетно ловит return от Lua-функции.

Новая стратегия:
1. Signal.Fire не запускает handler синхронно — складывает в JS-очередь
   _pendingHandlerQueue.
2. tickScheduler в начале каждого тика drain'ит очередь, для каждого
   handler'а вызывает Lua-функцию __rbxl_drain_handler в coroutine.
3. Поскольку tickScheduler уже стоит на main loop (не из JS-callback),
   wait() внутри handler'а корректно yield'ится в свою coroutine.

Это разрешает:
- Roblox-обработчики с wait() внутри (Tool.Equipped с reload-таймаут)
- Любые цепочки signal:Connect → wait → action
- Стандартные шаблоны Roblox-Lua flow.
This commit is contained in:
min 2026-06-08 15:41:55 +03:00
parent d750c94a78
commit 03a6c357d0

View File

@ -25,10 +25,11 @@ const SCHEDULER = {
const HEARTBEAT_SIGNAL = makeSignal(); const HEARTBEAT_SIGNAL = makeSignal();
const STEPPED_SIGNAL = makeSignal(); const STEPPED_SIGNAL = makeSignal();
// Глобальный helper для запуска Lua-handler'ов в собственной coroutine. // Очередь handler'ов которые надо запустить на следующем tickScheduler.
// Без этого Roblox-обработчики которые внутри делают wait() падают с // Этим мы выходим из C-boundary — wait() внутри handler'а становится
// "attempt to yield across a C-call boundary". // безопасным yield в собственной coroutine, потому что handler стартует
let _runHandlerInCoroutine = null; // уже из main loop, а не из синхронного JS-callback.
const _pendingHandlerQueue = [];
function makeSignal() { function makeSignal() {
const sig = { const sig = {
@ -50,17 +51,10 @@ function makeSignal() {
sig.connect = sig.Connect; sig.connect = sig.Connect;
sig.Fire = function (...args) { sig.Fire = function (...args) {
for (const fn of [...sig.connections]) { for (const fn of [...sig.connections]) {
// Запускаем handler в его собственной coroutine — это позволяет // Кладём в очередь, чтобы handler стартовал не в текущем
// делать wait() внутри без yield-across-C-boundary ошибки. // JS-callback (откуда yield запрещён), а из tickScheduler
if (_runHandlerInCoroutine) { // в своей coroutine. Безопасно для wait() внутри.
try { _runHandlerInCoroutine(fn, args); } catch (e) { _pendingHandlerQueue.push({ fn, args });
console.error('[Signal handler]', e);
}
} else {
try { fn(...args); } catch (e) {
console.error('[Signal handler]', e);
}
}
} }
}; };
sig.fire = sig.Fire; sig.fire = sig.Fire;
@ -1402,11 +1396,11 @@ export function registerRobloxShim(lua, opts) {
return 0 return 0
end end
-- Запуск Lua-handler'а в собственной coroutine. -- Запуск Lua-handler'а из очереди в собственной coroutine.
-- Используется при Fire сигнала из JS иначе wait() внутри handler'а -- Вызывается из JS tickScheduler мы УЖЕ вышли из C-callback,
-- падает с 'attempt to yield across a C-call boundary'. -- так что wait() внутри handler'а yield в свою coroutine.
__rbxl_next_handler_id = 0 __rbxl_next_handler_id = 0
function __rbxl_run_in_coroutine(fn, a1, a2, a3, a4) function __rbxl_drain_handler(fn, a1, a2, a3, a4)
__rbxl_next_handler_id = __rbxl_next_handler_id + 1 __rbxl_next_handler_id = __rbxl_next_handler_id + 1
local handlerId = "handler_" .. __rbxl_next_handler_id local handlerId = "handler_" .. __rbxl_next_handler_id
local co = coroutine.create(function() fn(a1, a2, a3, a4) end) local co = coroutine.create(function() fn(a1, a2, a3, a4) end)
@ -1422,8 +1416,8 @@ export function registerRobloxShim(lua, opts) {
end end
end end
`); `);
// Кешируем ссылку на функцию для использования из makeSignal // Кешируем ссылку на Lua-функцию запуска handler'а
_runHandlerInCoroutine = lua.global.get('__rbxl_run_in_coroutine'); const luaDrainHandler = lua.global.get('__rbxl_drain_handler');
// Добавим Lua-side helper для лога // Добавим Lua-side helper для лога
global.set('__log', (level, text) => { global.set('__log', (level, text) => {
send('log', { level: String(level || 'info'), text: String(text || '') }); send('log', { level: String(level || 'info'), text: String(text || '') });
@ -1469,7 +1463,20 @@ export function registerRobloxShim(lua, opts) {
onDataSnapshot() {}, onDataSnapshot() {},
tickScheduler(_dt) { tickScheduler(_dt) {
// 0. Tweens // 0a. Lua-handlers из очереди (signal.Fire отложил их сюда).
// Запускаем каждый в своей coroutine — wait() внутри безопасен.
if (_pendingHandlerQueue.length > 0) {
const queue = _pendingHandlerQueue.splice(0, _pendingHandlerQueue.length);
for (const h of queue) {
try {
const a = h.args || [];
luaDrainHandler(h.fn, a[0], a[1], a[2], a[3]);
} catch (e) {
console.error('[handler-drain]', e);
}
}
}
// 0b. Tweens
_stepTweens(_dt); _stepTweens(_dt);
const now = SCHEDULER.now(); const now = SCHEDULER.now();
// 1. task.delay / task.defer // 1. task.delay / task.defer