/* * Copyright 2025 the original author or authors. *

* Licensed under the Moderne Source Available License (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* https://docs.moderne.io/licensing/moderne-source-available-license *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import * as rpc from "vscode-jsonrpc/node"; import {emptyMarkers, Markers} from "../markers"; import {saveTrace, trace} from "./trace"; import {updateIfChanged} from "../util"; import {isRef, ReferenceMap} from "../reference"; /** * Interface representing an RPC codec that defines methods * for sending and receiving objects in an RPC communication. */ export interface RpcCodec { /** * Creates a new instance of the object type with proper constructor defaults. * If not provided, a plain object `{kind: type}` will be created. * * @returns A new instance of the object type. */ rpcNew?(): T; /** * Serializes and sends an object over an RPC send queue. * * @param after - The object to be sent. * @param q - The RPC send queue where the object will be enqueued. */ rpcSend(after: T, q: RpcSendQueue): Promise; /** * Receives and deserializes an object from an RPC receive queue. * * @param before - The initial object state before deserialization. * @param q - The RPC receive queue where the object data is retrieved. * @returns A Promise resolving to the deserialized object. */ rpcReceive(before: T, q: RpcReceiveQueue): Promise; } /** * A registry for managing RPC codecs based on object types. */ export class RpcCodecs { private static nonTreeCodecs = new Map>(); /** * The first key is on sourceFileType and the second on object type */ private static treeCodecs = new Map>>(); /** * Registers an RPC codec for a given type. * * @param type - The string identifier of the object type. * @param codec - The codec implementation to be registered. * @param sourceFileType The source file type of the source file containing (or will contain) this element. */ static registerCodec(type: string, codec: RpcCodec, sourceFileType?: string): void { if (sourceFileType) { let codecsForSourceFile = this.treeCodecs.get(sourceFileType); if (!codecsForSourceFile) { codecsForSourceFile = new Map>(); this.treeCodecs.set(sourceFileType, codecsForSourceFile); } codecsForSourceFile.set(type, codec); } else { this.nonTreeCodecs.set(type, codec); } } /** * Retrieves the registered codec for a given type. * * @param type - The string identifier of the object type. * @param sourceFileType The source file type of the source file containing (or will contain) this element. * @returns The corresponding `RpcCodec`, or `undefined` if not found. */ static forType(type: string, sourceFileType?: string): RpcCodec | undefined { if (sourceFileType) { const treeCodec = this.treeCodecs.get(sourceFileType)?.get(type); return treeCodec || this.nonTreeCodecs.get(type); } return this.nonTreeCodecs.get(type); } /** * Determines the appropriate codec for an instance based on its `kind` property. * * @param before - The object instance to find a codec for. * @param sourceFileType The source file type of the source file containing (or will contain) this element. * @returns The corresponding `RpcCodec`, or `undefined` if no matching codec is found. */ static forInstance(before: any, sourceFileType?: string): RpcCodec | undefined { if (before !== undefined && before !== null && typeof before === "object" && "kind" in before) { return RpcCodecs.forType(before["kind"] as string, sourceFileType); } } } export class RpcSendQueue { private q: RpcObjectData[] = []; private before?: any; constructor(private readonly refs: ReferenceMap, private readonly sourceFileType: string | undefined, private readonly trace: boolean) { } async generate(after: any, before: any): Promise { await this.send(after, before); const result = this.q; result.push({state: RpcObjectState.END_OF_OBJECT}); this.q = []; return result; } /** * Send a bare list (no enclosing object) and return the complete batch terminated by * END_OF_OBJECT. Used by self-contained responses like DependencyTypes, where the peer * drains one list of ref-deduplicated elements rather than a tree. */ async generateList(after: T[] | undefined, id: (value: T) => any, onChange: (value: T) => Promise): Promise { await this.sendList(after, undefined, id, onChange); return this.finish(); } /** * Terminate a hand-composed batch: append END_OF_OBJECT and return the accumulated data, * resetting the queue. Lets a caller emit several {@link sendList}s into one stream that the * peer drains as consecutive lists (e.g. DependencyTypes sends its FQN list then its type list). */ finish(): RpcObjectData[] { const result = this.q; result.push({state: RpcObjectState.END_OF_OBJECT}); this.q = []; return result; } private put(d: RpcObjectData): void { if (this.trace) { d.trace = trace("Sender"); } this.q.push(d); } getAndSend(parent: T, value: (parent: T) => U | undefined, onChange?: (value: U) => Promise): Promise { const after = value(parent); const before = this.before === undefined ? undefined : value(this.before as T); return this.send(after, before, onChange && (() => onChange(after!))); } getAndSendList(parent: T, values: (parent: T) => U[] | undefined, id: (value: U) => any, onChange?: (value: U) => Promise): Promise { const after = values(parent); const before = this.before === undefined ? undefined : values(this.before as T); return this.sendList(after, before, id, onChange); } send(after: T | undefined, before: T | undefined, onChange?: (() => Promise)): Promise { return saveTrace(this.trace, async () => { if (before === after) { this.put({state: RpcObjectState.NO_CHANGE}); } else if (before === undefined || (after !== undefined && this.typesAreDifferent(after, before))) { // Treat as ADD when before is undefined OR types differ (it's a new object, not a change) await this.add(after, onChange); } else if (after === undefined) { this.put({state: RpcObjectState.DELETE}); } else if (isRef(after)) { // A ref-deduplicated slot is resolved by the receiver against a persistent cache whose // instance may be aliased by any number of other slots and source files. A CHANGE would // be applied to that shared instance in place, corrupting every alias, so the new value // is re-added instead; the refs map collapses repeats of it into ref-only ADDs. await this.add(after, onChange); } else { let afterCodec = onChange ? undefined : RpcCodecs.forInstance(after, this.sourceFileType); this.put({state: RpcObjectState.CHANGE, value: onChange || afterCodec ? undefined : after}); await this.doChange(after, before, onChange, afterCodec); } }); } sendList(after: T[] | undefined, before: T[] | undefined, id: (value: T) => any, onChange?: (value: T) => Promise): Promise { return this.send(after, before, async () => { if (!after) { throw new Error("A DELETE event should have been sent."); } const beforeIdx = this.putListPositions(after, before, id); for (const anAfter of after) { const beforePos = beforeIdx.get(id(anAfter)); const onChangeRun = onChange ? () => onChange(anAfter) : undefined; if (beforePos === undefined) { await this.add(anAfter, onChangeRun); } else { const aBefore = before?.[beforePos]; if (aBefore === anAfter) { this.put({state: RpcObjectState.NO_CHANGE}); } else if (anAfter !== undefined && (isRef(anAfter) || this.typesAreDifferent(anAfter, aBefore))) { // Type changed - treat as ADD. Ref-deduplicated items are also always // re-added rather than CHANGEd (see send()). await this.add(anAfter, onChangeRun); } else { const afterCodec = onChangeRun ? undefined : RpcCodecs.forInstance(anAfter, this.sourceFileType); // Without an onChange callback or codec, no property messages follow, so the // value must travel inline (as in send()) or the receiver keeps the stale element this.put({state: RpcObjectState.CHANGE, value: onChangeRun || afterCodec ? undefined : anAfter}); await this.doChange(anAfter, aBefore, onChangeRun, afterCodec); } } } }); } private putListPositions(after: T[], before: T[] | undefined, id: (value: T) => any): Map { const beforeIdx = new Map(); if (before) { for (let i = 0; i < before.length; i++) { beforeIdx.set(id(before[i]), i); } } const positions: number[] = []; for (const t of after) { const beforePos = beforeIdx.get(id(t)); positions.push(beforePos === undefined ? -1 : beforePos); } this.put({state: RpcObjectState.CHANGE, value: positions}); return beforeIdx; } private async add(after: any, onChange: (() => Promise) | undefined): Promise { let ref: number | undefined; if (isRef(after)) { ref = this.refs.get(after); if (ref !== undefined) { this.put({ state: RpcObjectState.ADD, ref }); return; } ref = this.refs.create(after); } let afterCodec = onChange ? undefined : RpcCodecs.forInstance(after, this.sourceFileType); this.put({ state: RpcObjectState.ADD, valueType: this.getValueType(after), value: onChange || afterCodec ? undefined : after, ref: ref }); await this.doChange(after, undefined, onChange, afterCodec); } private async doChange(after: any, before: any, onChange?: () => Promise, afterCodec?: RpcCodec): Promise { const lastBefore = this.before; this.before = before; try { if (onChange) { if (after !== undefined) { await onChange(); } } else if (afterCodec) { await afterCodec.rpcSend(after, this); } } finally { this.before = lastBefore; } } private typesAreDifferent(after: any, before: any): boolean { const afterKind = after !== undefined && after !== null && typeof after === "object" ? after["kind"] : undefined; const beforeKind = before !== undefined && before !== null && typeof before === "object" ? before["kind"] : undefined; return afterKind !== undefined && beforeKind !== undefined && afterKind !== beforeKind; } private getValueType(after?: any): string | undefined { if (after !== undefined && after !== null && typeof after === "object" && "kind" in after) { return after["kind"]; } } } export class RpcReceiveQueue { private batch: RpcObjectData[] = []; constructor(private readonly refs: Map, private readonly sourceFileType: string | undefined, private readonly pull: () => Promise, private readonly logger: rpc.Logger | undefined, private readonly trace: boolean) { } async take(): Promise { if (this.batch.length === 0) { this.batch = await this.pull(); } return this.batch.shift()!; } receiveMarkers(markers?: Markers): Promise { if (markers === undefined) { markers = emptyMarkers; } return this.receive(markers, async m => { return saveTrace(this.trace, async () => { return updateIfChanged(markers!, { id: await this.receive(m.id), markers: (await this.receiveList(m.markers))!, }); }) }) } receive( before: T | undefined, onChange?: (before: T) => T | Promise | undefined ): Promise { return saveTrace(this.trace, async () => { const message = await this.take(); RpcObjectData.logTrace(message, this.trace, this.logger); let ref: number | undefined; switch (message.state) { case RpcObjectState.NO_CHANGE: return before!; case RpcObjectState.DELETE: return undefined as T; case RpcObjectState.ADD: ref = message.ref; if (ref !== undefined && message.valueType === undefined && message.value === undefined) { // This is a pure reference to an existing object if (this.refs.has(ref)) { return this.refs.get(ref); } else { throw new Error(`Received a reference to an object that was not previously sent: ${ref}`); } } else { // This is either a new object or a forward declaration with ref before = message.valueType === undefined ? message.value : this.newObj(message.valueType); if (ref !== undefined) { // For an object like JavaType that we will mutate in place rather than using // immutable updates because of its cyclic nature, the before instance will ultimately // be the same as the after instance below. this.refs.set(ref, before); } } // Intentional fall-through... case RpcObjectState.CHANGE: let after; let codec; if (onChange) { after = await onChange(before!); } else if ((codec = RpcCodecs.forInstance(before, this.sourceFileType))) { after = await codec.rpcReceive(before, this); } else if (message.value !== undefined) { after = message.valueType ? {kind: message.valueType, ...message.value} : message.value; } else if (message.state === RpcObjectState.ADD && message.valueType) { throw new Error( `No RPC codec registered on the TypeScript side for '${message.valueType}'. ` + `The Java side has a codec and sent property messages that will not be consumed, ` + `causing RPC queue desynchronization.` ); } else { after = before; } if (ref !== undefined) { this.refs.set(ref, after); } return after; default: throw new Error(`Unknown state type ${message.state}`); } }); } async receiveListDefined( before: T[] | undefined, onChange?: (before: T) => T | Promise | undefined ): Promise { return (await this.receiveList(before, onChange))!; } receiveList( before: T[] | undefined, onChange?: (before: T) => T | Promise | undefined ): Promise { return saveTrace(this.trace, async () => { const message = await this.take(); RpcObjectData.logTrace(message, this.trace, this.logger); switch (message.state) { case RpcObjectState.NO_CHANGE: return before; case RpcObjectState.DELETE: return undefined; case RpcObjectState.ADD: before = []; // Intentional fall-through... case RpcObjectState.CHANGE: // The next message should be a CHANGE with a list of positions const d = await this.take(); const positions = d.value as number[]; if (!positions) { throw new Error(`Expected positions array but got: ${JSON.stringify(d)}`); } const after: T[] = new Array(positions.length); for (let i = 0; i < positions.length; i++) { const beforeIdx = positions[i]; const b: T = await (beforeIdx >= 0 ? before![beforeIdx] as T : undefined) as T; let received: Promise = this.receive(b, onChange); after[i] = await received; } return after; default: throw new Error(`${message.state} is not supported for lists.`); } }); } private newObj(type: string): T { const codec = RpcCodecs.forType(type, this.sourceFileType); if (codec?.rpcNew) { return codec.rpcNew(); } return {kind: type} as T; } } /** * Refer to RpcObjectData.java for a description of these fields. */ export interface RpcObjectData { state: RpcObjectState valueType?: string value?: any ref?: number trace?: string } export namespace RpcObjectData { export function logTrace(message: RpcObjectData, enabled: boolean, logger: rpc.Logger | undefined): void { if (enabled && logger && message.trace) { const sendTrace = message.trace; delete message.trace; logger.info(`${JSON.stringify(message)}`); logger.info(` ${sendTrace || 'No sender trace'}`); logger.info(` ${trace("Receiver") || 'No receiver trace'}`); } } } export enum RpcObjectState { NO_CHANGE = "NO_CHANGE", ADD = "ADD", DELETE = "DELETE", CHANGE = "CHANGE", END_OF_OBJECT = "END_OF_OBJECT" }