@xpgamesllc/ipcs-be 0.0.2 → 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,21 +1,34 @@
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): string;
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
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;
19
32
  interface InitData {
20
33
  /**
21
34
  * The pack name.
@@ -30,10 +43,10 @@ interface KnownPack {
30
43
  name: string;
31
44
  author: string;
32
45
  }
33
- declare function init(initData: InitData): void;
46
+ declare function init(data: InitData): void;
34
47
  /**
35
48
  * Get the currently known packs.
36
49
  */
37
50
  declare function getKnownPacks(): KnownPack[];
38
51
 
39
- export { InitData, KnownPack, getKnownPacks, init, on, send, setVersion };
52
+ export { InitData, KnownPack, SendOptions, getKnownPacks, init, off, on, send, setVersion };
package/dist/index.d.ts CHANGED
@@ -1,21 +1,34 @@
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): string;
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
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;
19
32
  interface InitData {
20
33
  /**
21
34
  * The pack name.
@@ -30,10 +43,10 @@ interface KnownPack {
30
43
  name: string;
31
44
  author: string;
32
45
  }
33
- declare function init(initData: InitData): void;
46
+ declare function init(data: InitData): void;
34
47
  /**
35
48
  * Get the currently known packs.
36
49
  */
37
50
  declare function getKnownPacks(): KnownPack[];
38
51
 
39
- export { InitData, KnownPack, getKnownPacks, init, on, send, setVersion };
52
+ export { InitData, KnownPack, SendOptions, getKnownPacks, init, off, on, send, setVersion };
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ var src_exports = {};
22
22
  __export(src_exports, {
23
23
  getKnownPacks: () => getKnownPacks,
24
24
  init: () => init,
25
+ off: () => off,
25
26
  on: () => on,
26
27
  send: () => send,
27
28
  setVersion: () => setVersion
@@ -395,6 +396,45 @@ var Net = class _Net {
395
396
  static buffers = /* @__PURE__ */ new Map();
396
397
  static encoders = /* @__PURE__ */ new Map();
397
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
+ }
398
438
  /**
399
439
  * Register a new encoder implementation by its unique name.
400
440
  */
@@ -403,32 +443,37 @@ var Net = class _Net {
403
443
  }
404
444
  /** fire a script event, fragmenting JSON/text payload if needed. Optionally receive a response. */
405
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) {
406
453
  const encodingName = opts?.encoding ?? _Net.DefaultEncoding;
407
454
  const encoder = _Net.encoders.get(encodingName);
408
455
  if (!encoder)
409
456
  throw new Error(`Unknown encoder: ${encodingName}`);
410
457
  const raw = encoder.encode(payload);
411
- const id = (opts?.id ?? makeUUID()).replace(/-/g, "");
412
458
  if (callback) {
413
459
  const respChannel = `ipcs:${id}`;
414
460
  const wrapper = (id2, respPayload) => {
415
- try {
416
- callback(respPayload);
417
- } finally {
418
- _Net.off(respChannel, wrapper);
419
- }
461
+ import_server.system.clearRun(timer);
462
+ _Net.off(respChannel, wrapper);
463
+ callback(respPayload);
420
464
  };
421
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);
422
470
  }
423
471
  const totalLen = raw.length;
424
- const metaPart = `${SPEC_VERSION}|${encodingName}`;
425
472
  let chunks = [];
426
473
  const meta = { version: SPEC_VERSION, encoding: encodingName };
427
474
  const metaLength = getEnvelopeLength(meta);
428
475
  const noFragHdr = encoder.encodeHeader({ id });
429
- const noFragHdrLen = noFragHdr.length;
430
- const noFragOverhead = metaPart.length + RS.length + ETX.length + noFragHdrLen;
431
- if (totalLen + noFragOverhead + metaLength <= MAX_CHUNK) {
476
+ if (totalLen + noFragHdr.length + metaLength <= MAX_CHUNK) {
432
477
  chunks = [[noFragHdr, raw]];
433
478
  } else {
434
479
  let i = 0;
@@ -463,6 +508,7 @@ var Net = class _Net {
463
508
  const arr = _Net.listeners.get(channel) ?? [];
464
509
  arr.push(cb);
465
510
  _Net.listeners.set(channel, arr);
511
+ _Net.watchNamespace(channel);
466
512
  }
467
513
  /** unsubscribe a callback from a channel */
468
514
  static off(channel, cb) {
@@ -484,7 +530,7 @@ var Net = class _Net {
484
530
  try {
485
531
  if (id && !channel.startsWith("ipcs:")) {
486
532
  const respond = (responsePayload) => {
487
- _Net.send(`ipcs:${id}`, responsePayload, { encoding: encodingName, id });
533
+ _Net.sendWithId(`ipcs:${id}`, responsePayload, id, { encoding: encodingName });
488
534
  };
489
535
  cb(id, payload, respond);
490
536
  } else if (id) {
@@ -492,12 +538,17 @@ var Net = class _Net {
492
538
  } else {
493
539
  throw new Error(`No ID provided for channel ${channel}`);
494
540
  }
495
- } catch {
541
+ } catch (e) {
542
+ DEBUG: {
543
+ console.warn(`IPCS-BE: listener on ${channel} threw: ${e}`);
544
+ }
496
545
  }
497
546
  }
498
547
  }
499
548
  /** internal dispatcher: parse envelope, reassemble if needed, then fire user-cb */
500
549
  static handleScriptEvent(channel, raw) {
550
+ if (!_Net.listeners.has(channel))
551
+ return;
501
552
  const [metaPart, rest] = raw.split(RS, 2);
502
553
  if (!rest)
503
554
  return;
@@ -520,29 +571,56 @@ var Net = class _Net {
520
571
  _Net.dispatch(channel, payload, id, encodingName);
521
572
  return;
522
573
  }
523
- 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);
524
580
  if (!buf) {
525
- buf = { parts: [] };
526
- _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);
527
585
  }
528
- if (frag !== void 0 && buf.parts[frag] === void 0) {
529
- buf.parts[frag] = {
530
- body,
531
- frag: header.frag,
532
- last: header.last
533
- };
534
- 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)
535
591
  buf.lastIndex = frag;
536
- }
537
592
  }
538
- 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) {
539
- const full = buf.parts.map((p) => p.body).join("");
540
- _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);
541
596
  const payload = encoder.decode(full);
542
597
  if (payload !== null)
543
598
  _Net.dispatch(channel, payload, id, encodingName);
544
599
  }
