@adhdev/mesh-shared 1.0.56-rc.9 → 1.0.57-rc.1

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.mjs CHANGED
@@ -729,13 +729,197 @@ function stripStatusProbeMarker(args) {
729
729
  const { [STATUS_PROBE_ARG_KEY]: _drop, ...rest } = args;
730
730
  return rest;
731
731
  }
732
+
733
+ // src/rpc-chunking.ts
734
+ var MESH_MAX_INLINE_FRAME_BYTES = 6e4;
735
+ var MESH_CHUNK_PAYLOAD_CHARS = 16e3;
736
+ var MESH_MAX_CHUNKS = 1024;
737
+ var MESH_MAX_REASSEMBLED_BYTES = 16e6;
738
+ var MESH_CHUNK_TTL_MS = 6e4;
739
+ var MESH_CHUNK_KIND = "rpc_chunk";
740
+ function meshUtf8ByteLength(value) {
741
+ if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(value).byteLength;
742
+ let bytes = 0;
743
+ for (let i = 0; i < value.length; i += 1) {
744
+ const code = value.charCodeAt(i);
745
+ if (code < 128) bytes += 1;
746
+ else if (code < 2048) bytes += 2;
747
+ else if (code >= 55296 && code <= 56319) {
748
+ bytes += 4;
749
+ i += 1;
750
+ } else bytes += 3;
751
+ }
752
+ return bytes;
753
+ }
754
+ function measureWorstCaseChunkEnvelopeBytes() {
755
+ const densest = "\uD55C".repeat(MESH_CHUNK_PAYLOAD_CHARS);
756
+ return meshUtf8ByteLength(JSON.stringify(
757
+ buildChunkEnvelope(Number.MAX_SAFE_INTEGER, "x".repeat(64), MESH_MAX_CHUNKS, MESH_MAX_CHUNKS, densest)
758
+ ));
759
+ }
760
+ function meshFrameNeedsChunking(json) {
761
+ return meshUtf8ByteLength(json) > MESH_MAX_INLINE_FRAME_BYTES;
762
+ }
763
+ function buildChunkEnvelope(version, chunkId, index, total, data) {
764
+ return { v: version, kind: MESH_CHUNK_KIND, chunkId, index, total, data };
765
+ }
766
+ function splitMeshFrame(json, chunkId, version) {
767
+ const slices = [];
768
+ let offset = 0;
769
+ while (offset < json.length) {
770
+ let end = Math.min(json.length, offset + MESH_CHUNK_PAYLOAD_CHARS);
771
+ while (end > offset) {
772
+ const candidate = json.slice(offset, end);
773
+ const probe = JSON.stringify(buildChunkEnvelope(version, chunkId, slices.length, MESH_MAX_CHUNKS, candidate));
774
+ if (meshUtf8ByteLength(probe) <= MESH_MAX_INLINE_FRAME_BYTES) break;
775
+ const shrunk = Math.max(1, Math.floor((end - offset) * 0.8));
776
+ if (offset + shrunk >= end) {
777
+ end -= 1;
778
+ continue;
779
+ }
780
+ end = offset + shrunk;
781
+ }
782
+ if (end <= offset) {
783
+ return { ok: false, reason: "chunk_too_large", detail: "a single character did not fit the chunk envelope budget" };
784
+ }
785
+ slices.push(json.slice(offset, end));
786
+ if (slices.length > MESH_MAX_CHUNKS) {
787
+ return {
788
+ ok: false,
789
+ reason: "too_many_chunks",
790
+ detail: `frame needs more than ${MESH_MAX_CHUNKS} chunks (${meshUtf8ByteLength(json)} bytes)`
791
+ };
792
+ }
793
+ offset = end;
794
+ }
795
+ if (slices.length === 0) {
796
+ return { ok: false, reason: "chunk_too_large", detail: "refusing to chunk an empty frame" };
797
+ }
798
+ const total = slices.length;
799
+ return { ok: true, chunks: slices.map((data, index) => buildChunkEnvelope(version, chunkId, index, total, data)) };
800
+ }
801
+ var MeshChunkAssembler = class {
802
+ constructor(now = () => Date.now()) {
803
+ this.now = now;
804
+ }
805
+ buffers = /* @__PURE__ */ new Map();
806
+ /** True when the frame is a chunk envelope this assembler should handle. */
807
+ static isChunkFrame(frame) {
808
+ return !!frame && typeof frame === "object" && frame.kind === MESH_CHUNK_KIND;
809
+ }
810
+ /** Drop partials older than the TTL so a dead sender cannot pin memory. */
811
+ sweep() {
812
+ const now = this.now();
813
+ for (const [key, entry] of Array.from(this.buffers.entries())) {
814
+ if (now - entry.createdAt > MESH_CHUNK_TTL_MS) this.buffers.delete(key);
815
+ }
816
+ }
817
+ /** Discard any partial state for a peer (call on disconnect). */
818
+ reset() {
819
+ this.buffers.clear();
820
+ }
821
+ /** Number of frames currently mid-reassembly — for tests and diagnostics. */
822
+ get pendingCount() {
823
+ return this.buffers.size;
824
+ }
825
+ /**
826
+ * Accept one chunk envelope.
827
+ *
828
+ * Never throws and never returns a partially-applied frame: the result is exactly one
829
+ * of partial / complete / failed, and `failed` carries a typed reason the transport
830
+ * turns into an explicit RPC error.
831
+ */
832
+ accept(frame) {
833
+ this.sweep();
834
+ const raw = frame;
835
+ const chunkId = typeof raw?.chunkId === "string" ? raw.chunkId : "";
836
+ const index = Number(raw?.index);
837
+ const total = Number(raw?.total);
838
+ const data = typeof raw?.data === "string" ? raw.data : "";
839
+ if (!chunkId || !Number.isInteger(index) || !Number.isInteger(total) || index < 0 || total <= 0 || index >= total || !data) {
840
+ return {
841
+ status: "failed",
842
+ reason: "malformed_chunk",
843
+ detail: `malformed chunk envelope (chunkId=${chunkId || "-"} index=${raw?.index} total=${raw?.total})`,
844
+ chunkId
845
+ };
846
+ }
847
+ if (total > MESH_MAX_CHUNKS) {
848
+ this.buffers.delete(chunkId);
849
+ return {
850
+ status: "failed",
851
+ reason: "too_many_chunks",
852
+ detail: `chunk total ${total} exceeds the ${MESH_MAX_CHUNKS} cap`,
853
+ chunkId
854
+ };
855
+ }
856
+ let entry = this.buffers.get(chunkId);
857
+ if (!entry) {
858
+ entry = { total, chunks: new Array(total).fill(""), received: 0, bytesReceived: 0, createdAt: this.now() };
859
+ this.buffers.set(chunkId, entry);
860
+ } else if (entry.total !== total) {
861
+ this.buffers.delete(chunkId);
862
+ return {
863
+ status: "failed",
864
+ reason: "inconsistent_total",
865
+ detail: `chunk ${index} declares total ${total} but the group was opened with ${entry.total}`,
866
+ chunkId
867
+ };
868
+ }
869
+ const existing = entry.chunks[index];
870
+ if (existing) {
871
+ if (existing === data) return { status: "partial", received: entry.received, total: entry.total };
872
+ this.buffers.delete(chunkId);
873
+ return {
874
+ status: "failed",
875
+ reason: "duplicate_chunk_mismatch",
876
+ detail: `chunk ${index} arrived twice with different content`,
877
+ chunkId
878
+ };
879
+ }
880
+ const chunkBytes = meshUtf8ByteLength(data);
881
+ if (entry.bytesReceived + chunkBytes > MESH_MAX_REASSEMBLED_BYTES) {
882
+ this.buffers.delete(chunkId);
883
+ return {
884
+ status: "failed",
885
+ reason: "budget_exceeded",
886
+ detail: `reassembled frame would exceed ${MESH_MAX_REASSEMBLED_BYTES} bytes`,
887
+ chunkId
888
+ };
889
+ }
890
+ entry.chunks[index] = data;
891
+ entry.received += 1;
892
+ entry.bytesReceived += chunkBytes;
893
+ if (entry.received < entry.total) {
894
+ return { status: "partial", received: entry.received, total: entry.total };
895
+ }
896
+ this.buffers.delete(chunkId);
897
+ try {
898
+ return { status: "complete", frame: JSON.parse(entry.chunks.join("")) };
899
+ } catch (error) {
900
+ return {
901
+ status: "failed",
902
+ reason: "reassembled_parse_failed",
903
+ detail: `reassembled frame is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
904
+ chunkId
905
+ };
906
+ }
907
+ }
908
+ };
732
909
  export {
733
910
  CANONICAL_MESH_TOOL_COUNT,
734
911
  CANONICAL_MESH_TOOL_NAMES,
735
912
  CLI_SLOT_RECIPES,
736
913
  DEFAULT_DIFFICULTY_BRAINS,
737
914
  MAGI_RAW_ANSWER_CAP,
915
+ MESH_CHUNK_KIND,
916
+ MESH_CHUNK_PAYLOAD_CHARS,
917
+ MESH_CHUNK_TTL_MS,
918
+ MESH_MAX_CHUNKS,
919
+ MESH_MAX_INLINE_FRAME_BYTES,
920
+ MESH_MAX_REASSEMBLED_BYTES,
738
921
  MESH_TASK_DIFFICULTIES,
922
+ MeshChunkAssembler,
739
923
  QUOTA_SUPPORTED_PROVIDERS,
740
924
  STATUS_PROBE_ARG_KEY,
741
925
  UNKNOWN_CLI_SLOT_RECIPE,
@@ -756,7 +940,10 @@ export {
756
940
  isRawDaemonDoId,
757
941
  joinRepoPath,
758
942
  machineCoreFromDaemonId,
943
+ measureWorstCaseChunkEnvelopeBytes,
944
+ meshFrameNeedsChunking,
759
945
  meshNodeIdMatches,
946
+ meshUtf8ByteLength,
760
947
  meshWorkspacesEquivalent,
761
948
  normalizeBrainSlot,
762
949
  normalizeDifficultyBrainMap,
@@ -779,6 +966,7 @@ export {
779
966
  scoreGitStatusCandidate,
780
967
  scoreGitUpstreamFreshness,
781
968
  sessionIdsEquivalent,
969
+ splitMeshFrame,
782
970
  stripStatusProbeMarker,
783
971
  summarizeGitShape,
784
972
  supportsQuota,