accessio 1.1.2 → 1.3.0

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 (40) hide show
  1. package/README.md +109 -14
  2. package/cjs/accessio.cjs +59 -3
  3. package/cjs/accessio.cjs.map +1 -1
  4. package/cjs/core/accessioError.cjs +28 -3
  5. package/cjs/core/accessioError.cjs.map +1 -1
  6. package/cjs/core/buildURL.cjs +15 -1
  7. package/cjs/core/buildURL.cjs.map +1 -1
  8. package/cjs/core/fetchAdapter.cjs +54 -10
  9. package/cjs/core/fetchAdapter.cjs.map +1 -1
  10. package/cjs/core/request.cjs +107 -31
  11. package/cjs/core/request.cjs.map +1 -1
  12. package/cjs/core/retry.cjs +48 -4
  13. package/cjs/core/retry.cjs.map +1 -1
  14. package/cjs/helpers/flattenHeaders.cjs +37 -0
  15. package/cjs/helpers/flattenHeaders.cjs.map +1 -1
  16. package/cjs/helpers/memoryCache.cjs +51 -0
  17. package/cjs/helpers/memoryCache.cjs.map +1 -0
  18. package/cjs/helpers/rateLimiter.cjs +11 -22
  19. package/cjs/helpers/rateLimiter.cjs.map +1 -1
  20. package/cjs/helpers/toFormData.cjs +50 -0
  21. package/cjs/helpers/toFormData.cjs.map +1 -0
  22. package/cjs/helpers/transformData.cjs +2 -2
  23. package/cjs/helpers/transformData.cjs.map +1 -1
  24. package/cjs/index.cjs +4 -1
  25. package/cjs/index.cjs.map +1 -1
  26. package/index.d.ts +7 -0
  27. package/package.json +3 -2
  28. package/src/accessio.ts +81 -3
  29. package/src/core/accessioError.ts +26 -1
  30. package/src/core/buildURL.ts +16 -1
  31. package/src/core/fetchAdapter.ts +62 -10
  32. package/src/core/request.ts +134 -44
  33. package/src/core/retry.ts +44 -6
  34. package/src/helpers/flattenHeaders.ts +30 -0
  35. package/src/helpers/memoryCache.ts +30 -0
  36. package/src/helpers/rateLimiter.ts +11 -24
  37. package/src/helpers/toFormData.ts +25 -0
  38. package/src/helpers/transformData.ts +2 -1
  39. package/src/index.ts +4 -1
  40. package/src/types.ts +27 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/helpers/transformData.ts"],"sourcesContent":["import AccessioError from '../core/accessioError';\nimport type { TransformFunction, AccessioRequestConfig } from '../types';\n\nexport default async function transformData(\n transforms: TransformFunction | TransformFunction[] | undefined,\n data: unknown,\n headers: Record<string, string | string[]>,\n config?: AccessioRequestConfig,\n): Promise<unknown> {\n if (!transforms || !Array.isArray(transforms)) {\n return data;\n }\n\n let result = data;\n\n for (const transform of transforms) {\n if (typeof transform === 'function') {\n try {\n result = await transform(result, headers);\n } catch (err) {\n throw AccessioError.from(\n err instanceof Error ? err : new Error(String(err)),\n AccessioError.ERR_BAD_REQUEST,\n config ?? null,\n null,\n null,\n );\n }\n }\n }\n\n return result;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,2BAA0B;AAG1B,eAAO,cACL,YACA,MACA,SACA,QACkB;AAClB,MAAI,CAAC,cAAc,CAAC,MAAM,QAAQ,UAAU,GAAG;AAC7C,WAAO;AAAA,EACT;AAEA,MAAI,SAAS;AAEb,aAAW,aAAa,YAAY;AAClC,QAAI,OAAO,cAAc,YAAY;AACnC,UAAI;AACF,iBAAS,MAAM,UAAU,QAAQ,OAAO;AAAA,MAC1C,SAAS,KAAK;AACZ,cAAM,qBAAAA,QAAc;AAAA,UAClB,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;AAAA,UAClD,qBAAAA,QAAc;AAAA,UACd,UAAU;AAAA,UACV;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;","names":["AccessioError"]}
1
+ {"version":3,"sources":["../../src/helpers/transformData.ts"],"sourcesContent":["import AccessioError from '../core/accessioError';\nimport type { TransformFunction, AccessioRequestConfig } from '../types';\n\nexport default async function transformData(\n transforms: TransformFunction | TransformFunction[] | undefined,\n data: unknown,\n headers: Record<string, string | string[]>,\n config?: AccessioRequestConfig,\n direction: 'request' | 'response' = 'request',\n): Promise<unknown> {\n if (!transforms || !Array.isArray(transforms)) {\n return data;\n }\n\n let result = data;\n\n for (const transform of transforms) {\n if (typeof transform === 'function') {\n try {\n result = await transform(result, headers);\n } catch (err) {\n throw AccessioError.from(\n err instanceof Error ? err : new Error(String(err)),\n direction === 'response' ? AccessioError.ERR_BAD_RESPONSE : AccessioError.ERR_BAD_REQUEST,\n config ?? null,\n null,\n null,\n );\n }\n }\n }\n\n return result;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,2BAA0B;AAG1B,eAAO,cACL,YACA,MACA,SACA,QACA,YAAoC,WAClB;AAClB,MAAI,CAAC,cAAc,CAAC,MAAM,QAAQ,UAAU,GAAG;AAC7C,WAAO;AAAA,EACT;AAEA,MAAI,SAAS;AAEb,aAAW,aAAa,YAAY;AAClC,QAAI,OAAO,cAAc,YAAY;AACnC,UAAI;AACF,iBAAS,MAAM,UAAU,QAAQ,OAAO;AAAA,MAC1C,SAAS,KAAK;AACZ,cAAM,qBAAAA,QAAc;AAAA,UAClB,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;AAAA,UAClD,cAAc,aAAa,qBAAAA,QAAc,mBAAmB,qBAAAA,QAAc;AAAA,UAC1E,UAAU;AAAA,UACV;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;","names":["AccessioError"]}
package/cjs/index.cjs CHANGED
@@ -62,7 +62,10 @@ const PUBLIC_METHODS = [
62
62
  "patch",
63
63
  "postForm",
64
64
  "putForm",
65
- "patchForm"
65
+ "patchForm",
66
+ "stream",
67
+ "autoPaginate",
68
+ "gql"
66
69
  ];
67
70
  function createInstance(defaultConfig) {
68
71
  const context = new import_accessio.default(defaultConfig);
package/cjs/index.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import Accessio from './accessio';\nimport defaults from './defaults';\nimport AccessioError from './core/accessioError';\nimport mergeConfig from './core/mergeConfig';\nimport buildURL from './core/buildURL';\nimport InterceptorManager from './interceptors/interceptorManager';\nimport { createRateLimiter } from './helpers/rateLimiter';\nimport { logRequest, logResponse, logError } from './helpers/debug';\nimport { ERR_CANCELED } from './constants/errorCodes';\nimport type { AccessioRequestConfig, AccessioResponse } from './types';\n\nconst PUBLIC_METHODS = [\n 'request',\n 'getUri',\n 'get',\n 'delete',\n 'head',\n 'options',\n 'post',\n 'put',\n 'patch',\n 'postForm',\n 'putForm',\n 'patchForm',\n];\n\nfunction createInstance(defaultConfig: AccessioRequestConfig) {\n const context = new Accessio(defaultConfig);\n\n const instance: any = function accessio(\n configOrUrl: string | AccessioRequestConfig,\n config?: AccessioRequestConfig,\n ) {\n return context.request(configOrUrl, config);\n };\n\n for (const key of PUBLIC_METHODS) {\n const method: any = (context as any)[key];\n if (typeof method === 'function') {\n instance[key] = method.bind(context);\n }\n }\n\n instance.defaults = context.defaults;\n instance.interceptors = context.interceptors;\n instance.all = function all(promises: any[]): Promise<any[]> {\n return Promise.all(promises);\n };\n instance.spread = function spread<T>(callback: (...args: any[]) => T): (arr: any[]) => T {\n return function wrap(arr: any[]): T {\n return callback(...arr);\n };\n };\n instance.isCancel = function isCancel(value: any): boolean {\n return !!(value && value.isAccessioError && value.code === ERR_CANCELED);\n };\n instance.isAccessioError = function isAccessioError(value: any): boolean {\n return (\n value instanceof AccessioError ||\n !!(value && typeof value === 'object' && value.isAccessioError === true)\n );\n };\n instance.AccessioError = AccessioError;\n instance.Accessio = Accessio;\n instance.mergeConfig = mergeConfig;\n instance.buildURL = buildURL;\n instance.InterceptorManager = InterceptorManager;\n instance.createRateLimiter = createRateLimiter;\n\n return instance;\n}\n\nconst accessio = createInstance(defaults);\n\nfunction create(instanceConfig?: AccessioRequestConfig) {\n return createInstance(mergeConfig(defaults, instanceConfig));\n}\n\naccessio.create = create;\n\nexport default accessio;\n\nexport {\n Accessio,\n AccessioError,\n mergeConfig,\n buildURL,\n InterceptorManager,\n createInstance,\n createRateLimiter,\n logRequest,\n logResponse,\n logError,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA,kCAAAA;AAAA,EAAA,0CAAAC;AAAA,EAAA,oDAAAC;AAAA,EAAA,gCAAAC;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wCAAAC;AAAA;AAAA;AAAA,sBAAqB;AACrB,sBAAqB;AACrB,2BAA0B;AAC1B,yBAAwB;AACxB,sBAAqB;AACrB,gCAA+B;AAC/B,yBAAkC;AAClC,mBAAkD;AAClD,wBAA6B;AAG7B,MAAM,iBAAiB;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,SAAS,eAAe,eAAsC;AAC5D,QAAM,UAAU,IAAI,gBAAAJ,QAAS,aAAa;AAE1C,QAAM,WAAgB,SAASK,UAC7B,aACA,QACA;AACA,WAAO,QAAQ,QAAQ,aAAa,MAAM;AAAA,EAC5C;AAEA,aAAW,OAAO,gBAAgB;AAChC,UAAM,SAAe,QAAgB,GAAG;AACxC,QAAI,OAAO,WAAW,YAAY;AAChC,eAAS,GAAG,IAAI,OAAO,KAAK,OAAO;AAAA,IACrC;AAAA,EACF;AAEA,WAAS,WAAW,QAAQ;AAC5B,WAAS,eAAe,QAAQ;AAChC,WAAS,MAAM,SAAS,IAAI,UAAiC;AAC3D,WAAO,QAAQ,IAAI,QAAQ;AAAA,EAC7B;AACA,WAAS,SAAS,SAAS,OAAU,UAAoD;AACvF,WAAO,SAAS,KAAK,KAAe;AAClC,aAAO,SAAS,GAAG,GAAG;AAAA,IACxB;AAAA,EACF;AACA,WAAS,WAAW,SAAS,SAAS,OAAqB;AACzD,WAAO,CAAC,EAAE,SAAS,MAAM,mBAAmB,MAAM,SAAS;AAAA,EAC7D;AACA,WAAS,kBAAkB,SAAS,gBAAgB,OAAqB;AACvE,WACE,iBAAiB,qBAAAJ,WACjB,CAAC,EAAE,SAAS,OAAO,UAAU,YAAY,MAAM,oBAAoB;AAAA,EAEvE;AACA,WAAS,gBAAgB,qBAAAA;AACzB,WAAS,WAAW,gBAAAD;AACpB,WAAS,cAAc,mBAAAI;AACvB,WAAS,WAAW,gBAAAD;AACpB,WAAS,qBAAqB,0BAAAD;AAC9B,WAAS,oBAAoB;AAE7B,SAAO;AACT;AAEA,MAAM,WAAW,eAAe,gBAAAI,OAAQ;AAExC,SAAS,OAAO,gBAAwC;AACtD,SAAO,mBAAe,mBAAAF,SAAY,gBAAAE,SAAU,cAAc,CAAC;AAC7D;AAEA,SAAS,SAAS;AAElB,IAAO,cAAQ;","names":["Accessio","AccessioError","InterceptorManager","buildURL","mergeConfig","accessio","defaults"]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import Accessio from './accessio';\nimport defaults from './defaults';\nimport AccessioError from './core/accessioError';\nimport mergeConfig from './core/mergeConfig';\nimport buildURL from './core/buildURL';\nimport InterceptorManager from './interceptors/interceptorManager';\nimport { createRateLimiter } from './helpers/rateLimiter';\nimport { logRequest, logResponse, logError } from './helpers/debug';\nimport { ERR_CANCELED } from './constants/errorCodes';\nimport type { AccessioRequestConfig } from './types';\n\nconst PUBLIC_METHODS = [\n 'request',\n 'getUri',\n 'get',\n 'delete',\n 'head',\n 'options',\n 'post',\n 'put',\n 'patch',\n 'postForm',\n 'putForm',\n 'patchForm',\n 'stream',\n 'autoPaginate',\n 'gql',\n];\n\nfunction createInstance(defaultConfig: AccessioRequestConfig) {\n const context = new Accessio(defaultConfig);\n\n const instance: any = function accessio(\n configOrUrl: string | AccessioRequestConfig,\n config?: AccessioRequestConfig,\n ) {\n return context.request(configOrUrl, config);\n };\n\n for (const key of PUBLIC_METHODS) {\n const method: any = (context as any)[key];\n if (typeof method === 'function') {\n instance[key] = method.bind(context);\n }\n }\n\n instance.defaults = context.defaults;\n instance.interceptors = context.interceptors;\n instance.all = function all(promises: any[]): Promise<any[]> {\n return Promise.all(promises);\n };\n instance.spread = function spread<T>(callback: (...args: any[]) => T): (arr: any[]) => T {\n return function wrap(arr: any[]): T {\n return callback(...arr);\n };\n };\n instance.isCancel = function isCancel(value: any): boolean {\n return !!(value && value.isAccessioError && value.code === ERR_CANCELED);\n };\n instance.isAccessioError = function isAccessioError(value: any): boolean {\n return (\n value instanceof AccessioError ||\n !!(value && typeof value === 'object' && value.isAccessioError === true)\n );\n };\n instance.AccessioError = AccessioError;\n instance.Accessio = Accessio;\n instance.mergeConfig = mergeConfig;\n instance.buildURL = buildURL;\n instance.InterceptorManager = InterceptorManager;\n instance.createRateLimiter = createRateLimiter;\n\n return instance;\n}\n\nconst accessio = createInstance(defaults);\n\nfunction create(instanceConfig?: AccessioRequestConfig) {\n return createInstance(mergeConfig(defaults, instanceConfig));\n}\n\naccessio.create = create;\n\nexport default accessio;\n\nexport {\n Accessio,\n AccessioError,\n mergeConfig,\n buildURL,\n InterceptorManager,\n createInstance,\n createRateLimiter,\n logRequest,\n logResponse,\n logError,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA,kCAAAA;AAAA,EAAA,0CAAAC;AAAA,EAAA,oDAAAC;AAAA,EAAA,gCAAAC;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wCAAAC;AAAA;AAAA;AAAA,sBAAqB;AACrB,sBAAqB;AACrB,2BAA0B;AAC1B,yBAAwB;AACxB,sBAAqB;AACrB,gCAA+B;AAC/B,yBAAkC;AAClC,mBAAkD;AAClD,wBAA6B;AAG7B,MAAM,iBAAiB;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,SAAS,eAAe,eAAsC;AAC5D,QAAM,UAAU,IAAI,gBAAAJ,QAAS,aAAa;AAE1C,QAAM,WAAgB,SAASK,UAC7B,aACA,QACA;AACA,WAAO,QAAQ,QAAQ,aAAa,MAAM;AAAA,EAC5C;AAEA,aAAW,OAAO,gBAAgB;AAChC,UAAM,SAAe,QAAgB,GAAG;AACxC,QAAI,OAAO,WAAW,YAAY;AAChC,eAAS,GAAG,IAAI,OAAO,KAAK,OAAO;AAAA,IACrC;AAAA,EACF;AAEA,WAAS,WAAW,QAAQ;AAC5B,WAAS,eAAe,QAAQ;AAChC,WAAS,MAAM,SAAS,IAAI,UAAiC;AAC3D,WAAO,QAAQ,IAAI,QAAQ;AAAA,EAC7B;AACA,WAAS,SAAS,SAAS,OAAU,UAAoD;AACvF,WAAO,SAAS,KAAK,KAAe;AAClC,aAAO,SAAS,GAAG,GAAG;AAAA,IACxB;AAAA,EACF;AACA,WAAS,WAAW,SAAS,SAAS,OAAqB;AACzD,WAAO,CAAC,EAAE,SAAS,MAAM,mBAAmB,MAAM,SAAS;AAAA,EAC7D;AACA,WAAS,kBAAkB,SAAS,gBAAgB,OAAqB;AACvE,WACE,iBAAiB,qBAAAJ,WACjB,CAAC,EAAE,SAAS,OAAO,UAAU,YAAY,MAAM,oBAAoB;AAAA,EAEvE;AACA,WAAS,gBAAgB,qBAAAA;AACzB,WAAS,WAAW,gBAAAD;AACpB,WAAS,cAAc,mBAAAI;AACvB,WAAS,WAAW,gBAAAD;AACpB,WAAS,qBAAqB,0BAAAD;AAC9B,WAAS,oBAAoB;AAE7B,SAAO;AACT;AAEA,MAAM,WAAW,eAAe,gBAAAI,OAAQ;AAExC,SAAS,OAAO,gBAAwC;AACtD,SAAO,mBAAe,mBAAAF,SAAY,gBAAAE,SAAU,cAAc,CAAC;AAC7D;AAEA,SAAS,SAAS;AAElB,IAAO,cAAQ;","names":["Accessio","AccessioError","InterceptorManager","buildURL","mergeConfig","accessio","defaults"]}
package/index.d.ts CHANGED
@@ -55,6 +55,13 @@ export interface AccessioRequestConfig {
55
55
  /** Include credentials in cross-site requests */
56
56
  withCredentials?: boolean;
57
57
 
58
+ /**
59
+ * URL protocols accepted by the client. Defaults to `["http:", "https:"]`.
60
+ * Pass an extended array to allow more (e.g. `["http:", "https:", "ws:"]`),
61
+ * or `null` to disable the check entirely.
62
+ */
63
+ allowedProtocols?: string[] | null;
64
+
58
65
  /** Expected response data type */
59
66
  responseType?: "json" | "text" | "blob" | "arraybuffer" | "stream";
60
67
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "accessio",
3
- "version": "1.1.2",
3
+ "version": "1.3.0",
4
4
  "description": "Fast, flexible HTTP client — simple, modular, and dependency-free",
5
5
  "type": "module",
6
6
  "main": "./cjs/index.cjs",
@@ -29,7 +29,7 @@
29
29
  "./core/buildURL": {
30
30
  "types": "./index.d.ts",
31
31
  "import": "./src/core/buildURL.ts",
32
- "require": "./cjs/helpers/buildURL.cjs"
32
+ "require": "./cjs/core/buildURL.cjs"
33
33
  },
34
34
  "./core/mergeConfig": {
35
35
  "types": "./index.d.ts",
@@ -103,6 +103,7 @@
103
103
  "prettier": "^3.8.3",
104
104
  "tsup": "^8.0.0",
105
105
  "typescript": "^5.0.0",
106
+ "typescript-eslint": "^8.59.3",
106
107
  "vitest": "^3.1.0"
107
108
  }
108
109
  }
package/src/accessio.ts CHANGED
@@ -6,6 +6,7 @@ import buildURL from './core/buildURL';
6
6
  import retryRequest from './core/retry';
7
7
  import { logRequest, logResponse, logError } from './helpers/debug';
8
8
  import { rateLimitedRequest } from './helpers/rateLimiter';
9
+ import { toFormData } from './helpers/toFormData';
9
10
  import type {
10
11
  AccessioRequestConfig,
11
12
  AccessioResponse,
@@ -201,11 +202,12 @@ export class Accessio {
201
202
  data?: any,
202
203
  config?: AccessioRequestConfig,
203
204
  ): Promise<AccessioResponse<T>> {
205
+ const formData = data && !(data instanceof FormData) ? toFormData(data) : data;
204
206
  return this.request<T>(
205
207
  mergeConfig(config || {}, {
206
208
  method: 'post',
207
209
  url,
208
- data,
210
+ data: formData,
209
211
  headers: { 'Content-Type': 'multipart/form-data' },
210
212
  }),
211
213
  );
@@ -216,11 +218,12 @@ export class Accessio {
216
218
  data?: any,
217
219
  config?: AccessioRequestConfig,
218
220
  ): Promise<AccessioResponse<T>> {
221
+ const formData = data && !(data instanceof FormData) ? toFormData(data) : data;
219
222
  return this.request<T>(
220
223
  mergeConfig(config || {}, {
221
224
  method: 'put',
222
225
  url,
223
- data,
226
+ data: formData,
224
227
  headers: { 'Content-Type': 'multipart/form-data' },
225
228
  }),
226
229
  );
@@ -231,15 +234,90 @@ export class Accessio {
231
234
  data?: any,
232
235
  config?: AccessioRequestConfig,
233
236
  ): Promise<AccessioResponse<T>> {
237
+ const formData = data && !(data instanceof FormData) ? toFormData(data) : data;
234
238
  return this.request<T>(
235
239
  mergeConfig(config || {}, {
236
240
  method: 'patch',
237
241
  url,
238
- data,
242
+ data: formData,
239
243
  headers: { 'Content-Type': 'multipart/form-data' },
240
244
  }),
241
245
  );
242
246
  }
247
+
248
+ async *stream<T = any>(
249
+ url: string,
250
+ config?: AccessioRequestConfig,
251
+ ): AsyncGenerator<T, void, unknown> {
252
+ const response = await this.request<ReadableStream<Uint8Array>>(
253
+ mergeConfig(config || {}, { method: 'get', url, responseType: 'stream' }),
254
+ );
255
+ if (!response.data) return;
256
+
257
+ const reader = response.data.getReader();
258
+ const decoder = new TextDecoder();
259
+ let buffer = '';
260
+
261
+ while (true) {
262
+ const { done, value } = await reader.read();
263
+ if (done) break;
264
+
265
+ buffer += decoder.decode(value, { stream: true });
266
+ const lines = buffer.split('\n');
267
+ buffer = lines.pop() || '';
268
+
269
+ for (const line of lines) {
270
+ if (line.trim().startsWith('data:')) {
271
+ const dataStr = line.replace(/^data:\s*/, '');
272
+ if (dataStr === '[DONE]') return;
273
+ try {
274
+ yield JSON.parse(dataStr);
275
+ } catch (e) {
276
+ yield dataStr as any;
277
+ }
278
+ } else if (line.trim().startsWith('{') || line.trim().startsWith('[')) {
279
+ try {
280
+ yield JSON.parse(line);
281
+ } catch (e) {
282
+ // ignore partial json
283
+ }
284
+ }
285
+ }
286
+ }
287
+ }
288
+
289
+ async *autoPaginate<T = any>(
290
+ url: string,
291
+ config?: AccessioRequestConfig,
292
+ ): AsyncGenerator<T, void, unknown> {
293
+ let nextUrl: string | null = url;
294
+ let currentConfig = config || {};
295
+
296
+ while (nextUrl) {
297
+ const response: AccessioResponse<any> = await this.get(nextUrl, currentConfig);
298
+
299
+ const items = Array.isArray(response.data) ? response.data : response.data.data;
300
+ if (Array.isArray(items)) {
301
+ for (const item of items) {
302
+ yield item;
303
+ }
304
+ }
305
+
306
+ nextUrl = response.data.next || response.data.links?.next || null;
307
+ if (nextUrl) {
308
+ currentConfig = mergeConfig(currentConfig, { url: nextUrl, params: {} });
309
+ }
310
+ }
311
+ }
312
+
313
+ gql<T = any>(
314
+ url: string,
315
+ query: string,
316
+ variables?: Record<string, any>,
317
+ config?: AccessioRequestConfig,
318
+ ): Promise<AccessioResponse<T>> {
319
+ return this.post<T>(url, { query, variables }, config);
320
+ }
243
321
  }
244
322
 
245
323
  export default Accessio;
@@ -1,6 +1,30 @@
1
1
  import ErrorCodes from '../constants/errorCodes';
2
2
  import type { AccessioRequestConfig, AccessioResponse } from '../types';
3
3
 
4
+ function redactHeaders(headers: unknown): unknown {
5
+ if (!headers || typeof headers !== 'object') return headers;
6
+ const out: Record<string, unknown> = {};
7
+ for (const key of Object.keys(headers as Record<string, unknown>)) {
8
+ const value = (headers as Record<string, unknown>)[key];
9
+ if (/^authorization$/i.test(key)) {
10
+ out[key] = '[REDACTED]';
11
+ } else if (value && typeof value === 'object' && !Array.isArray(value)) {
12
+ out[key] = redactHeaders(value);
13
+ } else {
14
+ out[key] = value;
15
+ }
16
+ }
17
+ return out;
18
+ }
19
+
20
+ export function redactConfig(config: AccessioRequestConfig | null): AccessioRequestConfig | null {
21
+ if (!config) return config;
22
+ const clone = { ...config } as AccessioRequestConfig & { auth?: unknown };
23
+ if ('auth' in clone) delete clone.auth;
24
+ if (clone.headers) clone.headers = redactHeaders(clone.headers) as typeof clone.headers;
25
+ return clone;
26
+ }
27
+
4
28
  export class AccessioError extends Error {
5
29
  static ERR_BAD_OPTION_VALUE: string = ErrorCodes.ERR_BAD_OPTION_VALUE;
6
30
  static ERR_BAD_OPTION: string = ErrorCodes.ERR_BAD_OPTION;
@@ -20,6 +44,7 @@ export class AccessioError extends Error {
20
44
  readonly response: AccessioResponse | null;
21
45
  readonly isAccessioError: true;
22
46
  cause?: Error;
47
+ override name = 'AccessioError' as const;
23
48
 
24
49
  constructor(
25
50
  message: string,
@@ -31,7 +56,7 @@ export class AccessioError extends Error {
31
56
  super(message);
32
57
  this.name = 'AccessioError';
33
58
  this.code = code ?? null;
34
- this.config = config ?? null;
59
+ this.config = redactConfig(config ?? null);
35
60
  this.request = request ?? null;
36
61
  this.response = response ?? null;
37
62
  this.isAccessioError = true;
@@ -75,7 +75,22 @@ export default function buildURL(
75
75
  ): string {
76
76
  let fullURL = baseURL && !isAbsoluteURL(url) ? combineURLs(baseURL, url) : url || '';
77
77
 
78
- const serialized = serializeParams(params as Record<string, unknown>, paramsSerializer);
78
+ let finalParams = params;
79
+ if (params && typeof params === 'object') {
80
+ const unusedParams = { ...params };
81
+ fullURL = fullURL.replace(/(?::([a-zA-Z0-9_]+))|(?:{([a-zA-Z0-9_]+)})/g, (match, p1, p2) => {
82
+ const key = p1 || p2;
83
+ if (key && unusedParams[key] !== undefined) {
84
+ const val = unusedParams[key];
85
+ delete unusedParams[key];
86
+ return encodeURIComponent(String(val));
87
+ }
88
+ return match;
89
+ });
90
+ finalParams = unusedParams;
91
+ }
92
+
93
+ const serialized = serializeParams(finalParams as Record<string, unknown>, paramsSerializer);
79
94
  if (serialized) {
80
95
  const hashIndex = fullURL.indexOf('#');
81
96
  if (hashIndex !== -1) {
@@ -2,7 +2,11 @@ import AccessioError from './accessioError';
2
2
  import parseHeaders from '../helpers/parseHeaders';
3
3
  import type { AccessioRequestConfig, AccessioResponse } from '../types';
4
4
 
5
- async function readResponseData(fetchResponse: Response, responseType: string): Promise<unknown> {
5
+ async function readResponseData(
6
+ fetchResponse: Response,
7
+ config: AccessioRequestConfig,
8
+ ): Promise<unknown> {
9
+ const responseType = config.responseType || 'json';
6
10
  switch (responseType) {
7
11
  case 'arraybuffer':
8
12
  return await fetchResponse.arrayBuffer();
@@ -14,12 +18,20 @@ async function readResponseData(fetchResponse: Response, responseType: string):
14
18
  default: {
15
19
  const contentType = fetchResponse.headers.get('content-type') || '';
16
20
  if (contentType.includes('application/json')) {
17
- if (typeof fetchResponse.clone === 'function') {
18
- const text = await fetchResponse.clone().text();
19
- return text ? await fetchResponse.json() : '';
20
- } else {
21
- const text = await fetchResponse.text();
22
- return text ? JSON.parse(text) : '';
21
+ const text = await fetchResponse.text();
22
+ if (!text) return '';
23
+ try {
24
+ return JSON.parse(text);
25
+ } catch (err) {
26
+ throw new AccessioError(
27
+ `Failed to parse JSON response: ${(err as Error).message}. Raw body: ${
28
+ text.length > 500 ? text.slice(0, 500) + '…' : text
29
+ }`,
30
+ AccessioError.ERR_BAD_RESPONSE,
31
+ config,
32
+ fetchResponse,
33
+ null,
34
+ );
23
35
  }
24
36
  }
25
37
  return await fetchResponse.text();
@@ -94,10 +106,42 @@ export default async function fetchAdapter(
94
106
  }
95
107
 
96
108
  try {
97
- const fetchResponse = await fetch(fullURL, fetchOptions);
109
+ const fetchImpl = config.fetch || fetch;
110
+ let fetchResponse = await fetchImpl(fullURL, fetchOptions);
111
+
112
+ if (config.onDownloadProgress && fetchResponse.body && config.responseType !== 'stream') {
113
+ const contentLength = fetchResponse.headers.get('content-length');
114
+ const total = contentLength ? parseInt(contentLength, 10) : 0;
115
+ let loaded = 0;
116
+
117
+ const reader = fetchResponse.body.getReader();
118
+ const stream = new ReadableStream({
119
+ async start(controller) {
120
+ try {
121
+ while (true) {
122
+ const { done, value } = await reader.read();
123
+ if (done) {
124
+ controller.close();
125
+ break;
126
+ }
127
+ loaded += value.byteLength;
128
+ config.onDownloadProgress!({ loaded, total });
129
+ controller.enqueue(value);
130
+ }
131
+ } catch (e) {
132
+ controller.error(e);
133
+ }
134
+ },
135
+ });
136
+
137
+ fetchResponse = new Response(stream, {
138
+ headers: fetchResponse.headers,
139
+ status: fetchResponse.status,
140
+ statusText: fetchResponse.statusText,
141
+ });
142
+ }
98
143
 
99
144
  let responseData: unknown;
100
- const responseType = config.responseType || 'json';
101
145
 
102
146
  const contentLength = fetchResponse.headers.get('content-length');
103
147
  if (
@@ -115,8 +159,16 @@ export default async function fetchAdapter(
115
159
  }
116
160
 
117
161
  try {
118
- responseData = await readResponseData(fetchResponse, responseType);
162
+ responseData = await readResponseData(fetchResponse, config);
163
+ if (config.schema) {
164
+ if (typeof config.schema.parseAsync === 'function') {
165
+ responseData = await config.schema.parseAsync(responseData);
166
+ } else {
167
+ responseData = config.schema.parse(responseData);
168
+ }
169
+ }
119
170
  } catch (readError) {
171
+ if (readError instanceof AccessioError) throw readError;
120
172
  throw AccessioError.from(
121
173
  readError as Error,
122
174
  AccessioError.ERR_BAD_RESPONSE,
@@ -1,10 +1,12 @@
1
1
  import buildURL from './buildURL';
2
- import AccessioError from './accessioError';
2
+ import AccessioError, { redactConfig } from './accessioError';
3
+ import { ERR_BAD_OPTION } from '../constants/errorCodes';
3
4
  import transformData from '../helpers/transformData';
4
5
  import settle from '../helpers/settle';
5
6
  import { flattenHeaders, removeContentType, buildFetchHeaders } from '../helpers/flattenHeaders';
6
7
  import { setBasicAuth } from '../helpers/auth';
7
8
  import fetchAdapter from './fetchAdapter';
9
+ import { defaultMemoryCache } from '../helpers/memoryCache';
8
10
  import type { AccessioRequestConfig, AccessioResponse, TransformFunction } from '../types';
9
11
 
10
12
  type HeadersConfig = Record<string, Record<string, string | string[]>>;
@@ -17,6 +19,31 @@ function buildTransformArray(
17
19
  return [transform];
18
20
  }
19
21
 
22
+ const DEFAULT_ALLOWED_PROTOCOLS = ['http:', 'https:'];
23
+
24
+ function assertAllowedProtocol(fullURL: string, config: AccessioRequestConfig): void {
25
+ if (config.allowedProtocols === null) return;
26
+ const allowed = config.allowedProtocols ?? DEFAULT_ALLOWED_PROTOCOLS;
27
+
28
+ let scheme: string | null = null;
29
+ const match = /^([a-z][a-z\d+\-.]*):/i.exec(fullURL);
30
+ if (match) scheme = match[1].toLowerCase() + ':';
31
+ if (!scheme) return;
32
+
33
+ if (!allowed.includes(scheme)) {
34
+ throw new AccessioError(
35
+ `URL protocol "${scheme}" is not allowed. Allowed: ${allowed.join(', ')}. ` +
36
+ 'Set config.allowedProtocols to extend, or null to disable the check.',
37
+ ERR_BAD_OPTION,
38
+ config,
39
+ null,
40
+ null,
41
+ );
42
+ }
43
+ }
44
+
45
+ const activeRequests = new Map<string, Promise<AccessioResponse>>();
46
+
20
47
  export default async function dispatchRequest(
21
48
  config: AccessioRequestConfig,
22
49
  ): Promise<AccessioResponse> {
@@ -29,61 +56,124 @@ export default async function dispatchRequest(
29
56
  config.paramsSerializer,
30
57
  );
31
58
 
32
- const flatHeaders = flattenHeaders(config.headers as HeadersConfig | undefined, config.method);
59
+ assertAllowedProtocol(fullURL, config);
33
60
 
34
- const requestTransforms = buildTransformArray(config.transformRequest);
61
+ if (config.hooks?.onBeforeRequest) {
62
+ await config.hooks.onBeforeRequest(config);
63
+ }
35
64
 
36
- const requestData = await transformData(requestTransforms, config.data, flatHeaders, config);
65
+ const isGet = (config.method || 'GET').toUpperCase() === 'GET';
66
+ const cacheKey = isGet ? `GET:${fullURL}` : '';
67
+
68
+ if (isGet && config.cache) {
69
+ const cacheProvider = typeof config.cache === 'object' ? config.cache : defaultMemoryCache;
70
+ const cached = await cacheProvider.get(cacheKey);
71
+ if (cached) {
72
+ if (config.hooks?.onRequestResponse) {
73
+ await config.hooks.onRequestResponse(cached);
74
+ }
75
+ return cached;
76
+ }
77
+ }
37
78
 
38
- if (
39
- requestData === null ||
40
- requestData === undefined ||
41
- (typeof FormData !== 'undefined' && requestData instanceof FormData)
42
- ) {
43
- removeContentType(flatHeaders);
79
+ if (isGet && config.dedupe) {
80
+ if (activeRequests.has(cacheKey)) {
81
+ return activeRequests.get(cacheKey)!;
82
+ }
44
83
  }
45
84
 
46
- setBasicAuth(config, flatHeaders);
85
+ const performRequest = async () => {
86
+ const flatHeaders = flattenHeaders(config.headers as HeadersConfig | undefined, config.method);
87
+ const requestTransforms = buildTransformArray(config.transformRequest);
88
+ const requestData = await transformData(requestTransforms, config.data, flatHeaders, config);
89
+
90
+ if (
91
+ requestData === null ||
92
+ requestData === undefined ||
93
+ (typeof FormData !== 'undefined' && requestData instanceof FormData)
94
+ ) {
95
+ removeContentType(flatHeaders);
96
+ }
97
+
98
+ setBasicAuth(config, flatHeaders);
99
+
100
+ const fetchOptions: RequestInit = {
101
+ method: (config.method || 'GET').toUpperCase(),
102
+ headers: buildFetchHeaders(flatHeaders),
103
+ };
104
+
105
+ const methodsWithBody = ['POST', 'PUT', 'PATCH', 'DELETE'];
106
+ if (
107
+ methodsWithBody.includes(fetchOptions.method!) &&
108
+ requestData !== undefined &&
109
+ requestData !== null
110
+ ) {
111
+ fetchOptions.body = requestData as BodyInit;
112
+ }
113
+
114
+ if (config.withCredentials) {
115
+ fetchOptions.credentials = 'include';
116
+ }
117
+
118
+ if (config.dispatcher) {
119
+ (fetchOptions as any).dispatcher = config.dispatcher;
120
+ }
121
+ if (config.agent) {
122
+ (fetchOptions as any).agent = config.agent;
123
+ }
124
+
125
+ const requestStartTime = Date.now();
126
+
127
+ const response = await fetchAdapter(config, fullURL, fetchOptions, requestStartTime);
128
+ response.config = redactConfig(response.config) as typeof response.config;
129
+
130
+ const responseTransforms = buildTransformArray(config.transformResponse);
131
+
132
+ response.data = await transformData(
133
+ responseTransforms,
134
+ response.data,
135
+ response.headers,
136
+ config,
137
+ 'response',
138
+ );
47
139
 
48
- const fetchOptions: RequestInit = {
49
- method: (config.method || 'GET').toUpperCase(),
50
- headers: buildFetchHeaders(flatHeaders),
140
+ return new Promise<AccessioResponse>((resolve, reject) => {
141
+ settle(
142
+ resolve as (value: AccessioResponse) => void,
143
+ reject as (reason: AccessioError) => void,
144
+ response,
145
+ config,
146
+ );
147
+ });
51
148
  };
52
149
 
53
- const methodsWithBody = ['POST', 'PUT', 'PATCH', 'DELETE'];
54
- if (
55
- methodsWithBody.includes(fetchOptions.method!) &&
56
- requestData !== undefined &&
57
- requestData !== null
58
- ) {
59
- fetchOptions.body = requestData as BodyInit;
60
- }
61
-
62
- if (config.withCredentials) {
63
- fetchOptions.credentials = 'include';
64
- }
150
+ const promise = performRequest();
65
151
 
66
- if (config.dispatcher) {
67
- (fetchOptions as any).dispatcher = config.dispatcher;
152
+ if (isGet && config.dedupe) {
153
+ activeRequests.set(cacheKey, promise);
154
+ const cleanup = () => {
155
+ activeRequests.delete(cacheKey);
156
+ };
157
+ promise.then(cleanup, cleanup);
68
158
  }
69
- if (config.agent) {
70
- (fetchOptions as any).agent = config.agent;
71
- }
72
-
73
- const requestStartTime = Date.now();
74
159
 
75
- const response = await fetchAdapter(config, fullURL, fetchOptions, requestStartTime);
160
+ try {
161
+ const response = await promise;
76
162
 
77
- const responseTransforms = buildTransformArray(config.transformResponse);
163
+ if (isGet && config.cache) {
164
+ const cacheProvider = typeof config.cache === 'object' ? config.cache : defaultMemoryCache;
165
+ await cacheProvider.set(cacheKey, response, config.cacheTTL);
166
+ }
78
167
 
79
- response.data = await transformData(responseTransforms, response.data, response.headers, config);
168
+ if (config.hooks?.onRequestResponse) {
169
+ await config.hooks.onRequestResponse(response);
170
+ }
80
171
 
81
- return new Promise<AccessioResponse>((resolve, reject) => {
82
- settle(
83
- resolve as (value: AccessioResponse) => void,
84
- reject as (reason: AccessioError) => void,
85
- response,
86
- config,
87
- );
88
- });
172
+ return response;
173
+ } catch (error) {
174
+ if (config.hooks?.onRequestError && error instanceof AccessioError) {
175
+ await config.hooks.onRequestError(error);
176
+ }
177
+ throw error;
178
+ }
89
179
  }