Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
## 2026-07-20 - [Prisma Distinct requires matching OrderBy on PostgreSQL]
**Learning:** When using `distinct` combined with `orderBy` in Prisma, PostgreSQL translates this into a `DISTINCT ON` SQL clause. This strictly requires that the distinct field(s) be the first argument(s) in the `orderBy` array.
**Action:** Always prepend the `distinct` column(s) to the `orderBy` array when using Prisma deduplication to avoid fatal runtime errors in PostgreSQL environments.
## 2026-07-22 - [Avoid N+1 queries when mapping related lists]
**Learning:** Resolving DB N+1 queries manually mapped in code (like `localMap = new Map()`) per loop item is highly inefficient and creates significant latency as account sizes grow.
**Action:** When a controller requires related data (like `keys`) for a list of items (`accounts`), push this to a batched lookup method in the service layer using `where: { <foreign_key>: { in: <ids> } }`, returning a Map or grouping to prevent redundant query overhead.
6 changes: 5 additions & 1 deletion server/controllers/PoolController.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ class PoolController extends BaseController {
try {
const allAccounts = await store.getAllAccountsWithKeys(req.user.id);

// ⚡ Bolt: Batch fetch all local keys to prevent N+1 queries when mapping accounts
const accountIds = allAccounts.map(a => a.id);
const localKeysMap = await store.getLocalKeysForAccounts(req.user.id, accountIds);

const accountResults = await Promise.allSettled(
allAccounts.map(async (account) => {
throwIfAborted(requestAbort.signal);
Expand Down Expand Up @@ -87,7 +91,7 @@ class PoolController extends BaseController {
}

// Get local DB records to merge isPooled + hasKeyString
const localKeys = await store.getLocalKeys(req.user.id, account.id);
const localKeys = localKeysMap.get(account.id) || [];
const localMap = new Map(localKeys.map((k) => [k.hash, k]));

const enrichedKeys = liveKeys.map((k) => {
Expand Down
29 changes: 29 additions & 0 deletions server/services/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -1129,6 +1129,35 @@ export async function getLocalKeys(userId, accountId) {
});
}

export async function getLocalKeysForAccounts(userId, accountIds) {
if (!accountIds || accountIds.length === 0) return new Map();

const keys = await prisma.key.findMany({
where: { accountId: { in: accountIds }, account: { userId } },
});

const keysByAccount = new Map();
for (const keyRecord of keys) {
let key = null;
if (keyRecord.key) {
try {
key = decrypt(keyRecord.key) || null;
} catch (err) {
logger.warn(`[STORE] Failed to decrypt local key hash=${keyRecord.hash}: ${err.message}`);
key = null;
}
}
const hydratedKey = { ...keyRecord, key };

if (!keysByAccount.has(keyRecord.accountId)) {
keysByAccount.set(keyRecord.accountId, []);
}
keysByAccount.get(keyRecord.accountId).push(hydratedKey);
}

return keysByAccount;
}

export async function registerKeyString(userId, hash, rawKeyString) {
const normalizedKey = rawKeyString?.trim();
if (!normalizedKey) {
Expand Down
2 changes: 1 addition & 1 deletion src/components/AccountCard.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef, memo, useCallback } from 'react';
import { useState, useEffect, useRef, memo, useCallback } from 'react';
import AuthBadge from './AuthBadge';
import { timeAgo } from '../utils/time';
import { formatCurrency, getBalanceStatus } from '../utils/format';
Expand Down
2 changes: 1 addition & 1 deletion src/components/AccountRow.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { memo, useState } from 'react';
import { memo, useState } from 'react';
import KeyRow from './KeyRow';
import {
UserIcon,
Expand Down
2 changes: 1 addition & 1 deletion src/components/AuthBadge.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { memo } from 'react';
import { memo } from 'react';

// Auth method badge — identifies how the account logs in (OTP, PASS, MAGIC LINK, etc.)
// Dims when the session is not active so it's clear the method isn't currently signed in.
Expand Down
2 changes: 1 addition & 1 deletion src/components/EmailLinkTab.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';

import { parseEmailEntries } from '../utils/auth';
import DevBackendHint from '../components/DevBackendHint';

Expand Down
5 changes: 3 additions & 2 deletions src/components/ErrorBoundary.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { Component } from 'react';

import { isElectron, native, tryNative } from '../lib/native';

// Generate a short correlation ID for error tracking without exposing internals
Expand All @@ -10,7 +11,7 @@ function nextCorrelationId() {
return `${ts}-${rand}-${_correlationCounter}`;
}

export default class ErrorBoundary extends React.Component {
export default class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, correlationId: null };
Expand Down
2 changes: 1 addition & 1 deletion src/components/Icons.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';


// Common Icon Props
const defaultProps = {
Expand Down
2 changes: 1 addition & 1 deletion src/components/KeyRow.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { memo, useState } from 'react';
import { memo, useState } from 'react';
import ScrambleText from './ScrambleText';
import {
KeyIcon,
Expand Down
2 changes: 1 addition & 1 deletion src/components/OtpTab.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { parseEmailEntries, clerkErrorHint } from '../utils/auth';
import DevBackendHint from '../components/DevBackendHint';
Expand Down
2 changes: 1 addition & 1 deletion src/components/RegisterKeyModal.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect } from 'react';
import { useState, useRef, useEffect } from 'react';
import { useOwnedTimeouts } from '../hooks/useOwnedTimeouts';

export default function RegisterKeyModal({ hash, name, onClose, onConfirm }) {
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Dashboard.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useMetrics } from '../hooks/useMetrics';
import AccountCard from '../components/AccountCard';
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Generator.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import * as api from '../api';
import AnimeText from '../components/AnimeText';
import { clearTrackedTimeout, setTrackedTimeout } from '../lib/runtimeDiagnostics.js';
Expand Down
2 changes: 1 addition & 1 deletion src/pages/PoolManager.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { usePools } from '../hooks/usePools';
import { useOwnedTimeouts } from '../hooks/useOwnedTimeouts';
Expand Down