🐛 fix: Resolve MeiliSearch Startup Sync Failure from Model Loading Order (#12397)

* fix: resolve MeiliSearch startup sync failure from model loading order

`indexSync.js` captures `mongoose.models.Message` and
`mongoose.models.Conversation` at module load time (lines 9-10).
Since PR #11830 moved model registration into `createModels()`,
these references are always `undefined` because `require('./indexSync')`
ran before `createModels(mongoose)` in `api/db/index.js`.

Move the `require('./indexSync')` below `createModels(mongoose)` so
Mongoose models are registered before `indexSync.js` captures them.

* fix: defer model lookups in indexSync, add load-order regression test

- Move mongoose.models.Message and mongoose.models.Conversation lookups
  from module-level into performSync() and the indexSync() catch block,
  eliminating the fragile load-order dependency that caused the original
  regression
- Add guard assertion that throws a clear error if models are missing
- Add comment in api/db/index.js documenting the createModels ordering
  constraint
- Add api/db/index.spec.js regression test to prevent future re-introduction
  of the load-order bug

* fix: strengthen regression test to verify call order, add guard in setTimeout path

- Rewrite index.spec.js to track call sequence via callOrder array,
  ensuring createModels executes before indexSync module loads
  (verified: test fails if order is reversed)
- Add missing model guard assertion in the setTimeout fallback path
  in indexSync.js for consistency with performSync

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Adi Singhal 2026-03-25 14:02:44 -04:00 committed by GitHub
parent 734239346b
commit 6466483ae3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 47 additions and 4 deletions

View file

@ -1,8 +1,13 @@
const mongoose = require('mongoose');
const { createModels } = require('@librechat/data-schemas');
const { connectDb } = require('./connect');
const indexSync = require('./indexSync');
// createModels MUST run before requiring indexSync.
// indexSync.js captures mongoose.models.Message and mongoose.models.Conversation
// at module load time. If those models are not registered first, all MeiliSearch
// sync operations will silently fail on every startup.
createModels(mongoose);
const indexSync = require('./indexSync');
module.exports = { connectDb, indexSync };

26
api/db/index.spec.js Normal file
View file

@ -0,0 +1,26 @@
describe('api/db/index.js', () => {
test('createModels is called before indexSync is loaded', () => {
jest.resetModules();
const callOrder = [];
jest.mock('@librechat/data-schemas', () => ({
createModels: jest.fn((m) => {
callOrder.push('createModels');
m.models.Message = { name: 'Message' };
m.models.Conversation = { name: 'Conversation' };
}),
}));
jest.mock('./indexSync', () => {
callOrder.push('indexSync');
return jest.fn();
});
jest.mock('./connect', () => ({ connectDb: jest.fn() }));
require('./index');
expect(callOrder).toEqual(['createModels', 'indexSync']);
});
});

View file

@ -6,9 +6,6 @@ const { isEnabled, FlowStateManager } = require('@librechat/api');
const { getLogStores } = require('~/cache');
const { batchResetMeiliFlags } = require('./utils');
const Conversation = mongoose.models.Conversation;
const Message = mongoose.models.Message;
const searchEnabled = isEnabled(process.env.SEARCH);
const indexingDisabled = isEnabled(process.env.MEILI_NO_SYNC);
let currentTimeout = null;
@ -200,6 +197,14 @@ async function performSync(flowManager, flowId, flowType) {
return { messagesSync: false, convosSync: false };
}
const Message = mongoose.models.Message;
const Conversation = mongoose.models.Conversation;
if (!Message || !Conversation) {
throw new Error(
'[indexSync] Models not registered. Ensure createModels() has been called before indexSync.',
);
}
const client = MeiliSearchClient.getInstance();
const { status } = await client.health();
@ -349,6 +354,13 @@ async function indexSync() {
logger.debug('[indexSync] Creating indices...');
currentTimeout = setTimeout(async () => {
try {
const Message = mongoose.models.Message;
const Conversation = mongoose.models.Conversation;
if (!Message || !Conversation) {
throw new Error(
'[indexSync] Models not registered. Ensure createModels() has been called before indexSync.',
);
}
await Message.syncWithMeili();
await Conversation.syncWithMeili();
} catch (err) {