diff --git a/src/chunk.ts b/src/chunk.ts index d3ab263..5f9270c 100644 --- a/src/chunk.ts +++ b/src/chunk.ts @@ -35,13 +35,13 @@ export class Chunker { for (let i = 0; i < total; i++) { const chunk: Chunk = { - i: id, - s: i, - t: total, - d: data.slice(i * this.#chunkSize, (i + 1) * this.#chunkSize), + id, + seq: i, + total, + data: data.slice(i * this.#chunkSize, (i + 1) * this.#chunkSize), } if (compressed) { - chunk.c = 1 + chunk.compressed = true } chunks.push(chunk) } @@ -57,31 +57,31 @@ export class Chunker { assemble( chunk: Chunk, ): { done: false } | { done: true, data: string, compressed: boolean } { - if (chunk.t <= 0) { + if (chunk.total <= 0) { return { done: false } } - let pending = this.#buffer.get(chunk.i) + let pending = this.#buffer.get(chunk.id) if (!pending) { pending = { fragments: [], received: 0, - total: chunk.t, - compressed: chunk.c === 1, + total: chunk.total, + compressed: chunk.compressed === true, } - this.#buffer.set(chunk.i, pending) + this.#buffer.set(chunk.id, pending) } - if (pending.fragments[chunk.s] !== undefined) { + if (pending.fragments[chunk.seq] !== undefined) { return { done: false } } - pending.fragments[chunk.s] = chunk.d + pending.fragments[chunk.seq] = chunk.data pending.received++ if (pending.received === pending.total) { - this.#buffer.delete(chunk.i) + this.#buffer.delete(chunk.id) return { done: true, data: pending.fragments.join(''), compressed: pending.compressed } } diff --git a/src/constants.ts b/src/constants.ts index 086bc48..cd1588d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,8 +1,10 @@ -/** ScriptEvent ID prefix: `ipc:` */ -export const IPC_NAMESPACE = 'ipc' - -/** Internal endpoint used for invoke response routing */ -export const RESPONSE_ENDPOINT = '@response' +/** Channels used for internal message routing */ +export const CHANNELS = { + /** Base prefix for all ScriptEvent IDs: `ipc::` */ + PREFIX: 'ipc', + /** Internal response routing channel for invoke/handle */ + RESPONSE: '@response', +} as const /** Event emitter prefix for matching invoke requests to their responses */ export const RESPONSE_EVENT_PREFIX = 'invoke-response:' diff --git a/src/index.ts b/src/index.ts index bf5df6a..75f6008 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ export { Chunker } from './chunk' export { Compressor } from './compress' -export { IPC_NAMESPACE, PROTOCOL_VERSION, RESPONSE_ENDPOINT } from './constants' +export { PROTOCOL_VERSION } from './constants' export { IPC, IPC_SYSTEM_EVENTS } from './ipc' export type { IPCSystemEvents } from './ipc' export { Transport } from './transport' diff --git a/src/ipc.ts b/src/ipc.ts index 19ea5f1..708b181 100644 --- a/src/ipc.ts +++ b/src/ipc.ts @@ -13,7 +13,7 @@ import { system } from '@minecraft/server' import { EventEmitter } from 'mini-emit' import { Chunker } from './chunk' import { Compressor } from './compress' -import { PROTOCOL_VERSION, RESPONSE_ENDPOINT, RESPONSE_EVENT_PREFIX } from './constants' +import { CHANNELS, PROTOCOL_VERSION, RESPONSE_EVENT_PREFIX } from './constants' import { Transport } from './transport' const DEFAULT_OPTIONS: Required = { @@ -99,9 +99,9 @@ export class IPC { } /** - * Fire-and-forget: send data to an endpoint without expecting a response. + * Fire-and-forget: send data to a channel without expecting a response. * Use {@link on} on the receiving side to listen for these messages. - * @param endpoint - The endpoint name + * @param channel - The channel name * @param data - The data to send. If using a custom serializer, this is the typed value. * @example * ```ts @@ -116,23 +116,23 @@ export class IPC { * ipc.send('notify', mySerializer, { message: 'hello' }) * ``` */ - send(endpoint: string): void - send(endpoint: string, data: NoInfer): void - send(endpoint: string, serializer: Serializer, data: NoInfer): void - send(endpoint: string, serializerOrData?: Serializer | T, data?: T): void { + send(channel: string): void + send(channel: string, data: NoInfer): void + send(channel: string, serializer: Serializer, data: NoInfer): void + send(channel: string, serializerOrData?: Serializer | T, data?: T): void { const id = generateId() const d = data !== undefined ? (serializerOrData as Serializer).serialize(data as T) : (serializerOrData as T) - const packet: Packet = { v: PROTOCOL_VERSION, id, e: endpoint, d } + const packet: Packet = { version: PROTOCOL_VERSION, id, channel, data: d } this.#sendPacket(packet) } /** - * Register a listener for fire-and-forget messages on an endpoint. + * Register a listener for fire-and-forget messages on a channel. * Paired with {@link send} on the other side. * Returns an unsubscribe function. - * @param endpoint - The endpoint name to listen on + * @param channel - The channel name to listen on * @param handler - Called with the deserialized data each time a message arrives * @returns A function that unsubscribes this listener * @example @@ -149,10 +149,10 @@ export class IPC { * }) * ``` */ - on(endpoint: string, handler: (data: T) => void): () => void - on(endpoint: string, deserializer: Deserializer, handler: (data: T) => void): () => void + on(channel: string, handler: (data: T) => void): () => void + on(channel: string, deserializer: Deserializer, handler: (data: T) => void): () => void on( - endpoint: string, + channel: string, deserializerOrHandler: Deserializer | ((data: T) => void), handler?: (data: T) => void, ): () => void { @@ -174,17 +174,17 @@ export class IPC { userHandler(data) } - let handlers = this.#onHandlers.get(endpoint) + let handlers = this.#onHandlers.get(channel) if (!handlers) { handlers = new Set() - this.#onHandlers.set(endpoint, handlers) + this.#onHandlers.set(channel, handlers) } handlers.add(wrapped) return () => { handlers!.delete(wrapped) if (handlers!.size === 0) { - this.#onHandlers.delete(endpoint) + this.#onHandlers.delete(channel) } } } @@ -194,7 +194,7 @@ export class IPC { * Must be paired with {@link handle} on the receiving side. * The returned promise resolves with the handler's return value or rejects if * no handler is registered or the handler throws. - * @param endpoint - The endpoint name to invoke + * @param channel - The channel name to invoke * @param data - The data to send to the handler * @param options - Optional settings (timeout, serializer, deserializer) * @returns A promise that resolves with the handler's return value @@ -215,34 +215,34 @@ export class IPC { * const result = await ipc.invoke('calc', data, { serializer: mySer, deserializer: myDeser }) * ``` */ - invoke(endpoint: string): Promise - invoke(endpoint: string, options: InvokeOptions): Promise - invoke(endpoint: string, data: T, options?: InvokeOptions): Promise + invoke(channel: string): Promise + invoke(channel: string, options: InvokeOptions): Promise + invoke(channel: string, data: T, options?: InvokeOptions): Promise invoke( - endpoint: string, + channel: string, dataOrOptions?: T | InvokeOptions, options?: InvokeOptions, ): Promise { if (dataOrOptions === undefined) { - return this.#invokeImpl(endpoint) + return this.#invokeImpl(channel) } if (isInvokeOptions(dataOrOptions)) { - return this.#invokeImpl(endpoint, undefined, dataOrOptions) + return this.#invokeImpl(channel, undefined, dataOrOptions) } - return this.#invokeImpl(endpoint, dataOrOptions as T, options) + return this.#invokeImpl(channel, dataOrOptions as T, options) } /** - * Register a responder for an endpoint. + * Register a responder for a channel. * Must be paired with {@link invoke} on the other side. * The handler can return a value or a Promise. Throwing will cause the invoke to reject. - * Only one handler can be registered per endpoint — duplicate registration throws. - * @param endpoint - The endpoint name to handle + * Only one handler can be registered per channel — duplicate registration throws. + * @param channel - The channel name to handle * @param handler - Called with the deserialized data when an invoke arrives. Return a value or a Promise. * @returns A function that unregisters this handler - * @throws {Error} If a handler is already registered for this endpoint + * @throws {Error} If a handler is already registered for this channel * @example * ```ts * const off = ipc.handle<{ x: number }, { y: string }>('calc', async (req) => { @@ -252,28 +252,28 @@ export class IPC { * ``` */ handle( - endpoint: string, + channel: string, handler: (data: T) => R | Promise, ): () => void { - if (this.#handleHandlers.has(endpoint)) { - throw new Error(`Handler already registered for endpoint "${endpoint}"`) + if (this.#handleHandlers.has(channel)) { + throw new Error(`Handler already registered for channel "${channel}"`) } - this.#handleHandlers.set(endpoint, handler as (data: unknown) => unknown | Promise) + this.#handleHandlers.set(channel, handler as (data: unknown) => unknown | Promise) return () => { - this.#handleHandlers.delete(endpoint) + this.#handleHandlers.delete(channel) } } #invokeImpl( - endpoint: string, + channel: string, data?: T, options?: InvokeOptions, ): Promise { const id = generateId() const d = options?.serializer ? options.serializer.serialize(data as T) : data - const packet: Packet = { v: PROTOCOL_VERSION, id, e: endpoint, d } + const packet: Packet = { version: PROTOCOL_VERSION, id, channel, data: d } const timeout = options?.timeout ?? this.#options.invokeTimeout return new Promise((resolve, reject) => { @@ -312,7 +312,7 @@ export class IPC { return settled = true this.#sentIds.delete(id) - reject(new Error(`Invoke timed out for endpoint "${endpoint}"`)) + reject(new Error(`Invoke timed out for channel "${channel}"`)) }, ticks) } @@ -332,18 +332,18 @@ export class IPC { const { value, compressed } = this.#compressor.compress(raw) if (value.length <= this.#options.chunkSize && !compressed) { - this.#transport.send(packet.e, value) + this.#transport.send(packet.channel, value) return } const chunks = this.#chunker.split(packet.id, value, compressed) for (const chunk of chunks) { - this.#transport.send(packet.e, JSON.stringify(chunk)) + this.#transport.send(packet.channel, JSON.stringify(chunk)) } } #handleReceive(channel: string, payload: string): void { - if (channel !== RESPONSE_ENDPOINT + if (channel !== CHANNELS.RESPONSE && !this.#onHandlers.has(channel) && !this.#handleHandlers.has(channel)) { return @@ -351,10 +351,10 @@ export class IPC { const parsed = JSON.parse(payload) as Packet | Chunk - if ('v' in parsed) { + if ('version' in parsed) { this.#handleDirectPacket(parsed as Packet) } - else if ('i' in parsed) { + else if ('seq' in parsed) { this.#handleChunk(parsed as Chunk) } else { @@ -363,10 +363,10 @@ export class IPC { } #handleDirectPacket(packet: Packet): void { - const { e: endpoint, d: data, id } = packet + const { channel, data, id } = packet // Response from an invoke — resolve/reject the pending promise by id - if (endpoint === RESPONSE_ENDPOINT) { + if (channel === CHANNELS.RESPONSE) { this.#responses.emit(`${RESPONSE_EVENT_PREFIX}${id}`, data) return } @@ -379,7 +379,7 @@ export class IPC { } // Handle request — execute the registered responder and send back the result - const handleHandler = this.#handleHandlers.get(endpoint) + const handleHandler = this.#handleHandlers.get(channel) if (handleHandler) { Promise.resolve() .then(() => handleHandler(data)) @@ -393,7 +393,7 @@ export class IPC { } // Fire-and-forget — forward to all on() listeners - const onHandlers = this.#onHandlers.get(endpoint) + const onHandlers = this.#onHandlers.get(channel) if (onHandlers) { for (const handler of onHandlers) { try { @@ -407,7 +407,7 @@ export class IPC { } // No handler registered — notify the caller so invoke() doesn't hang - this.#sendResponse(id, { ok: false, err: `No handler registered for "${endpoint}"` }) + this.#sendResponse(id, { ok: false, err: `No handler registered for "${channel}"` }) } #handleChunk(chunk: Chunk): void { @@ -420,7 +420,7 @@ export class IPC { packet = JSON.parse(raw) as Packet } catch { - this.events.emit(IPC_SYSTEM_EVENTS.ERROR, new Error(`Failed to parse reassembled packet for chunk ${chunk.i}`)) + this.events.emit(IPC_SYSTEM_EVENTS.ERROR, new Error(`Failed to parse reassembled packet for chunk ${chunk.id}`)) return } this.#handleDirectPacket(packet) @@ -429,10 +429,10 @@ export class IPC { #sendResponse(id: string, data: ResponseData | ErrorResponseData): void { const packet: Packet = { - v: PROTOCOL_VERSION, + version: PROTOCOL_VERSION, id, - e: RESPONSE_ENDPOINT, - d: data, + channel: CHANNELS.RESPONSE, + data, } this.#sendPacket(packet) } diff --git a/src/transport.ts b/src/transport.ts index 0f9c9bf..b8ea6a1 100644 --- a/src/transport.ts +++ b/src/transport.ts @@ -7,13 +7,13 @@ * @see https://learn.microsoft.com/en-us/minecraft/creator/reference/content/commandsreference/examples/commands/scriptevent?view=minecraft-bedrock-stable#usage */ import { ScriptEventSource, system } from '@minecraft/server' -import { IPC_NAMESPACE } from './constants' +import { CHANNELS } from './constants' export class Transport { readonly #id: string constructor(namespace: string) { - this.#id = `${IPC_NAMESPACE}:${namespace}` + this.#id = `${CHANNELS.PREFIX}:${namespace}` } /** diff --git a/src/types.ts b/src/types.ts index c9183f9..d0531f4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -57,13 +57,13 @@ export interface IPCOptions { */ export interface Packet { /** Protocol version identifier */ - v: typeof PROTOCOL_VERSION + version: typeof PROTOCOL_VERSION /** Unique request id, used to match a response to its original invoke call */ id: string - /** Endpoint name — determines which handler or listener receives this packet */ - e: string + /** Channel name — determines which handler or listener receives this packet */ + channel: string /** Payload — the actual data being sent */ - d: T + data: T } /** @@ -73,15 +73,15 @@ export interface Packet { */ export interface Chunk { /** The id of the original packet this fragment belongs to */ - i: string + id: string /** Zero-based index of this fragment within the reassembly sequence */ - s: number + seq: number /** Total number of fragments expected for this packet */ - t: number - /** Set to `1` if the data slice is compressed with lz-string */ - c?: 1 + total: number + /** Set to `true` if the data slice is compressed with lz-string */ + compressed?: true /** Raw or lz-string-compressed segment of the serialized packet */ - d: string + data: string } /** diff --git a/test/__snapshots__/tsnapi/@mcbe-mods/ipc/index.snapshot.d.ts b/test/__snapshots__/tsnapi/@mcbe-mods/ipc/index.snapshot.d.ts index 645379d..abb802e 100644 --- a/test/__snapshots__/tsnapi/@mcbe-mods/ipc/index.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@mcbe-mods/ipc/index.snapshot.d.ts @@ -3,11 +3,11 @@ */ // #region Interfaces export interface Chunk { - i: string; - s: number; - t: number; - c?: 1; - d: string; + id: string; + seq: number; + total: number; + compressed?: true; + data: string; } export interface Deserializer { deserialize: (_: string) => T; @@ -30,10 +30,10 @@ export interface IPCSystemEvents { }; } export interface Packet { - v: typeof PROTOCOL_VERSION; + version: typeof PROTOCOL_VERSION; id: string; - e: string; - d: T; + channel: string; + data: T; } export interface ResponseData { ok: true; @@ -91,11 +91,9 @@ export declare class Transport { // #endregion // #region Variables -export declare const IPC_NAMESPACE: string; export declare const IPC_SYSTEM_EVENTS: { readonly ERROR: "error"; readonly INVALID_PACKET: "invalid-packet"; }; export declare const PROTOCOL_VERSION: 1; -export declare const RESPONSE_ENDPOINT: string; // #endregion \ No newline at end of file diff --git a/test/__snapshots__/tsnapi/@mcbe-mods/ipc/index.snapshot.js b/test/__snapshots__/tsnapi/@mcbe-mods/ipc/index.snapshot.js index 8107e81..8b1815d 100644 --- a/test/__snapshots__/tsnapi/@mcbe-mods/ipc/index.snapshot.js +++ b/test/__snapshots__/tsnapi/@mcbe-mods/ipc/index.snapshot.js @@ -48,8 +48,6 @@ export class Transport { // #endregion // #region Variables -export var IPC_NAMESPACE /* const */ export var IPC_SYSTEM_EVENTS /* const */ export var PROTOCOL_VERSION /* const */ -export var RESPONSE_ENDPOINT /* const */ // #endregion \ No newline at end of file diff --git a/test/chunk.test.ts b/test/chunk.test.ts index fa504c9..7d17933 100644 --- a/test/chunk.test.ts +++ b/test/chunk.test.ts @@ -8,11 +8,11 @@ describe('Chunker', () => { const chunks = chunker.split('test1', data, false) expect(chunks.length).toBe(3) - expect(chunks[0].i).toBe('test1') - expect(chunks[0].s).toBe(0) - expect(chunks[0].t).toBe(3) - expect(chunks[0].d.length).toBe(10) - expect(chunks[0].c).toBeUndefined() + expect(chunks[0].id).toBe('test1') + expect(chunks[0].seq).toBe(0) + expect(chunks[0].total).toBe(3) + expect(chunks[0].data.length).toBe(10) + expect(chunks[0].compressed).toBeUndefined() }) it('marks compressed flag on all chunks', () => { @@ -20,17 +20,17 @@ describe('Chunker', () => { const data = 'A'.repeat(25) const chunks = chunker.split('test2', data, true) - expect(chunks[0].c).toBe(1) - expect(chunks[1].c).toBe(1) - expect(chunks[2].c).toBe(1) + expect(chunks[0].compressed).toBe(true) + expect(chunks[1].compressed).toBe(true) + expect(chunks[2].compressed).toBe(true) }) it('single chunk for small data', () => { const chunker = new Chunker(100) const chunks = chunker.split('test3', 'hello', false) expect(chunks.length).toBe(1) - expect(chunks[0].s).toBe(0) - expect(chunks[0].t).toBe(1) + expect(chunks[0].seq).toBe(0) + expect(chunks[0].total).toBe(1) }) it('assembles chunks in order', () => { @@ -103,7 +103,7 @@ describe('Chunker', () => { it('returns false for chunk with t <= 0', () => { const chunker = new Chunker(10) - const r = chunker.assemble({ i: 'bad', s: 0, t: 0, d: 'data' }) + const r = chunker.assemble({ id: 'bad', seq: 0, total: 0, data: 'data' }) expect(r.done).toBe(false) }) }) diff --git a/test/ipc.bench.ts b/test/ipc.bench.ts index 9100276..19e2394 100644 --- a/test/ipc.bench.ts +++ b/test/ipc.bench.ts @@ -1,6 +1,6 @@ import { readFileSync } from 'node:fs' import { bench, describe } from 'vitest' -import { IPC_NAMESPACE, PROTOCOL_VERSION, RESPONSE_ENDPOINT } from '../src/constants' +import { CHANNELS, PROTOCOL_VERSION } from '../src/constants' import { IPC } from '../src/ipc' import { mockTransport } from './setup' @@ -36,7 +36,7 @@ describe('IPC.send + on — full fire-and-forget cycle', () => { mockTransport.send.mockClear() ipc.send('e', SMALL) const payload = mockTransport.send.mock.calls[0][1] - mockTransport.simulateReceive(`${IPC_NAMESPACE}:cycle:e`, payload) + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:cycle:e`, payload) }) bench('large — send (chunked) then simulate all chunks', () => { @@ -44,7 +44,7 @@ describe('IPC.send + on — full fire-and-forget cycle', () => { ipc.send('e', LARGE) const calls = mockTransport.send.mock.calls for (const [, payload] of calls) { - mockTransport.simulateReceive(`${IPC_NAMESPACE}:cycle:e`, payload) + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:cycle:e`, payload) } }) }) @@ -53,18 +53,16 @@ describe('IPC.invoke + handle — RPC round-trip', () => { const ipc = new IPC({ namespace: 'rpc' }) ipc.handle('echo', (d: string) => d) - // Chunk field 'i' = packet id; Packet field 'id' = packet id function invokeId(call: unknown): string { - const p = JSON.parse(call as string) - return p.i ?? p.id + return (JSON.parse(call as string) as { id: string }).id } bench('small (500 B — no chunk, no compress) — invoke + response', async () => { mockTransport.send.mockClear() const p = ipc.invoke('echo', SMALL) const id = invokeId(mockTransport.send.mock.calls[0][1]) - const resp = JSON.stringify({ v: PROTOCOL_VERSION, id, e: RESPONSE_ENDPOINT, d: { ok: true, data: SMALL } }) - mockTransport.simulateReceive(`${IPC_NAMESPACE}:rpc:@response`, resp) + const resp = JSON.stringify({ version: PROTOCOL_VERSION, id, channel: CHANNELS.RESPONSE, data: { ok: true, data: SMALL } }) + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:rpc:@response`, resp) await p }) @@ -72,8 +70,8 @@ describe('IPC.invoke + handle — RPC round-trip', () => { mockTransport.send.mockClear() const p = ipc.invoke('echo', MEDIUM) const id = invokeId(mockTransport.send.mock.calls[0][1]) - const resp = JSON.stringify({ v: PROTOCOL_VERSION, id, e: RESPONSE_ENDPOINT, d: { ok: true, data: MEDIUM } }) - mockTransport.simulateReceive(`${IPC_NAMESPACE}:rpc:@response`, resp) + const resp = JSON.stringify({ version: PROTOCOL_VERSION, id, channel: CHANNELS.RESPONSE, data: { ok: true, data: MEDIUM } }) + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:rpc:@response`, resp) await p }) @@ -81,8 +79,8 @@ describe('IPC.invoke + handle — RPC round-trip', () => { mockTransport.send.mockClear() const p = ipc.invoke('echo', LARGE) const id = invokeId(mockTransport.send.mock.calls[0][1]) - const resp = JSON.stringify({ v: PROTOCOL_VERSION, id, e: RESPONSE_ENDPOINT, d: { ok: true, data: LARGE } }) - mockTransport.simulateReceive(`${IPC_NAMESPACE}:rpc:@response`, resp) + const resp = JSON.stringify({ version: PROTOCOL_VERSION, id, channel: CHANNELS.RESPONSE, data: { ok: true, data: LARGE } }) + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:rpc:@response`, resp) await p }) }) @@ -93,7 +91,7 @@ describe('compression ratio — payload size comparison', () => { bench('raw JSON vs compressed — small (500 B)', () => { mockTransport.send.mockClear() ipc.send('e', SMALL) - const raw = JSON.stringify({ v: PROTOCOL_VERSION, id: '', e: 'e', d: SMALL }) + const raw = JSON.stringify({ version: PROTOCOL_VERSION, id: '', channel: 'e', data: SMALL }) const sent = mockTransport.send.mock.calls[0][1] JSON.stringify({ raw: raw.length, sent: sent.length }) }) @@ -101,7 +99,7 @@ describe('compression ratio — payload size comparison', () => { bench('raw JSON vs compressed — medium (5 KB)', () => { mockTransport.send.mockClear() ipc.send('e', MEDIUM) - const raw = JSON.stringify({ v: PROTOCOL_VERSION, id: '', e: 'e', d: MEDIUM }) + const raw = JSON.stringify({ version: PROTOCOL_VERSION, id: '', channel: 'e', data: MEDIUM }) const calls = mockTransport.send.mock.calls let sentLen = 0 for (const [, payload] of calls) { @@ -113,7 +111,7 @@ describe('compression ratio — payload size comparison', () => { bench('raw JSON vs compressed — large (26 KB)', () => { mockTransport.send.mockClear() ipc.send('e', LARGE) - const raw = JSON.stringify({ v: PROTOCOL_VERSION, id: '', e: 'e', d: LARGE }) + const raw = JSON.stringify({ version: PROTOCOL_VERSION, id: '', channel: 'e', data: LARGE }) const calls = mockTransport.send.mock.calls let sentLen = 0 for (const [, payload] of calls) { diff --git a/test/ipc.test.ts b/test/ipc.test.ts index 50bd399..883e675 100644 --- a/test/ipc.test.ts +++ b/test/ipc.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { IPC_NAMESPACE, PROTOCOL_VERSION, RESPONSE_ENDPOINT } from '../src/constants' +import { CHANNELS, PROTOCOL_VERSION } from '../src/constants' import { IPC, IPC_SYSTEM_EVENTS } from '../src/ipc' import { mockTransport } from './setup' @@ -21,20 +21,20 @@ describe('IPC', () => { expect(mockTransport.send).toHaveBeenCalledTimes(1) const [id, payload] = mockTransport.send.mock.calls[0] - expect(id).toBe(`${IPC_NAMESPACE}:test:ping`) + expect(id).toBe(`${CHANNELS.PREFIX}:test:ping`) const parsed = JSON.parse(payload) - expect(parsed.v).toBe(1) - expect(parsed.e).toBe('ping') - expect(parsed.d).toEqual({ msg: 'hello' }) + expect(parsed.version).toBe(1) + expect(parsed.channel).toBe('ping') + expect(parsed.data).toEqual({ msg: 'hello' }) }) it('receives a direct packet via on()', () => { const handler = vi.fn() ipc.on<{ msg: string }>('ping', handler) - const packet = JSON.stringify({ v: PROTOCOL_VERSION, id: 'ABC123', e: 'ping', d: { msg: 'hello' } }) - mockTransport.simulateReceive(`${IPC_NAMESPACE}:test:ping`, packet) + const packet = JSON.stringify({ version: PROTOCOL_VERSION, id: 'ABC123', channel: 'ping', data: { msg: 'hello' } }) + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:ping`, packet) expect(handler).toHaveBeenCalledTimes(1) expect(handler).toHaveBeenCalledWith({ msg: 'hello' }) @@ -46,7 +46,7 @@ describe('IPC', () => { ipc.on('test', h1) ipc.on('test', h2) - mockTransport.simulateReceive(`${IPC_NAMESPACE}:test:test`, JSON.stringify({ v: PROTOCOL_VERSION, id: 'X', e: 'test', d: 42 })) + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:test`, JSON.stringify({ version: PROTOCOL_VERSION, id: 'X', channel: 'test', data: 42 })) expect(h1).toHaveBeenCalledWith(42) expect(h2).toHaveBeenCalledWith(42) @@ -57,7 +57,7 @@ describe('IPC', () => { const off = ipc.on('test', handler) off() - mockTransport.simulateReceive(`${IPC_NAMESPACE}:test:test`, JSON.stringify({ v: PROTOCOL_VERSION, id: 'X', e: 'test', d: 42 })) + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:test`, JSON.stringify({ version: PROTOCOL_VERSION, id: 'X', channel: 'test', data: 42 })) expect(handler).not.toHaveBeenCalled() }) @@ -74,12 +74,12 @@ describe('IPC', () => { // Simulate response arriving back const responsePacket = JSON.stringify({ - v: PROTOCOL_VERSION, + version: PROTOCOL_VERSION, id: sentPacket.id, - e: RESPONSE_ENDPOINT, - d: { ok: true, data: { y: '42' } }, + channel: CHANNELS.RESPONSE, + data: { ok: true, data: { y: '42' } }, }) - mockTransport.simulateReceive(`${IPC_NAMESPACE}:test:@response`, responsePacket) + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:@response`, responsePacket) const result = await promise expect(result).toEqual({ y: '42' }) @@ -91,8 +91,8 @@ describe('IPC', () => { }) // Simulate incoming invoke request - const reqPacket = JSON.stringify({ v: PROTOCOL_VERSION, id: 'REQ1', e: 'fail', d: {} }) - mockTransport.simulateReceive(`${IPC_NAMESPACE}:test:fail`, reqPacket) + const reqPacket = JSON.stringify({ version: PROTOCOL_VERSION, id: 'REQ1', channel: 'fail', data: {} }) + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:fail`, reqPacket) // Let microtasks settle await vi.runAllTimersAsync() @@ -102,10 +102,10 @@ describe('IPC', () => { if (lastCall) { const parsed = JSON.parse(lastCall) // Could be chunk-wrapped or direct - const inner = parsed.v ? parsed : JSON.parse(parsed.d || '{}') - if (inner.e === RESPONSE_ENDPOINT) { - expect(inner.d.ok).toBe(false) - expect(inner.d.err).toBe('Error: oops') + const inner = parsed.version ? parsed : JSON.parse(parsed.data || '{}') + if (inner.channel === CHANNELS.RESPONSE) { + expect(inner.data.ok).toBe(false) + expect(inner.data.err).toBe('Error: oops') } } }) @@ -121,13 +121,13 @@ describe('IPC', () => { // All should use the ipc:test:big ID for (const [id] of mockTransport.send.mock.calls) { - expect(id).toBe(`${IPC_NAMESPACE}:test:big`) + expect(id).toBe(`${CHANNELS.PREFIX}:test:big`) } // First call should be a chunk (has 'i' field) const firstPayload = JSON.parse(mockTransport.send.mock.calls[0][1]) - expect(firstPayload.i).toBeDefined() - expect(firstPayload.s).toBe(0) + expect(firstPayload.id).toBeDefined() + expect(firstPayload.seq).toBe(0) }) it('reassembles chunked payloads', () => { @@ -135,7 +135,7 @@ describe('IPC', () => { ipc.on<{ data: string }>('big', handler) // Simulate a chunked packet - const packet = JSON.stringify({ v: PROTOCOL_VERSION, id: 'CHUNKID', e: 'big', d: { data: 'hello' } }) + const packet = JSON.stringify({ version: PROTOCOL_VERSION, id: 'CHUNKID', channel: 'big', data: { data: 'hello' } }) const compressed = packet // not compressing for test simplicity // Manually chunk at 10 chars @@ -146,8 +146,8 @@ describe('IPC', () => { // Send chunks for (let i = 0; i < chunks.length; i++) { - const chunk = JSON.stringify({ i: 'CHUNKID', s: i, t: chunks.length, d: chunks[i] }) - mockTransport.simulateReceive(`${IPC_NAMESPACE}:test:big`, chunk) + const chunk = JSON.stringify({ id: 'CHUNKID', seq: i, total: chunks.length, data: chunks[i] }) + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:big`, chunk) } expect(handler).toHaveBeenCalledTimes(1) @@ -159,8 +159,8 @@ describe('IPC', () => { ipc.events.on('error', errorHandler) ipc.on('dummy', () => {}) // register listener so pre-filter passes - const chunk = JSON.stringify({ i: 'BADID', s: 0, t: 1, d: 'not-json!!' }) - mockTransport.simulateReceive(`${IPC_NAMESPACE}:test:dummy`, chunk) + const chunk = JSON.stringify({ id: 'BADID', seq: 0, total: 1, data: 'not-json!!' }) + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:dummy`, chunk) expect(errorHandler).toHaveBeenCalled() }) @@ -173,7 +173,7 @@ describe('IPC', () => { const payload = mockTransport.send.mock.calls[0][1] const parsed = JSON.parse(payload) - expect(parsed.d).toBe('num:42') + expect(parsed.data).toBe('num:42') }) it('supports custom deserializer in on()', () => { @@ -183,8 +183,8 @@ describe('IPC', () => { const handler = vi.fn() ipc.on('custom', customDeserializer, handler) - const packet = JSON.stringify({ v: PROTOCOL_VERSION, id: 'X', e: 'custom', d: 'num:42' }) - mockTransport.simulateReceive(`${IPC_NAMESPACE}:test:custom`, packet) + const packet = JSON.stringify({ version: PROTOCOL_VERSION, id: 'X', channel: 'custom', data: 'num:42' }) + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:custom`, packet) expect(handler).toHaveBeenCalledWith(42) }) @@ -209,12 +209,12 @@ describe('IPC', () => { const sentPayload = mockTransport.send.mock.calls[0][1] const sentPacket = JSON.parse(sentPayload) const responsePacket = JSON.stringify({ - v: PROTOCOL_VERSION, + version: PROTOCOL_VERSION, id: sentPacket.id, - e: RESPONSE_ENDPOINT, - d: { ok: false, err: 'No handler registered for "ghost"' }, + channel: CHANNELS.RESPONSE, + data: { ok: false, err: 'No handler registered for "ghost"' }, }) - mockTransport.simulateReceive(`${IPC_NAMESPACE}:test:@response`, responsePacket) + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:@response`, responsePacket) await expect(promise).rejects.toThrow('No handler registered for "ghost"') }) @@ -230,11 +230,11 @@ describe('IPC', () => { // Simulate packet arriving on ipc:test:ping (sender's namespace) // ipc2 listens on ipc:ns2, so it should NOT receive this - mockTransport.simulateReceive(`${IPC_NAMESPACE}:test:ping`, sentPayload) + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:ping`, sentPayload) expect(handler).not.toHaveBeenCalled() // Simulate packet arriving on ipc:ns2:ping — ipc2 SHOULD receive it - mockTransport.simulateReceive(`${IPC_NAMESPACE}:ns2:ping`, sentPayload) + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:ns2:ping`, sentPayload) expect(handler).toHaveBeenCalledTimes(1) expect(handler).toHaveBeenCalledWith({ msg: 'hello' }) }) @@ -250,13 +250,13 @@ describe('IPC', () => { const sentPayload = mockTransport.send.mock.lastCall?.[1] // Simulate on ns2's namespace — ipc2 should NOT handle - mockTransport.simulateReceive(`${IPC_NAMESPACE}:ns2:ping`, sentPayload) + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:ns2:ping`, sentPayload) expect(handler).not.toHaveBeenCalled() - // No RESPONSE_ENDPOINT should have been sent from ipc2 back + // No CHANNELS.RESPONSE should have been sent from ipc2 back for (const [, payload] of mockTransport.send.mock.calls) { const parsed = JSON.parse(payload) - expect(parsed.e).not.toBe(RESPONSE_ENDPOINT) + expect(parsed.channel).not.toBe(CHANNELS.RESPONSE) } }) @@ -269,16 +269,16 @@ describe('IPC', () => { const sentPacket = JSON.parse(sentPayload) // Simulate loopback: invoke packet returns to sender - mockTransport.simulateReceive(`${IPC_NAMESPACE}:test:echo`, JSON.stringify(sentPacket)) + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:echo`, JSON.stringify(sentPacket)) // Simulate normal response from the other side const responsePacket = JSON.stringify({ - v: PROTOCOL_VERSION, + version: PROTOCOL_VERSION, id: sentPacket.id, - e: RESPONSE_ENDPOINT, - d: { ok: true, data: 'echo:hello' }, + channel: CHANNELS.RESPONSE, + data: { ok: true, data: 'echo:hello' }, }) - mockTransport.simulateReceive(`${IPC_NAMESPACE}:test:@response`, responsePacket) + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:@response`, responsePacket) await expect(promise).resolves.toBe('echo:hello') }) @@ -293,17 +293,17 @@ describe('IPC', () => { const sentPacket = JSON.parse(sentPayload) // Simulate loopback — handle() should NOT be triggered - mockTransport.simulateReceive(`${IPC_NAMESPACE}:test:test`, JSON.stringify(sentPacket)) + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:test`, JSON.stringify(sentPacket)) expect(handler).not.toHaveBeenCalled() // Resolve with a response from "the other side" const responsePacket = JSON.stringify({ - v: PROTOCOL_VERSION, + version: PROTOCOL_VERSION, id: sentPacket.id, - e: RESPONSE_ENDPOINT, - d: { ok: true, data: 'ok' }, + channel: CHANNELS.RESPONSE, + data: { ok: true, data: 'ok' }, }) - mockTransport.simulateReceive(`${IPC_NAMESPACE}:test:@response`, responsePacket) + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:@response`, responsePacket) await expect(promise).resolves.toBe('ok') }) @@ -312,7 +312,7 @@ describe('IPC', () => { ipc.on('test', handler) ipc.dispose() - mockTransport.simulateReceive(`${IPC_NAMESPACE}:test:test`, JSON.stringify({ v: PROTOCOL_VERSION, id: 'X', e: 'test', d: 42 })) + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:test`, JSON.stringify({ version: PROTOCOL_VERSION, id: 'X', channel: 'test', data: 42 })) expect(handler).not.toHaveBeenCalled() }) @@ -322,7 +322,7 @@ describe('IPC', () => { ipc.events.on(IPC_SYSTEM_EVENTS.INVALID_PACKET, handler) ipc.on('dummy', () => {}) // register listener so pre-filter passes - mockTransport.simulateReceive(`${IPC_NAMESPACE}:test:dummy`, JSON.stringify({ foo: 'bar' })) + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:dummy`, JSON.stringify({ foo: 'bar' })) expect(handler).toHaveBeenCalledTimes(1) expect(handler).toHaveBeenCalledWith({ payload: JSON.stringify({ foo: 'bar' }) }) @@ -333,7 +333,7 @@ describe('IPC', () => { const rejection = promise.catch(e => e) await vi.advanceTimersByTimeAsync(200) const err = await rejection - expect(err).toHaveProperty('message', 'Invoke timed out for endpoint "ghost"') + expect(err).toHaveProperty('message', 'Invoke timed out for channel "ghost"') }) it('invoke resolves before timeout when response arrives in time', async () => { @@ -341,11 +341,11 @@ describe('IPC', () => { const promise = ipc.invoke('fast', { timeout: 5000 }) const sent = JSON.parse(mockTransport.send.mock.calls[0][1]) - mockTransport.simulateReceive(`${IPC_NAMESPACE}:test:@response`, JSON.stringify({ - v: PROTOCOL_VERSION, + mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:@response`, JSON.stringify({ + version: PROTOCOL_VERSION, id: sent.id, - e: RESPONSE_ENDPOINT, - d: { ok: true, data: 'pong' }, + channel: CHANNELS.RESPONSE, + data: { ok: true, data: 'pong' }, })) await expect(promise).resolves.toBe('pong') @@ -357,7 +357,7 @@ describe('IPC', () => { const rejection = promise.catch(e => e) await vi.advanceTimersByTimeAsync(400) const err = await rejection - expect(err).toHaveProperty('message', 'Invoke timed out for endpoint "ghost"') + expect(err).toHaveProperty('message', 'Invoke timed out for channel "ghost"') }) it('invoke timeout set to 0 disables timeout', async () => {