@morscherlab/mint-sdk 1.1.25 → 1.2.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +9 -7
  2. package/dist/{ExperimentSelectorModal-B6P2msT4.js → ExperimentSelectorModal-DyPAUCLw.js} +3 -3
  3. package/dist/{ExperimentSelectorModal-B6P2msT4.js.map → ExperimentSelectorModal-DyPAUCLw.js.map} +1 -1
  4. package/dist/{SettingsModal-BW3Ar-uy.js → SettingsModal-BgKY93zh.js} +4 -4
  5. package/dist/{SettingsModal-BW3Ar-uy.js.map → SettingsModal-BgKY93zh.js.map} +1 -1
  6. package/dist/__tests__/composables/useEventStream.test.d.ts +1 -0
  7. package/dist/components/AutoGroupModal.vue.d.ts +1 -1
  8. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +1 -1
  9. package/dist/components/JobsStatusTray.vue.d.ts +2 -9
  10. package/dist/components/SampleLegend.vue.d.ts +1 -1
  11. package/dist/components/SampleSelector.vue.d.ts +1 -1
  12. package/dist/components/SettingsModal.vue.d.ts +1 -1
  13. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -1
  14. package/dist/components/index.js +4 -4
  15. package/dist/{components-DQt0tcpY.js → components-DkHddRMl.js} +10 -16
  16. package/dist/components-DkHddRMl.js.map +1 -0
  17. package/dist/composables/index.d.ts +2 -3
  18. package/dist/composables/index.js +8 -8
  19. package/dist/composables/useEventStream.d.ts +43 -0
  20. package/dist/composables/useGeneratedAnalysis.d.ts +1 -3
  21. package/dist/composables/useJobsStatusTray.d.ts +1 -25
  22. package/dist/composables/usePluginClient.d.ts +3 -18
  23. package/dist/{composables-CkXDe7nm.js → composables-C_i90sGS.js} +32 -308
  24. package/dist/composables-C_i90sGS.js.map +1 -0
  25. package/dist/{experiment-Y9Iz7KYi.js → experiment-Cq145oEW.js} +2 -2
  26. package/dist/{experiment-Y9Iz7KYi.js.map → experiment-Cq145oEW.js.map} +1 -1
  27. package/dist/index.js +12 -12
  28. package/dist/install.js +4 -4
  29. package/dist/stores/auth.d.ts +0 -6
  30. package/dist/stores/index.js +2 -2
  31. package/dist/templates/index.js +2 -2
  32. package/dist/{templates-CovvRZ-9.js → templates-DsP51i-S.js} +2 -2
  33. package/dist/{templates-CovvRZ-9.js.map → templates-DsP51i-S.js.map} +1 -1
  34. package/dist/types/auth.d.ts +0 -1
  35. package/dist/{useApi-CDBq6z23.js → useApi-J6zFkHmn.js} +1 -2
  36. package/dist/useApi-J6zFkHmn.js.map +1 -0
  37. package/dist/{useControlSchema-BhoJzWzx.js → useControlSchema-BDx2Olha.js} +239 -103
  38. package/dist/useControlSchema-BDx2Olha.js.map +1 -0
  39. package/dist/{useExperimentSelector-D0pjj9WH.js → useExperimentSelector-CV4iNfMS.js} +2 -2
  40. package/dist/{useExperimentSelector-D0pjj9WH.js.map → useExperimentSelector-CV4iNfMS.js.map} +1 -1
  41. package/dist/{useFormBuilder-DiEcDis2.js → useFormBuilder-BLxG3BOX.js} +2 -2
  42. package/dist/{useFormBuilder-DiEcDis2.js.map → useFormBuilder-BLxG3BOX.js.map} +1 -1
  43. package/dist/{useProtocolTemplates-DHwSveE6.js → useProtocolTemplates-CS_SQl6U.js} +22 -113
  44. package/dist/useProtocolTemplates-CS_SQl6U.js.map +1 -0
  45. package/package.json +1 -1
  46. package/src/__tests__/components/GeneratedPluginView.test.ts +0 -5
  47. package/src/__tests__/components/JobsStatusTray.test.ts +20 -18
  48. package/src/__tests__/composables/useAuth.test.ts +23 -0
  49. package/src/__tests__/composables/useEventStream.test.ts +140 -0
  50. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +103 -0
  51. package/src/__tests__/composables/useJobsStatusTray.test.ts +25 -161
  52. package/src/__tests__/composables/usePluginClient.test.ts +8 -13
  53. package/src/__tests__/docs/frontendDocsCatalog.test.ts +22 -1
  54. package/src/components/JobsStatusTray.story.vue +6 -10
  55. package/src/components/JobsStatusTray.vue +1 -16
  56. package/src/composables/index.ts +0 -7
  57. package/src/composables/useAuth.ts +0 -3
  58. package/src/composables/useEventStream.ts +314 -0
  59. package/src/composables/useGeneratedAnalysis.ts +40 -109
  60. package/src/composables/useJobsStatusTray.ts +17 -184
  61. package/src/composables/usePluginClient.ts +45 -249
  62. package/src/stores/auth.ts +0 -1
  63. package/src/types/auth.ts +0 -1
  64. package/dist/__tests__/composables/usePluginConfig.test.d.ts +0 -15
  65. package/dist/components-DQt0tcpY.js.map +0 -1
  66. package/dist/composables/usePluginConfig.d.ts +0 -25
  67. package/dist/composables-CkXDe7nm.js.map +0 -1
  68. package/dist/useApi-CDBq6z23.js.map +0 -1
  69. package/dist/useControlSchema-BhoJzWzx.js.map +0 -1
  70. package/dist/useProtocolTemplates-DHwSveE6.js.map +0 -1
  71. package/src/__tests__/composables/usePluginConfig.test.ts +0 -497
  72. package/src/composables/usePluginConfig.ts +0 -250
