Skip to content
Merged
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
2 changes: 2 additions & 0 deletions apps/presentation/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"license": "Apache-2.0",
"type": "module",
"scripts": {
"smoke:workspace-progressive-loader": "node smoke/workspace-progressive-status-smoke.mjs",
"smoke:workspace-progressive": "node ../../../examples/workspace-progressive-loading-browser-smoke.mjs",
"build": "tsc --noEmit && vite build && vite build --config vite.chat.config.ts",
"build:chat": "tsc --noEmit && vite build --config vite.chat.config.ts",
"build:desktop": "tsc --noEmit && vite build",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Exercise the production loader, including streamed error bodies and cancellation.
import assert from "node:assert/strict";
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { build } from "vite";

const outDir = resolve("node_modules/.cache/workspace-progressive");
await build({ configFile: false, logLevel: "silent", build: {
outDir, emptyOutDir: true,
lib: { entry: resolve("src/data/workspace-progressive-status.ts"), formats: ["es"], fileName: () => "loader.mjs" },
rolldownOptions: { external: ["zod"] },
} });
const { loadWorkspaceGoalSnapshots, directoryStatusPayload } = await import(pathToFileURL(resolve(outDir, "loader.mjs")).href);
const directory = { ok: true, schema_version: "loopx_workspace_directory_v1", registry_revision: "r1",
goals: [{ id: "alpha", display_name: "Alpha", activation_state: "active", registry_member: true }] };
const access = { error_code: "workspace_status_access_denied" };
const original = { fetch, setTimeout, clearTimeout };
const deadline = {};
let expireRequest;
let checks = 0;
globalThis.setTimeout = (callback, delay, ...args) => {
if (delay === 30_000) { expireRequest = callback; return deadline; }
return original.setTimeout(callback, 0, ...args);
};
globalThis.clearTimeout = (timer) => { if (timer !== deadline) original.clearTimeout(timer); };
async function check(name, respond, expected, attempts, verify = () => {}) {
let calls = 0;
let current = true;
const controller = new AbortController();
const results = [];
globalThis.fetch = async () => {
calls++;
return respond({ expire: () => expireRequest(), cancel: () => controller.abort(), invalidate: () => { current = false; } });
};
let watchdog;
try {
await Promise.race([
loadWorkspaceGoalSnapshots("http://status/status.json", "http://status", directory,
(_id, _payload, error) => results.push(error ?? "success"), () => current, () => "alpha", controller.signal),
new Promise((_resolve, reject) => { watchdog = original.setTimeout(() => reject(new Error(name + ": hung")), 2_000); }),
]);
assert.deepEqual(results, expected, name);
assert.equal(calls, attempts, name + ": attempts");
verify();
checks++;
} finally { original.clearTimeout(watchdog); }
}
const json = (body, status = 500) => Response.json(body, { status });
try {
await check("access", () => json(access), ["access"], 1);
for (const body of [null, {}, [], { error_code: "future_code" }, { error_code: 5 }, { error: "permission denied" }]) {
await check("unknown envelope", () => json(body), ["service"], 3);
}
await check("proxy HTML", () => new Response("<html>bad gateway</html>", { status: 502 }), ["service"], 3);
await check("empty body", () => new Response(null, { status: 503 }), ["service"], 3);
for (const status of [400, 401, 403, 404, 409]) {
await check("HTTP precedence " + status, () => json(access, status), [status === 409 ? "revision" : "scope"], 1);
}
let cancelled = 0;
await check("oversized stream ignores false content-length", () => new Response(new ReadableStream({
start(controller) { controller.enqueue(new Uint8Array(16 * 1024 + 1)); },
cancel() { cancelled++; },
}), { status: 500, headers: { "Content-Length": "1" } }), ["service"], 3, () => assert.equal(cancelled, 3));
const encoded = new TextEncoder().encode(JSON.stringify(access));
await check("fragmented JSON", () => new Response(new ReadableStream({
start(controller) { controller.enqueue(encoded.subarray(0, 5)); controller.enqueue(encoded.subarray(5)); controller.close(); },
}), { status: 500 }), ["access"], 1);
await check("broken error body", () => new Response(new ReadableStream({
start(controller) { controller.error(new TypeError("broken stream")); },
}), { status: 500 }), ["service"], 3);
for (const action of ["expire", "cancel", "invalidate"]) {
await check("incomplete body " + action, (actions) => new Response(new ReadableStream({
start(controller) { original.setTimeout(() => { actions[action](); if (action === "invalidate") controller.close(); }, 0); },
}), { status: 500 }), action === "expire" ? ["timeout"] : [], action === "expire" ? 3 : 1);
}
await check("network", () => { throw new TypeError("fetch failed"); }, ["network"], 3);
await check("invalid success", () => json({ workspace_registry_revision: "r1" }, 200), ["invalid"], 1);
await check("revision", () => json({ workspace_registry_revision: "r2" }, 200), ["revision"], 1);
const payload = { ...directoryStatusPayload(directory), workspace_registry_revision: "r1" };
await check("success", () => json(payload, 200), ["success"], 1);
await check("wrong Goal", () => json({ ...payload, run_history: { ...payload.run_history, goals: [{ id: "beta" }] } }, 200), ["scope"], 1);
console.log(JSON.stringify({ ok: true, checks }));
} finally {
globalThis.fetch = original.fetch;
globalThis.setTimeout = original.setTimeout;
globalThis.clearTimeout = original.clearTimeout;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const directorySchema = z.object({
})),
});
export type WorkspaceDirectory = z.infer<typeof directorySchema>;
export type WorkspaceLoadError = "timeout" | "network" | "service" | "revision" | "scope" | "invalid";
export type WorkspaceLoadError = "timeout" | "network" | "service" | "access" | "revision" | "scope" | "invalid";
export type WorkspaceProgress = {
directory: WorkspaceDirectory;
snapshots: Record<string, StatusPayload>;
Expand Down Expand Up @@ -51,6 +51,39 @@ export function directoryStatusPayload(directory: WorkspaceDirectory): StatusPay
});
}

