@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 +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +176 -36
- package/dist/index.mjs +179 -39
- package/package.json +1 -1
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
|
-
/**
|
|
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
|
-
/**
|
|
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 =
|
|
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
|
-
/**
|
|
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
|
|
28501
|
-
|
|
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
|
|
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
|
|
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() -
|
|
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
|
|
28631
|
-
logger.error(`[parcels] file upload failed for ${message.type}, sending an error instead: ${
|
|
28632
|
-
return { message: asError(message, `Could not send a file: ${
|
|
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 (
|
|
28643
|
-
|
|
28644
|
-
|
|
28645
|
-
|
|
28646
|
-
|
|
28647
|
-
|
|
28648
|
-
|
|
28649
|
-
|
|
28650
|
-
|
|
28651
|
-
|
|
28652
|
-
|
|
28653
|
-
|
|
28654
|
-
|
|
28655
|
-
|
|
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
|
-
|
|
28658
|
-
|
|
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
|
|
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 =
|
|
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
|
-
/**
|
|
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
|
|
28381
|
-
|
|
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
|
|
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
|
|
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() -
|
|
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
|
|
28511
|
-
logger.error(`[parcels] file upload failed for ${message.type}, sending an error instead: ${
|
|
28512
|
-
return { message: asError(message, `Could not send a file: ${
|
|
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 (
|
|
28523
|
-
|
|
28524
|
-
|
|
28525
|
-
|
|
28526
|
-
|
|
28527
|
-
|
|
28528
|
-
|
|
28529
|
-
|
|
28530
|
-
|
|
28531
|
-
|
|
28532
|
-
|
|
28533
|
-
|
|
28534
|
-
|
|
28535
|
-
|
|
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
|
-
|
|
28538
|
-
|
|
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()}_${
|
|
30735
|
+
id: `${reqType.toLowerCase()}_${randomUUID4()}`,
|
|
30596
30736
|
type: reqType,
|
|
30597
30737
|
from: { type: this.type },
|
|
30598
30738
|
to: { type: "runtime" },
|