@orpc/server 2.0.0-beta.14 → 2.0.0-beta.16

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.
Files changed (36) hide show
  1. package/README.md +2 -2
  2. package/dist/adapters/crossws/index.d.mts +1 -1
  3. package/dist/adapters/crossws/index.d.ts +1 -1
  4. package/dist/adapters/crossws/index.mjs +3 -3
  5. package/dist/adapters/fetch/index.d.mts +5 -53
  6. package/dist/adapters/fetch/index.d.ts +5 -53
  7. package/dist/adapters/fetch/index.mjs +6 -136
  8. package/dist/adapters/message-port/index.d.mts +1 -1
  9. package/dist/adapters/message-port/index.d.ts +1 -1
  10. package/dist/adapters/message-port/index.mjs +3 -3
  11. package/dist/adapters/node/index.d.mts +4 -34
  12. package/dist/adapters/node/index.d.ts +4 -34
  13. package/dist/adapters/node/index.mjs +6 -112
  14. package/dist/adapters/standard/index.d.mts +1 -1
  15. package/dist/adapters/standard/index.d.ts +1 -1
  16. package/dist/adapters/standard/index.mjs +3 -3
  17. package/dist/adapters/websocket/index.d.mts +1 -1
  18. package/dist/adapters/websocket/index.d.ts +1 -1
  19. package/dist/adapters/websocket/index.mjs +3 -3
  20. package/dist/extensions/callable.mjs +2 -2
  21. package/dist/helpers/index.d.mts +3 -3
  22. package/dist/helpers/index.d.ts +3 -3
  23. package/dist/helpers/index.mjs +13 -5
  24. package/dist/index.d.mts +2 -2
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.mjs +7 -7
  27. package/dist/plugins/index.d.mts +77 -2
  28. package/dist/plugins/index.d.ts +77 -2
  29. package/dist/plugins/index.mjs +305 -5
  30. package/dist/shared/{server.CrlKQucM.mjs → server.BDeup7Ky.mjs} +3 -3
  31. package/dist/shared/{server.CjOb6ItT.mjs → server.CUsS2s2p.mjs} +1 -1
  32. package/dist/shared/{server.BwHnWUuN.mjs → server.C_osSBcd.mjs} +1 -1
  33. package/dist/shared/{server.d_2NS3g5.d.ts → server.D0WUu0EH.d.ts} +3 -2
  34. package/dist/shared/{server.Cj5lgPuG.d.mts → server.D8cib80w.d.mts} +3 -2
  35. package/dist/shared/{server.GDpX6Df8.mjs → server.DZEvIn7o.mjs} +3 -3
  36. package/package.json +9 -10
@@ -1,9 +1,9 @@
1
- import { toArray, value } from '@orpc/shared';
2
- import { flattenStandardHeader, parseStandardUrl, mergeStandardHeaders } from '@standardserver/core';
1
+ import { toArray, value, isCompressibleContentType, isAsyncIteratorObject, stringifyJSON } from '@orpc/shared';
2
+ import { flattenStandardHeader, parseStandardUrl, generateContentDisposition, mergeStandardHeaders } from '@standardserver/core';
3
3
  import { isClientPeerSendMessage, ServerPeer, encodePeerMessage } from '@standardserver/peer';
4
4
  export { C as CSRFGuardHandlerPlugin } from '../shared/server.D_QauotT.mjs';
5
- import { toFetchHeaders, toStandardHeaders } from '@standardserver/fetch';
6
- import '@orpc/client';
5
+ import { toFetchHeaders, toStandardBody, toStandardHeaders } from '@standardserver/fetch';
6
+ import { ORPCError } from '@orpc/client';
7
7
 
8
8
  class BatchHandlerPlugin {
9
9
  name = "~batch";
@@ -244,6 +244,69 @@ class CORSHandlerPlugin {
244
244
  }
245
245
  }
246
246
 
