@lonca/core 0.2.0 → 0.4.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/README.md +18 -9
- package/dist/index.cjs +138 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +171 -1
- package/dist/index.d.ts +171 -1
- package/dist/index.js +133 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
<p align="left">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/loncadev/lonca/main/assets/brand/icon.svg" alt="Lonca" height="32">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# @lonca/core
|
|
2
6
|
|
|
3
7
|
Shared primitives for Lonca marketplace SDKs.
|
|
4
8
|
|
|
5
9
|
> Type-safe building blocks reused across every `@lonca/<marketplace>` SDK: money, errors, pagination, retry, logger, rate limiter.
|
|
6
10
|
|
|
11
|
+
> [!IMPORTANT]
|
|
12
|
+
> **Unofficial.** Part of Lonca, an independent, community-maintained project — not affiliated with, endorsed by, or supported by any marketplace. All marketplace names and trademarks belong to their respective owners.
|
|
13
|
+
|
|
7
14
|
## Install
|
|
8
15
|
|
|
9
16
|
```bash
|
|
@@ -12,18 +19,20 @@ pnpm add @lonca/core
|
|
|
12
19
|
|
|
13
20
|
## What's inside
|
|
14
21
|
|
|
15
|
-
| Primitive | Purpose
|
|
16
|
-
| ------------------------ |
|
|
17
|
-
| `Money`, `Currency` | ISO 4217 currency codes; integer minor-unit money representation
|
|
18
|
-
| `CursorPage`, `paginate` | Cursor-based pagination + async iterator helper
|
|
19
|
-
| `LoncaError` hierarchy | Structured errors (`AuthError`, `RateLimitError`, `ValidationError`, `NotFoundError`, `ServerError`, `NetworkError`, `TimeoutError`) with `retryable` flag |
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
22
|
+
| Primitive | Purpose |
|
|
23
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
24
|
+
| `Money`, `Currency` | ISO 4217 currency codes; integer minor-unit money representation; `moneyFromMajor`/`moneyToMajor` lira↔kuruş converters |
|
|
25
|
+
| `CursorPage`, `paginate` | Cursor-based pagination + async iterator helper |
|
|
26
|
+
| `LoncaError` hierarchy | Structured errors (`AuthError`, `RateLimitError`, `ValidationError`, `NotFoundError`, `ServerError`, `NetworkError`, `TimeoutError`) with `retryable` flag and a normalized `issues` array |
|
|
27
|
+
| `NormalizedOrderStatus` | Closed cross-marketplace order-status vocabulary + `createStatusNormalizer` (surfaces unmapped statuses via `mapped: false`, never a silent default) |
|
|
28
|
+
| `retry` | Exponential backoff with jitter, honors `retryAfterMs`, supports `AbortSignal`. `parseRetryAfter` parses `Retry-After`; `isRetryableIdempotentOnly` gates non-idempotent writes to 429-only replays |
|
|
29
|
+
| `createRequester` | Shared HTTP request lifecycle (rate-limit → fetch under a composed timeout → 204/error mapping → idempotency-aware retry → structured logging) that each SDK transport is built on; inject marketplace-specific URL/header/error-map seams |
|
|
30
|
+
| `Logger` | Structured logger interface (`debug`/`info`/`warn`/`error`/`child`) with `noopLogger` and `consoleLogger` |
|
|
31
|
+
| `TokenBucketRateLimiter` | Async token-bucket rate limiter with `AbortSignal` support |
|
|
23
32
|
|
|
24
33
|
## Design principles
|
|
25
34
|
|
|
26
|
-
- **Money is an integer in minor units** — `{ amount: 12550, currency: 'TRY' }` means 125.50 TRY. No floats, no surprises.
|
|
35
|
+
- **Money is an integer in minor units** — `{ amount: 12550, currency: 'TRY' }` means 125.50 TRY. No floats, no surprises. Marketplace SDKs hand you raw major-unit numbers (e.g. `199.9` lira); wrap them once with `moneyFromMajor(price, TRY)` instead of a hand-rolled `Math.round(price * 100)`.
|
|
27
36
|
- **Cursor pagination over offset** — opaque cursors compose better with async iterators and stay stable as datasets grow.
|
|
28
37
|
- **Errors are typed and tagged** — `err.code` plus a `retryable` boolean tells retry helpers what to do without sniffing messages.
|
|
29
38
|
- **Logger is an interface, not an implementation** — wire your own (pino, winston, console). A no-op default is provided.
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
|
|
3
5
|
// src/currency.ts
|
|
4
6
|
var TRY = "TRY";
|
|
5
7
|
var USD = "USD";
|
|
@@ -71,6 +73,15 @@ async function* paginateOffset(fetchPage, params = {}) {
|
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
// src/order-status.ts
|
|
77
|
+
function createStatusNormalizer(map) {
|
|
78
|
+
const lookup = map;
|
|
79
|
+
return (raw) => {
|
|
80
|
+
const normalized = lookup[raw];
|
|
81
|
+
return normalized !== void 0 ? { normalized, raw, mapped: true } : { normalized: "unknown", raw, mapped: false };
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
74
85
|
// src/errors.ts
|
|
75
86
|
var LoncaError = class extends Error {
|
|
76
87
|
code;
|
|
@@ -78,6 +89,12 @@ var LoncaError = class extends Error {
|
|
|
78
89
|
status;
|
|
79
90
|
retryAfterMs;
|
|
80
91
|
data;
|
|
92
|
+
/**
|
|
93
|
+
* Normalized, field-level error details mapped from the marketplace's raw
|
|
94
|
+
* error body by the SDK. Always an array (never `undefined`) so callers can
|
|
95
|
+
* iterate without a presence check; empty when nothing was parseable.
|
|
96
|
+
*/
|
|
97
|
+
issues;
|
|
81
98
|
constructor(opts) {
|
|
82
99
|
super(opts.message, opts.cause !== void 0 ? { cause: opts.cause } : void 0);
|
|
83
100
|
this.name = "LoncaError";
|
|
@@ -86,6 +103,7 @@ var LoncaError = class extends Error {
|
|
|
86
103
|
this.status = opts.status;
|
|
87
104
|
this.retryAfterMs = opts.retryAfterMs;
|
|
88
105
|
this.data = opts.data;
|
|
106
|
+
this.issues = opts.issues ?? [];
|
|
89
107
|
}
|
|
90
108
|
};
|
|
91
109
|
var AuthError = class extends LoncaError {
|
|
@@ -136,6 +154,20 @@ function isLoncaError(value) {
|
|
|
136
154
|
function isRetryableError(value) {
|
|
137
155
|
return isLoncaError(value) && value.retryable;
|
|
138
156
|
}
|
|
157
|
+
function isRetryableIdempotentOnly(value) {
|
|
158
|
+
return isLoncaError(value) && value.retryable && value.code === "RATE_LIMITED";
|
|
159
|
+
}
|
|
160
|
+
function parseRetryAfter(header) {
|
|
161
|
+
if (!header) return void 0;
|
|
162
|
+
const seconds = Number(header);
|
|
163
|
+
if (!Number.isNaN(seconds)) return seconds > 0 ? seconds * 1e3 : void 0;
|
|
164
|
+
const epoch = Date.parse(header);
|
|
165
|
+
if (!Number.isNaN(epoch)) {
|
|
166
|
+
const delta = epoch - Date.now();
|
|
167
|
+
return delta > 0 ? delta : void 0;
|
|
168
|
+
}
|
|
169
|
+
return void 0;
|
|
170
|
+
}
|
|
139
171
|
|
|
140
172
|
// src/retry.ts
|
|
141
173
|
var DEFAULTS = {
|
|
@@ -178,7 +210,7 @@ async function retry(fn, options = {}) {
|
|
|
178
210
|
}
|
|
179
211
|
const retryAfterMs = isLoncaError(err) ? err.retryAfterMs : void 0;
|
|
180
212
|
const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
|
|
181
|
-
const base = retryAfterMs
|
|
213
|
+
const base = retryAfterMs && retryAfterMs > 0 ? retryAfterMs : exponential;
|
|
182
214
|
const jitterMs = jitter ? Math.random() * base * 0.5 : 0;
|
|
183
215
|
const delay = Math.min(maxDelayMs, base + jitterMs);
|
|
184
216
|
options.onRetry?.(err, attempt, delay);
|
|
@@ -283,6 +315,105 @@ var TokenBucketRateLimiter = class {
|
|
|
283
315
|
return this.tokens;
|
|
284
316
|
}
|
|
285
317
|
};
|
|
318
|
+
function createRequester(config) {
|
|
319
|
+
const logger = config.logger ?? noopLogger;
|
|
320
|
+
return function request(opts) {
|
|
321
|
+
const safeToReplay = opts.method === "GET" || opts.idempotent === true;
|
|
322
|
+
const extra = () => config.logFields?.(opts) ?? {};
|
|
323
|
+
return retry(
|
|
324
|
+
async (attempt) => {
|
|
325
|
+
if (opts.rateLimiter) await opts.rateLimiter.acquire(opts.signal);
|
|
326
|
+
const correlationId = crypto.randomUUID();
|
|
327
|
+
const url = config.buildUrl(opts);
|
|
328
|
+
const headers = { ...config.buildHeaders(correlationId), ...opts.headers };
|
|
329
|
+
const init = {
|
|
330
|
+
method: opts.method,
|
|
331
|
+
headers,
|
|
332
|
+
signal: composeSignal(opts.signal, config.timeoutMs)
|
|
333
|
+
};
|
|
334
|
+
if (opts.body !== void 0 && opts.method !== "GET") {
|
|
335
|
+
if (opts.body instanceof FormData) {
|
|
336
|
+
init.body = opts.body;
|
|
337
|
+
delete headers["Content-Type"];
|
|
338
|
+
} else {
|
|
339
|
+
init.body = JSON.stringify(opts.body);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
logger.debug(`${config.logPrefix}.request`, {
|
|
343
|
+
method: opts.method,
|
|
344
|
+
url,
|
|
345
|
+
correlationId,
|
|
346
|
+
attempt,
|
|
347
|
+
...extra()
|
|
348
|
+
});
|
|
349
|
+
let response;
|
|
350
|
+
try {
|
|
351
|
+
response = await config.fetch(url, init);
|
|
352
|
+
} catch (err) {
|
|
353
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
354
|
+
throw new TimeoutError({
|
|
355
|
+
message: `${config.label} request timed out after ${config.timeoutMs}ms`,
|
|
356
|
+
cause: err
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
throw new NetworkError({ message: `${config.label} network failure`, cause: err });
|
|
360
|
+
}
|
|
361
|
+
if (!response.ok) {
|
|
362
|
+
const body = await safeJson(response);
|
|
363
|
+
const retryAfterMs = parseRetryAfter(response.headers.get("retry-after"));
|
|
364
|
+
const error = config.mapHttpError(response.status, body, retryAfterMs);
|
|
365
|
+
logger.warn(`${config.logPrefix}.error`, {
|
|
366
|
+
method: opts.method,
|
|
367
|
+
url,
|
|
368
|
+
correlationId,
|
|
369
|
+
status: response.status,
|
|
370
|
+
code: error.code,
|
|
371
|
+
retryable: error.retryable,
|
|
372
|
+
...extra()
|
|
373
|
+
});
|
|
374
|
+
throw error;
|
|
375
|
+
}
|
|
376
|
+
logger.debug(`${config.logPrefix}.response`, {
|
|
377
|
+
correlationId,
|
|
378
|
+
status: response.status
|
|
379
|
+
});
|
|
380
|
+
if (response.status === 204) return void 0;
|
|
381
|
+
return await safeJson(response);
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
signal: opts.signal,
|
|
385
|
+
// Non-idempotent writes only retry rate-limit (429) errors; ambiguous
|
|
386
|
+
// 5xx/network/timeout failures are not replayed to avoid duplicate
|
|
387
|
+
// side-effects. GET (and explicitly idempotent requests) retry normally.
|
|
388
|
+
isRetryable: safeToReplay ? void 0 : isRetryableIdempotentOnly,
|
|
389
|
+
onRetry: (err, attempt, delay) => {
|
|
390
|
+
if (err instanceof LoncaError) {
|
|
391
|
+
logger.warn(`${config.logPrefix}.retry`, {
|
|
392
|
+
attempt,
|
|
393
|
+
delayMs: delay,
|
|
394
|
+
code: err.code,
|
|
395
|
+
status: err.status
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function composeSignal(external, timeoutMs) {
|
|
404
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
405
|
+
if (!external) return timeoutSignal;
|
|
406
|
+
return AbortSignal.any([external, timeoutSignal]);
|
|
407
|
+
}
|
|
408
|
+
async function safeJson(response) {
|
|
409
|
+
const text = await response.text();
|
|
410
|
+
if (!text) return void 0;
|
|
411
|
+
try {
|
|
412
|
+
return JSON.parse(text);
|
|
413
|
+
} catch {
|
|
414
|
+
return text;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
286
417
|
|
|
287
418
|
exports.AuthError = AuthError;
|
|
288
419
|
exports.EUR = EUR;
|
|
@@ -298,9 +429,13 @@ exports.TokenBucketRateLimiter = TokenBucketRateLimiter;
|
|
|
298
429
|
exports.USD = USD;
|
|
299
430
|
exports.ValidationError = ValidationError;
|
|
300
431
|
exports.addMoney = addMoney;
|
|
432
|
+
exports.composeSignal = composeSignal;
|
|
301
433
|
exports.consoleLogger = consoleLogger;
|
|
434
|
+
exports.createRequester = createRequester;
|
|
435
|
+
exports.createStatusNormalizer = createStatusNormalizer;
|
|
302
436
|
exports.isLoncaError = isLoncaError;
|
|
303
437
|
exports.isRetryableError = isRetryableError;
|
|
438
|
+
exports.isRetryableIdempotentOnly = isRetryableIdempotentOnly;
|
|
304
439
|
exports.isSameCurrency = isSameCurrency;
|
|
305
440
|
exports.isValidCurrencyCode = isValidCurrencyCode;
|
|
306
441
|
exports.money = money;
|
|
@@ -309,7 +444,9 @@ exports.moneyToMajor = moneyToMajor;
|
|
|
309
444
|
exports.noopLogger = noopLogger;
|
|
310
445
|
exports.paginate = paginate;
|
|
311
446
|
exports.paginateOffset = paginateOffset;
|
|
447
|
+
exports.parseRetryAfter = parseRetryAfter;
|
|
312
448
|
exports.retry = retry;
|
|
449
|
+
exports.safeJson = safeJson;
|
|
313
450
|
exports.subMoney = subMoney;
|
|
314
451
|
//# sourceMappingURL=index.cjs.map
|
|
315
452
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/currency.ts","../src/money.ts","../src/pagination.ts","../src/errors.ts","../src/retry.ts","../src/logger.ts","../src/rate-limiter.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * ISO 4217 three-letter currency code.\n *\n * Typed as a string for flexibility — Trendyol and similar marketplaces may\n * expose currencies beyond the common set, so we don't lock down to a union.\n * Use `isValidCurrencyCode` for runtime validation.\n */\nexport type Currency = string;\n\nexport const TRY = 'TRY';\nexport const USD = 'USD';\nexport const EUR = 'EUR';\nexport const GBP = 'GBP';\n\nconst ISO_4217_RE = /^[A-Z]{3}$/;\n\nexport function isValidCurrencyCode(code: string): boolean {\n return ISO_4217_RE.test(code);\n}\n","import type { Currency } from './currency.js';\n\n/**\n * A monetary amount in integer minor units (e.g., kuruş for TRY, cents for USD).\n *\n * Using integers avoids floating-point surprises. Convert to/from major units\n * with `moneyFromMajor` and `moneyToMajor`.\n */\nexport interface Money {\n amount: number;\n currency: Currency;\n}\n\nexport function money(amount: number, currency: Currency): Money {\n if (!Number.isInteger(amount)) {\n throw new TypeError(`Money amount must be an integer minor unit, got ${amount}`);\n }\n return { amount, currency };\n}\n\nexport function moneyFromMajor(major: number, currency: Currency, minorScale = 2): Money {\n const factor = 10 ** minorScale;\n const amount = Math.round(major * factor);\n return money(amount, currency);\n}\n\nexport function moneyToMajor(value: Money, minorScale = 2): number {\n return value.amount / 10 ** minorScale;\n}\n\nexport function isSameCurrency(a: Money, b: Money): boolean {\n return a.currency === b.currency;\n}\n\nexport function addMoney(a: Money, b: Money): Money {\n if (!isSameCurrency(a, b)) {\n throw new TypeError(`Cannot add Money of different currencies: ${a.currency} vs ${b.currency}`);\n }\n return money(a.amount + b.amount, a.currency);\n}\n\nexport function subMoney(a: Money, b: Money): Money {\n if (!isSameCurrency(a, b)) {\n throw new TypeError(\n `Cannot subtract Money of different currencies: ${a.currency} vs ${b.currency}`,\n );\n }\n return money(a.amount - b.amount, a.currency);\n}\n","export interface CursorPaginationParams {\n /** Opaque cursor from previous response's `nextCursor`. Omit for the first page. */\n cursor?: string;\n /** Maximum items in the page. Marketplace SDKs may clamp this to their own bounds. */\n limit?: number;\n}\n\nexport interface CursorPage<T> {\n items: T[];\n /** Opaque cursor for the next page. Absent when there are no more pages. */\n nextCursor?: string;\n}\n\n/**\n * Drive a cursor-paginated endpoint as an async iterator.\n *\n * @example\n * for await (const order of paginate((p) => client.orders.list(p))) {\n * console.log(order.id);\n * }\n */\nexport async function* paginate<T>(\n fetchPage: (params: CursorPaginationParams) => Promise<CursorPage<T>>,\n params: Omit<CursorPaginationParams, 'cursor'> = {},\n): AsyncGenerator<T, void, undefined> {\n let cursor: string | undefined;\n do {\n const page = await fetchPage({ ...params, cursor });\n for (const item of page.items) {\n yield item;\n }\n cursor = page.nextCursor;\n } while (cursor);\n}\n\n// ─── Offset pagination ─────────────────────────────────────────────────────\n\nexport interface OffsetPaginationParams {\n /** Zero-based offset. Default depends on the SDK; usually `0`. */\n offset?: number;\n /** Maximum items in the page. */\n limit?: number;\n}\n\n/**\n * Offset-based pagination envelope returned by marketplaces that expose\n * `{ totalCount, limit, offset, pageCount, items[] }` instead of opaque\n * cursors. Hepsiburada's OMS uses this shape; some legacy Trendyol\n * endpoints do too. Distinct from `CursorPage<T>` so callers can tell at\n * the type level which pagination model an endpoint uses.\n */\nexport interface OffsetPage<T> {\n /** Total number of items matching the query across all pages. */\n totalCount: number;\n /** Echo of the request `limit`. */\n limit: number;\n /** Echo of the request `offset`. */\n offset: number;\n /** Total number of pages at the requested `limit`. */\n pageCount: number;\n /** Page rows. */\n items: T[];\n}\n\n/**\n * Drive an offset-paginated endpoint as an async iterator. Stops when the\n * server's `pageCount` is exhausted (or after the configured `maxPages`\n * to bound runaway iteration). Default page size is whatever the\n * underlying endpoint clamps to — pass `limit` explicitly for control.\n *\n * @example\n * for await (const order of paginateOffset((p) =>\n * client.orders.list(p),\n * )) {\n * console.log(order.orderNumber);\n * }\n */\nexport async function* paginateOffset<T>(\n fetchPage: (params: OffsetPaginationParams) => Promise<OffsetPage<T>>,\n params: { limit?: number; maxPages?: number } = {},\n): AsyncGenerator<T, void, undefined> {\n const limit = params.limit;\n const maxPages = params.maxPages ?? Infinity;\n let offset = 0;\n let pages = 0;\n while (pages < maxPages) {\n const page = await fetchPage({ offset, limit });\n for (const item of page.items) {\n yield item;\n }\n pages += 1;\n if (page.items.length === 0) return;\n if (page.pageCount > 0 && pages >= page.pageCount) return;\n // Step by the server-echoed limit (when present) so requests stay aligned with how\n // the server batches; fall back to the caller's `limit` or the observed batch size.\n offset += page.limit || limit || page.items.length;\n }\n}\n","export type LoncaErrorCode =\n | 'AUTH_FAILED'\n | 'RATE_LIMITED'\n | 'VALIDATION_FAILED'\n | 'NOT_FOUND'\n | 'SERVER_ERROR'\n | 'NETWORK_ERROR'\n | 'TIMEOUT'\n | 'UNKNOWN';\n\nexport interface LoncaErrorOptions {\n code: LoncaErrorCode;\n message: string;\n cause?: unknown;\n retryable?: boolean;\n status?: number;\n retryAfterMs?: number;\n data?: Record<string, unknown>;\n}\n\n/**\n * Root of the Lonca error hierarchy.\n *\n * Carries a tagged `code` and a `retryable` boolean so retry helpers\n * can decide what to do without sniffing message strings.\n */\nexport class LoncaError extends Error {\n readonly code: LoncaErrorCode;\n readonly retryable: boolean;\n readonly status?: number;\n readonly retryAfterMs?: number;\n readonly data?: Record<string, unknown>;\n\n constructor(opts: LoncaErrorOptions) {\n super(opts.message, opts.cause !== undefined ? { cause: opts.cause } : undefined);\n this.name = 'LoncaError';\n this.code = opts.code;\n this.retryable = opts.retryable ?? false;\n this.status = opts.status;\n this.retryAfterMs = opts.retryAfterMs;\n this.data = opts.data;\n }\n}\n\ntype SubclassOptions = Omit<LoncaErrorOptions, 'code' | 'retryable'>;\n\nexport class AuthError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'AUTH_FAILED', retryable: false });\n this.name = 'AuthError';\n }\n}\n\nexport class RateLimitError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'RATE_LIMITED', retryable: true });\n this.name = 'RateLimitError';\n }\n}\n\nexport class ValidationError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'VALIDATION_FAILED', retryable: false });\n this.name = 'ValidationError';\n }\n}\n\nexport class NotFoundError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'NOT_FOUND', retryable: false });\n this.name = 'NotFoundError';\n }\n}\n\nexport class ServerError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'SERVER_ERROR', retryable: true });\n this.name = 'ServerError';\n }\n}\n\nexport class NetworkError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'NETWORK_ERROR', retryable: true });\n this.name = 'NetworkError';\n }\n}\n\nexport class TimeoutError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'TIMEOUT', retryable: true });\n this.name = 'TimeoutError';\n }\n}\n\nexport function isLoncaError(value: unknown): value is LoncaError {\n return value instanceof LoncaError;\n}\n\nexport function isRetryableError(value: unknown): boolean {\n return isLoncaError(value) && value.retryable;\n}\n","import { isLoncaError, isRetryableError } from './errors.js';\n\nexport interface RetryOptions {\n /** Total number of attempts including the initial call. Defaults to 3. */\n maxAttempts?: number;\n /** Initial delay before the second attempt, in ms. Defaults to 200. */\n baseDelayMs?: number;\n /** Upper bound on delay between any two attempts, in ms. Defaults to 10_000. */\n maxDelayMs?: number;\n /** Add up to 50% random jitter on each delay. Defaults to true. */\n jitter?: boolean;\n /** Override the default predicate. Default treats `retryable` LoncaErrors as retryable. */\n isRetryable?: (err: unknown) => boolean;\n /** Called before each retry sleep. Useful for structured logging. */\n onRetry?: (err: unknown, attempt: number, nextDelayMs: number) => void;\n /** Abort all pending sleeps and bubble the signal's reason. */\n signal?: AbortSignal;\n}\n\nconst DEFAULTS = {\n maxAttempts: 3,\n baseDelayMs: 200,\n maxDelayMs: 10_000,\n jitter: true,\n} as const;\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(signal.reason);\n return;\n }\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n 'abort',\n () => {\n clearTimeout(timer);\n reject(signal.reason);\n },\n { once: true },\n );\n });\n}\n\nexport async function retry<T>(\n fn: (attempt: number) => Promise<T>,\n options: RetryOptions = {},\n): Promise<T> {\n const maxAttempts = options.maxAttempts ?? DEFAULTS.maxAttempts;\n const baseDelayMs = options.baseDelayMs ?? DEFAULTS.baseDelayMs;\n const maxDelayMs = options.maxDelayMs ?? DEFAULTS.maxDelayMs;\n const jitter = options.jitter ?? DEFAULTS.jitter;\n const isRetryable = options.isRetryable ?? isRetryableError;\n\n let lastErr: unknown;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n return await fn(attempt);\n } catch (err) {\n lastErr = err;\n if (attempt === maxAttempts || !isRetryable(err)) {\n throw err;\n }\n\n const retryAfterMs = isLoncaError(err) ? err.retryAfterMs : undefined;\n const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));\n const base = retryAfterMs ?? exponential;\n const jitterMs = jitter ? Math.random() * base * 0.5 : 0;\n const delay = Math.min(maxDelayMs, base + jitterMs);\n\n options.onRetry?.(err, attempt, delay);\n await sleep(delay, options.signal);\n }\n }\n throw lastErr;\n}\n","export type LogLevel = 'debug' | 'info' | 'warn' | 'error';\n\nexport type LogContext = Record<string, unknown>;\n\nexport interface Logger {\n debug(message: string, context?: LogContext): void;\n info(message: string, context?: LogContext): void;\n warn(message: string, context?: LogContext): void;\n error(message: string, context?: LogContext): void;\n /** Create a child logger that merges `bindings` into every log entry. */\n child(bindings: LogContext): Logger;\n}\n\nexport const noopLogger: Logger = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n child: () => noopLogger,\n};\n\n/**\n * Minimal JSON-line logger to stdout/stderr. Suitable for development and\n * containerized environments. For production prefer wiring a real logger\n * (pino, winston) by implementing the `Logger` interface.\n */\nexport function consoleLogger(bindings: LogContext = {}): Logger {\n const emit = (level: LogLevel, message: string, context?: LogContext): void => {\n const entry = {\n level,\n message,\n timestamp: new Date().toISOString(),\n ...bindings,\n ...context,\n };\n const line = JSON.stringify(entry);\n if (level === 'error' || level === 'warn') {\n console.error(line);\n } else {\n console.log(line);\n }\n };\n return {\n debug: (m, c) => emit('debug', m, c),\n info: (m, c) => emit('info', m, c),\n warn: (m, c) => emit('warn', m, c),\n error: (m, c) => emit('error', m, c),\n child: (b) => consoleLogger({ ...bindings, ...b }),\n };\n}\n","export interface RateLimiterOptions {\n /** Maximum number of tokens the bucket holds. Each `acquire()` consumes 1. */\n capacity: number;\n /** Time window in milliseconds over which `capacity` tokens are refilled. */\n intervalMs: number;\n}\n\n/**\n * Token-bucket rate limiter. `acquire()` returns immediately when a token\n * is available, otherwise it sleeps until enough tokens have been refilled.\n *\n * @example\n * const limiter = new TokenBucketRateLimiter({ capacity: 50, intervalMs: 60_000 });\n * for (const item of items) {\n * await limiter.acquire();\n * await fetchSomething(item);\n * }\n */\nexport class TokenBucketRateLimiter {\n private tokens: number;\n private lastRefillAt: number;\n private readonly capacity: number;\n private readonly intervalMs: number;\n\n constructor(options: RateLimiterOptions) {\n if (options.capacity <= 0 || options.intervalMs <= 0) {\n throw new RangeError('capacity and intervalMs must be > 0');\n }\n this.capacity = options.capacity;\n this.intervalMs = options.intervalMs;\n this.tokens = options.capacity;\n this.lastRefillAt = Date.now();\n }\n\n /**\n * Refill tokens based on elapsed time, then return ms to wait until the next\n * token is available (0 if one is available right now).\n */\n private timeUntilNextToken(): number {\n const now = Date.now();\n const elapsed = now - this.lastRefillAt;\n if (elapsed > 0) {\n const refill = (elapsed / this.intervalMs) * this.capacity;\n this.tokens = Math.min(this.capacity, this.tokens + refill);\n this.lastRefillAt = now;\n }\n if (this.tokens >= 1) return 0;\n const needed = 1 - this.tokens;\n return Math.ceil((needed / this.capacity) * this.intervalMs);\n }\n\n async acquire(signal?: AbortSignal): Promise<void> {\n for (;;) {\n if (signal?.aborted) throw signal.reason as Error;\n const wait = this.timeUntilNextToken();\n if (wait === 0) {\n this.tokens -= 1;\n return;\n }\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(resolve, wait);\n signal?.addEventListener(\n 'abort',\n () => {\n clearTimeout(timer);\n reject(signal.reason);\n },\n { once: true },\n );\n });\n }\n }\n\n /** Snapshot of available tokens (refills the bucket as a side effect). */\n available(): number {\n this.timeUntilNextToken();\n return this.tokens;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/currency.ts","../src/money.ts","../src/pagination.ts","../src/order-status.ts","../src/errors.ts","../src/retry.ts","../src/logger.ts","../src/rate-limiter.ts","../src/transport.ts"],"names":["randomUUID"],"mappings":";;;;;AASO,IAAM,GAAA,GAAM;AACZ,IAAM,GAAA,GAAM;AACZ,IAAM,GAAA,GAAM;AACZ,IAAM,GAAA,GAAM;AAEnB,IAAM,WAAA,GAAc,YAAA;AAEb,SAAS,oBAAoB,IAAA,EAAuB;AACzD,EAAA,OAAO,WAAA,CAAY,KAAK,IAAI,CAAA;AAC9B;;;ACLO,SAAS,KAAA,CAAM,QAAgB,QAAA,EAA2B;AAC/D,EAAA,IAAI,CAAC,MAAA,CAAO,SAAA,CAAU,MAAM,CAAA,EAAG;AAC7B,IAAA,MAAM,IAAI,SAAA,CAAU,CAAA,gDAAA,EAAmD,MAAM,CAAA,CAAE,CAAA;AAAA,EACjF;AACA,EAAA,OAAO,EAAE,QAAQ,QAAA,EAAS;AAC5B;AAeO,SAAS,cAAA,CAAe,KAAA,EAAe,QAAA,EAAoB,UAAA,GAAa,CAAA,EAAU;AACvF,EAAA,MAAM,SAAS,EAAA,IAAM,UAAA;AACrB,EAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,KAAA,GAAQ,MAAM,CAAA;AACxC,EAAA,OAAO,KAAA,CAAM,QAAQ,QAAQ,CAAA;AAC/B;AAYO,SAAS,YAAA,CAAa,KAAA,EAAc,UAAA,GAAa,CAAA,EAAW;AACjE,EAAA,OAAO,KAAA,CAAM,SAAS,EAAA,IAAM,UAAA;AAC9B;AAEO,SAAS,cAAA,CAAe,GAAU,CAAA,EAAmB;AAC1D,EAAA,OAAO,CAAA,CAAE,aAAa,CAAA,CAAE,QAAA;AAC1B;AAEO,SAAS,QAAA,CAAS,GAAU,CAAA,EAAiB;AAClD,EAAA,IAAI,CAAC,cAAA,CAAe,CAAA,EAAG,CAAC,CAAA,EAAG;AACzB,IAAA,MAAM,IAAI,UAAU,CAAA,0CAAA,EAA6C,CAAA,CAAE,QAAQ,CAAA,IAAA,EAAO,CAAA,CAAE,QAAQ,CAAA,CAAE,CAAA;AAAA,EAChG;AACA,EAAA,OAAO,MAAM,CAAA,CAAE,MAAA,GAAS,CAAA,CAAE,MAAA,EAAQ,EAAE,QAAQ,CAAA;AAC9C;AAEO,SAAS,QAAA,CAAS,GAAU,CAAA,EAAiB;AAClD,EAAA,IAAI,CAAC,cAAA,CAAe,CAAA,EAAG,CAAC,CAAA,EAAG;AACzB,IAAA,MAAM,IAAI,SAAA;AAAA,MACR,CAAA,+CAAA,EAAkD,CAAA,CAAE,QAAQ,CAAA,IAAA,EAAO,EAAE,QAAQ,CAAA;AAAA,KAC/E;AAAA,EACF;AACA,EAAA,OAAO,MAAM,CAAA,CAAE,MAAA,GAAS,CAAA,CAAE,MAAA,EAAQ,EAAE,QAAQ,CAAA;AAC9C;;;AClDA,gBAAuB,QAAA,CACrB,SAAA,EACA,MAAA,GAAiD,EAAC,EACd;AACpC,EAAA,IAAI,MAAA;AACJ,EAAA,GAAG;AACD,IAAA,MAAM,OAAO,MAAM,SAAA,CAAU,EAAE,GAAG,MAAA,EAAQ,QAAQ,CAAA;AAClD,IAAA,KAAA,MAAW,IAAA,IAAQ,KAAK,KAAA,EAAO;AAC7B,MAAA,MAAM,IAAA;AAAA,IACR;AACA,IAAA,MAAA,GAAS,IAAA,CAAK,UAAA;AAAA,EAChB,CAAA,QAAS,MAAA;AACX;AA4CA,gBAAuB,cAAA,CACrB,SAAA,EACA,MAAA,GAAgD,EAAC,EACb;AACpC,EAAA,MAAM,QAAQ,MAAA,CAAO,KAAA;AACrB,EAAA,MAAM,QAAA,GAAW,OAAO,QAAA,IAAY,QAAA;AACpC,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,IAAI,KAAA,GAAQ,CAAA;AACZ,EAAA,OAAO,QAAQ,QAAA,EAAU;AACvB,IAAA,MAAM,OAAO,MAAM,SAAA,CAAU,EAAE,MAAA,EAAQ,OAAO,CAAA;AAC9C,IAAA,KAAA,MAAW,IAAA,IAAQ,KAAK,KAAA,EAAO;AAC7B,MAAA,MAAM,IAAA;AAAA,IACR;AACA,IAAA,KAAA,IAAS,CAAA;AACT,IAAA,IAAI,IAAA,CAAK,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG;AAC7B,IAAA,IAAI,IAAA,CAAK,SAAA,GAAY,CAAA,IAAK,KAAA,IAAS,KAAK,SAAA,EAAW;AAGnD,IAAA,MAAA,IAAU,IAAA,CAAK,KAAA,IAAS,KAAA,IAAS,IAAA,CAAK,KAAA,CAAM,MAAA;AAAA,EAC9C;AACF;;;ACjDO,SAAS,uBACd,GAAA,EACyC;AACzC,EAAA,MAAM,MAAA,GAAS,GAAA;AACf,EAAA,OAAO,CAAC,GAAA,KAAwC;AAC9C,IAAA,MAAM,UAAA,GAAa,OAAO,GAAG,CAAA;AAC7B,IAAA,OAAO,UAAA,KAAe,MAAA,GAClB,EAAE,UAAA,EAAY,GAAA,EAAK,MAAA,EAAQ,IAAA,EAAK,GAChC,EAAE,UAAA,EAAY,SAAA,EAAW,GAAA,EAAK,QAAQ,KAAA,EAAM;AAAA,EAClD,CAAA;AACF;;;AChBO,IAAM,UAAA,GAAN,cAAyB,KAAA,CAAM;AAAA,EAC3B,IAAA;AAAA,EACA,SAAA;AAAA,EACA,MAAA;AAAA,EACA,YAAA;AAAA,EACA,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAA;AAAA,EAET,YAAY,IAAA,EAAyB;AACnC,IAAA,KAAA,CAAM,IAAA,CAAK,OAAA,EAAS,IAAA,CAAK,KAAA,KAAU,MAAA,GAAY,EAAE,KAAA,EAAO,IAAA,CAAK,KAAA,EAAM,GAAI,MAAS,CAAA;AAChF,IAAA,IAAA,CAAK,IAAA,GAAO,YAAA;AACZ,IAAA,IAAA,CAAK,OAAO,IAAA,CAAK,IAAA;AACjB,IAAA,IAAA,CAAK,SAAA,GAAY,KAAK,SAAA,IAAa,KAAA;AACnC,IAAA,IAAA,CAAK,SAAS,IAAA,CAAK,MAAA;AACnB,IAAA,IAAA,CAAK,eAAe,IAAA,CAAK,YAAA;AACzB,IAAA,IAAA,CAAK,OAAO,IAAA,CAAK,IAAA;AACjB,IAAA,IAAA,CAAK,MAAA,GAAS,IAAA,CAAK,MAAA,IAAU,EAAC;AAAA,EAChC;AACF;AAIO,IAAM,SAAA,GAAN,cAAwB,UAAA,CAAW;AAAA,EACxC,YAAY,IAAA,EAAuB;AACjC,IAAA,KAAA,CAAM,EAAE,GAAG,IAAA,EAAM,MAAM,aAAA,EAAe,SAAA,EAAW,OAAO,CAAA;AACxD,IAAA,IAAA,CAAK,IAAA,GAAO,WAAA;AAAA,EACd;AACF;AAEO,IAAM,cAAA,GAAN,cAA6B,UAAA,CAAW;AAAA,EAC7C,YAAY,IAAA,EAAuB;AACjC,IAAA,KAAA,CAAM,EAAE,GAAG,IAAA,EAAM,MAAM,cAAA,EAAgB,SAAA,EAAW,MAAM,CAAA;AACxD,IAAA,IAAA,CAAK,IAAA,GAAO,gBAAA;AAAA,EACd;AACF;AAEO,IAAM,eAAA,GAAN,cAA8B,UAAA,CAAW;AAAA,EAC9C,YAAY,IAAA,EAAuB;AACjC,IAAA,KAAA,CAAM,EAAE,GAAG,IAAA,EAAM,MAAM,mBAAA,EAAqB,SAAA,EAAW,OAAO,CAAA;AAC9D,IAAA,IAAA,CAAK,IAAA,GAAO,iBAAA;AAAA,EACd;AACF;AAEO,IAAM,aAAA,GAAN,cAA4B,UAAA,CAAW;AAAA,EAC5C,YAAY,IAAA,EAAuB;AACjC,IAAA,KAAA,CAAM,EAAE,GAAG,IAAA,EAAM,MAAM,WAAA,EAAa,SAAA,EAAW,OAAO,CAAA;AACtD,IAAA,IAAA,CAAK,IAAA,GAAO,eAAA;AAAA,EACd;AACF;AAEO,IAAM,WAAA,GAAN,cAA0B,UAAA,CAAW;AAAA,EAC1C,YAAY,IAAA,EAAuB;AACjC,IAAA,KAAA,CAAM,EAAE,GAAG,IAAA,EAAM,MAAM,cAAA,EAAgB,SAAA,EAAW,MAAM,CAAA;AACxD,IAAA,IAAA,CAAK,IAAA,GAAO,aAAA;AAAA,EACd;AACF;AAEO,IAAM,YAAA,GAAN,cAA2B,UAAA,CAAW;AAAA,EAC3C,YAAY,IAAA,EAAuB;AACjC,IAAA,KAAA,CAAM,EAAE,GAAG,IAAA,EAAM,MAAM,eAAA,EAAiB,SAAA,EAAW,MAAM,CAAA;AACzD,IAAA,IAAA,CAAK,IAAA,GAAO,cAAA;AAAA,EACd;AACF;AAEO,IAAM,YAAA,GAAN,cAA2B,UAAA,CAAW;AAAA,EAC3C,YAAY,IAAA,EAAuB;AACjC,IAAA,KAAA,CAAM,EAAE,GAAG,IAAA,EAAM,MAAM,SAAA,EAAW,SAAA,EAAW,MAAM,CAAA;AACnD,IAAA,IAAA,CAAK,IAAA,GAAO,cAAA;AAAA,EACd;AACF;AAEO,SAAS,aAAa,KAAA,EAAqC;AAChE,EAAA,OAAO,KAAA,YAAiB,UAAA;AAC1B;AAEO,SAAS,iBAAiB,KAAA,EAAyB;AACxD,EAAA,OAAO,YAAA,CAAa,KAAK,CAAA,IAAK,KAAA,CAAM,SAAA;AACtC;AAeO,SAAS,0BAA0B,KAAA,EAAyB;AACjE,EAAA,OAAO,aAAa,KAAK,CAAA,IAAK,KAAA,CAAM,SAAA,IAAa,MAAM,IAAA,KAAS,cAAA;AAClE;AAWO,SAAS,gBAAgB,MAAA,EAA2C;AACzE,EAAA,IAAI,CAAC,QAAQ,OAAO,MAAA;AACpB,EAAA,MAAM,OAAA,GAAU,OAAO,MAAM,CAAA;AAC7B,EAAA,IAAI,CAAC,OAAO,KAAA,CAAM,OAAO,GAAG,OAAO,OAAA,GAAU,CAAA,GAAI,OAAA,GAAU,GAAA,GAAO,MAAA;AAClE,EAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,MAAM,CAAA;AAC/B,EAAA,IAAI,CAAC,MAAA,CAAO,KAAA,CAAM,KAAK,CAAA,EAAG;AACxB,IAAA,MAAM,KAAA,GAAQ,KAAA,GAAQ,IAAA,CAAK,GAAA,EAAI;AAC/B,IAAA,OAAO,KAAA,GAAQ,IAAI,KAAA,GAAQ,MAAA;AAAA,EAC7B;AACA,EAAA,OAAO,MAAA;AACT;;;AC/IA,IAAM,QAAA,GAAW;AAAA,EACf,WAAA,EAAa,CAAA;AAAA,EACb,WAAA,EAAa,GAAA;AAAA,EACb,UAAA,EAAY,GAAA;AAAA,EACZ,MAAA,EAAQ;AACV,CAAA;AAEA,SAAS,KAAA,CAAM,IAAY,MAAA,EAAqC;AAC9D,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAS,MAAA,KAAW;AACtC,IAAA,IAAI,QAAQ,OAAA,EAAS;AACnB,MAAA,MAAA,CAAO,OAAO,MAAM,CAAA;AACpB,MAAA;AAAA,IACF;AACA,IAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,OAAA,EAAS,EAAE,CAAA;AACpC,IAAA,MAAA,EAAQ,gBAAA;AAAA,MACN,OAAA;AAAA,MACA,MAAM;AACJ,QAAA,YAAA,CAAa,KAAK,CAAA;AAClB,QAAA,MAAA,CAAO,OAAO,MAAM,CAAA;AAAA,MACtB,CAAA;AAAA,MACA,EAAE,MAAM,IAAA;AAAK,KACf;AAAA,EACF,CAAC,CAAA;AACH;AAEA,eAAsB,KAAA,CACpB,EAAA,EACA,OAAA,GAAwB,EAAC,EACb;AACZ,EAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,WAAA,IAAe,QAAA,CAAS,WAAA;AACpD,EAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,WAAA,IAAe,QAAA,CAAS,WAAA;AACpD,EAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,UAAA,IAAc,QAAA,CAAS,UAAA;AAClD,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,MAAA,IAAU,QAAA,CAAS,MAAA;AAC1C,EAAA,MAAM,WAAA,GAAc,QAAQ,WAAA,IAAe,gBAAA;AAE3C,EAAA,IAAI,OAAA;AACJ,EAAA,KAAA,IAAS,OAAA,GAAU,CAAA,EAAG,OAAA,IAAW,WAAA,EAAa,OAAA,EAAA,EAAW;AACvD,IAAA,IAAI;AACF,MAAA,OAAO,MAAM,GAAG,OAAO,CAAA;AAAA,IACzB,SAAS,GAAA,EAAK;AACZ,MAAA,OAAA,GAAU,GAAA;AACV,MAAA,IAAI,OAAA,KAAY,WAAA,IAAe,CAAC,WAAA,CAAY,GAAG,CAAA,EAAG;AAChD,QAAA,MAAM,GAAA;AAAA,MACR;AAEA,MAAA,MAAM,YAAA,GAAe,YAAA,CAAa,GAAG,CAAA,GAAI,IAAI,YAAA,GAAe,MAAA;AAC5D,MAAA,MAAM,cAAc,IAAA,CAAK,GAAA,CAAI,YAAY,WAAA,GAAc,CAAA,KAAM,UAAU,CAAA,CAAE,CAAA;AAGzE,MAAA,MAAM,IAAA,GAAO,YAAA,IAAgB,YAAA,GAAe,CAAA,GAAI,YAAA,GAAe,WAAA;AAC/D,MAAA,MAAM,WAAW,MAAA,GAAS,IAAA,CAAK,MAAA,EAAO,GAAI,OAAO,GAAA,GAAM,CAAA;AACvD,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,GAAA,CAAI,UAAA,EAAY,OAAO,QAAQ,CAAA;AAElD,MAAA,OAAA,CAAQ,OAAA,GAAU,GAAA,EAAK,OAAA,EAAS,KAAK,CAAA;AACrC,MAAA,MAAM,KAAA,CAAM,KAAA,EAAO,OAAA,CAAQ,MAAM,CAAA;AAAA,IACnC;AAAA,EACF;AACA,EAAA,MAAM,OAAA;AACR;;;AChEO,IAAM,UAAA,GAAqB;AAAA,EAChC,OAAO,MAAM;AAAA,EAAC,CAAA;AAAA,EACd,MAAM,MAAM;AAAA,EAAC,CAAA;AAAA,EACb,MAAM,MAAM;AAAA,EAAC,CAAA;AAAA,EACb,OAAO,MAAM;AAAA,EAAC,CAAA;AAAA,EACd,OAAO,MAAM;AACf;AAOO,SAAS,aAAA,CAAc,QAAA,GAAuB,EAAC,EAAW;AAC/D,EAAA,MAAM,IAAA,GAAO,CAAC,KAAA,EAAiB,OAAA,EAAiB,OAAA,KAA+B;AAC7E,IAAA,MAAM,KAAA,GAAQ;AAAA,MACZ,KAAA;AAAA,MACA,OAAA;AAAA,MACA,SAAA,EAAA,iBAAW,IAAI,IAAA,EAAK,EAAE,WAAA,EAAY;AAAA,MAClC,GAAG,QAAA;AAAA,MACH,GAAG;AAAA,KACL;AACA,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,SAAA,CAAU,KAAK,CAAA;AACjC,IAAA,IAAI,KAAA,KAAU,OAAA,IAAW,KAAA,KAAU,MAAA,EAAQ;AACzC,MAAA,OAAA,CAAQ,MAAM,IAAI,CAAA;AAAA,IACpB,CAAA,MAAO;AACL,MAAA,OAAA,CAAQ,IAAI,IAAI,CAAA;AAAA,IAClB;AAAA,EACF,CAAA;AACA,EAAA,OAAO;AAAA,IACL,OAAO,CAAC,CAAA,EAAG,MAAM,IAAA,CAAK,OAAA,EAAS,GAAG,CAAC,CAAA;AAAA,IACnC,MAAM,CAAC,CAAA,EAAG,MAAM,IAAA,CAAK,MAAA,EAAQ,GAAG,CAAC,CAAA;AAAA,IACjC,MAAM,CAAC,CAAA,EAAG,MAAM,IAAA,CAAK,MAAA,EAAQ,GAAG,CAAC,CAAA;AAAA,IACjC,OAAO,CAAC,CAAA,EAAG,MAAM,IAAA,CAAK,OAAA,EAAS,GAAG,CAAC,CAAA;AAAA,IACnC,KAAA,EAAO,CAAC,CAAA,KAAM,aAAA,CAAc,EAAE,GAAG,QAAA,EAAU,GAAG,CAAA,EAAG;AAAA,GACnD;AACF;;;AC/BO,IAAM,yBAAN,MAA6B;AAAA,EAC1B,MAAA;AAAA,EACA,YAAA;AAAA,EACS,QAAA;AAAA,EACA,UAAA;AAAA,EAEjB,YAAY,OAAA,EAA6B;AACvC,IAAA,IAAI,OAAA,CAAQ,QAAA,IAAY,CAAA,IAAK,OAAA,CAAQ,cAAc,CAAA,EAAG;AACpD,MAAA,MAAM,IAAI,WAAW,qCAAqC,CAAA;AAAA,IAC5D;AACA,IAAA,IAAA,CAAK,WAAW,OAAA,CAAQ,QAAA;AACxB,IAAA,IAAA,CAAK,aAAa,OAAA,CAAQ,UAAA;AAC1B,IAAA,IAAA,CAAK,SAAS,OAAA,CAAQ,QAAA;AACtB,IAAA,IAAA,CAAK,YAAA,GAAe,KAAK,GAAA,EAAI;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,kBAAA,GAA6B;AACnC,IAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,YAAA;AAC3B,IAAA,IAAI,UAAU,CAAA,EAAG;AACf,MAAA,MAAM,MAAA,GAAU,OAAA,GAAU,IAAA,CAAK,UAAA,GAAc,IAAA,CAAK,QAAA;AAClD,MAAA,IAAA,CAAK,SAAS,IAAA,CAAK,GAAA,CAAI,KAAK,QAAA,EAAU,IAAA,CAAK,SAAS,MAAM,CAAA;AAC1D,MAAA,IAAA,CAAK,YAAA,GAAe,GAAA;AAAA,IACtB;AACA,IAAA,IAAI,IAAA,CAAK,MAAA,IAAU,CAAA,EAAG,OAAO,CAAA;AAC7B,IAAA,MAAM,MAAA,GAAS,IAAI,IAAA,CAAK,MAAA;AACxB,IAAA,OAAO,KAAK,IAAA,CAAM,MAAA,GAAS,IAAA,CAAK,QAAA,GAAY,KAAK,UAAU,CAAA;AAAA,EAC7D;AAAA,EAEA,MAAM,QAAQ,MAAA,EAAqC;AACjD,IAAA,WAAS;AACP,MAAA,IAAI,MAAA,EAAQ,OAAA,EAAS,MAAM,MAAA,CAAO,MAAA;AAClC,MAAA,MAAM,IAAA,GAAO,KAAK,kBAAA,EAAmB;AACrC,MAAA,IAAI,SAAS,CAAA,EAAG;AACd,QAAA,IAAA,CAAK,MAAA,IAAU,CAAA;AACf,QAAA;AAAA,MACF;AACA,MAAA,MAAM,IAAI,OAAA,CAAc,CAAC,OAAA,EAAS,MAAA,KAAW;AAC3C,QAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,OAAA,EAAS,IAAI,CAAA;AACtC,QAAA,MAAA,EAAQ,gBAAA;AAAA,UACN,OAAA;AAAA,UACA,MAAM;AACJ,YAAA,YAAA,CAAa,KAAK,CAAA;AAClB,YAAA,MAAA,CAAO,OAAO,MAAM,CAAA;AAAA,UACtB,CAAA;AAAA,UACA,EAAE,MAAM,IAAA;AAAK,SACf;AAAA,MACF,CAAC,CAAA;AAAA,IACH;AAAA,EACF;AAAA;AAAA,EAGA,SAAA,GAAoB;AAClB,IAAA,IAAA,CAAK,kBAAA,EAAmB;AACxB,IAAA,OAAO,IAAA,CAAK,MAAA;AAAA,EACd;AACF;ACPO,SAAS,gBACd,MAAA,EAC4B;AAC5B,EAAA,MAAM,MAAA,GAAS,OAAO,MAAA,IAAU,UAAA;AAEhC,EAAA,OAAO,SAAS,QAAW,IAAA,EAAqB;AAC9C,IAAA,MAAM,YAAA,GAAe,IAAA,CAAK,MAAA,KAAW,KAAA,IAAS,KAAK,UAAA,KAAe,IAAA;AAClE,IAAA,MAAM,QAAQ,MAAM,MAAA,CAAO,SAAA,GAAY,IAAI,KAAK,EAAC;AAEjD,IAAA,OAAO,KAAA;AAAA,MACL,OAAO,OAAA,KAAY;AACjB,QAAA,IAAI,KAAK,WAAA,EAAa,MAAM,KAAK,WAAA,CAAY,OAAA,CAAQ,KAAK,MAAM,CAAA;AAEhE,QAAA,MAAM,gBAAgBA,iBAAA,EAAW;AACjC,QAAA,MAAM,GAAA,GAAM,MAAA,CAAO,QAAA,CAAS,IAAI,CAAA;AAChC,QAAA,MAAM,OAAA,GAAU,EAAE,GAAG,MAAA,CAAO,aAAa,aAAa,CAAA,EAAG,GAAG,IAAA,CAAK,OAAA,EAAQ;AACzE,QAAA,MAAM,IAAA,GAAoB;AAAA,UACxB,QAAQ,IAAA,CAAK,MAAA;AAAA,UACb,OAAA;AAAA,UACA,MAAA,EAAQ,aAAA,CAAc,IAAA,CAAK,MAAA,EAAQ,OAAO,SAAS;AAAA,SACrD;AACA,QAAA,IAAI,IAAA,CAAK,IAAA,KAAS,KAAA,CAAA,IAAa,IAAA,CAAK,WAAW,KAAA,EAAO;AACpD,UAAA,IAAI,IAAA,CAAK,gBAAgB,QAAA,EAAU;AAEjC,YAAA,IAAA,CAAK,OAAO,IAAA,CAAK,IAAA;AACjB,YAAA,OAAQ,QAAmC,cAAc,CAAA;AAAA,UAC3D,CAAA,MAAO;AACL,YAAA,IAAA,CAAK,IAAA,GAAO,IAAA,CAAK,SAAA,CAAU,IAAA,CAAK,IAAI,CAAA;AAAA,UACtC;AAAA,QACF;AAEA,QAAA,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,MAAA,CAAO,SAAS,CAAA,QAAA,CAAA,EAAY;AAAA,UAC1C,QAAQ,IAAA,CAAK,MAAA;AAAA,UACb,GAAA;AAAA,UACA,aAAA;AAAA,UACA,OAAA;AAAA,UACA,GAAG,KAAA;AAAM,SACV,CAAA;AAED,QAAA,IAAI,QAAA;AACJ,QAAA,IAAI;AACF,UAAA,QAAA,GAAW,MAAM,MAAA,CAAO,KAAA,CAAM,GAAA,EAAK,IAAI,CAAA;AAAA,QACzC,SAAS,GAAA,EAAK;AACZ,UAAA,IAAI,GAAA,YAAe,KAAA,IAAS,GAAA,CAAI,IAAA,KAAS,YAAA,EAAc;AACrD,YAAA,MAAM,IAAI,YAAA,CAAa;AAAA,cACrB,SAAS,CAAA,EAAG,MAAA,CAAO,KAAK,CAAA,yBAAA,EAA4B,OAAO,SAAS,CAAA,EAAA,CAAA;AAAA,cACpE,KAAA,EAAO;AAAA,aACR,CAAA;AAAA,UACH;AACA,UAAA,MAAM,IAAI,YAAA,CAAa,EAAE,OAAA,EAAS,CAAA,EAAG,OAAO,KAAK,CAAA,gBAAA,CAAA,EAAoB,KAAA,EAAO,GAAA,EAAK,CAAA;AAAA,QACnF;AAEA,QAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,UAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,QAAQ,CAAA;AACpC,UAAA,MAAM,eAAe,eAAA,CAAgB,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,aAAa,CAAC,CAAA;AACxE,UAAA,MAAM,QAAQ,MAAA,CAAO,YAAA,CAAa,QAAA,CAAS,MAAA,EAAQ,MAAM,YAAY,CAAA;AACrE,UAAA,MAAA,CAAO,IAAA,CAAK,CAAA,EAAG,MAAA,CAAO,SAAS,CAAA,MAAA,CAAA,EAAU;AAAA,YACvC,QAAQ,IAAA,CAAK,MAAA;AAAA,YACb,GAAA;AAAA,YACA,aAAA;AAAA,YACA,QAAQ,QAAA,CAAS,MAAA;AAAA,YACjB,MAAM,KAAA,CAAM,IAAA;AAAA,YACZ,WAAW,KAAA,CAAM,SAAA;AAAA,YACjB,GAAG,KAAA;AAAM,WACV,CAAA;AACD,UAAA,MAAM,KAAA;AAAA,QACR;AAEA,QAAA,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,MAAA,CAAO,SAAS,CAAA,SAAA,CAAA,EAAa;AAAA,UAC3C,aAAA;AAAA,UACA,QAAQ,QAAA,CAAS;AAAA,SAClB,CAAA;AAED,QAAA,IAAI,QAAA,CAAS,MAAA,KAAW,GAAA,EAAK,OAAO,KAAA,CAAA;AACpC,QAAA,OAAQ,MAAM,SAAS,QAAQ,CAAA;AAAA,MACjC,CAAA;AAAA,MACA;AAAA,QACE,QAAQ,IAAA,CAAK,MAAA;AAAA;AAAA;AAAA;AAAA,QAIb,WAAA,EAAa,eAAe,MAAA,GAAY,yBAAA;AAAA,QACxC,OAAA,EAAS,CAAC,GAAA,EAAK,OAAA,EAAS,KAAA,KAAU;AAChC,UAAA,IAAI,eAAe,UAAA,EAAY;AAC7B,YAAA,MAAA,CAAO,IAAA,CAAK,CAAA,EAAG,MAAA,CAAO,SAAS,CAAA,MAAA,CAAA,EAAU;AAAA,cACvC,OAAA;AAAA,cACA,OAAA,EAAS,KAAA;AAAA,cACT,MAAM,GAAA,CAAI,IAAA;AAAA,cACV,QAAQ,GAAA,CAAI;AAAA,aACb,CAAA;AAAA,UACH;AAAA,QACF;AAAA;AACF,KACF;AAAA,EACF,CAAA;AACF;AAGO,SAAS,aAAA,CAAc,UAAmC,SAAA,EAAgC;AAC/F,EAAA,MAAM,aAAA,GAAgB,WAAA,CAAY,OAAA,CAAQ,SAAS,CAAA;AACnD,EAAA,IAAI,CAAC,UAAU,OAAO,aAAA;AACtB,EAAA,OAAO,WAAA,CAAY,GAAA,CAAI,CAAC,QAAA,EAAU,aAAa,CAAC,CAAA;AAClD;AAGA,eAAsB,SAAS,QAAA,EAAsC;AACnE,EAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,EAAK;AACjC,EAAA,IAAI,CAAC,MAAM,OAAO,MAAA;AAClB,EAAA,IAAI;AACF,IAAA,OAAO,IAAA,CAAK,MAAM,IAAI,CAAA;AAAA,EACxB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF","file":"index.cjs","sourcesContent":["/**\n * ISO 4217 three-letter currency code.\n *\n * Typed as a string for flexibility — Trendyol and similar marketplaces may\n * expose currencies beyond the common set, so we don't lock down to a union.\n * Use `isValidCurrencyCode` for runtime validation.\n */\nexport type Currency = string;\n\nexport const TRY = 'TRY';\nexport const USD = 'USD';\nexport const EUR = 'EUR';\nexport const GBP = 'GBP';\n\nconst ISO_4217_RE = /^[A-Z]{3}$/;\n\nexport function isValidCurrencyCode(code: string): boolean {\n return ISO_4217_RE.test(code);\n}\n","import type { Currency } from './currency.js';\n\n/**\n * A monetary amount in integer minor units (e.g., kuruş for TRY, cents for USD).\n *\n * Using integers avoids floating-point surprises. Convert to/from major units\n * with `moneyFromMajor` and `moneyToMajor`.\n */\nexport interface Money {\n amount: number;\n currency: Currency;\n}\n\nexport function money(amount: number, currency: Currency): Money {\n if (!Number.isInteger(amount)) {\n throw new TypeError(`Money amount must be an integer minor unit, got ${amount}`);\n }\n return { amount, currency };\n}\n\n/**\n * Convert a major-unit amount (e.g. lira, dollars) to {@link Money} in integer\n * minor units. This is the canonical lira→kuruş converter — prefer it over a\n * hand-rolled `Math.round(x * 100)`, which scatters rounding logic and risks\n * float drift.\n *\n * @example\n * ```ts\n * import { moneyFromMajor, TRY } from '@lonca/core';\n * // A marketplace price of 199.90 ₺:\n * moneyFromMajor(199.9, TRY); // { amount: 19990, currency: 'TRY' } — kuruş\n * ```\n */\nexport function moneyFromMajor(major: number, currency: Currency, minorScale = 2): Money {\n const factor = 10 ** minorScale;\n const amount = Math.round(major * factor);\n return money(amount, currency);\n}\n\n/**\n * Convert {@link Money} (integer minor units) back to a major-unit number —\n * the canonical kuruş→lira converter.\n *\n * @example\n * ```ts\n * import { moneyToMajor, TRY } from '@lonca/core';\n * moneyToMajor({ amount: 19990, currency: TRY }); // 199.9 (lira)\n * ```\n */\nexport function moneyToMajor(value: Money, minorScale = 2): number {\n return value.amount / 10 ** minorScale;\n}\n\nexport function isSameCurrency(a: Money, b: Money): boolean {\n return a.currency === b.currency;\n}\n\nexport function addMoney(a: Money, b: Money): Money {\n if (!isSameCurrency(a, b)) {\n throw new TypeError(`Cannot add Money of different currencies: ${a.currency} vs ${b.currency}`);\n }\n return money(a.amount + b.amount, a.currency);\n}\n\nexport function subMoney(a: Money, b: Money): Money {\n if (!isSameCurrency(a, b)) {\n throw new TypeError(\n `Cannot subtract Money of different currencies: ${a.currency} vs ${b.currency}`,\n );\n }\n return money(a.amount - b.amount, a.currency);\n}\n","export interface CursorPaginationParams {\n /** Opaque cursor from previous response's `nextCursor`. Omit for the first page. */\n cursor?: string;\n /** Maximum items in the page. Marketplace SDKs may clamp this to their own bounds. */\n limit?: number;\n}\n\nexport interface CursorPage<T> {\n items: T[];\n /** Opaque cursor for the next page. Absent when there are no more pages. */\n nextCursor?: string;\n}\n\n/**\n * Drive a cursor-paginated endpoint as an async iterator.\n *\n * @example\n * for await (const order of paginate((p) => client.orders.list(p))) {\n * console.log(order.id);\n * }\n */\nexport async function* paginate<T>(\n fetchPage: (params: CursorPaginationParams) => Promise<CursorPage<T>>,\n params: Omit<CursorPaginationParams, 'cursor'> = {},\n): AsyncGenerator<T, void, undefined> {\n let cursor: string | undefined;\n do {\n const page = await fetchPage({ ...params, cursor });\n for (const item of page.items) {\n yield item;\n }\n cursor = page.nextCursor;\n } while (cursor);\n}\n\n// ─── Offset pagination ─────────────────────────────────────────────────────\n\nexport interface OffsetPaginationParams {\n /** Zero-based offset. Default depends on the SDK; usually `0`. */\n offset?: number;\n /** Maximum items in the page. */\n limit?: number;\n}\n\n/**\n * Offset-based pagination envelope returned by marketplaces that expose\n * `{ totalCount, limit, offset, pageCount, items[] }` instead of opaque\n * cursors. Hepsiburada's OMS uses this shape; some legacy Trendyol\n * endpoints do too. Distinct from `CursorPage<T>` so callers can tell at\n * the type level which pagination model an endpoint uses.\n */\nexport interface OffsetPage<T> {\n /** Total number of items matching the query across all pages. */\n totalCount: number;\n /** Echo of the request `limit`. */\n limit: number;\n /** Echo of the request `offset`. */\n offset: number;\n /** Total number of pages at the requested `limit`. */\n pageCount: number;\n /** Page rows. */\n items: T[];\n}\n\n/**\n * Drive an offset-paginated endpoint as an async iterator. Stops when the\n * server's `pageCount` is exhausted (or after the configured `maxPages`\n * to bound runaway iteration). Default page size is whatever the\n * underlying endpoint clamps to — pass `limit` explicitly for control.\n *\n * @example\n * for await (const order of paginateOffset((p) =>\n * client.orders.list(p),\n * )) {\n * console.log(order.orderNumber);\n * }\n */\nexport async function* paginateOffset<T>(\n fetchPage: (params: OffsetPaginationParams) => Promise<OffsetPage<T>>,\n params: { limit?: number; maxPages?: number } = {},\n): AsyncGenerator<T, void, undefined> {\n const limit = params.limit;\n const maxPages = params.maxPages ?? Infinity;\n let offset = 0;\n let pages = 0;\n while (pages < maxPages) {\n const page = await fetchPage({ offset, limit });\n for (const item of page.items) {\n yield item;\n }\n pages += 1;\n if (page.items.length === 0) return;\n if (page.pageCount > 0 && pages >= page.pageCount) return;\n // Step by the server-echoed limit (when present) so requests stay aligned with how\n // the server batches; fall back to the caller's `limit` or the observed batch size.\n offset += page.limit || limit || page.items.length;\n }\n}\n","/**\n * The closed, marketplace-agnostic order/shipment status vocabulary that every\n * SDK normalizes its raw statuses into.\n *\n * This union is intentionally **closed** (no `(string & {})` catch-all): a value\n * a marketplace SDK can't map lands on `'unknown'` at runtime via\n * {@link createStatusNormalizer}, surfaced by `mapped: false` — never silently\n * coerced to a valid-looking default.\n */\nexport type NormalizedOrderStatus =\n | 'created'\n | 'picking'\n | 'invoiced'\n | 'shipped'\n | 'delivered'\n | 'cancelled'\n | 'returned'\n | 'unknown';\n\n/**\n * Result of normalizing a raw marketplace status. The raw value is always\n * preserved, and `mapped` makes an unrecognized status visible at runtime\n * instead of letting it disappear into a default.\n */\nexport interface NormalizedStatusResult {\n /** Mapped normalized status, or `'unknown'` when the raw value wasn't recognized. */\n normalized: NormalizedOrderStatus;\n /** The original raw status string from the marketplace, preserved verbatim. */\n raw: string;\n /** `false` when the raw value had no mapping (normalized fell back to `'unknown'`). */\n mapped: boolean;\n}\n\n/**\n * Build a status normalizer from an SDK-owned, **exhaustive** map of that\n * marketplace's known statuses. Because the map key type is a closed union,\n * forgetting to map a newly added known status is a compile-time error in the\n * SDK — while an unknown value arriving on the wire stays safe at runtime\n * (`{ normalized: 'unknown', mapped: false }`).\n *\n * @example\n * ```ts\n * const statusMap = { Created: 'created', Shipped: 'shipped' } as const;\n * const normalizeStatus = createStatusNormalizer(statusMap);\n * normalizeStatus('Shipped'); // { normalized: 'shipped', raw: 'Shipped', mapped: true }\n * normalizeStatus('Martian'); // { normalized: 'unknown', raw: 'Martian', mapped: false }\n * ```\n */\nexport function createStatusNormalizer<Known extends string>(\n map: Record<Known, NormalizedOrderStatus>,\n): (raw: string) => NormalizedStatusResult {\n const lookup = map as Record<string, NormalizedOrderStatus | undefined>;\n return (raw: string): NormalizedStatusResult => {\n const normalized = lookup[raw];\n return normalized !== undefined\n ? { normalized, raw, mapped: true }\n : { normalized: 'unknown', raw, mapped: false };\n };\n}\n","export type LoncaErrorCode =\n | 'AUTH_FAILED'\n | 'RATE_LIMITED'\n | 'VALIDATION_FAILED'\n | 'NOT_FOUND'\n | 'SERVER_ERROR'\n | 'NETWORK_ERROR'\n | 'TIMEOUT'\n | 'UNKNOWN';\n\n/**\n * A single normalized, field-level error detail extracted from a marketplace's\n * raw error body. Each SDK maps its own (inconsistent) error JSON into this\n * shape so consumers don't have to sniff marketplace-specific payloads.\n */\nexport interface LoncaErrorIssue {\n /** The offending field/path, when the marketplace reports one. */\n field?: string;\n /** Marketplace-specific error code, when present. */\n code?: string;\n /** Human-readable message — always present. */\n message: string;\n}\n\nexport interface LoncaErrorOptions {\n code: LoncaErrorCode;\n message: string;\n cause?: unknown;\n retryable?: boolean;\n status?: number;\n retryAfterMs?: number;\n data?: Record<string, unknown>;\n /** Normalized, field-level error details. Defaults to `[]` when omitted. */\n issues?: LoncaErrorIssue[];\n}\n\n/**\n * Root of the Lonca error hierarchy.\n *\n * Carries a tagged `code` and a `retryable` boolean so retry helpers\n * can decide what to do without sniffing message strings.\n */\nexport class LoncaError extends Error {\n readonly code: LoncaErrorCode;\n readonly retryable: boolean;\n readonly status?: number;\n readonly retryAfterMs?: number;\n readonly data?: Record<string, unknown>;\n /**\n * Normalized, field-level error details mapped from the marketplace's raw\n * error body by the SDK. Always an array (never `undefined`) so callers can\n * iterate without a presence check; empty when nothing was parseable.\n */\n readonly issues: LoncaErrorIssue[];\n\n constructor(opts: LoncaErrorOptions) {\n super(opts.message, opts.cause !== undefined ? { cause: opts.cause } : undefined);\n this.name = 'LoncaError';\n this.code = opts.code;\n this.retryable = opts.retryable ?? false;\n this.status = opts.status;\n this.retryAfterMs = opts.retryAfterMs;\n this.data = opts.data;\n this.issues = opts.issues ?? [];\n }\n}\n\ntype SubclassOptions = Omit<LoncaErrorOptions, 'code' | 'retryable'>;\n\nexport class AuthError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'AUTH_FAILED', retryable: false });\n this.name = 'AuthError';\n }\n}\n\nexport class RateLimitError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'RATE_LIMITED', retryable: true });\n this.name = 'RateLimitError';\n }\n}\n\nexport class ValidationError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'VALIDATION_FAILED', retryable: false });\n this.name = 'ValidationError';\n }\n}\n\nexport class NotFoundError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'NOT_FOUND', retryable: false });\n this.name = 'NotFoundError';\n }\n}\n\nexport class ServerError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'SERVER_ERROR', retryable: true });\n this.name = 'ServerError';\n }\n}\n\nexport class NetworkError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'NETWORK_ERROR', retryable: true });\n this.name = 'NetworkError';\n }\n}\n\nexport class TimeoutError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'TIMEOUT', retryable: true });\n this.name = 'TimeoutError';\n }\n}\n\nexport function isLoncaError(value: unknown): value is LoncaError {\n return value instanceof LoncaError;\n}\n\nexport function isRetryableError(value: unknown): boolean {\n return isLoncaError(value) && value.retryable;\n}\n\n/**\n * Retry predicate for **non-idempotent** requests (POST/PUT/DELETE/PATCH).\n *\n * Only `RateLimitError` (HTTP 429) is replayed, because a 429 means the server\n * rejected the request *before* processing it, so a retry cannot duplicate a\n * side-effect. Ambiguous failures — 5xx, network drops, and client-side\n * timeouts — are deliberately NOT retried here: the write may already have\n * taken effect server-side, and a blind replay would duplicate it (double\n * order split, double cancel, re-pushed price/stock batch).\n *\n * Callers that have made a write idempotent (e.g. by sending an idempotency\n * key) should opt back into full retries via `isRetryableError` instead.\n */\nexport function isRetryableIdempotentOnly(value: unknown): boolean {\n return isLoncaError(value) && value.retryable && value.code === 'RATE_LIMITED';\n}\n\n/**\n * Parse a `Retry-After` header value (delta-seconds OR an HTTP-date) into\n * milliseconds. Returns `undefined` when the header is absent, unparseable, or\n * non-positive.\n *\n * A non-positive value is treated as \"no hint\" rather than `0`: a literal\n * `Retry-After: 0` must not collapse exponential backoff into a zero-delay\n * retry storm against the very endpoint that is rate-limiting the client.\n */\nexport function parseRetryAfter(header: string | null): number | undefined {\n if (!header) return undefined;\n const seconds = Number(header);\n if (!Number.isNaN(seconds)) return seconds > 0 ? seconds * 1000 : undefined;\n const epoch = Date.parse(header);\n if (!Number.isNaN(epoch)) {\n const delta = epoch - Date.now();\n return delta > 0 ? delta : undefined;\n }\n return undefined;\n}\n","import { isLoncaError, isRetryableError } from './errors.js';\n\nexport interface RetryOptions {\n /** Total number of attempts including the initial call. Defaults to 3. */\n maxAttempts?: number;\n /** Initial delay before the second attempt, in ms. Defaults to 200. */\n baseDelayMs?: number;\n /** Upper bound on delay between any two attempts, in ms. Defaults to 10_000. */\n maxDelayMs?: number;\n /** Add up to 50% random jitter on each delay. Defaults to true. */\n jitter?: boolean;\n /** Override the default predicate. Default treats `retryable` LoncaErrors as retryable. */\n isRetryable?: (err: unknown) => boolean;\n /** Called before each retry sleep. Useful for structured logging. */\n onRetry?: (err: unknown, attempt: number, nextDelayMs: number) => void;\n /** Abort all pending sleeps and bubble the signal's reason. */\n signal?: AbortSignal;\n}\n\nconst DEFAULTS = {\n maxAttempts: 3,\n baseDelayMs: 200,\n maxDelayMs: 10_000,\n jitter: true,\n} as const;\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(signal.reason);\n return;\n }\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n 'abort',\n () => {\n clearTimeout(timer);\n reject(signal.reason);\n },\n { once: true },\n );\n });\n}\n\nexport async function retry<T>(\n fn: (attempt: number) => Promise<T>,\n options: RetryOptions = {},\n): Promise<T> {\n const maxAttempts = options.maxAttempts ?? DEFAULTS.maxAttempts;\n const baseDelayMs = options.baseDelayMs ?? DEFAULTS.baseDelayMs;\n const maxDelayMs = options.maxDelayMs ?? DEFAULTS.maxDelayMs;\n const jitter = options.jitter ?? DEFAULTS.jitter;\n const isRetryable = options.isRetryable ?? isRetryableError;\n\n let lastErr: unknown;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n return await fn(attempt);\n } catch (err) {\n lastErr = err;\n if (attempt === maxAttempts || !isRetryable(err)) {\n throw err;\n }\n\n const retryAfterMs = isLoncaError(err) ? err.retryAfterMs : undefined;\n const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));\n // A non-positive `retryAfterMs` is ignored (not used as the base) so a\n // `Retry-After: 0` can never collapse backoff to an immediate retry.\n const base = retryAfterMs && retryAfterMs > 0 ? retryAfterMs : exponential;\n const jitterMs = jitter ? Math.random() * base * 0.5 : 0;\n const delay = Math.min(maxDelayMs, base + jitterMs);\n\n options.onRetry?.(err, attempt, delay);\n await sleep(delay, options.signal);\n }\n }\n throw lastErr;\n}\n","export type LogLevel = 'debug' | 'info' | 'warn' | 'error';\n\nexport type LogContext = Record<string, unknown>;\n\nexport interface Logger {\n debug(message: string, context?: LogContext): void;\n info(message: string, context?: LogContext): void;\n warn(message: string, context?: LogContext): void;\n error(message: string, context?: LogContext): void;\n /** Create a child logger that merges `bindings` into every log entry. */\n child(bindings: LogContext): Logger;\n}\n\nexport const noopLogger: Logger = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n child: () => noopLogger,\n};\n\n/**\n * Minimal JSON-line logger to stdout/stderr. Suitable for development and\n * containerized environments. For production prefer wiring a real logger\n * (pino, winston) by implementing the `Logger` interface.\n */\nexport function consoleLogger(bindings: LogContext = {}): Logger {\n const emit = (level: LogLevel, message: string, context?: LogContext): void => {\n const entry = {\n level,\n message,\n timestamp: new Date().toISOString(),\n ...bindings,\n ...context,\n };\n const line = JSON.stringify(entry);\n if (level === 'error' || level === 'warn') {\n console.error(line);\n } else {\n console.log(line);\n }\n };\n return {\n debug: (m, c) => emit('debug', m, c),\n info: (m, c) => emit('info', m, c),\n warn: (m, c) => emit('warn', m, c),\n error: (m, c) => emit('error', m, c),\n child: (b) => consoleLogger({ ...bindings, ...b }),\n };\n}\n","export interface RateLimiterOptions {\n /** Maximum number of tokens the bucket holds. Each `acquire()` consumes 1. */\n capacity: number;\n /** Time window in milliseconds over which `capacity` tokens are refilled. */\n intervalMs: number;\n}\n\n/**\n * Token-bucket rate limiter. `acquire()` returns immediately when a token\n * is available, otherwise it sleeps until enough tokens have been refilled.\n *\n * @example\n * const limiter = new TokenBucketRateLimiter({ capacity: 50, intervalMs: 60_000 });\n * for (const item of items) {\n * await limiter.acquire();\n * await fetchSomething(item);\n * }\n */\nexport class TokenBucketRateLimiter {\n private tokens: number;\n private lastRefillAt: number;\n private readonly capacity: number;\n private readonly intervalMs: number;\n\n constructor(options: RateLimiterOptions) {\n if (options.capacity <= 0 || options.intervalMs <= 0) {\n throw new RangeError('capacity and intervalMs must be > 0');\n }\n this.capacity = options.capacity;\n this.intervalMs = options.intervalMs;\n this.tokens = options.capacity;\n this.lastRefillAt = Date.now();\n }\n\n /**\n * Refill tokens based on elapsed time, then return ms to wait until the next\n * token is available (0 if one is available right now).\n */\n private timeUntilNextToken(): number {\n const now = Date.now();\n const elapsed = now - this.lastRefillAt;\n if (elapsed > 0) {\n const refill = (elapsed / this.intervalMs) * this.capacity;\n this.tokens = Math.min(this.capacity, this.tokens + refill);\n this.lastRefillAt = now;\n }\n if (this.tokens >= 1) return 0;\n const needed = 1 - this.tokens;\n return Math.ceil((needed / this.capacity) * this.intervalMs);\n }\n\n async acquire(signal?: AbortSignal): Promise<void> {\n for (;;) {\n if (signal?.aborted) throw signal.reason as Error;\n const wait = this.timeUntilNextToken();\n if (wait === 0) {\n this.tokens -= 1;\n return;\n }\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(resolve, wait);\n signal?.addEventListener(\n 'abort',\n () => {\n clearTimeout(timer);\n reject(signal.reason);\n },\n { once: true },\n );\n });\n }\n }\n\n /** Snapshot of available tokens (refills the bucket as a side effect). */\n available(): number {\n this.timeUntilNextToken();\n return this.tokens;\n }\n}\n","import { randomUUID } from 'node:crypto';\nimport {\n LoncaError,\n NetworkError,\n TimeoutError,\n parseRetryAfter,\n isRetryableIdempotentOnly,\n} from './errors.js';\nimport { retry } from './retry.js';\nimport { noopLogger, type Logger } from './logger.js';\nimport type { TokenBucketRateLimiter } from './rate-limiter.js';\n\n/**\n * The request lifecycle shared by every marketplace SDK transport: acquire a\n * rate-limit token, build the URL/headers, fire `fetch` under a composed\n * timeout signal, map non-2xx responses to `LoncaError`s, and replay through\n * {@link retry}. Only the marketplace-specific seams (URL building, headers,\n * status→error mapping, log labels) are injected via {@link RequesterConfig},\n * so the loop itself lives in one place instead of being copy-pasted — and\n * drifting — across SDKs.\n */\n\n/** The request fields the shared lifecycle reads. SDKs extend this with their own (e.g. `path`, `service`, `query`). */\nexport interface BaseRequestOptions {\n /** HTTP method. `GET` is treated as idempotent for retry purposes. */\n method: string;\n /** Request body. Serialized as JSON unless it is a `FormData` (sent as multipart). Skipped for `GET`. */\n body?: unknown;\n /** Caller abort signal, composed with the per-request timeout. */\n signal?: AbortSignal;\n /** Per-endpoint rate limiter; one token is acquired before each attempt. */\n rateLimiter?: TokenBucketRateLimiter;\n /**\n * Whether this request is safe to auto-replay on an ambiguous transient\n * failure (5xx / network drop / client timeout). `GET` is always idempotent.\n * For writes this defaults to `false`: a timed-out or 5xx write may already\n * have committed server-side, so only a `429` (provably rejected before\n * processing) is retried. Set `true` to opt a write back into full retries.\n */\n idempotent?: boolean;\n /** Extra per-request headers merged over the default header set (caller headers win). */\n headers?: Record<string, string>;\n}\n\n/** Marketplace-specific seams plus runtime dependencies for {@link createRequester}. */\nexport interface RequesterConfig<O extends BaseRequestOptions> {\n /** Underlying `fetch` (tests inject a mock). */\n fetch: typeof fetch;\n /** Structured logger. Defaults to a no-op. */\n logger?: Logger;\n /** Per-request timeout in ms. */\n timeoutMs: number;\n /** Human-readable marketplace name for error messages, e.g. `'Trendyol'`. */\n label: string;\n /** Log-event prefix, e.g. `'trendyol'` → `trendyol.request` / `trendyol.error` / `trendyol.retry`. */\n logPrefix: string;\n /** Build the absolute request URL from the SDK's options. */\n buildUrl(opts: O): string;\n /** Build the default header set; receives the generated correlation id. */\n buildHeaders(correlationId: string): Record<string, string>;\n /** Map a non-2xx response to a `LoncaError`. */\n mapHttpError(status: number, body: unknown, retryAfterMs?: number): LoncaError;\n /** Optional extra structured fields merged into request/error log lines (e.g. `{ service }`). */\n logFields?(opts: O): Record<string, unknown>;\n}\n\n/**\n * Build a `request<T>(opts)` function that runs the shared transport lifecycle\n * for a given marketplace. Each SDK calls this once (per transport instance)\n * and exposes the returned function from its own transport class.\n */\nexport function createRequester<O extends BaseRequestOptions>(\n config: RequesterConfig<O>,\n): <T>(opts: O) => Promise<T> {\n const logger = config.logger ?? noopLogger;\n\n return function request<T>(opts: O): Promise<T> {\n const safeToReplay = opts.method === 'GET' || opts.idempotent === true;\n const extra = () => config.logFields?.(opts) ?? {};\n\n return retry(\n async (attempt) => {\n if (opts.rateLimiter) await opts.rateLimiter.acquire(opts.signal);\n\n const correlationId = randomUUID();\n const url = config.buildUrl(opts);\n const headers = { ...config.buildHeaders(correlationId), ...opts.headers };\n const init: RequestInit = {\n method: opts.method,\n headers,\n signal: composeSignal(opts.signal, config.timeoutMs),\n };\n if (opts.body !== undefined && opts.method !== 'GET') {\n if (opts.body instanceof FormData) {\n // multipart: let fetch set Content-Type (it includes the boundary).\n init.body = opts.body;\n delete (headers as Record<string, string>)['Content-Type'];\n } else {\n init.body = JSON.stringify(opts.body);\n }\n }\n\n logger.debug(`${config.logPrefix}.request`, {\n method: opts.method,\n url,\n correlationId,\n attempt,\n ...extra(),\n });\n\n let response: Response;\n try {\n response = await config.fetch(url, init);\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n throw new TimeoutError({\n message: `${config.label} request timed out after ${config.timeoutMs}ms`,\n cause: err,\n });\n }\n throw new NetworkError({ message: `${config.label} network failure`, cause: err });\n }\n\n if (!response.ok) {\n const body = await safeJson(response);\n const retryAfterMs = parseRetryAfter(response.headers.get('retry-after'));\n const error = config.mapHttpError(response.status, body, retryAfterMs);\n logger.warn(`${config.logPrefix}.error`, {\n method: opts.method,\n url,\n correlationId,\n status: response.status,\n code: error.code,\n retryable: error.retryable,\n ...extra(),\n });\n throw error;\n }\n\n logger.debug(`${config.logPrefix}.response`, {\n correlationId,\n status: response.status,\n });\n\n if (response.status === 204) return undefined as T;\n return (await safeJson(response)) as T;\n },\n {\n signal: opts.signal,\n // Non-idempotent writes only retry rate-limit (429) errors; ambiguous\n // 5xx/network/timeout failures are not replayed to avoid duplicate\n // side-effects. GET (and explicitly idempotent requests) retry normally.\n isRetryable: safeToReplay ? undefined : isRetryableIdempotentOnly,\n onRetry: (err, attempt, delay) => {\n if (err instanceof LoncaError) {\n logger.warn(`${config.logPrefix}.retry`, {\n attempt,\n delayMs: delay,\n code: err.code,\n status: err.status,\n });\n }\n },\n },\n );\n };\n}\n\n/** Compose a caller signal with a per-request timeout signal. */\nexport function composeSignal(external: AbortSignal | undefined, timeoutMs: number): AbortSignal {\n const timeoutSignal = AbortSignal.timeout(timeoutMs);\n if (!external) return timeoutSignal;\n return AbortSignal.any([external, timeoutSignal]);\n}\n\n/** Read a response body as JSON, falling back to raw text (then `undefined` when empty). */\nexport async function safeJson(response: Response): Promise<unknown> {\n const text = await response.text();\n if (!text) return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -23,7 +23,30 @@ interface Money {
|
|
|
23
23
|
currency: Currency;
|
|
24
24
|
}
|
|
25
25
|
declare function money(amount: number, currency: Currency): Money;
|
|
26
|
+
/**
|
|
27
|
+
* Convert a major-unit amount (e.g. lira, dollars) to {@link Money} in integer
|
|
28
|
+
* minor units. This is the canonical lira→kuruş converter — prefer it over a
|
|
29
|
+
* hand-rolled `Math.round(x * 100)`, which scatters rounding logic and risks
|
|
30
|
+
* float drift.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { moneyFromMajor, TRY } from '@lonca/core';
|
|
35
|
+
* // A marketplace price of 199.90 ₺:
|
|
36
|
+
* moneyFromMajor(199.9, TRY); // { amount: 19990, currency: 'TRY' } — kuruş
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
26
39
|
declare function moneyFromMajor(major: number, currency: Currency, minorScale?: number): Money;
|
|
40
|
+
/**
|
|
41
|
+
* Convert {@link Money} (integer minor units) back to a major-unit number —
|
|
42
|
+
* the canonical kuruş→lira converter.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* import { moneyToMajor, TRY } from '@lonca/core';
|
|
47
|
+
* moneyToMajor({ amount: 19990, currency: TRY }); // 199.9 (lira)
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
27
50
|
declare function moneyToMajor(value: Money, minorScale?: number): number;
|
|
28
51
|
declare function isSameCurrency(a: Money, b: Money): boolean;
|
|
29
52
|
declare function addMoney(a: Money, b: Money): Money;
|
|
@@ -92,7 +115,60 @@ declare function paginateOffset<T>(fetchPage: (params: OffsetPaginationParams) =
|
|
|
92
115
|
maxPages?: number;
|
|
93
116
|
}): AsyncGenerator<T, void, undefined>;
|
|
94
117
|
|
|
118
|
+
/**
|
|
119
|
+
* The closed, marketplace-agnostic order/shipment status vocabulary that every
|
|
120
|
+
* SDK normalizes its raw statuses into.
|
|
121
|
+
*
|
|
122
|
+
* This union is intentionally **closed** (no `(string & {})` catch-all): a value
|
|
123
|
+
* a marketplace SDK can't map lands on `'unknown'` at runtime via
|
|
124
|
+
* {@link createStatusNormalizer}, surfaced by `mapped: false` — never silently
|
|
125
|
+
* coerced to a valid-looking default.
|
|
126
|
+
*/
|
|
127
|
+
type NormalizedOrderStatus = 'created' | 'picking' | 'invoiced' | 'shipped' | 'delivered' | 'cancelled' | 'returned' | 'unknown';
|
|
128
|
+
/**
|
|
129
|
+
* Result of normalizing a raw marketplace status. The raw value is always
|
|
130
|
+
* preserved, and `mapped` makes an unrecognized status visible at runtime
|
|
131
|
+
* instead of letting it disappear into a default.
|
|
132
|
+
*/
|
|
133
|
+
interface NormalizedStatusResult {
|
|
134
|
+
/** Mapped normalized status, or `'unknown'` when the raw value wasn't recognized. */
|
|
135
|
+
normalized: NormalizedOrderStatus;
|
|
136
|
+
/** The original raw status string from the marketplace, preserved verbatim. */
|
|
137
|
+
raw: string;
|
|
138
|
+
/** `false` when the raw value had no mapping (normalized fell back to `'unknown'`). */
|
|
139
|
+
mapped: boolean;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Build a status normalizer from an SDK-owned, **exhaustive** map of that
|
|
143
|
+
* marketplace's known statuses. Because the map key type is a closed union,
|
|
144
|
+
* forgetting to map a newly added known status is a compile-time error in the
|
|
145
|
+
* SDK — while an unknown value arriving on the wire stays safe at runtime
|
|
146
|
+
* (`{ normalized: 'unknown', mapped: false }`).
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```ts
|
|
150
|
+
* const statusMap = { Created: 'created', Shipped: 'shipped' } as const;
|
|
151
|
+
* const normalizeStatus = createStatusNormalizer(statusMap);
|
|
152
|
+
* normalizeStatus('Shipped'); // { normalized: 'shipped', raw: 'Shipped', mapped: true }
|
|
153
|
+
* normalizeStatus('Martian'); // { normalized: 'unknown', raw: 'Martian', mapped: false }
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
declare function createStatusNormalizer<Known extends string>(map: Record<Known, NormalizedOrderStatus>): (raw: string) => NormalizedStatusResult;
|
|
157
|
+
|
|
95
158
|
type LoncaErrorCode = 'AUTH_FAILED' | 'RATE_LIMITED' | 'VALIDATION_FAILED' | 'NOT_FOUND' | 'SERVER_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT' | 'UNKNOWN';
|
|
159
|
+
/**
|
|
160
|
+
* A single normalized, field-level error detail extracted from a marketplace's
|
|
161
|
+
* raw error body. Each SDK maps its own (inconsistent) error JSON into this
|
|
162
|
+
* shape so consumers don't have to sniff marketplace-specific payloads.
|
|
163
|
+
*/
|
|
164
|
+
interface LoncaErrorIssue {
|
|
165
|
+
/** The offending field/path, when the marketplace reports one. */
|
|
166
|
+
field?: string;
|
|
167
|
+
/** Marketplace-specific error code, when present. */
|
|
168
|
+
code?: string;
|
|
169
|
+
/** Human-readable message — always present. */
|
|
170
|
+
message: string;
|
|
171
|
+
}
|
|
96
172
|
interface LoncaErrorOptions {
|
|
97
173
|
code: LoncaErrorCode;
|
|
98
174
|
message: string;
|
|
@@ -101,6 +177,8 @@ interface LoncaErrorOptions {
|
|
|
101
177
|
status?: number;
|
|
102
178
|
retryAfterMs?: number;
|
|
103
179
|
data?: Record<string, unknown>;
|
|
180
|
+
/** Normalized, field-level error details. Defaults to `[]` when omitted. */
|
|
181
|
+
issues?: LoncaErrorIssue[];
|
|
104
182
|
}
|
|
105
183
|
/**
|
|
106
184
|
* Root of the Lonca error hierarchy.
|
|
@@ -114,6 +192,12 @@ declare class LoncaError extends Error {
|
|
|
114
192
|
readonly status?: number;
|
|
115
193
|
readonly retryAfterMs?: number;
|
|
116
194
|
readonly data?: Record<string, unknown>;
|
|
195
|
+
/**
|
|
196
|
+
* Normalized, field-level error details mapped from the marketplace's raw
|
|
197
|
+
* error body by the SDK. Always an array (never `undefined`) so callers can
|
|
198
|
+
* iterate without a presence check; empty when nothing was parseable.
|
|
199
|
+
*/
|
|
200
|
+
readonly issues: LoncaErrorIssue[];
|
|
117
201
|
constructor(opts: LoncaErrorOptions);
|
|
118
202
|
}
|
|
119
203
|
type SubclassOptions = Omit<LoncaErrorOptions, 'code' | 'retryable'>;
|
|
@@ -140,6 +224,30 @@ declare class TimeoutError extends LoncaError {
|
|
|
140
224
|
}
|
|
141
225
|
declare function isLoncaError(value: unknown): value is LoncaError;
|
|
142
226
|
declare function isRetryableError(value: unknown): boolean;
|
|
227
|
+
/**
|
|
228
|
+
* Retry predicate for **non-idempotent** requests (POST/PUT/DELETE/PATCH).
|
|
229
|
+
*
|
|
230
|
+
* Only `RateLimitError` (HTTP 429) is replayed, because a 429 means the server
|
|
231
|
+
* rejected the request *before* processing it, so a retry cannot duplicate a
|
|
232
|
+
* side-effect. Ambiguous failures — 5xx, network drops, and client-side
|
|
233
|
+
* timeouts — are deliberately NOT retried here: the write may already have
|
|
234
|
+
* taken effect server-side, and a blind replay would duplicate it (double
|
|
235
|
+
* order split, double cancel, re-pushed price/stock batch).
|
|
236
|
+
*
|
|
237
|
+
* Callers that have made a write idempotent (e.g. by sending an idempotency
|
|
238
|
+
* key) should opt back into full retries via `isRetryableError` instead.
|
|
239
|
+
*/
|
|
240
|
+
declare function isRetryableIdempotentOnly(value: unknown): boolean;
|
|
241
|
+
/**
|
|
242
|
+
* Parse a `Retry-After` header value (delta-seconds OR an HTTP-date) into
|
|
243
|
+
* milliseconds. Returns `undefined` when the header is absent, unparseable, or
|
|
244
|
+
* non-positive.
|
|
245
|
+
*
|
|
246
|
+
* A non-positive value is treated as "no hint" rather than `0`: a literal
|
|
247
|
+
* `Retry-After: 0` must not collapse exponential backoff into a zero-delay
|
|
248
|
+
* retry storm against the very endpoint that is rate-limiting the client.
|
|
249
|
+
*/
|
|
250
|
+
declare function parseRetryAfter(header: string | null): number | undefined;
|
|
143
251
|
|
|
144
252
|
interface RetryOptions {
|
|
145
253
|
/** Total number of attempts including the initial call. Defaults to 3. */
|
|
@@ -210,4 +318,66 @@ declare class TokenBucketRateLimiter {
|
|
|
210
318
|
available(): number;
|
|
211
319
|
}
|
|
212
320
|
|
|
213
|
-
|
|
321
|
+
/**
|
|
322
|
+
* The request lifecycle shared by every marketplace SDK transport: acquire a
|
|
323
|
+
* rate-limit token, build the URL/headers, fire `fetch` under a composed
|
|
324
|
+
* timeout signal, map non-2xx responses to `LoncaError`s, and replay through
|
|
325
|
+
* {@link retry}. Only the marketplace-specific seams (URL building, headers,
|
|
326
|
+
* status→error mapping, log labels) are injected via {@link RequesterConfig},
|
|
327
|
+
* so the loop itself lives in one place instead of being copy-pasted — and
|
|
328
|
+
* drifting — across SDKs.
|
|
329
|
+
*/
|
|
330
|
+
/** The request fields the shared lifecycle reads. SDKs extend this with their own (e.g. `path`, `service`, `query`). */
|
|
331
|
+
interface BaseRequestOptions {
|
|
332
|
+
/** HTTP method. `GET` is treated as idempotent for retry purposes. */
|
|
333
|
+
method: string;
|
|
334
|
+
/** Request body. Serialized as JSON unless it is a `FormData` (sent as multipart). Skipped for `GET`. */
|
|
335
|
+
body?: unknown;
|
|
336
|
+
/** Caller abort signal, composed with the per-request timeout. */
|
|
337
|
+
signal?: AbortSignal;
|
|
338
|
+
/** Per-endpoint rate limiter; one token is acquired before each attempt. */
|
|
339
|
+
rateLimiter?: TokenBucketRateLimiter;
|
|
340
|
+
/**
|
|
341
|
+
* Whether this request is safe to auto-replay on an ambiguous transient
|
|
342
|
+
* failure (5xx / network drop / client timeout). `GET` is always idempotent.
|
|
343
|
+
* For writes this defaults to `false`: a timed-out or 5xx write may already
|
|
344
|
+
* have committed server-side, so only a `429` (provably rejected before
|
|
345
|
+
* processing) is retried. Set `true` to opt a write back into full retries.
|
|
346
|
+
*/
|
|
347
|
+
idempotent?: boolean;
|
|
348
|
+
/** Extra per-request headers merged over the default header set (caller headers win). */
|
|
349
|
+
headers?: Record<string, string>;
|
|
350
|
+
}
|
|
351
|
+
/** Marketplace-specific seams plus runtime dependencies for {@link createRequester}. */
|
|
352
|
+
interface RequesterConfig<O extends BaseRequestOptions> {
|
|
353
|
+
/** Underlying `fetch` (tests inject a mock). */
|
|
354
|
+
fetch: typeof fetch;
|
|
355
|
+
/** Structured logger. Defaults to a no-op. */
|
|
356
|
+
logger?: Logger;
|
|
357
|
+
/** Per-request timeout in ms. */
|
|
358
|
+
timeoutMs: number;
|
|
359
|
+
/** Human-readable marketplace name for error messages, e.g. `'Trendyol'`. */
|
|
360
|
+
label: string;
|
|
361
|
+
/** Log-event prefix, e.g. `'trendyol'` → `trendyol.request` / `trendyol.error` / `trendyol.retry`. */
|
|
362
|
+
logPrefix: string;
|
|
363
|
+
/** Build the absolute request URL from the SDK's options. */
|
|
364
|
+
buildUrl(opts: O): string;
|
|
365
|
+
/** Build the default header set; receives the generated correlation id. */
|
|
366
|
+
buildHeaders(correlationId: string): Record<string, string>;
|
|
367
|
+
/** Map a non-2xx response to a `LoncaError`. */
|
|
368
|
+
mapHttpError(status: number, body: unknown, retryAfterMs?: number): LoncaError;
|
|
369
|
+
/** Optional extra structured fields merged into request/error log lines (e.g. `{ service }`). */
|
|
370
|
+
logFields?(opts: O): Record<string, unknown>;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Build a `request<T>(opts)` function that runs the shared transport lifecycle
|
|
374
|
+
* for a given marketplace. Each SDK calls this once (per transport instance)
|
|
375
|
+
* and exposes the returned function from its own transport class.
|
|
376
|
+
*/
|
|
377
|
+
declare function createRequester<O extends BaseRequestOptions>(config: RequesterConfig<O>): <T>(opts: O) => Promise<T>;
|
|
378
|
+
/** Compose a caller signal with a per-request timeout signal. */
|
|
379
|
+
declare function composeSignal(external: AbortSignal | undefined, timeoutMs: number): AbortSignal;
|
|
380
|
+
/** Read a response body as JSON, falling back to raw text (then `undefined` when empty). */
|
|
381
|
+
declare function safeJson(response: Response): Promise<unknown>;
|
|
382
|
+
|
|
383
|
+
export { AuthError, type BaseRequestOptions, type Currency, type CursorPage, type CursorPaginationParams, EUR, GBP, type LogContext, type LogLevel, type Logger, LoncaError, type LoncaErrorCode, type LoncaErrorIssue, type LoncaErrorOptions, type Money, NetworkError, type NormalizedOrderStatus, type NormalizedStatusResult, NotFoundError, type OffsetPage, type OffsetPaginationParams, RateLimitError, type RateLimiterOptions, type RequesterConfig, type RetryOptions, ServerError, TRY, TimeoutError, TokenBucketRateLimiter, USD, ValidationError, addMoney, composeSignal, consoleLogger, createRequester, createStatusNormalizer, isLoncaError, isRetryableError, isRetryableIdempotentOnly, isSameCurrency, isValidCurrencyCode, money, moneyFromMajor, moneyToMajor, noopLogger, paginate, paginateOffset, parseRetryAfter, retry, safeJson, subMoney };
|
package/dist/index.d.ts
CHANGED
|
@@ -23,7 +23,30 @@ interface Money {
|
|
|
23
23
|
currency: Currency;
|
|
24
24
|
}
|
|
25
25
|
declare function money(amount: number, currency: Currency): Money;
|
|
26
|
+
/**
|
|
27
|
+
* Convert a major-unit amount (e.g. lira, dollars) to {@link Money} in integer
|
|
28
|
+
* minor units. This is the canonical lira→kuruş converter — prefer it over a
|
|
29
|
+
* hand-rolled `Math.round(x * 100)`, which scatters rounding logic and risks
|
|
30
|
+
* float drift.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { moneyFromMajor, TRY } from '@lonca/core';
|
|
35
|
+
* // A marketplace price of 199.90 ₺:
|
|
36
|
+
* moneyFromMajor(199.9, TRY); // { amount: 19990, currency: 'TRY' } — kuruş
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
26
39
|
declare function moneyFromMajor(major: number, currency: Currency, minorScale?: number): Money;
|
|
40
|
+
/**
|
|
41
|
+
* Convert {@link Money} (integer minor units) back to a major-unit number —
|
|
42
|
+
* the canonical kuruş→lira converter.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* import { moneyToMajor, TRY } from '@lonca/core';
|
|
47
|
+
* moneyToMajor({ amount: 19990, currency: TRY }); // 199.9 (lira)
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
27
50
|
declare function moneyToMajor(value: Money, minorScale?: number): number;
|
|
28
51
|
declare function isSameCurrency(a: Money, b: Money): boolean;
|
|
29
52
|
declare function addMoney(a: Money, b: Money): Money;
|
|
@@ -92,7 +115,60 @@ declare function paginateOffset<T>(fetchPage: (params: OffsetPaginationParams) =
|
|
|
92
115
|
maxPages?: number;
|
|
93
116
|
}): AsyncGenerator<T, void, undefined>;
|
|
94
117
|
|
|
118
|
+
/**
|
|
119
|
+
* The closed, marketplace-agnostic order/shipment status vocabulary that every
|
|
120
|
+
* SDK normalizes its raw statuses into.
|
|
121
|
+
*
|
|
122
|
+
* This union is intentionally **closed** (no `(string & {})` catch-all): a value
|
|
123
|
+
* a marketplace SDK can't map lands on `'unknown'` at runtime via
|
|
124
|
+
* {@link createStatusNormalizer}, surfaced by `mapped: false` — never silently
|
|
125
|
+
* coerced to a valid-looking default.
|
|
126
|
+
*/
|
|
127
|
+
type NormalizedOrderStatus = 'created' | 'picking' | 'invoiced' | 'shipped' | 'delivered' | 'cancelled' | 'returned' | 'unknown';
|
|
128
|
+
/**
|
|
129
|
+
* Result of normalizing a raw marketplace status. The raw value is always
|
|
130
|
+
* preserved, and `mapped` makes an unrecognized status visible at runtime
|
|
131
|
+
* instead of letting it disappear into a default.
|
|
132
|
+
*/
|
|
133
|
+
interface NormalizedStatusResult {
|
|
134
|
+
/** Mapped normalized status, or `'unknown'` when the raw value wasn't recognized. */
|
|
135
|
+
normalized: NormalizedOrderStatus;
|
|
136
|
+
/** The original raw status string from the marketplace, preserved verbatim. */
|
|
137
|
+
raw: string;
|
|
138
|
+
/** `false` when the raw value had no mapping (normalized fell back to `'unknown'`). */
|
|
139
|
+
mapped: boolean;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Build a status normalizer from an SDK-owned, **exhaustive** map of that
|
|
143
|
+
* marketplace's known statuses. Because the map key type is a closed union,
|
|
144
|
+
* forgetting to map a newly added known status is a compile-time error in the
|
|
145
|
+
* SDK — while an unknown value arriving on the wire stays safe at runtime
|
|
146
|
+
* (`{ normalized: 'unknown', mapped: false }`).
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```ts
|
|
150
|
+
* const statusMap = { Created: 'created', Shipped: 'shipped' } as const;
|
|
151
|
+
* const normalizeStatus = createStatusNormalizer(statusMap);
|
|
152
|
+
* normalizeStatus('Shipped'); // { normalized: 'shipped', raw: 'Shipped', mapped: true }
|
|
153
|
+
* normalizeStatus('Martian'); // { normalized: 'unknown', raw: 'Martian', mapped: false }
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
declare function createStatusNormalizer<Known extends string>(map: Record<Known, NormalizedOrderStatus>): (raw: string) => NormalizedStatusResult;
|
|
157
|
+
|
|
95
158
|
type LoncaErrorCode = 'AUTH_FAILED' | 'RATE_LIMITED' | 'VALIDATION_FAILED' | 'NOT_FOUND' | 'SERVER_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT' | 'UNKNOWN';
|
|
159
|
+
/**
|
|
160
|
+
* A single normalized, field-level error detail extracted from a marketplace's
|
|
161
|
+
* raw error body. Each SDK maps its own (inconsistent) error JSON into this
|
|
162
|
+
* shape so consumers don't have to sniff marketplace-specific payloads.
|
|
163
|
+
*/
|
|
164
|
+
interface LoncaErrorIssue {
|
|
165
|
+
/** The offending field/path, when the marketplace reports one. */
|
|
166
|
+
field?: string;
|
|
167
|
+
/** Marketplace-specific error code, when present. */
|
|
168
|
+
code?: string;
|
|
169
|
+
/** Human-readable message — always present. */
|
|
170
|
+
message: string;
|
|
171
|
+
}
|
|
96
172
|
interface LoncaErrorOptions {
|
|
97
173
|
code: LoncaErrorCode;
|
|
98
174
|
message: string;
|
|
@@ -101,6 +177,8 @@ interface LoncaErrorOptions {
|
|
|
101
177
|
status?: number;
|
|
102
178
|
retryAfterMs?: number;
|
|
103
179
|
data?: Record<string, unknown>;
|
|
180
|
+
/** Normalized, field-level error details. Defaults to `[]` when omitted. */
|
|
181
|
+
issues?: LoncaErrorIssue[];
|
|
104
182
|
}
|
|
105
183
|
/**
|
|
106
184
|
* Root of the Lonca error hierarchy.
|
|
@@ -114,6 +192,12 @@ declare class LoncaError extends Error {
|
|
|
114
192
|
readonly status?: number;
|
|
115
193
|
readonly retryAfterMs?: number;
|
|
116
194
|
readonly data?: Record<string, unknown>;
|
|
195
|
+
/**
|
|
196
|
+
* Normalized, field-level error details mapped from the marketplace's raw
|
|
197
|
+
* error body by the SDK. Always an array (never `undefined`) so callers can
|
|
198
|
+
* iterate without a presence check; empty when nothing was parseable.
|
|
199
|
+
*/
|
|
200
|
+
readonly issues: LoncaErrorIssue[];
|
|
117
201
|
constructor(opts: LoncaErrorOptions);
|
|
118
202
|
}
|
|
119
203
|
type SubclassOptions = Omit<LoncaErrorOptions, 'code' | 'retryable'>;
|
|
@@ -140,6 +224,30 @@ declare class TimeoutError extends LoncaError {
|
|
|
140
224
|
}
|
|
141
225
|
declare function isLoncaError(value: unknown): value is LoncaError;
|
|
142
226
|
declare function isRetryableError(value: unknown): boolean;
|
|
227
|
+
/**
|
|
228
|
+
* Retry predicate for **non-idempotent** requests (POST/PUT/DELETE/PATCH).
|
|
229
|
+
*
|
|
230
|
+
* Only `RateLimitError` (HTTP 429) is replayed, because a 429 means the server
|
|
231
|
+
* rejected the request *before* processing it, so a retry cannot duplicate a
|
|
232
|
+
* side-effect. Ambiguous failures — 5xx, network drops, and client-side
|
|
233
|
+
* timeouts — are deliberately NOT retried here: the write may already have
|
|
234
|
+
* taken effect server-side, and a blind replay would duplicate it (double
|
|
235
|
+
* order split, double cancel, re-pushed price/stock batch).
|
|
236
|
+
*
|
|
237
|
+
* Callers that have made a write idempotent (e.g. by sending an idempotency
|
|
238
|
+
* key) should opt back into full retries via `isRetryableError` instead.
|
|
239
|
+
*/
|
|
240
|
+
declare function isRetryableIdempotentOnly(value: unknown): boolean;
|
|
241
|
+
/**
|
|
242
|
+
* Parse a `Retry-After` header value (delta-seconds OR an HTTP-date) into
|
|
243
|
+
* milliseconds. Returns `undefined` when the header is absent, unparseable, or
|
|
244
|
+
* non-positive.
|
|
245
|
+
*
|
|
246
|
+
* A non-positive value is treated as "no hint" rather than `0`: a literal
|
|
247
|
+
* `Retry-After: 0` must not collapse exponential backoff into a zero-delay
|
|
248
|
+
* retry storm against the very endpoint that is rate-limiting the client.
|
|
249
|
+
*/
|
|
250
|
+
declare function parseRetryAfter(header: string | null): number | undefined;
|
|
143
251
|
|
|
144
252
|
interface RetryOptions {
|
|
145
253
|
/** Total number of attempts including the initial call. Defaults to 3. */
|
|
@@ -210,4 +318,66 @@ declare class TokenBucketRateLimiter {
|
|
|
210
318
|
available(): number;
|
|
211
319
|
}
|
|
212
320
|
|
|
213
|
-
|
|
321
|
+
/**
|
|
322
|
+
* The request lifecycle shared by every marketplace SDK transport: acquire a
|
|
323
|
+
* rate-limit token, build the URL/headers, fire `fetch` under a composed
|
|
324
|
+
* timeout signal, map non-2xx responses to `LoncaError`s, and replay through
|
|
325
|
+
* {@link retry}. Only the marketplace-specific seams (URL building, headers,
|
|
326
|
+
* status→error mapping, log labels) are injected via {@link RequesterConfig},
|
|
327
|
+
* so the loop itself lives in one place instead of being copy-pasted — and
|
|
328
|
+
* drifting — across SDKs.
|
|
329
|
+
*/
|
|
330
|
+
/** The request fields the shared lifecycle reads. SDKs extend this with their own (e.g. `path`, `service`, `query`). */
|
|
331
|
+
interface BaseRequestOptions {
|
|
332
|
+
/** HTTP method. `GET` is treated as idempotent for retry purposes. */
|
|
333
|
+
method: string;
|
|
334
|
+
/** Request body. Serialized as JSON unless it is a `FormData` (sent as multipart). Skipped for `GET`. */
|
|
335
|
+
body?: unknown;
|
|
336
|
+
/** Caller abort signal, composed with the per-request timeout. */
|
|
337
|
+
signal?: AbortSignal;
|
|
338
|
+
/** Per-endpoint rate limiter; one token is acquired before each attempt. */
|
|
339
|
+
rateLimiter?: TokenBucketRateLimiter;
|
|
340
|
+
/**
|
|
341
|
+
* Whether this request is safe to auto-replay on an ambiguous transient
|
|
342
|
+
* failure (5xx / network drop / client timeout). `GET` is always idempotent.
|
|
343
|
+
* For writes this defaults to `false`: a timed-out or 5xx write may already
|
|
344
|
+
* have committed server-side, so only a `429` (provably rejected before
|
|
345
|
+
* processing) is retried. Set `true` to opt a write back into full retries.
|
|
346
|
+
*/
|
|
347
|
+
idempotent?: boolean;
|
|
348
|
+
/** Extra per-request headers merged over the default header set (caller headers win). */
|
|
349
|
+
headers?: Record<string, string>;
|
|
350
|
+
}
|
|
351
|
+
/** Marketplace-specific seams plus runtime dependencies for {@link createRequester}. */
|
|
352
|
+
interface RequesterConfig<O extends BaseRequestOptions> {
|
|
353
|
+
/** Underlying `fetch` (tests inject a mock). */
|
|
354
|
+
fetch: typeof fetch;
|
|
355
|
+
/** Structured logger. Defaults to a no-op. */
|
|
356
|
+
logger?: Logger;
|
|
357
|
+
/** Per-request timeout in ms. */
|
|
358
|
+
timeoutMs: number;
|
|
359
|
+
/** Human-readable marketplace name for error messages, e.g. `'Trendyol'`. */
|
|
360
|
+
label: string;
|
|
361
|
+
/** Log-event prefix, e.g. `'trendyol'` → `trendyol.request` / `trendyol.error` / `trendyol.retry`. */
|
|
362
|
+
logPrefix: string;
|
|
363
|
+
/** Build the absolute request URL from the SDK's options. */
|
|
364
|
+
buildUrl(opts: O): string;
|
|
365
|
+
/** Build the default header set; receives the generated correlation id. */
|
|
366
|
+
buildHeaders(correlationId: string): Record<string, string>;
|
|
367
|
+
/** Map a non-2xx response to a `LoncaError`. */
|
|
368
|
+
mapHttpError(status: number, body: unknown, retryAfterMs?: number): LoncaError;
|
|
369
|
+
/** Optional extra structured fields merged into request/error log lines (e.g. `{ service }`). */
|
|
370
|
+
logFields?(opts: O): Record<string, unknown>;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Build a `request<T>(opts)` function that runs the shared transport lifecycle
|
|
374
|
+
* for a given marketplace. Each SDK calls this once (per transport instance)
|
|
375
|
+
* and exposes the returned function from its own transport class.
|
|
376
|
+
*/
|
|
377
|
+
declare function createRequester<O extends BaseRequestOptions>(config: RequesterConfig<O>): <T>(opts: O) => Promise<T>;
|
|
378
|
+
/** Compose a caller signal with a per-request timeout signal. */
|
|
379
|
+
declare function composeSignal(external: AbortSignal | undefined, timeoutMs: number): AbortSignal;
|
|
380
|
+
/** Read a response body as JSON, falling back to raw text (then `undefined` when empty). */
|
|
381
|
+
declare function safeJson(response: Response): Promise<unknown>;
|
|
382
|
+
|
|
383
|
+
export { AuthError, type BaseRequestOptions, type Currency, type CursorPage, type CursorPaginationParams, EUR, GBP, type LogContext, type LogLevel, type Logger, LoncaError, type LoncaErrorCode, type LoncaErrorIssue, type LoncaErrorOptions, type Money, NetworkError, type NormalizedOrderStatus, type NormalizedStatusResult, NotFoundError, type OffsetPage, type OffsetPaginationParams, RateLimitError, type RateLimiterOptions, type RequesterConfig, type RetryOptions, ServerError, TRY, TimeoutError, TokenBucketRateLimiter, USD, ValidationError, addMoney, composeSignal, consoleLogger, createRequester, createStatusNormalizer, isLoncaError, isRetryableError, isRetryableIdempotentOnly, isSameCurrency, isValidCurrencyCode, money, moneyFromMajor, moneyToMajor, noopLogger, paginate, paginateOffset, parseRetryAfter, retry, safeJson, subMoney };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
|
|
1
3
|
// src/currency.ts
|
|
2
4
|
var TRY = "TRY";
|
|
3
5
|
var USD = "USD";
|
|
@@ -69,6 +71,15 @@ async function* paginateOffset(fetchPage, params = {}) {
|
|
|
69
71
|
}
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
// src/order-status.ts
|
|
75
|
+
function createStatusNormalizer(map) {
|
|
76
|
+
const lookup = map;
|
|
77
|
+
return (raw) => {
|
|
78
|
+
const normalized = lookup[raw];
|
|
79
|
+
return normalized !== void 0 ? { normalized, raw, mapped: true } : { normalized: "unknown", raw, mapped: false };
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
72
83
|
// src/errors.ts
|
|
73
84
|
var LoncaError = class extends Error {
|
|
74
85
|
code;
|
|
@@ -76,6 +87,12 @@ var LoncaError = class extends Error {
|
|
|
76
87
|
status;
|
|
77
88
|
retryAfterMs;
|
|
78
89
|
data;
|
|
90
|
+
/**
|
|
91
|
+
* Normalized, field-level error details mapped from the marketplace's raw
|
|
92
|
+
* error body by the SDK. Always an array (never `undefined`) so callers can
|
|
93
|
+
* iterate without a presence check; empty when nothing was parseable.
|
|
94
|
+
*/
|
|
95
|
+
issues;
|
|
79
96
|
constructor(opts) {
|
|
80
97
|
super(opts.message, opts.cause !== void 0 ? { cause: opts.cause } : void 0);
|
|
81
98
|
this.name = "LoncaError";
|
|
@@ -84,6 +101,7 @@ var LoncaError = class extends Error {
|
|
|
84
101
|
this.status = opts.status;
|
|
85
102
|
this.retryAfterMs = opts.retryAfterMs;
|
|
86
103
|
this.data = opts.data;
|
|
104
|
+
this.issues = opts.issues ?? [];
|
|
87
105
|
}
|
|
88
106
|
};
|
|
89
107
|
var AuthError = class extends LoncaError {
|
|
@@ -134,6 +152,20 @@ function isLoncaError(value) {
|
|
|
134
152
|
function isRetryableError(value) {
|
|
135
153
|
return isLoncaError(value) && value.retryable;
|
|
136
154
|
}
|
|
155
|
+
function isRetryableIdempotentOnly(value) {
|
|
156
|
+
return isLoncaError(value) && value.retryable && value.code === "RATE_LIMITED";
|
|
157
|
+
}
|
|
158
|
+
function parseRetryAfter(header) {
|
|
159
|
+
if (!header) return void 0;
|
|
160
|
+
const seconds = Number(header);
|
|
161
|
+
if (!Number.isNaN(seconds)) return seconds > 0 ? seconds * 1e3 : void 0;
|
|
162
|
+
const epoch = Date.parse(header);
|
|
163
|
+
if (!Number.isNaN(epoch)) {
|
|
164
|
+
const delta = epoch - Date.now();
|
|
165
|
+
return delta > 0 ? delta : void 0;
|
|
166
|
+
}
|
|
167
|
+
return void 0;
|
|
168
|
+
}
|
|
137
169
|
|
|
138
170
|
// src/retry.ts
|
|
139
171
|
var DEFAULTS = {
|
|
@@ -176,7 +208,7 @@ async function retry(fn, options = {}) {
|
|
|
176
208
|
}
|
|
177
209
|
const retryAfterMs = isLoncaError(err) ? err.retryAfterMs : void 0;
|
|
178
210
|
const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
|
|
179
|
-
const base = retryAfterMs
|
|
211
|
+
const base = retryAfterMs && retryAfterMs > 0 ? retryAfterMs : exponential;
|
|
180
212
|
const jitterMs = jitter ? Math.random() * base * 0.5 : 0;
|
|
181
213
|
const delay = Math.min(maxDelayMs, base + jitterMs);
|
|
182
214
|
options.onRetry?.(err, attempt, delay);
|
|
@@ -281,7 +313,106 @@ var TokenBucketRateLimiter = class {
|
|
|
281
313
|
return this.tokens;
|
|
282
314
|
}
|
|
283
315
|
};
|
|
316
|
+
function createRequester(config) {
|
|
317
|
+
const logger = config.logger ?? noopLogger;
|
|
318
|
+
return function request(opts) {
|
|
319
|
+
const safeToReplay = opts.method === "GET" || opts.idempotent === true;
|
|
320
|
+
const extra = () => config.logFields?.(opts) ?? {};
|
|
321
|
+
return retry(
|
|
322
|
+
async (attempt) => {
|
|
323
|
+
if (opts.rateLimiter) await opts.rateLimiter.acquire(opts.signal);
|
|
324
|
+
const correlationId = randomUUID();
|
|
325
|
+
const url = config.buildUrl(opts);
|
|
326
|
+
const headers = { ...config.buildHeaders(correlationId), ...opts.headers };
|
|
327
|
+
const init = {
|
|
328
|
+
method: opts.method,
|
|
329
|
+
headers,
|
|
330
|
+
signal: composeSignal(opts.signal, config.timeoutMs)
|
|
331
|
+
};
|
|
332
|
+
if (opts.body !== void 0 && opts.method !== "GET") {
|
|
333
|
+
if (opts.body instanceof FormData) {
|
|
334
|
+
init.body = opts.body;
|
|
335
|
+
delete headers["Content-Type"];
|
|
336
|
+
} else {
|
|
337
|
+
init.body = JSON.stringify(opts.body);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
logger.debug(`${config.logPrefix}.request`, {
|
|
341
|
+
method: opts.method,
|
|
342
|
+
url,
|
|
343
|
+
correlationId,
|
|
344
|
+
attempt,
|
|
345
|
+
...extra()
|
|
346
|
+
});
|
|
347
|
+
let response;
|
|
348
|
+
try {
|
|
349
|
+
response = await config.fetch(url, init);
|
|
350
|
+
} catch (err) {
|
|
351
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
352
|
+
throw new TimeoutError({
|
|
353
|
+
message: `${config.label} request timed out after ${config.timeoutMs}ms`,
|
|
354
|
+
cause: err
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
throw new NetworkError({ message: `${config.label} network failure`, cause: err });
|
|
358
|
+
}
|
|
359
|
+
if (!response.ok) {
|
|
360
|
+
const body = await safeJson(response);
|
|
361
|
+
const retryAfterMs = parseRetryAfter(response.headers.get("retry-after"));
|
|
362
|
+
const error = config.mapHttpError(response.status, body, retryAfterMs);
|
|
363
|
+
logger.warn(`${config.logPrefix}.error`, {
|
|
364
|
+
method: opts.method,
|
|
365
|
+
url,
|
|
366
|
+
correlationId,
|
|
367
|
+
status: response.status,
|
|
368
|
+
code: error.code,
|
|
369
|
+
retryable: error.retryable,
|
|
370
|
+
...extra()
|
|
371
|
+
});
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
logger.debug(`${config.logPrefix}.response`, {
|
|
375
|
+
correlationId,
|
|
376
|
+
status: response.status
|
|
377
|
+
});
|
|
378
|
+
if (response.status === 204) return void 0;
|
|
379
|
+
return await safeJson(response);
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
signal: opts.signal,
|
|
383
|
+
// Non-idempotent writes only retry rate-limit (429) errors; ambiguous
|
|
384
|
+
// 5xx/network/timeout failures are not replayed to avoid duplicate
|
|
385
|
+
// side-effects. GET (and explicitly idempotent requests) retry normally.
|
|
386
|
+
isRetryable: safeToReplay ? void 0 : isRetryableIdempotentOnly,
|
|
387
|
+
onRetry: (err, attempt, delay) => {
|
|
388
|
+
if (err instanceof LoncaError) {
|
|
389
|
+
logger.warn(`${config.logPrefix}.retry`, {
|
|
390
|
+
attempt,
|
|
391
|
+
delayMs: delay,
|
|
392
|
+
code: err.code,
|
|
393
|
+
status: err.status
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
);
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function composeSignal(external, timeoutMs) {
|
|
402
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
403
|
+
if (!external) return timeoutSignal;
|
|
404
|
+
return AbortSignal.any([external, timeoutSignal]);
|
|
405
|
+
}
|
|
406
|
+
async function safeJson(response) {
|
|
407
|
+
const text = await response.text();
|
|
408
|
+
if (!text) return void 0;
|
|
409
|
+
try {
|
|
410
|
+
return JSON.parse(text);
|
|
411
|
+
} catch {
|
|
412
|
+
return text;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
284
415
|
|
|
285
|
-
export { AuthError, EUR, GBP, LoncaError, NetworkError, NotFoundError, RateLimitError, ServerError, TRY, TimeoutError, TokenBucketRateLimiter, USD, ValidationError, addMoney, consoleLogger, isLoncaError, isRetryableError, isSameCurrency, isValidCurrencyCode, money, moneyFromMajor, moneyToMajor, noopLogger, paginate, paginateOffset, retry, subMoney };
|
|
416
|
+
export { AuthError, EUR, GBP, LoncaError, NetworkError, NotFoundError, RateLimitError, ServerError, TRY, TimeoutError, TokenBucketRateLimiter, USD, ValidationError, addMoney, composeSignal, consoleLogger, createRequester, createStatusNormalizer, isLoncaError, isRetryableError, isRetryableIdempotentOnly, isSameCurrency, isValidCurrencyCode, money, moneyFromMajor, moneyToMajor, noopLogger, paginate, paginateOffset, parseRetryAfter, retry, safeJson, subMoney };
|
|
286
417
|
//# sourceMappingURL=index.js.map
|
|
287
418
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/currency.ts","../src/money.ts","../src/pagination.ts","../src/errors.ts","../src/retry.ts","../src/logger.ts","../src/rate-limiter.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * ISO 4217 three-letter currency code.\n *\n * Typed as a string for flexibility — Trendyol and similar marketplaces may\n * expose currencies beyond the common set, so we don't lock down to a union.\n * Use `isValidCurrencyCode` for runtime validation.\n */\nexport type Currency = string;\n\nexport const TRY = 'TRY';\nexport const USD = 'USD';\nexport const EUR = 'EUR';\nexport const GBP = 'GBP';\n\nconst ISO_4217_RE = /^[A-Z]{3}$/;\n\nexport function isValidCurrencyCode(code: string): boolean {\n return ISO_4217_RE.test(code);\n}\n","import type { Currency } from './currency.js';\n\n/**\n * A monetary amount in integer minor units (e.g., kuruş for TRY, cents for USD).\n *\n * Using integers avoids floating-point surprises. Convert to/from major units\n * with `moneyFromMajor` and `moneyToMajor`.\n */\nexport interface Money {\n amount: number;\n currency: Currency;\n}\n\nexport function money(amount: number, currency: Currency): Money {\n if (!Number.isInteger(amount)) {\n throw new TypeError(`Money amount must be an integer minor unit, got ${amount}`);\n }\n return { amount, currency };\n}\n\nexport function moneyFromMajor(major: number, currency: Currency, minorScale = 2): Money {\n const factor = 10 ** minorScale;\n const amount = Math.round(major * factor);\n return money(amount, currency);\n}\n\nexport function moneyToMajor(value: Money, minorScale = 2): number {\n return value.amount / 10 ** minorScale;\n}\n\nexport function isSameCurrency(a: Money, b: Money): boolean {\n return a.currency === b.currency;\n}\n\nexport function addMoney(a: Money, b: Money): Money {\n if (!isSameCurrency(a, b)) {\n throw new TypeError(`Cannot add Money of different currencies: ${a.currency} vs ${b.currency}`);\n }\n return money(a.amount + b.amount, a.currency);\n}\n\nexport function subMoney(a: Money, b: Money): Money {\n if (!isSameCurrency(a, b)) {\n throw new TypeError(\n `Cannot subtract Money of different currencies: ${a.currency} vs ${b.currency}`,\n );\n }\n return money(a.amount - b.amount, a.currency);\n}\n","export interface CursorPaginationParams {\n /** Opaque cursor from previous response's `nextCursor`. Omit for the first page. */\n cursor?: string;\n /** Maximum items in the page. Marketplace SDKs may clamp this to their own bounds. */\n limit?: number;\n}\n\nexport interface CursorPage<T> {\n items: T[];\n /** Opaque cursor for the next page. Absent when there are no more pages. */\n nextCursor?: string;\n}\n\n/**\n * Drive a cursor-paginated endpoint as an async iterator.\n *\n * @example\n * for await (const order of paginate((p) => client.orders.list(p))) {\n * console.log(order.id);\n * }\n */\nexport async function* paginate<T>(\n fetchPage: (params: CursorPaginationParams) => Promise<CursorPage<T>>,\n params: Omit<CursorPaginationParams, 'cursor'> = {},\n): AsyncGenerator<T, void, undefined> {\n let cursor: string | undefined;\n do {\n const page = await fetchPage({ ...params, cursor });\n for (const item of page.items) {\n yield item;\n }\n cursor = page.nextCursor;\n } while (cursor);\n}\n\n// ─── Offset pagination ─────────────────────────────────────────────────────\n\nexport interface OffsetPaginationParams {\n /** Zero-based offset. Default depends on the SDK; usually `0`. */\n offset?: number;\n /** Maximum items in the page. */\n limit?: number;\n}\n\n/**\n * Offset-based pagination envelope returned by marketplaces that expose\n * `{ totalCount, limit, offset, pageCount, items[] }` instead of opaque\n * cursors. Hepsiburada's OMS uses this shape; some legacy Trendyol\n * endpoints do too. Distinct from `CursorPage<T>` so callers can tell at\n * the type level which pagination model an endpoint uses.\n */\nexport interface OffsetPage<T> {\n /** Total number of items matching the query across all pages. */\n totalCount: number;\n /** Echo of the request `limit`. */\n limit: number;\n /** Echo of the request `offset`. */\n offset: number;\n /** Total number of pages at the requested `limit`. */\n pageCount: number;\n /** Page rows. */\n items: T[];\n}\n\n/**\n * Drive an offset-paginated endpoint as an async iterator. Stops when the\n * server's `pageCount` is exhausted (or after the configured `maxPages`\n * to bound runaway iteration). Default page size is whatever the\n * underlying endpoint clamps to — pass `limit` explicitly for control.\n *\n * @example\n * for await (const order of paginateOffset((p) =>\n * client.orders.list(p),\n * )) {\n * console.log(order.orderNumber);\n * }\n */\nexport async function* paginateOffset<T>(\n fetchPage: (params: OffsetPaginationParams) => Promise<OffsetPage<T>>,\n params: { limit?: number; maxPages?: number } = {},\n): AsyncGenerator<T, void, undefined> {\n const limit = params.limit;\n const maxPages = params.maxPages ?? Infinity;\n let offset = 0;\n let pages = 0;\n while (pages < maxPages) {\n const page = await fetchPage({ offset, limit });\n for (const item of page.items) {\n yield item;\n }\n pages += 1;\n if (page.items.length === 0) return;\n if (page.pageCount > 0 && pages >= page.pageCount) return;\n // Step by the server-echoed limit (when present) so requests stay aligned with how\n // the server batches; fall back to the caller's `limit` or the observed batch size.\n offset += page.limit || limit || page.items.length;\n }\n}\n","export type LoncaErrorCode =\n | 'AUTH_FAILED'\n | 'RATE_LIMITED'\n | 'VALIDATION_FAILED'\n | 'NOT_FOUND'\n | 'SERVER_ERROR'\n | 'NETWORK_ERROR'\n | 'TIMEOUT'\n | 'UNKNOWN';\n\nexport interface LoncaErrorOptions {\n code: LoncaErrorCode;\n message: string;\n cause?: unknown;\n retryable?: boolean;\n status?: number;\n retryAfterMs?: number;\n data?: Record<string, unknown>;\n}\n\n/**\n * Root of the Lonca error hierarchy.\n *\n * Carries a tagged `code` and a `retryable` boolean so retry helpers\n * can decide what to do without sniffing message strings.\n */\nexport class LoncaError extends Error {\n readonly code: LoncaErrorCode;\n readonly retryable: boolean;\n readonly status?: number;\n readonly retryAfterMs?: number;\n readonly data?: Record<string, unknown>;\n\n constructor(opts: LoncaErrorOptions) {\n super(opts.message, opts.cause !== undefined ? { cause: opts.cause } : undefined);\n this.name = 'LoncaError';\n this.code = opts.code;\n this.retryable = opts.retryable ?? false;\n this.status = opts.status;\n this.retryAfterMs = opts.retryAfterMs;\n this.data = opts.data;\n }\n}\n\ntype SubclassOptions = Omit<LoncaErrorOptions, 'code' | 'retryable'>;\n\nexport class AuthError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'AUTH_FAILED', retryable: false });\n this.name = 'AuthError';\n }\n}\n\nexport class RateLimitError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'RATE_LIMITED', retryable: true });\n this.name = 'RateLimitError';\n }\n}\n\nexport class ValidationError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'VALIDATION_FAILED', retryable: false });\n this.name = 'ValidationError';\n }\n}\n\nexport class NotFoundError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'NOT_FOUND', retryable: false });\n this.name = 'NotFoundError';\n }\n}\n\nexport class ServerError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'SERVER_ERROR', retryable: true });\n this.name = 'ServerError';\n }\n}\n\nexport class NetworkError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'NETWORK_ERROR', retryable: true });\n this.name = 'NetworkError';\n }\n}\n\nexport class TimeoutError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'TIMEOUT', retryable: true });\n this.name = 'TimeoutError';\n }\n}\n\nexport function isLoncaError(value: unknown): value is LoncaError {\n return value instanceof LoncaError;\n}\n\nexport function isRetryableError(value: unknown): boolean {\n return isLoncaError(value) && value.retryable;\n}\n","import { isLoncaError, isRetryableError } from './errors.js';\n\nexport interface RetryOptions {\n /** Total number of attempts including the initial call. Defaults to 3. */\n maxAttempts?: number;\n /** Initial delay before the second attempt, in ms. Defaults to 200. */\n baseDelayMs?: number;\n /** Upper bound on delay between any two attempts, in ms. Defaults to 10_000. */\n maxDelayMs?: number;\n /** Add up to 50% random jitter on each delay. Defaults to true. */\n jitter?: boolean;\n /** Override the default predicate. Default treats `retryable` LoncaErrors as retryable. */\n isRetryable?: (err: unknown) => boolean;\n /** Called before each retry sleep. Useful for structured logging. */\n onRetry?: (err: unknown, attempt: number, nextDelayMs: number) => void;\n /** Abort all pending sleeps and bubble the signal's reason. */\n signal?: AbortSignal;\n}\n\nconst DEFAULTS = {\n maxAttempts: 3,\n baseDelayMs: 200,\n maxDelayMs: 10_000,\n jitter: true,\n} as const;\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(signal.reason);\n return;\n }\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n 'abort',\n () => {\n clearTimeout(timer);\n reject(signal.reason);\n },\n { once: true },\n );\n });\n}\n\nexport async function retry<T>(\n fn: (attempt: number) => Promise<T>,\n options: RetryOptions = {},\n): Promise<T> {\n const maxAttempts = options.maxAttempts ?? DEFAULTS.maxAttempts;\n const baseDelayMs = options.baseDelayMs ?? DEFAULTS.baseDelayMs;\n const maxDelayMs = options.maxDelayMs ?? DEFAULTS.maxDelayMs;\n const jitter = options.jitter ?? DEFAULTS.jitter;\n const isRetryable = options.isRetryable ?? isRetryableError;\n\n let lastErr: unknown;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n return await fn(attempt);\n } catch (err) {\n lastErr = err;\n if (attempt === maxAttempts || !isRetryable(err)) {\n throw err;\n }\n\n const retryAfterMs = isLoncaError(err) ? err.retryAfterMs : undefined;\n const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));\n const base = retryAfterMs ?? exponential;\n const jitterMs = jitter ? Math.random() * base * 0.5 : 0;\n const delay = Math.min(maxDelayMs, base + jitterMs);\n\n options.onRetry?.(err, attempt, delay);\n await sleep(delay, options.signal);\n }\n }\n throw lastErr;\n}\n","export type LogLevel = 'debug' | 'info' | 'warn' | 'error';\n\nexport type LogContext = Record<string, unknown>;\n\nexport interface Logger {\n debug(message: string, context?: LogContext): void;\n info(message: string, context?: LogContext): void;\n warn(message: string, context?: LogContext): void;\n error(message: string, context?: LogContext): void;\n /** Create a child logger that merges `bindings` into every log entry. */\n child(bindings: LogContext): Logger;\n}\n\nexport const noopLogger: Logger = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n child: () => noopLogger,\n};\n\n/**\n * Minimal JSON-line logger to stdout/stderr. Suitable for development and\n * containerized environments. For production prefer wiring a real logger\n * (pino, winston) by implementing the `Logger` interface.\n */\nexport function consoleLogger(bindings: LogContext = {}): Logger {\n const emit = (level: LogLevel, message: string, context?: LogContext): void => {\n const entry = {\n level,\n message,\n timestamp: new Date().toISOString(),\n ...bindings,\n ...context,\n };\n const line = JSON.stringify(entry);\n if (level === 'error' || level === 'warn') {\n console.error(line);\n } else {\n console.log(line);\n }\n };\n return {\n debug: (m, c) => emit('debug', m, c),\n info: (m, c) => emit('info', m, c),\n warn: (m, c) => emit('warn', m, c),\n error: (m, c) => emit('error', m, c),\n child: (b) => consoleLogger({ ...bindings, ...b }),\n };\n}\n","export interface RateLimiterOptions {\n /** Maximum number of tokens the bucket holds. Each `acquire()` consumes 1. */\n capacity: number;\n /** Time window in milliseconds over which `capacity` tokens are refilled. */\n intervalMs: number;\n}\n\n/**\n * Token-bucket rate limiter. `acquire()` returns immediately when a token\n * is available, otherwise it sleeps until enough tokens have been refilled.\n *\n * @example\n * const limiter = new TokenBucketRateLimiter({ capacity: 50, intervalMs: 60_000 });\n * for (const item of items) {\n * await limiter.acquire();\n * await fetchSomething(item);\n * }\n */\nexport class TokenBucketRateLimiter {\n private tokens: number;\n private lastRefillAt: number;\n private readonly capacity: number;\n private readonly intervalMs: number;\n\n constructor(options: RateLimiterOptions) {\n if (options.capacity <= 0 || options.intervalMs <= 0) {\n throw new RangeError('capacity and intervalMs must be > 0');\n }\n this.capacity = options.capacity;\n this.intervalMs = options.intervalMs;\n this.tokens = options.capacity;\n this.lastRefillAt = Date.now();\n }\n\n /**\n * Refill tokens based on elapsed time, then return ms to wait until the next\n * token is available (0 if one is available right now).\n */\n private timeUntilNextToken(): number {\n const now = Date.now();\n const elapsed = now - this.lastRefillAt;\n if (elapsed > 0) {\n const refill = (elapsed / this.intervalMs) * this.capacity;\n this.tokens = Math.min(this.capacity, this.tokens + refill);\n this.lastRefillAt = now;\n }\n if (this.tokens >= 1) return 0;\n const needed = 1 - this.tokens;\n return Math.ceil((needed / this.capacity) * this.intervalMs);\n }\n\n async acquire(signal?: AbortSignal): Promise<void> {\n for (;;) {\n if (signal?.aborted) throw signal.reason as Error;\n const wait = this.timeUntilNextToken();\n if (wait === 0) {\n this.tokens -= 1;\n return;\n }\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(resolve, wait);\n signal?.addEventListener(\n 'abort',\n () => {\n clearTimeout(timer);\n reject(signal.reason);\n },\n { once: true },\n );\n });\n }\n }\n\n /** Snapshot of available tokens (refills the bucket as a side effect). */\n available(): number {\n this.timeUntilNextToken();\n return this.tokens;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/currency.ts","../src/money.ts","../src/pagination.ts","../src/order-status.ts","../src/errors.ts","../src/retry.ts","../src/logger.ts","../src/rate-limiter.ts","../src/transport.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * ISO 4217 three-letter currency code.\n *\n * Typed as a string for flexibility — Trendyol and similar marketplaces may\n * expose currencies beyond the common set, so we don't lock down to a union.\n * Use `isValidCurrencyCode` for runtime validation.\n */\nexport type Currency = string;\n\nexport const TRY = 'TRY';\nexport const USD = 'USD';\nexport const EUR = 'EUR';\nexport const GBP = 'GBP';\n\nconst ISO_4217_RE = /^[A-Z]{3}$/;\n\nexport function isValidCurrencyCode(code: string): boolean {\n return ISO_4217_RE.test(code);\n}\n","import type { Currency } from './currency.js';\n\n/**\n * A monetary amount in integer minor units (e.g., kuruş for TRY, cents for USD).\n *\n * Using integers avoids floating-point surprises. Convert to/from major units\n * with `moneyFromMajor` and `moneyToMajor`.\n */\nexport interface Money {\n amount: number;\n currency: Currency;\n}\n\nexport function money(amount: number, currency: Currency): Money {\n if (!Number.isInteger(amount)) {\n throw new TypeError(`Money amount must be an integer minor unit, got ${amount}`);\n }\n return { amount, currency };\n}\n\n/**\n * Convert a major-unit amount (e.g. lira, dollars) to {@link Money} in integer\n * minor units. This is the canonical lira→kuruş converter — prefer it over a\n * hand-rolled `Math.round(x * 100)`, which scatters rounding logic and risks\n * float drift.\n *\n * @example\n * ```ts\n * import { moneyFromMajor, TRY } from '@lonca/core';\n * // A marketplace price of 199.90 ₺:\n * moneyFromMajor(199.9, TRY); // { amount: 19990, currency: 'TRY' } — kuruş\n * ```\n */\nexport function moneyFromMajor(major: number, currency: Currency, minorScale = 2): Money {\n const factor = 10 ** minorScale;\n const amount = Math.round(major * factor);\n return money(amount, currency);\n}\n\n/**\n * Convert {@link Money} (integer minor units) back to a major-unit number —\n * the canonical kuruş→lira converter.\n *\n * @example\n * ```ts\n * import { moneyToMajor, TRY } from '@lonca/core';\n * moneyToMajor({ amount: 19990, currency: TRY }); // 199.9 (lira)\n * ```\n */\nexport function moneyToMajor(value: Money, minorScale = 2): number {\n return value.amount / 10 ** minorScale;\n}\n\nexport function isSameCurrency(a: Money, b: Money): boolean {\n return a.currency === b.currency;\n}\n\nexport function addMoney(a: Money, b: Money): Money {\n if (!isSameCurrency(a, b)) {\n throw new TypeError(`Cannot add Money of different currencies: ${a.currency} vs ${b.currency}`);\n }\n return money(a.amount + b.amount, a.currency);\n}\n\nexport function subMoney(a: Money, b: Money): Money {\n if (!isSameCurrency(a, b)) {\n throw new TypeError(\n `Cannot subtract Money of different currencies: ${a.currency} vs ${b.currency}`,\n );\n }\n return money(a.amount - b.amount, a.currency);\n}\n","export interface CursorPaginationParams {\n /** Opaque cursor from previous response's `nextCursor`. Omit for the first page. */\n cursor?: string;\n /** Maximum items in the page. Marketplace SDKs may clamp this to their own bounds. */\n limit?: number;\n}\n\nexport interface CursorPage<T> {\n items: T[];\n /** Opaque cursor for the next page. Absent when there are no more pages. */\n nextCursor?: string;\n}\n\n/**\n * Drive a cursor-paginated endpoint as an async iterator.\n *\n * @example\n * for await (const order of paginate((p) => client.orders.list(p))) {\n * console.log(order.id);\n * }\n */\nexport async function* paginate<T>(\n fetchPage: (params: CursorPaginationParams) => Promise<CursorPage<T>>,\n params: Omit<CursorPaginationParams, 'cursor'> = {},\n): AsyncGenerator<T, void, undefined> {\n let cursor: string | undefined;\n do {\n const page = await fetchPage({ ...params, cursor });\n for (const item of page.items) {\n yield item;\n }\n cursor = page.nextCursor;\n } while (cursor);\n}\n\n// ─── Offset pagination ─────────────────────────────────────────────────────\n\nexport interface OffsetPaginationParams {\n /** Zero-based offset. Default depends on the SDK; usually `0`. */\n offset?: number;\n /** Maximum items in the page. */\n limit?: number;\n}\n\n/**\n * Offset-based pagination envelope returned by marketplaces that expose\n * `{ totalCount, limit, offset, pageCount, items[] }` instead of opaque\n * cursors. Hepsiburada's OMS uses this shape; some legacy Trendyol\n * endpoints do too. Distinct from `CursorPage<T>` so callers can tell at\n * the type level which pagination model an endpoint uses.\n */\nexport interface OffsetPage<T> {\n /** Total number of items matching the query across all pages. */\n totalCount: number;\n /** Echo of the request `limit`. */\n limit: number;\n /** Echo of the request `offset`. */\n offset: number;\n /** Total number of pages at the requested `limit`. */\n pageCount: number;\n /** Page rows. */\n items: T[];\n}\n\n/**\n * Drive an offset-paginated endpoint as an async iterator. Stops when the\n * server's `pageCount` is exhausted (or after the configured `maxPages`\n * to bound runaway iteration). Default page size is whatever the\n * underlying endpoint clamps to — pass `limit` explicitly for control.\n *\n * @example\n * for await (const order of paginateOffset((p) =>\n * client.orders.list(p),\n * )) {\n * console.log(order.orderNumber);\n * }\n */\nexport async function* paginateOffset<T>(\n fetchPage: (params: OffsetPaginationParams) => Promise<OffsetPage<T>>,\n params: { limit?: number; maxPages?: number } = {},\n): AsyncGenerator<T, void, undefined> {\n const limit = params.limit;\n const maxPages = params.maxPages ?? Infinity;\n let offset = 0;\n let pages = 0;\n while (pages < maxPages) {\n const page = await fetchPage({ offset, limit });\n for (const item of page.items) {\n yield item;\n }\n pages += 1;\n if (page.items.length === 0) return;\n if (page.pageCount > 0 && pages >= page.pageCount) return;\n // Step by the server-echoed limit (when present) so requests stay aligned with how\n // the server batches; fall back to the caller's `limit` or the observed batch size.\n offset += page.limit || limit || page.items.length;\n }\n}\n","/**\n * The closed, marketplace-agnostic order/shipment status vocabulary that every\n * SDK normalizes its raw statuses into.\n *\n * This union is intentionally **closed** (no `(string & {})` catch-all): a value\n * a marketplace SDK can't map lands on `'unknown'` at runtime via\n * {@link createStatusNormalizer}, surfaced by `mapped: false` — never silently\n * coerced to a valid-looking default.\n */\nexport type NormalizedOrderStatus =\n | 'created'\n | 'picking'\n | 'invoiced'\n | 'shipped'\n | 'delivered'\n | 'cancelled'\n | 'returned'\n | 'unknown';\n\n/**\n * Result of normalizing a raw marketplace status. The raw value is always\n * preserved, and `mapped` makes an unrecognized status visible at runtime\n * instead of letting it disappear into a default.\n */\nexport interface NormalizedStatusResult {\n /** Mapped normalized status, or `'unknown'` when the raw value wasn't recognized. */\n normalized: NormalizedOrderStatus;\n /** The original raw status string from the marketplace, preserved verbatim. */\n raw: string;\n /** `false` when the raw value had no mapping (normalized fell back to `'unknown'`). */\n mapped: boolean;\n}\n\n/**\n * Build a status normalizer from an SDK-owned, **exhaustive** map of that\n * marketplace's known statuses. Because the map key type is a closed union,\n * forgetting to map a newly added known status is a compile-time error in the\n * SDK — while an unknown value arriving on the wire stays safe at runtime\n * (`{ normalized: 'unknown', mapped: false }`).\n *\n * @example\n * ```ts\n * const statusMap = { Created: 'created', Shipped: 'shipped' } as const;\n * const normalizeStatus = createStatusNormalizer(statusMap);\n * normalizeStatus('Shipped'); // { normalized: 'shipped', raw: 'Shipped', mapped: true }\n * normalizeStatus('Martian'); // { normalized: 'unknown', raw: 'Martian', mapped: false }\n * ```\n */\nexport function createStatusNormalizer<Known extends string>(\n map: Record<Known, NormalizedOrderStatus>,\n): (raw: string) => NormalizedStatusResult {\n const lookup = map as Record<string, NormalizedOrderStatus | undefined>;\n return (raw: string): NormalizedStatusResult => {\n const normalized = lookup[raw];\n return normalized !== undefined\n ? { normalized, raw, mapped: true }\n : { normalized: 'unknown', raw, mapped: false };\n };\n}\n","export type LoncaErrorCode =\n | 'AUTH_FAILED'\n | 'RATE_LIMITED'\n | 'VALIDATION_FAILED'\n | 'NOT_FOUND'\n | 'SERVER_ERROR'\n | 'NETWORK_ERROR'\n | 'TIMEOUT'\n | 'UNKNOWN';\n\n/**\n * A single normalized, field-level error detail extracted from a marketplace's\n * raw error body. Each SDK maps its own (inconsistent) error JSON into this\n * shape so consumers don't have to sniff marketplace-specific payloads.\n */\nexport interface LoncaErrorIssue {\n /** The offending field/path, when the marketplace reports one. */\n field?: string;\n /** Marketplace-specific error code, when present. */\n code?: string;\n /** Human-readable message — always present. */\n message: string;\n}\n\nexport interface LoncaErrorOptions {\n code: LoncaErrorCode;\n message: string;\n cause?: unknown;\n retryable?: boolean;\n status?: number;\n retryAfterMs?: number;\n data?: Record<string, unknown>;\n /** Normalized, field-level error details. Defaults to `[]` when omitted. */\n issues?: LoncaErrorIssue[];\n}\n\n/**\n * Root of the Lonca error hierarchy.\n *\n * Carries a tagged `code` and a `retryable` boolean so retry helpers\n * can decide what to do without sniffing message strings.\n */\nexport class LoncaError extends Error {\n readonly code: LoncaErrorCode;\n readonly retryable: boolean;\n readonly status?: number;\n readonly retryAfterMs?: number;\n readonly data?: Record<string, unknown>;\n /**\n * Normalized, field-level error details mapped from the marketplace's raw\n * error body by the SDK. Always an array (never `undefined`) so callers can\n * iterate without a presence check; empty when nothing was parseable.\n */\n readonly issues: LoncaErrorIssue[];\n\n constructor(opts: LoncaErrorOptions) {\n super(opts.message, opts.cause !== undefined ? { cause: opts.cause } : undefined);\n this.name = 'LoncaError';\n this.code = opts.code;\n this.retryable = opts.retryable ?? false;\n this.status = opts.status;\n this.retryAfterMs = opts.retryAfterMs;\n this.data = opts.data;\n this.issues = opts.issues ?? [];\n }\n}\n\ntype SubclassOptions = Omit<LoncaErrorOptions, 'code' | 'retryable'>;\n\nexport class AuthError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'AUTH_FAILED', retryable: false });\n this.name = 'AuthError';\n }\n}\n\nexport class RateLimitError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'RATE_LIMITED', retryable: true });\n this.name = 'RateLimitError';\n }\n}\n\nexport class ValidationError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'VALIDATION_FAILED', retryable: false });\n this.name = 'ValidationError';\n }\n}\n\nexport class NotFoundError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'NOT_FOUND', retryable: false });\n this.name = 'NotFoundError';\n }\n}\n\nexport class ServerError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'SERVER_ERROR', retryable: true });\n this.name = 'ServerError';\n }\n}\n\nexport class NetworkError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'NETWORK_ERROR', retryable: true });\n this.name = 'NetworkError';\n }\n}\n\nexport class TimeoutError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'TIMEOUT', retryable: true });\n this.name = 'TimeoutError';\n }\n}\n\nexport function isLoncaError(value: unknown): value is LoncaError {\n return value instanceof LoncaError;\n}\n\nexport function isRetryableError(value: unknown): boolean {\n return isLoncaError(value) && value.retryable;\n}\n\n/**\n * Retry predicate for **non-idempotent** requests (POST/PUT/DELETE/PATCH).\n *\n * Only `RateLimitError` (HTTP 429) is replayed, because a 429 means the server\n * rejected the request *before* processing it, so a retry cannot duplicate a\n * side-effect. Ambiguous failures — 5xx, network drops, and client-side\n * timeouts — are deliberately NOT retried here: the write may already have\n * taken effect server-side, and a blind replay would duplicate it (double\n * order split, double cancel, re-pushed price/stock batch).\n *\n * Callers that have made a write idempotent (e.g. by sending an idempotency\n * key) should opt back into full retries via `isRetryableError` instead.\n */\nexport function isRetryableIdempotentOnly(value: unknown): boolean {\n return isLoncaError(value) && value.retryable && value.code === 'RATE_LIMITED';\n}\n\n/**\n * Parse a `Retry-After` header value (delta-seconds OR an HTTP-date) into\n * milliseconds. Returns `undefined` when the header is absent, unparseable, or\n * non-positive.\n *\n * A non-positive value is treated as \"no hint\" rather than `0`: a literal\n * `Retry-After: 0` must not collapse exponential backoff into a zero-delay\n * retry storm against the very endpoint that is rate-limiting the client.\n */\nexport function parseRetryAfter(header: string | null): number | undefined {\n if (!header) return undefined;\n const seconds = Number(header);\n if (!Number.isNaN(seconds)) return seconds > 0 ? seconds * 1000 : undefined;\n const epoch = Date.parse(header);\n if (!Number.isNaN(epoch)) {\n const delta = epoch - Date.now();\n return delta > 0 ? delta : undefined;\n }\n return undefined;\n}\n","import { isLoncaError, isRetryableError } from './errors.js';\n\nexport interface RetryOptions {\n /** Total number of attempts including the initial call. Defaults to 3. */\n maxAttempts?: number;\n /** Initial delay before the second attempt, in ms. Defaults to 200. */\n baseDelayMs?: number;\n /** Upper bound on delay between any two attempts, in ms. Defaults to 10_000. */\n maxDelayMs?: number;\n /** Add up to 50% random jitter on each delay. Defaults to true. */\n jitter?: boolean;\n /** Override the default predicate. Default treats `retryable` LoncaErrors as retryable. */\n isRetryable?: (err: unknown) => boolean;\n /** Called before each retry sleep. Useful for structured logging. */\n onRetry?: (err: unknown, attempt: number, nextDelayMs: number) => void;\n /** Abort all pending sleeps and bubble the signal's reason. */\n signal?: AbortSignal;\n}\n\nconst DEFAULTS = {\n maxAttempts: 3,\n baseDelayMs: 200,\n maxDelayMs: 10_000,\n jitter: true,\n} as const;\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(signal.reason);\n return;\n }\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n 'abort',\n () => {\n clearTimeout(timer);\n reject(signal.reason);\n },\n { once: true },\n );\n });\n}\n\nexport async function retry<T>(\n fn: (attempt: number) => Promise<T>,\n options: RetryOptions = {},\n): Promise<T> {\n const maxAttempts = options.maxAttempts ?? DEFAULTS.maxAttempts;\n const baseDelayMs = options.baseDelayMs ?? DEFAULTS.baseDelayMs;\n const maxDelayMs = options.maxDelayMs ?? DEFAULTS.maxDelayMs;\n const jitter = options.jitter ?? DEFAULTS.jitter;\n const isRetryable = options.isRetryable ?? isRetryableError;\n\n let lastErr: unknown;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n return await fn(attempt);\n } catch (err) {\n lastErr = err;\n if (attempt === maxAttempts || !isRetryable(err)) {\n throw err;\n }\n\n const retryAfterMs = isLoncaError(err) ? err.retryAfterMs : undefined;\n const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));\n // A non-positive `retryAfterMs` is ignored (not used as the base) so a\n // `Retry-After: 0` can never collapse backoff to an immediate retry.\n const base = retryAfterMs && retryAfterMs > 0 ? retryAfterMs : exponential;\n const jitterMs = jitter ? Math.random() * base * 0.5 : 0;\n const delay = Math.min(maxDelayMs, base + jitterMs);\n\n options.onRetry?.(err, attempt, delay);\n await sleep(delay, options.signal);\n }\n }\n throw lastErr;\n}\n","export type LogLevel = 'debug' | 'info' | 'warn' | 'error';\n\nexport type LogContext = Record<string, unknown>;\n\nexport interface Logger {\n debug(message: string, context?: LogContext): void;\n info(message: string, context?: LogContext): void;\n warn(message: string, context?: LogContext): void;\n error(message: string, context?: LogContext): void;\n /** Create a child logger that merges `bindings` into every log entry. */\n child(bindings: LogContext): Logger;\n}\n\nexport const noopLogger: Logger = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n child: () => noopLogger,\n};\n\n/**\n * Minimal JSON-line logger to stdout/stderr. Suitable for development and\n * containerized environments. For production prefer wiring a real logger\n * (pino, winston) by implementing the `Logger` interface.\n */\nexport function consoleLogger(bindings: LogContext = {}): Logger {\n const emit = (level: LogLevel, message: string, context?: LogContext): void => {\n const entry = {\n level,\n message,\n timestamp: new Date().toISOString(),\n ...bindings,\n ...context,\n };\n const line = JSON.stringify(entry);\n if (level === 'error' || level === 'warn') {\n console.error(line);\n } else {\n console.log(line);\n }\n };\n return {\n debug: (m, c) => emit('debug', m, c),\n info: (m, c) => emit('info', m, c),\n warn: (m, c) => emit('warn', m, c),\n error: (m, c) => emit('error', m, c),\n child: (b) => consoleLogger({ ...bindings, ...b }),\n };\n}\n","export interface RateLimiterOptions {\n /** Maximum number of tokens the bucket holds. Each `acquire()` consumes 1. */\n capacity: number;\n /** Time window in milliseconds over which `capacity` tokens are refilled. */\n intervalMs: number;\n}\n\n/**\n * Token-bucket rate limiter. `acquire()` returns immediately when a token\n * is available, otherwise it sleeps until enough tokens have been refilled.\n *\n * @example\n * const limiter = new TokenBucketRateLimiter({ capacity: 50, intervalMs: 60_000 });\n * for (const item of items) {\n * await limiter.acquire();\n * await fetchSomething(item);\n * }\n */\nexport class TokenBucketRateLimiter {\n private tokens: number;\n private lastRefillAt: number;\n private readonly capacity: number;\n private readonly intervalMs: number;\n\n constructor(options: RateLimiterOptions) {\n if (options.capacity <= 0 || options.intervalMs <= 0) {\n throw new RangeError('capacity and intervalMs must be > 0');\n }\n this.capacity = options.capacity;\n this.intervalMs = options.intervalMs;\n this.tokens = options.capacity;\n this.lastRefillAt = Date.now();\n }\n\n /**\n * Refill tokens based on elapsed time, then return ms to wait until the next\n * token is available (0 if one is available right now).\n */\n private timeUntilNextToken(): number {\n const now = Date.now();\n const elapsed = now - this.lastRefillAt;\n if (elapsed > 0) {\n const refill = (elapsed / this.intervalMs) * this.capacity;\n this.tokens = Math.min(this.capacity, this.tokens + refill);\n this.lastRefillAt = now;\n }\n if (this.tokens >= 1) return 0;\n const needed = 1 - this.tokens;\n return Math.ceil((needed / this.capacity) * this.intervalMs);\n }\n\n async acquire(signal?: AbortSignal): Promise<void> {\n for (;;) {\n if (signal?.aborted) throw signal.reason as Error;\n const wait = this.timeUntilNextToken();\n if (wait === 0) {\n this.tokens -= 1;\n return;\n }\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(resolve, wait);\n signal?.addEventListener(\n 'abort',\n () => {\n clearTimeout(timer);\n reject(signal.reason);\n },\n { once: true },\n );\n });\n }\n }\n\n /** Snapshot of available tokens (refills the bucket as a side effect). */\n available(): number {\n this.timeUntilNextToken();\n return this.tokens;\n }\n}\n","import { randomUUID } from 'node:crypto';\nimport {\n LoncaError,\n NetworkError,\n TimeoutError,\n parseRetryAfter,\n isRetryableIdempotentOnly,\n} from './errors.js';\nimport { retry } from './retry.js';\nimport { noopLogger, type Logger } from './logger.js';\nimport type { TokenBucketRateLimiter } from './rate-limiter.js';\n\n/**\n * The request lifecycle shared by every marketplace SDK transport: acquire a\n * rate-limit token, build the URL/headers, fire `fetch` under a composed\n * timeout signal, map non-2xx responses to `LoncaError`s, and replay through\n * {@link retry}. Only the marketplace-specific seams (URL building, headers,\n * status→error mapping, log labels) are injected via {@link RequesterConfig},\n * so the loop itself lives in one place instead of being copy-pasted — and\n * drifting — across SDKs.\n */\n\n/** The request fields the shared lifecycle reads. SDKs extend this with their own (e.g. `path`, `service`, `query`). */\nexport interface BaseRequestOptions {\n /** HTTP method. `GET` is treated as idempotent for retry purposes. */\n method: string;\n /** Request body. Serialized as JSON unless it is a `FormData` (sent as multipart). Skipped for `GET`. */\n body?: unknown;\n /** Caller abort signal, composed with the per-request timeout. */\n signal?: AbortSignal;\n /** Per-endpoint rate limiter; one token is acquired before each attempt. */\n rateLimiter?: TokenBucketRateLimiter;\n /**\n * Whether this request is safe to auto-replay on an ambiguous transient\n * failure (5xx / network drop / client timeout). `GET` is always idempotent.\n * For writes this defaults to `false`: a timed-out or 5xx write may already\n * have committed server-side, so only a `429` (provably rejected before\n * processing) is retried. Set `true` to opt a write back into full retries.\n */\n idempotent?: boolean;\n /** Extra per-request headers merged over the default header set (caller headers win). */\n headers?: Record<string, string>;\n}\n\n/** Marketplace-specific seams plus runtime dependencies for {@link createRequester}. */\nexport interface RequesterConfig<O extends BaseRequestOptions> {\n /** Underlying `fetch` (tests inject a mock). */\n fetch: typeof fetch;\n /** Structured logger. Defaults to a no-op. */\n logger?: Logger;\n /** Per-request timeout in ms. */\n timeoutMs: number;\n /** Human-readable marketplace name for error messages, e.g. `'Trendyol'`. */\n label: string;\n /** Log-event prefix, e.g. `'trendyol'` → `trendyol.request` / `trendyol.error` / `trendyol.retry`. */\n logPrefix: string;\n /** Build the absolute request URL from the SDK's options. */\n buildUrl(opts: O): string;\n /** Build the default header set; receives the generated correlation id. */\n buildHeaders(correlationId: string): Record<string, string>;\n /** Map a non-2xx response to a `LoncaError`. */\n mapHttpError(status: number, body: unknown, retryAfterMs?: number): LoncaError;\n /** Optional extra structured fields merged into request/error log lines (e.g. `{ service }`). */\n logFields?(opts: O): Record<string, unknown>;\n}\n\n/**\n * Build a `request<T>(opts)` function that runs the shared transport lifecycle\n * for a given marketplace. Each SDK calls this once (per transport instance)\n * and exposes the returned function from its own transport class.\n */\nexport function createRequester<O extends BaseRequestOptions>(\n config: RequesterConfig<O>,\n): <T>(opts: O) => Promise<T> {\n const logger = config.logger ?? noopLogger;\n\n return function request<T>(opts: O): Promise<T> {\n const safeToReplay = opts.method === 'GET' || opts.idempotent === true;\n const extra = () => config.logFields?.(opts) ?? {};\n\n return retry(\n async (attempt) => {\n if (opts.rateLimiter) await opts.rateLimiter.acquire(opts.signal);\n\n const correlationId = randomUUID();\n const url = config.buildUrl(opts);\n const headers = { ...config.buildHeaders(correlationId), ...opts.headers };\n const init: RequestInit = {\n method: opts.method,\n headers,\n signal: composeSignal(opts.signal, config.timeoutMs),\n };\n if (opts.body !== undefined && opts.method !== 'GET') {\n if (opts.body instanceof FormData) {\n // multipart: let fetch set Content-Type (it includes the boundary).\n init.body = opts.body;\n delete (headers as Record<string, string>)['Content-Type'];\n } else {\n init.body = JSON.stringify(opts.body);\n }\n }\n\n logger.debug(`${config.logPrefix}.request`, {\n method: opts.method,\n url,\n correlationId,\n attempt,\n ...extra(),\n });\n\n let response: Response;\n try {\n response = await config.fetch(url, init);\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n throw new TimeoutError({\n message: `${config.label} request timed out after ${config.timeoutMs}ms`,\n cause: err,\n });\n }\n throw new NetworkError({ message: `${config.label} network failure`, cause: err });\n }\n\n if (!response.ok) {\n const body = await safeJson(response);\n const retryAfterMs = parseRetryAfter(response.headers.get('retry-after'));\n const error = config.mapHttpError(response.status, body, retryAfterMs);\n logger.warn(`${config.logPrefix}.error`, {\n method: opts.method,\n url,\n correlationId,\n status: response.status,\n code: error.code,\n retryable: error.retryable,\n ...extra(),\n });\n throw error;\n }\n\n logger.debug(`${config.logPrefix}.response`, {\n correlationId,\n status: response.status,\n });\n\n if (response.status === 204) return undefined as T;\n return (await safeJson(response)) as T;\n },\n {\n signal: opts.signal,\n // Non-idempotent writes only retry rate-limit (429) errors; ambiguous\n // 5xx/network/timeout failures are not replayed to avoid duplicate\n // side-effects. GET (and explicitly idempotent requests) retry normally.\n isRetryable: safeToReplay ? undefined : isRetryableIdempotentOnly,\n onRetry: (err, attempt, delay) => {\n if (err instanceof LoncaError) {\n logger.warn(`${config.logPrefix}.retry`, {\n attempt,\n delayMs: delay,\n code: err.code,\n status: err.status,\n });\n }\n },\n },\n );\n };\n}\n\n/** Compose a caller signal with a per-request timeout signal. */\nexport function composeSignal(external: AbortSignal | undefined, timeoutMs: number): AbortSignal {\n const timeoutSignal = AbortSignal.timeout(timeoutMs);\n if (!external) return timeoutSignal;\n return AbortSignal.any([external, timeoutSignal]);\n}\n\n/** Read a response body as JSON, falling back to raw text (then `undefined` when empty). */\nexport async function safeJson(response: Response): Promise<unknown> {\n const text = await response.text();\n if (!text) return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lonca/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Shared primitives for Lonca marketplace SDKs: money, errors, retry, logger, rate limiter.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@types/node": "^24.0.0",
|
|
39
39
|
"tsup": "^8.3.5",
|
|
40
40
|
"typescript": "^6.0.3",
|
|
41
|
-
"vitest": "^4.1.
|
|
41
|
+
"vitest": "^4.1.8"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build": "tsup",
|