import { randomUUID } from 'node:crypto' import type { RuntimeRpcResponse } from './runtime-rpc-envelope' import { getCleanupRequest, getSubscriptionId } from './remote-runtime-shared-control-protocol' import { finishSharedControlSubscription, handleSharedControlSubscriptionResponse } from './remote-runtime-shared-control-state' import type { SharedControlLogicalSubscription, SharedControlSubscriptionCallbacks } from './remote-runtime-shared-control-types' export function createSharedControlSubscription(args: { requestId: string method: string params: unknown callbacks: SharedControlSubscriptionCallbacks }): SharedControlLogicalSubscription { return { requestId: args.requestId, method: args.method, params: args.params, callbacks: args.callbacks, sent: false, closed: false, closeAfterReady: false, remoteSubscriptionId: null } } export function handleSharedControlLogicalResponse(args: { subscriptions: Map> subscription: SharedControlLogicalSubscription response: RuntimeRpcResponse request: (method: string, params: unknown) => void }): void { if (!args.subscription.closeAfterReady) { handleSharedControlSubscriptionResponse(args.subscriptions, args.subscription, args.response) return } if (args.response.ok) { const subscriptionId = getSubscriptionId(args.response.result) if (subscriptionId) { args.subscription.remoteSubscriptionId = subscriptionId } const cleanup = getCleanupRequest(args.subscription) if (cleanup) { args.request(cleanup.method, cleanup.params) } } finishSharedControlSubscription(args.subscriptions, args.subscription, false) } export function closeSharedControlLogicalSubscription(args: { subscriptions: Map> subscription: SharedControlLogicalSubscription request: (method: string, params: unknown) => void }): void { const cleanup = getCleanupRequest(args.subscription) if (cleanup) { finishSharedControlSubscription(args.subscriptions, args.subscription, false) args.request(cleanup.method, cleanup.params) return } if (args.subscription.sent && cleanupNeedsRemoteSubscriptionId(args.subscription.method)) { // Why: id-scoped server subscriptions can only be cleaned up after the // server returns its concrete subscription id in the ready response. args.subscription.closeAfterReady = true return } finishSharedControlSubscription(args.subscriptions, args.subscription, false) } export function sendSharedControlCleanupRequest(args: { deviceToken: string method: string params: unknown send: (payload: unknown) => boolean }): void { // Why: cleanup is best-effort and often runs during teardown; send it // synchronously so close() cannot race the async request path. args.send({ id: randomUUID(), deviceToken: args.deviceToken, method: args.method, params: args.params }) } export function replaySharedControlSubscriptions(args: { subscriptions: Map> send: (subscription: SharedControlLogicalSubscription) => void // Why: true only for a reconnect of a previously-ready connection; first // connects deliver their initial snapshot through the normal gated path. tagReplayedResponses?: boolean }): void { for (const subscription of args.subscriptions.values()) { if (subscription.closeAfterReady) { continue } subscription.sent = false subscription.remoteSubscriptionId = null if (args.tagReplayedResponses) { subscription.pendingReplayTag = true } args.send(subscription) } } export function finishCloseAfterReadySubscriptions( subscriptions: Map> ): void { for (const subscription of Array.from(subscriptions.values())) { if (subscription.closeAfterReady) { finishSharedControlSubscription(subscriptions, subscription, false) } } } function cleanupNeedsRemoteSubscriptionId(method: string): boolean { return ( method === 'accounts.subscribe' || method === 'notifications.subscribe' || method === 'runtime.clientEvents.subscribe' || method === 'files.watch' ) }