accessio 1.1.1 → 1.2.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 (50) hide show
  1. package/README.md +98 -1
  2. package/cjs/accessio.cjs +102 -10
  3. package/cjs/accessio.cjs.map +1 -1
  4. package/cjs/core/accessioError.cjs +1 -0
  5. package/cjs/core/accessioError.cjs.map +1 -1
  6. package/cjs/core/buildURL.cjs +16 -2
  7. package/cjs/core/buildURL.cjs.map +1 -1
  8. package/cjs/core/fetchAdapter.cjs +224 -0
  9. package/cjs/core/fetchAdapter.cjs.map +1 -0
  10. package/cjs/core/mergeConfig.cjs +2 -2
  11. package/cjs/core/mergeConfig.cjs.map +1 -1
  12. package/cjs/core/request.cjs +74 -199
  13. package/cjs/core/request.cjs.map +1 -1
  14. package/cjs/core/retry.cjs +23 -4
  15. package/cjs/core/retry.cjs.map +1 -1
  16. package/cjs/defaults/transforms.cjs.map +1 -1
  17. package/cjs/helpers/auth.cjs +45 -0
  18. package/cjs/helpers/auth.cjs.map +1 -0
  19. package/cjs/helpers/flattenHeaders.cjs +78 -0
  20. package/cjs/helpers/flattenHeaders.cjs.map +1 -0
  21. package/cjs/helpers/memoryCache.cjs +51 -0
  22. package/cjs/helpers/memoryCache.cjs.map +1 -0
  23. package/cjs/helpers/parseHeaders.cjs +16 -4
  24. package/cjs/helpers/parseHeaders.cjs.map +1 -1
  25. package/cjs/helpers/rateLimiter.cjs +18 -8
  26. package/cjs/helpers/rateLimiter.cjs.map +1 -1
  27. package/cjs/helpers/toFormData.cjs +50 -0
  28. package/cjs/helpers/toFormData.cjs.map +1 -0
  29. package/cjs/helpers/transformData.cjs +2 -2
  30. package/cjs/helpers/transformData.cjs.map +1 -1
  31. package/cjs/index.cjs +4 -1
  32. package/cjs/index.cjs.map +1 -1
  33. package/package.json +4 -3
  34. package/src/accessio.ts +126 -10
  35. package/src/core/accessioError.ts +1 -0
  36. package/src/core/buildURL.ts +17 -2
  37. package/src/core/fetchAdapter.ts +227 -0
  38. package/src/core/mergeConfig.ts +2 -2
  39. package/src/core/request.ts +100 -250
  40. package/src/core/retry.ts +26 -6
  41. package/src/defaults/transforms.ts +4 -1
  42. package/src/helpers/auth.ts +26 -0
  43. package/src/helpers/flattenHeaders.ts +59 -0
  44. package/src/helpers/memoryCache.ts +30 -0
  45. package/src/helpers/parseHeaders.ts +19 -6
  46. package/src/helpers/rateLimiter.ts +18 -8
  47. package/src/helpers/toFormData.ts +25 -0
  48. package/src/helpers/transformData.ts +4 -4
  49. package/src/index.ts +4 -1
  50. package/src/types.ts +32 -3
package/src/core/retry.ts CHANGED
@@ -1,7 +1,6 @@
1
- import { ERR_CANCELED, ERR_NETWORK, ETIMEDOUT } from '../constants/errorCodes';
1
+ import { ERR_CANCELED, ERR_NETWORK } from '../constants/errorCodes';
2
2
  import type {
3
3
  AccessioRequestConfig,
4
- AccessioResponse,
5
4
  AccessioError,
6
5
  RetryConditionFunction,
7
6
  OnRetryFunction,
@@ -20,6 +19,10 @@ function defaultRetryCondition(error: any): boolean {
20
19
  return true;
21
20
  }
22
21
 
22
+ if (error.config?.retryOn429 && error.response && error.response.status === 429) {
23
+ return true;
24
+ }
25
+
23
26
  return false;
24
27
  }
25
28
 
