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
12 changes: 11 additions & 1 deletion bff/src/routes/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,23 +75,33 @@ function sendSSE(
'X-Accel-Buffering': 'no',
});

let closed = false;

const heartbeat = setInterval(() => {
res.write(': keepalive\n\n');
if (!closed) res.write(': keepalive\n\n');
}, 15_000);

res.on('close', () => {
closed = true;
clearInterval(heartbeat);
});

const onProgress: LifecycleProgressCallback = (steps) => {
if (closed) return;
const data = JSON.stringify({ steps: steps.map(s => ({ ...s })) });
res.write(`event: progress\ndata: ${data}\n\n`);
};

serviceFn(onProgress)
.then((result) => {
clearInterval(heartbeat);
if (closed) return;
res.write(`event: complete\ndata: ${JSON.stringify(result)}\n\n`);
res.end();
})
.catch(() => {
clearInterval(heartbeat);
if (closed) return;
const fallback: LifecycleResponse = {
success: false,
message: 'Operation failed',
Expand Down
31 changes: 28 additions & 3 deletions src/app/hooks/usePluginLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ import { LifecycleResponse, LifecycleStep, LifecycleOperation, LifecycleProgress

const API_BASE = '/community-plugins-admin/api/plugins';

class StreamInterruptedError extends Error {
constructor() {
super(
'Connection lost during operation. The operation may still be running. ' +
'Close this dialog and refresh to check the current status.',
);
this.name = 'StreamInterruptedError';
}
}

export interface PluginLifecycleState {
loading: boolean;
operation: LifecycleOperation | null;
Expand Down Expand Up @@ -54,7 +64,13 @@ async function lifecycleStreamRequest(
let finalResult: LifecycleResponse | null = null;

for (;;) {
const { done, value } = await reader.read();
let done: boolean;
let value: string | undefined;
try {
({ done, value } = await reader.read());
} catch {
throw new StreamInterruptedError();
}
if (done) break;

buffer += value;
Expand Down Expand Up @@ -87,7 +103,7 @@ async function lifecycleStreamRequest(
}

if (!finalResult) {
throw new Error('Stream ended without a complete event');
throw new StreamInterruptedError();
}

return finalResult;
Expand Down Expand Up @@ -124,7 +140,16 @@ export function usePluginLifecycle(): PluginLifecycle {
message,
steps: [],
};
setState({ loading: false, operation, steps: [], result: failedResult, error: message });
setState((prev) => ({
loading: false,
operation,
steps: err instanceof StreamInterruptedError ? prev.steps : [],
result: {
...failedResult,
steps: err instanceof StreamInterruptedError ? prev.steps : [],
},
error: message,
}));
return failedResult;
}
},
Expand Down
Loading