@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.
Files changed (74) 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-DoS2fzyz.js → SettingsModal-BgKY93zh.js} +5 -4
  5. package/dist/SettingsModal-BgKY93zh.js.map +1 -0
  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 +29 -10
  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-CW3hE1T3.js → components-DkHddRMl.js} +39 -40
  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 +43 -19
  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 +16 -10
  55. package/src/components/JobsStatusTray.vue +22 -28
  56. package/src/components/SettingsModal.schema.ts +3 -0
  57. package/src/composables/index.ts +0 -7
  58. package/src/composables/useAuth.ts +0 -3
  59. package/src/composables/useEventStream.ts +314 -0
  60. package/src/composables/useGeneratedAnalysis.ts +40 -109
  61. package/src/composables/useJobsStatusTray.ts +17 -184
  62. package/src/composables/usePluginClient.ts +45 -249
  63. package/src/stores/auth.ts +0 -1
  64. package/src/types/auth.ts +0 -1
  65. package/dist/SettingsModal-DoS2fzyz.js.map +0 -1
  66. package/dist/__tests__/composables/usePluginConfig.test.d.ts +0 -15
  67. package/dist/components-CW3hE1T3.js.map +0 -1
  68. package/dist/composables/usePluginConfig.d.ts +0 -25
  69. package/dist/composables-CkXDe7nm.js.map +0 -1
  70. package/dist/useApi-CDBq6z23.js.map +0 -1
  71. package/dist/useControlSchema-BhoJzWzx.js.map +0 -1
  72. package/dist/useProtocolTemplates-DHwSveE6.js.map +0 -1
  73. package/src/__tests__/composables/usePluginConfig.test.ts +0 -497
  74. 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
- 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
- }
@@ -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 interface PluginEventStreamMessage<TData = string> {
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 interface UsePluginEventStreamReturn<TData = string> {
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
- options: PluginEventStreamOptions<unknown>,
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(options.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 parseSseEvent<TData>(
823
- block: string,
824
- parseJson: boolean,
825
- ): PluginEventStreamMessage<TData> | null {
826
- let event = 'message'
827
- let id: string | undefined
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
- if (!dataLines.length && !id && retry === undefined) return null
849
- const data = dataLines.join('\n')
850
- return {
851
- event,
852
- data: (parseJson && data ? JSON.parse(data) : data) as TData,
853
- id,
854
- retry,
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
- let controller: AbortController | null = null
910
- let reconnectTimer: ReturnType<typeof setTimeout> | null = null
911
- let stopped = true
912
- let generation = 0
913
-
914
- function clearReconnectTimer(): void {
915
- if (reconnectTimer !== null) {
916
- clearTimeout(reconnectTimer)
917
- reconnectTimer = null
918
- }
919
- }
920
-
921
- function ownsConnection(
922
- connectionGeneration: number,
923
- connectionController: AbortController,
924
- ): boolean {
925
- return !stopped
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. */