@standardserver/peer 0.0.0 → 0.0.2

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
@@ -202,22 +202,60 @@ declare class ClientPeer {
202
202
  close(options?: AsyncIdQueueCloseOptions): Promise<void>;
203
203
  }
204
204
 
205
+ interface EncodePeerMessageOptions {
206
+ /**
207
+ * Optional string prepended to the encoded message.
208
+ * Used to distinguish messages when multiple protocols share the same peer.
209
+ */
210
+ prefix?: string;
211
+ }
212
+ /**
213
+ * Encodes a {@link PeerMessage} into a wire-safe representation.
214
+ *
215
+ * Encoding rules:
216
+ * - If no binary payload is present, the message is encoded as a JSON string.
217
+ * - If binary data exists, the output layout is:
218
+ *
219
+ * [ UTF-8 (prefix + JSON) | delimiter byte | raw binary bytes ]
220
+ *
221
+ * The optional prefix is prepended to the JSON portion before encoding.
222
+ */
223
+ declare function encodePeerMessage(message: PeerMessage, options?: EncodePeerMessageOptions): Promise<string | Uint8Array<ArrayBuffer>>;
224
+ interface DecodePeerMessageOptions {
225
+ /**
226
+ * Optional prefix expected at the start of the encoded message.
227
+ * If present and the message does not start with this prefix,
228
+ * the decoder returns `matched: false`.
229
+ */
230
+ prefix?: string;
231
+ }
205
232
  /**
206
- * Encodes a PeerMessage into a wire-safe representation.
233
+ * Result of a decode attempt.
207
234
  *
208
- * - If no binary data is present, the message is encoded as a JSON string.
209
- * - If binary data exists, the output is:
210
- * [ UTF-8 JSON bytes | separator byte | raw binary bytes ]
235
+ * - `matched: false` indicates the input does not belong to this decoder
236
+ * (typically due to a prefix mismatch).
237
+ * - `matched: true` indicates successful decoding of a {@link PeerMessage}.
211
238
  */
212
- declare function encodePeerMessage(message: PeerMessage): Promise<string | Uint8Array<ArrayBuffer>>;
239
+ type DecodePeerMessageResult = {
240
+ matched: false;
241
+ message?: undefined;
242
+ } | {
243
+ matched: true;
244
+ message: PeerMessage;
245
+ };
213
246
  /**
214
- * Decodes a wire-encoded PeerMessage.
247
+ * Decodes a wire-encoded {@link PeerMessage}.
248
+ *
249
+ * Decoding rules:
250
+ * - String input is treated as a JSON-only message.
251
+ * - Binary input may contain:
252
+ * - JSON only, or
253
+ * - JSON followed by binary data separated by the delimiter byte.
215
254
  *
216
- * - String input is treated as pure JSON.
217
- * - Binary input may contain only JSON bytes, or JSON followed by binary data
218
- * separated by the separator byte.
255
+ * If a prefix is provided, it must be present at the start of the payload
256
+ * or the decode attempt will return `matched: false`.
219
257
  */
220
- declare function decodePeerMessage(data: string | Uint8Array<ArrayBuffer>): PeerMessage;
258
+ declare function decodePeerMessage(encoded: string | Uint8Array<ArrayBuffer>, options?: DecodePeerMessageOptions): DecodePeerMessageResult;
221
259
 
