@xpgamesllc/ipcs-be 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,20 @@
1
- # ipcs-be
2
-
3
- A set of standardized types and functions for interacting with other Minecraft Bedrock Edition add-ons.
4
-
5
- ## Contributing
6
-
7
- Feel free to raise an issue or submit a pull request if you have any improvements or features to suggest.
8
-
9
- ## License
10
-
11
- This project is licensed under the GNU GPLv3 License.
1
+ # ipcs-be
2
+
3
+ A set of standardized types and functions for interacting with other Minecraft Bedrock Edition add-ons.
4
+
5
+ ## Requirements
6
+
7
+ The pack must depend on `@minecraft/server` 2.10.0 or newer in its manifest (stable or beta).
8
+
9
+ ## Logging
10
+
11
+ Warnings (invalid hello packets, errors thrown by listeners) are wrapped in a `DEBUG:` label. Strip them in
12
+ release builds by dropping the label in your bundler, e.g. esbuild `dropLabels: ["DEBUG"]`.
13
+
14
+ ## Contributing
15
+
16
+ Feel free to raise an issue or submit a pull request if you have any improvements or features to suggest.
17
+
18
+ ## License
19
+
20
+ This project is licensed under the MIT License.
package/dist/index.d.mts CHANGED
@@ -1,20 +1,52 @@
1
+ interface SendOptions {
2
+ encoding?: string;
3
+ /** message UUID: 32 hex digits, hyphens allowed, any case */
4
+ id?: string;
5
+ /** with a callback: stop waiting for the response after this many ticks (default Net.ResponseTimeoutTicks) */
6
+ timeoutTicks?: number;
7
+ /** with a callback: called once when no response came in time */
8
+ onTimeout?: () => void;
9
+ }
10
+
1
11
  declare function setVersion(v: number): void;
2
12
  /**
3
13
  * Send a script event, fragmenting JSON/text payload if needed. Optionally receive a response.
4
14
  * @param channel - The script-event channel to send to.
5
15
  * @param payload - The payload to send.
6
- * @param opts - Optional encoding and message ID.
7
- * @param callback - Optional callback to receive a response.
16
+ * @param opts - Optional encoding, message ID, and response timeout (`timeoutTicks`, `onTimeout`).
17
+ * @param callback - Optional callback to receive a response. It is dropped after the timeout.
8
18
  */
9
- declare function send(channel: string, payload: any, opts?: {
10
- encoding?: string;
11
- id?: string;
12
- }, callback?: (response: any) => void): void;
19
+ declare function send(channel: string, payload: any, opts?: SendOptions, callback?: (response: any) => void): string;
13
20
  /**
14
21
  * Subscribe to reassembled messages on a channel.
15
22
  * @param channel - The script-event channel to subscribe to.
16
23
  * @param cb - The callback to invoke when a message is received.
17
24
  */
18
- declare function on(channel: string, cb: (payload: any, respond?: (resp: any) => void) => void): void;
25
+ declare function on(channel: string, cb: (id: string, payload: any, respond?: (resp: any) => void) => void): void;
26
+ /**
27
+ * Unsubscribe a callback registered with `on`.
28
+ * @param channel - The script-event channel.
29
+ * @param cb - The same callback that was passed to `on`.
30
+ */
31
+ declare function off(channel: string, cb: (id: string, payload: any, respond?: (resp: any) => void) => void): void;
32
+ interface InitData {
33
+ /**
34
+ * The pack name.
35
+ */
36
+ packName: string;
37
+ /**
38
+ * The pack author.
39
+ */
40
+ packAuthor: string;
41
+ }
42
+ interface KnownPack {
43
+ name: string;
44
+ author: string;
45
+ }
46
+ declare function init(data: InitData): void;
47
+ /**
48
+ * Get the currently known packs.
49
+ */
50
+ declare function getKnownPacks(): KnownPack[];
19
51
 
20
- export { on, send, setVersion };
52
+ export { InitData, KnownPack, SendOptions, getKnownPacks, init, off, on, send, setVersion };
package/dist/index.d.ts CHANGED
@@ -1,20 +1,52 @@
1
+ interface SendOptions {
2
+ encoding?: string;
3
+ /** message UUID: 32 hex digits, hyphens allowed, any case */
4
+ id?: string;
5
+ /** with a callback: stop waiting for the response after this many ticks (default Net.ResponseTimeoutTicks) */
6
+ timeoutTicks?: number;
7
+ /** with a callback: called once when no response came in time */
8
+ onTimeout?: () => void;
9
+ }
10
+
1
11
  declare function setVersion(v: number): void;
