|
| 1 | +import type { RootLogger, ScopedLogger } from "@posthog/di/logger"; |
| 2 | +import type { IAnalytics } from "@posthog/platform/analytics"; |
1 | 3 | import { |
2 | | - ROOT_LOGGER, |
3 | | - type RootLogger, |
4 | | - type ScopedLogger, |
5 | | -} from "@posthog/di/logger"; |
6 | | -import { |
7 | | - ANALYTICS_SERVICE, |
8 | | - type IAnalytics, |
9 | | -} from "@posthog/platform/analytics"; |
10 | | -import type { StoredLogEntry } from "@posthog/shared"; |
11 | | -import { |
| 4 | + type CloudTaskPermissionRequestUpdate, |
| 5 | + isTerminalStatus, |
12 | 6 | mcpToolKey, |
13 | 7 | posthogToolMeta, |
| 8 | + type StoredLogEntry, |
14 | 9 | serializeError, |
| 10 | + type TaskRunStatus, |
15 | 11 | TypedEventEmitter, |
16 | 12 | } from "@posthog/shared"; |
17 | 13 | import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; |
18 | | -import { inject, injectable, optional, preDestroy } from "inversify"; |
19 | | -import type { CloudTaskPermissionRequestUpdate } from "./cloud-task-types"; |
20 | | -import { |
21 | | - CLOUD_TASK_AUTH, |
22 | | - type ICloudTaskAuth, |
23 | | - MCP_RELAY_EXECUTOR, |
24 | | - type McpRelayExecutor, |
25 | | -} from "./identifiers"; |
| 14 | +import type { ICloudTaskAuth, McpRelayExecutor } from "./identifiers"; |
26 | 15 | import { |
27 | 16 | CloudTaskEvent, |
28 | 17 | type CloudTaskEvents, |
29 | | - isTerminalStatus, |
30 | 18 | type SendCommandInput, |
31 | 19 | type SendCommandOutput, |
32 | 20 | type StopInput, |
33 | 21 | type StopOutput, |
34 | | - type TaskRunStatus, |
35 | 22 | type WatchInput, |
36 | 23 | } from "./schemas"; |
37 | 24 | import { type SseEvent, SseEventParser } from "./sse-parser"; |
@@ -435,23 +422,45 @@ function sandboxAlivePayload(watcher: { lastSandboxAlive: boolean | null }): { |
435 | 422 | : { sandboxAlive: watcher.lastSandboxAlive }; |
436 | 423 | } |
437 | 424 |
|
438 | | -@injectable() |
439 | | -export class CloudTaskService extends TypedEventEmitter<CloudTaskEvents> { |
| 425 | +export interface CloudTaskEngineDependencies { |
| 426 | + auth: ICloudTaskAuth; |
| 427 | + analytics: IAnalytics; |
| 428 | + logger: RootLogger; |
| 429 | + mcpRelayExecutor?: McpRelayExecutor | null; |
| 430 | + streamFetch?: CloudTaskFetch; |
| 431 | +} |
| 432 | + |
| 433 | +export type CloudTaskFetch = ( |
| 434 | + input: string | URL | Request, |
| 435 | + init?: RequestInit, |
| 436 | +) => Promise<Response>; |
| 437 | + |
| 438 | +export function createCloudTaskEngine( |
| 439 | + dependencies: CloudTaskEngineDependencies, |
| 440 | +): CloudTaskEngine { |
| 441 | + return new CloudTaskEngine(dependencies); |
| 442 | +} |
| 443 | + |
| 444 | +export class CloudTaskEngine extends TypedEventEmitter<CloudTaskEvents> { |
440 | 445 | private watchers = new Map<string, WatcherState>(); |
441 | 446 | private readonly log: ScopedLogger; |
442 | | - |
443 | | - constructor( |
444 | | - @inject(CLOUD_TASK_AUTH) |
445 | | - private readonly auth: ICloudTaskAuth, |
446 | | - @inject(ANALYTICS_SERVICE) |
447 | | - private readonly analytics: IAnalytics, |
448 | | - @inject(ROOT_LOGGER) |
449 | | - logger: RootLogger, |
450 | | - @inject(MCP_RELAY_EXECUTOR) |
451 | | - @optional() |
452 | | - private readonly mcpRelayExecutor: McpRelayExecutor | null = null, |
453 | | - ) { |
| 447 | + private readonly auth: ICloudTaskAuth; |
| 448 | + private readonly analytics: IAnalytics; |
| 449 | + private readonly mcpRelayExecutor: McpRelayExecutor | null; |
| 450 | + private readonly streamFetch: CloudTaskFetch; |
| 451 | + |
| 452 | + constructor({ |
| 453 | + auth, |
| 454 | + analytics, |
| 455 | + logger, |
| 456 | + mcpRelayExecutor = null, |
| 457 | + streamFetch = globalThis.fetch.bind(globalThis), |
| 458 | + }: CloudTaskEngineDependencies) { |
454 | 459 | super(); |
| 460 | + this.auth = auth; |
| 461 | + this.analytics = analytics; |
| 462 | + this.mcpRelayExecutor = mcpRelayExecutor; |
| 463 | + this.streamFetch = streamFetch; |
455 | 464 | this.log = logger.scope("cloud-task"); |
456 | 465 | } |
457 | 466 |
|
@@ -770,6 +779,22 @@ export class CloudTaskService extends TypedEventEmitter<CloudTaskEvents> { |
770 | 779 | void this.bootstrapWatcher(key); |
771 | 780 | } |
772 | 781 |
|
| 782 | + reconnectIfDisconnected(taskId: string, runId: string): void { |
| 783 | + const key = watcherKey(taskId, runId); |
| 784 | + const watcher = this.watchers.get(key); |
| 785 | + if ( |
| 786 | + !watcher || |
| 787 | + watcher.sseAbortController || |
| 788 | + watcher.reconnectTimeoutId || |
| 789 | + watcher.isBootstrapping || |
| 790 | + isTerminalStatus(watcher.lastStatus) |
| 791 | + ) { |
| 792 | + return; |
| 793 | + } |
| 794 | + |
| 795 | + void this.connectSse(key); |
| 796 | + } |
| 797 | + |
773 | 798 | // Resets a watcher to its pre-bootstrap state so bootstrapWatcher can rebuild it from server truth. |
774 | 799 | private resetWatcherForRebootstrap(watcher: WatcherState): void { |
775 | 800 | watcher.reconnectAttempts = 0; |
@@ -959,7 +984,6 @@ export class CloudTaskService extends TypedEventEmitter<CloudTaskEvents> { |
959 | 984 | } |
960 | 985 | } |
961 | 986 |
|
962 | | - @preDestroy() |
963 | 987 | unwatchAll(): void { |
964 | 988 | for (const key of [...this.watchers.keys()]) { |
965 | 989 | this.stopWatcher(key); |
@@ -1306,7 +1330,7 @@ export class CloudTaskService extends TypedEventEmitter<CloudTaskEvents> { |
1306 | 1330 | try { |
1307 | 1331 | // The proxy authenticates with the run-scoped Bearer token; the Django leg uses the session. |
1308 | 1332 | const response = usingProxy |
1309 | | - ? await fetch(url.toString(), { |
| 1333 | + ? await this.streamFetch(url.toString(), { |
1310 | 1334 | method: "GET", |
1311 | 1335 | headers, |
1312 | 1336 | signal: controller.signal, |
|
0 commit comments