222
260
  /**
223
261
  * Creates an AsyncIterator from a queue of peer event-stream messages.
@@ -277,4 +315,4 @@ declare class ServerPeer {
277
315
  }
278
316
 
279
317
  export { ClientPeer, EventStreamTransmitter, HibernationEventIterator, ServerPeer, decodePeerMessage, encodePeerMessage, toEventIterator };
280
- export type { ClientPeerCloseOptions, HibernationEventIteratorCallback, PeerAbortMessage, PeerEventStreamMessage, PeerMessage, PeerOctetStreamMessage, PeerRequestMessage, PeerResponseMessage, PeerStreamCancelMessage, ServerPeerCloseOptions };
318
+ export type { ClientPeerCloseOptions, DecodePeerMessageOptions, DecodePeerMessageResult, EncodePeerMessageOptions, HibernationEventIteratorCallback, PeerAbortMessage, PeerEventStreamMessage, PeerMessage, PeerOctetStreamMessage, PeerRequestMessage, PeerResponseMessage, PeerStreamCancelMessage, ServerPeerCloseOptions };
package/dist/index.d.ts CHANGED
@@ -202,22 +202,60 @@ declare class ClientPeer {
202
202
  close(options?: AsyncIdQueueCloseOptions): Promise<void>;
203
203
  }
204
204
 
205
+ interface EncodePeerMessageOptions {
206
+ /**
207
+ * Optional string prepended to the encoded message.
208
+ * Used to distinguish messages when multiple protocols share the same peer.
209
+ */
210
+ prefix?: string;
211
+ }
212
+ /**
213
+ * Encodes a {@link PeerMessage} into a wire-safe representation.
214
+ *
215
+ * Encoding rules:
216
+ * - If no binary payload is present, the message is encoded as a JSON string.
217
+ * - If binary data exists, the output layout is:
218
+ *
219
+ * [ UTF-8 (prefix + JSON) | delimiter byte | raw binary bytes ]
220
+ *
221
+ * The optional prefix is prepended to the JSON portion before encoding.
222
+ */
223
+ declare function encodePeerMessage(message: PeerMessage, options?: EncodePeerMessageOptions): Promise<string | Uint8Array<ArrayBuffer>>;
224
+ interface DecodePeerMessageOptions {
225
+ /**
226
+ * Optional prefix expected at the start of the encoded message.
227
+ * If present and the message does not start with this prefix,
228
+ * the decoder returns `matched: false`.
229
+ */
230
+ prefix?: string;
231
+ }
205
232
  /**
206
- * Encodes a PeerMessage into a wire-safe representation.
233
+ * Result of a decode attempt.
207
234
  *
208
- * - If no binary data is present, the message is encoded as a JSON string.
209
- * - If binary data exists, the output is:
210
- * [ UTF-8 JSON bytes | separator byte | raw binary bytes ]
235
+ * - `matched: false` indicates the input does not belong to this decoder
236
+ * (typically due to a prefix mismatch).
237
+ * - `matched: true` indicates successful decoding of a {@link PeerMessage}.
211
238
  */
212
- declare function encodePeerMessage(message: PeerMessage): Promise<string | Uint8Array<ArrayBuffer>>;
239
+ type DecodePeerMessageResult = {
240
+ matched: false;
241
+ message?: undefined;
242
+ } | {
243
+ matched: true;
244
+ message: PeerMessage;
245
+ };
213
246
  /**
214
- * Decodes a wire-encoded PeerMessage.
247
+ * Decodes a wire-encoded {@link PeerMessage}.
248
+ *
249
+ * Decoding rules:
250
+ * - String input is treated as a JSON-only message.
251
+ * - Binary input may contain:
252
+ * - JSON only, or
253
+ * - JSON followed by binary data separated by the delimiter byte.
215
254
  *
216
- * - String input is treated as pure JSON.
217
- * - Binary input may contain only JSON bytes, or JSON followed by binary data
218
- * separated by the separator byte.
255
+ * If a prefix is provided, it must be present at the start of the payload
256
+ * or the decode attempt will return `matched: false`.
219
257
  */
220
- declare function decodePeerMessage(data: string | Uint8Array<ArrayBuffer>): PeerMessage;
258
+ declare function decodePeerMessage(encoded: string | Uint8Array<ArrayBuffer>, options?: DecodePeerMessageOptions): DecodePeerMessageResult;
221
259
 
