Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
})
```
Expand Down
4 changes: 0 additions & 4 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
15 changes: 15 additions & 0 deletions src/events.ts
Original file line number Diff line number Diff line change
@@ -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
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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'
59 changes: 14 additions & 45 deletions src/ipc.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { IPCEvents } from './events'

import type {
Chunk,
ErrorResponseData,
Expand All @@ -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<IPCOptions> = {
namespace: 'global',
Expand All @@ -25,34 +28,6 @@ const DEFAULT_OPTIONS: Required<IPCOptions> = {
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.
*
Expand Down Expand Up @@ -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<IPCSystemEvents>()
readonly events = new EventEmitter<IPCEvents>()

/**
* Creates an IPC instance bound to the given namespace.
Expand All @@ -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)
}
})
}
Expand Down Expand Up @@ -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<R> | ErrorResponseData
Expand Down Expand Up @@ -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)
Expand All @@ -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 })
}
}

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
}
20 changes: 20 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -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)
}
16 changes: 8 additions & 8 deletions test/__snapshots__/tsnapi/@mcbe-mods/ipc/index.snapshot.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,19 @@ export interface InvokeOptions<T = never, R = unknown> {
serializer?: Serializer<T>;
deserializer?: Deserializer<R>;
}
export interface IPCEvents {
[EVENTS.ERROR]: Error;
[EVENTS.INVALID_PACKET]: {
payload: string;
};
}
export interface IPCOptions {
namespace?: string;
chunkSize?: number;
compressThreshold?: number;
maxPacketSize?: number;
invokeTimeout?: number;
}
export interface IPCSystemEvents {
[IPC_SYSTEM_EVENTS.ERROR]: Error;
[IPC_SYSTEM_EVENTS.INVALID_PACKET]: {
payload: string;
};
}
export interface OnOptions<T = never> {
deserializer?: Deserializer<T>;
}
Expand Down Expand Up @@ -83,7 +83,7 @@ export declare class Compressor {
}
export declare class IPC {
#private;
readonly events: EventEmitter<IPCSystemEvents>;
readonly events: EventEmitter<IPCEvents>;
constructor(_?: IPCOptions);
dispose(): void;
send(_: string): void;
Expand All @@ -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";
};
Expand Down
2 changes: 1 addition & 1 deletion test/__snapshots__/tsnapi/@mcbe-mods/ipc/index.snapshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 4 additions & 3 deletions test/ipc.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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!!' })
Expand Down Expand Up @@ -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' }))
Expand Down
Loading