@vuevox/sdk 0.3.0 → 0.5.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.
package/dist/client.js CHANGED
@@ -4,12 +4,15 @@ export function createVueVoxClient(options) {
4
4
  const baseUrl = trimTrailingSlash(options.baseUrl ?? "https://api.vuevox.com");
5
5
  const fetchFn = options.fetch ?? fetch;
6
6
  const raw = createClient({ baseUrl, fetch: fetchFn });
7
+ const retries = options.retries ?? 0;
8
+ const retryBaseDelayMs = options.retryBaseDelayMs ?? 250;
9
+ const retryMaxDelayMs = options.retryMaxDelayMs ?? 2_000;
7
10
  let cachedToken = null;
8
11
  async function getAccessToken() {
9
12
  if (cachedToken && Date.now() < cachedToken.expiresAt - 30_000) {
10
13
  return cachedToken.accessToken;
11
14
  }
12
- const response = await fetchFn(`${baseUrl}/oauth/token`, {
15
+ const result = await requestJson("POST", "/oauth/token", {
13
16
  method: "POST",
14
17
  headers: {
15
18
  "Content-Type": "application/json",
@@ -21,57 +24,95 @@ export function createVueVoxClient(options) {
21
24
  scope: formatScope(options.scope),
22
25
  }),
23
26
  });
24
- const body = await parseJson(response);
25
- const requestId = getRequestId(response, body);
26
- notifyResponse(options, "POST", "/oauth/token", response, requestId);
27
- if (!response.ok) {
28
- const error = isErrorResponse(body) ? body.error : null;
29
- throw new VueVoxApiError(response.status, error?.code ?? "token_request_failed", error?.message ?? "VueVox token request failed.", isErrorResponse(body) ? body : undefined, requestId);
30
- }
31
- if (!isTokenResponse(body)) {
32
- throw new VueVoxApiError(response.status, "invalid_token_response", "VueVox returned an invalid token response.", undefined, requestId);
33
- }
27
+ const body = result.data;
34
28
  cachedToken = {
35
29
  accessToken: body.access_token,
36
30
  expiresAt: Date.now() + body.expires_in * 1000,
37
31
  };
38
32
  return cachedToken.accessToken;
39
33
  }
40
- async function hello() {
41
- const accessToken = await getAccessToken();
42
- const { data, error, response } = await raw.GET("/v1/hello", {
43
- headers: {
44
- Authorization: `Bearer ${accessToken}`,
45
- },
46
- });
47
- const requestId = getRequestId(response, error);
48
- notifyResponse(options, "GET", "/v1/hello", response, requestId);
49
- if (error) {
50
- throw new VueVoxApiError(response.status, error.error.code, error.error.message, error, requestId);
51
- }
52
- return withMetadata(data, response, requestId);
34
+ async function getHello() {
35
+ return apiGet("/v1/hello");
53
36
  }
54
37
  async function listSpaces(listOptions = {}) {
38
+ return apiGet("/v1/spaces", listOptions);
39
+ }
40
+ async function listAgents(listOptions = {}) {
41
+ return apiGet("/v1/agents", listOptions);
42
+ }
43
+ async function listCalls(listOptions = {}) {
44
+ return apiGet("/v1/calls", listOptions);
45
+ }
46
+ async function getCall(callId) {
47
+ return apiGet(`/v1/calls/${encodeURIComponent(callId)}`);
48
+ }
49
+ async function listLeads(listOptions = {}) {
50
+ return apiGet("/v1/leads", listOptions);
51
+ }
52
+ async function getLead(leadId) {
53
+ return apiGet(`/v1/leads/${encodeURIComponent(leadId)}`);
54
+ }
55
+ async function apiGet(path, query) {
55
56
  const accessToken = await getAccessToken();
56
- const { data, error, response } = await raw.GET("/v1/spaces", {
57
- params: {
58
- query: listOptions,
59
- },
57
+ const result = await requestJson("GET", path, {
58
+ method: "GET",
60
59
  headers: {
61
60
  Authorization: `Bearer ${accessToken}`,
62
61
  },
62
+ query,
63
63
  });
64
- const requestId = getRequestId(response, error);
65
- notifyResponse(options, "GET", "/v1/spaces", response, requestId);
66
- if (error) {
67
- throw new VueVoxApiError(response.status, error.error.code, error.error.message, error, requestId);
64
+ return withMetadata(result.data, result.response, result.requestId);
65
+ }
66
+ async function requestJson(method, path, init) {
67
+ for (let attempt = 0;; attempt++) {
68
+ const response = await fetchFn(buildUrl(baseUrl, path, init.query), init);
69
+ const body = await parseJson(response);
70
+ const requestId = getRequestId(response, body);
71
+ const retryAfter = retryAfterSeconds(response);
72
+ notifyResponse(options, method, path, response, requestId, retryAfter);
73
+ if (response.ok) {
74
+ if (body === null || isErrorResponse(body)) {
75
+ throw new VueVoxApiError(response.status, "invalid_response", "VueVox returned an invalid response.", undefined, requestId, retryAfter);
76
+ }
77
+ return { data: body, requestId, response };
78
+ }
79
+ if (attempt < retries && shouldRetry(response.status)) {
80
+ await sleep(retryDelayMs(attempt, retryAfter));
81
+ continue;
82
+ }
83
+ const error = isErrorResponse(body) ? body.error : null;
84
+ throw new VueVoxApiError(response.status, error?.code ?? "api_request_failed", error?.message ?? "VueVox API request failed.", isErrorResponse(body) ? body : undefined, requestId, retryAfter);
85
+ }
86
+ }
87
+ function retryDelayMs(attempt, retryAfter) {
88
+ if (retryAfter !== undefined) {
89
+ return retryAfter * 1000;
68
90
  }
69
- return withMetadata(data, response, requestId);
91
+ return Math.min(retryBaseDelayMs * 2 ** attempt, retryMaxDelayMs);
70
92
  }
71
93
  return {
72
94
  getAccessToken,
73
- hello,
74
- listSpaces,
95
+ hello: {
96
+ get: getHello,
97
+ },
98
+ spaces: {
99
+ list: listSpaces,
100
+ paginate: (listOptions = {}) => paginate(listSpaces, listOptions),
101
+ },
102
+ agents: {
103
+ list: listAgents,
104
+ paginate: (listOptions = {}) => paginate(listAgents, listOptions),
105
+ },
106
+ calls: {
107
+ list: listCalls,
108
+ get: getCall,
109
+ paginate: (listOptions = {}) => paginate(listCalls, listOptions),
110
+ },
111
+ leads: {
112
+ list: listLeads,
113
+ get: getLead,
114
+ paginate: (listOptions = {}) => paginate(listLeads, listOptions),
115
+ },
75
116
  raw,
76
117
  };
77
118
  }
@@ -84,6 +125,15 @@ function formatScope(scope) {
84
125
  function trimTrailingSlash(value) {
85
126
  return value.replace(/\/+$/, "");
86
127
  }
128
+ function buildUrl(baseUrl, path, query) {
129
+ const url = new URL(`${baseUrl}${path}`);
130
+ for (const [key, value] of Object.entries(query ?? {})) {
131
+ if ((typeof value === "string" || typeof value === "number") && value !== "") {
132
+ url.searchParams.set(key, String(value));
133
+ }
134
+ }
135
+ return url.toString();
136
+ }
87
137
  async function parseJson(response) {
88
138
  try {
89
139
  return (await response.json());
@@ -93,16 +143,19 @@ async function parseJson(response) {
93
143
  }
94
144
  }
95
145
  function isTokenResponse(value) {
96
- return Boolean(value && "access_token" in value);
146
+ return isObject(value) && "access_token" in value;
97
147
  }
98
148
  function isErrorResponse(value) {
99
- return Boolean(value && "error" in value);
149
+ return isObject(value) && "error" in value;
100
150
  }
101
151
  function getRequestId(response, body) {
102
152
  return response.headers.get("X-Request-Id") ?? (isErrorBody(body) ? body.error.requestId : undefined);
103
153
  }
104
154
  function isErrorBody(value) {
105
- return Boolean(value && "error" in value);
155
+ return isObject(value) && "error" in value;
156
+ }
157
+ function isObject(value) {
158
+ return typeof value === "object" && value !== null;
106
159
  }
107
160
  function withMetadata(data, response, requestId) {
108
161
  return {
@@ -111,11 +164,40 @@ function withMetadata(data, response, requestId) {
111
164
  status: response.status,
112
165
  };
113
166
  }
114
- function notifyResponse(options, method, path, response, requestId) {
167
+ function notifyResponse(options, method, path, response, requestId, retryAfter) {
115
168
  options.onResponse?.({
116
169
  method,
117
170
  path,
118
171
  requestId,
172
+ retryAfter,
119
173
  status: response.status,
120
174
  });
121
175
  }
176
+ async function* paginate(list, listOptions) {
177
+ let cursor = listOptions.cursor;
178
+ do {
179
+ const response = await list({ ...listOptions, cursor });
180
+ for (const item of response.data.data) {
181
+ yield item;
182
+ }
183
+ cursor = response.data.pagination.nextCursor ?? undefined;
184
+ } while (cursor);
185
+ }
186
+ function shouldRetry(status) {
187
+ return [429, 500, 502, 503, 504].includes(status);
188
+ }
189
+ function retryAfterSeconds(response) {
190
+ const value = response.headers.get("Retry-After");
191
+ if (!value) {
192
+ return undefined;
193
+ }
194
+ const seconds = Number(value);
195
+ if (Number.isFinite(seconds)) {
196
+ return Math.max(0, seconds);
197
+ }
198
+ const timestamp = Date.parse(value);
199
+ return Number.isNaN(timestamp) ? undefined : Math.max(0, Math.ceil((timestamp - Date.now()) / 1000));
200
+ }
201
+ function sleep(ms) {
202
+ return new Promise((resolve) => setTimeout(resolve, ms));
203
+ }
package/dist/errors.d.ts CHANGED
@@ -3,7 +3,10 @@ export type VueVoxErrorResponse = components["schemas"]["ErrorResponse"];
3
3
  export declare class VueVoxApiError extends Error {
4
4
  readonly status: number;
5
5
  readonly code: string;
6
+ readonly details?: Record<string, unknown>;
7
+ readonly isRateLimited: boolean;
6
8
  readonly requestId?: string;
9
+ readonly retryAfter?: number;
7
10
  readonly response?: VueVoxErrorResponse;
8
- constructor(status: number, code: string, message: string, response?: VueVoxErrorResponse, requestId?: string);
11
+ constructor(status: number, code: string, message: string, response?: VueVoxErrorResponse, requestId?: string, retryAfter?: number);
9
12
  }
package/dist/errors.js CHANGED
@@ -1,14 +1,20 @@
1
1
  export class VueVoxApiError extends Error {
2
2
  status;
3
3
  code;
4
+ details;
5
+ isRateLimited;
4
6
  requestId;
7
+ retryAfter;
5
8
  response;
6
- constructor(status, code, message, response, requestId) {
9
+ constructor(status, code, message, response, requestId, retryAfter) {
7
10
  super(message);
8
11
  this.name = "VueVoxApiError";
9
12
  this.status = status;
10
13
  this.code = code;
14
+ this.details = response?.error.details;
15
+ this.isRateLimited = status === 429 || code === "rate_limited";
11
16
  this.requestId = requestId ?? response?.error.requestId;
17
+ this.retryAfter = retryAfter;
12
18
  this.response = response;
13
19
  }
14
20
  }