From 108581f7a32028153310343c720727a42fb6bb18 Mon Sep 17 00:00:00 2001 From: lete114 Date: Sun, 24 May 2026 12:24:56 +0800 Subject: [PATCH] feat: introduce system domains for event ID routing - Add SYSTEM_DOMAINS constant object (PREFIX, USER, RESPONSE) - Add EVENTS constant object (INVOKE_RESPONSE) - Change event ID from ipc:: to ipc::: with domain fixed at position 1 to prevent namespace injection - Transport.send/onReceive now use (systemDomain, route) parameters - #handleReceive dispatches by system domain: RESPONSE -> emit to matching invoke Promise by route (= invoke id) USER -> parse Packet, route to on/handle handlers - Remove fixed @response channel, response routing via domain isolation - Clean up old constants (CHANNELS, RESPONSE_ENDPOINT, IPC_NAMESPACE, RESPONSE_EVENT_PREFIX) - Update tests and snapshots --- src/constants.ts | 17 ++-- src/ipc.ts | 61 +++++++----- src/transport.ts | 47 +++++++--- .../tsnapi/@mcbe-mods/ipc/index.snapshot.d.ts | 4 +- .../tsnapi/@mcbe-mods/ipc/index.snapshot.js | 10 +- test/ipc.bench.ts | 18 ++-- test/ipc.test.ts | 93 +++++++++---------- 7 files changed, 141 insertions(+), 109 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index cd1588d..bbf3052 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,13 +1,16 @@ -/** Channels used for internal message routing */ -export const CHANNELS = { - /** Base prefix for all ScriptEvent IDs: `ipc::` */ +/** System domains used for message routing in ScriptEvent IDs: `ipc:::` */ +export const SYSTEM_DOMAINS = { + /** Fixed prefix for all IPC ScriptEvent IDs */ PREFIX: 'ipc', - /** Internal response routing channel for invoke/handle */ - RESPONSE: '@response', + /** User-facing domain — send/on/invoke/handle messages */ + USER: 'user', + /** Internal domain — invoke response routing */ + RESPONSE: 'response', } as const -/** Event emitter prefix for matching invoke requests to their responses */ -export const RESPONSE_EVENT_PREFIX = 'invoke-response:' +export const EVENTS = { + INVOKE_RESPONSE: 'invoke-response', +} as const /** Current IPC protocol version */ export const PROTOCOL_VERSION = 1 as const diff --git a/src/ipc.ts b/src/ipc.ts index 708b181..313ceb0 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 { CHANNELS, PROTOCOL_VERSION, RESPONSE_EVENT_PREFIX } from './constants' +import { EVENTS, PROTOCOL_VERSION, SYSTEM_DOMAINS } from './constants' import { Transport } from './transport' const DEFAULT_OPTIONS: Required = { @@ -75,9 +75,9 @@ export class IPC { this.#compressor = new Compressor(this.#options.compressThreshold) this.#chunker = new Chunker(this.#options.chunkSize) - this.#transportUnsubscribe = this.#transport.onReceive((channel, payload) => { + this.#transportUnsubscribe = this.#transport.onReceive((systemDomain, route, payload) => { try { - this.#handleReceive(channel, payload) + this.#handleReceive(systemDomain, route, payload) } catch (e) { this.events.emit(IPC_SYSTEM_EVENTS.ERROR, e as Error) @@ -125,7 +125,7 @@ export class IPC { ? (serializerOrData as Serializer).serialize(data as T) : (serializerOrData as T) const packet: Packet = { version: PROTOCOL_VERSION, id, channel, data: d } - this.#sendPacket(packet) + this.#sendPacket(SYSTEM_DOMAINS.USER, packet) } /** @@ -290,7 +290,7 @@ export class IPC { this.#sentIds.add(id) - this.#responses.once(`${RESPONSE_EVENT_PREFIX}${id}`, (response: unknown) => { + this.#responses.once(`${EVENTS.INVOKE_RESPONSE}:${id}`, (response: unknown) => { cleanup() this.#sentIds.delete(id) const resp = response as ResponseData | ErrorResponseData @@ -316,11 +316,11 @@ export class IPC { }, ticks) } - this.#sendPacket(packet) + this.#sendPacket(SYSTEM_DOMAINS.USER, packet) }) } - #sendPacket(packet: Packet): void { + #sendPacket(systemDomain: string, packet: Packet): void { const raw = JSON.stringify(packet) if (raw.length > this.#options.maxPacketSize) { @@ -330,22 +330,36 @@ export class IPC { } const { value, compressed } = this.#compressor.compress(raw) + const route = systemDomain === SYSTEM_DOMAINS.USER ? packet.channel : packet.id if (value.length <= this.#options.chunkSize && !compressed) { - this.#transport.send(packet.channel, value) + this.#transport.send(systemDomain, route, value) return } const chunks = this.#chunker.split(packet.id, value, compressed) for (const chunk of chunks) { - this.#transport.send(packet.channel, JSON.stringify(chunk)) + this.#transport.send(systemDomain, route, JSON.stringify(chunk)) } } - #handleReceive(channel: string, payload: string): void { - if (channel !== CHANNELS.RESPONSE - && !this.#onHandlers.has(channel) - && !this.#handleHandlers.has(channel)) { + #handleReceive(systemDomain: string, route: string, payload: string): void { + if (systemDomain === SYSTEM_DOMAINS.RESPONSE) { + const parsed = JSON.parse(payload) as Packet | Chunk + if ('version' in parsed) { + this.#responses.emit(`${EVENTS.INVOKE_RESPONSE}:${route}`, (parsed as Packet).data) + } + else if ('seq' in parsed) { + this.#handleChunk(parsed as Chunk, systemDomain, route) + } + return + } + + if (systemDomain !== SYSTEM_DOMAINS.USER) { + return + } + + if (!this.#onHandlers.has(route) && !this.#handleHandlers.has(route)) { return } @@ -355,7 +369,7 @@ export class IPC { this.#handleDirectPacket(parsed as Packet) } else if ('seq' in parsed) { - this.#handleChunk(parsed as Chunk) + this.#handleChunk(parsed as Chunk, systemDomain, route) } else { this.events.emit(IPC_SYSTEM_EVENTS.INVALID_PACKET, { payload }) @@ -365,12 +379,6 @@ export class IPC { #handleDirectPacket(packet: Packet): void { const { channel, data, id } = packet - // Response from an invoke — resolve/reject the pending promise by id - if (channel === CHANNELS.RESPONSE) { - this.#responses.emit(`${RESPONSE_EVENT_PREFIX}${id}`, data) - return - } - // Packet was sent by this instance itself (loopback via ScriptEvent) // Must check before handleHandler to prevent self-invocation of handle() if (this.#sentIds.has(id)) { @@ -410,7 +418,7 @@ export class IPC { this.#sendResponse(id, { ok: false, err: `No handler registered for "${channel}"` }) } - #handleChunk(chunk: Chunk): void { + #handleChunk(chunk: Chunk, systemDomain: string, route: string): void { const result = this.#chunker.assemble(chunk) if (result.done) { @@ -423,7 +431,12 @@ export class IPC { this.events.emit(IPC_SYSTEM_EVENTS.ERROR, new Error(`Failed to parse reassembled packet for chunk ${chunk.id}`)) return } - this.#handleDirectPacket(packet) + if (systemDomain === SYSTEM_DOMAINS.RESPONSE) { + this.#responses.emit(`${EVENTS.INVOKE_RESPONSE}:${route}`, packet.data) + } + else { + this.#handleDirectPacket(packet) + } } } @@ -431,10 +444,10 @@ export class IPC { const packet: Packet = { version: PROTOCOL_VERSION, id, - channel: CHANNELS.RESPONSE, + channel: SYSTEM_DOMAINS.RESPONSE, data, } - this.#sendPacket(packet) + this.#sendPacket(SYSTEM_DOMAINS.RESPONSE, packet) } } diff --git a/src/transport.ts b/src/transport.ts index b8ea6a1..6f37098 100644 --- a/src/transport.ts +++ b/src/transport.ts @@ -5,42 +5,61 @@ * * **Limit**: The underlying `/scriptevent` command accepts at most **2048 bytes** per message. * @see https://learn.microsoft.com/en-us/minecraft/creator/reference/content/commandsreference/examples/commands/scriptevent?view=minecraft-bedrock-stable#usage + * + * **Event ID format**: `ipc:::` + * - systemDomain: system domain for routing (user, response, etc.) + * - namespace: user-configured namespace, validated against injection + * - route: channel name (user domain) or invoke id (response domain) */ import { ScriptEventSource, system } from '@minecraft/server' -import { CHANNELS } from './constants' +import { SYSTEM_DOMAINS } from './constants' export class Transport { - readonly #id: string + readonly #namespace: string constructor(namespace: string) { - this.#id = `${CHANNELS.PREFIX}:${namespace}` + this.#namespace = namespace } /** - * Broadcast a raw string payload to all addons listening on the same namespace and channel. - * @param channel - The channel name to send on (appended to event ID for fast routing) - * @param payload - The raw string to send (usually a serialized packet) + * Broadcast a raw string payload to all addons listening on the same namespace and system domain. + * @param systemDomain - The system domain (user, response, etc.) + * @param route - The route within the domain (channel name or invoke id) + * @param payload - The raw string to send */ - send(channel: string, payload: string): void { - system.sendScriptEvent(`${this.#id}:${channel}`, payload) + send(systemDomain: string, route: string, payload: string): void { + system.sendScriptEvent(`${SYSTEM_DOMAINS.PREFIX}:${systemDomain}:${this.#namespace}:${route}`, payload) } /** * Subscribe to incoming messages from other addons. - * @param handler - Called with each incoming message, pre-routed by channel + * @param handler - Called with (systemDomain, route, payload) for each matching message * @returns A function that unsubscribes this handler */ - onReceive(handler: (channel: string, payload: string) => void): () => void { - const prefix = `${this.#id}:` + onReceive(handler: (systemDomain: string, route: string, payload: string) => void): () => void { + const basePrefix = `${SYSTEM_DOMAINS.PREFIX}:` + const nsPrefix = `${this.#namespace}:` + const callback = (event: { id: string, message: string, sourceType: ScriptEventSource }): void => { if (event.sourceType !== ScriptEventSource.Server) { return } - if (!event.id.startsWith(prefix)) { + if (!event.id.startsWith(basePrefix)) { + return + } + const suffix = event.id.slice(basePrefix.length) + const firstColon = suffix.indexOf(':') + if (firstColon < 0) { + return + } + const systemDomain = suffix.slice(0, firstColon) + const nsAndRoute = suffix.slice(firstColon + 1) + + if (!nsAndRoute.startsWith(nsPrefix)) { return } - const channel = event.id.slice(prefix.length) - handler(channel, event.message) + const route = nsAndRoute.slice(nsPrefix.length) + handler(systemDomain, route, event.message) } system.afterEvents.scriptEventReceive.subscribe(callback) 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 abb802e..a4e383e 100644 --- a/test/__snapshots__/tsnapi/@mcbe-mods/ipc/index.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@mcbe-mods/ipc/index.snapshot.d.ts @@ -85,8 +85,8 @@ export declare class IPC { export declare class Transport { #private; constructor(_: string); - send(_: string, _: string): void; - onReceive(_: (_: string, _: string) => void): () => void; + send(_: string, _: string, _: string): void; + onReceive(_: (_: string, _: string, _: string) => void): () => void; } // #endregion diff --git a/test/__snapshots__/tsnapi/@mcbe-mods/ipc/index.snapshot.js b/test/__snapshots__/tsnapi/@mcbe-mods/ipc/index.snapshot.js index 8b1815d..eea6a19 100644 --- a/test/__snapshots__/tsnapi/@mcbe-mods/ipc/index.snapshot.js +++ b/test/__snapshots__/tsnapi/@mcbe-mods/ipc/index.snapshot.js @@ -33,16 +33,16 @@ export class IPC { invoke(_, _, _) {} handle(_, _) {} invokeImpl(_, _, _) {} - sendPacket(_) {} - handleReceive(_, _) {} + sendPacket(_, _) {} + handleReceive(_, _, _) {} handleDirectPacket(_) {} - handleChunk(_) {} + handleChunk(_, _, _) {} sendResponse(_, _) {} } export class Transport { - id + namespace constructor(_) {} - send(_, _) {} + send(_, _, _) {} onReceive(_) {} } // #endregion diff --git a/test/ipc.bench.ts b/test/ipc.bench.ts index 19e2394..2664bdd 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 { CHANNELS, PROTOCOL_VERSION } from '../src/constants' +import { PROTOCOL_VERSION, SYSTEM_DOMAINS } 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(`${CHANNELS.PREFIX}:cycle:e`, payload) + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.USER}: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(`${CHANNELS.PREFIX}:cycle:e`, payload) + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.USER}:cycle:e`, payload) } }) }) @@ -61,8 +61,8 @@ describe('IPC.invoke + handle — RPC round-trip', () => { mockTransport.send.mockClear() const p = ipc.invoke('echo', SMALL) const id = invokeId(mockTransport.send.mock.calls[0][1]) - const resp = JSON.stringify({ version: PROTOCOL_VERSION, id, channel: CHANNELS.RESPONSE, data: { ok: true, data: SMALL } }) - mockTransport.simulateReceive(`${CHANNELS.PREFIX}:rpc:@response`, resp) + const resp = JSON.stringify({ version: PROTOCOL_VERSION, id, channel: SYSTEM_DOMAINS.RESPONSE, data: { ok: true, data: SMALL } }) + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.RESPONSE}:rpc:${id}`, resp) await p }) @@ -70,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({ version: PROTOCOL_VERSION, id, channel: CHANNELS.RESPONSE, data: { ok: true, data: MEDIUM } }) - mockTransport.simulateReceive(`${CHANNELS.PREFIX}:rpc:@response`, resp) + const resp = JSON.stringify({ version: PROTOCOL_VERSION, id, channel: SYSTEM_DOMAINS.RESPONSE, data: { ok: true, data: MEDIUM } }) + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.RESPONSE}:rpc:${id}`, resp) await p }) @@ -79,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({ version: PROTOCOL_VERSION, id, channel: CHANNELS.RESPONSE, data: { ok: true, data: LARGE } }) - mockTransport.simulateReceive(`${CHANNELS.PREFIX}:rpc:@response`, resp) + const resp = JSON.stringify({ version: PROTOCOL_VERSION, id, channel: SYSTEM_DOMAINS.RESPONSE, data: { ok: true, data: LARGE } }) + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.RESPONSE}:rpc:${id}`, resp) await p }) }) diff --git a/test/ipc.test.ts b/test/ipc.test.ts index 883e675..eddeb22 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 { CHANNELS, PROTOCOL_VERSION } from '../src/constants' +import { PROTOCOL_VERSION, SYSTEM_DOMAINS } from '../src/constants' import { IPC, IPC_SYSTEM_EVENTS } from '../src/ipc' import { mockTransport } from './setup' @@ -21,7 +21,7 @@ describe('IPC', () => { expect(mockTransport.send).toHaveBeenCalledTimes(1) const [id, payload] = mockTransport.send.mock.calls[0] - expect(id).toBe(`${CHANNELS.PREFIX}:test:ping`) + expect(id).toBe(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.USER}:test:ping`) const parsed = JSON.parse(payload) expect(parsed.version).toBe(1) @@ -34,19 +34,19 @@ describe('IPC', () => { ipc.on<{ msg: string }>('ping', handler) const packet = JSON.stringify({ version: PROTOCOL_VERSION, id: 'ABC123', channel: 'ping', data: { msg: 'hello' } }) - mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:ping`, packet) + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.USER}:test:ping`, packet) expect(handler).toHaveBeenCalledTimes(1) expect(handler).toHaveBeenCalledWith({ msg: 'hello' }) }) - it('supports multiple on() handlers per endpoint', () => { + it('supports multiple on() handlers per channel', () => { const h1 = vi.fn() const h2 = vi.fn() ipc.on('test', h1) ipc.on('test', h2) - mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:test`, JSON.stringify({ version: PROTOCOL_VERSION, id: 'X', channel: 'test', data: 42 })) + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.USER}: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(`${CHANNELS.PREFIX}:test:test`, JSON.stringify({ version: PROTOCOL_VERSION, id: 'X', channel: 'test', data: 42 })) + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.USER}:test:test`, JSON.stringify({ version: PROTOCOL_VERSION, id: 'X', channel: 'test', data: 42 })) expect(handler).not.toHaveBeenCalled() }) @@ -72,14 +72,14 @@ describe('IPC', () => { const sentPayload = mockTransport.send.mock.calls[0][1] const sentPacket = JSON.parse(sentPayload) - // Simulate response arriving back + // Simulate response arriving back on the response domain const responsePacket = JSON.stringify({ version: PROTOCOL_VERSION, id: sentPacket.id, - channel: CHANNELS.RESPONSE, + channel: SYSTEM_DOMAINS.RESPONSE, data: { ok: true, data: { y: '42' } }, }) - mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:@response`, responsePacket) + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.RESPONSE}:test:${sentPacket.id}`, responsePacket) const result = await promise expect(result).toEqual({ y: '42' }) @@ -92,7 +92,7 @@ describe('IPC', () => { // Simulate incoming invoke request const reqPacket = JSON.stringify({ version: PROTOCOL_VERSION, id: 'REQ1', channel: 'fail', data: {} }) - mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:fail`, reqPacket) + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.USER}:test:fail`, reqPacket) // Let microtasks settle await vi.runAllTimersAsync() @@ -101,11 +101,9 @@ describe('IPC', () => { const lastCall = mockTransport.send.mock.lastCall?.[1] if (lastCall) { const parsed = JSON.parse(lastCall) - // Could be chunk-wrapped or direct - 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') + if (parsed.data && typeof parsed.data === 'object' && 'ok' in parsed.data) { + expect(parsed.data.ok).toBe(false) + expect(parsed.data.err).toBe('Error: oops') } } }) @@ -119,12 +117,12 @@ describe('IPC', () => { // Should have sent multiple scriptEvents expect(mockTransport.send.mock.calls.length).toBeGreaterThan(1) - // All should use the ipc:test:big ID + // All should use the ipc:user:test:big ID for (const [id] of mockTransport.send.mock.calls) { - expect(id).toBe(`${CHANNELS.PREFIX}:test:big`) + expect(id).toBe(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.USER}:test:big`) } - // First call should be a chunk (has 'i' field) + // First call should be a chunk const firstPayload = JSON.parse(mockTransport.send.mock.calls[0][1]) expect(firstPayload.id).toBeDefined() expect(firstPayload.seq).toBe(0) @@ -147,7 +145,7 @@ describe('IPC', () => { // Send chunks for (let i = 0; i < chunks.length; i++) { const chunk = JSON.stringify({ id: 'CHUNKID', seq: i, total: chunks.length, data: chunks[i] }) - mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:big`, chunk) + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.USER}:test:big`, chunk) } expect(handler).toHaveBeenCalledTimes(1) @@ -160,7 +158,7 @@ describe('IPC', () => { ipc.on('dummy', () => {}) // register listener so pre-filter passes const chunk = JSON.stringify({ id: 'BADID', seq: 0, total: 1, data: 'not-json!!' }) - mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:dummy`, chunk) + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.USER}:test:dummy`, chunk) expect(errorHandler).toHaveBeenCalled() }) @@ -184,7 +182,7 @@ describe('IPC', () => { ipc.on('custom', customDeserializer, handler) const packet = JSON.stringify({ version: PROTOCOL_VERSION, id: 'X', channel: 'custom', data: 'num:42' }) - mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:custom`, packet) + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.USER}:test:custom`, packet) expect(handler).toHaveBeenCalledWith(42) }) @@ -194,11 +192,11 @@ describe('IPC', () => { const off = ipc.handle('temp', handler) off() - // Second handle with same endpoint should not throw since first was removed + // Second handle with same channel should not throw since first was removed expect(() => ipc.handle('temp', handler)).not.toThrow() }) - it('handle() throws on duplicate endpoint', () => { + it('handle() throws on duplicate channel', () => { ipc.handle('dup', () => 'ok') expect(() => ipc.handle('dup', () => 'ok')).toThrow('already registered') }) @@ -211,10 +209,10 @@ describe('IPC', () => { const responsePacket = JSON.stringify({ version: PROTOCOL_VERSION, id: sentPacket.id, - channel: CHANNELS.RESPONSE, + channel: SYSTEM_DOMAINS.RESPONSE, data: { ok: false, err: 'No handler registered for "ghost"' }, }) - mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:@response`, responsePacket) + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.RESPONSE}:test:${sentPacket.id}`, responsePacket) await expect(promise).rejects.toThrow('No handler registered for "ghost"') }) @@ -224,17 +222,17 @@ describe('IPC', () => { const handler = vi.fn() ipc2.on('ping', handler) - // ipc (namespace: 'test') sends — payload goes on ipc:test + // ipc (namespace: 'test') sends — payload goes on ipc:user:test ipc.send('ping', { msg: 'hello' }) const sentPayload = mockTransport.send.mock.lastCall?.[1] - // Simulate packet arriving on ipc:test:ping (sender's namespace) - // ipc2 listens on ipc:ns2, so it should NOT receive this - mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:ping`, sentPayload) + // Simulate packet arriving on ipc:user:test:ping (sender's namespace) + // ipc2 listens on ipc:user:ns2, so it should NOT receive this + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.USER}:test:ping`, sentPayload) expect(handler).not.toHaveBeenCalled() - // Simulate packet arriving on ipc:ns2:ping — ipc2 SHOULD receive it - mockTransport.simulateReceive(`${CHANNELS.PREFIX}:ns2:ping`, sentPayload) + // Simulate packet arriving on ipc:user:ns2:ping — ipc2 SHOULD receive it + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.USER}:ns2:ping`, sentPayload) expect(handler).toHaveBeenCalledTimes(1) expect(handler).toHaveBeenCalledWith({ msg: 'hello' }) }) @@ -250,13 +248,12 @@ describe('IPC', () => { const sentPayload = mockTransport.send.mock.lastCall?.[1] // Simulate on ns2's namespace — ipc2 should NOT handle - mockTransport.simulateReceive(`${CHANNELS.PREFIX}:ns2:ping`, sentPayload) + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.USER}:ns2:ping`, sentPayload) expect(handler).not.toHaveBeenCalled() - // 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.channel).not.toBe(CHANNELS.RESPONSE) + // No RESPONSE domain sends should have happened from ipc2 + for (const [id] of mockTransport.send.mock.calls) { + expect(id.startsWith(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.RESPONSE}:`)).toBe(false) } }) @@ -268,17 +265,17 @@ describe('IPC', () => { const sentPayload = mockTransport.send.mock.calls[0][1] const sentPacket = JSON.parse(sentPayload) - // Simulate loopback: invoke packet returns to sender - mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:echo`, JSON.stringify(sentPacket)) + // Simulate loopback: invoke packet returns to sender via USER domain + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.USER}:test:echo`, JSON.stringify(sentPacket)) - // Simulate normal response from the other side + // Simulate normal response from the other side via RESPONSE domain const responsePacket = JSON.stringify({ version: PROTOCOL_VERSION, id: sentPacket.id, - channel: CHANNELS.RESPONSE, + channel: SYSTEM_DOMAINS.RESPONSE, data: { ok: true, data: 'echo:hello' }, }) - mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:@response`, responsePacket) + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.RESPONSE}:test:${sentPacket.id}`, responsePacket) await expect(promise).resolves.toBe('echo:hello') }) @@ -293,17 +290,17 @@ describe('IPC', () => { const sentPacket = JSON.parse(sentPayload) // Simulate loopback — handle() should NOT be triggered - mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:test`, JSON.stringify(sentPacket)) + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.USER}:test:test`, JSON.stringify(sentPacket)) expect(handler).not.toHaveBeenCalled() // Resolve with a response from "the other side" const responsePacket = JSON.stringify({ version: PROTOCOL_VERSION, id: sentPacket.id, - channel: CHANNELS.RESPONSE, + channel: SYSTEM_DOMAINS.RESPONSE, data: { ok: true, data: 'ok' }, }) - mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:@response`, responsePacket) + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.RESPONSE}:test:${sentPacket.id}`, responsePacket) await expect(promise).resolves.toBe('ok') }) @@ -312,7 +309,7 @@ describe('IPC', () => { ipc.on('test', handler) ipc.dispose() - mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:test`, JSON.stringify({ version: PROTOCOL_VERSION, id: 'X', channel: 'test', data: 42 })) + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.USER}:test:test`, JSON.stringify({ version: PROTOCOL_VERSION, id: 'X', channel: 'test', data: 42 })) expect(handler).not.toHaveBeenCalled() }) @@ -322,7 +319,7 @@ describe('IPC', () => { ipc.events.on(IPC_SYSTEM_EVENTS.INVALID_PACKET, handler) ipc.on('dummy', () => {}) // register listener so pre-filter passes - mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:dummy`, JSON.stringify({ foo: 'bar' })) + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.USER}:test:dummy`, JSON.stringify({ foo: 'bar' })) expect(handler).toHaveBeenCalledTimes(1) expect(handler).toHaveBeenCalledWith({ payload: JSON.stringify({ foo: 'bar' }) }) @@ -341,10 +338,10 @@ describe('IPC', () => { const promise = ipc.invoke('fast', { timeout: 5000 }) const sent = JSON.parse(mockTransport.send.mock.calls[0][1]) - mockTransport.simulateReceive(`${CHANNELS.PREFIX}:test:@response`, JSON.stringify({ + mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.RESPONSE}:test:${sent.id}`, JSON.stringify({ version: PROTOCOL_VERSION, id: sent.id, - channel: CHANNELS.RESPONSE, + channel: SYSTEM_DOMAINS.RESPONSE, data: { ok: true, data: 'pong' }, }))