@ai-ecoverse/slicc-kernel 1.37.1 → 1.39.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/dist/index.js CHANGED
@@ -509,7 +509,153 @@ async function readWithin(reader, ms) {
509
509
  }
510
510
  }
511
511
 
512
+ // src/kernel/net/ws-queue.ts
513
+ var INBOUND_LIMIT = 16 * 1024 * 1024;
514
+ var MESSAGE_LIMIT = 64 * 1024 * 1024;
515
+ var TOO_BIG = 1009;
516
+ function payloadSize(message) {
517
+ return typeof message === "string" ? new TextEncoder().encode(message).length : message.length;
518
+ }
519
+ var MessageQueue = class {
520
+ items = [];
521
+ limit;
522
+ waiter;
523
+ size = 0;
524
+ ended = false;
525
+ overflow;
526
+ taken;
527
+ closed;
528
+ settle;
529
+ constructor(overflow, limit = INBOUND_LIMIT, taken) {
530
+ this.overflow = overflow;
531
+ this.limit = limit;
532
+ this.taken = taken;
533
+ this.closed = new Promise((resolve) => {
534
+ this.settle = resolve;
535
+ });
536
+ }
537
+ push(message) {
538
+ if (this.ended) return;
539
+ const size = payloadSize(message);
540
+ const huge = size > MESSAGE_LIMIT;
541
+ if (huge || this.items.length > 0 && this.size + size > this.limit) {
542
+ const reason = huge ? `a message is over ${MESSAGE_LIMIT} bytes` : `over ${this.limit} bytes of messages are waiting`;
543
+ this.end({ code: TOO_BIG, reason });
544
+ this.overflow();
545
+ return;
546
+ }
547
+ this.size += size;
548
+ this.items.push(message);
549
+ this.wake();
550
+ }
551
+ end(close) {
552
+ if (this.ended) return;
553
+ this.ended = true;
554
+ this.settle(close);
555
+ this.wake();
556
+ }
557
+ wake() {
558
+ const waiter = this.waiter;
559
+ this.waiter = void 0;
560
+ waiter?.();
561
+ }
562
+ async *[Symbol.asyncIterator]() {
563
+ for (; ; ) {
564
+ const next = this.items.shift();
565
+ if (next !== void 0) {
566
+ const n = payloadSize(next);
567
+ this.size -= n;
568
+ this.taken?.(n);
569
+ yield next;
570
+ continue;
571
+ }
572
+ if (this.ended) return;
573
+ await new Promise((resolve) => {
574
+ this.waiter = resolve;
575
+ });
576
+ }
577
+ }
578
+ };
579
+
580
+ // src/websocket-transport.ts
581
+ var SENT_HEADERS_DROP = /* @__PURE__ */ new Set([
582
+ "host",
583
+ "connection",
584
+ "upgrade",
585
+ "sec-websocket-key",
586
+ "sec-websocket-version",
587
+ "sec-websocket-extensions",
588
+ "sec-websocket-protocol"
589
+ ]);
590
+ function headerRecord(headers) {
591
+ const out = {};
592
+ for (const [name, value] of headers) {
593
+ if (!SENT_HEADERS_DROP.has(name.toLowerCase())) out[name] = value;
594
+ }
595
+ return out;
596
+ }
597
+ function sendableClose(code) {
598
+ return code === 1e3 || code !== void 0 && code >= 3e3 && code <= 4999 ? code : 1e3;
599
+ }
600
+ function blockedReason(url, e) {
601
+ const name = e?.name;
602
+ if (name === "SecurityError" && url.startsWith("ws:")) {
603
+ return `${url}: ws:// is blocked from an https page; use wss:// or slicc-node`;
604
+ }
605
+ return `${url}: ${e instanceof Error ? e.message : String(e)}`;
606
+ }
607
+ function openWebSocket(Socket, request, withHeaders) {
608
+ const { url, protocols, signal } = request;
609
+ signal.throwIfAborted();
610
+ let socket;
611
+ try {
612
+ socket = withHeaders ? new Socket(url, { protocols, headers: headerRecord(request.headers) }) : new Socket(url, protocols);
613
+ } catch (e) {
614
+ return Promise.reject(Object.assign(new Error(blockedReason(url, e)), { status: 502 }));
615
+ }
616
+ socket.binaryType = "arraybuffer";
617
+ const queue = new MessageQueue(() => socket.close(1e3, "too many waiting messages"));
618
+ return new Promise((resolve, reject) => {
619
+ let open = false;
620
+ const abort = () => {
621
+ socket.close(1e3, "aborted");
622
+ reject(signal.reason);
623
+ };
624
+ signal.addEventListener("abort", abort, { once: true });
625
+ socket.addEventListener("open", () => {
626
+ open = true;
627
+ signal.removeEventListener("abort", abort);
628
+ resolve({
629
+ protocol: socket.protocol,
630
+ get buffered() {
631
+ return socket.bufferedAmount;
632
+ },
633
+ send: (data) => socket.send(data),
634
+ messages: queue,
635
+ close: (code, reason) => socket.close(sendableClose(code), reason),
636
+ closed: queue.closed
637
+ });
638
+ });
639
+ socket.addEventListener("message", (event) => {
640
+ queue.push(typeof event.data === "string" ? event.data : new Uint8Array(event.data));
641
+ });
642
+ socket.addEventListener("close", (event) => {
643
+ queue.end({ code: event.code, reason: event.reason });
644
+ if (open) return;
645
+ signal.removeEventListener("abort", abort);
646
+ reject(
647
+ Object.assign(new Error(`${url}: the WebSocket closed (${event.code}) before it opened`), {
648
+ status: 502
649
+ })
650
+ );
651
+ });
652
+ });
653
+ }
654
+
512
655
  // src/transport.ts
