UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

223 lines (222 loc) 8.98 kB
import { C as resolveExpiresAtMsFromDurationMs, S as resolveDateTimestampMs, m as isFutureDateTimestampMs, o as asDateTimestampMs } from "./number-coercion-CJQ8TR--.js"; import { in as errorShape, rn as ErrorCodes } from "./schema-BwaBORnA.js"; import { Ct as validateNodePendingEnqueueParams, St as validateNodePendingDrainParams } from "./src-oj0IwW6K.js"; import { r as respondUnavailableOnThrow, t as respondInvalidParams } from "./nodes.helpers-DeAuiiQp.js"; import { i as waitForNodeReconnect, n as maybeWakeNodeWithApns, t as maybeSendNodeWakeNudge } from "./nodes-s5E3bsVs.js"; import { n as NODE_WAKE_RECONNECT_WAIT_MS, t as NODE_WAKE_RECONNECT_RETRY_WAIT_MS } from "./nodes-wake-state-D3Lnk3Xv.js"; import { randomUUID } from "node:crypto"; //#region src/gateway/node-pending-work.ts const DEFAULT_STATUS_ITEM_ID = "baseline-status"; const DEFAULT_STATUS_PRIORITY = "default"; const DEFAULT_PRIORITY = "normal"; const DEFAULT_MAX_ITEMS = 4; const MAX_ITEMS = 10; const PRIORITY_RANK = { high: 3, normal: 2, default: 1 }; const stateByNodeId = /* @__PURE__ */ new Map(); function getOrCreateState(nodeId) { let state = stateByNodeId.get(nodeId); if (!state) { state = { revision: 0, itemsById: /* @__PURE__ */ new Map() }; stateByNodeId.set(nodeId, state); } return state; } function pruneExpired(state, nowMs) { const validNowMs = asDateTimestampMs(nowMs); if (validNowMs === void 0) return false; let changed = false; for (const [id, item] of state.itemsById) if (item.expiresAtMs !== null && !isFutureDateTimestampMs(item.expiresAtMs, { nowMs: validNowMs })) { state.itemsById.delete(id); changed = true; } if (changed) state.revision += 1; return changed; } function pruneStateIfEmpty(nodeId, state) { if (state.itemsById.size === 0) stateByNodeId.delete(nodeId); } function sortedItems(state) { return [...state.itemsById.values()].toSorted((a, b) => { const priorityDelta = PRIORITY_RANK[b.priority] - PRIORITY_RANK[a.priority]; if (priorityDelta !== 0) return priorityDelta; if (a.createdAtMs !== b.createdAtMs) return a.createdAtMs - b.createdAtMs; return a.id.localeCompare(b.id); }); } function makeBaselineStatusItem(nowMs) { return { id: DEFAULT_STATUS_ITEM_ID, type: "status.request", priority: DEFAULT_STATUS_PRIORITY, createdAtMs: resolveDateTimestampMs(nowMs), expiresAtMs: null }; } function resolvePendingWorkExpiresAtMs(expiresInMs, nowMs) { if (typeof expiresInMs !== "number" || !Number.isFinite(expiresInMs)) return null; return resolveExpiresAtMsFromDurationMs(Math.max(1e3, Math.trunc(expiresInMs)), { nowMs }) ?? 0; } function enqueueNodePendingWork(params) { const nodeId = params.nodeId.trim(); if (!nodeId) throw new Error("nodeId required"); const rawNowMs = Date.now(); const nowMs = resolveDateTimestampMs(rawNowMs); const state = getOrCreateState(nodeId); pruneExpired(state, nowMs); const existing = [...state.itemsById.values()].find((item) => item.type === params.type); if (existing) return { revision: state.revision, item: existing, deduped: true }; const item = { id: randomUUID(), type: params.type, priority: params.priority ?? DEFAULT_PRIORITY, createdAtMs: nowMs, expiresAtMs: resolvePendingWorkExpiresAtMs(params.expiresInMs, rawNowMs), ...params.payload ? { payload: params.payload } : {} }; state.itemsById.set(item.id, item); state.revision += 1; return { revision: state.revision, item, deduped: false }; } /** Drains pending work for a node, including a baseline status request unless disabled. */ function drainNodePendingWork(nodeId, opts = {}) { const normalizedNodeId = nodeId.trim(); if (!normalizedNodeId) return { revision: 0, items: [], hasMore: false }; const nowMs = resolveDateTimestampMs(opts.nowMs ?? Date.now()); const state = stateByNodeId.get(normalizedNodeId); if (state) { pruneExpired(state, nowMs); pruneStateIfEmpty(normalizedNodeId, state); } const revision = state?.revision ?? 0; const maxItems = Math.min(MAX_ITEMS, Math.max(1, Math.trunc(opts.maxItems ?? DEFAULT_MAX_ITEMS))); const explicitItems = state ? sortedItems(state) : []; const items = explicitItems.slice(0, maxItems); const hasExplicitStatus = explicitItems.some((item) => item.type === "status.request"); const includeBaseline = opts.includeDefaultStatus !== false && !hasExplicitStatus; if (includeBaseline && items.length < maxItems) items.push(makeBaselineStatusItem(nowMs)); const explicitReturnedCount = items.filter((item) => item.id !== DEFAULT_STATUS_ITEM_ID).length; const baselineIncluded = items.some((item) => item.id === DEFAULT_STATUS_ITEM_ID); return { revision, items, hasMore: explicitItems.length > explicitReturnedCount || includeBaseline && !baselineIncluded }; } //#endregion //#region src/gateway/server-methods/nodes-pending.ts function resolveClientNodeId(client) { const trimmed = (client?.connect?.device?.id ?? client?.connect?.client?.id ?? "").trim(); return trimmed.length > 0 ? trimmed : null; } /** Gateway handlers for queueing work until a paired node reconnects. */ const nodePendingHandlers = { "node.pending.drain": async ({ params, respond, client }) => { if (!validateNodePendingDrainParams(params)) { respondInvalidParams({ respond, method: "node.pending.drain", validator: validateNodePendingDrainParams }); return; } const nodeId = resolveClientNodeId(client); if (!nodeId) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "node.pending.drain requires a connected device identity")); return; } respond(true, { nodeId, ...drainNodePendingWork(nodeId, { maxItems: params.maxItems, includeDefaultStatus: true }) }, void 0); }, "node.pending.enqueue": async ({ params, respond, context }) => { if (!validateNodePendingEnqueueParams(params)) { respondInvalidParams({ respond, method: "node.pending.enqueue", validator: validateNodePendingEnqueueParams }); return; } const p = params; await respondUnavailableOnThrow(respond, async () => { const queued = enqueueNodePendingWork({ nodeId: p.nodeId, type: p.type, priority: p.priority, expiresInMs: p.expiresInMs }); let wakeTriggered = false; if (p.wake !== false && !queued.deduped && !context.nodeRegistry.get(p.nodeId)) { const wakeReqId = queued.item.id; context.logGateway.info(`node pending wake start node=${p.nodeId} req=${wakeReqId} type=${queued.item.type}`); const cfg = context.getRuntimeConfig(); const wake = await maybeWakeNodeWithApns(p.nodeId, { wakeReason: "node.pending", cfg }); context.logGateway.info(`node pending wake stage=wake1 node=${p.nodeId} req=${wakeReqId} available=${wake.available} throttled=${wake.throttled} path=${wake.path} durationMs=${wake.durationMs} apnsStatus=${wake.apnsStatus ?? -1} apnsReason=${wake.apnsReason ?? "-"}`); wakeTriggered = wake.available; if (wake.available) { const reconnected = await waitForNodeReconnect({ nodeId: p.nodeId, context, timeoutMs: NODE_WAKE_RECONNECT_WAIT_MS }); context.logGateway.info(`node pending wake stage=wait1 node=${p.nodeId} req=${wakeReqId} reconnected=${reconnected} timeoutMs=${NODE_WAKE_RECONNECT_WAIT_MS}`); } if (!context.nodeRegistry.get(p.nodeId) && wake.available) { const retryWake = await maybeWakeNodeWithApns(p.nodeId, { force: true, wakeReason: "node.pending", cfg }); context.logGateway.info(`node pending wake stage=wake2 node=${p.nodeId} req=${wakeReqId} force=true available=${retryWake.available} throttled=${retryWake.throttled} path=${retryWake.path} durationMs=${retryWake.durationMs} apnsStatus=${retryWake.apnsStatus ?? -1} apnsReason=${retryWake.apnsReason ?? "-"}`); if (retryWake.available) { const reconnected = await waitForNodeReconnect({ nodeId: p.nodeId, context, timeoutMs: NODE_WAKE_RECONNECT_RETRY_WAIT_MS }); context.logGateway.info(`node pending wake stage=wait2 node=${p.nodeId} req=${wakeReqId} reconnected=${reconnected} timeoutMs=${NODE_WAKE_RECONNECT_RETRY_WAIT_MS}`); } } if (!context.nodeRegistry.get(p.nodeId)) { const nudge = await maybeSendNodeWakeNudge(p.nodeId, { cfg }); context.logGateway.info(`node pending wake nudge node=${p.nodeId} req=${wakeReqId} sent=${nudge.sent} throttled=${nudge.throttled} reason=${nudge.reason} durationMs=${nudge.durationMs} apnsStatus=${nudge.apnsStatus ?? -1} apnsReason=${nudge.apnsReason ?? "-"}`); context.logGateway.warn(`node pending wake done node=${p.nodeId} req=${wakeReqId} connected=false reason=not_connected`); } else context.logGateway.info(`node pending wake done node=${p.nodeId} req=${wakeReqId} connected=true`); } respond(true, { nodeId: p.nodeId, revision: queued.revision, queued: queued.item, wakeTriggered }, void 0); }); } }; //#endregion export { nodePendingHandlers };