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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +9 -7
  2. package/dist/{ExperimentSelectorModal-B6P2msT4.js → ExperimentSelectorModal-DyPAUCLw.js} +3 -3
  3. package/dist/{ExperimentSelectorModal-B6P2msT4.js.map → ExperimentSelectorModal-DyPAUCLw.js.map} +1 -1
  4. package/dist/{SettingsModal-BW3Ar-uy.js → SettingsModal-BgKY93zh.js} +4 -4
  5. package/dist/{SettingsModal-BW3Ar-uy.js.map → SettingsModal-BgKY93zh.js.map} +1 -1
  6. package/dist/__tests__/composables/useEventStream.test.d.ts +1 -0
  7. package/dist/components/AutoGroupModal.vue.d.ts +1 -1
  8. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +1 -1
  9. package/dist/components/JobsStatusTray.vue.d.ts +2 -9
  10. package/dist/components/SampleLegend.vue.d.ts +1 -1
  11. package/dist/components/SampleSelector.vue.d.ts +1 -1
  12. package/dist/components/SettingsModal.vue.d.ts +1 -1
  13. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -1
  14. package/dist/components/index.js +4 -4
  15. package/dist/{components-DQt0tcpY.js → components-DkHddRMl.js} +10 -16
  16. package/dist/components-DkHddRMl.js.map +1 -0
  17. package/dist/composables/index.d.ts +2 -3
  18. package/dist/composables/index.js +8 -8
  19. package/dist/composables/useEventStream.d.ts +43 -0
  20. package/dist/composables/useGeneratedAnalysis.d.ts +1 -3
  21. package/dist/composables/useJobsStatusTray.d.ts +1 -25
  22. package/dist/composables/usePluginClient.d.ts +3 -18
  23. package/dist/{composables-CkXDe7nm.js → composables-C_i90sGS.js} +32 -308
  24. package/dist/composables-C_i90sGS.js.map +1 -0
  25. package/dist/{experiment-Y9Iz7KYi.js → experiment-Cq145oEW.js} +2 -2
  26. package/dist/{experiment-Y9Iz7KYi.js.map → experiment-Cq145oEW.js.map} +1 -1
  27. package/dist/index.js +12 -12
  28. package/dist/install.js +4 -4
  29. package/dist/stores/auth.d.ts +0 -6
  30. package/dist/stores/index.js +2 -2
  31. package/dist/templates/index.js +2 -2
  32. package/dist/{templates-CovvRZ-9.js → templates-DsP51i-S.js} +2 -2
  33. package/dist/{templates-CovvRZ-9.js.map → templates-DsP51i-S.js.map} +1 -1
  34. package/dist/types/auth.d.ts +0 -1
  35. package/dist/{useApi-CDBq6z23.js → useApi-J6zFkHmn.js} +1 -2
  36. package/dist/useApi-J6zFkHmn.js.map +1 -0
  37. package/dist/{useControlSchema-BhoJzWzx.js → useControlSchema-BDx2Olha.js} +239 -103
  38. package/dist/useControlSchema-BDx2Olha.js.map +1 -0
  39. package/dist/{useExperimentSelector-D0pjj9WH.js → useExperimentSelector-CV4iNfMS.js} +2 -2
  40. package/dist/{useExperimentSelector-D0pjj9WH.js.map → useExperimentSelector-CV4iNfMS.js.map} +1 -1
  41. package/dist/{useFormBuilder-DiEcDis2.js → useFormBuilder-BLxG3BOX.js} +2 -2
  42. package/dist/{useFormBuilder-DiEcDis2.js.map → useFormBuilder-BLxG3BOX.js.map} +1 -1
  43. package/dist/{useProtocolTemplates-DHwSveE6.js → useProtocolTemplates-CS_SQl6U.js} +22 -113
  44. package/dist/useProtocolTemplates-CS_SQl6U.js.map +1 -0
  45. package/package.json +1 -1
  46. package/src/__tests__/components/GeneratedPluginView.test.ts +0 -5
  47. package/src/__tests__/components/JobsStatusTray.test.ts +20 -18
  48. package/src/__tests__/composables/useAuth.test.ts +23 -0
  49. package/src/__tests__/composables/useEventStream.test.ts +140 -0
  50. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +103 -0
  51. package/src/__tests__/composables/useJobsStatusTray.test.ts +25 -161
  52. package/src/__tests__/composables/usePluginClient.test.ts +8 -13
  53. package/src/__tests__/docs/frontendDocsCatalog.test.ts +22 -1
  54. package/src/components/JobsStatusTray.story.vue +6 -10
  55. package/src/components/JobsStatusTray.vue +1 -16
  56. package/src/composables/index.ts +0 -7
  57. package/src/composables/useAuth.ts +0 -3
  58. package/src/composables/useEventStream.ts +314 -0
  59. package/src/composables/useGeneratedAnalysis.ts +40 -109
  60. package/src/composables/useJobsStatusTray.ts +17 -184
  61. package/src/composables/usePluginClient.ts +45 -249
  62. package/src/stores/auth.ts +0 -1
  63. package/src/types/auth.ts +0 -1
  64. package/dist/__tests__/composables/usePluginConfig.test.d.ts +0 -15
  65. package/dist/components-DQt0tcpY.js.map +0 -1
  66. package/dist/composables/usePluginConfig.d.ts +0 -25
  67. package/dist/composables-CkXDe7nm.js.map +0 -1
  68. package/dist/useApi-CDBq6z23.js.map +0 -1
  69. package/dist/useControlSchema-BhoJzWzx.js.map +0 -1
  70. package/dist/useProtocolTemplates-DHwSveE6.js.map +0 -1
  71. package/src/__tests__/composables/usePluginConfig.test.ts +0 -497
  72. package/src/composables/usePluginConfig.ts +0 -250
@@ -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. */
@@ -58,7 +58,6 @@ export const useAuthStore = defineStore('mint-auth', () => {
58
58
  passkeyEnabled: false,
59
59
  passkeyRegistered: false,
60
60
  registrationEnabled: false,
61
- databaseMode: 'none',
62
61
  experimentVisibilityMode: 'open',
63
62
  })
64
63
  const isInitialized = ref(false)
package/src/types/auth.ts CHANGED
@@ -6,7 +6,6 @@ export interface AuthConfig {
6
6
  passkeyEnabled: boolean
7
7
  passkeyRegistered: boolean
8
8
  registrationEnabled: boolean
9
- databaseMode: string
10
9
  experimentVisibilityMode?: 'open' | 'restricted'
11
10
  }
12
11
 
@@ -1,15 +0,0 @@
1
- /**
2
- * Tests for usePluginConfig.
3
- *
4
- * Covers the plugin settings life-cycle used by every plugin's
5
- * settings pane:
6
- *
7
- * - resolvedName prefers explicit pluginName, falls back to platform plugin
8
- * - load() populates config + savedConfig from /plugins/{name}/config
9
- * - save() PATCHes the endpoint, updates savedConfig, returns true
10
- * - isDirty reflects user edits vs last saved state
11
- * - reset() restores config to savedConfig and clears error
12
- * - lastLoadedAt / lastSavedAt track successful syncs
13
- * - error messages bubble up from network failures
14
- */
15
- export {};