Add product source job resolver
This commit is contained in:
@@ -1,7 +1,30 @@
|
||||
import { createServer } from 'node:http';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { WebSocketServer } from 'ws';
|
||||
|
||||
function selectLatestSourceJobId(product) {
|
||||
const versions = (product?.versions ?? [])
|
||||
.filter((version) => Array.isArray(version?.wordcloud_archives))
|
||||
.sort((left, right) => {
|
||||
const leftTime = Date.parse(left?.created_at ?? '');
|
||||
const rightTime = Date.parse(right?.created_at ?? '');
|
||||
return (Number.isFinite(rightTime) ? rightTime : 0)
|
||||
- (Number.isFinite(leftTime) ? leftTime : 0);
|
||||
});
|
||||
|
||||
for (const version of versions) {
|
||||
const archive = version.wordcloud_archives.find((item) => (
|
||||
typeof item?.source_job_id === 'string' && item.source_job_id.length > 0
|
||||
));
|
||||
if (archive) {
|
||||
return archive.source_job_id;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isFocusEvent(value) {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
@@ -18,7 +41,22 @@ function isFocusEvent(value) {
|
||||
);
|
||||
}
|
||||
|
||||
export function createRealtimeServer(port = Number(process.env.REALTIME_PORT || 8787)) {
|
||||
export function createRealtimeServer(
|
||||
port = Number(process.env.REALTIME_PORT || 8787),
|
||||
options = {},
|
||||
) {
|
||||
const upstreamUrl = options.upstreamUrl
|
||||
?? process.env.WORDCLOUD_API_UPSTREAM
|
||||
?? 'http://192.168.31.213:8000';
|
||||
const adminPassword = options.adminPassword
|
||||
?? process.env.ORDERS_ADMIN_PASSWORD
|
||||
?? 'zhihui2024';
|
||||
const tokenSecret = options.tokenSecret
|
||||
?? process.env.ORDERS_TOKEN_SECRET
|
||||
?? 'wordcloud-orders-demo';
|
||||
const fetchImpl = options.fetchImpl ?? fetch;
|
||||
const upstream = upstreamUrl.replace(/\/+$/, '');
|
||||
|
||||
const server = createServer((request, response) => {
|
||||
if (request.url === '/healthz') {
|
||||
response.writeHead(200, { 'content-type': 'application/json' });
|
||||
@@ -26,6 +64,47 @@ export function createRealtimeServer(port = Number(process.env.REALTIME_PORT ||
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceJobMatch = request.url.match(/^\/api\/products\/([^/]+)\/source-job$/);
|
||||
if (sourceJobMatch && request.method === 'GET') {
|
||||
const productId = decodeURIComponent(sourceJobMatch[1]);
|
||||
const sendError = (status, message) => {
|
||||
response.writeHead(status, { 'content-type': 'application/json' });
|
||||
response.end(JSON.stringify({ detail: message }));
|
||||
};
|
||||
|
||||
fetchImpl(`${upstream}/api/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ password: adminPassword }),
|
||||
})
|
||||
.then(async (loginResponse) => {
|
||||
if (!loginResponse.ok) {
|
||||
return sendError(502, '词云产品服务认证失败');
|
||||
}
|
||||
const { token } = await loginResponse.json();
|
||||
return fetchImpl(`${upstream}/api/products/${encodeURIComponent(productId)}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
})
|
||||
.then(async (productResponse) => {
|
||||
if (!productResponse.ok) {
|
||||
return sendError(
|
||||
productResponse.status === 404 ? 404 : 502,
|
||||
productResponse.status === 404 ? 'product not found' : '词云产品服务不可用',
|
||||
);
|
||||
}
|
||||
const sourceJobId = selectLatestSourceJobId(await productResponse.json());
|
||||
if (!sourceJobId) {
|
||||
return sendError(404, 'product has no wordcloud archive');
|
||||
}
|
||||
|
||||
response.writeHead(200, { 'content-type': 'application/json' });
|
||||
response.end(JSON.stringify({ product_id: productId, source_job_id: sourceJobId }));
|
||||
})
|
||||
.catch(() => sendError(502, '词云产品服务不可用'));
|
||||
return;
|
||||
}
|
||||
|
||||
response.writeHead(404, { 'content-type': 'text/plain' });
|
||||
response.end('Not found');
|
||||
});
|
||||
|
||||
@@ -3,6 +3,68 @@ import assert from 'node:assert/strict';
|
||||
import { createRealtimeServer } from './realtime-server.mjs';
|
||||
import WebSocket from 'ws';
|
||||
|
||||
const productDetail = {
|
||||
product_id: 'prod_abc123',
|
||||
versions: [
|
||||
{
|
||||
version_id: 'ver_old',
|
||||
created_at: '2026-09-13T08:00:00Z',
|
||||
wordcloud_archives: [
|
||||
{ source_job_id: 'job_old', created_at: '2026-09-13T08:00:00Z' },
|
||||
],
|
||||
},
|
||||
{
|
||||
version_id: 'ver_new',
|
||||
created_at: '2026-09-13T09:00:00Z',
|
||||
wordcloud_archives: [
|
||||
{ source_job_id: 'job_new', created_at: '2026-09-13T09:00:00Z' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
test('resolves a product id to the latest source job id', async () => {
|
||||
const requests = [];
|
||||
const fetchImpl = async (input, init) => {
|
||||
requests.push({ input: String(input), init });
|
||||
if (input === 'http://wordcloud.test/api/login') {
|
||||
return new Response(JSON.stringify({ token: 'product-token' }), { status: 200 });
|
||||
}
|
||||
if (input === 'http://wordcloud.test/api/products/prod_abc123') {
|
||||
return new Response(JSON.stringify(productDetail), { status: 200 });
|
||||
}
|
||||
return new Response('not found', { status: 404 });
|
||||
};
|
||||
|
||||
const server = createRealtimeServer(0, {
|
||||
upstreamUrl: 'http://wordcloud.test',
|
||||
adminPassword: 'test-password',
|
||||
tokenSecret: 'test-secret',
|
||||
fetchImpl,
|
||||
});
|
||||
await new Promise((resolve) => server.listen(resolve));
|
||||
const { port } = server.address();
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${port}/api/products/prod_abc123/source-job`);
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(await response.json(), {
|
||||
product_id: 'prod_abc123',
|
||||
source_job_id: 'job_new',
|
||||
});
|
||||
assert.deepEqual(requests.map((request) => request.input), [
|
||||
'http://wordcloud.test/api/login',
|
||||
'http://wordcloud.test/api/products/prod_abc123',
|
||||
]);
|
||||
|
||||
const loginBody = JSON.parse(requests[0].init.body);
|
||||
assert.equal(loginBody.password, 'test-password');
|
||||
assert.equal(requests[1].init.headers.Authorization, 'Bearer product-token');
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('broadcasts focus events to other connected clients', async () => {
|
||||
const server = createRealtimeServer(0);
|
||||
await new Promise((resolve) => server.listen(resolve));
|
||||
|
||||
Reference in New Issue
Block a user