async function statusFailure(response: Response, signal: AbortSignal): Promise<"access" | "service"> {
if (!response.body) return "service";
const reader = response.body.getReader();
const cancel = () => { void reader.cancel().catch(() => {}); };
signal.addEventListener("abort", cancel, { once: true });
// Error bodies may come from older servers or proxies. Never buffer them unboundedly.
const bytes = new Uint8Array(16 * 1024);
let size = 0;
try {
signal.throwIfAborted();
while (true) {
const { done, value } = await reader.read();
signal.throwIfAborted();
if (done) break;
if (size + value.byteLength > bytes.byteLength) return "service";
bytes.set(value, size);
size += value.byteLength;
}
const payload: unknown = JSON.parse(new TextDecoder().decode(bytes.subarray(0, size)));
return payload !== null && typeof payload === "object" && !Array.isArray(payload)
&& "error_code" in payload && payload.error_code === "workspace_status_access_denied"
? "access" : "service";
} catch {
// Parsing/transport errors after 5xx headers stay service errors; aborts keep their meaning.
signal.throwIfAborted();
return "service";
} finally {
signal.removeEventListener("abort", cancel);
cancel();
reader.releaseLock();
}
}

/** Bounded fan-out: a slow/failed Goal cannot block the directory or its peers. */
export async function loadWorkspaceGoalSnapshots(
url: string,
Expand Down Expand Up @@ -83,7 +116,9 @@ export async function loadWorkspaceGoalSnapshots(
cache: "no-store", signal: controller.signal,
});
if (!response.ok) {
failure = response.status === 409 ? "revision" : response.status >= 500 ? "service" : "scope";
failure = response.status === 409 ? "revision" : response.status >= 500
? await statusFailure(response, controller.signal) : "scope";
controller.signal.throwIfAborted();
} else {
const raw = await response.json();
if (raw.workspace_registry_revision !== directory.registry_revision) failure = "revision";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,8 @@ const en = {
"startup.independent": "Other Goals remain available while this Goal loads.",
"startup.error.timeout": "The request timed out. Retry when the local service is ready.",
"startup.error.network": "The connection was interrupted. Retry to reconnect.",
"startup.error.service": "The local service is temporarily unavailable. Retry to continue.",
"startup.error.service": "The Goal status could not be read. Retry later; if the problem continues, check the status source.",
"startup.error.access": "The status source cannot access files or runtime directories required for this Goal. Check the permissions of the account running that source, then retry.",
"startup.error.revision": "The Goal directory changed during loading. Refresh to synchronize.",
"startup.error.scope": "This Goal is no longer available in the selected source. Refresh the directory.",
"startup.error.invalid": "The status response could not be read. Refresh or check for a LoopX update.",
Expand Down Expand Up @@ -1412,7 +1413,8 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"startup.independent": "此 Goal 的加载不会阻塞其他 Goal,你可以先查看已加载的内容。",
"startup.error.timeout": "读取超时,可在本地服务就绪后重试。",
"startup.error.network": "连接中断,请重试以重新连接。",
"startup.error.service": "本地服务暂时不可用,请重试继续加载。",
"startup.error.service": "Goal 状态读取失败。请稍后重试;若问题持续,请检查状态来源。",
"startup.error.access": "状态来源无法访问此 Goal 所需的文件或运行时目录。请检查该来源运行账户的访问权限,修复后重试。",
"startup.error.revision": "加载期间 Goal 目录发生变化,请刷新以同步。",
"startup.error.scope": "该 Goal 已不在当前来源中,请刷新目录。",
"startup.error.invalid": "无法解析状态响应,请刷新或检查 LoopX 更新。",
Expand Down
Loading