@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.
- package/README.md +9 -7
- package/dist/{ExperimentSelectorModal-B6P2msT4.js → ExperimentSelectorModal-DyPAUCLw.js} +3 -3
- package/dist/{ExperimentSelectorModal-B6P2msT4.js.map → ExperimentSelectorModal-DyPAUCLw.js.map} +1 -1
- package/dist/{SettingsModal-BW3Ar-uy.js → SettingsModal-BgKY93zh.js} +4 -4
- package/dist/{SettingsModal-BW3Ar-uy.js.map → SettingsModal-BgKY93zh.js.map} +1 -1
- package/dist/__tests__/composables/useEventStream.test.d.ts +1 -0
- package/dist/components/AutoGroupModal.vue.d.ts +1 -1
- package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +1 -1
- package/dist/components/JobsStatusTray.vue.d.ts +2 -9
- package/dist/components/SampleLegend.vue.d.ts +1 -1
- package/dist/components/SampleSelector.vue.d.ts +1 -1
- package/dist/components/SettingsModal.vue.d.ts +1 -1
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -1
- package/dist/components/index.js +4 -4
- package/dist/{components-DQt0tcpY.js → components-DkHddRMl.js} +10 -16
- package/dist/components-DkHddRMl.js.map +1 -0
- package/dist/composables/index.d.ts +2 -3
- package/dist/composables/index.js +8 -8
- package/dist/composables/useEventStream.d.ts +43 -0
- package/dist/composables/useGeneratedAnalysis.d.ts +1 -3
- package/dist/composables/useJobsStatusTray.d.ts +1 -25
- package/dist/composables/usePluginClient.d.ts +3 -18
- package/dist/{composables-CkXDe7nm.js → composables-C_i90sGS.js} +32 -308
- package/dist/composables-C_i90sGS.js.map +1 -0
- package/dist/{experiment-Y9Iz7KYi.js → experiment-Cq145oEW.js} +2 -2
- package/dist/{experiment-Y9Iz7KYi.js.map → experiment-Cq145oEW.js.map} +1 -1
- package/dist/index.js +12 -12
- package/dist/install.js +4 -4
- package/dist/stores/auth.d.ts +0 -6
- package/dist/stores/index.js +2 -2
- package/dist/templates/index.js +2 -2
- package/dist/{templates-CovvRZ-9.js → templates-DsP51i-S.js} +2 -2
- package/dist/{templates-CovvRZ-9.js.map → templates-DsP51i-S.js.map} +1 -1
- package/dist/types/auth.d.ts +0 -1
- package/dist/{useApi-CDBq6z23.js → useApi-J6zFkHmn.js} +1 -2
- package/dist/useApi-J6zFkHmn.js.map +1 -0
- package/dist/{useControlSchema-BhoJzWzx.js → useControlSchema-BDx2Olha.js} +239 -103
- package/dist/useControlSchema-BDx2Olha.js.map +1 -0
- package/dist/{useExperimentSelector-D0pjj9WH.js → useExperimentSelector-CV4iNfMS.js} +2 -2
- package/dist/{useExperimentSelector-D0pjj9WH.js.map → useExperimentSelector-CV4iNfMS.js.map} +1 -1
- package/dist/{useFormBuilder-DiEcDis2.js → useFormBuilder-BLxG3BOX.js} +2 -2
- package/dist/{useFormBuilder-DiEcDis2.js.map → useFormBuilder-BLxG3BOX.js.map} +1 -1
- package/dist/{useProtocolTemplates-DHwSveE6.js → useProtocolTemplates-CS_SQl6U.js} +22 -113
- package/dist/useProtocolTemplates-CS_SQl6U.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/components/GeneratedPluginView.test.ts +0 -5
- package/src/__tests__/components/JobsStatusTray.test.ts +20 -18
- package/src/__tests__/composables/useAuth.test.ts +23 -0
- package/src/__tests__/composables/useEventStream.test.ts +140 -0
- package/src/__tests__/composables/useGeneratedAnalysis.test.ts +103 -0
- package/src/__tests__/composables/useJobsStatusTray.test.ts +25 -161
- package/src/__tests__/composables/usePluginClient.test.ts +8 -13
- package/src/__tests__/docs/frontendDocsCatalog.test.ts +22 -1
- package/src/components/JobsStatusTray.story.vue +6 -10
- package/src/components/JobsStatusTray.vue +1 -16
- package/src/composables/index.ts +0 -7
- package/src/composables/useAuth.ts +0 -3
- package/src/composables/useEventStream.ts +314 -0
- package/src/composables/useGeneratedAnalysis.ts +40 -109
- package/src/composables/useJobsStatusTray.ts +17 -184
- package/src/composables/usePluginClient.ts +45 -249
- package/src/stores/auth.ts +0 -1
- package/src/types/auth.ts +0 -1
- package/dist/__tests__/composables/usePluginConfig.test.d.ts +0 -15
- package/dist/components-DQt0tcpY.js.map +0 -1
- package/dist/composables/usePluginConfig.d.ts +0 -25
- package/dist/composables-CkXDe7nm.js.map +0 -1
- package/dist/useApi-CDBq6z23.js.map +0 -1
- package/dist/useControlSchema-BhoJzWzx.js.map +0 -1
- package/dist/useProtocolTemplates-DHwSveE6.js.map +0 -1
- package/src/__tests__/composables/usePluginConfig.test.ts +0 -497
- 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
|
-
|
|
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)
|
|
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 (!
|
|
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 && !
|
|
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)
|
|
544
|
-
if (!
|
|
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 (!
|
|
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 ||
|
|
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
|
-
|
|
912
|
-
if (
|
|
913
|
-
|
|
914
|
-
|
|
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
|
-
|
|
942
|
-
|
|
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
|
|
963
|
-
|
|
964
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
246
|
-
|
|
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
|
|
259
|
-
const canCancel = computed(() => Boolean(source.value?.cancelJob
|
|
260
|
-
const canDelete = computed(() => Boolean(source.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
|
|
332
|
-
|
|
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
|
-
|
|
340
|
-
|
|
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
|
-
|
|
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
|
-
|
|
366
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|