@atproto/lex-client 0.0.4 → 0.0.5

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 (105) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/agent.d.ts +9 -8
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js.map +1 -1
  5. package/dist/client.d.ts +32 -96
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/client.js +31 -31
  8. package/dist/client.js.map +1 -1
  9. package/dist/index.d.ts +0 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +0 -2
  12. package/dist/index.js.map +1 -1
  13. package/dist/lexicons/com/atproto/repo/createRecord.defs.d.ts +7 -7
  14. package/dist/lexicons/com/atproto/repo/createRecord.defs.d.ts.map +1 -1
  15. package/dist/lexicons/com/atproto/repo/createRecord.defs.js +3 -5
  16. package/dist/lexicons/com/atproto/repo/createRecord.defs.js.map +1 -1
  17. package/dist/lexicons/com/atproto/repo/deleteRecord.defs.d.ts +7 -7
  18. package/dist/lexicons/com/atproto/repo/deleteRecord.defs.d.ts.map +1 -1
  19. package/dist/lexicons/com/atproto/repo/deleteRecord.defs.js +3 -5
  20. package/dist/lexicons/com/atproto/repo/deleteRecord.defs.js.map +1 -1
  21. package/dist/lexicons/com/atproto/repo/getRecord.defs.d.ts +5 -6
  22. package/dist/lexicons/com/atproto/repo/getRecord.defs.d.ts.map +1 -1
  23. package/dist/lexicons/com/atproto/repo/getRecord.defs.js +3 -5
  24. package/dist/lexicons/com/atproto/repo/getRecord.defs.js.map +1 -1
  25. package/dist/lexicons/com/atproto/repo/listRecords.defs.d.ts +5 -6
  26. package/dist/lexicons/com/atproto/repo/listRecords.defs.d.ts.map +1 -1
  27. package/dist/lexicons/com/atproto/repo/listRecords.defs.js +3 -5
  28. package/dist/lexicons/com/atproto/repo/listRecords.defs.js.map +1 -1
  29. package/dist/lexicons/com/atproto/repo/putRecord.defs.d.ts +7 -7
  30. package/dist/lexicons/com/atproto/repo/putRecord.defs.d.ts.map +1 -1
  31. package/dist/lexicons/com/atproto/repo/putRecord.defs.js +3 -5
  32. package/dist/lexicons/com/atproto/repo/putRecord.defs.js.map +1 -1
  33. package/dist/lexicons/com/atproto/repo/uploadBlob.defs.d.ts +7 -7
  34. package/dist/lexicons/com/atproto/repo/uploadBlob.defs.d.ts.map +1 -1
  35. package/dist/lexicons/com/atproto/repo/uploadBlob.defs.js +3 -5
  36. package/dist/lexicons/com/atproto/repo/uploadBlob.defs.js.map +1 -1
  37. package/dist/lexicons/com/atproto/sync/getBlob.d.ts +3 -0
  38. package/dist/lexicons/com/atproto/sync/getBlob.d.ts.map +1 -0
  39. package/dist/lexicons/com/atproto/sync/getBlob.defs.d.ts +25 -0
  40. package/dist/lexicons/com/atproto/sync/getBlob.defs.d.ts.map +1 -0
  41. package/dist/lexicons/com/atproto/sync/getBlob.defs.js +27 -0
  42. package/dist/lexicons/com/atproto/sync/getBlob.defs.js.map +1 -0
  43. package/dist/lexicons/com/atproto/sync/getBlob.js +10 -0
  44. package/dist/lexicons/com/atproto/sync/getBlob.js.map +1 -0
  45. package/dist/lexicons/com/atproto/sync.d.ts +2 -0
  46. package/dist/lexicons/com/atproto/sync.d.ts.map +1 -0
  47. package/dist/lexicons/com/atproto/sync.js +9 -0
  48. package/dist/lexicons/com/atproto/sync.js.map +1 -0
  49. package/dist/lexicons/com/atproto.d.ts +1 -0
  50. package/dist/lexicons/com/atproto.d.ts.map +1 -1
  51. package/dist/lexicons/com/atproto.js +2 -1
  52. package/dist/lexicons/com/atproto.js.map +1 -1
  53. package/dist/lexicons.d.ts +2 -0
  54. package/dist/lexicons.d.ts.map +1 -0
  55. package/dist/lexicons.js +6 -0
  56. package/dist/lexicons.js.map +1 -0
  57. package/dist/types.d.ts +18 -0
  58. package/dist/types.d.ts.map +1 -1
  59. package/dist/types.js.map +1 -1
  60. package/dist/util.d.ts +14 -0
  61. package/dist/util.d.ts.map +1 -0
  62. package/dist/util.js +65 -0
  63. package/dist/util.js.map +1 -0
  64. package/dist/xrpc-error.d.ts +87 -0
  65. package/dist/xrpc-error.d.ts.map +1 -0
  66. package/dist/xrpc-error.js +127 -0
  67. package/dist/xrpc-error.js.map +1 -0
  68. package/dist/xrpc-response.d.ts +35 -0
  69. package/dist/xrpc-response.d.ts.map +1 -0
  70. package/dist/xrpc-response.js +140 -0
  71. package/dist/xrpc-response.js.map +1 -0
  72. package/dist/xrpc.d.ts +29 -32
  73. package/dist/xrpc.d.ts.map +1 -1
  74. package/dist/xrpc.js +119 -125
  75. package/dist/xrpc.js.map +1 -1
  76. package/package.json +6 -6
  77. package/src/agent.ts +12 -12
  78. package/src/client.ts +92 -77
  79. package/src/index.ts +0 -2
  80. package/src/lexicons/com/atproto/repo/createRecord.defs.ts +9 -8
  81. package/src/lexicons/com/atproto/repo/deleteRecord.defs.ts +9 -8
  82. package/src/lexicons/com/atproto/repo/getRecord.defs.ts +7 -7
  83. package/src/lexicons/com/atproto/repo/listRecords.defs.ts +7 -6
  84. package/src/lexicons/com/atproto/repo/putRecord.defs.ts +9 -8
  85. package/src/lexicons/com/atproto/repo/uploadBlob.defs.ts +9 -8
  86. package/src/lexicons/com/atproto/sync/getBlob.defs.ts +37 -0
  87. package/src/lexicons/com/atproto/sync/getBlob.ts +6 -0
  88. package/src/lexicons/com/atproto/sync.ts +5 -0
  89. package/src/lexicons/com/atproto.ts +1 -0
  90. package/src/lexicons.ts +1 -0
  91. package/src/types.ts +27 -0
  92. package/src/util.ts +84 -0
  93. package/src/xrpc-error.ts +195 -0
  94. package/src/xrpc-response.ts +213 -0
  95. package/src/xrpc.ts +209 -220
  96. package/dist/error.d.ts +0 -66
  97. package/dist/error.d.ts.map +0 -1
  98. package/dist/error.js +0 -100
  99. package/dist/error.js.map +0 -1
  100. package/dist/response.d.ts +0 -21
  101. package/dist/response.d.ts.map +0 -1
  102. package/dist/response.js +0 -31
  103. package/dist/response.js.map +0 -1
  104. package/src/error.ts +0 -145
  105. package/src/response.ts +0 -42
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.XrpcResponse = void 0;
4
+ const lex_json_1 = require("@atproto/lex-json");
5
+ const xrpc_error_js_1 = require("./xrpc-error.js");
6
+ /**
7
+ * Small container for XRPC response data.
8
+ *
9
+ * @implements {ResultSuccess<XrpcResponse<M>>} for convenience in result handling contexts.
10
+ */
11
+ class XrpcResponse {
12
+ method;
13
+ status;
14
+ headers;
15
+ payload;
16
+ /** @see {@link ResultSuccess.success} */
17
+ success = true;
18
+ /** @see {@link ResultSuccess.value} */
19
+ get value() {
20
+ return this;
21
+ }
22
+ constructor(method, status, headers, payload) {
23
+ this.method = method;
24
+ this.status = status;
25
+ this.headers = headers;
26
+ this.payload = payload;
27
+ }
28
+ /**
29
+ * Whether the response payload was parsed as {@link LexValue} (`true`) or is
30
+ * in binary form {@link Uint8Array} (`false`).
31
+ */
32
+ get isParsed() {
33
+ return this.encoding === 'application/json' && shouldParse(this.method);
34
+ }
35
+ get encoding() {
36
+ return this.payload?.encoding;
37
+ }
38
+ get body() {
39
+ return this.payload?.body;
40
+ }
41
+ /**
42
+ * @throws {XrpcInvalidResponseError} when the response is invalid according
43
+ * to the method schema.
44
+ */
45
+ static async fromFetchResponse(method, response, options) {
46
+ // @NOTE The body MUST either be read or canceled to avoid resource leaks.
47
+ // Since nothing should cause an exception before "readPayload" is
48
+ // called, we can safely not use a try/finally here.
49
+ // @NOTE redirect is set to 'follow', so we shouldn't get 3xx responses here
50
+ if (response.status < 200 || response.status >= 300) {
51
+ // Always parse json for error responses
52
+ const payload = await readPayload(response, { parse: true });
53
+ if (response.status >= 400 && (0, xrpc_error_js_1.isXrpcErrorPayload)(payload)) {
54
+ throw new xrpc_error_js_1.XrpcResponseError(method, response.status, response.headers, payload);
55
+ }
56
+ throw new xrpc_error_js_1.XrpcInvalidResponseError(response.status >= 500
57
+ ? `Upstream server encountered an error`
58
+ : response.status >= 400
59
+ ? `Upstream server returned an invalid response payload`
60
+ : `Upstream server returned an invalid status code`, response, payload);
61
+ }
62
+ // Only parse json if the schema expects it
63
+ const payload = await readPayload(response, {
64
+ parse: shouldParse(method),
65
+ });
66
+ // Response is successful (2xx). Validate payload (data and encoding) against schema.
67
+ if (method.output.encoding == null) {
68
+ // Schema expects no payload
69
+ if (payload) {
70
+ throw new xrpc_error_js_1.XrpcInvalidResponseError(`Expected response with no body, got ${payload.encoding}`, response, payload);
71
+ }
72
+ }
73
+ else {
74
+ // Schema expects a payload
75
+ if (!payload || !method.output.matchesEncoding(payload.encoding)) {
76
+ throw new xrpc_error_js_1.XrpcInvalidResponseError(payload
77
+ ? `Expected ${method.output.encoding} response, got ${payload.encoding}`
78
+ : `Expected non-empty response with content-type ${method.output.encoding}`, response, payload);
79
+ }
80
+ // Assert valid response body.
81
+ if (method.output.schema && options?.validateResponse !== false) {
82
+ const result = method.output.schema.safeParse(payload.body, {
83
+ allowTransform: false,
84
+ });
85
+ if (!result.success) {
86
+ throw new xrpc_error_js_1.XrpcInvalidResponseError(`Response validation failed: ${result.reason.message}`, response, payload, { cause: result.reason });
87
+ }
88
+ }
89
+ }
90
+ return new XrpcResponse(method, response.status, response.headers, payload);
91
+ }
92
+ }
93
+ exports.XrpcResponse = XrpcResponse;
94
+ function shouldParse(method) {
95
+ return method.output.encoding === 'application/json';
96
+ }
97
+ /**
98
+ * @note this function always consumes the response body
99
+ */
100
+ async function readPayload(response, options) {
101
+ // @TODO Should we limit the maximum response size here (this could also be
102
+ // done by the FetchHandler)?
103
+ const encoding = response.headers
104
+ .get('content-type')
105
+ ?.split(';')[0]
106
+ .trim()
107
+ .toLowerCase();
108
+ // Response content-type is undefined
109
+ if (!encoding) {
110
+ // If the body is empty, return null (= no payload)
111
+ const body = await response.arrayBuffer();
112
+ if (body.byteLength === 0)
113
+ return null;
114
+ // If we got data despite no content-type, treat it as binary
115
+ return {
116
+ encoding: 'application/octet-stream',
117
+ body: new Uint8Array(body),
118
+ };
119
+ }
120
+ if (options?.parse && encoding === 'application/json') {
121
+ // @NOTE It might be worth returning the raw bytes here (Uint8Array) and
122
+ // perform the lex parsing using cborg/json, allowing to do
123
+ // bytes->LexValue in one step instead of bytes->text->JSON->LexValue.
124
+ // This would require adding encode/decode utilities to lex-json (similar
125
+ // to @ipld/dag-json)
126
+ const text = await response.text();
127
+ try {
128
+ // @NOTE Using `lexParse(text)` (instead of `jsonToLex(json)`) here as
129
+ // using a reviver function during JSON.parse should be faster than
130
+ // parsing to JSON then converting to Lex (?)
131
+ // @TODO verify statement above
132
+ return { encoding, body: (0, lex_json_1.lexParse)(text) };
133
+ }
134
+ catch (cause) {
135
+ throw new xrpc_error_js_1.XrpcInvalidResponseError('Invalid JSON response body', response, null, { cause });
136
+ }
137
+ }
138
+ return { encoding, body: new Uint8Array(await response.arrayBuffer()) };
139
+ }
140
+ //# sourceMappingURL=xrpc-response.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xrpc-response.js","sourceRoot":"","sources":["../src/xrpc-response.ts"],"names":[],"mappings":";;;AAAA,gDAA4C;AAS5C,mDAIwB;AAUxB;;;;GAIG;AACH,MAAa,YAAY;IAYZ;IACA;IACA;IACA;IAZX,yCAAyC;IAChC,OAAO,GAAG,IAAa,CAAA;IAEhC,uCAAuC;IACvC,IAAI,KAAK;QACP,OAAO,IAAI,CAAA;IACb,CAAC;IAED,YACW,MAAS,EACT,MAAc,EACd,OAAgB,EAChB,OAA+B;QAH/B,WAAM,GAAN,MAAM,CAAG;QACT,WAAM,GAAN,MAAM,CAAQ;QACd,YAAO,GAAP,OAAO,CAAS;QAChB,YAAO,GAAP,OAAO,CAAwB;IACvC,CAAC;IAEJ;;;OAGG;IACH,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,QAAQ,KAAK,kBAAkB,IAAI,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IACzE,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,OAAO,EAAE,QAAwC,CAAA;IAC/D,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,OAAO,EAAE,IAA2B,CAAA;IAClD,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAC5B,MAAS,EACT,QAAkB,EAClB,OAAwC;QAExC,0EAA0E;QAC1E,kEAAkE;QAClE,oDAAoD;QAEpD,4EAA4E;QAC5E,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;YACpD,wCAAwC;YACxC,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;YAE5D,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,IAAI,IAAA,kCAAkB,EAAC,OAAO,CAAC,EAAE,CAAC;gBAC1D,MAAM,IAAI,iCAAiB,CACzB,MAAM,EACN,QAAQ,CAAC,MAAM,EACf,QAAQ,CAAC,OAAO,EAChB,OAAO,CACR,CAAA;YACH,CAAC;YAED,MAAM,IAAI,wCAAwB,CAChC,QAAQ,CAAC,MAAM,IAAI,GAAG;gBACpB,CAAC,CAAC,sCAAsC;gBACxC,CAAC,CAAC,QAAQ,CAAC,MAAM,IAAI,GAAG;oBACtB,CAAC,CAAC,sDAAsD;oBACxD,CAAC,CAAC,iDAAiD,EACvD,QAAQ,EACR,OAAO,CACR,CAAA;QACH,CAAC;QAED,2CAA2C;QAC3C,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,QAAQ,EAAE;YAC1C,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC;SAC3B,CAAC,CAAA;QAEF,qFAAqF;QACrF,IAAI,MAAM,CAAC,MAAM,CAAC,QAAQ,IAAI,IAAI,EAAE,CAAC;YACnC,4BAA4B;YAC5B,IAAI,OAAO,EAAE,CAAC;gBACZ,MAAM,IAAI,wCAAwB,CAChC,uCAAuC,OAAO,CAAC,QAAQ,EAAE,EACzD,QAAQ,EACR,OAAO,CACR,CAAA;YACH,CAAC;QACH,CAAC;aAAM,CAAC;YACN,2BAA2B;YAC3B,IAAI,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACjE,MAAM,IAAI,wCAAwB,CAChC,OAAO;oBACL,CAAC,CAAC,YAAY,MAAM,CAAC,MAAM,CAAC,QAAQ,kBAAkB,OAAO,CAAC,QAAQ,EAAE;oBACxE,CAAC,CAAC,iDAAiD,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,EAC7E,QAAQ,EACR,OAAO,CACR,CAAA;YACH,CAAC;YAED,8BAA8B;YAC9B,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,OAAO,EAAE,gBAAgB,KAAK,KAAK,EAAE,CAAC;gBAChE,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,EAAE;oBAC1D,cAAc,EAAE,KAAK;iBACtB,CAAC,CAAA;gBAEF,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;oBACpB,MAAM,IAAI,wCAAwB,CAChC,+BAA+B,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,EACtD,QAAQ,EACR,OAAO,EACP,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,CACzB,CAAA;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,IAAI,YAAY,CACrB,MAAM,EACN,QAAQ,CAAC,MAAM,EACf,QAAQ,CAAC,OAAO,EAChB,OAAiC,CAClC,CAAA;IACH,CAAC;CACF;AA3HD,oCA2HC;AAED,SAAS,WAAW,CAAC,MAAyB;IAC5C,OAAO,MAAM,CAAC,MAAM,CAAC,QAAQ,KAAK,kBAAkB,CAAA;AACtD,CAAC;AAED;;GAEG;AACH,KAAK,UAAU,WAAW,CACxB,QAAkB,EAClB,OAA6B;IAE7B,2EAA2E;IAC3E,6BAA6B;IAE7B,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO;SAC9B,GAAG,CAAC,cAAc,CAAC;QACpB,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;SACd,IAAI,EAAE;SACN,WAAW,EAAE,CAAA;IAEhB,qCAAqC;IACrC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,mDAAmD;QACnD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAA;QACzC,IAAI,IAAI,CAAC,UAAU,KAAK,CAAC;YAAE,OAAO,IAAI,CAAA;QAEtC,6DAA6D;QAC7D,OAAO;YACL,QAAQ,EAAE,0BAA0B;YACpC,IAAI,EAAE,IAAI,UAAU,CAAC,IAAI,CAAC;SAC3B,CAAA;IACH,CAAC;IAED,IAAI,OAAO,EAAE,KAAK,IAAI,QAAQ,KAAK,kBAAkB,EAAE,CAAC;QACtD,wEAAwE;QACxE,2DAA2D;QAC3D,sEAAsE;QACtE,yEAAyE;QACzE,qBAAqB;QACrB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;QAElC,IAAI,CAAC;YACH,sEAAsE;YACtE,mEAAmE;YACnE,6CAA6C;YAE7C,+BAA+B;YAC/B,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAA,mBAAQ,EAAC,IAAI,CAAC,EAAE,CAAA;QAC3C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,wCAAwB,CAChC,4BAA4B,EAC5B,QAAQ,EACR,IAAI,EACJ,EAAE,KAAK,EAAE,CACV,CAAA;QACH,CAAC;IACH,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,UAAU,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC,EAAE,CAAA;AACzE,CAAC","sourcesContent":["import { lexParse } from '@atproto/lex-json'\nimport {\n InferMethodOutputBody,\n InferMethodOutputEncoding,\n Procedure,\n Query,\n ResultSuccess,\n} from '@atproto/lex-schema'\nimport { Payload } from './util.js'\nimport {\n XrpcInvalidResponseError,\n XrpcResponseError,\n isXrpcErrorPayload,\n} from './xrpc-error.js'\n\nexport type XrpcResponseBody<M extends Procedure | Query> =\n InferMethodOutputBody<M, Uint8Array>\n\nexport type XrpcResponsePayload<M extends Procedure | Query> =\n InferMethodOutputEncoding<M> extends infer E extends string\n ? Payload<XrpcResponseBody<M>, E>\n : null\n\n/**\n * Small container for XRPC response data.\n *\n * @implements {ResultSuccess<XrpcResponse<M>>} for convenience in result handling contexts.\n */\nexport class XrpcResponse<const M extends Procedure | Query>\n implements ResultSuccess<XrpcResponse<M>>\n{\n /** @see {@link ResultSuccess.success} */\n readonly success = true as const\n\n /** @see {@link ResultSuccess.value} */\n get value(): this {\n return this\n }\n\n constructor(\n readonly method: M,\n readonly status: number,\n readonly headers: Headers,\n readonly payload: XrpcResponsePayload<M>,\n ) {}\n\n /**\n * Whether the response payload was parsed as {@link LexValue} (`true`) or is\n * in binary form {@link Uint8Array} (`false`).\n */\n get isParsed() {\n return this.encoding === 'application/json' && shouldParse(this.method)\n }\n\n get encoding() {\n return this.payload?.encoding as InferMethodOutputEncoding<M>\n }\n\n get body() {\n return this.payload?.body as XrpcResponseBody<M>\n }\n\n /**\n * @throws {XrpcInvalidResponseError} when the response is invalid according\n * to the method schema.\n */\n static async fromFetchResponse<const M extends Procedure | Query>(\n method: M,\n response: Response,\n options?: { validateResponse?: boolean },\n ): Promise<XrpcResponse<M>> {\n // @NOTE The body MUST either be read or canceled to avoid resource leaks.\n // Since nothing should cause an exception before \"readPayload\" is\n // called, we can safely not use a try/finally here.\n\n // @NOTE redirect is set to 'follow', so we shouldn't get 3xx responses here\n if (response.status < 200 || response.status >= 300) {\n // Always parse json for error responses\n const payload = await readPayload(response, { parse: true })\n\n if (response.status >= 400 && isXrpcErrorPayload(payload)) {\n throw new XrpcResponseError(\n method,\n response.status,\n response.headers,\n payload,\n )\n }\n\n throw new XrpcInvalidResponseError(\n response.status >= 500\n ? `Upstream server encountered an error`\n : response.status >= 400\n ? `Upstream server returned an invalid response payload`\n : `Upstream server returned an invalid status code`,\n response,\n payload,\n )\n }\n\n // Only parse json if the schema expects it\n const payload = await readPayload(response, {\n parse: shouldParse(method),\n })\n\n // Response is successful (2xx). Validate payload (data and encoding) against schema.\n if (method.output.encoding == null) {\n // Schema expects no payload\n if (payload) {\n throw new XrpcInvalidResponseError(\n `Expected response with no body, got ${payload.encoding}`,\n response,\n payload,\n )\n }\n } else {\n // Schema expects a payload\n if (!payload || !method.output.matchesEncoding(payload.encoding)) {\n throw new XrpcInvalidResponseError(\n payload\n ? `Expected ${method.output.encoding} response, got ${payload.encoding}`\n : `Expected non-empty response with content-type ${method.output.encoding}`,\n response,\n payload,\n )\n }\n\n // Assert valid response body.\n if (method.output.schema && options?.validateResponse !== false) {\n const result = method.output.schema.safeParse(payload.body, {\n allowTransform: false,\n })\n\n if (!result.success) {\n throw new XrpcInvalidResponseError(\n `Response validation failed: ${result.reason.message}`,\n response,\n payload,\n { cause: result.reason },\n )\n }\n }\n }\n\n return new XrpcResponse<M>(\n method,\n response.status,\n response.headers,\n payload as XrpcResponsePayload<M>,\n )\n }\n}\n\nfunction shouldParse(method: Procedure | Query) {\n return method.output.encoding === 'application/json'\n}\n\n/**\n * @note this function always consumes the response body\n */\nasync function readPayload(\n response: Response,\n options?: { parse?: boolean },\n): Promise<Payload | null> {\n // @TODO Should we limit the maximum response size here (this could also be\n // done by the FetchHandler)?\n\n const encoding = response.headers\n .get('content-type')\n ?.split(';')[0]\n .trim()\n .toLowerCase()\n\n // Response content-type is undefined\n if (!encoding) {\n // If the body is empty, return null (= no payload)\n const body = await response.arrayBuffer()\n if (body.byteLength === 0) return null\n\n // If we got data despite no content-type, treat it as binary\n return {\n encoding: 'application/octet-stream',\n body: new Uint8Array(body),\n }\n }\n\n if (options?.parse && encoding === 'application/json') {\n // @NOTE It might be worth returning the raw bytes here (Uint8Array) and\n // perform the lex parsing using cborg/json, allowing to do\n // bytes->LexValue in one step instead of bytes->text->JSON->LexValue.\n // This would require adding encode/decode utilities to lex-json (similar\n // to @ipld/dag-json)\n const text = await response.text()\n\n try {\n // @NOTE Using `lexParse(text)` (instead of `jsonToLex(json)`) here as\n // using a reviver function during JSON.parse should be faster than\n // parsing to JSON then converting to Lex (?)\n\n // @TODO verify statement above\n return { encoding, body: lexParse(text) }\n } catch (cause) {\n throw new XrpcInvalidResponseError(\n 'Invalid JSON response body',\n response,\n null,\n { cause },\n )\n }\n }\n\n return { encoding, body: new Uint8Array(await response.arrayBuffer()) }\n}\n"]}
package/dist/xrpc.d.ts CHANGED
@@ -1,37 +1,34 @@
1
- import { LexValue } from '@atproto/lex-data';
2
- import { DidString, InferParamsSchema, InferPayloadBody, Params, ParamsSchema, Procedure, Query, Restricted, Subscription } from '@atproto/lex-schema';
1
+ import { InferMethodInput, InferMethodParams, Params, Procedure, Query, Restricted } from '@atproto/lex-schema';
3
2
  import { Agent } from './agent.js';