@@ -62,7 +65,7 @@ async function retryRequest(
62
65
  ): Promise<any> {
63
66
  const maxRetries = config.retry ?? 0;
64
67
 
65
- if (maxRetries <= 0) {
68
+ if (maxRetries <= 0 && !config.retryOn429) {
66
69
  return dispatchFn(config);
67
70
  }
68
71
 
@@ -70,22 +73,39 @@ async function retryRequest(
70
73
  const retryCondition: RetryConditionFunction = config.retryCondition ?? defaultRetryCondition;
71
74
 
72
75
  let lastError: any;
76
+ const actualMaxRetries = Math.max(maxRetries, config.retryOn429 ? 3 : 0);
73
77
 
74
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
78
+ for (let attempt = 0; attempt <= actualMaxRetries; attempt++) {
75
79
  try {
76
80
  const response = await dispatchFn(config);
77
81
  return response;
78
82
  } catch (error) {
79
83
  lastError = error;
80
84
 
81
- const isLastAttempt = attempt >= maxRetries;
85
+ const isLastAttempt = attempt >= actualMaxRetries;
82
86
  const shouldRetry = !isLastAttempt && retryCondition(error as AccessioError);
83
87
 
84
88
  if (!shouldRetry) {
85
89
  throw error;
86
90
  }
87
91
 
88
- const delay = calculateDelay(attempt, retryDelay);
92
+ let delay = calculateDelay(attempt, retryDelay);
93
+
94
+ if (config.retryOn429 && (error as any).response?.status === 429) {
95
+ const headers = (error as any).response?.headers;
96
+ const retryAfterStr = headers?.['retry-after'] || headers?.['Retry-After'];
97
+ if (retryAfterStr) {
98
+ const parsed = parseInt(retryAfterStr, 10);
99
+ if (!isNaN(parsed)) {
100
+ delay = parsed * 1000;
101
+ } else {
102
+ const date = new Date(retryAfterStr);
103
+ if (!isNaN(date.getTime())) {
104
+ delay = Math.max(0, date.getTime() - Date.now());
105
+ }
106
+ }
107
+ }
108
+ }
89
109
 
90
110
  if (typeof config.onRetry === 'function') {
91
111
  (config.onRetry as OnRetryFunction)(attempt + 1, error as AccessioError, config);
@@ -1,4 +1,7 @@
1
- export function defaultTransformRequest(data: unknown, headers: Record<string, string>): unknown {
1
+ export function defaultTransformRequest(
2
+ data: unknown,
3
+ headers: Record<string, string | string[]>,
4
+ ): unknown {
2
5
  if (data === null || data === undefined) {
3
6
  return data;
4
7
  }
@@ -0,0 +1,26 @@
1
+ import type { AccessioRequestConfig } from '../types';
2
+
3
+ export function setBasicAuth(
4
+ config: AccessioRequestConfig,
5
+ headers: Record<string, string | string[]>,
6
+ ): void {
7
+ if (!config.auth) return;
8
+ const username = config.auth.username || '';
9
+ const password = config.auth.password || '';
10
+ const credentials = `${username}:${password}`;
11
+
12
+ let encoded: string;
13
+ if (typeof Buffer !== 'undefined') {
14
+ encoded = Buffer.from(credentials).toString('base64');
15
+ } else {
16
+ // Cryptic but effective UTF-8 to Base64 conversion for browsers lacking Buffer.
17
+ // encodeURIComponent converts non-ASCII to %XX, then we replace %XX with raw bytes
18
+ // before applying btoa.
19
+ encoded = btoa(
20
+ encodeURIComponent(credentials).replace(/%([0-9A-F]{2})/g, (match, p1) => {
21
+ return String.fromCharCode(parseInt(p1, 16));
22
+ }),
23
+ );
24
+ }
25
+ headers['Authorization'] = `Basic ${encoded}`;
26
+ }
@@ -0,0 +1,59 @@
1
+ const METHOD_KEYS = new Set<string>([
2
+ 'common',
3
+ 'delete',
4
+ 'get',
5
+ 'head',
6
+ 'options',
7
+ 'post',
8
+ 'put',
9
+ 'patch',
10
+ ]);
11
+
12
+ type HeadersConfig = Record<string, Record<string, string | string[]>>;
13
+
14
+ export function flattenHeaders(
15
+ headers: HeadersConfig | undefined,
16
+ method?: string,
17
+ ): Record<string, string | string[]> {
18
+ if (!headers) return {};
19
+
20
+ const merged: Record<string, string | string[]> = {};
21
+ const methodLower = (method || 'get').toLowerCase();
22
+
23
+ if (headers['common']) {
24
+ Object.assign(merged, headers['common']);
25
+ }
26
+
27
+ if (headers[methodLower]) {
28
+ Object.assign(merged, headers[methodLower]);
29
+ }
30
+
31
+ for (const key in headers) {
32
+ if (Object.prototype.hasOwnProperty.call(headers, key) && !METHOD_KEYS.has(key)) {
33
+ merged[key] = headers[key] as unknown as string | string[];
34
+ }
35
+ }
36
+
37
+ return merged;
38
+ }
39
+
40
+ export function removeContentType(headers: Record<string, string | string[]>): void {
41
+ const keys = Object.keys(headers).filter((k) => k.toLowerCase() === 'content-type');
42
+ for (const key of keys) {
43
+ delete headers[key];
44
+ }
45
+ }
46
+
47
+ export function buildFetchHeaders(headers: Record<string, string | string[]>): Headers {
48
+ const fetchHeaders = new Headers();
49
+ for (const [key, value] of Object.entries(headers)) {
50
+ if (Array.isArray(value)) {
51
+ for (const v of value) {
52
+ fetchHeaders.append(key, v);
53
+ }
54
+ } else {
55
+ fetchHeaders.set(key, value);
56
+ }
57
+ }
58
+ return fetchHeaders;
59
+ }
@@ -0,0 +1,30 @@
1
+ import type { CacheProvider } from '../types';
2
+
3
+ class MemoryCache implements CacheProvider {
4
+ private cache = new Map<string, { value: any; expiry: number | null }>();
5
+
6
+ get(key: string) {
7
+ const item = this.cache.get(key);
8
+ if (!item) return null;
9
+ if (item.expiry && Date.now() > item.expiry) {
10
+ this.cache.delete(key);
11
+ return null;
12
+ }
13
+ return item.value;
14
+ }
15
+
16
+ set(key: string, value: any, ttl?: number) {
17
+ const expiry = ttl ? Date.now() + ttl : null;
18
+ this.cache.set(key, { value, expiry });
19
+ }
20
+
21
+ delete(key: string) {
22
+ this.cache.delete(key);
23
+ }
24
+
25
+ clear() {
26
+ this.cache.clear();
27
+ }
28
+ }
29
+
30
+ export const defaultMemoryCache = new MemoryCache();
@@ -1,11 +1,24 @@
1
- export default function parseHeaders(headers: any): Record<string, string> {
2
- const parsed: Record<string, string> = {};
1
+ export default function parseHeaders(headers: any): Record<string, string | string[]> {
2
+ const parsed: Record<string, string | string[]> = {};
3
3
 
4
4
  if (!headers) return parsed;
5
5
 
6
+ const addHeader = (key: string, value: string) => {
7
+ const k = key.toLowerCase();
8
+ if (parsed[k]) {
9
+ if (Array.isArray(parsed[k])) {
10
+ (parsed[k] as string[]).push(value);
11
+ } else {
12
+ parsed[k] = [parsed[k] as string, value];
13
+ }
14
+ } else {
15
+ parsed[k] = value;
16
+ }
17
+ };
18
+
6
19
  if (typeof headers.forEach === 'function') {
7
20
  headers.forEach((value: string, key: string) => {
8
- parsed[key.toLowerCase()] = value;
21
+ addHeader(key, value);
9
22
  });
10
23
  return parsed;
11
24
  }
@@ -14,9 +27,9 @@ export default function parseHeaders(headers: any): Record<string, string> {
14
27
  headers.split('\n').forEach((line: string) => {
15
28
  const index = line.indexOf(':');
16
29
  if (index > 0) {
17
- const key = line.substring(0, index).trim().toLowerCase();
30
+ const key = line.substring(0, index).trim();
18
31
  const value = line.substring(index + 1).trim();
19
- parsed[key] = value;
32
+ addHeader(key, value);
20
33
  }
21
34
  });
22
35
  return parsed;
@@ -24,7 +37,7 @@ export default function parseHeaders(headers: any): Record<string, string> {
24
37
 
25
38
  if (typeof headers === 'object') {
26
39
  Object.keys(headers).forEach((key) => {
27
- parsed[key.toLowerCase()] = headers[key];
40
+ addHeader(key, headers[key]);
28
41
  });
29
42
  return parsed;
30
43
  }
@@ -21,7 +21,10 @@ export function createRateLimiter(
21
21
  }
22
22
  let active = 0;
23
23
  let destroyed = false;
24
- const queue: QueueItem[] = [];
24
+ let head = 0;
25
+ let tail = 0;
26
+ let pendingCount = 0;
27
+ const queue: Record<number, QueueItem> = {};
25
28
 
26
29
  function acquire(): Promise<void> {
27
30
  if (destroyed) {
@@ -33,14 +36,15 @@ export function createRateLimiter(
33
36
  return Promise.resolve();
34
37
  }
35
38
 
36
- if (queue.length >= maxQueueSize) {
39
+ if (pendingCount >= maxQueueSize) {
37
40
  return Promise.reject(
38
41
  new Error(`[Accessio] Rate limiter queue size exceeded maxQueueSize (${maxQueueSize})`),
39
42
  );
40
43
  }
41
44
 
42
45
  return new Promise((resolve, reject) => {
43
- queue.push({ resolve, reject });
46
+ queue[tail++] = { resolve, reject };
47
+ pendingCount++;
44
48
  });
45
49
  }
46
50
 
@@ -51,9 +55,12 @@ export function createRateLimiter(
51
55
 
52
56
  active--;
53
57
 
54
- if (queue.length > 0 && active < maxConcurrent) {
58
+ if (pendingCount > 0 && active < maxConcurrent) {
55
59
  active++;
56
- const next = queue.shift();
60
+ const next = queue[head];
61
+ delete queue[head];
62
+ head++;
63
+ pendingCount--;
57
64
  next?.resolve();
58
65
  }
59
66
  }
@@ -61,8 +68,11 @@ export function createRateLimiter(
61
68
  function destroy(): void {
62
69
  destroyed = true;
63
70
  const reason = new Error('[Accessio] Rate limiter destroyed — pending request cancelled');
64
- while (queue.length > 0) {
65
- const next = queue.shift();
71
+ while (pendingCount > 0) {
72
+ const next = queue[head];
73
+ delete queue[head];
74
+ head++;
75
+ pendingCount--;
66
76
  next?.reject(reason);
67
77
  }
68
78
  }
@@ -72,7 +82,7 @@ export function createRateLimiter(
72
82
  release,
73
83
  destroy,
74
84
  get pending() {
75
- return queue.length;
85
+ return pendingCount;
76
86
  },
77
87
  get active() {
78
88
  return active;
@@ -0,0 +1,25 @@
1
+ export function toFormData(obj: any, form?: FormData, namespace?: string): FormData {
2
+ const fd = form || new FormData();
3
+ let formKey: string;
4
+
5
+ if (obj === null || obj === undefined) {
6
+ return fd;
7
+ }
8
+
9
+ if (obj instanceof Date) {
10
+ fd.append(namespace || '', obj.toISOString());
11
+ } else if (typeof obj === 'object' && !(obj instanceof File) && !(obj instanceof Blob)) {
12
+ Object.keys(obj).forEach((key) => {
13
+ if (Array.isArray(obj)) {
14
+ formKey = namespace ? `${namespace}[${key}]` : key;
15
+ } else {
16
+ formKey = namespace ? `${namespace}.${key}` : key;
17
+ }
18
+ toFormData(obj[key], fd, formKey);
19
+ });
20
+ } else {
21
+ fd.append(namespace || '', obj);
22
+ }
23
+
24
+ return fd;
25
+ }
@@ -1,12 +1,12 @@
1
1
  import AccessioError from '../core/accessioError';
2
2
  import type { TransformFunction, AccessioRequestConfig } from '../types';
3
3
 
4
- export default function transformData(
4
+ export default async function transformData(
5
5
  transforms: TransformFunction | TransformFunction[] | undefined,
6
6
  data: unknown,
7
- headers: Record<string, string>,
7
+ headers: Record<string, string | string[]>,
8
8
  config?: AccessioRequestConfig,
9
- ): unknown {
9
+ ): Promise<unknown> {
10
10
  if (!transforms || !Array.isArray(transforms)) {
11
11
  return data;
12
12
  }
@@ -16,7 +16,7 @@ export default function transformData(
16
16
  for (const transform of transforms) {
17
17
  if (typeof transform === 'function') {
18
18
  try {
19
- result = transform(result, headers);
19
+ result = await transform(result, headers);
20
20
  } catch (err) {
21
21
  throw AccessioError.from(
22
22
  err instanceof Error ? err : new Error(String(err)),
package/src/index.ts CHANGED
@@ -7,7 +7,7 @@ import InterceptorManager from './interceptors/interceptorManager';
7
7
  import { createRateLimiter } from './helpers/rateLimiter';
8
8
  import { logRequest, logResponse, logError } from './helpers/debug';
9
9
  import { ERR_CANCELED } from './constants/errorCodes';
10
- import type { AccessioRequestConfig, AccessioResponse } from './types';
10
+ import type { AccessioRequestConfig } from './types';
11
11
 
12
12
  const PUBLIC_METHODS = [
13
13
  'request',
@@ -22,6 +22,9 @@ const PUBLIC_METHODS = [
22
22
  'postForm',
23
23
  'putForm',
24
24
  'patchForm',
25
+ 'stream',
26
+ 'autoPaginate',
27
+ 'gql',
25
28
  ];
26
29
 
27
30
  function createInstance(defaultConfig: AccessioRequestConfig) {
package/src/types.ts CHANGED
@@ -11,7 +11,10 @@ export interface AuthConfig {
11
11
 
12
12
  export type ParamsSerializer = (params: Record<string, unknown>) => string;
13
13
 
14
- export type TransformFunction = (data: unknown, headers: Record<string, string>) => unknown;
14
+ export type TransformFunction = (
15
+ data: unknown,
16
+ headers: Record<string, string | string[]>,
17
+ ) => unknown | Promise<unknown>;
15
18
 
16
19
  export type RetryConditionFunction = (error: AccessioError) => boolean;
17
20
 
@@ -40,6 +43,24 @@ export interface Interceptors {
40
43
  response: InterceptorManager;
41
44
  }
42
45
 
46
+ export interface AccessioHooks {
47
+ onBeforeRequest?: (config: AccessioRequestConfig) => void | Promise<void>;
48
+ onRequestResponse?: (response: AccessioResponse) => void | Promise<void>;
49
+ onRequestError?: (error: AccessioError) => void | Promise<void>;
50
+ }
51
+
52
+ export interface CacheProvider {
53
+ get: (key: string) => Promise<any> | any;
54
+ set: (key: string, value: any, ttl?: number) => Promise<void> | void;
55
+ delete: (key: string) => Promise<void> | void;
56
+ clear: () => Promise<void> | void;
57
+ }
58
+
59
+ export interface SchemaValidator<T = any> {
60
+ parse(data: unknown): T;
61
+ parseAsync?(data: unknown): Promise<T>;
62
+ }
63
+
43
64
  export interface AccessioRequestConfig {
44
65
  url?: string;
45
66
  baseURL?: string;
@@ -47,7 +68,7 @@ export interface AccessioRequestConfig {
47
68
  params?: Record<string, unknown>;
48
69
  paramsSerializer?: ParamsSerializer;
49
70
  data?: unknown;
50
- headers?: Record<string, string> | Record<string, Record<string, string>>;
71
+ headers?: Record<string, string | string[]> | Record<string, Record<string, string | string[]>>;
51
72
  auth?: AuthConfig;
52
73
  timeout?: number;
53
74
  withCredentials?: boolean;
@@ -66,13 +87,21 @@ export interface AccessioRequestConfig {
66
87
  maxContentLength?: number;
67
88
  dispatcher?: unknown;
68
89
  agent?: unknown;
90
+ dedupe?: boolean;
91
+ cache?: boolean | CacheProvider;
92
+ cacheTTL?: number;
93
+ onDownloadProgress?: (progressEvent: { loaded: number; total: number }) => void;
94
+ hooks?: AccessioHooks;
95
+ schema?: SchemaValidator;
96
+ fetch?: typeof fetch;
97
+ retryOn429?: boolean;
69
98
  }
70
99
 
71
100
  export interface AccessioResponse<T = unknown> {
72
101
  data: T;
73
102
  status: number;
74
103
  statusText: string;
75
- headers: Record<string, string>;
104
+ headers: Record<string, string | string[]>;
76
105
  config: AccessioRequestConfig;
77
106
  request: Response;
78
107
  duration: number;