545
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
+ }
546
624
  };
547
625
  Net.registerEncoder(new TextPacketEncoder());
548
626
  Net.registerEncoder(new Base64PacketEncoder());
@@ -552,17 +630,17 @@ var version = 1;
552
630
  var MaxVersion = 1;
553
631
  var MinVersion = 1;
554
632
  function setVersion(v) {
555
- if (version < MinVersion || version > MaxVersion) {
556
- 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`);
557
635
  }
558
636
  version = v;
559
637
  }
560
638
  function send(channel, payload, opts, callback) {
561
639
  if (version === 1) {
562
640
  return Net.send(channel, payload, {
641
+ ...opts,
563
642
  // By default, use the most preferred encoding
564
- encoding: opts?.encoding ?? supportedEncodings[0],
565
- ...opts
643
+ encoding: opts?.encoding ?? supportedEncodings[0]
566
644
  }, callback);
567
645
  }
568
646
  throw new Error(`IPCS-BE spec version ${version} is not supported`);
@@ -570,63 +648,79 @@ function send(channel, payload, opts, callback) {
570
648
  function on(channel, cb) {
571
649
  Net.on(channel, cb);
572
650
  }
573
- import_server2.system.beforeEvents.startup.subscribe(() => {
574
- import_server2.system.afterEvents.scriptEventReceive.subscribe((evt) => {
575
- Net.handleScriptEvent(evt.id, evt.message);
576
- });
577
- });
651
+ function off(channel, cb) {
652
+ Net.off(channel, cb);
653
+ }
654
+ Net.subscribe();
578
655
  var initialized = false;
579
656
  var KnownPacks = [];
580
657
  var supportedEncodings = ["b64", "text"];
581
658
  var HelloChannel = "ipcs:hello";
582
- function init(initData) {
659
+ var worldLoaded = false;
660
+ var initData;
661
+ var helloId = "";
662
+ function init(data) {
583
663
  if (initialized) {
584
664
  throw new Error("IPCS-BE is already initialized");
585
665
  }
586
- let helloId = "";
587
- import_server2.world.afterEvents.worldLoad.subscribe(() => {
588
- const packet = {
589
- name: initData.packName,
590
- author: initData.packAuthor,
591
- // Adjusted according to the library
592
- v: version,
593
- enc: ["text", "b64"]
594
- };
595
- helloId = send(HelloChannel, packet);
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"
596
685
  });
597
- on(HelloChannel, (id, payload) => {
598
- if (id === helloId) {
599
- return;
600
- }
601
- const helloPayload = payload;
602
- if (helloPayload.v > MaxVersion || helloPayload.v < MinVersion) {
603
- warn(`Received hello packet with unsupported version ${helloPayload.v}`);
604
- return;
605
- }
606
- if (!helloPayload.name || !helloPayload.author) {
607
- warn("Received hello packet with missing name or author");
608
- return;
609
- }
610
- if (!helloPayload.enc || !Array.isArray(helloPayload.enc) || helloPayload.enc.length === 0) {
611
- warn("Received hello packet with missing or invalid encodings");
612
- return;
613
- }
614
- for (const enc of [...supportedEncodings]) {
615
- if (enc === "text")
616
- continue;
617
- if (!helloPayload.enc.includes(enc)) {
618
- supportedEncodings = supportedEncodings.filter((e) => e !== enc);
619
- }
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);
620
713
  }
714
+ }
715
+ const known = KnownPacks.some((p) => p.name === helloPayload.name && p.author === helloPayload.author);
716
+ if (!known)
621
717
  KnownPacks.push({ name: helloPayload.name, author: helloPayload.author });
622
- });
623
- initialized = true;
624
- }
718
+ });
625
719
  function getKnownPacks() {
626
720
  if (!initialized) {
627
721
  throw new Error("IPCS-BE is not initialized");
628
722
  }
629
- return KnownPacks;
723
+ return KnownPacks.map((p) => ({ ...p }));
630
724
  }
631
725
  function warn(message) {
632
726
  DEBUG: {
@@ -637,6 +731,7 @@ function warn(message) {
637
731
  0 && (module.exports = {
638
732
  getKnownPacks,
639
733
  init,
734
+ off,
640
735
  on,
641
736
  send,
642
737
  setVersion