mindspace: decouple page list sync latency

This commit is contained in:
john
2026-07-27 18:07:16 +08:00
parent 40dcdaf49f
commit 6878827224
2 changed files with 140 additions and 2 deletions
+60 -1
View File
@@ -12,6 +12,56 @@ function assertRouter(api) {
}
}
const DEFAULT_PAGE_LIST_SYNC_INTERVAL_MS = 30_000;
export function createMindSpacePageListSyncScheduler({
syncUserGeneratedPages,
logger = console,
minIntervalMs = DEFAULT_PAGE_LIST_SYNC_INTERVAL_MS,
now = () => Date.now(),
} = {}) {
if (typeof syncUserGeneratedPages !== 'function') {
throw new Error('createMindSpacePageListSyncScheduler requires syncUserGeneratedPages');
}
const inFlightByUser = new Map();
const lastStartedAtByUser = new Map();
return function schedulePageListSync(userId) {
if (!userId) return null;
const inFlight = inFlightByUser.get(userId);
if (inFlight) return inFlight;
const currentTime = now();
const lastStartedAt = lastStartedAtByUser.get(userId);
if (
Number.isFinite(minIntervalMs) &&
minIntervalMs > 0 &&
Number.isFinite(lastStartedAt) &&
currentTime - lastStartedAt < minIntervalMs
) {
return null;
}
lastStartedAtByUser.set(userId, currentTime);
const task = Promise.resolve()
.then(() => syncUserGeneratedPages(userId))
.catch((error) => {
logger?.warn?.(
`[MindSpace] background page list sync failed for user ${userId}:`,
error instanceof Error ? error.message : error,
);
return null;
})
.finally(() => {
if (inFlightByUser.get(userId) === task) {
inFlightByUser.delete(userId);
}
});
inFlightByUser.set(userId, task);
return task;
};
}
export function attachPortalMindSpacePageCoreRoutes(
api,
{
@@ -28,6 +78,8 @@ export function attachPortalMindSpacePageCoreRoutes(
sendData,
sendError,
handleMindSpaceError,
logger = console,
pageListSyncIntervalMs = DEFAULT_PAGE_LIST_SYNC_INTERVAL_MS,
} = {},
) {
assertRouter(api);
@@ -43,6 +95,11 @@ export function attachPortalMindSpacePageCoreRoutes(
'attachPortalMindSpacePageCoreRoutes requires page route dependencies',
);
}
const schedulePageListSync = createMindSpacePageListSyncScheduler({
syncUserGeneratedPages,
logger,
minIntervalMs: pageListSyncIntervalMs,
});
api.post('/mindspace/v1/pages', async (req, res) => {
const pages = getMindSpacePages();
@@ -72,7 +129,9 @@ export function attachPortalMindSpacePageCoreRoutes(
return res.status(503).json({ message: 'MindSpace 未启用' });
}
try {
await syncUserGeneratedPages(req.currentUser.id);
// Keep remote/public HTML discovery alive, but never put the full
// workspace delivery pass on the latency path for listing pages.
schedulePageListSync(req.currentUser.id);
const limit = Number.parseInt(String(req.query.limit ?? ''), 10);
const offset = Number.parseInt(String(req.query.offset ?? ''), 10);
const result = await pages.listPages(req.currentUser.id, {
@@ -1,6 +1,9 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { attachPortalMindSpacePageCoreRoutes } from './portal-mindspace-page-core-routes.mjs';
import {
attachPortalMindSpacePageCoreRoutes,
createMindSpacePageListSyncScheduler,
} from './portal-mindspace-page-core-routes.mjs';
function createRouterRecorder() {
const routes = new Map();
@@ -179,6 +182,82 @@ test('create and list preserve payload mapping, sync, and pagination envelope',
});
});
test('list pages responds without waiting for background workspace sync', async () => {
let syncStarted = 0;
let resolveSync;
const received = [];
const api = createRouterRecorder();
const setup = createDependencies({
getMindSpacePages: () => ({
async listPages(userId, input) {
received.push({ userId, input });
return {
items: [{ id: 'page-1' }],
total: 1,
limit: 6,
offset: 0,
hasMore: false,
};
},
}),
syncUserGeneratedPages() {
syncStarted += 1;
return new Promise((resolve) => {
resolveSync = resolve;
});
},
});
attachPortalMindSpacePageCoreRoutes(api, setup.dependencies);
const listRes = createResponseRecorder();
await api.routes.get('GET /mindspace/v1/pages')(
createRequest({ query: { limit: '6', offset: '0' } }),
listRes,
);
assert.equal(syncStarted, 1);
assert.equal(received.length, 1);
assert.deepEqual(listRes.body.data, [{ id: 'page-1' }]);
resolveSync?.({ created: 0, updated: 0, skipped: 0 });
});
test('page list background sync coalesces in-flight and throttled requests', async () => {
let currentTime = 1_000;
const calls = [];
const resolvers = [];
const scheduler = createMindSpacePageListSyncScheduler({
now: () => currentTime,
minIntervalMs: 30_000,
syncUserGeneratedPages(userId) {
calls.push(userId);
return new Promise((resolve) => {
resolvers.push(resolve);
});
},
});
const first = scheduler('user-1');
const second = scheduler('user-1');
assert.equal(first, second);
await Promise.resolve();
assert.deepEqual(calls, ['user-1']);
resolvers.shift()?.({ created: 0, updated: 0, skipped: 0 });
await first;
currentTime += 1_000;
assert.equal(scheduler('user-1'), null);
assert.deepEqual(calls, ['user-1']);
currentTime += 30_000;
const third = scheduler('user-1');
await Promise.resolve();
assert.deepEqual(calls, ['user-1', 'user-1']);
resolvers.shift()?.({ created: 0, updated: 0, skipped: 0 });
await third;
});
test('get and update preserve parallel lookup and version mapping', async () => {
const api = createRouterRecorder();
const setup = createDependencies({