2
12
  /**
3
13
  * Send a script event, fragmenting JSON/text payload if needed. Optionally receive a response.
4
14
  * @param channel - The script-event channel to send to.
5
15
  * @param payload - The payload to send.
6
- * @param opts - Optional encoding and message ID.
7
- * @param callback - Optional callback to receive a response.
16
+ * @param opts - Optional encoding, message ID, and response timeout (`timeoutTicks`, `onTimeout`).
17
+ * @param callback - Optional callback to receive a response. It is dropped after the timeout.
8
18
  */
9
- declare function send(channel: string, payload: any, opts?: {
10
- encoding?: string;
11
- id?: string;
12
- }, callback?: (response: any) => void): void;
19
+ declare function send(channel: string, payload: any, opts?: SendOptions, callback?: (response: any) => void): string;
13
20
  /**
14
21
  * Subscribe to reassembled messages on a channel.
15
22
  * @param channel - The script-event channel to subscribe to.
16
23
  * @param cb - The callback to invoke when a message is received.
17
24
  */
18
- declare function on(channel: string, cb: (payload: any, respond?: (resp: any) => void) => void): void;
25
+ declare function on(channel: string, cb: (id: string, payload: any, respond?: (resp: any) => void) => void): void;
26
+ /**
27
+ * Unsubscribe a callback registered with `on`.
28
+ * @param channel - The script-event channel.
29
+ * @param cb - The same callback that was passed to `on`.
30
+ */
31
+ declare function off(channel: string, cb: (id: string, payload: any, respond?: (resp: any) => void) => void): void;
32
+ interface InitData {
33
+ /**
34
+ * The pack name.
35
+ */
36
+ packName: string;
37
+ /**
38
+ * The pack author.
39
+ */
40
+ packAuthor: string;
41
+ }
42
+ interface KnownPack {
43
+ name: string;
44
+ author: string;
45
+ }
46
+ declare function init(data: InitData): void;
47
+ /**
48
+ * Get the currently known packs.
49
+ */
50
+ declare function getKnownPacks(): KnownPack[];
19
51
 
20
- export { on, send, setVersion };
52
+ export { InitData, KnownPack, SendOptions, getKnownPacks, init, off, on, send, setVersion };
package/dist/index.js CHANGED
@@ -20,6 +20,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
+ getKnownPacks: () => getKnownPacks,
24
+ init: () => init,
25
+ off: () => off,
23
26
  on: () => on,
24
27
  send: () => send,
25
28
  setVersion: () => setVersion