4
- import { XrpcResponse } from './response.js';
5
- import { CallOptions, Namespace, Service } from './types.js';
6
- export type XrpcOptions<M extends Procedure | Query = Procedure | Query> = CallOptions & XrpcRequestUrlOptions<M> & XrpcRequestInitOptions<M>;
7
- export declare function xrpc<const M extends Query | Procedure>(agent: Agent, ns: NonNullable<unknown> extends XrpcOptions<M> ? Namespace<M> : Restricted<'This XRPC method requires an "options" argument'>): Promise<XrpcResponse<M>>;
8
- export declare function xrpc<const M extends Query | Procedure>(agent: Agent, ns: Namespace<M>, options: XrpcOptions<M>): Promise<XrpcResponse<M>>;
9
- export type XrpcRequestUrlOptions<M extends Query | Procedure | Subscription> = CallOptions & (undefined extends InferParamsSchema<M['parameters']> ? {
10
- params?: InferParamsSchema<M['parameters']>;
11
- } : {
12
- params: InferParamsSchema<M['parameters']>;
13
- });
14
- export declare function xrpcRequestUrl<M extends Procedure | Query | Subscription>(method: M, options: XrpcRequestUrlOptions<M>): string;
15
- export declare function xrpcRequestParams(schema: ParamsSchema | undefined, params: Params | undefined, options: CallOptions): undefined | string;
16
- export type XrpcRequestInitOptions<T extends Query | Procedure> = CallOptions & (T extends Procedure ? never extends InferPayloadBody<T['input']> ? {
17
- body?: InferPayloadBody<T['input']>;
3
+ import { BinaryBodyInit, CallOptions, Namespace } from './types.js';
4
+ import { XrpcInvalidResponseError, XrpcResponseError, XrpcUnexpectedError } from './xrpc-error.js';
5
+ import { XrpcResponse } from './xrpc-response.js';
6
+ export * from './xrpc-error.js';
7
+ export * from './xrpc-response.js';
8
+ type XrpcParamsOptions<P extends Params> = NonNullable<unknown> extends P ? {
9
+ params?: P;
18
10
  } : {
19
- body: InferPayloadBody<T['input']>;
11
+ params: P;
12
+ };
13
+ type XrpcRequestPayload<M extends Procedure | Query> = InferMethodInput<M, BinaryBodyInit>;
14
+ type XrpcInputOptions<In> = In extends {
15
+ body: infer B;
16
+ encoding: infer E;
17
+ } ? {
18
+ body: B;
19
+ encoding?: E;
20
20
  } : {
21
- body?: never;
22
- });
23
- export declare function xrpcRequestInit<T extends Procedure | Query>(schema: T, options: XrpcRequestInitOptions<T>): RequestInit & {
24
- duplex?: 'half';
21
+ body?: undefined;
22
+ encoding?: undefined;
25
23
  };
26
- export declare function xrpcRequestHeaders(options: {
27
- headers?: HeadersInit;
28
- service?: Service;
29
- labelers?: Iterable<DidString>;
30
- }): Headers;
31
- export declare function xrpcResponseHandler<M extends Procedure | Query>(response: Response, schema: M, options?: {
32
- validateResponse?: boolean;
33
- }): Promise<XrpcResponse<M>>;
34
- export declare function extractEncoding(headers: Headers): string | undefined;
35
- export declare function readResponseBody(response: Response, encoding: string): Promise<LexValue>;
36
- export declare function readResponseBody(response: Response, encoding: string | undefined): Promise<LexValue | undefined>;
24
+ export type XrpcOptions<M extends Procedure | Query = Procedure | Query> = CallOptions & XrpcInputOptions<XrpcRequestPayload<M>> & XrpcParamsOptions<InferMethodParams<M>>;
25
+ export type XrpcFailure<M extends Procedure | Query> = XrpcResponseError<M> | XrpcInvalidResponseError | XrpcUnexpectedError;
26
+ export type XrpcResult<M extends Procedure | Query> = XrpcResponse<M> | XrpcFailure<M>;
27
+ /**
28
+ * @throws XrpcFailure<M>
29
+ */
30
+ export declare function xrpc<const M extends Query | Procedure>(agent: Agent, ns: NonNullable<unknown> extends XrpcOptions<M> ? Namespace<M> : Restricted<'This XRPC method requires an "options" argument'>): Promise<XrpcResponse<M>>;
31
+ export declare function xrpc<const M extends Query | Procedure>(agent: Agent, ns: Namespace<M>, options: XrpcOptions<M>): Promise<XrpcResponse<M>>;
32
+ export declare function xrpcSafe<const M extends Query | Procedure>(agent: Agent, ns: NonNullable<unknown> extends XrpcOptions<M> ? Namespace<M> : Restricted<'This XRPC method requires an "options" argument'>): Promise<XrpcResult<M>>;
33
+ export declare function xrpcSafe<const M extends Query | Procedure>(agent: Agent, ns: Namespace<M>, options: XrpcOptions<M>): Promise<XrpcResult<M>>;
37
34
  //# sourceMappingURL=xrpc.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"xrpc.d.ts","sourceRoot":"","sources":["../src/xrpc.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"xrpc.d.ts","sourceRoot":"","sources":["../src/xrpc.ts"],"names":[],"mappings":"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"}
package/dist/xrpc.js CHANGED
@@ -1,25 +1,47 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.xrpc = xrpc;
4
- exports.xrpcRequestUrl = xrpcRequestUrl;
5
- exports.xrpcRequestParams = xrpcRequestParams;
6
- exports.xrpcRequestInit = xrpcRequestInit;
7
- exports.xrpcRequestHeaders = xrpcRequestHeaders;
8
- exports.xrpcResponseHandler = xrpcResponseHandler;
9
- exports.extractEncoding = extractEncoding;
10
- exports.readResponseBody = readResponseBody;
4
+ exports.xrpcSafe = xrpcSafe;
5
+ const tslib_1 = require("tslib");
6
+ const lex_data_1 = require("@atproto/lex-data");
11
7
  const lex_json_1 = require("@atproto/lex-json");
12
- const lex_schema_1 = require("@atproto/lex-schema");
13
- const error_js_1 = require("./error.js");
14
- const response_js_1 = require("./response.js");
15
8
  const types_js_1 = require("./types.js");
9
+ const util_js_1 = require("./util.js");
10
+ const xrpc_error_js_1 = require("./xrpc-error.js");
11
+ const xrpc_response_js_1 = require("./xrpc-response.js");
12
+ tslib_1.__exportStar(require("./xrpc-error.js"), exports);
13
+ tslib_1.__exportStar(require("./xrpc-response.js"), exports);
14
+ /**
15
+ * Utility method to type cast the error thrown by {@link xrpc} to an
16
+ * {@link XrpcFailure} matching the provided method. Only use this function
17
+ * inside a catch block right after calling {@link xrpc}, and use the same
18
+ * method type parameter as used in the {@link xrpc} call.
19
+ */
20
+ function asXrpcFailure(err) {
21
+ if (err instanceof xrpc_error_js_1.XrpcResponseError)
22
+ return err;
23
+ if (err instanceof xrpc_error_js_1.XrpcInvalidResponseError)
24
+ return err;
25
+ return xrpc_error_js_1.XrpcUnexpectedError.from(err);
26
+ }
16
27
  async function xrpc(agent, ns, options = {}) {
17
- options.signal?.throwIfAborted();
28
+ try {
29
+ return await xrpcRequest(agent, ns, options);
30
+ }
31
+ catch (err) {
32
+ throw asXrpcFailure(err);
33
+ }
34
+ }
35
+ async function xrpcSafe(agent, ns, options = {}) {
36
+ return xrpcRequest(agent, ns, options).catch((asXrpcFailure));
37
+ }
38
+ async function xrpcRequest(agent, ns, options = {}) {
18
39
  const method = (0, types_js_1.getMain)(ns);
40
+ options.signal?.throwIfAborted();
19
41
  const url = xrpcRequestUrl(method, options);
20
42
  const request = xrpcRequestInit(method, options);
21
43
  const response = await agent.fetchHandler(url, request);
22
- return xrpcResponseHandler(response, method, options);
44
+ return xrpc_response_js_1.XrpcResponse.fromFetchResponse(method, response, options);
23
45
  }
24
46
  function xrpcRequestUrl(method, options) {
25
47
  const path = `/xrpc/${method.nsid}`;
@@ -33,15 +55,26 @@ function xrpcRequestParams(schema, params, options) {
33
55
  return urlSearchParams?.size ? urlSearchParams.toString() : undefined;
34
56
  }
35
57
  function xrpcRequestInit(schema, options) {
36
- const headers = xrpcRequestHeaders(options);
58
+ const headers = (0, util_js_1.buildAtprotoHeaders)(options);
59
+ // Tell the server what type of response we're expecting
60
+ if (schema.output.encoding) {
61
+ headers.set('accept', schema.output.encoding);
62
+ }
63
+ // Caller should not set content-type header
64
+ if (headers.has('content-type')) {
65
+ const contentType = headers.get('content-type');
66
+ throw new TypeError(`Unexpected content-type header (${contentType})`);
67
+ }
37
68
  // Requests with body
38
- if ('input' in schema && schema.input?.encoding) {
39
- if (options.validateRequest &&
40
- schema.input == null &&
41
- options.body !== undefined) {
42
- throw new TypeError(`XRPC method ${schema.nsid} does not accept a request body`);
69
+ if ('input' in schema) {
70
+ const encodingHint = options.encoding;
71
+ const input = xrpcProcedureInput(schema, options, encodingHint);
72
+ if (input) {
73
+ headers.set('content-type', input.encoding);
74
+ }
75
+ else if (encodingHint != null) {
76
+ throw new TypeError(`Unexpected encoding hint (${encodingHint})`);
43
77
  }
44
- headers.set('content-type', schema.input.encoding);
45
78
  return {
46
79
  duplex: 'half',
47
80
  redirect: 'follow',
@@ -50,9 +83,7 @@ function xrpcRequestInit(schema, options) {
50
83
  signal: options.signal,
51
84
  method: 'POST',
52
85
  headers,
53
- body: xrpcRequestBody(schema.input?.encoding, options.validateRequest
54
- ? schema.input?.body.parse(options.body)
55
- : options.body),
86
+ body: input?.body,
56
87
  };
57
88
  }
58
89
  // Requests without body
@@ -62,124 +93,87 @@ function xrpcRequestInit(schema, options) {
62
93
  referrerPolicy: 'strict-origin-when-cross-origin', // (default)
63
94
  mode: 'cors', // (default)
64
95
  signal: options.signal,
65
- method: schema instanceof lex_schema_1.Query ? 'GET' : 'POST',
96
+ method: 'GET',
66
97
  headers,
67
98
  };
68
99
  }
69
- function xrpcRequestHeaders(options) {
70
- const headers = new Headers(options.headers);
71
- if (options.service && !headers.has('atproto-proxy')) {
72
- headers.set('atproto-proxy', options.service);
73
- }
74
- if (options.labelers) {
75
- headers.set('atproto-accept-labelers', [...options.labelers, headers.get('atproto-accept-labelers')?.trim()]
76
- .filter(Boolean)
77
- .join(', '));
78
- }
79
- return headers;
80
- }
81
- function xrpcRequestBody(encoding, body) {
82
- if (encoding === undefined) {
83
- return null;
84
- }
85
- if (encoding === 'application/json') {
86
- if (body !== undefined)
87
- return (0, lex_json_1.lexStringify)(body);
88
- }
89
- else if (encoding.startsWith('text/')) {
90
- if (typeof body === 'string')
91
- return body;
92
- }
93
- else {
94
- if (ArrayBuffer.isView(body) || body instanceof ArrayBuffer)
95
- return body;
100
+ function xrpcProcedureInput(method, options, encodingHint) {
101
+ const { input } = method;
102
+ const { body } = options;
103
+ if (options.validateRequest) {
104
+ input.schema?.check(body);
96
105
  }
97
- throw new TypeError(`Invalid ${typeof body} body for ${encoding} encoding`);
98
- }
99
- async function xrpcResponseHandler(response, schema, options) {
100
- // @NOTE The body MUST either be read or canceled to avoid resource leaks.
101
- // Since nothing should cause an exception before "readXrpcResponseBody" is
102
- // called, we can safely not use a try/finally here.
103
- const encoding = extractEncoding(response.headers);
104
- const body = await readResponseBody(response, encoding).catch((cause) => {
105
- throw new error_js_1.XrpcServiceError(error_js_1.KnownError.InvalidResponse, response.status, response.headers, undefined, 'Failed to read XRPC response', { cause });
106
- });
107
- // @NOTE redirect is set to 'follow', so we shouldn't get 3xx responses here
108
- if (response.status < 200 || response.status >= 300) {
109
- // All unsuccessful responses should follow a standard error response
110
- // schema. The Content-Type should be application/json, and the payload
111
- // should be a JSON object with the following fields:
112
- // - error (string, required): type name of the error (generic ASCII
113
- // constant, no whitespace)
114
- // - message (string, optional): description of the error, appropriate for
115
- // display to humans
116
- if (body != null &&
117
- encoding === 'application/json' &&
118
- error_js_1.xrpcErrorBodySchema.matches(body)) {
119
- throw new error_js_1.XrpcResponseError(response.status, response.headers, encoding, body);
106
+ // Special handling for endpoints expecting application/json input
107
+ if (input.encoding === 'application/json') {
108
+ // @NOTE **NOT** using isLexValue here to avoid deep checks in order to
109
+ // distinguish between LexValue and BinaryBodyInit.
110
+ if (!(0, lex_data_1.isLexScalar)(body) && !(0, lex_data_1.isPlainObject)(body) && !Array.isArray(body)) {
111
+ throw new TypeError(`Expected LexValue body, got ${typeof body}`);
120
112
  }
121
- throw new error_js_1.XrpcServiceError(response.status >= 500
122
- ? error_js_1.KnownError.InternalServerError
123
- : error_js_1.KnownError.InvalidResponse, response.status, response.headers, body);
113
+ return buildPayload(input, (0, lex_json_1.lexStringify)(body), encodingHint);
124
114
  }
125
- // Check response encoding
126
- if (schema.output.encoding !== encoding) {
127
- throw new error_js_1.XrpcServiceError(error_js_1.KnownError.InvalidResponse, response.status, response.headers, body, `Expected response with content-type ${schema.output.encoding}, got ${encoding}`);
115
+ // Other encodings will be sent unaltered (ie. as binary data)
116
+ switch (typeof body) {
117
+ case 'undefined':
118
+ case 'string':
119
+ return buildPayload(input, body, encodingHint);
120
+ case 'object': {
121
+ if (body === null)
122
+ break;
123
+ if (ArrayBuffer.isView(body) ||
124
+ body instanceof ArrayBuffer ||
125
+ body instanceof ReadableStream) {
126
+ return buildPayload(input, body, encodingHint);
127
+ }
128
+ else if ((0, util_js_1.isAsyncIterable)(body)) {
129
+ return buildPayload(input, (0, util_js_1.toReadableStream)(body), encodingHint);
130
+ }
131
+ else if ((0, util_js_1.isBlobLike)(body)) {
132
+ return buildPayload(input, body, encodingHint || body.type);
133
+ }
134
+ }
128
135
  }
129
- if (schema.output.encoding == null) {
136
+ throw new TypeError(`Invalid ${typeof body} body for ${input.encoding} encoding`);
137
+ }
138
+ function buildPayload(schema, body, encodingHint) {
139
+ if (schema.encoding === undefined) {
130
140
  if (body !== undefined) {
131
- throw new error_js_1.XrpcServiceError(error_js_1.KnownError.InvalidResponse, response.status, response.headers, body, `Expected empty response body`);
141
+ throw new TypeError(`Cannot send a ${typeof body} body with undefined encoding`);
132
142
  }
133
- return new response_js_1.XrpcResponse(schema, response.status, response.headers, undefined);
143
+ return null;
134
144
  }
135
- else {
136
- // @NOTE this should already be enforced by readXrpcResponseBody
137
- if (body === undefined) {
138
- throw new error_js_1.XrpcServiceError(error_js_1.KnownError.InvalidResponse, response.status, response.headers, body, `Expected non-empty response body`);
139
- }
140
- return new response_js_1.XrpcResponse(schema, response.status, response.headers, schema.output.schema == null || options?.validateResponse === false
141
- ? body
142
- : schema.output.schema.parse(body));
145
+ if (body === undefined) {
146
+ // This error would be returned by the server, but we can catch it earlier
147
+ // to avoid un-necessary requests. Note that a content-length of 0 does not
148
+ // necessary mean that the body is "empty" (e.g. an empty txt file).
149
+ throw new TypeError(`A request body is expected but none was provided`);
143
150
  }
151
+ const encoding = buildEncoding(schema, encodingHint);
152
+ return { encoding, body };
144
153
  }
145
- function extractEncoding(headers) {
146
- const contentType = headers.get('content-type');
147
- if (!contentType)
148
- return undefined;
149
- return contentType.split(';')[0].trim();
150
- }
151
- async function readResponseBody(response, encoding) {
152
- // When encoding is undefined or empty, we expect no body
153
- if (encoding == null) {
154
- if (response.body == null)
155
- return undefined;
156
- // Let's make sure the body is empty (while avoiding reading it all).
157
- if (!('getReader' in response.body)) {
158
- // Some environments may not support body.getReader(), fall back to
159
- // reading the whole body.
160
- const buffer = await response.arrayBuffer();
161
- if (buffer.byteLength === 0)
162
- return undefined;
163
- }
164
- else {
165
- const reader = response.body.getReader();
166
- const next = await reader.read();
167
- if (next.done)
168
- return undefined;
169
- await reader.cancel(); // Drain the rest of the (non-empty) body stream
154
+ function buildEncoding(schema, encodingHint) {
155
+ // Should never happen (required for type safety)
156
+ if (!schema.encoding) {
157
+ throw new TypeError('Unexpected payload');
158
+ }
159
+ if (encodingHint?.length) {
160
+ if (!schema.matchesEncoding(encodingHint)) {
161
+ throw new TypeError(`Cannot send a body with content-type "${encodingHint}" for "${schema.encoding}" encoding`);
170
162
  }
171
- throw new SyntaxError('Content-type is undefined but body is not empty');
163
+ return encodingHint;
164
+ }
165
+ // Fallback
166
+ if (schema.encoding === '*/*') {
167
+ return 'application/octet-stream';
172
168
  }
173
- if (encoding === 'application/json') {
174
- // @NOTE Using `lexParse(text)` (instead of `jsonToLex(json)`) here as using
175
- // a reviver function during JSON.parse should be faster than parsing to
176
- // JSON then converting to Lex (?)
177
- // @TODO verify statement above
178
- return (0, lex_json_1.lexParse)(await response.text());
169
+ if (schema.encoding.startsWith('text/')) {
170
+ return schema.encoding.includes('*')
171
+ ? 'text/plain; charset=utf-8'
172
+ : `${schema.encoding}; charset=utf-8`;
179
173
  }
180
- if (encoding.startsWith('text/')) {
181
- return response.text();
174
+ if (!schema.encoding.includes('*')) {
175
+ return schema.encoding;
182
176
  }
183
- return new Uint8Array(await response.arrayBuffer());
177
+ throw new TypeError(`Unable to determine payload encoding. Please provide a 'content-type' header matching ${schema.encoding}.`);
184
178
  }
185
179
  //# sourceMappingURL=xrpc.js.map