222
260
  /**
223
261
  * Creates an AsyncIterator from a queue of peer event-stream messages.
@@ -277,4 +315,4 @@ declare class ServerPeer {
277
315
  }
278
316
 
279
317
  export { ClientPeer, EventStreamTransmitter, HibernationEventIterator, ServerPeer, decodePeerMessage, encodePeerMessage, toEventIterator };
280
- export type { ClientPeerCloseOptions, HibernationEventIteratorCallback, PeerAbortMessage, PeerEventStreamMessage, PeerMessage, PeerOctetStreamMessage, PeerRequestMessage, PeerResponseMessage, PeerStreamCancelMessage, ServerPeerCloseOptions };
318
+ export type { ClientPeerCloseOptions, DecodePeerMessageOptions, DecodePeerMessageResult, EncodePeerMessageOptions, HibernationEventIteratorCallback, PeerAbortMessage, PeerEventStreamMessage, PeerMessage, PeerOctetStreamMessage, PeerRequestMessage, PeerResponseMessage, PeerStreamCancelMessage, ServerPeerCloseOptions };
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { flattenStandardHeader, getFilenameFromContentDisposition, generateContentDisposition } from '@standardserver/core';
2
- import { AsyncIteratorClass, isTypescriptObject, SequentialIdGenerator, AsyncIdQueue, isAsyncIteratorObject, AbortError, stringifyJSON } from '@standardserver/shared';
1
+ import { AsyncIteratorClass, isTypescriptObject, isAsyncIteratorObject, SequentialIdGenerator, AsyncIdQueue, AbortError, stringifyJSON } from '@standardserver/shared';
2
+ import { generateContentDisposition, flattenStandardHeader, getFilenameFromContentDisposition } from '@standardserver/core';
3
3
  import { withEventIteratorEventMeta, EventIteratorErrorEvent, resolveEventIteratorEvent } from '@standardserver/core/event-stream';
4
4
 
5
5
  function toEventIterator(pull, cleanup) {
@@ -181,6 +181,37 @@ async function toStandardBody(message, eventStreamMessageQueue, octetStreamMessa
181
181
  await cleanup(true);
182
182
  }
183
183
  }
184
+ async function encodeAtomicStandardBody(body, headers) {
185
+ headers = { ...headers };
186
+ let binary;
187
+ let jsonBody = body;
188
+ headers["standard-server"] = void 0;
189
+ if (body instanceof ReadableStream) {
190
+ jsonBody = void 0;
191
+ headers["standard-server"] = "octet-stream";
192
+ } else if (isAsyncIteratorObject(body)) {
193
+ jsonBody = void 0;
194
+ headers["standard-server"] = "event-stream";
195
+ } else if (body instanceof FormData) {
196
+ const res = new Response(body);
197
+ binary = await res.blob();
198
+ jsonBody = void 0;
199
+ headers["standard-server"] = "form-data";
200
+ headers["content-type"] ??= res.headers.get("content-type");
201
+ } else if (body instanceof Blob) {
202
+ binary = body;
203
+ jsonBody = void 0;
204
+ headers["standard-server"] = "file";
205
+ headers["content-disposition"] ??= generateContentDisposition(
206
+ body instanceof File ? body.name : "blob"
207
+ );
208
+ headers["content-type"] ??= body.type;
209
+ } else if (body instanceof URLSearchParams) {
210
+ jsonBody = body.toString();
211
+ headers["standard-server"] = "url-search-params";
212
+ }
213
+ return [jsonBody, headers, binary];
214
+ }
184
215
 
185
216
  class ClientPeer {
186
217
  constructor(send) {
@@ -245,39 +276,20 @@ class ClientPeer {
245
276
  ];
246
277
  this.cleanupFns.set(id, cleanupFns);
247
278
  try {
279
+ const [jsonBody, headers, binary] = await encodeAtomicStandardBody(request.body, request.headers);
280
+ signal?.throwIfAborted();
248
281
  const requestMessage = {
249
282
  id,
250
283
  kind: "request",
251
284
  json: {
252
- ...{ ...request, signal: void 0 },
253
- // clone and remove signal from request
254
- headers: { ...request.headers }
255
- // clone headers
256
- }
285
+ ...request,
286
+ headers,
287
+ body: jsonBody,
288
+ ...{ signal: void 0 }
289
+ // remove signal from request
290
+ },
291
+ binary
257
292
  };
258
- if (request.body instanceof ReadableStream) {
259
- requestMessage.json.body = void 0;
260
- requestMessage.json.headers["standard-server"] = "octet-stream";
261
- } else if (isAsyncIteratorObject(request.body)) {
262
- requestMessage.json.body = void 0;
263
- requestMessage.json.headers["standard-server"] = "event-stream";
264
- } else if (request.body instanceof FormData) {
265
- const res = new Response(request.body);
266
- requestMessage.binary = await res.blob();
267
- requestMessage.json.body = void 0;
268
- requestMessage.json.headers["standard-server"] = "form-data";
269
- requestMessage.json.headers["content-type"] = res.headers.get("content-type") ?? void 0;
270
- } else if (request.body instanceof Blob) {
271
- requestMessage.binary = request.body;
272
- requestMessage.json.body = void 0;
273
- requestMessage.json.headers["standard-server"] = "file";
274
- requestMessage.json.headers["content-disposition"] = generateContentDisposition(request.body instanceof File ? request.body.name : "blob");
275
- requestMessage.json.headers["content-type"] = request.body.type;
276
- } else if (request.body instanceof URLSearchParams) {
277
- requestMessage.json.body = request.body.toString();
278
- requestMessage.json.headers["standard-server"] = "url-search-params";
279
- }
280
- signal?.throwIfAborted();
281
293
  await this.send(requestMessage);
282
294
  signal?.throwIfAborted();
283
295
  if (isAsyncIteratorObject(request.body)) {
@@ -391,36 +403,57 @@ class ClientPeer {
391
403
  }
392
404
  }
393
405
 
394
- const JSON_BINARY_SEPARATOR_BYTE = 255;
406
+ const JSON_BINARY_DELIMITER = 255;
395
407
  const textEncoder = new TextEncoder();
396
408
  const textDecoder = new TextDecoder();
397
- async function encodePeerMessage(message) {
409
+ async function encodePeerMessage(message, options = {}) {
410
+ const jsonPart = stringifyJSON({ ...message, binary: void 0 });
398
411
  if (message.binary === void 0) {
399
- return stringifyJSON(message);
412
+ return options.prefix ? options.prefix + jsonPart : jsonPart;
400
413
  }
401
- const jsonBytes = textEncoder.encode(stringifyJSON(message));
414
+ const textBytes = textEncoder.encode(
415
+ options.prefix ? options.prefix + jsonPart : jsonPart
416
+ );
402
417
  const binaryBytes = message.binary instanceof Blob ? new Uint8Array(await message.binary.arrayBuffer()) : message.binary;
403
418
  const output = new Uint8Array(
404
- jsonBytes.length + 1 + binaryBytes.length
419
+ textBytes.length + 1 + binaryBytes.length
405
420
  );
406
- output.set(jsonBytes, 0);
407
- output[jsonBytes.length] = JSON_BINARY_SEPARATOR_BYTE;
408
- output.set(binaryBytes, jsonBytes.length + 1);
421
+ output.set(textBytes, 0);
422
+ output[textBytes.length] = JSON_BINARY_DELIMITER;
423
+ output.set(binaryBytes, textBytes.length + 1);
409
424
  return output;
410
425
  }
411
- function decodePeerMessage(data) {
412
- if (typeof data === "string") {
413
- return JSON.parse(data);
426
+ function decodePeerMessage(encoded, options = {}) {
427
+ if (typeof encoded === "string") {
428
+ if (options.prefix) {
429
+ if (!encoded.startsWith(options.prefix)) {
430
+ return { matched: false };
431
+ }
432
+ encoded = encoded.slice(options.prefix.length);
433
+ }
434
+ return { matched: true, message: JSON.parse(encoded) };
435
+ }
436
+ if (options.prefix) {
437
+ const prefixBytes = textEncoder.encode(options.prefix);
438
+ for (let i = 0; i < prefixBytes.length; i++) {
439
+ if (encoded[i] !== prefixBytes[i]) {
440
+ return { matched: false };
441
+ }
442
+ }
443
+ encoded = encoded.subarray(prefixBytes.length);
414
444
  }
415
- const separatorIndex = data.indexOf(JSON_BINARY_SEPARATOR_BYTE);
445
+ const separatorIndex = encoded.indexOf(JSON_BINARY_DELIMITER);
416
446
  if (separatorIndex === -1) {
417
- return JSON.parse(textDecoder.decode(data));
447
+ return { matched: true, message: JSON.parse(textDecoder.decode(encoded)) };
418
448
  }
419
- const jsonBytes = data.subarray(0, separatorIndex);
420
- const binaryBytes = data.subarray(separatorIndex + 1);
449
+ const jsonBytes = encoded.subarray(0, separatorIndex);
450
+ const binaryBytes = encoded.subarray(separatorIndex + 1);
421
451
  return {
422
- ...JSON.parse(textDecoder.decode(jsonBytes)),
423
- binary: binaryBytes
452
+ matched: true,
453
+ message: {
454
+ ...JSON.parse(textDecoder.decode(jsonBytes)),
455
+ binary: binaryBytes
456
+ }
424
457
  };
425
458
  }
426
459
 
@@ -509,41 +542,22 @@ class ServerPeer {
509
542
  if (signal.aborted) {
510
543
  return;
511
544
  }
545
+ const [jsonBody, headers, binary] = await encodeAtomicStandardBody(response.body, response.headers);
546
+ if (signal.aborted) {
547
+ return;
548
+ }
512
549
  const responseMessage = {
513
550
  id: message.id,
514
551
  kind: "response",
515
552
  json: {
516
- ...{ ...response, signal: void 0 },
517
- // clone and remove signal from request
518
- headers: { ...response.headers }
519
- // clone headers
520
- }
553
+ ...response,
554
+ headers,
555
+ body: jsonBody,
556
+ ...{ signal: void 0 }
557
+ // remove signal from request
558
+ },
559
+ binary
521
560
  };
522
- if (response.body instanceof ReadableStream) {
523
- responseMessage.json.body = void 0;
524
- responseMessage.json.headers["standard-server"] = "octet-stream";
525
- } else if (isAsyncIteratorObject(response.body)) {
526
- responseMessage.json.body = void 0;
527
- responseMessage.json.headers["standard-server"] = "event-stream";
528
- } else if (response.body instanceof FormData) {
529
- const res = new Response(response.body);
530
- responseMessage.binary = await res.blob();
531
- responseMessage.json.body = void 0;
532
- responseMessage.json.headers["standard-server"] = "form-data";
533
- responseMessage.json.headers["content-type"] = res.headers.get("content-type") ?? void 0;
534
- } else if (response.body instanceof Blob) {
535
- responseMessage.binary = response.body;
536
- responseMessage.json.body = void 0;
537
- responseMessage.json.headers["standard-server"] = "file";
538
- responseMessage.json.headers["content-disposition"] = generateContentDisposition(response.body instanceof File ? response.body.name : "blob");
539
- responseMessage.json.headers["content-type"] = response.body.type;
540
- } else if (response.body instanceof URLSearchParams) {
541
- responseMessage.json.body = response.body.toString();
542
- responseMessage.json.headers["standard-server"] = "url-search-params";
543
- }
544
- if (signal.aborted) {
545
- return;
546
- }
547
561
  await this.send(responseMessage);
548
562
  if (signal.aborted) {
549
563
  return;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@standardserver/peer",
3
3
  "type": "module",
4
- "version": "0.0.0",
4
+ "version": "0.0.2",
5
5
  "license": "MIT",
6
6
  "homepage": "https://standardserver.dev",
7
7
  "repository": {
@@ -20,8 +20,8 @@
20
20
  "dist"
21
21
  ],
22
22
  "dependencies": {
23
- "@standardserver/core": "0.0.0",
24
- "@standardserver/shared": "0.0.0"
23
+ "@standardserver/core": "0.0.2",
24
+ "@standardserver/shared": "0.0.2"
25
25
  },
26
26
  "scripts": {
27
27
  "build": "unbuild",