(jobService: JobService)
| 66 | }; |
| 67 | |
| 68 | export function useJobStream(jobService: JobService) { |
| 69 | const [state, setState] = useState<JobState>(INITIAL_STATE); |
| 70 | const streamRef = useRef<JobStream | null>(null); |
| 71 | |
| 72 | /** Consume a JobStream: drive the React state machine from its |
| 73 | * events. Shared by `start` (new submissions) and `attach` |
| 74 | * (Fix #14 resume-after-reload). */ |
| 75 | const consumeStream = useCallback((stream: JobStream) => { |
| 76 | streamRef.current = stream; |
| 77 | (async () => { |
| 78 | try { |
| 79 | for await (const event of stream) { |
| 80 | switch (event.kind) { |
| 81 | case JobEventKind.JOB_EVENT_KIND_PROGRESS: { |
| 82 | const d = event.detail ?? EMPTY_DETAIL; |
| 83 | setState((s) => { |
| 84 | // Don't reopen a completed stage with new progress events |
| 85 | // (e.g. enrichment batches fire "submitting" after parse already completed it) |
| 86 | const existing = s.stages[event.phase]; |
| 87 | const stageUpdate = |
| 88 | existing?.status === 'completed' |
| 89 | ? { |
| 90 | ...existing, |
| 91 | current: d.current, |
| 92 | total: d.total, |
| 93 | message: event.message, |
| 94 | } |
| 95 | : { |
| 96 | status: 'active' as StageStatus, |
| 97 | current: d.current, |
| 98 | total: d.total, |
| 99 | message: event.message, |
| 100 | fileName: d.fileName || undefined, |
| 101 | ...(event.phase === JobPhase.JOB_PHASE_FETCHING |
| 102 | ? { format: 'bytes' as const } |
| 103 | : {}), |
| 104 | }; |
| 105 | return { |
| 106 | ...s, |
| 107 | // Keep "enriching" status during enrichment progress updates |
| 108 | status: s.status === 'enriching' ? 'enriching' : s.status, |
| 109 | phase: event.phase, |
| 110 | message: event.message, |
| 111 | detail: d, |
| 112 | nodesCreated: d.nodesCreated || s.nodesCreated, |
| 113 | relationshipsCreated: |
| 114 | d.relationshipsCreated || s.relationshipsCreated, |
| 115 | stages: { ...s.stages, [event.phase]: stageUpdate }, |
| 116 | }; |
| 117 | }); |
| 118 | break; |
| 119 | } |
| 120 | case JobEventKind.JOB_EVENT_KIND_STAGE_COMPLETE: |
| 121 | setState((s) => ({ |
| 122 | ...s, |
| 123 | stages: { |
| 124 | ...s.stages, |
| 125 | [event.phase]: { |
no test coverage detected