@morscherlab/mint-sdk 1.1.9 → 1.1.11
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/dist/__tests__/components/AppTopBarPresence.test.d.ts +1 -0
- package/dist/__tests__/composables/usePresenceHeartbeat.test.d.ts +1 -0
- package/dist/components/AutoGroupModal.adapter.d.ts +12 -1
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +2 -2
- package/dist/components/index.js +1 -1
- package/dist/{components-BbtUpCTv.js → components-DjJsjKqe.js} +43 -18
- package/dist/components-DjJsjKqe.js.map +1 -0
- package/dist/composables/index.d.ts +1 -0
- package/dist/composables/index.js +2 -2
- package/dist/composables/usePresenceHeartbeat.d.ts +44 -0
- package/dist/index.js +3 -3
- package/dist/install.js +1 -1
- package/dist/{useProtocolTemplates-BWfNaynA.js → useProtocolTemplates-CIzDGj2R.js} +112 -10
- package/dist/useProtocolTemplates-CIzDGj2R.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/components/AppTopBarPresence.test.ts +47 -0
- package/src/__tests__/components/AutoGroupModal.adapter.test.ts +31 -1
- package/src/__tests__/components/SampleSelector.test.ts +35 -0
- package/src/__tests__/composables/autoGroup/groupByOrder.test.ts +31 -2
- package/src/__tests__/composables/usePresenceHeartbeat.test.ts +147 -0
- package/src/components/AppTopBar.vue +6 -0
- package/src/components/AutoGroupModal.adapter.ts +52 -1
- package/src/components/AutoGroupModal.vue +6 -4
- package/src/composables/index.ts +7 -0
- package/src/composables/useAutoGroup.ts +11 -7
- package/src/composables/usePresenceHeartbeat.ts +194 -0
- package/dist/components-BbtUpCTv.js.map +0 -1
- package/dist/useProtocolTemplates-BWfNaynA.js.map +0 -1
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
onMounted,
|
|
4
|
+
onScopeDispose,
|
|
5
|
+
readonly,
|
|
6
|
+
ref,
|
|
7
|
+
toValue,
|
|
8
|
+
watch,
|
|
9
|
+
type MaybeRefOrGetter,
|
|
10
|
+
type Ref,
|
|
11
|
+
} from 'vue'
|
|
12
|
+
|
|
13
|
+
import { useApi } from './useApi'
|
|
14
|
+
|
|
15
|
+
const PRESENCE_CLIENT_ID_KEY = 'mint:presence:client-id'
|
|
16
|
+
const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000
|
|
17
|
+
|
|
18
|
+
let pageFallbackClientId: string | null = null
|
|
19
|
+
|
|
20
|
+
export interface PresenceHeartbeatPayload {
|
|
21
|
+
client_id: string
|
|
22
|
+
scope: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface PresenceDocument {
|
|
26
|
+
readonly visibilityState: DocumentVisibilityState
|
|
27
|
+
addEventListener(type: 'visibilitychange', listener: EventListener): void
|
|
28
|
+
removeEventListener(type: 'visibilitychange', listener: EventListener): void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface PresenceStorage {
|
|
32
|
+
getItem(key: string): string | null
|
|
33
|
+
setItem(key: string, value: string): void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PresenceHeartbeatOptions {
|
|
37
|
+
/** Platform, plugin id, or plugin name reported for this browser tab. */
|
|
38
|
+
scope: MaybeRefOrGetter<string | null | undefined>
|
|
39
|
+
/** Disable all traffic, including for standalone plugins. */
|
|
40
|
+
enabled?: MaybeRefOrGetter<boolean>
|
|
41
|
+
/** Heartbeat interval. Defaults to 30 seconds. */
|
|
42
|
+
intervalMs?: number
|
|
43
|
+
/** Test or host storage seam. Defaults to sessionStorage. */
|
|
44
|
+
storage?: PresenceStorage | null
|
|
45
|
+
/** Test or host document seam. Defaults to the current document. */
|
|
46
|
+
document?: PresenceDocument | null
|
|
47
|
+
/** Test seam for generating the per-tab id. */
|
|
48
|
+
createClientId?: () => string
|
|
49
|
+
/** Test or custom transport seam. Defaults to authenticated POST /api/presence/heartbeat. */
|
|
50
|
+
send?: (payload: PresenceHeartbeatPayload) => Promise<unknown> | unknown
|
|
51
|
+
/** Test timing seam. Defaults to the browser scheduler. */
|
|
52
|
+
setInterval?: (callback: () => void, intervalMs: number) => unknown
|
|
53
|
+
/** Test timing seam. Defaults to the browser scheduler. */
|
|
54
|
+
clearInterval?: (intervalId: unknown) => void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface UsePresenceHeartbeatReturn {
|
|
58
|
+
clientId: string
|
|
59
|
+
isRunning: Readonly<Ref<boolean>>
|
|
60
|
+
sendNow: () => Promise<void>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function defaultClientId(): string {
|
|
64
|
+
if (typeof globalThis.crypto?.randomUUID === 'function') {
|
|
65
|
+
return globalThis.crypto.randomUUID()
|
|
66
|
+
}
|
|
67
|
+
return `presence-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function defaultStorage(): PresenceStorage | null {
|
|
71
|
+
try {
|
|
72
|
+
return globalThis.sessionStorage ?? null
|
|
73
|
+
} catch {
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function defaultDocument(): PresenceDocument | null {
|
|
79
|
+
return typeof globalThis.document === 'undefined' ? null : globalThis.document
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Return the stable id shared by presence callers in the current browser tab. */
|
|
83
|
+
export function getPresenceClientId(
|
|
84
|
+
storage: PresenceStorage | null = defaultStorage(),
|
|
85
|
+
createClientId: () => string = defaultClientId,
|
|
86
|
+
): string {
|
|
87
|
+
try {
|
|
88
|
+
const stored = storage?.getItem(PRESENCE_CLIENT_ID_KEY)
|
|
89
|
+
if (stored) return stored
|
|
90
|
+
} catch {
|
|
91
|
+
// Keep the tab usable when storage is blocked.
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const nextClientId = createClientId()
|
|
95
|
+
try {
|
|
96
|
+
if (storage) {
|
|
97
|
+
storage.setItem(PRESENCE_CLIENT_ID_KEY, nextClientId)
|
|
98
|
+
return nextClientId
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// Fall through to the page-local fallback when storage is blocked.
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!pageFallbackClientId) pageFallbackClientId = nextClientId
|
|
105
|
+
return pageFallbackClientId
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Report one visible integrated MINT browser tab to the platform presence tracker. */
|
|
109
|
+
export function usePresenceHeartbeat(
|
|
110
|
+
options: PresenceHeartbeatOptions,
|
|
111
|
+
): UsePresenceHeartbeatReturn {
|
|
112
|
+
const presenceDocument = options.document === undefined ? defaultDocument() : options.document
|
|
113
|
+
const clientId = getPresenceClientId(
|
|
114
|
+
options.storage === undefined ? defaultStorage() : options.storage,
|
|
115
|
+
options.createClientId ?? defaultClientId,
|
|
116
|
+
)
|
|
117
|
+
const intervalMs = options.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS
|
|
118
|
+
const scheduleInterval = options.setInterval ?? ((callback: () => void, delayMs: number) => (
|
|
119
|
+
globalThis.setInterval(callback, delayMs)
|
|
120
|
+
))
|
|
121
|
+
const cancelInterval = options.clearInterval ?? ((intervalId: unknown) => {
|
|
122
|
+
globalThis.clearInterval(intervalId as ReturnType<typeof globalThis.setInterval>)
|
|
123
|
+
})
|
|
124
|
+
if (intervalMs <= 0) throw new Error('Presence heartbeat interval must be positive')
|
|
125
|
+
|
|
126
|
+
const api = options.send ? null : useApi({ baseUrl: '/api' })
|
|
127
|
+
const send = options.send ?? ((payload: PresenceHeartbeatPayload) => (
|
|
128
|
+
api!.post('/presence/heartbeat', payload)
|
|
129
|
+
))
|
|
130
|
+
const enabled = computed(() => toValue(options.enabled ?? true))
|
|
131
|
+
const scope = computed(() => toValue(options.scope)?.trim() ?? '')
|
|
132
|
+
const isRunning = ref(false)
|
|
133
|
+
let intervalId: unknown | null = null
|
|
134
|
+
let sending = false
|
|
135
|
+
let mounted = false
|
|
136
|
+
|
|
137
|
+
function isVisible(): boolean {
|
|
138
|
+
return presenceDocument?.visibilityState !== 'hidden'
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function shouldRun(): boolean {
|
|
142
|
+
return mounted && enabled.value && Boolean(scope.value) && isVisible()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function sendNow(): Promise<void> {
|
|
146
|
+
if (!shouldRun() || sending) return
|
|
147
|
+
sending = true
|
|
148
|
+
try {
|
|
149
|
+
await send({ client_id: clientId, scope: scope.value })
|
|
150
|
+
} catch {
|
|
151
|
+
// Presence is advisory and must never break plugin or platform UI.
|
|
152
|
+
} finally {
|
|
153
|
+
sending = false
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function stop(): void {
|
|
158
|
+
if (intervalId !== null) cancelInterval(intervalId)
|
|
159
|
+
intervalId = null
|
|
160
|
+
isRunning.value = false
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function start(): void {
|
|
164
|
+
stop()
|
|
165
|
+
if (!shouldRun()) return
|
|
166
|
+
isRunning.value = true
|
|
167
|
+
void sendNow()
|
|
168
|
+
intervalId = scheduleInterval(() => void sendNow(), intervalMs)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function handleVisibilityChange(): void {
|
|
172
|
+
start()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
watch([enabled, scope], start)
|
|
176
|
+
|
|
177
|
+
onMounted(() => {
|
|
178
|
+
mounted = true
|
|
179
|
+
presenceDocument?.addEventListener('visibilitychange', handleVisibilityChange)
|
|
180
|
+
start()
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
onScopeDispose(() => {
|
|
184
|
+
mounted = false
|
|
185
|
+
presenceDocument?.removeEventListener('visibilitychange', handleVisibilityChange)
|
|
186
|
+
stop()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
clientId,
|
|
191
|
+
isRunning: readonly(isRunning),
|
|
192
|
+
sendNow,
|
|
193
|
+
}
|
|
194
|
+
}
|