UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

198 lines (197 loc) • 9.14 kB
import { r as racePromiseWithAbortSignal } from "./abort-signal-Z3A36sLL.mjs"; import { g as NODE_WORKER_BUNDLE_INSTALL_COMMAND } from "./node-commands-BLhGKTZa.mjs"; import "./worker-admission-GvAR4z9-.mjs"; import { a as workerBootstrapOperationTimeoutMs } from "./bootstrap-timeouts-CDaMuZ4A.mjs"; import { o as parseNodeWorkerBundleInstallResult } from "./node-bundle-install-protocol-BoFDw6pL.mjs"; import { t as sameWorkerBuild } from "./worker-build-identity-D_c48Wx_.mjs"; //#region src/gateway/worker-environments/node-worker-bundle-installer.ts const megabytes = (bytes) => (bytes / 1e6).toFixed(1); function createGatewayNodeWorkerBundleInstaller(options) { const now = options.now ?? Date.now; const active = /* @__PURE__ */ new Map(); let version = 0; const notifyProgress = (listener) => { try { listener(); } catch {} }; const publish = (entry, environmentIds = entry.observation.environmentIds) => { entry.lastPublicationAtMs = now(); try { options.onObservationChange?.(entry.observation.nodeId, environmentIds); } catch {} entry.progressListeners.forEach(notifyProgress); }; const install = async (params) => { let startedAtMs = now(); const prefix = `worker runtime install: node=${params.deviceId} bundle=${params.artifact.bundleHash.slice(0, 12)}`; const elapsedSeconds = () => Math.max(0, (now() - startedAtMs) / 1e3); try { params.signal?.throwIfAborted(); const transport = options.getTransport(); if (!transport) throw new Error("Device worker node transport is unavailable"); const node = await racePromiseWithAbortSignal(transport.getCurrentNode(params.deviceId), params.signal); params.signal?.throwIfAborted(); if (!node) throw new Error("Device worker node is not connected with the installer dialect"); const { artifact } = params; const isAuthorized = () => { params.assertCurrent?.(); return !params.signal?.aborted && options.getTransport() === transport && transport.isCurrent(node); }; if (!isAuthorized()) throw new Error("Device worker installation connection is no longer current"); const bundlePrewarm = params.prewarm && (node.workerHost.bundlePrewarm ?? 0) >= 1 ? 1 : void 0; const key = `${node.nodeId}:${artifact.bundleHash}`; let entry = active.get(key); if (!entry) { startedAtMs = now(); entry = { observation: { nodeId: node.nodeId, environmentIds: [], bundleHash: artifact.bundleHash, totalBytes: artifact.tarballBytes, transferredBytes: 0, phase: "transferring", startedAtMs, updatedAtMs: startedAtMs }, references: 0, environmentReferences: /* @__PURE__ */ new Map(), progressListeners: /* @__PURE__ */ new Set(), lastProgressLogAtMs: startedAtMs, lastPublicationAtMs: startedAtMs }; active.set(key, entry); options.log.info(`worker runtime install started (${params.reason}): node=${node.nodeId} bundle=${artifact.bundleHash.slice(0, 12)} size=${megabytes(artifact.tarballBytes)} MB`); } entry.references++; const progressListener = params.onProgress && (() => params.onProgress?.()); if (progressListener) entry.progressListeners.add(progressListener); const environmentReferences = entry.environmentReferences.get(params.environmentId) ?? 0; entry.environmentReferences.set(params.environmentId, environmentReferences + 1); if (environmentReferences === 0) { entry.observation = { ...entry.observation, environmentIds: [...entry.environmentReferences.keys()] }; version++; publish(entry); } else if (progressListener) notifyProgress(progressListener); const currentEntry = entry; startedAtMs = currentEntry.observation.startedAtMs; let prepared; let serve; try { prepared = options.transfer.prepare({ node, gatewayNamespace: options.gatewayNamespace, artifact, ...bundlePrewarm ? { bundlePrewarm } : {}, isAuthorized, signal: params.signal, onProgress: (servedBytes) => { const updatedAtMs = now(); if (serve?.interrupted) serve = void 0; else if (serve && currentEntry.serve !== serve) return; serve ??= { startedAtMs: updatedAtMs, interrupted: false }; const serveChanged = currentEntry.serve !== serve; if (serveChanged) { if (currentEntry.serve?.interrupted) options.log.info(`${prefix} transfer restarted`); currentEntry.serve = serve; currentEntry.lastProgressLogAtMs = updatedAtMs; } const observation = currentEntry.observation; const transferredBytes = Math.min(observation.totalBytes, servedBytes); if (!serveChanged && transferredBytes <= observation.transferredBytes) return; const transferSeconds = Math.max(0, (updatedAtMs - serve.startedAtMs) / 1e3); const phase = transferredBytes === observation.totalBytes ? "installing" : "transferring"; const phaseChanged = phase !== observation.phase; currentEntry.observation = { ...observation, transferredBytes, updatedAtMs, phase }; version++; if (phase === "installing" && (phaseChanged || serveChanged)) options.log.info(`${prefix} transfer complete after ${transferSeconds.toFixed(1)}s; node is installing`); else if (updatedAtMs - currentEntry.lastProgressLogAtMs >= 3e4) { currentEntry.lastProgressLogAtMs = updatedAtMs; const bytesPerSecond = transferredBytes / Math.max(1, transferSeconds); const rate = bytesPerSecond >= 1e6 ? `${megabytes(bytesPerSecond)} MB/s` : `${(bytesPerSecond / 1e3).toFixed(1)} KB/s`; options.log.info(`${prefix} ${megabytes(transferredBytes)}/${megabytes(observation.totalBytes)} MB (${Math.round(transferredBytes / observation.totalBytes * 100)}%) ${rate}`); } if (serveChanged || phaseChanged || updatedAtMs - currentEntry.lastPublicationAtMs >= 2e3) publish(currentEntry); }, onInterrupted: (servedBytes, reason) => { if (serve?.interrupted) return; serve ??= { startedAtMs: now(), interrupted: false }; serve.interrupted = true; currentEntry.serve ??= serve; options.log.warn(`${prefix} transfer interrupted at ${megabytes(Math.min(artifact.tarballBytes, servedBytes))}/${megabytes(artifact.tarballBytes)} MB: ${reason}`); } }); const result = await transport.invoke({ node, command: NODE_WORKER_BUNDLE_INSTALL_COMMAND, params: prepared.input, timeoutMs: workerBootstrapOperationTimeoutMs(artifact), idempotencyKey: `${options.gatewayNamespace}:${artifact.bundleHash}`, isDispatchAuthorized: isAuthorized, ...params.signal ? { signal: params.signal } : {} }); if (!isAuthorized()) throw new Error("Device worker installation connection is no longer current"); if (!result.ok) throw new Error(result.error?.message ? `Device worker bundle installation failed: ${result.error.message}` : "Device worker bundle installation failed"); let payload = result.payload; if (result.payloadJSON) try { payload = JSON.parse(result.payloadJSON); } catch { payload = void 0; } const receipt = parseNodeWorkerBundleInstallResult(payload); if (!receipt || !sameWorkerBuild(receipt, artifact)) throw new Error("Device worker bundle installer returned a mismatched build receipt"); if (currentEntry.references === 1) options.log.info(`${prefix} installed in ${elapsedSeconds().toFixed(1)}s`); return receipt; } finally { if (prepared) options.transfer.revoke(prepared.token); currentEntry.references--; if (progressListener) currentEntry.progressListeners.delete(progressListener); const remaining = currentEntry.environmentReferences.get(params.environmentId) - 1; if (remaining === 0) { currentEntry.environmentReferences.delete(params.environmentId); currentEntry.observation = { ...currentEntry.observation, environmentIds: [...currentEntry.environmentReferences.keys()] }; if (currentEntry.references === 0) active.delete(key); version++; publish(currentEntry, [...currentEntry.observation.environmentIds, params.environmentId]); } else currentEntry.environmentReferences.set(params.environmentId, remaining); } } catch (error) { const message = error instanceof Error ? error.message : String(error); options.log.warn(`${prefix} failed after ${elapsedSeconds().toFixed(1)}s: ${message}`); throw error; } }; const readInstall = (matches) => { let latest; for (const entry of active.values()) if (matches(entry) && (!latest || entry.observation.updatedAtMs >= latest.updatedAtMs)) latest = entry.observation; return latest ? { ...latest, environmentIds: [...latest.environmentIds] } : void 0; }; return Object.assign(install, { readInstall: (nodeId) => readInstall((entry) => entry.observation.nodeId === nodeId), readInstallForEnvironment: (environmentId) => readInstall((entry) => entry.environmentReferences.has(environmentId)), version: () => version }); } //#endregion export { createGatewayNodeWorkerBundleInstaller };