656
+ var SEND_BUFFER = 1024 * 1024;
657
+ var CLOSE_WAIT_MS = 5e3;
658
+ var NO_WEBSOCKET = "WebSocket not supported by this transport (fetchTransport needs { webSocket: true })";
513
659
  var MAX_REQUEST_BODY = 64 * 1024 * 1024;
514
660
  function joinHeaders(headers) {
515
661
  const out = new Headers();
@@ -534,13 +680,17 @@ async function* chunks(body, idleMs) {
534
680
  }
535
681
  function fetchTransport(options = {}) {
536
682
  const send2 = options.fetch ?? globalThis.fetch.bind(globalThis);
683
+ const Socket = options.webSocket === true ? globalThis.WebSocket : options.webSocket || void 0;
684
+ const headers = options.webSocketHeaders === true;
537
685
  return {
538
686
  traits: {
539
687
  manualRedirects: false,
540
688
  encodedBodies: false,
541
689
  maxRequestBody: options.maxRequestBody ?? MAX_REQUEST_BODY,
542
- crossOrigin: "cors"
690
+ crossOrigin: "cors",
691
+ ...Socket ? { websocket: true } : {}
543
692
  },
693
+ ...Socket ? { websocket: (request) => openWebSocket(Socket, request, headers) } : {},
544
694
  async fetch(request) {
545
695
  let response;
546
696
  try {
@@ -562,15 +712,15 @@ function fetchTransport(options = {}) {
562
712
  { status: 502 }
563
713
  );
564
714
  }
565
- const headers = [];
715
+ const headers2 = [];
566
716
  response.headers.forEach((value, name) => {
567
- headers.push([name, value]);
717
+ headers2.push([name, value]);
568
718
  });
569
719
  const body = chunks(response.body, options.bodyIdleMs ?? BODY_IDLE_MS);
570
720
  return {
571
721
  status: response.status,
572
722
  statusText: response.statusText,
573
- headers,
723
+ headers: headers2,
574
724
  body,
575
725
  cancel: async () => {
576
726
  await body.return(void 0);
@@ -580,7 +730,7 @@ function fetchTransport(options = {}) {
580
730
  }
581
731
  };
582
732
  }
583
- function serveTransport(peer, transport) {
733
+ function serveTransport(peer, transport, { closeWaitMs = CLOSE_WAIT_MS } = {}) {
584
734
  const open = /* @__PURE__ */ new Map();
585
735
  const fail = (nid, e) => {
586
736
  open.delete(nid);
@@ -621,6 +771,96 @@ function serveTransport(peer, transport) {
621
771
  fail(call.nid, e);
622
772
  }
623
773
  };
774
+ const sockets = /* @__PURE__ */ new Map();
775
+ const credit = /* @__PURE__ */ new Map();
776
+ const refill = (nid, n, closing = false) => {
777
+ const flow = credit.get(nid);
778
+ if (!flow) return;
779
+ flow.inflight -= n;
780
+ if (closing && !flow.closing) {
781
+ flow.closing = true;
782
+ flow.deadline();
783
+ }
784
+ flow.wake?.();
785
+ };
786
+ const openSocket = async (call) => {
787
+ const abort = new AbortController();
788
+ open.set(call.nid, { abort });
789
+ try {
790
+ if (!transport.websocket) throw new Error(NO_WEBSOCKET);
791
+ const socket = await transport.websocket({
792
+ url: call.url,
793
+ protocols: call.protocols,
794
+ headers: call.headers,
795
+ signal: abort.signal
796
+ });
797
+ if (!open.delete(call.nid)) {
798
+ socket.close(1e3, "cancelled");
799
+ return;
800
+ }
801
+ sockets.set(call.nid, socket);
802
+ peer.postMessage({ net: "ws-opened", nid: call.nid, protocol: socket.protocol });
803
+ void relay(call.nid, socket);
804
+ } catch (e) {
805
+ fail(call.nid, e);
806
+ }
807
+ };
808
+ const relay = async (nid, socket) => {
809
+ let ending;
810
+ let late;
811
+ const unanswered = new Promise((resolve) => {
812
+ late = () => resolve("late");
813
+ });
814
+ const flow = {
815
+ inflight: 0,
816
+ closing: false,
817
+ deadline: () => AbortSignal.timeout(closeWaitMs).addEventListener("abort", late, { once: true })
818
+ };
819
+ credit.set(nid, flow);
820
+ const timedOut = { code: 1006, reason: "the far end did not answer the close" };
821
+ let oversize = false;
822
+ try {
823
+ const messages = socket.messages[Symbol.asyncIterator]();
824
+ for (; ; ) {
825
+ const next = await Promise.race([messages.next(), unanswered]);
826
+ if (next === "late" || next.done) break;
827
+ const data = next.value;
828
+ if (flow.closing) continue;
829
+ if (payloadSize(data) > MESSAGE_LIMIT) {
830
+ oversize = true;
831
+ break;
832
+ }
833
+ if (typeof data === "string") peer.postMessage({ net: "ws-message", nid, data });
834
+ else {
835
+ const bytes = data.slice();
836
+ peer.postMessage({ net: "ws-message", nid, data: bytes }, [bytes.buffer]);
837
+ }
838
+ flow.inflight += payloadSize(data);
839
+ while (flow.inflight > INBOUND_LIMIT && !flow.closing) {
840
+ await new Promise((resolve) => {
841
+ flow.wake = resolve;
842
+ });
843
+ }
844
+ }
845
+ if (oversize) socket.close(1e3, "a message is too big");
846
+ ending = oversize ? { code: 1009, reason: `a message is over ${MESSAGE_LIMIT} bytes` } : await Promise.race([socket.closed, unanswered.then(() => timedOut)]);
847
+ } catch (e) {
848
+ socket.close(1e3, "the relay failed");
849
+ ending = { code: 1011, reason: e instanceof Error ? e.message : String(e) };
850
+ }
851
+ sockets.delete(nid);
852
+ credit.delete(nid);
853
+ peer.postMessage({ net: "ws-closed", nid, ...ending });
854
+ };
855
+ const sendOn = async (call) => {
856
+ const socket = sockets.get(call.nid);
857
+ if (!socket) return;
858
+ socket.send(call.data);
859
+ while (socket.buffered > SEND_BUFFER && sockets.has(call.nid)) {
860
+ await new Promise((resolve) => setTimeout(resolve, 10));
861
+ }
862
+ peer.postMessage({ net: "ws-acked", nid: call.nid, n: payloadSize(call.data) });
863
+ };
624
864
  const read = async (nid) => {
625
865
  const body = open.get(nid)?.body;
626
866
  if (!body) return;
@@ -638,6 +878,8 @@ function serveTransport(peer, transport) {
638
878
  }
639
879
  };
640
880
  const drop = (nid) => {
881
+ sockets.get(nid)?.close(1e3, "cancelled");
882
+ refill(nid, 0, true);
641
883
  const entry = open.get(nid);
642
884
  open.delete(nid);
643
885
  entry?.abort.abort();
@@ -647,10 +889,16 @@ function serveTransport(peer, transport) {
647
889
  answer(call) {
648
890
  if (call.net === "fetch") void start(call);
649
891
  else if (call.net === "read") void read(call.nid);
892
+ else if (call.net === "ws-open") void openSocket(call);
893
+ else if (call.net === "ws-send") void sendOn(call);
894
+ else if (call.net === "ws-close") {
895
+ sockets.get(call.nid)?.close(call.code, call.reason);
896
+ refill(call.nid, 0, true);
897
+ } else if (call.net === "ws-consumed") refill(call.nid, call.n);
650
898
  else drop(call.nid);
651
899
  },
652
900
  close() {
653
- for (const nid of [...open.keys()]) drop(nid);
901
+ for (const nid of [...open.keys(), ...sockets.keys()]) drop(nid);
654
902
  }
655
903
  };
656
904
  }
@@ -670,12 +918,15 @@ var RemoteTransport = class {
670
918
  port;
671
919
  waiting = /* @__PURE__ */ new Map();
672
920
  nextId = 0;
921
+ sockets = /* @__PURE__ */ new Map();
673
922
  failure;
674
923
  constructor(port, traits) {
675
924
  this.port = port;
676
925
  this.traits = traits;
677
926
  }
678
927
  receive(reply) {
928
+ const socket = this.sockets.get(reply.nid);
929
+ if (socket && this.toSocket(reply, socket)) return;
679
930
  const waiter = this.waiting.get(reply.nid);
680
931
  if (!waiter) return;
681
932
  this.waiting.delete(reply.nid);
@@ -683,10 +934,22 @@ var RemoteTransport = class {
683
934
  waiter.reject(new TransportError(reply.message, reply.status, reply.code));
684
935
  } else waiter.resolve(reply);
685
936
  }
937
+ toSocket(reply, socket) {
938
+ if (reply.net === "ws-message") socket.queue.push(reply.data);
939
+ else if (reply.net === "ws-acked") socket.acked(reply.n);
940
+ else if (reply.net === "ws-closed") {
941
+ this.sockets.delete(reply.nid);
942
+ socket.queue.end({ code: reply.code, reason: reply.reason });
943
+ } else return false;
944
+ return true;
945
+ }
686
946
  fail(error) {
687
947
  this.failure = error;
688
948
  for (const waiter of this.waiting.values()) waiter.reject(error);
689
949
  this.waiting.clear();
950
+ for (const socket of this.sockets.values())
951
+ socket.queue.end({ code: 1011, reason: error.message });
952
+ this.sockets.clear();
690
953
  }
691
954
  ask(call) {
692
955
  if (this.failure) return Promise.reject(this.failure);
@@ -765,6 +1028,46 @@ var RemoteTransport = class {
765
1028
  }
766
1029
  };
767
1030
  }
1031
+ async websocket(request) {
1032
+ const nid = ++this.nextId;
1033
+ const { signal } = request;
1034
+ signal.throwIfAborted();
1035
+ const abort = () => this.cancel(nid);
1036
+ signal.addEventListener("abort", abort, { once: true });
1037
+ let opened;
1038
+ try {
1039
+ opened = await this.ask({
1040
+ net: "ws-open",
1041
+ nid,
1042
+ url: request.url,
1043
+ protocols: request.protocols,
1044
+ headers: request.headers
1045
+ });
1046
+ } finally {
1047
+ signal.removeEventListener("abort", abort);
1048
+ }
1049
+ const close = (code, reason) => this.port.postMessage({ net: "ws-close", nid, code, reason });
1050
+ const queue = new MessageQueue(
1051
+ () => close(1e3, "a message is too big"),
1052
+ Number.POSITIVE_INFINITY,
1053
+ (n) => this.port.postMessage({ net: "ws-consumed", nid, n })
1054
+ );
1055
+ let unacked = 0;
1056
+ this.sockets.set(nid, { queue, acked: (n) => void (unacked -= n) });
1057
+ return {
1058
+ protocol: opened.protocol,
1059
+ get buffered() {
1060
+ return unacked;
1061
+ },
1062
+ send: (data) => {
1063
+ unacked += payloadSize(data);
1064
+ this.port.postMessage({ net: "ws-send", nid, data });
1065
+ },
1066
+ messages: queue,
1067
+ close,
1068
+ closed: queue.closed
1069
+ };
1070
+ }
768
1071
  };
769
1072
 
770
1073
  // src/client/protocol.ts