Merge pull request 'perf: gateway + DB + frontend optimizations (P8-003)' (#211) from feat/p8-003-performance into main
Some checks failed
ci/woodpecker/push/ci Pipeline failed

Reviewed-on: mosaic/mosaic-stack#211
This commit was merged in pull request #211.
This commit is contained in:
2026-03-21 12:28:30 +00:00
11 changed files with 2823 additions and 88 deletions

View File

@@ -1,4 +1,9 @@
import { eq, type Db, conversations, messages } from '@mosaic/db';
import { eq, asc, desc, type Db, conversations, messages } from '@mosaic/db';
/** Maximum number of conversations returned per list query. */
const MAX_CONVERSATIONS = 200;
/** Maximum number of messages returned per conversation history query. */
const MAX_MESSAGES = 500;
export type Conversation = typeof conversations.$inferSelect;
export type NewConversation = typeof conversations.$inferInsert;
@@ -8,7 +13,12 @@ export type NewMessage = typeof messages.$inferInsert;
export function createConversationsRepo(db: Db) {
return {
async findAll(userId: string): Promise<Conversation[]> {
return db.select().from(conversations).where(eq(conversations.userId, userId));
return db
.select()
.from(conversations)
.where(eq(conversations.userId, userId))
.orderBy(desc(conversations.updatedAt))
.limit(MAX_CONVERSATIONS);
},
async findById(id: string): Promise<Conversation | undefined> {
@@ -36,7 +46,12 @@ export function createConversationsRepo(db: Db) {
},
async findMessages(conversationId: string): Promise<Message[]> {
return db.select().from(messages).where(eq(messages.conversationId, conversationId));
return db
.select()
.from(messages)
.where(eq(messages.conversationId, conversationId))
.orderBy(asc(messages.createdAt))
.limit(MAX_MESSAGES);
},
async addMessage(data: NewMessage): Promise<Message> {

View File

@@ -0,0 +1,14 @@
DROP INDEX "agent_logs_session_id_idx";--> statement-breakpoint
DROP INDEX "agent_logs_tier_idx";--> statement-breakpoint
DROP INDEX "agent_logs_created_at_idx";--> statement-breakpoint
DROP INDEX "conversations_user_id_idx";--> statement-breakpoint
DROP INDEX "conversations_archived_idx";--> statement-breakpoint
DROP INDEX "preferences_user_key_idx";--> statement-breakpoint
CREATE INDEX "accounts_provider_account_idx" ON "accounts" USING btree ("provider_id","account_id");--> statement-breakpoint
CREATE INDEX "accounts_user_id_idx" ON "accounts" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "agent_logs_session_tier_idx" ON "agent_logs" USING btree ("session_id","tier");--> statement-breakpoint
CREATE INDEX "agent_logs_tier_created_at_idx" ON "agent_logs" USING btree ("tier","created_at");--> statement-breakpoint
CREATE INDEX "conversations_user_archived_idx" ON "conversations" USING btree ("user_id","archived");--> statement-breakpoint
CREATE INDEX "sessions_user_id_idx" ON "sessions" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "sessions_expires_at_idx" ON "sessions" USING btree ("expires_at");--> statement-breakpoint
CREATE UNIQUE INDEX "preferences_user_key_idx" ON "preferences" USING btree ("user_id","key");

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,13 @@
"when": 1773625181629,
"tag": "0002_nebulous_mimic",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1773887085247,
"tag": "0003_p8003_perf_indexes",
"breakpoints": true
}
]
}

View File

@@ -12,7 +12,15 @@ export interface DbHandle {
export function createDb(url?: string): DbHandle {
const connectionString = url ?? process.env['DATABASE_URL'] ?? DEFAULT_DATABASE_URL;
const sql = postgres(connectionString);
const sql = postgres(connectionString, {
// Pool sizing: allow up to 20 concurrent connections per gateway instance.
// Each NestJS module (brain, preferences, memory, coord) shares this pool.
max: Number(process.env['DB_POOL_MAX'] ?? 20),
// Recycle idle connections after 30 s to avoid stale TCP state.
idle_timeout: Number(process.env['DB_IDLE_TIMEOUT'] ?? 30),
// Fail fast (5 s) on connection problems rather than hanging indefinitely.
connect_timeout: Number(process.env['DB_CONNECT_TIMEOUT'] ?? 5),
});
const db = drizzle(sql, { schema });
return { db, close: () => sql.end() };
}

View File

@@ -33,36 +33,54 @@ export const users = pgTable('users', {
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
export const sessions = pgTable('sessions', {
id: text('id').primaryKey(),
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
token: text('token').notNull().unique(),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
userId: text('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
export const sessions = pgTable(
'sessions',
{
id: text('id').primaryKey(),
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
token: text('token').notNull().unique(),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
userId: text('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
// Auth hot path: look up all sessions for a user (BetterAuth session list).
index('sessions_user_id_idx').on(t.userId),
// Session expiry cleanup queries.
index('sessions_expires_at_idx').on(t.expiresAt),
],
);
export const accounts = pgTable('accounts', {
id: text('id').primaryKey(),
accountId: text('account_id').notNull(),
providerId: text('provider_id').notNull(),
userId: text('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
accessToken: text('access_token'),
refreshToken: text('refresh_token'),
idToken: text('id_token'),
accessTokenExpiresAt: timestamp('access_token_expires_at', { withTimezone: true }),
refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }),
scope: text('scope'),
password: text('password'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
export const accounts = pgTable(
'accounts',
{
id: text('id').primaryKey(),
accountId: text('account_id').notNull(),
providerId: text('provider_id').notNull(),
userId: text('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
accessToken: text('access_token'),
refreshToken: text('refresh_token'),
idToken: text('id_token'),
accessTokenExpiresAt: timestamp('access_token_expires_at', { withTimezone: true }),
refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }),
scope: text('scope'),
password: text('password'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
// BetterAuth looks up accounts by (provider_id, account_id) on OAuth callback.
index('accounts_provider_account_idx').on(t.providerId, t.accountId),
// Also used in session validation to find linked accounts for a user.
index('accounts_user_id_idx').on(t.userId),
],
);
export const verifications = pgTable('verifications', {
id: text('id').primaryKey(),
@@ -306,10 +324,10 @@ export const conversations = pgTable(
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('conversations_user_id_idx').on(t.userId),
// Compound index for the most common query: conversations for a user filtered by archived.
index('conversations_user_archived_idx').on(t.userId, t.archived),
index('conversations_project_id_idx').on(t.projectId),
index('conversations_agent_id_idx').on(t.agentId),
index('conversations_archived_idx').on(t.archived),
],
);
@@ -369,7 +387,8 @@ export const preferences = pgTable(
},
(t) => [
index('preferences_user_id_idx').on(t.userId),
index('preferences_user_key_idx').on(t.userId, t.key),
// Unique constraint enables single-round-trip INSERT … ON CONFLICT DO UPDATE.
uniqueIndex('preferences_user_key_idx').on(t.userId, t.key),
],
);
@@ -431,10 +450,11 @@ export const agentLogs = pgTable(
archivedAt: timestamp('archived_at', { withTimezone: true }),
},
(t) => [
index('agent_logs_session_id_idx').on(t.sessionId),
// Compound index for session log queries (most common: session + tier filter).
index('agent_logs_session_tier_idx').on(t.sessionId, t.tier),
index('agent_logs_user_id_idx').on(t.userId),
index('agent_logs_tier_idx').on(t.tier),
index('agent_logs_created_at_idx').on(t.createdAt),
// Used by summarization cron to find hot logs older than a cutoff.
index('agent_logs_tier_created_at_idx').on(t.tier, t.createdAt),
],
);