@pellux/goodvibes-transport-http 0.18.3 → 0.30.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.
@@ -1 +1 @@
1
- {"version":3,"file":"http-core.d.ts","sourceRoot":"","sources":["../src/http-core.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"http-core.d.ts","sourceRoot":"","sources":["../src/http-core.ts"],"names":[],"mappings":"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"}
package/dist/http-core.js CHANGED
@@ -1,88 +1,90 @@
1
- // Synced from goodvibes-tui/src/runtime/transports/http-json-transport.ts
1
+ import { ConfigurationError, ContractError, GoodVibesSdkError, HttpStatusError, createHttpStatusError } from '@pellux/goodvibes-errors';
2
2
  import { sleepWithSignal } from './backoff.js';
3
- import { mergeHeaders, resolveAuthToken, resolveHeaders } from './auth.js';
4
- import { getHttpRetryDelay, isRetryableHttpStatus, isRetryableNetworkError, resolveHttpRetryPolicy, } from './retry.js';
3
+ import { mergeHeaderRecord, normalizeAuthToken, resolveAuthToken, resolveHeaders } from './auth.js';
4
+ import { applyPerMethodPolicy, getHttpRetryDelay, isRetryableHttpStatus, isRetryableNetworkError, resolveHttpRetryPolicy, } from './retry.js';
5
5
  import { buildUrl, createTransportPaths } from './paths.js';
6
+ import { composeMiddleware, createUuidV4, injectTraceparentAsync, invokeTransportObserver, transportErrorFromUnknown, } from '@pellux/goodvibes-transport-core';
7
+ /**
8
+ * Generate a UUID v4 idempotency key.
9
+ * Uses `crypto.randomUUID()` when available (Bun, browsers, Workers, Node 14.17+).
10
+ * Falls back to a manual RFC 4122 v4 implementation otherwise.
11
+ */
12
+ export function generateIdempotencyKey() {
13
+ return createUuidV4();
14
+ }
15
+ /** Methods that are safe to send idempotency keys for (all non-GET requests). */
16
+ const IDEMPOTENCY_KEY_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
6
17
  function isPlainObject(value) {
7
18
  return typeof value === 'object' && value !== null && !Array.isArray(value);
8
19
  }
9
- function isRecord(value) {
10
- return typeof value === 'object' && value !== null;
11
- }
12
- function applyHeaderSource(target, source) {
13
- if (!source)
14
- return;
15
- if (source instanceof Headers) {
16
- source.forEach((value, key) => {
17
- target[key] = value;
18
- });
19
- return;
20
- }
21
- if (Array.isArray(source)) {
22
- for (const [key, value] of source) {
23
- target[key] = value;
24
- }
25
- return;
26
- }
27
- for (const [key, value] of Object.entries(source)) {
28
- if (value !== undefined) {
29
- target[key] = value;
30
- }
31
- }
32
- }
33
- function mergeHeaderRecord(...sources) {
34
- const merged = {};
35
- for (const source of sources) {
36
- applyHeaderSource(merged, source);
20
+ export function inferTransportHint(status, url, retryAfterMs) {
21
+ if (status === 0)
22
+ return `Transport could not reach ${url}. Verify the baseUrl is reachable.`;
23
+ if (status === 401)
24
+ return 'Check your authentication token or credentials.';
25
+ if (status === 403)
26
+ return 'Valid credentials but insufficient permissions for this operation.';
27
+ if (status === 404)
28
+ return 'The requested resource was not found.';
29
+ if (status === 408)
30
+ return 'The request timed out. Consider retrying.';
31
+ if (status === 429) {
32
+ return retryAfterMs !== undefined
33
+ ? `Rate limit exceeded. Retry after ${retryAfterMs}ms.`
34
+ : 'Rate limit exceeded. Back off and retry.';
37
35
  }
38
- return merged;
36
+ if (status >= 500)
37
+ return 'Remote server error. The service may be temporarily unavailable.';
38
+ return undefined;
39
39
  }
40
- function readErrorMessage(status, url, body) {
41
- if (isRecord(body) && typeof body.error === 'string' && body.error.trim()) {
42
- return body.error.trim();
43
- }
44
- if (typeof body === 'string' && body.trim()) {
45
- return body.trim();
46
- }
47
- return `Transport request failed with status ${status} for ${url}`;
48
- }
49
- function createTransportError(status, url, method, body) {
50
- return Object.assign(new Error(readErrorMessage(status, url, body)), {
51
- transport: {
52
- status,
53
- body,
54
- url,
55
- method,
56
- },
40
+ export function createTransportError(status, url, method, body, retryAfterMs) {
41
+ const inferred = inferTransportHint(status, url, retryAfterMs);
42
+ const baseError = createHttpStatusError(status, url, method, body, inferred);
43
+ const transportPayload = {
44
+ status,
45
+ body,
46
+ url,
47
+ method,
48
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
49
+ };
50
+ return Object.assign(baseError, {
51
+ transport: transportPayload,
52
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
57
53
  });
58
54
  }
59
- function createNetworkTransportError(error, url, method) {
55
+ export function createNetworkTransportError(error, url, method) {
60
56
  const message = error instanceof Error && error.message.trim()
61
57
  ? error.message.trim()
62
58
  : `Transport request failed before receiving a response for ${url}`;
63
- return Object.assign(new Error(message), {
59
+ const hint = `Transport could not reach ${url}. Verify the baseUrl is reachable.`;
60
+ const networkError = new HttpStatusError(message, {
61
+ category: 'network',
62
+ source: 'transport',
63
+ recoverable: true,
64
+ url,
65
+ method,
66
+ body: { error: message },
67
+ hint,
64
68
  cause: error,
65
- transport: {
66
- status: 0,
67
- body: { error: message },
68
- url,
69
- method,
70
- },
71
69
  });
70
+ const transportPayload = {
71
+ status: 0,
72
+ body: { error: message },
73
+ url,
74
+ method,
75
+ cause: error,
76
+ };
77
+ return Object.assign(networkError, { transport: transportPayload });
72
78
  }
73
79
  function toStringValue(value, key) {
74
80
  if (value === undefined || value === null) {
75
- throw new Error(`Missing required path parameter "${key}"`);
81
+ throw new ContractError(`Missing required path parameter "${key}". Ensure the input object includes a non-null value for this field before invoking the route.`);
76
82
  }
77
83
  return String(value);
78
84
  }
79
85
  function addQueryValue(url, key, value) {
80
- if (value === undefined)
81
- return;
82
- if (value === null) {
83
- url.searchParams.append(key, 'null');
86
+ if (value === undefined || value === null)
84
87
  return;
85
- }
86
88
  if (Array.isArray(value)) {
87
89
  for (const item of value) {
88
90
  addQueryValue(url, key, item);
@@ -90,18 +92,28 @@ function addQueryValue(url, key, value) {
90
92
  return;
91
93
  }
92
94
  if (typeof value === 'object') {
95
+ // Contract query parameters are primitive or repeated primitive values.
96
+ // Object values are preserved as JSON strings so callers do not silently
97
+ // lose structured filters when a route explicitly accepts them.
93
98
  url.searchParams.append(key, JSON.stringify(value));
94
99
  return;
95
100
  }
96
101
  url.searchParams.append(key, String(value));
97
102
  }
103
+ function hasHeader(headers, name) {
104
+ const normalized = name.toLowerCase();
105
+ return Object.keys(headers).some((key) => key.toLowerCase() === normalized);
106
+ }
98
107
  function splitContractInput(path, input = {}) {
99
108
  const remaining = { ...input };
100
- const interpolatedPath = path.replace(/\{([^}]+)\}/g, (_match, key) => {
109
+ const interpolatedPath = path.replace(/\{([A-Za-z_][A-Za-z0-9_.-]*)\}/g, (_match, key) => {
101
110
  const value = toStringValue(remaining[key], key);
102
111
  delete remaining[key];
103
112
  return encodeURIComponent(value);
104
113
  });
114
+ if (/[{}]/.test(interpolatedPath)) {
115
+ throw new ContractError(`Malformed contract path "${path}". Path parameters must use "{name}" with identifier-like names.`);
116
+ }
105
117
  return { interpolatedPath, remaining };
106
118
  }
107
119
  export function createJsonRequestInit(token, body, method = 'GET', headers = {}, signal, defaultHeaders = {}) {
@@ -117,10 +129,27 @@ export const createJsonInit = createJsonRequestInit;
117
129
  export function createFetch(fetchImpl, fallbackFetch) {
118
130
  const resolved = fetchImpl ?? fallbackFetch ?? globalThis.fetch;
119
131
  if (typeof resolved !== 'function') {
120
- throw new Error('Fetch implementation is required');
132
+ throw new ConfigurationError('Fetch implementation is required. Pass a fetch option (e.g. options.fetch) or ensure globalThis.fetch is available in your runtime.');
121
133
  }
122
134
  return resolved.bind(globalThis);
123
135
  }
136
+ function parseRetryAfterMs(headers) {
137
+ const retryAfter = headers.get('retry-after');
138
+ if (!retryAfter)
139
+ return undefined;
140
+ // Numeric seconds
141
+ const seconds = Number(retryAfter);
142
+ if (!Number.isNaN(seconds) && seconds >= 0) {
143
+ return Math.ceil(seconds * 1000);
144
+ }
145
+ // HTTP-date
146
+ const date = new Date(retryAfter);
147
+ if (!Number.isNaN(date.getTime())) {
148
+ const ms = date.getTime() - Date.now();
149
+ return ms > 0 ? ms : 0;
150
+ }
151
+ return undefined;
152
+ }
124
153
  export async function readJsonBody(response) {
125
154
  const text = await response.text();
126
155
  if (!text.trim())
@@ -128,10 +157,16 @@ export async function readJsonBody(response) {
128
157
  try {
129
158
  return JSON.parse(text);
130
159
  }
131
- catch {
160
+ catch (error) {
161
+ void error;
132
162
  return text;
133
163
  }
134
164
  }
165
+ /**
166
+ * @internal Low-level one-shot helper retained for legacy internal callers.
167
+ * Public code should use `createHttpTransport(...).requestJson()` so auth,
168
+ * middleware, retry policy, idempotency keys, and observers are applied.
169
+ */
135
170
  export async function requestJson(fetchImpl, url, init = {}) {
136
171
  let response;
137
172
  try {
@@ -142,7 +177,8 @@ export async function requestJson(fetchImpl, url, init = {}) {
142
177
  }
143
178
  const body = await readJsonBody(response);
144
179
  if (!response.ok) {
145
- throw createTransportError(response.status, url, init.method ?? 'GET', body);
180
+ const retryAfterMs = parseRetryAfterMs(response.headers);
181
+ throw createTransportError(response.status, url, init.method ?? 'GET', body, retryAfterMs);
146
182
  }
147
183
  return body;
148
184
  }
@@ -150,31 +186,156 @@ export function createHttpJsonTransport(options) {
150
186
  const baseUrl = options.baseUrl.trim();
151
187
  const fetchImpl = createFetch(options.fetchImpl, options.fetch);
152
188
  const authToken = options.authToken ?? null;
189
+ // Normalize at the boundary: downstream always works with a single resolver.
190
+ const getAuthToken = options.getAuthToken ?? normalizeAuthToken(options.authToken ?? undefined);
153
191
  const defaultHeaders = options.headers;
154
192
  const retryPolicy = options.retry;
155
193
  const paths = createTransportPaths(baseUrl);
194
+ const observer = options.observer;
195
+ // Persistent middleware chain — mutated via use().
196
+ const middlewareChain = [...(options.middleware ?? [])];
156
197
  const requestJsonForTransport = async (pathOrUrl, requestOptions = {}) => {
157
198
  const url = pathOrUrl.startsWith('http://') || pathOrUrl.startsWith('https://')
158
199
  ? pathOrUrl
159
200
  : buildUrl(baseUrl, pathOrUrl);
160
201
  const method = requestOptions.method ?? (requestOptions.body === undefined ? 'GET' : 'POST');
161
- const resolvedRetry = resolveHttpRetryPolicy(retryPolicy, requestOptions.retry);
202
+ const methodId = requestOptions.methodId;
203
+ // Resolve idempotent flag from request options (set by contract-client from contract.idempotent).
204
+ const contractIdempotent = requestOptions.idempotent === true;
205
+ // Apply per-method retry policy override if a methodId is provided.
206
+ const baseRetry = resolveHttpRetryPolicy(retryPolicy, requestOptions.retry);
207
+ const resolvedRetry = methodId ? applyPerMethodPolicy(baseRetry, methodId) : baseRetry;
208
+ // Determine idempotency: non-GET mutations without an explicit idempotent flag do NOT retry.
209
+ // This is enforced below by gating the retry check on method type.
210
+ const isMutatingMethod = IDEMPOTENCY_KEY_METHODS.has(method.toUpperCase());
211
+ const idempotencyKey = isMutatingMethod ? generateIdempotencyKey() : undefined;
162
212
  let attempt = 0;
163
213
  while (true) {
164
214
  attempt += 1;
165
- const token = await resolveAuthToken(authToken, options.getAuthToken);
166
- const headers = await resolveHeaders(defaultHeaders, options.getHeaders);
215
+ const token = (await getAuthToken()) ?? null;
216
+ const resolvedHeaders = await resolveHeaders(defaultHeaders, options.getHeaders);
217
+ // Build merged headers record: default + per-request, then inject cross-cutting headers.
218
+ const mergedHeaders = mergeHeaderRecord(resolvedHeaders ?? {}, requestOptions.headers ?? {});
219
+ if (token) {
220
+ mergedHeaders['Authorization'] = `Bearer ${token}`;
221
+ }
222
+ if (requestOptions.body !== undefined) {
223
+ mergedHeaders['Content-Type'] = 'application/json';
224
+ }
225
+ // Inject W3C traceparent if OTel is active. This path is async so pure
226
+ // ESM OpenTelemetry providers can be loaded before the request is sent.
227
+ await injectTraceparentAsync(mergedHeaders);
228
+ // Inject idempotency key for mutating methods.
229
+ if (idempotencyKey && !hasHeader(mergedHeaders, 'Idempotency-Key')) {
230
+ mergedHeaders['Idempotency-Key'] = idempotencyKey;
231
+ }
232
+ // Notify observer before dispatching the request.
233
+ invokeTransportObserver(() => observer?.onTransportActivity?.({ direction: 'send', url, kind: 'http' }));
234
+ const sendAt = Date.now();
235
+ // Build the middleware context for this attempt.
236
+ const ctx = {
237
+ method,
238
+ url,
239
+ headers: mergedHeaders,
240
+ body: requestOptions.body,
241
+ options: requestOptions,
242
+ signal: requestOptions.signal,
243
+ };
244
+ // Build the inner fetch that middleware wraps (and also used directly without middleware).
245
+ const innerFetch = async (c) => {
246
+ const init = {
247
+ method: c.method,
248
+ credentials: 'include',
249
+ signal: c.signal,
250
+ headers: c.headers,
251
+ ...(c.body !== undefined ? { body: JSON.stringify(c.body) } : {}),
252
+ };
253
+ let response;
254
+ try {
255
+ response = await fetchImpl(c.url, init);
256
+ }
257
+ catch (error) {
258
+ throw createNetworkTransportError(error, c.url, c.method);
259
+ }
260
+ const body = await readJsonBody(response);
261
+ if (!response.ok) {
262
+ const retryAfterMs = parseRetryAfterMs(response.headers);
263
+ throw createTransportError(response.status, c.url, c.method, body, retryAfterMs);
264
+ }
265
+ // Return synthetic Response carrying parsed body so callers can .json() it.
266
+ return new Response(JSON.stringify(body), { status: response.status });
267
+ };
167
268
  try {
168
- return await requestJson(fetchImpl, url, createJsonRequestInit(token, requestOptions.body, method, mergeHeaders(headers, requestOptions.headers), requestOptions.signal));
269
+ if (middlewareChain.length > 0) {
270
+ // Middleware path — compose chain around innerFetch.
271
+ const composed = composeMiddleware(middlewareChain, innerFetch);
272
+ await composed(ctx);
273
+ if (ctx.error)
274
+ throw ctx.error;
275
+ if (!ctx.response) {
276
+ throw new GoodVibesSdkError('HTTP middleware chain completed without producing a response.', {
277
+ category: 'protocol',
278
+ source: 'transport',
279
+ recoverable: false,
280
+ url,
281
+ method,
282
+ });
283
+ }
284
+ const result = await ctx.response.json();
285
+ invokeTransportObserver(() => observer?.onTransportActivity?.({
286
+ direction: 'recv',
287
+ url,
288
+ kind: 'http',
289
+ durationMs: ctx.durationMs,
290
+ }));
291
+ return result;
292
+ }
293
+ // No-middleware fast path — directly invoke innerFetch with ctx.
294
+ const rawResponse = await innerFetch(ctx);
295
+ const result = await rawResponse.json();
296
+ // Notify observer after a successful response.
297
+ invokeTransportObserver(() => observer?.onTransportActivity?.({
298
+ direction: 'recv',
299
+ url,
300
+ kind: 'http',
301
+ durationMs: Date.now() - sendAt,
302
+ }));
303
+ return result;
169
304
  }
170
305
  catch (error) {
171
- const status = typeof error === 'object' && error !== null && 'transport' in error
172
- ? error.transport?.status
306
+ // Wrap middleware errors as SDKError{kind:'unknown'} with middleware identity in cause.
307
+ // ALL errors originating from the middleware chain are wrapped — including HttpStatusError.
308
+ const wrappedError = (() => {
309
+ if (ctx.middlewareError) {
310
+ // Error came from within the middleware chain — wrap regardless of error type.
311
+ const msg = transportErrorFromUnknown(error, 'transport middleware error').message;
312
+ const middlewareName = ctx.activeMiddlewareName ?? 'unknown';
313
+ const wrapped = new GoodVibesSdkError(`Transport middleware error: ${msg}`, {
314
+ category: 'unknown',
315
+ source: 'transport',
316
+ recoverable: false,
317
+ cause: { middleware: middlewareName, originalError: error },
318
+ });
319
+ return wrapped;
320
+ }
321
+ if (error instanceof GoodVibesSdkError)
322
+ return error;
323
+ return error;
324
+ })();
325
+ // Notify observer of the transport error before deciding to retry or rethrow.
326
+ invokeTransportObserver(() => observer?.onError?.(transportErrorFromUnknown(wrappedError, 'HTTP transport error')));
327
+ const status = typeof wrappedError === 'object' && wrappedError !== null && 'transport' in wrappedError
328
+ ? wrappedError.transport?.status
173
329
  : undefined;
174
- const shouldRetry = attempt < resolvedRetry.maxAttempts && ((typeof status === 'number' && status > 0 && isRetryableHttpStatus(method, status, resolvedRetry))
330
+ // Mutating methods (POST/PUT/PATCH/DELETE) without idempotent contract mark:
331
+ // do NOT retry on 5xx to avoid duplicate side effects.
332
+ // Precedence: explicit perMethodPolicy > contract.idempotent flag > HTTP-verb default.
333
+ const hasPerMethodOverride = methodId !== undefined && baseRetry.perMethodPolicy[methodId] !== undefined;
334
+ const canRetry = !isMutatingMethod || hasPerMethodOverride || contractIdempotent;
335
+ const shouldRetry = canRetry && attempt < resolvedRetry.maxAttempts && ((typeof status === 'number' && status > 0 && isRetryableHttpStatus(method, status, resolvedRetry))
175
336
  || (typeof status === 'number' && status === 0 && isRetryableNetworkError(method, resolvedRetry)));
176
337
  if (!shouldRetry) {
177
- throw error;
338
+ throw wrappedError;
178
339
  }
179
340
  await sleepWithSignal(getHttpRetryDelay(attempt + 1, resolvedRetry), requestOptions.signal);
180
341
  }
@@ -209,9 +370,12 @@ export function createHttpJsonTransport(options) {
209
370
  return buildUrl(baseUrl, path);
210
371
  },
211
372
  async getAuthToken() {
212
- return await resolveAuthToken(authToken, options.getAuthToken);
373
+ return (await getAuthToken()) ?? null;
213
374
  },
214
375
  requestJson: requestJsonForTransport,
215
376
  resolveContractRequest,
377
+ use(middleware) {
378
+ middlewareChain.push(middleware);
379
+ },
216
380
  };
217
381
  }
package/dist/http.d.ts CHANGED
@@ -2,11 +2,11 @@ import { type AuthTokenResolver, type HeaderResolver, type MaybePromise, mergeHe
2
2
  import { type BackoffPolicy, type ResolvedBackoffPolicy, computeBackoffDelay, normalizeBackoffPolicy, sleepWithSignal } from './backoff.js';
3
3
  import { type HttpRetryPolicy, type ResolvedHttpRetryPolicy, DEFAULT_HTTP_RETRY_POLICY, getHttpRetryDelay, isRetryableHttpStatus, isRetryableNetworkError, normalizeHttpRetryPolicy, resolveHttpRetryPolicy } from './retry.js';
4
4
  import { type ResolvedStreamReconnectPolicy, type StreamReconnectPolicy, DEFAULT_STREAM_RECONNECT_POLICY, getStreamReconnectDelay, normalizeStreamReconnectPolicy } from './reconnect.js';
5
- import { createFetch, createJsonInit, createJsonRequestInit, readJsonBody, type HttpJsonRequestOptions, type HttpJsonTransport, type HttpJsonTransportOptions, type JsonObject, type JsonValue, type ResolvedContractRequest } from './http-core.js';
6
- export type { AuthTokenResolver, BackoffPolicy, HeaderResolver, HttpJsonRequestOptions, HttpRetryPolicy, JsonObject, JsonValue, MaybePromise, ResolvedBackoffPolicy, ResolvedContractRequest, ResolvedHttpRetryPolicy, ResolvedStreamReconnectPolicy, StreamReconnectPolicy, };
5
+ import { createFetch, createJsonInit, createJsonRequestInit, generateIdempotencyKey, readJsonBody, requestJson, type HttpJsonRequestOptions, type HttpJsonTransport, type HttpJsonTransportOptions, type JsonObject, type JsonValue, type ResolvedContractRequest, type TransportContext, type TransportMiddleware, type TransportJsonError } from './http-core.js';
6
+ export type { AuthTokenResolver, BackoffPolicy, HeaderResolver, HttpJsonRequestOptions, HttpRetryPolicy, JsonObject, JsonValue, MaybePromise, ResolvedBackoffPolicy, ResolvedContractRequest, ResolvedHttpRetryPolicy, ResolvedStreamReconnectPolicy, StreamReconnectPolicy, TransportContext, TransportJsonError, TransportMiddleware, };
7
7
  export type HttpTransportOptions = HttpJsonTransportOptions;
8
8
  export type HttpTransport = HttpJsonTransport;
9
- export { createFetch, createJsonInit, createJsonRequestInit, readJsonBody, mergeHeaders, resolveAuthToken, resolveHeaders, computeBackoffDelay, normalizeBackoffPolicy, sleepWithSignal, DEFAULT_HTTP_RETRY_POLICY, getHttpRetryDelay, isRetryableHttpStatus, isRetryableNetworkError, normalizeHttpRetryPolicy, resolveHttpRetryPolicy, DEFAULT_STREAM_RECONNECT_POLICY, getStreamReconnectDelay, normalizeStreamReconnectPolicy, };
9
+ export { createFetch, createJsonInit, createJsonRequestInit, generateIdempotencyKey, readJsonBody, requestJson, mergeHeaders, resolveAuthToken, resolveHeaders, computeBackoffDelay, normalizeBackoffPolicy, sleepWithSignal, DEFAULT_HTTP_RETRY_POLICY, getHttpRetryDelay, isRetryableHttpStatus, isRetryableNetworkError, normalizeHttpRetryPolicy, resolveHttpRetryPolicy, DEFAULT_STREAM_RECONNECT_POLICY, getStreamReconnectDelay, normalizeStreamReconnectPolicy, };
10
10
  export declare function normalizeTransportError(error: unknown): Error;
11
11
  export declare function createHttpTransport(options: HttpTransportOptions): HttpTransport;
12
12
  //# sourceMappingURL=http.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACf,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,qBAAqB,EAC1B,mBAAmB,EACnB,sBAAsB,EACtB,eAAe,EAChB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,yBAAyB,EACzB,iBAAiB,EACjB,qBAAqB,EACrB,uBAAuB,EACvB,wBAAwB,EACxB,sBAAsB,EACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,KAAK,6BAA6B,EAClC,KAAK,qBAAqB,EAC1B,+BAA+B,EAC/B,uBAAuB,EACvB,8BAA8B,EAC/B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,WAAW,EAEX,cAAc,EACd,qBAAqB,EACrB,YAAY,EACZ,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,uBAAuB,EAC7B,MAAM,gBAAgB,CAAC;AAExB,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,sBAAsB,EACtB,eAAe,EACf,UAAU,EACV,SAAS,EACT,YAAY,EACZ,qBAAqB,EACrB,uBAAuB,EACvB,uBAAuB,EACvB,6BAA6B,EAC7B,qBAAqB,GACtB,CAAC;AACF,MAAM,MAAM,oBAAoB,GAAG,wBAAwB,CAAC;AAC5D,MAAM,MAAM,aAAa,GAAG,iBAAiB,CAAC;AAC9C,OAAO,EACL,WAAW,EACX,cAAc,EACd,qBAAqB,EACrB,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,mBAAmB,EACnB,sBAAsB,EACtB,eAAe,EACf,yBAAyB,EACzB,iBAAiB,EACjB,qBAAqB,EACrB,uBAAuB,EACvB,wBAAwB,EACxB,sBAAsB,EACtB,+BAA+B,EAC/B,uBAAuB,EACvB,8BAA8B,GAC/B,CAAC;AAsBF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAkB7D;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CAmBhF"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACf,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,qBAAqB,EAC1B,mBAAmB,EACnB,sBAAsB,EACtB,eAAe,EAChB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,yBAAyB,EACzB,iBAAiB,EACjB,qBAAqB,EACrB,uBAAuB,EACvB,wBAAwB,EACxB,sBAAsB,EACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,KAAK,6BAA6B,EAClC,KAAK,qBAAqB,EAC1B,+BAA+B,EAC/B,uBAAuB,EACvB,8BAA8B,EAC/B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,WAAW,EAEX,cAAc,EACd,qBAAqB,EACrB,sBAAsB,EAEtB,YAAY,EACZ,WAAW,EACX,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,uBAAuB,EAC5B,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACxB,MAAM,gBAAgB,CAAC;AAExB,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,sBAAsB,EACtB,eAAe,EACf,UAAU,EACV,SAAS,EACT,YAAY,EACZ,qBAAqB,EACrB,uBAAuB,EACvB,uBAAuB,EACvB,6BAA6B,EAC7B,qBAAqB,EACrB,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,GACpB,CAAC;AACF,MAAM,MAAM,oBAAoB,GAAG,wBAAwB,CAAC;AAC5D,MAAM,MAAM,aAAa,GAAG,iBAAiB,CAAC;AAC9C,OAAO,EACL,WAAW,EACX,cAAc,EACd,qBAAqB,EACrB,sBAAsB,EACtB,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,mBAAmB,EACnB,sBAAsB,EACtB,eAAe,EACf,yBAAyB,EACzB,iBAAiB,EACjB,qBAAqB,EACrB,uBAAuB,EACvB,wBAAwB,EACxB,sBAAsB,EACtB,+BAA+B,EAC/B,uBAAuB,EACvB,8BAA8B,GAC/B,CAAC;AAwBF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CA8D7D;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CAmBhF"}
package/dist/http.js CHANGED
@@ -1,10 +1,10 @@
1
- import { ConfigurationError, ContractError, createHttpStatusError } from '@pellux/goodvibes-errors';
1
+ import { ConfigurationError, ContractError, GoodVibesSdkError, HttpStatusError, createHttpStatusError } from '@pellux/goodvibes-errors';
2
2
  import { mergeHeaders, resolveAuthToken, resolveHeaders, } from './auth.js';
3
3
  import { computeBackoffDelay, normalizeBackoffPolicy, sleepWithSignal, } from './backoff.js';
4
4
  import { DEFAULT_HTTP_RETRY_POLICY, getHttpRetryDelay, isRetryableHttpStatus, isRetryableNetworkError, normalizeHttpRetryPolicy, resolveHttpRetryPolicy, } from './retry.js';
5
5
  import { DEFAULT_STREAM_RECONNECT_POLICY, getStreamReconnectDelay, normalizeStreamReconnectPolicy, } from './reconnect.js';
6
- import { createFetch, createHttpJsonTransport, createJsonInit, createJsonRequestInit, readJsonBody, } from './http-core.js';
7
- export { createFetch, createJsonInit, createJsonRequestInit, readJsonBody, mergeHeaders, resolveAuthToken, resolveHeaders, computeBackoffDelay, normalizeBackoffPolicy, sleepWithSignal, DEFAULT_HTTP_RETRY_POLICY, getHttpRetryDelay, isRetryableHttpStatus, isRetryableNetworkError, normalizeHttpRetryPolicy, resolveHttpRetryPolicy, DEFAULT_STREAM_RECONNECT_POLICY, getStreamReconnectDelay, normalizeStreamReconnectPolicy, };
6
+ import { createFetch, createHttpJsonTransport, createJsonInit, createJsonRequestInit, generateIdempotencyKey, inferTransportHint, readJsonBody, requestJson, } from './http-core.js';
7
+ export { createFetch, createJsonInit, createJsonRequestInit, generateIdempotencyKey, readJsonBody, requestJson, mergeHeaders, resolveAuthToken, resolveHeaders, computeBackoffDelay, normalizeBackoffPolicy, sleepWithSignal, DEFAULT_HTTP_RETRY_POLICY, getHttpRetryDelay, isRetryableHttpStatus, isRetryableNetworkError, normalizeHttpRetryPolicy, resolveHttpRetryPolicy, DEFAULT_STREAM_RECONNECT_POLICY, getStreamReconnectDelay, normalizeStreamReconnectPolicy, };
8
8
  function isTransportError(error) {
9
9
  return Boolean(error
10
10
  && typeof error === 'object'
@@ -14,10 +14,45 @@ function isTransportError(error) {
14
14
  && typeof error.transport.url === 'string');
15
15
  }
16
16
  export function normalizeTransportError(error) {
17
+ // Fast path: already a structured SDK error — return directly, no re-wrapping needed.
18
+ // Covers HttpStatusError (subclass) and GoodVibesSdkError (e.g. SSE stream errors) alike.
19
+ if (error instanceof GoodVibesSdkError) {
20
+ return error;
21
+ }
17
22
  if (isTransportError(error)) {
18
- return createHttpStatusError(error.transport.status, error.transport.url, typeof error.transport.method === 'string' ? error.transport.method : 'GET', error.transport.body);
23
+ const { status, url, body, method, retryAfterMs, cause } = error.transport;
24
+ const resolvedMethod = typeof method === 'string' ? method : 'GET';
25
+ const hint = inferTransportHint(status, url, retryAfterMs);
26
+ if (status === 0) {
27
+ // Network-level failure: no HTTP response received
28
+ const networkError = new HttpStatusError(error instanceof Error ? error.message : `Transport could not reach ${url}`, {
29
+ status: undefined,
30
+ url,
31
+ method: resolvedMethod,
32
+ body,
33
+ category: 'network',
34
+ source: 'transport',
35
+ recoverable: true,
36
+ hint,
37
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
38
+ });
39
+ if (cause !== undefined) {
40
+ Object.defineProperty(networkError, 'cause', { value: cause, writable: true, configurable: true });
41
+ }
42
+ return Object.assign(networkError, { transport: error.transport });
43
+ }
44
+ const baseError = createHttpStatusError(status, url, resolvedMethod, body);
45
+ // Only apply inferred hint if the daemon body didn't supply one already
46
+ const effectiveHint = baseError.hint ?? hint;
47
+ return Object.assign(baseError, {
48
+ transport: error.transport,
49
+ ...(effectiveHint !== undefined ? { hint: effectiveHint } : {}),
50
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
51
+ });
19
52
  }
20
53
  if (error instanceof Error) {
54
+ // Defensive string-match fallback for non-SDK errors that slip through.
55
+ // With structured throws in http-core.ts, these paths are rarely exercised.
21
56
  if (error.message === 'Fetch implementation is required' || error.message === 'Transport baseUrl is required') {
22
57
  return new ConfigurationError(error.message);
23
58
  }
@@ -25,7 +60,13 @@ export function normalizeTransportError(error) {
25
60
  return new ContractError(error.message);
26
61
  }
27
62
  }
28
- return error instanceof Error ? error : new Error(String(error));
63
+ return error instanceof Error
64
+ ? error
65
+ : new GoodVibesSdkError(`Transport operation failed with a non-Error value: ${String(error)}`, {
66
+ category: 'network',
67
+ source: 'transport',
68
+ recoverable: true,
69
+ });
29
70
  }
30
71
  export function createHttpTransport(options) {
31
72
  const transport = createHttpJsonTransport(options);
package/dist/index.d.ts CHANGED
@@ -1,17 +1,21 @@
1
1
  export type { ContractInvokeOptions, ContractRouteDefinition, ContractRouteLike, ContractStreamOptions, } from './contract-client.js';
2
2
  export { buildContractInput, invokeContractRoute, openContractRouteStream, requireContractRoute, } from './contract-client.js';
3
- export type { HttpJsonRequestOptions, HttpTransport, HttpTransportOptions, JsonObject, JsonValue, ResolvedContractRequest, } from './http.js';
4
- export { createFetch, createHttpTransport, createJsonInit, createJsonRequestInit, normalizeTransportError, readJsonBody, } from './http.js';
3
+ export type { JsonSchemaValidationFailure, MethodArgs, RequiredKeys, WithoutKeys } from './client-plumbing.js';
4
+ export { clientInputRecord, firstJsonSchemaFailure, mergeClientInput, splitClientArgs } from './client-plumbing.js';
5
+ export type { HttpJsonRequestOptions, HttpTransport, HttpTransportOptions, JsonObject, JsonValue, ResolvedContractRequest, TransportJsonError, } from './http.js';
6
+ export { createFetch, createHttpTransport, createJsonInit, createJsonRequestInit, normalizeTransportError, readJsonBody, requestJson, } from './http.js';
7
+ export type { TransportContext, TransportMiddleware } from './http-core.js';
8
+ export { generateIdempotencyKey } from './http-core.js';
5
9
  export type { ServerSentEventHandlers, ServerSentEventOptions } from './sse.js';
6
10
  export { openServerSentEventStream } from './sse.js';
7
11
  export type { ServerSentEventHandlers as RawServerSentEventHandlers, ServerSentEventOptions as RawServerSentEventOptions } from './sse-stream.js';
8
- export { openServerSentEventStream as openRawServerSentEventStream } from './sse-stream.js';
9
- export type { AuthTokenResolver, HeaderResolver, MaybePromise } from './auth.js';
10
- export { mergeHeaders, resolveAuthToken, resolveHeaders } from './auth.js';
12
+ export { openRawServerSentEventStream } from './sse-stream.js';
13
+ export type { AuthTokenInput, AuthTokenResolver, HeaderResolver, MaybePromise } from './auth.js';
14
+ export { mergeHeaderRecord, mergeHeaders, normalizeAuthToken, resolveAuthToken, resolveHeaders } from './auth.js';
11
15
  export type { BackoffPolicy, ResolvedBackoffPolicy } from './backoff.js';
12
16
  export { computeBackoffDelay, normalizeBackoffPolicy, sleepWithSignal } from './backoff.js';
13
- export type { HttpRetryPolicy, ResolvedHttpRetryPolicy } from './retry.js';
14
- export { DEFAULT_HTTP_RETRY_POLICY, getHttpRetryDelay, isRetryableHttpStatus, isRetryableNetworkError, normalizeHttpRetryPolicy, resolveHttpRetryPolicy } from './retry.js';
17
+ export type { HttpRetryPolicy, PerMethodRetryPolicy, ResolvedHttpRetryPolicy } from './retry.js';
18
+ export { DEFAULT_HTTP_RETRY_POLICY, applyPerMethodPolicy, getHttpRetryDelay, isRetryableHttpStatus, isRetryableNetworkError, normalizeHttpRetryPolicy, resolveHttpRetryPolicy } from './retry.js';
15
19
  export type { StreamReconnectPolicy, ResolvedStreamReconnectPolicy } from './reconnect.js';
16
20
  export { DEFAULT_STREAM_RECONNECT_POLICY, getStreamReconnectDelay, normalizeStreamReconnectPolicy } from './reconnect.js';
17
21
  export type { TransportPaths } from './paths.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,qBAAqB,EACrB,uBAAuB,EACvB,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,sBAAsB,EACtB,aAAa,EACb,oBAAoB,EACpB,UAAU,EACV,SAAS,EACT,uBAAuB,GACxB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,cAAc,EACd,qBAAqB,EACrB,uBAAuB,EACvB,YAAY,GACb,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAChF,OAAO,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAC;AACrD,YAAY,EAAE,uBAAuB,IAAI,0BAA0B,EAAE,sBAAsB,IAAI,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AAClJ,OAAO,EAAE,yBAAyB,IAAI,4BAA4B,EAAE,MAAM,iBAAiB,CAAC;AAC5F,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3E,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC5F,YAAY,EAAE,eAAe,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAC3E,OAAO,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAC5K,YAAY,EAAE,qBAAqB,EAAE,6BAA6B,EAAE,MAAM,gBAAgB,CAAC;AAC3F,OAAO,EAAE,+BAA+B,EAAE,uBAAuB,EAAE,8BAA8B,EAAE,MAAM,gBAAgB,CAAC;AAC1H,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,qBAAqB,EACrB,uBAAuB,EACvB,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,2BAA2B,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC/G,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACpH,YAAY,EACV,sBAAsB,EACtB,aAAa,EACb,oBAAoB,EACpB,UAAU,EACV,SAAS,EACT,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,cAAc,EACd,qBAAqB,EACrB,uBAAuB,EACvB,YAAY,EACZ,WAAW,GACZ,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACxD,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAChF,OAAO,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAC;AACrD,YAAY,EAAE,uBAAuB,IAAI,0BAA0B,EAAE,sBAAsB,IAAI,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AAClJ,OAAO,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAC;AAC/D,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACjG,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAClH,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC5F,YAAY,EAAE,eAAe,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AACjG,OAAO,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAClM,YAAY,EAAE,qBAAqB,EAAE,6BAA6B,EAAE,MAAM,gBAAgB,CAAC;AAC3F,OAAO,EAAE,+BAA+B,EAAE,uBAAuB,EAAE,8BAA8B,EAAE,MAAM,gBAAgB,CAAC;AAC1H,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  export { buildContractInput, invokeContractRoute, openContractRouteStream, requireContractRoute, } from './contract-client.js';
2
- export { createFetch, createHttpTransport, createJsonInit, createJsonRequestInit, normalizeTransportError, readJsonBody, } from './http.js';
2
+ export { clientInputRecord, firstJsonSchemaFailure, mergeClientInput, splitClientArgs } from './client-plumbing.js';
3
+ export { createFetch, createHttpTransport, createJsonInit, createJsonRequestInit, normalizeTransportError, readJsonBody, requestJson, } from './http.js';
4
+ export { generateIdempotencyKey } from './http-core.js';
3
5
  export { openServerSentEventStream } from './sse.js';
4
- export { openServerSentEventStream as openRawServerSentEventStream } from './sse-stream.js';
5
- export { mergeHeaders, resolveAuthToken, resolveHeaders } from './auth.js';
6
+ export { openRawServerSentEventStream } from './sse-stream.js';
7
+ export { mergeHeaderRecord, mergeHeaders, normalizeAuthToken, resolveAuthToken, resolveHeaders } from './auth.js';
6
8
  export { computeBackoffDelay, normalizeBackoffPolicy, sleepWithSignal } from './backoff.js';
7
- export { DEFAULT_HTTP_RETRY_POLICY, getHttpRetryDelay, isRetryableHttpStatus, isRetryableNetworkError, normalizeHttpRetryPolicy, resolveHttpRetryPolicy } from './retry.js';
9
+ export { DEFAULT_HTTP_RETRY_POLICY, applyPerMethodPolicy, getHttpRetryDelay, isRetryableHttpStatus, isRetryableNetworkError, normalizeHttpRetryPolicy, resolveHttpRetryPolicy } from './retry.js';
8
10
  export { DEFAULT_STREAM_RECONNECT_POLICY, getStreamReconnectDelay, normalizeStreamReconnectPolicy } from './reconnect.js';
9
11
  export { buildUrl, createTransportPaths, normalizeBaseUrl } from './paths.js';
@@ -1 +1 @@
1
- {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AACA,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAC;IACvC,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;IACxC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;IACxC,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,uBAAuB,EAAE,MAAM,CAAC;IACzC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAMxD;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG9D;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CA8BpE"}
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAC;IACvC,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;IACxC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;IACxC,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,uBAAuB,EAAE,MAAM,CAAC;IACzC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAMxD;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG9D;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CA8BpE"}
package/dist/paths.js CHANGED
@@ -1,9 +1,10 @@
1
+ import { ConfigurationError } from '@pellux/goodvibes-errors';
1
2
  export function normalizeBaseUrl(baseUrl) {
2
3
  const normalized = baseUrl.trim();
3
4
  if (!normalized) {
4
- throw new Error('Transport baseUrl is required');
5
+ throw new ConfigurationError('Transport baseUrl is required. Pass a non-empty baseUrl string to your transport or SDK options.', { code: 'SDK_TRANSPORT_BASE_URL_REQUIRED' });
5
6
  }
6
- return normalized.endsWith('/') ? normalized.slice(0, -1) : normalized;
7
+ return normalized.replace(/\/+$/, '');
7
8
  }
8
9
  export function buildUrl(baseUrl, path) {
9
10
  const normalized = normalizeBaseUrl(baseUrl);
@@ -5,6 +5,8 @@ export interface StreamReconnectPolicy extends BackoffPolicy {
5
5
  export interface ResolvedStreamReconnectPolicy extends ResolvedBackoffPolicy {
6
6
  readonly enabled: boolean;
7
7
  }
8
+ /** Maximum reconnect attempts when reconnect is enabled and the caller does not set a limit. */
9
+ export declare const DEFAULT_STREAM_MAX_ATTEMPTS = 10;
8
10
  export declare const DEFAULT_STREAM_RECONNECT_POLICY: ResolvedStreamReconnectPolicy;
9
11
  export declare function normalizeStreamReconnectPolicy(policy?: StreamReconnectPolicy): ResolvedStreamReconnectPolicy;
10
12
  export declare function getStreamReconnectDelay(attempt: number, policy: ResolvedStreamReconnectPolicy): number;