@morscherlab/mint-sdk 1.1.24 → 1.2.0-beta.1
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-DoS2fzyz.js → SettingsModal-BgKY93zh.js} +5 -4
- package/dist/SettingsModal-BgKY93zh.js.map +1 -0
- 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 +29 -10
- 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-CW3hE1T3.js → components-DkHddRMl.js} +39 -40
- 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 +43 -19
- 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 +16 -10
- package/src/components/JobsStatusTray.vue +22 -28
- package/src/components/SettingsModal.schema.ts +3 -0
- 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/SettingsModal-DoS2fzyz.js.map +0 -1
- package/dist/__tests__/composables/usePluginConfig.test.d.ts +0 -15
- package/dist/components-CW3hE1T3.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
|
@@ -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
|
-
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
computed,
|
|
3
|
-
getCurrentInstance,
|
|
4
3
|
onMounted,
|
|
5
|
-
onUnmounted,
|
|
6
4
|
ref,
|
|
7
5
|
shallowRef,
|
|
8
6
|
watch,
|
|
@@ -28,6 +26,12 @@ import {
|
|
|
28
26
|
pluginEndpointRequestParts,
|
|
29
27
|
requestBodyFromPayload,
|
|
30
28
|
} from './pluginEndpointBuilder'
|
|
29
|
+
import {
|
|
30
|
+
useEventStream,
|
|
31
|
+
type EventStreamMessage,
|
|
32
|
+
type EventStreamTransport,
|
|
33
|
+
type UseEventStreamReturn,
|
|
34
|
+
} from './useEventStream'
|
|
31
35
|
import type {
|
|
32
36
|
ExperimentSummary,
|
|
33
37
|
GeneratedJobDefinition,
|
|
@@ -204,13 +208,7 @@ export interface UsePluginSettingsReturn<TSettings = Record<string, unknown>> {
|
|
|
204
208
|
reset: () => void
|
|
205
209
|
}
|
|
206
210
|
|
|
207
|
-
export
|
|
208
|
-
event: string
|
|
209
|
-
data: TData
|
|
210
|
-
id?: string
|
|
211
|
-
retry?: number
|
|
212
|
-
raw: string
|
|
213
|
-
}
|
|
211
|
+
export type PluginEventStreamMessage<TData = string> = EventStreamMessage<TData>
|
|
214
212
|
|
|
215
213
|
export interface PluginEventStreamOptions<TData = string> {
|
|
216
214
|
/** Override the generated or platform-injected API base URL. */
|
|
@@ -232,15 +230,7 @@ export interface PluginEventStreamOptions<TData = string> {
|
|
|
232
230
|
onError?: (error: unknown) => void
|
|
233
231
|
}
|
|
234
232
|
|
|
235
|
-
export
|
|
236
|
-
isConnected: Ref<boolean>
|
|
237
|
-
isConnecting: Ref<boolean>
|
|
238
|
-
error: Ref<string | null>
|
|
239
|
-
lastMessage: Ref<PluginEventStreamMessage<TData> | null>
|
|
240
|
-
lastEventId: Ref<string | null>
|
|
241
|
-
start: () => void
|
|
242
|
-
stop: () => void
|
|
243
|
-
}
|
|
233
|
+
export type UsePluginEventStreamReturn<TData = string> = UseEventStreamReturn<TData>
|
|
244
234
|
|
|
245
235
|
export type PluginFormDataValue =
|
|
246
236
|
| string
|
|
@@ -544,11 +534,6 @@ export async function downloadPluginEndpoint(
|
|
|
544
534
|
return blob
|
|
545
535
|
}
|
|
546
536
|
|
|
547
|
-
/** Return a generated plugin client from setup code with a stable typed identity. */
|
|
548
|
-
export function usePluginClient<TClient>(client: TClient): TClient {
|
|
549
|
-
return client
|
|
550
|
-
}
|
|
551
|
-
|
|
552
537
|
function normalizePluginSettingsOptions(options: UsePluginSettingsOptions | string = {}): UsePluginSettingsOptions {
|
|
553
538
|
return typeof options === 'string' ? { pluginName: options } : options
|
|
554
539
|
}
|
|
@@ -797,13 +782,13 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
|
|
|
797
782
|
}
|
|
798
783
|
|
|
799
784
|
function streamRequestAuth(
|
|
800
|
-
|
|
785
|
+
requestHeaders: HeadersInit | undefined,
|
|
801
786
|
policy: { requiresAuth: boolean },
|
|
802
787
|
): {
|
|
803
788
|
headers: Headers
|
|
804
789
|
token?: string
|
|
805
790
|
} {
|
|
806
|
-
const headers = new Headers(
|
|
791
|
+
const headers = new Headers(requestHeaders)
|
|
807
792
|
const authStore = useAuthStore()
|
|
808
793
|
if (!authStore.isInitialized) {
|
|
809
794
|
authStore.initialize()
|
|
@@ -811,7 +796,6 @@ function streamRequestAuth(
|
|
|
811
796
|
if (policy.requiresAuth && authStore.token && !headers.has('Authorization')) {
|
|
812
797
|
headers.set('Authorization', `Bearer ${authStore.token}`)
|
|
813
798
|
}
|
|
814
|
-
headers.set('Accept', 'text/event-stream')
|
|
815
799
|
const token = policy.requiresAuth ? authStore.token : undefined
|
|
816
800
|
return {
|
|
817
801
|
headers,
|
|
@@ -819,40 +803,22 @@ function streamRequestAuth(
|
|
|
819
803
|
}
|
|
820
804
|
}
|
|
821
805
|
|
|
822
|
-
function
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
let retry: number | undefined
|
|
829
|
-
const dataLines: string[] = []
|
|
830
|
-
|
|
831
|
-
for (const line of block.split(/\r?\n/)) {
|
|
832
|
-
if (!line || line.startsWith(':')) continue
|
|
833
|
-
const separator = line.indexOf(':')
|
|
834
|
-
const field = separator === -1 ? line : line.slice(0, separator)
|
|
835
|
-
const value = separator === -1
|
|
836
|
-
? ''
|
|
837
|
-
: line.slice(separator + 1).replace(/^ /, '')
|
|
838
|
-
|
|
839
|
-
if (field === 'event') event = value || 'message'
|
|
840
|
-
if (field === 'data') dataLines.push(value)
|
|
841
|
-
if (field === 'id') id = value
|
|
842
|
-
if (field === 'retry') {
|
|
843
|
-
const parsed = Number(value)
|
|
844
|
-
if (Number.isFinite(parsed)) retry = parsed
|
|
806
|
+
function pluginEventStreamTransport(
|
|
807
|
+
endpoint: PluginEndpointDefinition,
|
|
808
|
+
): EventStreamTransport {
|
|
809
|
+
return async (url, init = {}) => {
|
|
810
|
+
if (typeof fetch !== 'function') {
|
|
811
|
+
throw new Error('[MINT SDK] fetch is required for plugin event streams.')
|
|
845
812
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
raw: block,
|
|
813
|
+
const requestAuth = streamRequestAuth(
|
|
814
|
+
init.headers,
|
|
815
|
+
{ requiresAuth: endpoint.requiresAuth !== false },
|
|
816
|
+
)
|
|
817
|
+
const response = await fetch(url, { ...init, headers: requestAuth.headers })
|
|
818
|
+
if (response.status === 401) {
|
|
819
|
+
throw authenticationRequiredError(response, requestAuth.token)
|
|
820
|
+
}
|
|
821
|
+
return response
|
|
856
822
|
}
|
|
857
823
|
}
|
|
858
824
|
|
|
@@ -891,6 +857,9 @@ export function usePluginEventStream<TData = string>(
|
|
|
891
857
|
payloadOrOptions?: unknown,
|
|
892
858
|
maybeOptions?: PluginEventStreamOptions<TData>,
|
|
893
859
|
): UsePluginEventStreamReturn<TData> {
|
|
860
|
+
if (endpoint.method !== 'get') {
|
|
861
|
+
throw new Error(`[MINT SDK] Plugin event streams must use GET; got ${endpoint.method}`)
|
|
862
|
+
}
|
|
894
863
|
const payload = maybeOptions === undefined && looksLikeEventStreamOptions(payloadOrOptions)
|
|
895
864
|
? undefined
|
|
896
865
|
: payloadOrOptions
|
|
@@ -899,198 +868,25 @@ export function usePluginEventStream<TData = string>(
|
|
|
899
868
|
? payloadOrOptions
|
|
900
869
|
: maybeOptions ?? {}
|
|
901
870
|
) as PluginEventStreamOptions<TData>
|
|
902
|
-
const isConnected = ref(false)
|
|
903
|
-
const isConnecting = ref(false)
|
|
904
|
-
const error = ref<string | null>(null)
|
|
905
|
-
const lastMessage = shallowRef<PluginEventStreamMessage<TData> | null>(null)
|
|
906
|
-
const lastEventId = ref<string | null>(null)
|
|
907
871
|
const responseType = endpoint.responseType?.trim()
|
|
908
872
|
const parseJson = options.parseJson ?? Boolean(responseType && responseType !== 'string')
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
&& generation === connectionGeneration
|
|
927
|
-
&& controller === connectionController
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
function scheduleReconnect(connectionGeneration: number): void {
|
|
931
|
-
if (
|
|
932
|
-
stopped
|
|
933
|
-
|| generation !== connectionGeneration
|
|
934
|
-
|| options.reconnect === false
|
|
935
|
-
) return
|
|
936
|
-
clearReconnectTimer()
|
|
937
|
-
reconnectTimer = setTimeout(() => {
|
|
938
|
-
reconnectTimer = null
|
|
939
|
-
if (stopped || generation !== connectionGeneration) return
|
|
940
|
-
void connect(connectionGeneration)
|
|
941
|
-
}, options.reconnectDelayMs ?? 2000)
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
async function connect(connectionGeneration: number): Promise<void> {
|
|
945
|
-
if (stopped || generation !== connectionGeneration) return
|
|
946
|
-
if (endpoint.method !== 'get') {
|
|
947
|
-
throw new Error(`[MINT SDK] Plugin event streams must use GET; got ${endpoint.method}`)
|
|
948
|
-
}
|
|
949
|
-
if (typeof fetch !== 'function') {
|
|
950
|
-
throw new Error('[MINT SDK] fetch is required for plugin event streams.')
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
controller?.abort()
|
|
954
|
-
const connectionController = new AbortController()
|
|
955
|
-
controller = connectionController
|
|
956
|
-
isConnecting.value = true
|
|
957
|
-
if (!ownsConnection(connectionGeneration, connectionController)) return
|
|
958
|
-
error.value = null
|
|
959
|
-
if (!ownsConnection(connectionGeneration, connectionController)) return
|
|
960
|
-
|
|
961
|
-
let authenticationFailed = false
|
|
962
|
-
try {
|
|
963
|
-
const url = buildPluginEndpointUrl(contract, endpoint, payload, {
|
|
964
|
-
baseUrl: options.baseUrl,
|
|
965
|
-
})
|
|
966
|
-
const requestAuth = streamRequestAuth(
|
|
967
|
-
options as PluginEventStreamOptions<unknown>,
|
|
968
|
-
{ requiresAuth: endpoint.requiresAuth !== false },
|
|
969
|
-
)
|
|
970
|
-
const response = await fetch(url, {
|
|
971
|
-
method: 'GET',
|
|
972
|
-
headers: requestAuth.headers,
|
|
973
|
-
credentials: options.credentials ?? 'same-origin',
|
|
974
|
-
signal: connectionController.signal,
|
|
975
|
-
})
|
|
976
|
-
|
|
977
|
-
if (!ownsConnection(connectionGeneration, connectionController)) return
|
|
978
|
-
|
|
979
|
-
if (!response.ok) {
|
|
980
|
-
if (response.status === 401) {
|
|
981
|
-
authenticationFailed = true
|
|
982
|
-
throw authenticationRequiredError(response, requestAuth.token)
|
|
983
|
-
}
|
|
984
|
-
throw new Error(`Plugin event stream failed with HTTP ${response.status}`)
|
|
985
|
-
}
|
|
986
|
-
if (!response.body) {
|
|
987
|
-
throw new Error('Plugin event stream response did not include a readable body.')
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
isConnected.value = true
|
|
991
|
-
if (!ownsConnection(connectionGeneration, connectionController)) return
|
|
992
|
-
options.onOpen?.()
|
|
993
|
-
if (!ownsConnection(connectionGeneration, connectionController)) return
|
|
994
|
-
const reader = response.body.getReader()
|
|
995
|
-
const decoder = new TextDecoder()
|
|
996
|
-
let buffer = ''
|
|
997
|
-
|
|
998
|
-
for (;;) {
|
|
999
|
-
const { value, done } = await reader.read()
|
|
1000
|
-
if (!ownsConnection(connectionGeneration, connectionController)) return
|
|
1001
|
-
if (done) break
|
|
1002
|
-
buffer += decoder.decode(value, { stream: true })
|
|
1003
|
-
|
|
1004
|
-
let boundary = buffer.search(/\r?\n\r?\n/)
|
|
1005
|
-
while (boundary !== -1) {
|
|
1006
|
-
const block = buffer.slice(0, boundary)
|
|
1007
|
-
buffer = buffer.slice(buffer[boundary] === '\r' ? boundary + 4 : boundary + 2)
|
|
1008
|
-
const message = parseSseEvent<TData>(block, parseJson)
|
|
1009
|
-
if (message) {
|
|
1010
|
-
lastMessage.value = message
|
|
1011
|
-
if (!ownsConnection(connectionGeneration, connectionController)) return
|
|
1012
|
-
if (message.id !== undefined) {
|
|
1013
|
-
lastEventId.value = message.id
|
|
1014
|
-
if (!ownsConnection(connectionGeneration, connectionController)) return
|
|
1015
|
-
}
|
|
1016
|
-
if (message.retry !== undefined) {
|
|
1017
|
-
options.reconnectDelayMs = message.retry
|
|
1018
|
-
}
|
|
1019
|
-
if (!ownsConnection(connectionGeneration, connectionController)) return
|
|
1020
|
-
options.onMessage?.(message)
|
|
1021
|
-
if (!ownsConnection(connectionGeneration, connectionController)) return
|
|
1022
|
-
}
|
|
1023
|
-
boundary = buffer.search(/\r?\n\r?\n/)
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
} catch (err) {
|
|
1027
|
-
if (err instanceof AuthenticationRequiredError) authenticationFailed = true
|
|
1028
|
-
if (
|
|
1029
|
-
ownsConnection(connectionGeneration, connectionController)
|
|
1030
|
-
&& !(err instanceof DOMException && err.name === 'AbortError')
|
|
1031
|
-
) {
|
|
1032
|
-
const message = err instanceof Error ? err.message : 'Plugin event stream failed'
|
|
1033
|
-
error.value = message
|
|
1034
|
-
if (ownsConnection(connectionGeneration, connectionController)) {
|
|
1035
|
-
options.onError?.(err)
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
} finally {
|
|
1039
|
-
if (ownsConnection(connectionGeneration, connectionController)) {
|
|
1040
|
-
isConnecting.value = false
|
|
1041
|
-
if (ownsConnection(connectionGeneration, connectionController)) {
|
|
1042
|
-
isConnected.value = false
|
|
1043
|
-
if (ownsConnection(connectionGeneration, connectionController)) {
|
|
1044
|
-
controller = null
|
|
1045
|
-
if (authenticationFailed) {
|
|
1046
|
-
stopped = true
|
|
1047
|
-
} else {
|
|
1048
|
-
scheduleReconnect(connectionGeneration)
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
function start(): void {
|
|
1057
|
-
if (!stopped) return
|
|
1058
|
-
stopped = false
|
|
1059
|
-
clearReconnectTimer()
|
|
1060
|
-
generation += 1
|
|
1061
|
-
void connect(generation)
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
function stop(): void {
|
|
1065
|
-
if (stopped) return
|
|
1066
|
-
stopped = true
|
|
1067
|
-
generation += 1
|
|
1068
|
-
clearReconnectTimer()
|
|
1069
|
-
controller?.abort()
|
|
1070
|
-
controller = null
|
|
1071
|
-
isConnecting.value = false
|
|
1072
|
-
isConnected.value = false
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
const currentInstance = getCurrentInstance()
|
|
1076
|
-
if (currentInstance && options.immediate !== false && typeof window === 'undefined') {
|
|
1077
|
-
onMounted(start)
|
|
1078
|
-
}
|
|
1079
|
-
if (currentInstance) onUnmounted(stop)
|
|
1080
|
-
|
|
1081
|
-
if (options.immediate !== false && (typeof window !== 'undefined' || !currentInstance)) {
|
|
1082
|
-
start()
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
return {
|
|
1086
|
-
isConnected,
|
|
1087
|
-
isConnecting,
|
|
1088
|
-
error,
|
|
1089
|
-
lastMessage,
|
|
1090
|
-
lastEventId,
|
|
1091
|
-
start,
|
|
1092
|
-
stop,
|
|
1093
|
-
}
|
|
873
|
+
return useEventStream<TData>(
|
|
874
|
+
() => buildPluginEndpointUrl(contract, endpoint, payload, { baseUrl: options.baseUrl }),
|
|
875
|
+
{
|
|
876
|
+
transport: pluginEventStreamTransport(endpoint),
|
|
877
|
+
immediate: options.immediate,
|
|
878
|
+
reconnect: options.reconnect,
|
|
879
|
+
reconnectDelayMs: options.reconnectDelayMs,
|
|
880
|
+
parseJson,
|
|
881
|
+
credentials: options.credentials,
|
|
882
|
+
headers: options.headers,
|
|
883
|
+
shouldReconnect: cause => !(cause instanceof AuthenticationRequiredError),
|
|
884
|
+
onOpen: options.onOpen,
|
|
885
|
+
onMessage: options.onMessage,
|
|
886
|
+
onError: options.onError,
|
|
887
|
+
errorLabel: 'Plugin event stream',
|
|
888
|
+
},
|
|
889
|
+
)
|
|
1094
890
|
}
|
|
1095
891
|
|
|
1096
892
|
/** Read and optionally load the current platform experiment for integrated plugin views. */
|