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