@@ -393,6 +396,45 @@ var Net = class _Net {
393
396
  static buffers = /* @__PURE__ */ new Map();
394
397
  static encoders = /* @__PURE__ */ new Map();
395
398
  static DefaultEncoding = "text";
399
+ /** a partial message is dropped when no fragment arrived for this long */
400
+ static BufferTimeoutMs = 1e4;
401
+ /** at most this many partial messages are kept; a new one drops the oldest */
402
+ static MaxBuffers = 64;
403
+ /** fragment indices at or above this are refused */
404
+ static MaxFragments = 4096;
405
+ /** how long send() waits for a response by default (20 ticks = 1 s) */
406
+ static ResponseTimeoutTicks = 200;
407
+ /** the engine only passes script events of these namespaces to the script */
408
+ static namespaces = /* @__PURE__ */ new Set(["ipcs"]);
409
+ static handler;
410
+ /** (re)subscribe to script events with the current namespace filter; the old handler works until the new one is in */
411
+ static subscribe(retry = true) {
412
+ const signal = import_server.system.afterEvents.scriptEventReceive;
413
+ const handler = (evt) => _Net.handleScriptEvent(evt.id, evt.message);
414
+ try {
415
+ signal.subscribe(handler, { namespaces: [..._Net.namespaces] });
416
+ } catch (e) {
417
+ if (retry) {
418
+ import_server.system.run(() => _Net.subscribe(false));
419
+ return;
420
+ }
421
+ DEBUG: {
422
+ console.warn(`IPCS-BE: could not subscribe to script events: ${e}`);
423
+ }
424
+ return;
425
+ }
426
+ if (_Net.handler)
427
+ signal.unsubscribe(_Net.handler);
428
+ _Net.handler = handler;
429
+ }
430
+ static watchNamespace(channel) {
431
+ const namespace = channel.split(":")[0];
432
+ if (_Net.namespaces.has(namespace))
433
+ return;
434
+ _Net.namespaces.add(namespace);
435
+ if (_Net.handler)
436
+ _Net.subscribe();
437
+ }
396
438
  /**
397
439
  * Register a new encoder implementation by its unique name.
398
440
  */
@@ -401,32 +443,37 @@ var Net = class _Net {
401
443
  }
402
444
  /** fire a script event, fragmenting JSON/text payload if needed. Optionally receive a response. */
403
445
  static send(channel, payload, opts, callback) {
446
+ const id = (opts?.id ?? makeUUID()).replace(/-/g, "").toLowerCase();
447
+ if (!/^[0-9a-f]{32}$/.test(id))
448
+ throw new Error(`Invalid message id "${opts?.id}": expected a UUID`);
449
+ return _Net.sendWithId(channel, payload, id, opts, callback);
450
+ }
451
+ /** responses reuse the request id as it came, older versions did not check it */
452
+ static sendWithId(channel, payload, id, opts, callback) {
404
453
  const encodingName = opts?.encoding ?? _Net.DefaultEncoding;
405
454
  const encoder = _Net.encoders.get(encodingName);
406
455
  if (!encoder)
407
456
  throw new Error(`Unknown encoder: ${encodingName}`);
408
457
  const raw = encoder.encode(payload);
409
- const id = (opts?.id ?? makeUUID()).replace(/-/g, "");
410
458
  if (callback) {
411
459
  const respChannel = `ipcs:${id}`;
412
- const wrapper = (respPayload) => {
413
- try {
414
- callback(respPayload);
415
- } finally {
416
- _Net.off(respChannel, wrapper);
417
- }
460
+ const wrapper = (id2, respPayload) => {
461
+ import_server.system.clearRun(timer);
462
+ _Net.off(respChannel, wrapper);
463
+ callback(respPayload);
418
464
  };
419
465
  _Net.on(respChannel, wrapper);
466
+ const timer = import_server.system.runTimeout(() => {
467
+ _Net.off(respChannel, wrapper);
468
+ opts?.onTimeout?.();
469
+ }, opts?.timeoutTicks ?? _Net.ResponseTimeoutTicks);
420
470
  }
421
471
  const totalLen = raw.length;
422
- const metaPart = `${SPEC_VERSION}|${encodingName}`;
423
472
  let chunks = [];
424
473
  const meta = { version: SPEC_VERSION, encoding: encodingName };
425
474
  const metaLength = getEnvelopeLength(meta);
426
475
  const noFragHdr = encoder.encodeHeader({ id });
427
- const noFragHdrLen = noFragHdr.length;
428
- const noFragOverhead = metaPart.length + RS.length + ETX.length + noFragHdrLen;
429
- if (totalLen + noFragOverhead + metaLength <= MAX_CHUNK) {
476
+ if (totalLen + noFragHdr.length + metaLength <= MAX_CHUNK) {
430
477
  chunks = [[noFragHdr, raw]];
431
478
  } else {
432
479
  let i = 0;
@@ -454,12 +501,14 @@ var Net = class _Net {
454
501
  import_server.system.sendScriptEvent(channel, envelope);
455
502
  }
456
503
  }
504
+ return id;
457
505
  }
458
506
  /** subscribe to reassembled messages on a channel */
459
507
  static on(channel, cb) {
460
508
  const arr = _Net.listeners.get(channel) ?? [];
461
509
  arr.push(cb);
462
510
  _Net.listeners.set(channel, arr);
511
+ _Net.watchNamespace(channel);
463
512
  }
464
513
  /** unsubscribe a callback from a channel */
465
514
  static off(channel, cb) {
@@ -481,18 +530,25 @@ var Net = class _Net {
481
530
  try {
482
531
  if (id && !channel.startsWith("ipcs:")) {
483
532
  const respond = (responsePayload) => {
484
- _Net.send(`ipcs:${id}`, responsePayload, { encoding: encodingName, id });
533
+ _Net.sendWithId(`ipcs:${id}`, responsePayload, id, { encoding: encodingName });
485
534
  };
486
- cb(payload, respond);
535
+ cb(id, payload, respond);
536
+ } else if (id) {
537
+ cb(id, payload);
487
538
  } else {
488
- cb(payload);
539
+ throw new Error(`No ID provided for channel ${channel}`);
540
+ }
541
+ } catch (e) {
542
+ DEBUG: {
543
+ console.warn(`IPCS-BE: listener on ${channel} threw: ${e}`);
489
544
  }
490
- } catch {
491
545
  }
492
546
  }
493
547
  }
494
548
  /** internal dispatcher: parse envelope, reassemble if needed, then fire user-cb */
495
549
  static handleScriptEvent(channel, raw) {
550
+ if (!_Net.listeners.has(channel))
551
+ return;
496
552
  const [metaPart, rest] = raw.split(RS, 2);
497
553
  if (!rest)
498
554
  return;
@@ -515,29 +571,56 @@ var Net = class _Net {
515
571
  _Net.dispatch(channel, payload, id, encodingName);
516
572
  return;
517
573
  }
518
- let buf = _Net.buffers.get(id);
574
+ if (!Number.isInteger(frag) || frag < 0 || frag >= _Net.MaxFragments)
575
+ return;
576
+ const now = Date.now();
577
+ _Net.dropStale(now);
578
+ const key = `${channel}|${id}`;
579
+ let buf = _Net.buffers.get(key);
519
580
  if (!buf) {
520
- buf = { parts: [] };
521
- _Net.buffers.set(id, buf);
581
+ if (_Net.buffers.size >= _Net.MaxBuffers)
582
+ _Net.dropOldest();
583
+ buf = { parts: [], received: 0, touched: now };
584
+ _Net.buffers.set(key, buf);
522
585
  }
523
- if (frag !== void 0 && buf.parts[frag] === void 0) {
524
- buf.parts[frag] = {
525
- body,
526
- frag: header.frag,
527
- last: header.last
528
- };
529
- if (header.last) {
586
+ buf.touched = now;
587
+ if (buf.parts[frag] === void 0) {
588
+ buf.parts[frag] = body;
589
+ buf.received++;
590
+ if (last)
530
591
  buf.lastIndex = frag;
531
- }
532
592
  }
533
- if (buf.lastIndex !== void 0 && buf.parts.length === buf.lastIndex + 1 && buf.parts.sort((a, b) => a.frag - b.frag)[buf.lastIndex].frag === buf.lastIndex) {
534
- const full = buf.parts.map((p) => p.body).join("");
535
- _Net.buffers.delete(id);
593
+ if (buf.lastIndex !== void 0 && buf.received === buf.lastIndex + 1) {
594
+ const full = buf.parts.join("");
595
+ _Net.buffers.delete(key);
536
596
  const payload = encoder.decode(full);
537
597
  if (payload !== null)
538
598
  _Net.dispatch(channel, payload, id, encodingName);
539
599
  }
540
600
  }
601
+ /** drop partial messages that got no fragment for BufferTimeoutMs */
602
+ static dropStale(now) {
603
+ for (const [key, buf] of _Net.buffers) {
604
+ if (now - buf.touched > _Net.BufferTimeoutMs)
605
+ _Net.buffers.delete(key);
606
+ }
607
+ }
608
+ static dropOldest() {
609
+ let oldestKey;
610
+ let oldest = Infinity;
611
+ for (const [key, buf] of _Net.buffers) {
612
+ if (buf.touched < oldest) {
613
+ oldest = buf.touched;
614
+ oldestKey = key;
615
+ }
616
+ }
617
+ if (oldestKey !== void 0)
618
+ _Net.buffers.delete(oldestKey);
619
+ }
620
+ /** number of partial messages waiting for fragments */
621
+ static pendingCount() {
622
+ return _Net.buffers.size;
623
+ }
541
624
  };
542
625
  Net.registerEncoder(new TextPacketEncoder());
543
626
  Net.registerEncoder(new Base64PacketEncoder());
@@ -547,24 +630,108 @@ var version = 1;
547
630
  var MaxVersion = 1;
548
631
  var MinVersion = 1;
549
632
  function setVersion(v) {
550
- if (version < MinVersion || version > MaxVersion) {
551
- throw new Error(`IPCS-BE spec version ${version} is not supported`);
633
+ if (v < MinVersion || v > MaxVersion) {
634
+ throw new Error(`IPCS-BE spec version ${v} is not supported`);
552
635
  }
553
636
  version = v;
554
637
  }
555
638
  function send(channel, payload, opts, callback) {
556
639
  if (version === 1) {
557
- Net.send(channel, payload, opts, callback);
640
+ return Net.send(channel, payload, {
641
+ ...opts,
642
+ // By default, use the most preferred encoding
643
+ encoding: opts?.encoding ?? supportedEncodings[0]
644
+ }, callback);
558
645
  }
646
+ throw new Error(`IPCS-BE spec version ${version} is not supported`);
559
647
  }
560
648
  function on(channel, cb) {
561
649
  Net.on(channel, cb);
562
650
  }
563
- import_server2.system.afterEvents.scriptEventReceive.subscribe((evt) => {
564
- Net.handleScriptEvent(evt.id, evt.message);
651
+ function off(channel, cb) {
652
+ Net.off(channel, cb);
653
+ }
654
+ Net.subscribe();
655
+ var initialized = false;
656
+ var KnownPacks = [];
657
+ var supportedEncodings = ["b64", "text"];
658
+ var HelloChannel = "ipcs:hello";
659
+ var worldLoaded = false;
660
+ var initData;
661
+ var helloId = "";
662
+ function init(data) {
663
+ if (initialized) {
664
+ throw new Error("IPCS-BE is already initialized");
665
+ }
666
+ initData = data;
667
+ helloId = makeUUID().replace(/-/g, "");
668
+ initialized = true;
669
+ if (worldLoaded)
670
+ import_server2.system.run(sendHello);
671
+ }
672
+ function sendHello() {
673
+ if (!initData)
674
+ return;
675
+ const packet = {
676
+ name: initData.packName,
677
+ author: initData.packAuthor,
678
+ // Adjusted according to the library
679
+ v: version,
680
+ enc: ["text", "b64"]
681
+ };
682
+ send(HelloChannel, packet, {
683
+ id: helloId,
684
+ encoding: "text"
685
+ });
686
+ }
687
+ import_server2.world.afterEvents.worldLoad.subscribe(() => {
688
+ worldLoaded = true;
689
+ sendHello();
690
+ });
691
+ on(HelloChannel, (id, payload) => {
692
+ if (id === helloId) {
693
+ return;
694
+ }
695
+ const helloPayload = payload;
696
+ if (helloPayload.v > MaxVersion || helloPayload.v < MinVersion) {
697
+ warn(`Received hello packet with unsupported version ${helloPayload.v}`);
698
+ return;
699
+ }
700
+ if (!helloPayload.name || !helloPayload.author) {
701
+ warn("Received hello packet with missing name or author");
702
+ return;
703
+ }
704
+ if (!helloPayload.enc || !Array.isArray(helloPayload.enc) || helloPayload.enc.length === 0) {
705
+ warn("Received hello packet with missing or invalid encodings");
706
+ return;
707
+ }
708
+ for (const enc of [...supportedEncodings]) {
709
+ if (enc === "text")
710
+ continue;
711
+ if (!helloPayload.enc.includes(enc)) {
712
+ supportedEncodings = supportedEncodings.filter((e) => e !== enc);
713
+ }
714
+ }
715
+ const known = KnownPacks.some((p) => p.name === helloPayload.name && p.author === helloPayload.author);
716
+ if (!known)
717
+ KnownPacks.push({ name: helloPayload.name, author: helloPayload.author });
565
718
  });
719
+ function getKnownPacks() {
720
+ if (!initialized) {
721
+ throw new Error("IPCS-BE is not initialized");
722
+ }
723
+ return KnownPacks.map((p) => ({ ...p }));
724
+ }
725
+ function warn(message) {
726
+ DEBUG: {
727
+ console.warn(message);
728
+ }
729
+ }
566
730
  // Annotate the CommonJS export names for ESM import in node:
567
731
  0 && (module.exports = {
732
+ getKnownPacks,
733
+ init,
734
+ off,
568
735
  on,
569
736
  send,
570
737
  setVersion