fix: Lua factory init failure under concurrent display editing (#812)

# PR Checklist
- [ ] Have you checked if it works normally in all models? *Ignore this
if it doesn't use models.*
- [ ] Have you checked if it works normally in all web, local, and node
hosted versions? If it doesn't, have you blocked it in those versions?
- [x] Have you added type definitions?

# Description
This PR addresses an issue where `luaFactory` fails to initialize
correctly in 'editDisplay' mode under specific conditions.

## Problem Reproduction:
The initialization failure can be reliably reproduced under the
following conditions:
1. Disable all modules.
2. Select any chatbot with 'editDisplay' in trigger lua.
3. Select or make chat with length of 2 or more.
4. Restart app and revisit the chatbot

## Changes:
1. Guaranteed `luaFactory` initialization completes successfully during
concurrent calls.
2. Added `Promise Map` to ensure only one Lua engine instance is created
per mode, preventing redundant initializations.
3. Moved variable updates within mutex scope to prevent potential issues
during concurrent operations.
4.  Resolved merge conflicts with another PR. During the resolution:
* Removed logic related to caching or updating methods for Global Vars.
* **Reasoning:** Since Global Vars are stored directly in the database
and are not chat-specific, I believe caching/updating these methods to
be unnecessary, so I've simply made it calls exported one directly
without frequently re-cache itself.
* **Verification:** Re-validated that this change does not negatively
impact functionality.
* **Note:** Please let me know if anyone has concerns about this
approach to conflict resolution.
This commit is contained in:
kwaroran
2025-04-14 14:12:03 +09:00
committed by GitHub

View File

@@ -20,23 +20,23 @@ let LuaEditDisplayIds = new Set<string>()
let LuaLowLevelIds = new Set<string>() let LuaLowLevelIds = new Set<string>()
interface LuaEngineState { interface LuaEngineState {
code: string; code?: string;
engine: LuaEngine; engine?: LuaEngine;
mutex: Mutex; mutex: Mutex;
chat: Chat; chat?: Chat;
setVar: (key:string, value:string) => void, setVar?: (key:string, value:string) => void,
getVar: (key:string) => string, getVar?: (key:string) => string,
getGlobalVar: (key:string) => any,
} }
let LuaEngines = new Map<string, LuaEngineState>() let LuaEngines = new Map<string, LuaEngineState>()
let luaFactoryPromise: Promise<void> | null = null;
let pendingEngineCreations = new Map<string, Promise<LuaEngineState>>();
export async function runLua(code:string, arg:{ export async function runLua(code:string, arg:{
char?:character|groupChat|simpleCharacterArgument, char?:character|groupChat|simpleCharacterArgument,
chat?:Chat chat?:Chat
setVar?: (key:string, value:string) => void, setVar?: (key:string, value:string) => void,
getVar?: (key:string) => string, getVar?: (key:string) => string,
getGlobalVar?: (key:string) => any,
lowLevelAccess?: boolean, lowLevelAccess?: boolean,
mode?: string, mode?: string,
data?: any data?: any
@@ -44,40 +44,22 @@ export async function runLua(code:string, arg:{
const char = arg.char ?? getCurrentCharacter() const char = arg.char ?? getCurrentCharacter()
const setVar = arg.setVar ?? setChatVar const setVar = arg.setVar ?? setChatVar
const getVar = arg.getVar ?? getChatVar const getVar = arg.getVar ?? getChatVar
const getGlobalVar = arg.getGlobalVar ?? getGlobalChatVar
const mode = arg.mode ?? 'manual' const mode = arg.mode ?? 'manual'
const data = arg.data ?? {} const data = arg.data ?? {}
let chat = arg.chat ?? getCurrentChat() let chat = arg.chat ?? getCurrentChat()
let stopSending = false let stopSending = false
let lowLevelAccess = arg.lowLevelAccess ?? false let lowLevelAccess = arg.lowLevelAccess ?? false
if(!luaFactory){ await ensureLuaFactory()
await makeLuaFactory() let luaEngineState = await getOrCreateEngineState(mode);
}
let luaEngineState = LuaEngines.get(mode) return await luaEngineState.mutex.runExclusive(async () => {
let wasEmpty = false
if (!luaEngineState) {
luaEngineState = {
code,
engine: await luaFactory.createEngine({injectObjects: true}),
mutex: new Mutex(),
chat,
setVar,
getVar,
getGlobalVar
}
LuaEngines.set(mode, luaEngineState)
wasEmpty = true
} else {
luaEngineState.chat = chat luaEngineState.chat = chat
luaEngineState.setVar = setVar luaEngineState.setVar = setVar
luaEngineState.getVar = getVar luaEngineState.getVar = getVar
luaEngineState.getGlobalVar = getGlobalVar if (code !== luaEngineState.code) {
} luaEngineState.engine?.global.close()
return await luaEngineState.mutex.runExclusive(async () => { luaEngineState.code = code
if (wasEmpty || code !== luaEngineState.code) {
if (!wasEmpty)
luaEngineState.engine.global.close()
luaEngineState.engine = await luaFactory.createEngine({injectObjects: true}) luaEngineState.engine = await luaFactory.createEngine({injectObjects: true})
const luaEngine = luaEngineState.engine const luaEngine = luaEngineState.engine
luaEngine.global.set('setChatVar', (id:string,key:string, value:string) => { luaEngine.global.set('setChatVar', (id:string,key:string, value:string) => {
@@ -96,7 +78,7 @@ export async function runLua(code:string, arg:{
if(!LuaSafeIds.has(id) && !LuaEditDisplayIds.has(id)){ if(!LuaSafeIds.has(id) && !LuaEditDisplayIds.has(id)){
return return
} }
return luaEngineState.getGlobalVar(key) return getGlobalChatVar(key)
}) })
luaEngine.global.set('stopChat', (id:string) => { luaEngine.global.set('stopChat', (id:string) => {
if(!LuaSafeIds.has(id)){ if(!LuaSafeIds.has(id)){
@@ -564,7 +546,7 @@ export async function runLua(code:string, arg:{
} }
async function makeLuaFactory(){ async function makeLuaFactory(){
luaFactory = new LuaFactory() const _luaFactory = new LuaFactory()
async function mountFile(name:string){ async function mountFile(name:string){
let code = '' let code = ''
for(let i = 0; i < 3; i++){ for(let i = 0; i < 3; i++){
@@ -576,10 +558,60 @@ async function makeLuaFactory(){
} }
} catch (error) {} } catch (error) {}
} }
await luaFactory.mountFile(name,code) await _luaFactory.mountFile(name,code)
} }
await mountFile('json.lua') await mountFile('json.lua')
luaFactory = _luaFactory
}
async function ensureLuaFactory() {
if (luaFactory) return;
if (luaFactoryPromise) {
try {
await luaFactoryPromise;
} catch (error) {
luaFactoryPromise = null;
}
return;
}
try {
luaFactoryPromise = makeLuaFactory();
await luaFactoryPromise;
} finally {
luaFactoryPromise = null;
}
}
async function getOrCreateEngineState(
mode: string,
): Promise<LuaEngineState> {
let engineState = LuaEngines.get(mode);
if (engineState) {
return engineState;
}
let pendingCreation = pendingEngineCreations.get(mode);
if (pendingCreation) {
return pendingCreation;
}
const creationPromise = (async () => {
const engineState: LuaEngineState = {
mutex: new Mutex(),
};
LuaEngines.set(mode, engineState);
pendingEngineCreations.delete(mode);
return engineState;
})();
pendingEngineCreations.set(mode, creationPromise);
return creationPromise;
} }
function luaCodeWarper(code:string){ function luaCodeWarper(code:string){
@@ -687,7 +719,6 @@ callListenMain = async(function(type, id, value)
if type == 'editDisplay' then if type == 'editDisplay' then
for _, func in ipairs(editDisplayFuncs) do for _, func in ipairs(editDisplayFuncs) do
realValue = func(id, realValue) realValue = func(id, realValue)
print(realValue)
end end
end end