Skip to content

Commit cea7cfb

Browse files
committed
frontend: kraken: move extension endpoints to zenoh
1 parent ac0437a commit cea7cfb

2 files changed

Lines changed: 177 additions & 115 deletions

File tree

core/frontend/src/components/kraken/KrakenManager.ts

Lines changed: 143 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { QueryTarget, Sample, Subscriber } from '@eclipse-zenoh/zenoh-ts'
2+
13
import zenoh from '@/libs/zenoh'
24
import {
35
ExtensionData,
@@ -9,26 +11,14 @@ import {
911
UploadProgressEvent,
1012
} from '@/types/kraken'
1113
import back_axios from '@/utils/api'
12-
import { QueryTarget, Sample, Subscriber } from '@eclipse-zenoh/zenoh-ts'
14+
import { createDeferred } from '@/utils/deferred'
1315

1416
const KRAKEN_BASE_URL = '/kraken'
1517
const KRAKEN_API_V2_URL = `${KRAKEN_BASE_URL}/v2.0`
1618
const KRAKEN_BASE_ZENOH = 'kraken'
19+
const INSTALL_PROGRESS_TOPIC = `${KRAKEN_BASE_ZENOH}/extension/install/progress`
1720
const ZENOH_QUERY_STANDARD_TIMEOUT = 10000
1821

19-
/**
20-
* List details of all installed extensions.
21-
* @returns {Promise<InstalledExtensionData[]>}
22-
*/
23-
export async function fetchInstalledExtensions(): Promise<InstalledExtensionData[]> {
24-
const response = await back_axios({
25-
method: 'get',
26-
url: `${KRAKEN_API_V2_URL}/extension/`,
27-
timeout: 10000,
28-
})
29-
30-
return response.data as InstalledExtensionData[]
31-
}
3222

3323
/**
3424
* List all manifest sources from kraken, uses API v2
@@ -190,78 +180,161 @@ export async function setManifestSourceOrder(identifier: string, order: number):
190180
})
191181
}
192182

183+
function buildInstallQueryKey(identifier: string, tag: string | undefined, stable: boolean): string {
184+
let key = `${KRAKEN_BASE_ZENOH}/extension/install?identifier=${encodeURIComponent(identifier)}`
185+
if (tag) key += `;tag=${encodeURIComponent(tag)}`
186+
if (!stable) key += ';stable=false'
187+
return key
188+
}
189+
190+
type InstallSample =
191+
| { kind: 'error'; message: string }
192+
| { kind: 'complete' }
193+
| { kind: 'progress'; raw: string }
194+
| null
195+
196+
function parseInstallSample(raw: string, identifier: string): InstallSample {
197+
let data: { identifier?: string; status?: string; error?: string }
198+
try {
199+
data = JSON.parse(raw)
200+
} catch {
201+
return null
202+
}
203+
if (data.identifier !== identifier) return null
204+
if (data.error) return { kind: 'error', message: data.error }
205+
if (data.status === 'complete') return { kind: 'complete' }
206+
return { kind: 'progress', raw }
207+
}
208+
193209
/**
194-
* Install an extension to the latest version available
195-
* @param {InstalledExtensionData} extension The extension to be installed
210+
* Install an extension to the latest version available.
211+
* The backend publishes the pull progress on `INSTALL_PROGRESS_TOPIC`.
212+
*
213+
* @param {string} identifier The identifier of the extension
196214
* @param {function} progressHandler The progress handler for the download
215+
* @param {string} tag The tag of the extension
216+
* @param {boolean} stable If true, will install the latest stable version, default is true
217+
* @param {number} timeout The timeout for the install
197218
*/
198219
export async function installExtension(
199-
extension: InstalledExtensionData,
200-
progressHandler: (event: any) => void,
220+
identifier: string,
221+
progressHandler?: (fragment: string) => void,
222+
tag?: string,
223+
stable = true,
224+
timeout = 600000,
201225
): Promise<void> {
202-
await back_axios({
203-
url: `${KRAKEN_API_V2_URL}/extension/install`,
204-
method: 'POST',
205-
data: {
206-
identifier: extension.identifier,
207-
name: extension.name,
208-
docker: extension.docker,
209-
tag: extension.tag,
210-
enabled: true,
211-
permissions: extension?.permissions ?? '',
212-
user_permissions: extension?.user_permissions ?? '',
213-
},
214-
timeout: 600000,
215-
onDownloadProgress: progressHandler,
216-
})
226+
const deferred = createDeferred<void>()
227+
let subscriber: Subscriber | null = null
228+
let timer: ReturnType<typeof setTimeout> | null = null
229+
230+
async function cleanup(): Promise<void> {
231+
if (timer !== null) {
232+
clearTimeout(timer)
233+
timer = null
234+
}
235+
try {
236+
await subscriber?.undeclare()
237+
} catch {
238+
// The subscriber may already be gone. Ignore cleanup errors.
239+
}
240+
subscriber = null
241+
}
242+
243+
async function handleSample(sample: Sample): Promise<void> {
244+
const result = parseInstallSample(sample.payload().to_string(), identifier)
245+
if (result === null) return
246+
switch (result.kind) {
247+
case 'error':
248+
cleanup().finally(() => deferred.reject(new Error(result.message)))
249+
break
250+
case 'complete':
251+
cleanup().finally(() => deferred.resolve())
252+
break
253+
case 'progress':
254+
progressHandler?.(result.raw)
255+
break
256+
default:
257+
break
258+
}
259+
}
260+
261+
// Subscribe before triggering the install.
262+
subscriber = await zenoh.subscriber(INSTALL_PROGRESS_TOPIC, handleSample)
263+
if (!subscriber) {
264+
throw new Error('Failed to subscribe to install progress topic')
265+
}
266+
timer = setTimeout(
267+
() => cleanup().finally(() => deferred.reject(new Error(`Install timed out after ${timeout}ms`))),
268+
timeout,
269+
)
270+
271+
try {
272+
const reply = await zenoh.query(
273+
buildInstallQueryKey(identifier, tag, stable),
274+
QueryTarget.BestMatching,
275+
timeout,
276+
)
277+
if (!reply || reply.error) {
278+
throw new Error(reply?.error ?? 'Install query failed')
279+
}
280+
} catch (error) {
281+
await cleanup()
282+
throw error
283+
}
284+
285+
return deferred.promise
217286
}
218287

219288
/**
220-
* Enable an extension by its identifier and tag, uses API v2
289+
* Enable an extension by its identifier and tag, uses zenoh
221290
* @param {string} identifier The identifier of the extension
222291
* @param {string} tag The tag of the extension
223292
*/
224293
export async function enableExtension(identifier: string, tag: string): Promise<void> {
225-
await back_axios({
226-
method: 'POST',
227-
url: `${KRAKEN_API_V2_URL}/extension/${identifier}/${tag}/enable`,
228-
timeout: 10000,
229-
})
294+
await zenoh.query(
295+
`${KRAKEN_BASE_ZENOH}/extension/enable?identifier=${encodeURIComponent(identifier)};tag=${encodeURIComponent(tag)}`,
296+
QueryTarget.BestMatching,
297+
ZENOH_QUERY_STANDARD_TIMEOUT,
298+
)
230299
}
231300

232301
/**
233-
* Disable an extension by its identifier, uses API v2
302+
* Disable an extension by its identifier, uses zenoh
234303
* @param {string} identifier The identifier of the extension
235304
*/
236305
export async function disableExtension(identifier: string): Promise<void> {
237-
await back_axios({
238-
method: 'POST',
239-
url: `${KRAKEN_API_V2_URL}/extension/${identifier}/disable`,
240-
timeout: 10000,
241-
})
306+
await zenoh.query(
307+
`${KRAKEN_BASE_ZENOH}/extension/disable?identifier=${encodeURIComponent(identifier)}`,
308+
QueryTarget.BestMatching,
309+
ZENOH_QUERY_STANDARD_TIMEOUT,
310+
)
242311
}
243312

244313
/**
245-
* Uninstall an extension by its identifier, uses API v2
314+
* Uninstall an extension by its identifier, uses zenoh
246315
* @param {string} identifier The identifier of the extension
247316
*/
248-
export async function uninstallExtension(identifier: string): Promise<void> {
249-
await back_axios({
250-
method: 'DELETE',
251-
url: `${KRAKEN_API_V2_URL}/extension/${identifier}`,
252-
})
317+
export async function uninstallExtension(identifier: string, tag?: string): Promise<void> {
318+
let queryKey = `${KRAKEN_BASE_ZENOH}/extension/uninstall?identifier=${encodeURIComponent(identifier)}`
319+
if (tag) queryKey += `;tag=${encodeURIComponent(tag)}`
320+
321+
await zenoh.query(
322+
queryKey,
323+
QueryTarget.BestMatching,
324+
ZENOH_QUERY_STANDARD_TIMEOUT,
325+
)
253326
}
254327

255328
/**
256-
* Restart an extension by its identifier, uses API v2
329+
* Restart an extension by its identifier, uses zenoh
257330
* @param {string} identifier The identifier of the extension
258331
*/
259332
export async function restartExtension(identifier: string): Promise<void> {
260-
await back_axios({
261-
method: 'POST',
262-
url: `${KRAKEN_API_V2_URL}/extension/${identifier}/restart`,
263-
timeout: 10000,
264-
})
333+
await zenoh.query(
334+
`${KRAKEN_BASE_ZENOH}/extension/restart?identifier=${encodeURIComponent(identifier)}`,
335+
QueryTarget.BestMatching,
336+
ZENOH_QUERY_STANDARD_TIMEOUT,
337+
)
265338
}
266339

267340
/**
@@ -284,16 +357,15 @@ export async function updateExtensionToVersion(
284357
}
285358

286359
/**
287-
* List all installed extensions from kraken, uses API v2
360+
* List details of all installed extensions.
361+
* @returns {Promise<InstalledExtensionData[]> | null}
288362
*/
289-
export async function getInstalledExtensions(): Promise<InstalledExtensionData[]> {
290-
const response = await back_axios({
291-
method: 'GET',
292-
url: `${KRAKEN_API_V2_URL}/extension/`,
293-
timeout: 30000,
294-
})
295-
296-
return response.data as InstalledExtensionData[]
363+
export async function fetchInstalledExtensions(): Promise<InstalledExtensionData[] | null> {
364+
return zenoh.query(
365+
`${KRAKEN_BASE_ZENOH}/extension/fetch`,
366+
QueryTarget.BestMatching,
367+
30000,
368+
)
297369
}
298370

299371
/**
@@ -350,11 +422,11 @@ export async function uploadExtensionTarFile(
350422
* @returns {Promise<void>}
351423
*/
352424
export async function keepTemporaryExtensionAlive(tempTag: string): Promise<void> {
353-
await back_axios({
354-
method: 'POST',
355-
url: `${KRAKEN_API_V2_URL}/extension/upload/keep-alive?temp_tag=${tempTag}`,
356-
timeout: 10000,
357-
})
425+
await zenoh.query(
426+
`${KRAKEN_BASE_ZENOH}/extension/upload/keep-alive?temp_tag=${encodeURIComponent(tempTag)}`,
427+
QueryTarget.BestMatching,
428+
ZENOH_QUERY_STANDARD_TIMEOUT,
429+
)
358430
}
359431

360432
/**
@@ -422,9 +494,7 @@ export default {
422494
disabledManifestSource,
423495
setManifestSourcesOrders,
424496
setManifestSourceOrder,
425-
updateExtensionToVersion,
426497
installExtension,
427-
getInstalledExtensions,
428498
enableExtension,
429499
disableExtension,
430500
uninstallExtension,

0 commit comments

Comments
 (0)