@superatomai/sdk-node 0.0.94-dsp → 0.0.96-dsp

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.d.mts CHANGED
@@ -1463,10 +1463,12 @@ declare const MessageSchema: z.ZodObject<{
1463
1463
  from: z.ZodObject<{
1464
1464
  id: z.ZodOptional<z.ZodString>;
1465
1465
  type: z.ZodOptional<z.ZodString>;
1466
+ parcels: z.ZodOptional<z.ZodBoolean>;
1466
1467
  }, z.core.$strip>;
1467
1468
  to: z.ZodOptional<z.ZodObject<{
1468
1469
  id: z.ZodOptional<z.ZodString>;
1469
1470
  type: z.ZodOptional<z.ZodString>;
1471
+ parcels: z.ZodOptional<z.ZodBoolean>;
1470
1472
  }, z.core.$strip>>;
1471
1473
  payload: z.ZodUnknown;
1472
1474
  }, z.core.$strip>;
@@ -1477,10 +1479,12 @@ declare const IncomingMessageSchema: z.ZodObject<{
1477
1479
  from: z.ZodObject<{
1478
1480
  id: z.ZodOptional<z.ZodString>;
1479
1481
  type: z.ZodOptional<z.ZodString>;
1482
+ parcels: z.ZodOptional<z.ZodBoolean>;
1480
1483
  }, z.core.$strip>;
1481
1484
  to: z.ZodOptional<z.ZodObject<{
1482
1485
  id: z.ZodOptional<z.ZodString>;
1483
1486
  type: z.ZodOptional<z.ZodString>;
1487
+ parcels: z.ZodOptional<z.ZodBoolean>;
1484
1488
  }, z.core.$strip>>;
1485
1489
  payload: z.ZodUnknown;
1486
1490
  }, z.core.$strip>;
