From 8ab637366ff70576b72b38e50da412ced29e3fc2 Mon Sep 17 00:00:00 2001 From: lete114 Date: Mon, 25 May 2026 20:35:26 +0800 Subject: [PATCH] refactor: extract events and utils from ipc.ts --- README.md | 8 +-- src/constants.ts | 4 -- src/events.ts | 15 +++++ src/index.ts | 5 +- src/ipc.ts | 59 +++++-------------- src/utils.ts | 20 +++++++ .../tsnapi/@mcbe-mods/ipc/index.snapshot.d.ts | 16 ++--- .../tsnapi/@mcbe-mods/ipc/index.snapshot.js | 2 +- test/ipc.test.ts | 7 ++- 9 files changed, 69 insertions(+), 67 deletions(-) create mode 100644 src/events.ts create mode 100644 src/utils.ts diff --git a/README.md b/README.md index 73a5f57..eb7f4c5 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ npm install @mcbe-mods/ipc ## Usage ```ts -import { IPC, IPC_SYSTEM_EVENTS } from '@mcbe-mods/ipc' +import { EVENTS, IPC } from '@mcbe-mods/ipc' const ipc = new IPC({ namespace: 'myAddon' }) // scriptEvent ID → ipc:myAddon @@ -143,14 +143,14 @@ interface IPCOptions { ## Events -System-level events emitted by `ipc.events` — listen with type safety via `IPC_SYSTEM_EVENTS`: +System-level events emitted by `ipc.events` — listen with type safety via `EVENTS`: ```ts -ipc.events.on(IPC_SYSTEM_EVENTS.ERROR, (err) => { +ipc.events.on(EVENTS.ERROR, (err) => { console.error('IPC error:', err.message) }) -ipc.events.on(IPC_SYSTEM_EVENTS.INVALID_PACKET, ({ payload }) => { +ipc.events.on(EVENTS.INVALID_PACKET, ({ payload }) => { console.warn('Received unrecognized payload:', payload) }) ``` diff --git a/src/constants.ts b/src/constants.ts index bbf3052..ef4e1d8 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -8,9 +8,5 @@ export const SYSTEM_DOMAINS = { RESPONSE: 'response', } as const -export const EVENTS = { - INVOKE_RESPONSE: 'invoke-response', -} as const - /** Current IPC protocol version */ export const PROTOCOL_VERSION = 1 as const diff --git a/src/events.ts b/src/events.ts new file mode 100644 index 0000000..e4aaa47 --- /dev/null +++ b/src/events.ts @@ -0,0 +1,15 @@ +/** Public — system events emitted by {@link IPC.events}. */ +export const EVENTS = { + ERROR: 'error', + INVALID_PACKET: 'invalid-packet', +} as const + +export interface IPCEvents { + [EVENTS.ERROR]: Error + [EVENTS.INVALID_PACKET]: { payload: string } +} + +/** Internal — EventEmitter routing keys (not exported from package) */ +export const SYSTEM_EVENTS = { + INVOKE_RESPONSE: 'invoke-response', +} as const diff --git a/src/index.ts b/src/index.ts index c799d16..f659d0f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,8 @@ export { Chunker } from './chunk' export { Compressor } from './compress' export { PROTOCOL_VERSION } from './constants' -export { IPC, IPC_SYSTEM_EVENTS } from './ipc' -export type { IPCSystemEvents } from './ipc' +export { EVENTS } from './events' +export type { IPCEvents } from './events' +export { IPC } from './ipc' export { Transport } from './transport' export type { Chunk, Deserializer, ErrorResponseData, HandleOptions, InvokeOptions, IPCOptions, OnOptions, Packet, ResponseData, SendOptions, Serializer } from './types' diff --git a/src/ipc.ts b/src/ipc.ts index 85f7e2b..366bb40 100644 --- a/src/ipc.ts +++ b/src/ipc.ts @@ -1,3 +1,5 @@ +import type { IPCEvents } from './events' + import type { Chunk, ErrorResponseData, @@ -9,13 +11,14 @@ import type { ResponseData, SendOptions, } from './types' - import { system } from '@minecraft/server' import { EventEmitter } from 'mini-emit' import { Chunker } from './chunk' import { Compressor } from './compress' -import { EVENTS, PROTOCOL_VERSION, SYSTEM_DOMAINS } from './constants' +import { PROTOCOL_VERSION, SYSTEM_DOMAINS } from './constants' +import { EVENTS, SYSTEM_EVENTS } from './events' import { Transport } from './transport' +import { generateId, isInvokeOptions } from './utils' const DEFAULT_OPTIONS: Required = { namespace: 'global', @@ -25,34 +28,6 @@ const DEFAULT_OPTIONS: Required = { invokeTimeout: 30_000, } -/** - * Events emitted by {@link IPC.events}. - * - `error`: An internal error occurred (e.g., malformed chunk reassembly). - * - `invalid-packet`: A received payload could not be parsed as a valid packet. - */ -export const IPC_SYSTEM_EVENTS = { - ERROR: 'error', - INVALID_PACKET: 'invalid-packet', -} as const - -export interface IPCSystemEvents { - [IPC_SYSTEM_EVENTS.ERROR]: Error - [IPC_SYSTEM_EVENTS.INVALID_PACKET]: { payload: string } -} - -const ID_RANDOM_BITS = 0x100000000 -const ID_RANDOM_CHARS = 6 -const ID_COUNTER_RADIX = 36 - -let idCounter = 0 - -/** Generate a short unique identifier for packet correlation (hex random + counter suffix). */ -function generateId(): string { - const r = ((Math.random() * ID_RANDOM_BITS) >>> 0).toString(16).slice(0, ID_RANDOM_CHARS).toUpperCase() - const c = (idCounter++ % ID_COUNTER_RADIX).toString(ID_COUNTER_RADIX).toUpperCase() - return r + c -} - /** * IPC (Inter-Pack Communication) — message passing between Minecraft Bedrock behavior packs. * @@ -82,9 +57,9 @@ export class IPC { /** * System-level event emitter for internal IPC events. - * See {@link IPC_SYSTEM_EVENTS} for available events. + * See {@link EVENTS} for available events. */ - readonly events = new EventEmitter() + readonly events = new EventEmitter() /** * Creates an IPC instance bound to the given namespace. @@ -102,7 +77,7 @@ export class IPC { this.#handleReceive(systemDomain, route, payload) } catch (e) { - this.events.emit(IPC_SYSTEM_EVENTS.ERROR, e as Error) + this.events.emit(EVENTS.ERROR, e as Error) } }) } @@ -317,7 +292,7 @@ export class IPC { this.#sentIds.add(id) - this.#responses.once(`${EVENTS.INVOKE_RESPONSE}:${id}`, (response: unknown) => { + this.#responses.once(`${SYSTEM_EVENTS.INVOKE_RESPONSE}:${id}`, (response: unknown) => { cleanup() this.#sentIds.delete(id) const resp = response as ResponseData | ErrorResponseData @@ -374,7 +349,7 @@ export class IPC { 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) + this.#responses.emit(`${SYSTEM_EVENTS.INVOKE_RESPONSE}:${route}`, (parsed as Packet).data) } else if ('seq' in parsed) { this.#handleChunk(parsed as Chunk, systemDomain, route) @@ -399,7 +374,7 @@ export class IPC { this.#handleChunk(parsed as Chunk, systemDomain, route) } else { - this.events.emit(IPC_SYSTEM_EVENTS.INVALID_PACKET, { payload }) + this.events.emit(EVENTS.INVALID_PACKET, { payload }) } } @@ -435,7 +410,7 @@ export class IPC { handler(data) } catch (e) { - this.events.emit(IPC_SYSTEM_EVENTS.ERROR, e as Error) + this.events.emit(EVENTS.ERROR, e as Error) } } return @@ -455,11 +430,11 @@ 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.id}`)) + this.events.emit(EVENTS.ERROR, new Error(`Failed to parse reassembled packet for chunk ${chunk.id}`)) return } if (systemDomain === SYSTEM_DOMAINS.RESPONSE) { - this.#responses.emit(`${EVENTS.INVOKE_RESPONSE}:${route}`, packet.data) + this.#responses.emit(`${SYSTEM_EVENTS.INVOKE_RESPONSE}:${route}`, packet.data) } else { this.#handleDirectPacket(packet) @@ -477,9 +452,3 @@ export class IPC { this.#sendPacket(SYSTEM_DOMAINS.RESPONSE, packet) } } - -/** Type guard: checks whether an unknown value is an {@link InvokeOptions} object. */ -function isInvokeOptions(obj: unknown): obj is InvokeOptions { - return typeof obj === 'object' && obj !== null - && ('timeout' in obj || 'serializer' in obj || 'deserializer' in obj) -} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..2bf3e91 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,20 @@ +import type { InvokeOptions } from './types' + +const ID_RANDOM_BITS = 0x100000000 +const ID_RANDOM_CHARS = 6 +const ID_COUNTER_RADIX = 36 + +let idCounter = 0 + +/** Generate a short unique identifier for packet correlation (hex random + counter suffix). */ +export function generateId(): string { + const r = ((Math.random() * ID_RANDOM_BITS) >>> 0).toString(16).slice(0, ID_RANDOM_CHARS).toUpperCase() + const c = (idCounter++ % ID_COUNTER_RADIX).toString(ID_COUNTER_RADIX).toUpperCase() + return r + c +} + +/** Type guard: checks whether an unknown value is an {@link InvokeOptions} object. */ +export function isInvokeOptions(obj: unknown): obj is InvokeOptions { + return typeof obj === 'object' && obj !== null + && ('timeout' in obj || 'serializer' in obj || 'deserializer' in obj) +} 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 4c81603..44fa10d 100644 --- a/test/__snapshots__/tsnapi/@mcbe-mods/ipc/index.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@mcbe-mods/ipc/index.snapshot.d.ts @@ -24,6 +24,12 @@ export interface InvokeOptions { serializer?: Serializer; deserializer?: Deserializer; } +export interface IPCEvents { + [EVENTS.ERROR]: Error; + [EVENTS.INVALID_PACKET]: { + payload: string; + }; +} export interface IPCOptions { namespace?: string; chunkSize?: number; @@ -31,12 +37,6 @@ export interface IPCOptions { maxPacketSize?: number; invokeTimeout?: number; } -export interface IPCSystemEvents { - [IPC_SYSTEM_EVENTS.ERROR]: Error; - [IPC_SYSTEM_EVENTS.INVALID_PACKET]: { - payload: string; - }; -} export interface OnOptions { deserializer?: Deserializer; } @@ -83,7 +83,7 @@ export declare class Compressor { } export declare class IPC { #private; - readonly events: EventEmitter; + readonly events: EventEmitter; constructor(_?: IPCOptions); dispose(): void; send(_: string): void; @@ -106,7 +106,7 @@ export declare class Transport { // #endregion // #region Variables -export declare const IPC_SYSTEM_EVENTS: { +export declare const EVENTS: { readonly ERROR: "error"; readonly INVALID_PACKET: "invalid-packet"; }; diff --git a/test/__snapshots__/tsnapi/@mcbe-mods/ipc/index.snapshot.js b/test/__snapshots__/tsnapi/@mcbe-mods/ipc/index.snapshot.js index a35260e..c22851f 100644 --- a/test/__snapshots__/tsnapi/@mcbe-mods/ipc/index.snapshot.js +++ b/test/__snapshots__/tsnapi/@mcbe-mods/ipc/index.snapshot.js @@ -48,6 +48,6 @@ export class Transport { // #endregion // #region Variables -export var IPC_SYSTEM_EVENTS /* const */ +export var EVENTS /* const */ export var PROTOCOL_VERSION /* const */ // #endregion \ No newline at end of file diff --git a/test/ipc.test.ts b/test/ipc.test.ts index 0625f7c..f724bcd 100644 --- a/test/ipc.test.ts +++ b/test/ipc.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { PROTOCOL_VERSION, SYSTEM_DOMAINS } from '../src/constants' -import { IPC, IPC_SYSTEM_EVENTS } from '../src/ipc' +import { EVENTS } from '../src/events' +import { IPC } from '../src/ipc' import { mockTransport } from './setup' describe('IPC', () => { @@ -230,7 +231,7 @@ describe('IPC', () => { it('emits error on malformed chunk reassembly', () => { const errorHandler = vi.fn() - ipc.events.on('error', errorHandler) + ipc.events.on(EVENTS.ERROR, errorHandler) ipc.on('dummy', () => {}) // register listener so pre-filter passes const chunk = JSON.stringify({ id: 'BADID', seq: 0, total: 1, data: 'not-json!!' }) @@ -412,7 +413,7 @@ describe('IPC', () => { it('emits invalid-packet event for unrecognized payloads', () => { const handler = vi.fn() - ipc.events.on(IPC_SYSTEM_EVENTS.INVALID_PACKET, handler) + ipc.events.on(EVENTS.INVALID_PACKET, handler) ipc.on('dummy', () => {}) // register listener so pre-filter passes mockTransport.simulateReceive(`${SYSTEM_DOMAINS.PREFIX}:${SYSTEM_DOMAINS.USER}:test:dummy`, JSON.stringify({ foo: 'bar' }))