mindspace: decouple page list sync latency
This commit is contained in:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user