Skip to content

Commit 8ee678e

Browse files
feat(SolidStorage): ensure base structure once after login, baton for writes
- Run ensureBaseStructure in background after Solid/OpenID login (oauth handler) - Writes await baton so they don't run until base structure is ready - Lazy init if user never hit OAuth callback; return promise so .catch() is safe
1 parent a546054 commit 8ee678e

2 files changed

Lines changed: 74 additions & 2 deletions

File tree

api/server/routes/oauth.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const { checkDomainAllowed, loginLimiter, logHeaders, checkBan } = require('~/se
99
const { createOAuthHandler: _createOAuthHandler } = require('~/server/controllers/auth/oauth');
1010
const { setAuthTokens, setOpenIDAuthTokens } = require('~/server/services/AuthService');
1111
const { syncUserEntraGroupMemberships } = require('~/server/services/PermissionService');
12+
const { startBaseStructureAfterLogin } = require('~/server/services/SolidStorage');
1213
const { getAppConfig } = require('~/server/services/Config');
1314
const { Balance } = require('~/db/models');
1415

@@ -62,6 +63,15 @@ const oauthHandler = async (req, res, next) => {
6263
// When OPENID_REUSE_TOKENS is disabled, create standard JWT tokens
6364
await setAuthTokens(req.user._id, res);
6465
}
66+
// Ensure Solid Pod base structure in background so writes don't call it every time (baton)
67+
if (req.user.openidId) {
68+
startBaseStructureAfterLogin(req).catch((err) =>
69+
logger.warn('[oauthHandler] Solid base structure init after login failed', {
70+
openidId: req.user.openidId,
71+
error: err?.message,
72+
}),
73+
);
74+
}
6575
} else {
6676
await setAuthTokens(req.user._id, res);
6777
}

api/server/services/SolidStorage.js

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,67 @@ async function ensureBaseStructure(podUrl, fetch) {
562562
}
563563
}
564564

565+
/** Per-user baton: openidId -> Promise that resolves when ensureBaseStructure has completed for that Pod */
566+
const baseStructureBatonMap = new Map();
567+
568+
/**
569+
* Waits for the Pod base structure to be ready for this user (baton).
570+
* If login already started ensureBaseStructure in the background, we wait for it.
571+
* Otherwise we run it once (lazy) and cache the promise so other writes wait too.
572+
*
573+
* @param {Object} req - Express request (must have req.user.openidId)
574+
* @returns {Promise<void>}
575+
*/
576+
async function ensureBaseStructureReady(req) {
577+
const openidId = req?.user?.openidId;
578+
if (!openidId) {
579+
throw new Error('User not authenticated with Solid/OpenID');
580+
}
581+
let promise = baseStructureBatonMap.get(openidId);
582+
if (!promise) {
583+
const authenticatedFetch = await getSolidFetch(req);
584+
const podUrl = await getPodUrl(openidId, authenticatedFetch);
585+
promise = ensureBaseStructure(podUrl, authenticatedFetch).catch((err) => {
586+
baseStructureBatonMap.delete(openidId);
587+
throw err;
588+
});
589+
baseStructureBatonMap.set(openidId, promise);
590+
}
591+
await promise;
592+
}
593+
594+
/**
595+
* Starts ensureBaseStructure in the background for this user (call after Solid/OpenID login).
596+
* Writes will wait for this to complete via ensureBaseStructureReady (baton).
597+
* Safe to call multiple times per user; only the first run is used.
598+
*
599+
* @param {Object} req - Express request (must have req.user with openidId and session for fetch)
600+
*/
601+
function startBaseStructureAfterLogin(req) {
602+
const openidId = req?.user?.openidId;
603+
if (!openidId) return Promise.resolve();
604+
const existing = baseStructureBatonMap.get(openidId);
605+
if (existing) return existing;
606+
607+
const promise = (async () => {
608+
try {
609+
const authenticatedFetch = await getSolidFetch(req);
610+
const podUrl = await getPodUrl(openidId, authenticatedFetch);
611+
await ensureBaseStructure(podUrl, authenticatedFetch);
612+
logger.debug('[SolidStorage] Base structure ready after login', { openidId });
613+
} catch (err) {
614+
baseStructureBatonMap.delete(openidId);
615+
logger.warn('[SolidStorage] Base structure init after login failed', {
616+
openidId,
617+
error: err?.message,
618+
});
619+
throw err;
620+
}
621+
})();
622+
baseStructureBatonMap.set(openidId, promise);
623+
return promise;
624+
}
625+
565626
/**
566627
* Save a message to Solid Pod.
567628
* Document content is built in the model layer (schema-aligned); this only persists it.
@@ -589,9 +650,9 @@ async function saveMessageToSolid(req, messageDocument, metadata) {
589650
throw new Error('conversationId is required');
590651
}
591652

653+
await ensureBaseStructureReady(req);
592654
const authenticatedFetch = await getSolidFetch(req);
593655
const podUrl = await getPodUrl(req.user.openidId, authenticatedFetch);
594-
await ensureBaseStructure(podUrl, authenticatedFetch);
595656

596657
const messagesContainerPath = getMessagesContainerPath(podUrl, messageDocument.conversationId);
597658
await ensureContainerExists(messagesContainerPath, authenticatedFetch);
@@ -1209,9 +1270,9 @@ async function saveConvoToSolid(req, convoDocument, metadata) {
12091270
throw new Error('conversationId is required');
12101271
}
12111272

1273+
await ensureBaseStructureReady(req);
12121274
const authenticatedFetch = await getSolidFetch(req);
12131275
const podUrl = await getPodUrl(req.user.openidId, authenticatedFetch);
1214-
await ensureBaseStructure(podUrl, authenticatedFetch);
12151276

12161277
const conversationPath = getConversationPath(podUrl, finalConversationId);
12171278

@@ -2948,6 +3009,7 @@ module.exports = {
29483009
getMessagePath,
29493010
ensureContainerExists,
29503011
ensureBaseStructure,
3012+
startBaseStructureAfterLogin,
29513013
saveMessageToSolid,
29523014
getMessagesFromSolid,
29533015
updateMessageInSolid,

0 commit comments

Comments
 (0)