247
+ class RequestCompressionHandlerPlugin {
248
+ name = "~request-compression";
249
+ /**
250
+ * Should decompress the original batch request body instead of sub-requests.
251
+ */
252
+ after = ["~batch"];
253
+ init(options) {
254
+ const routingInterceptor = async ({ next, ...interceptorOptions }) => {
255
+ const encodings = parseContentEncodings(
256
+ flattenStandardHeader(interceptorOptions.request.headers["content-encoding"])
257
+ );
258
+ if (encodings.length === 0 || !encodings.every(isSupportedEncoding)) {
259
+ return next();
260
+ }
261
+ const decompressedHeaders = {
262
+ ...interceptorOptions.request.headers,
263
+ "content-length": void 0,
264
+ "content-encoding": void 0
265
+ };
266
+ return next({
267
+ ...interceptorOptions,
268
+ request: {
269
+ ...interceptorOptions.request,
270
+ headers: decompressedHeaders,
271
+ async resolveBody(hint) {
272
+ const stream = await interceptorOptions.request.resolveBody("octet-stream");
273
+ if (!(stream instanceof ReadableStream)) {
274
+ return stream;
275
+ }
276
+ let decompressedStream = stream;
277
+ for (let i = encodings.length - 1; i >= 0; i--) {
278
+ decompressedStream = decompressedStream.pipeThrough(
279
+ new DecompressionStream(encodings[i])
280
+ );
281
+ }
282
+ const response = new Response(decompressedStream, {
283
+ headers: toFetchHeaders(decompressedHeaders)
284
+ });
285
+ return toStandardBody(response, { hint });
286
+ }
287
+ }
288
+ });
289
+ };
290
+ return {
291
+ ...options,
292
+ routingInterceptors: [
293
+ routingInterceptor,
294
+ ...toArray(options.routingInterceptors)
295
+ ]
296
+ };
297
+ }
298
+ }
299
+ const SUPPORTED_ENCODINGS = ["gzip", "deflate", "deflate-raw"];
300
+ function isSupportedEncoding(encoding) {
301
+ return SUPPORTED_ENCODINGS.includes(encoding);
302
+ }
303
+ function parseContentEncodings(header) {
304
+ if (header === void 0) {
305
+ return [];
306
+ }
307
+ return header.split(",").map((part) => part.trim().toLowerCase());
308
+ }
309
+
247
310
  class RequestHeadersHandlerPlugin {
248
311
  name = "~request-headers";
249
312
  init(options) {
@@ -266,6 +329,243 @@ class RequestHeadersHandlerPlugin {
266
329
  }
267
330
  }
268
331
 
332
+ class RequestLimitHandlerPlugin {
333
+ name = "~request-limit";
334
+ /**
335
+ * Should limit the original batch request body instead of sub-requests.
336
+ */
337
+ after = ["~batch"];
338
+ /**
339
+ * Should limit the final body size instead of the compressed one.
340
+ */
341
+ before = ["~request-compression"];
342
+ maxBodySize;
343
+ constructor(options) {
344
+ this.maxBodySize = options.maxBodySize;
345
+ }
346
+ init(options) {
347
+ const maxBodySize = this.maxBodySize;
348
+ const routingInterceptor = async ({ next, ...interceptorOptions }) => {
349
+ return next({
350
+ ...interceptorOptions,
351
+ request: {
352
+ ...interceptorOptions.request,
353
+ async resolveBody(hint) {
354
+ const contentLength = Number(
355
+ flattenStandardHeader(interceptorOptions.request.headers["content-length"])
356
+ );
357
+ if (Number.isFinite(contentLength) && contentLength > maxBodySize) {
358
+ throw new ORPCError("PAYLOAD_TOO_LARGE");
359
+ }
360
+ const stream = await interceptorOptions.request.resolveBody("octet-stream");
361
+ if (!(stream instanceof ReadableStream)) {
362
+ return stream;
363
+ }
364
+ let currentBodySize = 0;
365
+ const limitedStream = stream.pipeThrough(
366
+ new TransformStream({
367
+ transform(chunk, controller) {
368
+ currentBodySize += chunk.byteLength;
369
+ if (currentBodySize > maxBodySize) {
370
+ controller.error(new ORPCError("PAYLOAD_TOO_LARGE"));
371
+ return;
372
+ }
373
+ controller.enqueue(chunk);
374
+ }
375
+ })
376
+ );
377
+ const response = new Response(limitedStream, {
378
+ headers: toFetchHeaders(interceptorOptions.request.headers)
379
+ });
380
+ return toStandardBody(response, { hint });
381
+ }
382
+ }
383
+ });
384
+ };
385
+ return {
386
+ ...options,
387
+ routingInterceptors: [
388
+ routingInterceptor,
389
+ ...toArray(options.routingInterceptors)
390
+ ]
391
+ };
392
+ }
393
+ }
394
+
395
+ const AVG_BYTES_PER_CHAR = 1.2;
396
+ class ResponseCompressionHandlerPlugin {
397
+ name = "~response-compression";
398
+ /**
399
+ * Compression should be done after batching, to compress the final response.
400
+ * Compression should also be done after response headers are set, to access final headers like Content-Type and Cache-Control.
401
+ */
402
+ after = ["~batch", "~response-headers"];
403
+ encodings;
404
+ threshold;
405
+ constructor(options = {}) {
406
+ this.encodings = options.encodings ?? ["gzip", "deflate"];
407
+ this.threshold = options.threshold ?? 1024;
408
+ }
409
+ init(options) {
410
+ const routingInterceptor = async ({ next, ...interceptorOptions }) => {
411
+ const result = await next();
412
+ if (!result.matched) {
413
+ return result;
414
+ }
415
+ const response = result.response;
416
+ const contentEncoding = flattenStandardHeader(response.headers["content-encoding"])?.trim()?.toLowerCase();
417
+ if (contentEncoding !== void 0) {
418
+ return result;
419
+ }
420
+ if (isNoTransformCacheControl(flattenStandardHeader(response.headers["cache-control"]))) {
421
+ return result;
422
+ }
423
+ const acceptEncodings = parseAcceptEncodings(
424
+ flattenStandardHeader(interceptorOptions.request.headers["accept-encoding"])
425
+ );
426
+ const encoding = this.encodings.find((enc) => acceptEncodings.includes(enc));
427
+ if (encoding === void 0) {
428
+ return result;
429
+ }
430
+ const body = response.body;
431
+ const headers = response.headers;
432
+ if (body instanceof ReadableStream) {
433
+ const contentLength = Number(flattenStandardHeader(headers["content-length"]));
434
+ if ((!Number.isFinite(contentLength) || contentLength >= this.threshold) && isCompressibleContentType(flattenStandardHeader(headers["content-type"]))) {
435
+ return {
436
+ ...result,
437
+ response: {
438
+ ...response,
439
+ body: body.pipeThrough(new CompressionStream(encoding)),
440
+ headers: {
441
+ ...headers,
442
+ "standard-server": "octet-stream",
443
+ "content-length": [],
444
+ "content-encoding": encoding
445
+ }
446
+ }
447
+ };
448
+ }
449
+ } else if (body instanceof Blob) {
450
+ if ((!Number.isFinite(body.size) || body.size >= this.threshold) && isCompressibleContentType(body.type)) {
451
+ const contentDisposition = headers["content-disposition"] ?? generateContentDisposition(
452
+ body instanceof File ? body.name : "blob"
453
+ );
454
+ return {
455
+ ...result,
456
+ response: {
457
+ ...response,
458
+ body: body.stream().pipeThrough(new CompressionStream(encoding)),
459
+ headers: {
460
+ ...headers,
461
+ "standard-server": "file",
462
+ "content-type": body.type,
463
+ "content-length": [],
464
+ "content-disposition": contentDisposition,
465
+ "content-encoding": encoding
466
+ }
467
+ }
468
+ };
469
+ }
470
+ } else if (body instanceof FormData) {
471
+ const PART_OVERHEAD = 64;
472
+ let contentLength = 0;
473
+ for (const [key, value] of body) {
474
+ contentLength += PART_OVERHEAD + key.length;
475
+ if (value instanceof Blob) {
476
+ if (!Number.isFinite(value.size)) {
477
+ if (!isCompressibleContentType(value.type)) {
478
+ contentLength = -Infinity;
479
+ break;
480
+ }
481
+ contentLength = Infinity;
482
+ } else {
483
+ contentLength += isCompressibleContentType(value.type) ? value.size : -value.size;
484
+ }
485
+ } else {
486
+ contentLength += value.length * AVG_BYTES_PER_CHAR;
487
+ }
488
+ }
489
+ if (contentLength >= this.threshold) {
490
+ const res = new Response(body);
491
+ const compressedStream = res.body.pipeThrough(new CompressionStream(encoding));
492
+ return {
493
+ ...result,
494
+ response: {
495
+ ...response,
496
+ body: compressedStream,
497
+ headers: {
498
+ ...headers,
499
+ "standard-server": [],
500
+ "content-type": res.headers.get("content-type"),
501
+ "content-length": [],
502
+ "content-encoding": encoding
503
+ }
504
+ }
505
+ };
506
+ }
507
+ } else if (body instanceof URLSearchParams) {
508
+ const string = body.toString();
509
+ if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) {
510
+ return {
511
+ ...result,
512
+ response: {
513
+ ...response,
514
+ body: new Blob([string]).stream().pipeThrough(new CompressionStream(encoding)),
515
+ headers: {
516
+ ...headers,
517
+ "standard-server": [],
518
+ "content-type": "application/x-www-form-urlencoded",
519
+ "content-length": [],
520
+ "content-encoding": encoding
521
+ }
522
+ }
523
+ };
524
+ }
525
+ } else if (body !== void 0 && !isAsyncIteratorObject(body)) {
526
+ const string = stringifyJSON(body);
527
+ if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) {
528
+ return {
529
+ ...result,
530
+ response: {
531
+ ...response,
532
+ body: new Blob([string]).stream().pipeThrough(new CompressionStream(encoding)),
533
+ headers: {
534
+ ...headers,
535
+ "standard-server": [],
536
+ "content-type": "application/json",
537
+ "content-length": [],
538
+ "content-encoding": encoding
539
+ }
540
+ }
541
+ };
542
+ }
543
+ }
544
+ return result;
545
+ };
546
+ return {
547
+ ...options,
548
+ routingInterceptors: [
549
+ routingInterceptor,
550
+ ...toArray(options.routingInterceptors)
551
+ ]
552
+ };
553
+ }
554
+ }
555
+ function parseAcceptEncodings(header) {
556
+ if (header === void 0) {
557
+ return [];
558
+ }
559
+ return header.split(",").map((part) => part.trim().split(";")[0].trim().toLowerCase()).filter(Boolean);
560
+ }
561
+ const CACHE_CONTROL_NO_TRANSFORM_REGEX = /(?:^|,)\s*no-transform\s*(?:,|$)/i;
562
+ function isNoTransformCacheControl(cacheControl) {
563
+ if (cacheControl === void 0) {
564
+ return false;
565
+ }
566
+ return CACHE_CONTROL_NO_TRANSFORM_REGEX.test(cacheControl);
567
+ }
568
+
269
569
  class ResponseHeadersHandlerPlugin {
270
570
  name = "~response-headers";
271
571
  /**
@@ -354,4 +654,4 @@ class RethrowHandlerPlugin {
354
654
  }
355
655
  }
356
656
 
357
- export { BatchHandlerPlugin, CORSHandlerPlugin, RequestHeadersHandlerPlugin, ResponseHeadersHandlerPlugin, RethrowHandlerPlugin };
657
+ export { BatchHandlerPlugin, CORSHandlerPlugin, RequestCompressionHandlerPlugin, RequestHeadersHandlerPlugin, RequestLimitHandlerPlugin, ResponseCompressionHandlerPlugin, ResponseHeadersHandlerPlugin, RethrowHandlerPlugin };
@@ -1,4 +1,4 @@
1
- import { ORPCError, wrapEventIteratorPreservingMeta, cloneORPCError } from '@orpc/client';
1
+ import { ORPCError, wrapAsyncIteratorPreservingEventMeta, cloneORPCError } from '@orpc/client';
2
2
  import { reconcileORPCError, ValidationError, ProcedureContract } from '@orpc/contract';
3
3
  import { getOrBind, resolveMaybeOptionalOptions, getConstructor, isTypescriptObject, toArray, value, runWithSpan, intercept, isAsyncIteratorObject, override, traceAsyncIterator, traceReadableStream } from '@orpc/shared';
4
4
 
@@ -84,8 +84,8 @@ function createProcedureClient(lazyableProcedure, ...rest) {
84
84
  );
85
85
  });
86
86
  if (isAsyncIteratorObject(output)) {
87
- return override(output, wrapEventIteratorPreservingMeta(
88
- traceAsyncIterator("consume_event_iterator_output", output),
87
+ return override(output, wrapAsyncIteratorPreservingEventMeta(
88
+ traceAsyncIterator("consume_async_iterator_object_output", output),
89
89
  { mapError: reconcileError }
90
90
  ));
91
91
  }
@@ -1,5 +1,5 @@
1
1
  import { resolveMetaPlugins, mergeErrorMap, getHiddenMetaPlugins } from '@orpc/contract';
2
- import { P as Procedure } from './server.CrlKQucM.mjs';
2
+ import { P as Procedure } from './server.BDeup7Ky.mjs';
3
3
 
4
4
  class DecoratedProcedure extends Procedure {
5
5
  meta(...plugins) {
@@ -1,4 +1,4 @@
1
- import { c as createProcedureClient, P as Procedure, L as Lazy, u as unlazy } from './server.CrlKQucM.mjs';
1
+ import { c as createProcedureClient, P as Procedure, L as Lazy, u as unlazy } from './server.BDeup7Ky.mjs';
2
2
  import { resolveMetaPlugins, mergeErrorMap, ProcedureContract } from '@orpc/contract';
3
3
  import { getOrBind, isTypescriptObject } from '@orpc/shared';
4
4
 
@@ -38,14 +38,15 @@ interface RPCHandlerCodecOptions<T extends Context> extends RPCMatcherOptions {
38
38
  /**
39
39
  * Resolve HTTP status for encoded successful outputs.
40
40
  *
41
- * Value should be in range `200-299`
41
+ * Value should be in the `2xx` range and must be less than `400`.
42
42
  * Return `undefined` or `null` to fallback to default
43
43
  *
44
- * @default DEFAULT_ERROR_STATUS, DEFAULT_ERROR_STATUS
44
+ * @default DEFAULT_SUCCESS_STATUS (200)
45
45
  */
46
46
  outputStatus?: Value<number | undefined | null, [output: unknown, procedure: AnyProcedure, path: string[], options: StandardHandlerHandleOptions<T>]>;
47
47
  /**
48
48
  * Mapping ORPCError Code -> HTTP Status Code
49
+ * The status code should be in the `4xx` or `5xx` range (must be greater than or equal to `400`).
49
50
  *
50
51
  * @default COMMON_ERROR_STATUS_MAP
51
52
  */
@@ -38,14 +38,15 @@ interface RPCHandlerCodecOptions<T extends Context> extends RPCMatcherOptions {
38
38
  /**
39
39
  * Resolve HTTP status for encoded successful outputs.
40
40
  *
41
- * Value should be in range `200-299`
41
+ * Value should be in the `2xx` range and must be less than `400`.
42
42
  * Return `undefined` or `null` to fallback to default
43
43
  *
44
- * @default DEFAULT_ERROR_STATUS, DEFAULT_ERROR_STATUS
44
+ * @default DEFAULT_SUCCESS_STATUS (200)
45
45
  */
46
46
  outputStatus?: Value<number | undefined | null, [output: unknown, procedure: AnyProcedure, path: string[], options: StandardHandlerHandleOptions<T>]>;
47
47
  /**
48
48
  * Mapping ORPCError Code -> HTTP Status Code
49
+ * The status code should be in the `4xx` or `5xx` range (must be greater than or equal to `400`).
49
50
  *
50
51
  * @default COMMON_ERROR_STATUS_MAP
51
52
  */
@@ -1,8 +1,8 @@
1
1
  import { ORPCError, toORPCError, RPCSerializer, COMMON_ERROR_STATUS_MAP } from '@orpc/client';
2
2
  import { sortPlugins, getOpenTelemetryConfig, runWithSpan, toArray, matchesHttpPathPrefix, intercept, ORPC_NAME, isAsyncIteratorObject, override, traceAsyncIterator, recordSpanError, pathToHttpPath, normalizeHttpPath, value, parseEmptyableJSON } from '@orpc/shared';
3
3
  import { flattenStandardHeader, parseStandardUrl } from '@standardserver/core';
4
- import { c as createProcedureClient, u as unlazy, P as Procedure } from './server.CrlKQucM.mjs';
5
- import { w as walkProcedureContractsSync, g as getRouter, c as createContractProcedure, D as DEFAULT_SUCCESS_STATUS, a as DEFAULT_ERROR_STATUS } from './server.BwHnWUuN.mjs';
4
+ import { c as createProcedureClient, u as unlazy, P as Procedure } from './server.BDeup7Ky.mjs';
5
+ import { w as walkProcedureContractsSync, g as getRouter, c as createContractProcedure, D as DEFAULT_SUCCESS_STATUS, a as DEFAULT_ERROR_STATUS } from './server.C_osSBcd.mjs';
6
6
 
7
7
  class CompositeStandardHandlerPlugin {
8
8
  name = "~composite";
@@ -64,7 +64,7 @@ class StandardHandler {
64
64
  let input = await runWithSpan("decode_input", decodeInput2);
65
65
  step = void 0;
66
66
  if (isAsyncIteratorObject(input)) {
67
- input = override(input, traceAsyncIterator("consume_event_iterator_input", input));
67
+ input = override(input, traceAsyncIterator("consume_async_iterator_object_input", input));
68
68
  }
69
69
  const client = createProcedureClient(procedure2, {
70
70
  context: context3,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@orpc/server",
3
3
  "type": "module",
4
- "version": "2.0.0-beta.14",
4
+ "version": "2.0.0-beta.16",
5
5
  "license": "MIT",
6
6
  "homepage": "https://orpc.dev",
7
7
  "repository": {
@@ -85,15 +85,14 @@
85
85
  }
86
86
  },
87
87
  "dependencies": {
88
- "@standardserver/core": "^0.0.25",
89
- "@standardserver/fetch": "^0.0.25",
90
- "@standardserver/node": "^0.0.25",
91
- "@standardserver/peer": "^0.0.25",
92
- "cookie": "^1.1.1",
93
- "@orpc/client": "2.0.0-beta.14",
94
- "@orpc/shared": "2.0.0-beta.14",
95
- "@orpc/contract": "2.0.0-beta.14",
96
- "@orpc/interop": "2.0.0-beta.14"
88
+ "@standardserver/core": "^0.2.0",
89
+ "@standardserver/fetch": "^0.2.0",
90
+ "@standardserver/node": "^0.2.0",
91
+ "@standardserver/peer": "^0.2.0",
92
+ "cookie": "^2.0.1",
93
+ "@orpc/client": "2.0.0-beta.16",
94
+ "@orpc/shared": "2.0.0-beta.16",
95
+ "@orpc/contract": "2.0.0-beta.16"
97
96
  },
98
97
  "devDependencies": {
99
98
  "crossws": "^0.4.6",