@@ -8,10 +8,8 @@ import {
8
8
  type ShallowRef,
9
9
  } from 'vue'
10
10
 
11
- import type {
12
- JobsStatusTrayAdapter,
13
- PluginJobCenterSource,
14
- } from './useJobsStatusTray'
11
+ import type { PluginJobCenterSource } from './useJobsStatusTray'
12
+ import { useEventStream, type EventStreamMessage } from './useEventStream'
15
13
  import { normalizeJobState } from '../jobs'
16
14
  import type {
17
15
  GeneratedAnalysisJob,
@@ -104,8 +102,6 @@ export interface UsePluginJobsReturn extends PluginJobCenterSource {
104
102
  pause: () => void
105
103
  resume: () => void
106
104
  dispose: () => void
107
- /** @deprecated Pass the runtime itself to SDK job UI as its source. */
108
- jobsAdapter: JobsStatusTrayAdapter
109
105
  }
110
106
 
111
107
  export type UseGeneratedAnalysisReturn = UsePluginJobsReturn
@@ -125,6 +121,10 @@ interface PluginJobWaiter {
125
121
  abort?: () => void
126
122
  }
127
123
 
124
+ type PluginJobStreamData = GeneratedAnalysisJobInput | {
125
+ jobs: GeneratedAnalysisJobInput[]
126
+ }
127
+
128
128
  class GeneratedAnalysisRequestError extends Error {
129
129
  constructor(
130
130
  readonly status: number,
@@ -200,10 +200,6 @@ function createPluginJobs(
200
200
  const error = shallowRef<string | null>(null)
201
201
  const status = shallowRef<'idle' | 'initializing' | 'ready' | 'paused' | 'error' | 'disposed'>('idle')
202
202
  let pollTimer: ReturnType<typeof setTimeout> | undefined
203
- let streamReconnectTimer: ReturnType<typeof setTimeout> | undefined
204
- let streamController: AbortController | undefined
205
- let streamConnected = false
206
- let streamConnecting = false
207
203
  let disposed = false
208
204
  let paused = false
209
205
  let initialized = false
@@ -217,6 +213,17 @@ function createPluginJobs(
217
213
  const resultRetryAttempts = new Map<string, number>()
218
214
  const resultRetryTimers = new Map<string, ReturnType<typeof setTimeout>>()
219
215
  const downloadRefreshTimers = new Map<string, ReturnType<typeof setTimeout>>()
216
+ const jobStream = useEventStream<PluginJobStreamData>(`${baseUrl}/jobs/events`, {
217
+ transport,
218
+ immediate: false,
219
+ reconnectDelayMs: streamReconnectDelay,
220
+ parseJson: true,
221
+ errorLabel: 'Job stream',
222
+ onOpen: clearPoll,
223
+ onMessage: applyJobStreamMessage,
224
+ onError: schedulePollForActiveJobs,
225
+ onClose: schedulePollForActiveJobs,
226
+ })
220
227
 
221
228
  const latestResult = computed<GeneratedAnalysisResult | null>(() => {
222
229
  const job = currentCompletedJob()
@@ -242,7 +249,7 @@ function createPluginJobs(
242
249
  if (disposed) return
243
250
  initialized = true
244
251
  status.value = paused ? 'paused' : 'ready'
245
- if (streamEnabled && !paused) void connectJobStream()
252
+ if (streamEnabled && !paused) jobStream.start()
246
253
  })().catch((cause: unknown) => {
247
254
  if (!disposed) {
248
255
  error.value = errorMessage(cause, 'Failed to initialize generated analysis')
@@ -303,7 +310,7 @@ function createPluginJobs(
303
310
  )
304
311
  }
305
312
  const job = upsertJob(jobInput, definitionId)
306
- if (!streamConnected) schedulePoll()
313
+ if (!jobStream.isConnected.value) schedulePoll()
307
314
  return job
308
315
  } catch (cause) {
309
316
  error.value = errorMessage(cause, 'Failed to submit analysis')
@@ -466,7 +473,7 @@ function createPluginJobs(
466
473
  }
467
474
  replaceJobs(payload.jobs)
468
475
  error.value = null
469
- if (!paused && !streamConnected && jobs.value.some(job => ACTIVE_STATUSES.has(job.status))) {
476
+ if (!paused && !jobStream.isConnected.value && jobs.value.some(job => ACTIVE_STATUSES.has(job.status))) {
470
477
  schedulePoll()
471
478
  }
472
479
  return jobs.value
@@ -540,12 +547,12 @@ function createPluginJobs(
540
547
  waiting.add(waiter)
541
548
  jobWaiters.set(jobId, waiting)
542
549
  if (!paused) {
543
- if (streamEnabled) void connectJobStream()
544
- if (!streamConnected) schedulePoll()
550
+ if (streamEnabled) jobStream.start()
551
+ if (!jobStream.isConnected.value) schedulePoll()
545
552
  }
546
553
  void getJob(jobId, { signal: options.signal }).catch((cause: unknown) => {
547
554
  if (current && isRetryableJobLookupError(cause)) {
548
- if (!streamConnected) schedulePoll()
555
+ if (!jobStream.isConnected.value) schedulePoll()
549
556
  return
550
557
  }
551
558
  removeJobWaiter(jobId, waiter)
@@ -895,7 +902,7 @@ function createPluginJobs(
895
902
  }
896
903
 
897
904
  function schedulePoll(): void {
898
- if (disposed || paused || streamConnected || pollTimer !== undefined) return
905
+ if (disposed || paused || jobStream.isConnected.value || pollTimer !== undefined) return
899
906
  pollTimer = setTimeout(async () => {
900
907
  pollTimer = undefined
901
908
  if (disposed) return
@@ -908,85 +915,26 @@ function createPluginJobs(
908
915
  }, pollInterval)
909
916
  }
910
917
 
911
- async function connectJobStream(): Promise<void> {
912
- if (disposed || paused || streamConnecting || streamConnected) return
913
- streamConnecting = true
914
- streamController = new AbortController()
915
- try {
916
- const response = await transport(`${baseUrl}/jobs/events`, {
917
- method: 'GET',
918
- headers: { Accept: 'text/event-stream' },
919
- signal: streamController.signal,
920
- })
921
- if (!response.ok) throw new Error(`Job stream failed (${response.status})`)
922
- if (!response.body) throw new Error('Job stream response has no readable body')
923
- streamConnected = true
924
- if (pollTimer !== undefined) {
925
- clearTimeout(pollTimer)
926
- pollTimer = undefined
927
- }
928
- await consumeJobStream(response.body)
929
- } catch (cause) {
930
- if (!isAbortError(cause) && !disposed) {
931
- if (jobs.value.some(job => ACTIVE_STATUSES.has(job.status))) schedulePoll()
932
- }
933
- } finally {
934
- streamConnected = false
935
- streamConnecting = false
936
- streamController = undefined
937
- if (!disposed && !paused) scheduleStreamReconnect()
938
- }
918
+ function clearPoll(): void {
919
+ if (pollTimer === undefined) return
920
+ clearTimeout(pollTimer)
921
+ pollTimer = undefined
939
922
  }
940
923
 
941
- async function consumeJobStream(body: ReadableStream<Uint8Array>): Promise<void> {
942
- const reader = body.getReader()
943
- const decoder = new TextDecoder()
944
- let buffer = ''
945
- try {
946
- while (!disposed) {
947
- const { done, value } = await reader.read()
948
- buffer += decoder.decode(value, { stream: !done })
949
- const blocks = buffer.split(/\r?\n\r?\n/)
950
- buffer = blocks.pop() ?? ''
951
- for (const block of blocks) applyJobStreamEvent(block)
952
- if (done) {
953
- if (buffer.trim()) applyJobStreamEvent(buffer)
954
- return
955
- }
956
- }
957
- } finally {
958
- reader.releaseLock()
959
- }
924
+ function schedulePollForActiveJobs(): void {
925
+ if (jobs.value.some(job => ACTIVE_STATUSES.has(job.status))) schedulePoll()
960
926
  }
961
927
 
962
- function applyJobStreamEvent(block: string): void {
963
- let event = 'message'
964
- const data: string[] = []
965
- for (const line of block.split(/\r?\n/)) {
966
- if (!line || line.startsWith(':')) continue
967
- if (line.startsWith('event:')) event = line.slice('event:'.length).trim()
968
- if (line.startsWith('data:')) data.push(line.slice('data:'.length).trimStart())
969
- }
970
- if (!data.length) return
971
- const payload = JSON.parse(data.join('\n')) as (
972
- GeneratedAnalysisJobInput | { jobs: GeneratedAnalysisJobInput[] }
973
- )
974
- if (event === 'snapshot' && 'jobs' in payload) {
928
+ function applyJobStreamMessage(message: EventStreamMessage<PluginJobStreamData>): void {
929
+ if (message.event === 'snapshot' && 'jobs' in message.data) {
930
+ const payload = message.data
975
931
  replaceJobs(payload.jobs)
976
- } else if (event === 'job' || event === 'terminal') {
977
- upsertJob(payload as GeneratedAnalysisJobInput)
932
+ } else if (message.event === 'job' || message.event === 'terminal') {
933
+ upsertJob(message.data as GeneratedAnalysisJobInput)
978
934
  }
979
935
  error.value = null
980
936
  }
981
937
 
982
- function scheduleStreamReconnect(): void {
983
- if (disposed || paused || streamReconnectTimer !== undefined) return
984
- streamReconnectTimer = setTimeout(() => {
985
- streamReconnectTimer = undefined
986
- void connectJobStream()
987
- }, streamReconnectDelay)
988
- }
989
-
990
938
  async function cancelJob(job: GeneratedAnalysisJob | string): Promise<GeneratedAnalysisJob> {
991
939
  const jobId = typeof job === 'string' ? job : job.job_id
992
940
  const revision = beginJobOperation(jobId)
@@ -1024,25 +972,16 @@ function createPluginJobs(
1024
972
  return payload.deleted
1025
973
  }
1026
974
 
1027
- const jobsAdapter: JobsStatusTrayAdapter = {
1028
- listJobs: async () => ({ jobs: await refreshJobs() }),
1029
- cancelJob: async job => cancelJob(job as GeneratedAnalysisJob),
1030
- deleteJob: async job => deleteJob(job as GeneratedAnalysisJob),
1031
- clearFinishedJobs: async () => clearFinishedJobs(),
1032
- }
1033
975
  function pause(): void {
1034
976
  if (disposed || paused) return
1035
977
  paused = true
1036
978
  status.value = 'paused'
1037
- if (pollTimer !== undefined) clearTimeout(pollTimer)
1038
- if (streamReconnectTimer !== undefined) clearTimeout(streamReconnectTimer)
1039
- pollTimer = undefined
1040
- streamReconnectTimer = undefined
979
+ clearPoll()
1041
980
  for (const jobId of resultRetryTimers.keys()) clearResultRetry(jobId)
1042
981
  for (const jobId of downloadRefreshTimers.keys()) {
1043
982
  clearManagedDownloadRefresh(jobId)
1044
983
  }
1045
- streamController?.abort()
984
+ jobStream.stop()
1046
985
  }
1047
986
 
1048
987
  function resume(): void {
@@ -1053,7 +992,7 @@ function createPluginJobs(
1053
992
  for (const [jobId, result] of Object.entries(resultCache.value)) {
1054
993
  scheduleManagedDownloadRefresh(jobId, managedDownloadRefreshDelay(result))
1055
994
  }
1056
- if (streamEnabled) void connectJobStream()
995
+ if (streamEnabled) jobStream.start()
1057
996
  else if (jobs.value.some(job => ACTIVE_STATUSES.has(job.status))) schedulePoll()
1058
997
  }
1059
998
 
@@ -1062,10 +1001,7 @@ function createPluginJobs(
1062
1001
  disposed = true
1063
1002
  paused = true
1064
1003
  status.value = 'disposed'
1065
- if (pollTimer !== undefined) clearTimeout(pollTimer)
1066
- if (streamReconnectTimer !== undefined) clearTimeout(streamReconnectTimer)
1067
- pollTimer = undefined
1068
- streamReconnectTimer = undefined
1004
+ clearPoll()
1069
1005
  for (const timer of resultRetryTimers.values()) clearTimeout(timer)
1070
1006
  resultRetryTimers.clear()
1071
1007
  resultRetryAttempts.clear()
@@ -1075,7 +1011,7 @@ function createPluginJobs(
1075
1011
  for (const jobId of jobWaiters.keys()) rejectJobWaiters(jobId, cause)
1076
1012
  jobRevisions.clear()
1077
1013
  pendingJobOperations.clear()
1078
- streamController?.abort()
1014
+ jobStream.stop()
1079
1015
  }
1080
1016
 
1081
1017
  async function request<T>(path: string, init: RequestInit): Promise<T> {
@@ -1129,7 +1065,6 @@ function createPluginJobs(
1129
1065
  pause,
1130
1066
  resume,
1131
1067
  dispose,
1132
- jobsAdapter,
1133
1068
  }
1134
1069
  }
1135
1070
 
@@ -1229,10 +1164,6 @@ function createGeneratedSessionStorage(candidate?: Storage): GeneratedSessionSto
1229
1164
  }
1230
1165
  }
1231
1166
 
1232
- function isAbortError(error: unknown): boolean {
1233
- return error instanceof DOMException && error.name === 'AbortError'
1234
- }
1235
-
1236
1167
  function isSessionNotFoundError(error: unknown): boolean {
1237
1168
  return error instanceof GeneratedAnalysisRequestError
1238
1169
  && error.status === 404
@@ -5,7 +5,6 @@ import {
5
5
  ref,
6
6
  shallowRef,
7
7
  toValue,
8
- watch,
9
8
  type ComputedRef,
10
9
  type MaybeRefOrGetter,
11
10
  type Ref,
@@ -17,12 +16,7 @@ import type {
17
16
  JobListPayload,
18
17
  JobState,
19
18
  JobStateInput,
20
- JobStreamData,
21
19
  } from '../types/jobs'
22
- import type {
23
- PluginEventStreamMessage,
24
- UsePluginEventStreamReturn,
25
- } from './usePluginClient'
26
20
 
27
21
  export type JobsStatusTrayTab = 'active' | 'history'
28
22
 
@@ -146,32 +140,11 @@ function duplicatePluginJobId(jobId: string): Error {
146
140
  return new Error(`Duplicate plugin job id across combined sources: ${jobId}`)
147
141
  }
148
142
 
149
- /** @deprecated Prefer a {@link PluginJobCenterSource}. */
150
- export interface JobsStatusTrayAdapter {
151
- listJobs?: () => Promise<JobListPayload | readonly JobStateInput[]>
152
- cancelJob?: (job: JobState) => Promise<JobStateInput | void>
153
- deleteJob?: (job: JobState) => Promise<boolean | void>
154
- clearFinishedJobs?: (jobs: readonly JobState[]) => Promise<number | void>
155
- }
156
-
157
143
  export interface UsePluginJobCenterOptions {
158
144
  source?: MaybeRefOrGetter<PluginJobCenterSource | undefined>
159
- /** @deprecated Pass a source with its canonical jobs ref. */
160
- jobs?: MaybeRefOrGetter<readonly JobStateInput[] | undefined>
161
- /** @deprecated Pass source actions instead. */
162
- adapter?: MaybeRefOrGetter<JobsStatusTrayAdapter | undefined>
163
- /** @deprecated Event handling belongs to the source runtime. */
164
- eventStream?: MaybeRefOrGetter<UsePluginEventStreamReturn<JobStreamData> | undefined>
165
145
  loadOnMount?: boolean
166
146
  }
167
147
 
168
- export interface UseJobsStatusTrayOptions extends UsePluginJobCenterOptions {}
169
-
170
- export interface JobStreamMessage {
171
- event: string
172
- data: JobStreamData
173
- }
174
-
175
148
  export interface UsePluginJobCenterReturn {
176
149
  jobs: ComputedRef<JobState[]>
177
150
  activeJobs: ComputedRef<JobState[]>
@@ -191,30 +164,12 @@ export interface UsePluginJobCenterReturn {
191
164
  close: () => void
192
165
  toggle: () => void
193
166
  refresh: () => Promise<boolean>
194
- applyStreamMessage: (message: JobStreamMessage) => void
195
167
  cancelJob: (job: JobState) => Promise<boolean>
196
168
  deleteJob: (job: JobState) => Promise<boolean>
197
169
  clearFinished: () => Promise<boolean>
198
170
  isPending: (job: JobState) => boolean
199
171
  }
200
172
 
201
- export type UseJobsStatusTrayReturn = UsePluginJobCenterReturn
202
-
203
- interface LegacyJobsOverride {
204
- readonly input: readonly JobStateInput[] | undefined
205
- readonly jobs: JobState[]
206
- }
207
-
208
- function responseJobs(
209
- response: JobListPayload | readonly JobStateInput[],
210
- ): readonly JobStateInput[] {
211
- return 'jobs' in response ? response.jobs : response
212
- }
213
-
214
- function isSnapshotPayload(data: JobStreamData): data is { jobs: JobStateInput[] } {
215
- return 'jobs' in data && Array.isArray(data.jobs)
216
- }
217
-
218
173
  function errorMessage(error: unknown, fallback: string): string {
219
174
  return error instanceof Error ? error.message : fallback
220
175
  }
@@ -228,84 +183,30 @@ function errorMessage(error: unknown, fallback: string): string {
228
183
  export function usePluginJobCenter(
229
184
  options: UsePluginJobCenterOptions = {},
230
185
  ): UsePluginJobCenterReturn {
231
- const legacyOverride = shallowRef<LegacyJobsOverride | null>(null)
232
186
  const isOpen = ref(false)
233
187
  const activeTab = ref<JobsStatusTrayTab>('active')
234
188
  const isLoading = ref(false)
235
189
  const error = shallowRef<string | null>(null)
236
190
  const pendingJobIds = ref(new Set<string>())
237
- const jobRevisions = new Map<string, number>()
238
- let stateRevision = 0
239
191
  let requestGeneration = 0
240
192
  let active = true
241
193
  const source = computed(() => toValue(options.source))
242
- const adapter = computed(() => toValue(options.adapter))
243
194
 
244
- const jobs = computed<JobState[]>(() => {
245
- const currentSource = source.value
246
- if (currentSource) {
247
- return [...toValue(currentSource.jobs)].map(normalizeJobState)
248
- }
249
- const input = toValue(options.jobs)
250
- const override = legacyOverride.value
251
- if (override && override.input === input) return override.jobs
252
- return input ? [...input].map(normalizeJobState) : []
253
- })
195
+ const jobs = computed<JobState[]>(() => (
196
+ source.value ? [...toValue(source.value.jobs)].map(normalizeJobState) : []
197
+ ))
254
198
  const activeJobs = computed(() => jobs.value.filter(job => isActiveJobStatus(job.status)))
255
199
  const historyJobs = computed(() => jobs.value.filter(job => !isActiveJobStatus(job.status)))
256
200
  const deletableJobs = computed(() => historyJobs.value.filter(job => job.can_delete))
257
201
  const featuredJob = computed(() => activeJobs.value[0] ?? historyJobs.value[0] ?? null)
258
- const canRefresh = computed(() => Boolean(source.value?.refreshJobs || adapter.value?.listJobs))
259
- const canCancel = computed(() => Boolean(source.value?.cancelJob || adapter.value?.cancelJob))
260
- const canDelete = computed(() => Boolean(source.value?.deleteJob || adapter.value?.deleteJob))
202
+ const canRefresh = computed(() => Boolean(source.value?.refreshJobs))
203
+ const canCancel = computed(() => Boolean(source.value?.cancelJob))
204
+ const canDelete = computed(() => Boolean(source.value?.deleteJob))
261
205
  const canClearFinished = computed(() => Boolean(
262
206
  source.value?.clearFinishedJobs
263
207
  || source.value?.deleteJob
264
- || adapter.value?.clearFinishedJobs
265
- || adapter.value?.deleteJob,
266
208
  ))
267
209
 
268
- function bumpJobRevision(jobId: string): void {
269
- jobRevisions.set(jobId, (jobRevisions.get(jobId) ?? 0) + 1)
270
- }
271
-
272
- function replaceLegacyJobs(nextJobs: readonly JobStateInput[]): void {
273
- const normalized = nextJobs.map(normalizeJobState)
274
- const touchedJobIds = new Set([
275
- ...jobs.value.map(job => job.job_id),
276
- ...normalized.map(job => job.job_id),
277
- ])
278
- touchedJobIds.forEach(bumpJobRevision)
279
- legacyOverride.value = {
280
- input: toValue(options.jobs),
281
- jobs: normalized,
282
- }
283
- stateRevision += 1
284
- }
285
-
286
- function upsertLegacyJob(nextJob: JobStateInput): void {
287
- const normalized = normalizeJobState(nextJob)
288
- const existingIndex = jobs.value.findIndex(job => job.job_id === normalized.job_id)
289
- const nextJobs = [...jobs.value]
290
- if (existingIndex === -1) nextJobs.unshift(normalized)
291
- else nextJobs[existingIndex] = normalized
292
- legacyOverride.value = {
293
- input: toValue(options.jobs),
294
- jobs: nextJobs,
295
- }
296
- bumpJobRevision(normalized.job_id)
297
- stateRevision += 1
298
- }
299
-
300
- function removeLegacyJobs(jobIds: Set<string>): void {
301
- legacyOverride.value = {
302
- input: toValue(options.jobs),
303
- jobs: jobs.value.filter(job => !jobIds.has(job.job_id)),
304
- }
305
- jobIds.forEach(bumpJobRevision)
306
- stateRevision += 1
307
- }
308
-
309
210
  function setPending(jobId: string, pending: boolean): void {
310
211
  const next = new Set(pendingJobIds.value)
311
212
  if (pending) next.add(jobId)
@@ -313,35 +214,15 @@ export function usePluginJobCenter(
313
214
  pendingJobIds.value = next
314
215
  }
315
216
 
316
- function applyStreamMessage(message: JobStreamMessage): void {
317
- if (!active || source.value) return
318
- if (message.event === 'snapshot' && isSnapshotPayload(message.data)) {
319
- replaceLegacyJobs(message.data.jobs)
320
- return
321
- }
322
- if (
323
- (message.event === 'job' || message.event === 'terminal')
324
- && !isSnapshotPayload(message.data)
325
- ) {
326
- upsertLegacyJob(message.data)
327
- }
328
- }
329
-
330
217
  async function refresh(): Promise<boolean> {
331
- const currentSource = source.value
332
- const listJobs = currentSource?.refreshJobs ?? adapter.value?.listJobs
333
- if (!listJobs) return false
218
+ const refreshJobs = source.value?.refreshJobs
219
+ if (!refreshJobs) return false
334
220
  const generation = ++requestGeneration
335
- const revision = stateRevision
336
221
  isLoading.value = true
337
222
  error.value = null
338
223
  try {
339
- const response = await listJobs()
340
- if (!active || generation !== requestGeneration || revision !== stateRevision) {
341
- return false
342
- }
343
- if (!currentSource && response) replaceLegacyJobs(responseJobs(response))
344
- return true
224
+ await refreshJobs()
225
+ return active && generation === requestGeneration
345
226
  } catch (cause) {
346
227
  if (active && generation === requestGeneration) {
347
228
  error.value = errorMessage(cause, 'Failed to load jobs')
@@ -354,23 +235,14 @@ export function usePluginJobCenter(
354
235
 
355
236
  async function cancelJob(job: JobState): Promise<boolean> {
356
237
  const sourceCancel = source.value?.cancelJob
357
- const adapterCancel = adapter.value?.cancelJob
358
- if (!job.can_cancel || (!sourceCancel && !adapterCancel) || pendingJobIds.value.has(job.job_id)) {
238
+ if (!job.can_cancel || !sourceCancel || pendingJobIds.value.has(job.job_id)) {
359
239
  return false
360
240
  }
361
- const revision = jobRevisions.get(job.job_id) ?? 0
362
241
  setPending(job.job_id, true)
363
242
  error.value = null
364
243
  try {
365
- const updated = sourceCancel
366
- ? await sourceCancel(job.job_id)
367
- : await adapterCancel!(job)
368
- if (!active) return false
369
- if (!sourceCancel && revision === (jobRevisions.get(job.job_id) ?? 0)) {
370
- if (updated) upsertLegacyJob(updated)
371
- else await refresh()
372
- }
373
- return true
244
+ await sourceCancel(job.job_id)
245
+ return active
374
246
  } catch (cause) {
375
247
  if (active) error.value = errorMessage(cause, 'Failed to cancel job')
376
248
  return false
@@ -381,22 +253,14 @@ export function usePluginJobCenter(
381
253
 
382
254
  async function deleteJob(job: JobState): Promise<boolean> {
383
255
  const sourceDelete = source.value?.deleteJob
384
- const adapterDelete = adapter.value?.deleteJob
385
- if (!job.can_delete || (!sourceDelete && !adapterDelete) || pendingJobIds.value.has(job.job_id)) {
256
+ if (!job.can_delete || !sourceDelete || pendingJobIds.value.has(job.job_id)) {
386
257
  return false
387
258
  }
388
259
  setPending(job.job_id, true)
389
260
  error.value = null
390
261
  try {
391
- const deleted = sourceDelete
392
- ? await sourceDelete(job.job_id)
393
- : await adapterDelete!(job)
394
- if (!active) return false
395
- if (!sourceDelete) {
396
- if (deleted === false) await refresh()
397
- else removeLegacyJobs(new Set([job.job_id]))
398
- }
399
- return deleted !== false
262
+ const deleted = await sourceDelete(job.job_id)
263
+ return active && deleted !== false
400
264
  } catch (cause) {
401
265
  if (active) error.value = errorMessage(cause, 'Failed to delete job')
402
266
  return false
@@ -420,36 +284,13 @@ export function usePluginJobCenter(
420
284
  const results = await Promise.all(targets.map(job => deleteJob(job)))
421
285
  return results.every(Boolean)
422
286
  }
423
- const currentAdapter = adapter.value
424
- if (currentAdapter?.clearFinishedJobs) {
425
- const deleted = await currentAdapter.clearFinishedJobs(targets)
426
- if (!active) return false
427
- if (typeof deleted === 'number' && deleted !== targets.length) {
428
- await refresh()
429
- return false
430
- }
431
- removeLegacyJobs(new Set(targets.map(job => job.job_id)))
432
- return true
433
- }
434
- if (!currentAdapter?.deleteJob) return false
435
- const results = await Promise.all(targets.map(job => deleteJob(job)))
436
- return results.every(Boolean)
287
+ return false
437
288
  } catch (cause) {
438
289
  if (active) error.value = errorMessage(cause, 'Failed to clear jobs')
439
290
  return false
440
291
  }
441
292
  }
442
293
 
443
- if (options.eventStream !== undefined) {
444
- watch(
445
- () => toValue(options.eventStream)?.lastMessage.value ?? null,
446
- (message: PluginEventStreamMessage<JobStreamData> | null) => {
447
- if (message) applyStreamMessage(message)
448
- },
449
- { immediate: true },
450
- )
451
- }
452
-
453
294
  onMounted(() => {
454
295
  if (options.loadOnMount !== false) void refresh()
455
296
  })
@@ -477,17 +318,9 @@ export function usePluginJobCenter(
477
318
  close: () => { isOpen.value = false },
478
319
  toggle: () => { isOpen.value = !isOpen.value },
479
320
  refresh,
480
- applyStreamMessage,
481
321
  cancelJob,
482
322
  deleteJob,
483
323
  clearFinished,
484
324
  isPending: job => pendingJobIds.value.has(job.job_id),
485
325
  }
486
326
  }
487
-
488
- /** Backward-compatible name for the plugin job-center controller. */
489
- export function useJobsStatusTray(
490
- options: UseJobsStatusTrayOptions = {},
491
- ): UseJobsStatusTrayReturn {
492
- return usePluginJobCenter(options)
493
- }