@@ -4836,6 +4840,8 @@ interface ParcelRef {
4836
4840
  interface ParcelFileRef extends ParcelRef {
4837
4841
  name?: string;
4838
4842
  contentType: string;
4843
+ /** The file's bytes as base64, when storage was down and they travel in the message instead. */
4844
+ data?: string;
4839
4845
  }
4840
4846
  interface ParcelClientOptions {
4841
4847
  apiUrl: string;
@@ -4854,6 +4860,8 @@ declare class ParcelClient {
4854
4860
  constructor(opts: ParcelClientOptions);
4855
4861
  private url;
4856
4862
  private call;
4863
+ /** The receipt the bytes would get, without storing them. */
4864
+ refFor(bytes: Uint8Array, ext: string): ParcelRef;
4857
4865
  /** Stores the bytes and returns the receipt for them. */
4858
4866
  upload(bytes: Uint8Array, ext?: string, contentType?: string): Promise<ParcelRef>;
4859
4867
  /** The bytes behind a receipt, checked against its hash. */
@@ -4872,7 +4880,11 @@ declare class ParcelFile {
4872
4880
  readonly size: number;
4873
4881
  readonly contentType: string;
4874
4882
  constructor(ref: ParcelFileRef, client: ParcelClient | null);
4875
- /** The file's bytes, checked against their hash. Throws if they cannot be fetched. */
4883
+ /**
4884
+ * The file's bytes, checked against their hash — from the message itself when
4885
+ * it carried them (storage was down), otherwise from storage. Throws if they
4886
+ * cannot be fetched.
4887
+ */
4876
4888
  download(): Promise<Uint8Array>;
4877
4889
  toJSON(): {
4878
4890
  __parcelFile: ParcelFileRef;
@@ -6061,6 +6073,8 @@ declare class SuperatomSDK {
6061
6073
  private parcelClient;
6062
6074
  private outgoing;
6063
6075
  private incoming;
6076
+ /** The client sockets that said they read parcels; only they get large JSON as parcels or parts. */
6077
+ private parcelReaders;
6064
6078
  constructor(config: SuperatomSDKConfig);
6065
6079
  /**
6066
6080
  * Initialize PromptLoader and load prompts into memory
package/dist/index.d.ts CHANGED
@@ -1463,10 +1463,12 @@ declare const MessageSchema: z.ZodObject<{
1463
1463
  from: z.ZodObject<{
1464
1464
  id: z.ZodOptional<z.ZodString>;
1465
1465
  type: z.ZodOptional<z.ZodString>;
1466
+ parcels: z.ZodOptional<z.ZodBoolean>;
1466
1467
  }, z.core.$strip>;
1467
1468
  to: z.ZodOptional<z.ZodObject<{
1468
1469
  id: z.ZodOptional<z.ZodString>;
1469
1470
  type: z.ZodOptional<z.ZodString>;
1471
+ parcels: z.ZodOptional<z.ZodBoolean>;
1470
1472
  }, z.core.$strip>>;
1471
1473
  payload: z.ZodUnknown;
1472
1474
  }, z.core.$strip>;
@@ -1477,10 +1479,12 @@ declare const IncomingMessageSchema: z.ZodObject<{
1477
1479
  from: z.ZodObject<{
1478
1480
  id: z.ZodOptional<z.ZodString>;
1479
1481
  type: z.ZodOptional<z.ZodString>;
1482
+ parcels: z.ZodOptional<z.ZodBoolean>;
1480
1483
  }, z.core.$strip>;
1481
1484
  to: z.ZodOptional<z.ZodObject<{
1482
1485
  id: z.ZodOptional<z.ZodString>;
1483
1486
  type: z.ZodOptional<z.ZodString>;
1487
+ parcels: z.ZodOptional<z.ZodBoolean>;
1484
1488
  }, z.core.$strip>>;
1485
1489
  payload: z.ZodUnknown;
1486
1490
  }, z.core.$strip>;
@@ -4836,6 +4840,8 @@ interface ParcelRef {
4836
4840
  interface ParcelFileRef extends ParcelRef {
4837
4841
  name?: string;
4838
4842
  contentType: string;
4843
+ /** The file's bytes as base64, when storage was down and they travel in the message instead. */
4844
+ data?: string;
4839
4845
  }
4840
4846
  interface ParcelClientOptions {
4841
4847
  apiUrl: string;
@@ -4854,6 +4860,8 @@ declare class ParcelClient {
4854
4860
  constructor(opts: ParcelClientOptions);
4855
4861
  private url;
4856
4862
  private call;
4863
+ /** The receipt the bytes would get, without storing them. */
4864
+ refFor(bytes: Uint8Array, ext: string): ParcelRef;
4857
4865
  /** Stores the bytes and returns the receipt for them. */
4858
4866
  upload(bytes: Uint8Array, ext?: string, contentType?: string): Promise<ParcelRef>;
4859
4867
  /** The bytes behind a receipt, checked against its hash. */
@@ -4872,7 +4880,11 @@ declare class ParcelFile {
4872
4880
  readonly size: number;
4873
4881
  readonly contentType: string;
4874
4882
  constructor(ref: ParcelFileRef, client: ParcelClient | null);
4875
- /** The file's bytes, checked against their hash. Throws if they cannot be fetched. */
4883
+ /**
4884
+ * The file's bytes, checked against their hash — from the message itself when
4885
+ * it carried them (storage was down), otherwise from storage. Throws if they
4886
+ * cannot be fetched.
4887
+ */
4876
4888
  download(): Promise<Uint8Array>;
4877
4889
  toJSON(): {
4878
4890
  __parcelFile: ParcelFileRef;
@@ -6061,6 +6073,8 @@ declare class SuperatomSDK {
6061
6073
  private parcelClient;
6062
6074
  private outgoing;
6063
6075
  private incoming;
6076
+ /** The client sockets that said they read parcels; only they get large JSON as parcels or parts. */
6077
+ private parcelReaders;
6064
6078
  constructor(config: SuperatomSDKConfig);
6065
6079
  /**
6066
6080
  * Initialize PromptLoader and load prompts into memory
package/dist/index.js CHANGED
@@ -348,7 +348,9 @@ var UsersDataSchema = import_zod3.z.object({
348
348
  });
349
349
  var MessageParticipantSchema = import_zod3.z.object({
350
350
  id: import_zod3.z.string().optional(),
351
- type: import_zod3.z.string().optional()
351
+ type: import_zod3.z.string().optional(),
352
+ /** On `from`: the sending client reads parcels and parts (see parcels/ParcelReaders). */
353
+ parcels: import_zod3.z.boolean().optional()
352
354
  });
353
355
  var MessageSchema = import_zod3.z.object({
354
356
  id: import_zod3.z.string().optional(),
@@ -28370,12 +28372,19 @@ var import_crypto7 = require("crypto");
28370
28372
  var PARCEL_THRESHOLD_BYTES = 256 * 1024;
28371
28373
  var MAX_INLINE_BYTES = 900 * 1024;
28372
28374
  var MAX_PARCEL_BYTES = 95 * 1024 * 1024;
28375
+ var PART_BYTES = 600 * 1024;
28376
+ var PART_WAIT_MS = 6e4;
28373
28377
  var NEVER_PARCEL = /* @__PURE__ */ new Set(["PING", "PONG", "BUNDLE_CHUNK", "BUNDLE_CSS_CHUNK"]);
28374
28378
  var RETRY_DELAYS_MS = [500, 1500];
28375
28379
  var REQUEST_TIMEOUT_MS = 6e4;
28376
28380
  var MAX_DEPTH = 32;
28377
28381
  var PARCEL_FIELD = "__parcel";
28378
28382
  var FILE_FIELD = "__parcelFile";
28383
+ var PART_FIELD = "__parcelPart";
28384
+ var partOf = (payload) => {
28385
+ const p = payload?.[PART_FIELD];
28386
+ return p && typeof p.id === "string" && typeof p.data === "string" && Number.isInteger(p.total) && Number.isInteger(p.index) && p.total > 0 && p.index >= 0 && p.index < p.total ? p : null;
28387
+ };
28379
28388
  var isRef = (ref) => !!ref && typeof ref === "object" && typeof ref.projectId === "string" && typeof ref.hash === "string" && typeof ref.ext === "string" && typeof ref.size === "number";
28380
28389
  function parcelRefOf(payload) {
28381
28390
  const ref = payload?.[PARCEL_FIELD];
@@ -28430,12 +28439,16 @@ var ParcelClient = class {
28430
28439
  }
28431
28440
  return res;
28432
28441
  }
28442
+ /** The receipt the bytes would get, without storing them. */
28443
+ refFor(bytes, ext) {
28444
+ return { projectId: this.opts.projectId, hash: sha256Hex(bytes), ext, size: bytes.byteLength };
28445
+ }
28433
28446
  /** Stores the bytes and returns the receipt for them. */
28434
28447
  async upload(bytes, ext = "json", contentType = "application/json") {
28435
28448
  if (bytes.byteLength > MAX_PARCEL_BYTES) {
28436
28449
  throw new Error(`Parcel too large (${bytes.byteLength} bytes, limit ${MAX_PARCEL_BYTES})`);
28437
28450
  }
28438
- const ref = { projectId: this.opts.projectId, hash: sha256Hex(bytes), ext, size: bytes.byteLength };
28451
+ const ref = this.refFor(bytes, ext);
28439
28452
  await withRetries(
28440
28453
  "upload",
28441
28454
  () => this.call(this.url(ref), { method: "PUT", headers: { "Content-Type": contentType }, body: bytes })
@@ -28462,8 +28475,17 @@ var ParcelFile = class {
28462
28475
  this.size = ref.size;
28463
28476
  this.contentType = ref.contentType;
28464
28477
  }
28465
- /** The file's bytes, checked against their hash. Throws if they cannot be fetched. */
28478
+ /**
28479
+ * The file's bytes, checked against their hash — from the message itself when
28480
+ * it carried them (storage was down), otherwise from storage. Throws if they
28481
+ * cannot be fetched.
28482
+ */
28466
28483
  async download() {
28484
+ if (typeof this.ref.data === "string") {
28485
+ const bytes = new Uint8Array(Buffer.from(this.ref.data, "base64"));
28486
+ if (sha256Hex(bytes) !== this.ref.hash) throw new Error("File content does not match its hash");
28487
+ return bytes;
28488
+ }
28467
28489
  if (!this.client) throw new Error("Cannot download files: SA_API_URL or the API key is not set");
28468
28490
  return this.client.download(this.ref);
28469
28491
  }
@@ -28492,28 +28514,55 @@ function hasFileRef(value, depth = 0) {
28492
28514
  return false;
28493
28515
  }
28494
28516
  var extOf = (name) => typeof name === "string" && /\.([a-z0-9]{1,10})$/i.exec(name)?.[1]?.toLowerCase() || "bin";
28495
- async function uploadFiles(value, client, parent, depth = 0) {
28517
+ async function uploadFiles(value, client, storage, parent, depth = 0) {
28496
28518
  if (isBinary(value)) {
28497
28519
  const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
28498
28520
  const name = parent?.name ?? parent?.fileName;
28499
28521
  const contentType = String(parent?.contentType ?? parent?.mimeType ?? "application/octet-stream");
28500
- const ref = await client.upload(bytes, extOf(name), contentType);
28501
- const fileRef = { ...ref, contentType, ...typeof name === "string" ? { name } : {} };
28522
+ const named = typeof name === "string" ? { name } : {};
28523
+ if (!storage.down) {
28524
+ try {
28525
+ const ref = await client.upload(bytes, extOf(name), contentType);
28526
+ const fileRef2 = { ...ref, contentType, ...named };
28527
+ return { [FILE_FIELD]: fileRef2 };
28528
+ } catch (err) {
28529
+ if (bytes.byteLength > MAX_PARCEL_BYTES) throw err;
28530
+ storage.down = true;
28531
+ logger.warn(`[parcels] file upload failed, sending it in the message instead: ${err?.message ?? err}`);
28532
+ }
28533
+ }
28534
+ const fileRef = {
28535
+ ...client.refFor(bytes, extOf(name)),
28536
+ contentType,
28537
+ ...named,
28538
+ data: Buffer.from(bytes).toString("base64")
28539
+ };
28502
28540
  return { [FILE_FIELD]: fileRef };
28503
28541
  }
28504
28542
  if (depth >= MAX_DEPTH) return value;
28505
28543
  if (Array.isArray(value)) {
28506
28544
  const out = [];
28507
- for (const v of value) out.push(await uploadFiles(v, client, void 0, depth + 1));
28545
+ for (const v of value) out.push(await uploadFiles(v, client, storage, void 0, depth + 1));
28508
28546
  return out;
28509
28547
  }
28510
28548
  if (isPlain(value)) {
28511
28549
  const out = {};
28512
- for (const [k, v] of Object.entries(value)) out[k] = await uploadFiles(v, client, value, depth + 1);
28550
+ for (const [k, v] of Object.entries(value)) out[k] = await uploadFiles(v, client, storage, value, depth + 1);
28513
28551
  return out;
28514
28552
  }
28515
28553
  return value;
28516
28554
  }
28555
+ function splitPayload(payload) {
28556
+ const bytes = Buffer.from(JSON.stringify(payload ?? null), "utf8");
28557
+ const total = Math.max(1, Math.ceil(bytes.byteLength / PART_BYTES));
28558
+ const id = (0, import_crypto7.randomUUID)();
28559
+ return Array.from({ length: total }, (_, index) => ({
28560
+ id,
28561
+ index,
28562
+ total,
28563
+ data: bytes.subarray(index * PART_BYTES, (index + 1) * PART_BYTES).toString("base64")
28564
+ }));
28565
+ }
28517
28566
  function toFileHandles(value, client, depth = 0) {
28518
28567
  const ref = fileRefOf(value);
28519
28568
  if (ref) return new ParcelFile(ref, client);
@@ -28534,6 +28583,26 @@ var recipientOf = (message) => {
28534
28583
  const id = message.to?.id;
28535
28584
  return typeof id === "string" ? id : "";
28536
28585
  };
28586
+ var MAX_READERS = 1e4;
28587
+ var ParcelReaders = class {
28588
+ constructor(max = MAX_READERS) {
28589
+ this.max = max;
28590
+ this.ids = /* @__PURE__ */ new Set();
28591
+ }
28592
+ /** Notes what a received message says about its sender. */
28593
+ noteSender(message) {
28594
+ const from = message?.from;
28595
+ if (typeof from?.id !== "string" || !from.id) return;
28596
+ this.ids.delete(from.id);
28597
+ if (from.parcels !== true) return;
28598
+ this.ids.add(from.id);
28599
+ if (this.ids.size > this.max) this.ids.delete(this.ids.values().next().value);
28600
+ }
28601
+ /** Whether the message is addressed to a socket that said it reads parcels. */
28602
+ reads(message) {
28603
+ return this.ids.has(recipientOf(message));
28604
+ }
28605
+ };
28537
28606
  var InOrder = class {
28538
28607
  /** `onIdle` runs each time the last waiting task finishes. */
28539
28608
  constructor(onIdle) {
@@ -28555,15 +28624,19 @@ var InOrder = class {
28555
28624
  }
28556
28625
  };
28557
28626
  var OutgoingParcels = class {
28558
- constructor(client, jsonParcels, deliver, threshold = PARCEL_THRESHOLD_BYTES) {
28627
+ constructor(client, jsonParcels, deliver, readers = null, threshold = PARCEL_THRESHOLD_BYTES) {
28559
28628
  this.client = client;
28560
28629
  this.jsonParcels = jsonParcels;
28561
28630
  this.deliver = deliver;
28631
+ this.readers = readers;
28562
28632
  this.threshold = threshold;
28563
28633
  /** Only recipients with something waiting; a queue is dropped once it empties. */
28564
28634
  this.queues = /* @__PURE__ */ new Map();
28565
28635
  this.warnedNoClient = false;
28566
28636
  }
28637
+ readsParcels(message) {
28638
+ return this.readers ? this.readers.reads(message) : true;
28639
+ }
28567
28640
  /**
28568
28641
  * Same contract as the SDK's `send`: never throws, returns whether the message
28569
28642
  * went out (or, for a queued one, was accepted).
@@ -28583,7 +28656,7 @@ var OutgoingParcels = class {
28583
28656
  files = false;
28584
28657
  }
28585
28658
  if (!files) {
28586
- if (!this.jsonParcels || !this.client) return this.now(message);
28659
+ if (!this.jsonParcels || !this.client || !this.readsParcels(message)) return this.now(message);
28587
28660
  let wire;
28588
28661
  try {
28589
28662
  wire = JSON.stringify(message);
@@ -28593,8 +28666,7 @@ var OutgoingParcels = class {
28593
28666
  if (Buffer.byteLength(wire, "utf8") < this.threshold) return this.now(message, wire);
28594
28667
  }
28595
28668
  this.queueFor(message).run(async () => {
28596
- const out = await this.prepare(message, files);
28597
- this.deliver(out.message, out.wire);
28669
+ for (const out of await this.prepare(message, files)) this.deliver(out.message, out.wire);
28598
28670
  });
28599
28671
  return true;
28600
28672
  }
@@ -28619,44 +28691,66 @@ var OutgoingParcels = class {
28619
28691
  async idle() {
28620
28692
  await Promise.all(Array.from(this.queues.values(), (queue) => queue.idle()));
28621
28693
  }
28694
+ /**
28695
+ * What actually goes on the socket for this message: usually one message (as
28696
+ * is, or as a parcel receipt); several parts when storage is down and the
28697
+ * message is too large for the relay.
28698
+ */
28622
28699
  async prepare(message, files) {
28623
28700
  let current = message;
28701
+ const storage = { down: false };
28624
28702
  if (files) {
28625
- const started2 = Date.now();
28703
+ const started = Date.now();
28626
28704
  try {
28627
- current = { ...message, payload: await uploadFiles(message.payload, this.client) };
28628
- logger.info(`[parcels] ${message.type} ${message.id ?? ""}: file(s) uploaded (${Date.now() - started2}ms)`);
28705
+ current = { ...message, payload: await uploadFiles(message.payload, this.client, storage) };
28706
+ if (!storage.down) logger.info(`[parcels] ${message.type} ${message.id ?? ""}: file(s) uploaded (${Date.now() - started}ms)`);
28629
28707
  } catch (err) {
28630
- const reason = err?.message ?? String(err);
28631
- logger.error(`[parcels] file upload failed for ${message.type}, sending an error instead: ${reason}`);
28632
- return { message: asError(message, `Could not send a file: ${reason}`) };
28708
+ const reason2 = err?.message ?? String(err);
28709
+ logger.error(`[parcels] file upload failed for ${message.type}, sending an error instead: ${reason2}`);
28710
+ return [{ message: asError(message, `Could not send a file: ${reason2}`) }];
28633
28711
  }
28634
28712
  }
28635
28713
  let wire;
28636
28714
  try {
28637
28715
  wire = JSON.stringify(current);
28638
28716
  } catch {
28639
- return { message: current };
28717
+ return [{ message: current }];
28640
28718
  }
28641
28719
  const size = Buffer.byteLength(wire, "utf8");
28642
- if (!this.jsonParcels || size < this.threshold) return { message: current, wire };
28643
- const started = Date.now();
28644
- try {
28645
- const bytes = Buffer.from(JSON.stringify(current.payload ?? null), "utf8");
28646
- const ref = await this.client.upload(bytes);
28647
- logger.info(
28648
- `[parcels] ${message.type} ${message.id ?? ""} \u2192 parcel ${ref.hash.slice(0, 12)} (${ref.size} bytes, ${Date.now() - started}ms)`
28649
- );
28650
- return { message: { ...current, payload: { [PARCEL_FIELD]: ref } } };
28651
- } catch (err) {
28652
- const reason = err?.message ?? String(err);
28653
- if (size <= MAX_INLINE_BYTES) {
28654
- logger.warn(`[parcels] upload failed for ${message.type}, sending inline (${size} bytes): ${reason}`);
28655
- return { message: current, wire };
28720
+ if (size < this.threshold) return [{ message: current, wire }];
28721
+ if (!this.readsParcels(message)) {
28722
+ if (size > MAX_INLINE_BYTES) {
28723
+ logger.warn(`[parcels] ${message.type} (${size} bytes) is for a client that doesn't read parcels \u2014 sent inline; the relay may refuse it`);
28724
+ }
28725
+ return [{ message: current, wire }];
28726
+ }
28727
+ let reason = "large-JSON parcels are off";
28728
+ if (this.jsonParcels && !storage.down) {
28729
+ const started = Date.now();
28730
+ try {
28731
+ const bytes = Buffer.from(JSON.stringify(current.payload ?? null), "utf8");
28732
+ const ref = await this.client.upload(bytes);
28733
+ logger.info(
28734
+ `[parcels] ${message.type} ${message.id ?? ""} \u2192 parcel ${ref.hash.slice(0, 12)} (${ref.size} bytes, ${Date.now() - started}ms)`
28735
+ );
28736
+ return [{ message: { ...current, payload: { [PARCEL_FIELD]: ref } } }];
28737
+ } catch (err) {
28738
+ reason = err?.message ?? String(err);
28656
28739
  }
28657
- logger.error(`[parcels] upload failed for ${message.type} (${size} bytes), sending an error instead: ${reason}`);
28658
- return { message: asError(message, `The message was too large to deliver (${size} bytes): ${reason}`) };
28740
+ } else if (storage.down) {
28741
+ reason = "storage is down";
28742
+ }
28743
+ if (size <= MAX_INLINE_BYTES) {
28744
+ if (this.jsonParcels) logger.warn(`[parcels] ${message.type} not stored (${reason}), sending inline (${size} bytes)`);
28745
+ return [{ message: current, wire }];
28746
+ }
28747
+ if (size > MAX_PARCEL_BYTES) {
28748
+ logger.error(`[parcels] ${message.type} (${size} bytes) is too large to send`);
28749
+ return [{ message: asError(message, `The message was too large to deliver (${size} bytes): ${reason}`) }];
28659
28750
  }
28751
+ const parts = splitPayload(current.payload);
28752
+ logger.warn(`[parcels] ${message.type} not stored (${reason}), sending in ${parts.length} parts (${size} bytes)`);
28753
+ return parts.map((part) => ({ message: { ...current, payload: { [PART_FIELD]: part } } }));
28660
28754
  }
28661
28755
  };
28662
28756
  var IncomingParcels = class {
@@ -28664,12 +28758,22 @@ var IncomingParcels = class {
28664
28758
  this.client = client;
28665
28759
  this.dispatch = dispatch;
28666
28760
  this.queue = new InOrder();
28761
+ /** Split messages still waiting for parts, by part id. */
28762
+ this.partial = /* @__PURE__ */ new Map();
28763
+ /** Split messages already rebuilt, so a second copy of their parts is ignored. */
28764
+ this.rebuilt = /* @__PURE__ */ new Set();
28667
28765
  }
28668
28766
  /**
28669
28767
  * `raw` is the message's text when it is at hand: a message that never
28670
28768
  * mentions a file placeholder is not searched for one.
28671
28769
  */
28672
28770
  receive(message, raw) {
28771
+ const part = partOf(message?.payload);
28772
+ if (part) {
28773
+ const whole = this.collect(message, part);
28774
+ if (whole) this.receive(whole);
28775
+ return;
28776
+ }
28673
28777
  const ref = parcelRefOf(message?.payload);
28674
28778
  if (!ref) {
28675
28779
  const withFiles = raw === void 0 || raw.includes(FILE_FIELD) ? this.withFiles(message) : message;
@@ -28685,6 +28789,38 @@ var IncomingParcels = class {
28685
28789
  idle() {
28686
28790
  return this.queue.idle();
28687
28791
  }
28792
+ /**
28793
+ * Holds one part; once all of them are here, the original message (envelope
28794
+ * from the last part, payload rebuilt). Null while parts are still missing.
28795
+ */
28796
+ collect(message, part) {
28797
+ if (this.rebuilt.has(part.id)) return null;
28798
+ let entry = this.partial.get(part.id);
28799
+ if (!entry) {
28800
+ const timer = setTimeout(() => {
28801
+ this.partial.delete(part.id);
28802
+ logger.warn(`[parcels] ${message.type}: gave up waiting for the rest of a split message (${part.id})`);
28803
+ }, PART_WAIT_MS).unref();
28804
+ entry = { parts: new Array(part.total), got: 0, timer };
28805
+ this.partial.set(part.id, entry);
28806
+ }
28807
+ if (entry.parts.length !== part.total || entry.parts[part.index] !== void 0) return null;
28808
+ entry.parts[part.index] = part.data;
28809
+ if (++entry.got < part.total) return null;
28810
+ clearTimeout(entry.timer);
28811
+ this.partial.delete(part.id);
28812
+ this.rebuilt.add(part.id);
28813
+ setTimeout(() => this.rebuilt.delete(part.id), PART_WAIT_MS).unref();
28814
+ try {
28815
+ const bytes = Buffer.concat(entry.parts.map((data) => Buffer.from(data, "base64")));
28816
+ logger.info(`[parcels] ${message.type} ${message.id ?? ""} rebuilt from ${part.total} parts (${bytes.byteLength} bytes)`);
28817
+ return { ...message, payload: JSON.parse(bytes.toString("utf8")) };
28818
+ } catch (err) {
28819
+ const reason = err?.message ?? String(err);
28820
+ logger.error(`[parcels] could not rebuild a split ${message.type}: ${reason}`);
28821
+ return asError(message, `Could not rebuild the message: ${reason}`);
28822
+ }
28823
+ }
28688
28824
  withFiles(message) {
28689
28825
  try {
28690
28826
  return hasFileRef(message.payload) ? { ...message, payload: toFileHandles(message.payload, this.client) } : message;
@@ -30048,6 +30184,8 @@ var SuperatomSDK = class {
30048
30184
  // since an outage of unknown length shouldn't grow this without limit.
30049
30185
  this.pendingOutbox = [];
30050
30186
  this.MAX_OUTBOX_SIZE = 50;
30187
+ /** The client sockets that said they read parcels; only they get large JSON as parcels or parts. */
30188
+ this.parcelReaders = new ParcelReaders();
30051
30189
  /**
30052
30190
  * The dashboard contract, as registered by the browser bundle that holds it.
30053
30191
  *
@@ -30074,7 +30212,8 @@ var SuperatomSDK = class {
30074
30212
  this.outgoing = new OutgoingParcels(
30075
30213
  this.parcelClient,
30076
30214
  jsonParcels,
30077
- (message, wire) => this.deliver(message, wire)
30215
+ (message, wire) => this.deliver(message, wire),
30216
+ this.parcelReaders
30078
30217
  );
30079
30218
  this.incoming = new IncomingParcels(this.parcelClient, (parsed) => this.dispatchMessage(parsed));
30080
30219
  this.anthropicApiKey = config2.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY || "";
@@ -30241,6 +30380,7 @@ var SuperatomSDK = class {
30241
30380
  logger.error("Failed to parse incoming message:", error);
30242
30381
  return;
30243
30382
  }
30383
+ this.parcelReaders.noteSender(parsed);
30244
30384
  this.incoming.receive(parsed, data);
30245
30385
  }
30246
30386
  /** Routes one parsed message — everything `handleMessage` did after parsing. */
package/dist/index.mjs CHANGED
@@ -6,7 +6,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
6
6
  });
7
7
 
8
8
  // src/index.ts
9
- import { randomUUID as randomUUID3 } from "crypto";
9
+ import { randomUUID as randomUUID4 } from "crypto";
10
10
 
11
11
  // src/websocket.ts
12
12
  import WebSocket from "ws";
@@ -228,7 +228,9 @@ var UsersDataSchema = z3.object({
228
228
  });
229
229
  var MessageParticipantSchema = z3.object({
230
230
  id: z3.string().optional(),
231
- type: z3.string().optional()
231
+ type: z3.string().optional(),
232
+ /** On `from`: the sending client reads parcels and parts (see parcels/ParcelReaders). */
233
+ parcels: z3.boolean().optional()
232
234
  });
233
235
  var MessageSchema = z3.object({
234
236
  id: z3.string().optional(),
@@ -28246,16 +28248,23 @@ var ReportManager = class {
28246
28248
  };
28247
28249
 
28248
28250
  // src/parcels/index.ts
28249
- import { createHash as createHash3 } from "crypto";
28251
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
28250
28252
  var PARCEL_THRESHOLD_BYTES = 256 * 1024;
28251
28253
  var MAX_INLINE_BYTES = 900 * 1024;
28252
28254
  var MAX_PARCEL_BYTES = 95 * 1024 * 1024;
28255
+ var PART_BYTES = 600 * 1024;
28256
+ var PART_WAIT_MS = 6e4;
28253
28257
  var NEVER_PARCEL = /* @__PURE__ */ new Set(["PING", "PONG", "BUNDLE_CHUNK", "BUNDLE_CSS_CHUNK"]);
28254
28258
  var RETRY_DELAYS_MS = [500, 1500];
28255
28259
  var REQUEST_TIMEOUT_MS = 6e4;
28256
28260
  var MAX_DEPTH = 32;
28257
28261
  var PARCEL_FIELD = "__parcel";
28258
28262
  var FILE_FIELD = "__parcelFile";
28263
+ var PART_FIELD = "__parcelPart";
28264
+ var partOf = (payload) => {
28265
+ const p = payload?.[PART_FIELD];
28266
+ return p && typeof p.id === "string" && typeof p.data === "string" && Number.isInteger(p.total) && Number.isInteger(p.index) && p.total > 0 && p.index >= 0 && p.index < p.total ? p : null;
28267
+ };
28259
28268
  var isRef = (ref) => !!ref && typeof ref === "object" && typeof ref.projectId === "string" && typeof ref.hash === "string" && typeof ref.ext === "string" && typeof ref.size === "number";
28260
28269
  function parcelRefOf(payload) {
28261
28270
  const ref = payload?.[PARCEL_FIELD];
@@ -28310,12 +28319,16 @@ var ParcelClient = class {
28310
28319
  }
28311
28320
  return res;
28312
28321
  }
28322
+ /** The receipt the bytes would get, without storing them. */
28323
+ refFor(bytes, ext) {
28324
+ return { projectId: this.opts.projectId, hash: sha256Hex(bytes), ext, size: bytes.byteLength };
28325
+ }
28313
28326
  /** Stores the bytes and returns the receipt for them. */
28314
28327
  async upload(bytes, ext = "json", contentType = "application/json") {
28315
28328
  if (bytes.byteLength > MAX_PARCEL_BYTES) {
28316
28329
  throw new Error(`Parcel too large (${bytes.byteLength} bytes, limit ${MAX_PARCEL_BYTES})`);
28317
28330
  }
28318
- const ref = { projectId: this.opts.projectId, hash: sha256Hex(bytes), ext, size: bytes.byteLength };
28331
+ const ref = this.refFor(bytes, ext);
28319
28332
  await withRetries(
28320
28333
  "upload",
28321
28334
  () => this.call(this.url(ref), { method: "PUT", headers: { "Content-Type": contentType }, body: bytes })
@@ -28342,8 +28355,17 @@ var ParcelFile = class {
28342
28355
  this.size = ref.size;
28343
28356
  this.contentType = ref.contentType;
28344
28357
  }
28345
- /** The file's bytes, checked against their hash. Throws if they cannot be fetched. */
28358
+ /**
28359
+ * The file's bytes, checked against their hash — from the message itself when
28360
+ * it carried them (storage was down), otherwise from storage. Throws if they
28361
+ * cannot be fetched.
28362
+ */
28346
28363
  async download() {
28364
+ if (typeof this.ref.data === "string") {
28365
+ const bytes = new Uint8Array(Buffer.from(this.ref.data, "base64"));
28366
+ if (sha256Hex(bytes) !== this.ref.hash) throw new Error("File content does not match its hash");
28367
+ return bytes;
28368
+ }
28347
28369
  if (!this.client) throw new Error("Cannot download files: SA_API_URL or the API key is not set");
28348
28370
  return this.client.download(this.ref);
28349
28371
  }
@@ -28372,28 +28394,55 @@ function hasFileRef(value, depth = 0) {
28372
28394
  return false;
28373
28395
  }
28374
28396
  var extOf = (name) => typeof name === "string" && /\.([a-z0-9]{1,10})$/i.exec(name)?.[1]?.toLowerCase() || "bin";
28375
- async function uploadFiles(value, client, parent, depth = 0) {
28397
+ async function uploadFiles(value, client, storage, parent, depth = 0) {
28376
28398
  if (isBinary(value)) {
28377
28399
  const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
28378
28400
  const name = parent?.name ?? parent?.fileName;
28379
28401
  const contentType = String(parent?.contentType ?? parent?.mimeType ?? "application/octet-stream");
28380
- const ref = await client.upload(bytes, extOf(name), contentType);
28381
- const fileRef = { ...ref, contentType, ...typeof name === "string" ? { name } : {} };
28402
+ const named = typeof name === "string" ? { name } : {};
28403
+ if (!storage.down) {
28404
+ try {
28405
+ const ref = await client.upload(bytes, extOf(name), contentType);
28406
+ const fileRef2 = { ...ref, contentType, ...named };
28407
+ return { [FILE_FIELD]: fileRef2 };
28408
+ } catch (err) {
28409
+ if (bytes.byteLength > MAX_PARCEL_BYTES) throw err;
28410
+ storage.down = true;
28411
+ logger.warn(`[parcels] file upload failed, sending it in the message instead: ${err?.message ?? err}`);
28412
+ }
28413
+ }
28414
+ const fileRef = {
28415
+ ...client.refFor(bytes, extOf(name)),
28416
+ contentType,
28417
+ ...named,
28418
+ data: Buffer.from(bytes).toString("base64")
28419
+ };
28382
28420
  return { [FILE_FIELD]: fileRef };
28383
28421
  }
28384
28422
  if (depth >= MAX_DEPTH) return value;
28385
28423
  if (Array.isArray(value)) {
28386
28424
  const out = [];
28387
- for (const v of value) out.push(await uploadFiles(v, client, void 0, depth + 1));
28425
+ for (const v of value) out.push(await uploadFiles(v, client, storage, void 0, depth + 1));
28388
28426
  return out;
28389
28427
  }
28390
28428
  if (isPlain(value)) {
28391
28429
  const out = {};
28392
- for (const [k, v] of Object.entries(value)) out[k] = await uploadFiles(v, client, value, depth + 1);
28430
+ for (const [k, v] of Object.entries(value)) out[k] = await uploadFiles(v, client, storage, value, depth + 1);
28393
28431
  return out;
28394
28432
  }
28395
28433
  return value;
28396
28434
  }
28435
+ function splitPayload(payload) {
28436
+ const bytes = Buffer.from(JSON.stringify(payload ?? null), "utf8");
28437
+ const total = Math.max(1, Math.ceil(bytes.byteLength / PART_BYTES));
28438
+ const id = randomUUID3();
28439
+ return Array.from({ length: total }, (_, index) => ({
28440
+ id,
28441
+ index,
28442
+ total,
28443
+ data: bytes.subarray(index * PART_BYTES, (index + 1) * PART_BYTES).toString("base64")
28444
+ }));
28445
+ }
28397
28446
  function toFileHandles(value, client, depth = 0) {
28398
28447
  const ref = fileRefOf(value);
28399
28448
  if (ref) return new ParcelFile(ref, client);
@@ -28414,6 +28463,26 @@ var recipientOf = (message) => {
28414
28463
  const id = message.to?.id;
28415
28464
  return typeof id === "string" ? id : "";
28416
28465
  };
28466
+ var MAX_READERS = 1e4;
28467
+ var ParcelReaders = class {
28468
+ constructor(max = MAX_READERS) {
28469
+ this.max = max;
28470
+ this.ids = /* @__PURE__ */ new Set();
28471
+ }
28472
+ /** Notes what a received message says about its sender. */
28473
+ noteSender(message) {
28474
+ const from = message?.from;
28475
+ if (typeof from?.id !== "string" || !from.id) return;
28476
+ this.ids.delete(from.id);
28477
+ if (from.parcels !== true) return;
28478
+ this.ids.add(from.id);
28479
+ if (this.ids.size > this.max) this.ids.delete(this.ids.values().next().value);
28480
+ }
28481
+ /** Whether the message is addressed to a socket that said it reads parcels. */
28482
+ reads(message) {
28483
+ return this.ids.has(recipientOf(message));
28484
+ }
28485
+ };
28417
28486
  var InOrder = class {
28418
28487
  /** `onIdle` runs each time the last waiting task finishes. */
28419
28488
  constructor(onIdle) {
@@ -28435,15 +28504,19 @@ var InOrder = class {
28435
28504
  }
28436
28505
  };
28437
28506
  var OutgoingParcels = class {
28438
- constructor(client, jsonParcels, deliver, threshold = PARCEL_THRESHOLD_BYTES) {
28507
+ constructor(client, jsonParcels, deliver, readers = null, threshold = PARCEL_THRESHOLD_BYTES) {
28439
28508
  this.client = client;
28440
28509
  this.jsonParcels = jsonParcels;
28441
28510
  this.deliver = deliver;
28511
+ this.readers = readers;
28442
28512
  this.threshold = threshold;
28443
28513
  /** Only recipients with something waiting; a queue is dropped once it empties. */
28444
28514
  this.queues = /* @__PURE__ */ new Map();
28445
28515
  this.warnedNoClient = false;
28446
28516
  }
28517
+ readsParcels(message) {
28518
+ return this.readers ? this.readers.reads(message) : true;
28519
+ }
28447
28520
  /**
28448
28521
  * Same contract as the SDK's `send`: never throws, returns whether the message
28449
28522
  * went out (or, for a queued one, was accepted).
@@ -28463,7 +28536,7 @@ var OutgoingParcels = class {
28463
28536
  files = false;
28464
28537
  }
28465
28538
  if (!files) {
28466
- if (!this.jsonParcels || !this.client) return this.now(message);
28539
+ if (!this.jsonParcels || !this.client || !this.readsParcels(message)) return this.now(message);
28467
28540
  let wire;
28468
28541
  try {
28469
28542
  wire = JSON.stringify(message);
@@ -28473,8 +28546,7 @@ var OutgoingParcels = class {
28473
28546
  if (Buffer.byteLength(wire, "utf8") < this.threshold) return this.now(message, wire);
28474
28547
  }
28475
28548
  this.queueFor(message).run(async () => {
28476
- const out = await this.prepare(message, files);
28477
- this.deliver(out.message, out.wire);
28549
+ for (const out of await this.prepare(message, files)) this.deliver(out.message, out.wire);
28478
28550
  });
28479
28551
  return true;
28480
28552
  }
@@ -28499,44 +28571,66 @@ var OutgoingParcels = class {
28499
28571
  async idle() {
28500
28572
  await Promise.all(Array.from(this.queues.values(), (queue) => queue.idle()));
28501
28573
  }
28574
+ /**
28575
+ * What actually goes on the socket for this message: usually one message (as
28576
+ * is, or as a parcel receipt); several parts when storage is down and the
28577
+ * message is too large for the relay.
28578
+ */
28502
28579
  async prepare(message, files) {
28503
28580
  let current = message;
28581
+ const storage = { down: false };
28504
28582
  if (files) {
28505
- const started2 = Date.now();
28583
+ const started = Date.now();
28506
28584
  try {
28507
- current = { ...message, payload: await uploadFiles(message.payload, this.client) };
28508
- logger.info(`[parcels] ${message.type} ${message.id ?? ""}: file(s) uploaded (${Date.now() - started2}ms)`);
28585
+ current = { ...message, payload: await uploadFiles(message.payload, this.client, storage) };
28586
+ if (!storage.down) logger.info(`[parcels] ${message.type} ${message.id ?? ""}: file(s) uploaded (${Date.now() - started}ms)`);
28509
28587
  } catch (err) {
28510
- const reason = err?.message ?? String(err);
28511
- logger.error(`[parcels] file upload failed for ${message.type}, sending an error instead: ${reason}`);
28512
- return { message: asError(message, `Could not send a file: ${reason}`) };
28588
+ const reason2 = err?.message ?? String(err);
28589
+ logger.error(`[parcels] file upload failed for ${message.type}, sending an error instead: ${reason2}`);
28590
+ return [{ message: asError(message, `Could not send a file: ${reason2}`) }];
28513
28591
  }
28514
28592
  }
28515
28593
  let wire;
28516
28594
  try {
28517
28595
  wire = JSON.stringify(current);
28518
28596
  } catch {
28519
- return { message: current };
28597
+ return [{ message: current }];
28520
28598
  }
28521
28599
  const size = Buffer.byteLength(wire, "utf8");
28522
- if (!this.jsonParcels || size < this.threshold) return { message: current, wire };
28523
- const started = Date.now();
28524
- try {
28525
- const bytes = Buffer.from(JSON.stringify(current.payload ?? null), "utf8");
28526
- const ref = await this.client.upload(bytes);
28527
- logger.info(
28528
- `[parcels] ${message.type} ${message.id ?? ""} \u2192 parcel ${ref.hash.slice(0, 12)} (${ref.size} bytes, ${Date.now() - started}ms)`
28529
- );
28530
- return { message: { ...current, payload: { [PARCEL_FIELD]: ref } } };
28531
- } catch (err) {
28532
- const reason = err?.message ?? String(err);
28533
- if (size <= MAX_INLINE_BYTES) {
28534
- logger.warn(`[parcels] upload failed for ${message.type}, sending inline (${size} bytes): ${reason}`);
28535
- return { message: current, wire };
28600
+ if (size < this.threshold) return [{ message: current, wire }];
28601
+ if (!this.readsParcels(message)) {
28602
+ if (size > MAX_INLINE_BYTES) {
28603
+ logger.warn(`[parcels] ${message.type} (${size} bytes) is for a client that doesn't read parcels \u2014 sent inline; the relay may refuse it`);
28604
+ }
28605
+ return [{ message: current, wire }];
28606
+ }
28607
+ let reason = "large-JSON parcels are off";
28608
+ if (this.jsonParcels && !storage.down) {
28609
+ const started = Date.now();
28610
+ try {
28611
+ const bytes = Buffer.from(JSON.stringify(current.payload ?? null), "utf8");
28612
+ const ref = await this.client.upload(bytes);
28613
+ logger.info(
28614
+ `[parcels] ${message.type} ${message.id ?? ""} \u2192 parcel ${ref.hash.slice(0, 12)} (${ref.size} bytes, ${Date.now() - started}ms)`
28615
+ );
28616
+ return [{ message: { ...current, payload: { [PARCEL_FIELD]: ref } } }];
28617
+ } catch (err) {
28618
+ reason = err?.message ?? String(err);
28536
28619
  }
28537
- logger.error(`[parcels] upload failed for ${message.type} (${size} bytes), sending an error instead: ${reason}`);
28538
- return { message: asError(message, `The message was too large to deliver (${size} bytes): ${reason}`) };
28620
+ } else if (storage.down) {
28621
+ reason = "storage is down";
28622
+ }
28623
+ if (size <= MAX_INLINE_BYTES) {
28624
+ if (this.jsonParcels) logger.warn(`[parcels] ${message.type} not stored (${reason}), sending inline (${size} bytes)`);
28625
+ return [{ message: current, wire }];
28626
+ }
28627
+ if (size > MAX_PARCEL_BYTES) {
28628
+ logger.error(`[parcels] ${message.type} (${size} bytes) is too large to send`);
28629
+ return [{ message: asError(message, `The message was too large to deliver (${size} bytes): ${reason}`) }];
28539
28630
  }
28631
+ const parts = splitPayload(current.payload);
28632
+ logger.warn(`[parcels] ${message.type} not stored (${reason}), sending in ${parts.length} parts (${size} bytes)`);
28633
+ return parts.map((part) => ({ message: { ...current, payload: { [PART_FIELD]: part } } }));
28540
28634
  }
28541
28635
  };
28542
28636
  var IncomingParcels = class {
@@ -28544,12 +28638,22 @@ var IncomingParcels = class {
28544
28638
  this.client = client;
28545
28639
  this.dispatch = dispatch;
28546
28640
  this.queue = new InOrder();
28641
+ /** Split messages still waiting for parts, by part id. */
28642
+ this.partial = /* @__PURE__ */ new Map();
28643
+ /** Split messages already rebuilt, so a second copy of their parts is ignored. */
28644
+ this.rebuilt = /* @__PURE__ */ new Set();
28547
28645
  }
28548
28646
  /**
28549
28647
  * `raw` is the message's text when it is at hand: a message that never
28550
28648
  * mentions a file placeholder is not searched for one.
28551
28649
  */
28552
28650
  receive(message, raw) {
28651
+ const part = partOf(message?.payload);
28652
+ if (part) {
28653
+ const whole = this.collect(message, part);
28654
+ if (whole) this.receive(whole);
28655
+ return;
28656
+ }
28553
28657
  const ref = parcelRefOf(message?.payload);
28554
28658
  if (!ref) {
28555
28659
  const withFiles = raw === void 0 || raw.includes(FILE_FIELD) ? this.withFiles(message) : message;
@@ -28565,6 +28669,38 @@ var IncomingParcels = class {
28565
28669
  idle() {
28566
28670
  return this.queue.idle();
28567
28671
  }
28672
+ /**
28673
+ * Holds one part; once all of them are here, the original message (envelope
28674
+ * from the last part, payload rebuilt). Null while parts are still missing.
28675
+ */
28676
+ collect(message, part) {
28677
+ if (this.rebuilt.has(part.id)) return null;
28678
+ let entry = this.partial.get(part.id);
28679
+ if (!entry) {
28680
+ const timer = setTimeout(() => {
28681
+ this.partial.delete(part.id);
28682
+ logger.warn(`[parcels] ${message.type}: gave up waiting for the rest of a split message (${part.id})`);
28683
+ }, PART_WAIT_MS).unref();
28684
+ entry = { parts: new Array(part.total), got: 0, timer };
28685
+ this.partial.set(part.id, entry);
28686
+ }
28687
+ if (entry.parts.length !== part.total || entry.parts[part.index] !== void 0) return null;
28688
+ entry.parts[part.index] = part.data;
28689
+ if (++entry.got < part.total) return null;
28690
+ clearTimeout(entry.timer);
28691
+ this.partial.delete(part.id);
28692
+ this.rebuilt.add(part.id);
28693
+ setTimeout(() => this.rebuilt.delete(part.id), PART_WAIT_MS).unref();
28694
+ try {
28695
+ const bytes = Buffer.concat(entry.parts.map((data) => Buffer.from(data, "base64")));
28696
+ logger.info(`[parcels] ${message.type} ${message.id ?? ""} rebuilt from ${part.total} parts (${bytes.byteLength} bytes)`);
28697
+ return { ...message, payload: JSON.parse(bytes.toString("utf8")) };
28698
+ } catch (err) {
28699
+ const reason = err?.message ?? String(err);
28700
+ logger.error(`[parcels] could not rebuild a split ${message.type}: ${reason}`);
28701
+ return asError(message, `Could not rebuild the message: ${reason}`);
28702
+ }
28703
+ }
28568
28704
  withFiles(message) {
28569
28705
  try {
28570
28706
  return hasFileRef(message.payload) ? { ...message, payload: toFileHandles(message.payload, this.client) } : message;
@@ -29928,6 +30064,8 @@ var SuperatomSDK = class {
29928
30064
  // since an outage of unknown length shouldn't grow this without limit.
29929
30065
  this.pendingOutbox = [];
29930
30066
  this.MAX_OUTBOX_SIZE = 50;
30067
+ /** The client sockets that said they read parcels; only they get large JSON as parcels or parts. */
30068
+ this.parcelReaders = new ParcelReaders();
29931
30069
  /**
29932
30070
  * The dashboard contract, as registered by the browser bundle that holds it.
29933
30071
  *
@@ -29954,7 +30092,8 @@ var SuperatomSDK = class {
29954
30092
  this.outgoing = new OutgoingParcels(
29955
30093
  this.parcelClient,
29956
30094
  jsonParcels,
29957
- (message, wire) => this.deliver(message, wire)
30095
+ (message, wire) => this.deliver(message, wire),
30096
+ this.parcelReaders
29958
30097
  );
29959
30098
  this.incoming = new IncomingParcels(this.parcelClient, (parsed) => this.dispatchMessage(parsed));
29960
30099
  this.anthropicApiKey = config2.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY || "";
@@ -30121,6 +30260,7 @@ var SuperatomSDK = class {
30121
30260
  logger.error("Failed to parse incoming message:", error);
30122
30261
  return;
30123
30262
  }
30263
+ this.parcelReaders.noteSender(parsed);
30124
30264
  this.incoming.receive(parsed, data);
30125
30265
  }
30126
30266
  /** Routes one parsed message — everything `handleMessage` did after parsing. */
@@ -30592,7 +30732,7 @@ var SuperatomSDK = class {
30592
30732
  waiters.push(finish);
30593
30733
  this.registryWaiters.set(resType, waiters);
30594
30734
  const sent = this.send({
30595
- id: `${reqType.toLowerCase()}_${randomUUID3()}`,
30735
+ id: `${reqType.toLowerCase()}_${randomUUID4()}`,
30596
30736
  type: reqType,
30597
30737
  from: { type: this.type },
30598
30738
  to: { type: "runtime" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@superatomai/sdk-node",
3
- "version": "0.0.94-dsp",
3
+ "version": "0.0.96-dsp",
4
4
  "description": "Node.js TypeScript SDK for Superatom",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",