@lonca/core 0.3.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 +13 -9
- package/dist/index.cjs +121 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +87 -1
- package/dist/index.d.ts +87 -1
- package/dist/index.js +117 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,9 @@ Shared primitives for Lonca marketplace SDKs.
|
|
|
8
8
|
|
|
9
9
|
> Type-safe building blocks reused across every `@lonca/<marketplace>` SDK: money, errors, pagination, retry, logger, rate limiter.
|
|
10
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
|
+
|
|
11
14
|
## Install
|
|
12
15
|
|
|
13
16
|
```bash
|
|
@@ -16,15 +19,16 @@ pnpm add @lonca/core
|
|
|
16
19
|
|
|
17
20
|
## What's inside
|
|
18
21
|
|
|
19
|
-
| Primitive | Purpose
|
|
20
|
-
| ------------------------ |
|
|
21
|
-
| `Money`, `Currency` | ISO 4217 currency codes; integer minor-unit money representation; `moneyFromMajor`/`moneyToMajor` lira↔kuruş converters
|
|
22
|
-
| `CursorPage`, `paginate` | Cursor-based pagination + async iterator helper
|
|
23
|
-
| `LoncaError` hierarchy | Structured errors (`AuthError`, `RateLimitError`, `ValidationError`, `NotFoundError`, `ServerError`, `NetworkError`, `TimeoutError`) with `retryable` flag and a normalized `issues` array
|
|
24
|
-
| `NormalizedOrderStatus` | Closed cross-marketplace order-status vocabulary + `createStatusNormalizer` (surfaces unmapped statuses via `mapped: false`, never a silent default)
|
|
25
|
-
| `retry` | Exponential backoff with jitter, honors `retryAfterMs`, supports `AbortSignal`
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
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 |
|
|
28
32
|
|
|
29
33
|
## Design principles
|
|
30
34
|
|
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";
|
|
@@ -152,6 +154,20 @@ function isLoncaError(value) {
|
|
|
152
154
|
function isRetryableError(value) {
|
|
153
155
|
return isLoncaError(value) && value.retryable;
|
|
154
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
|
+
}
|
|
155
171
|
|
|
156
172
|
// src/retry.ts
|
|
157
173
|
var DEFAULTS = {
|
|
@@ -194,7 +210,7 @@ async function retry(fn, options = {}) {
|
|
|
194
210
|
}
|
|
195
211
|
const retryAfterMs = isLoncaError(err) ? err.retryAfterMs : void 0;
|
|
196
212
|
const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
|
|
197
|
-
const base = retryAfterMs
|
|
213
|
+
const base = retryAfterMs && retryAfterMs > 0 ? retryAfterMs : exponential;
|
|
198
214
|
const jitterMs = jitter ? Math.random() * base * 0.5 : 0;
|
|
199
215
|
const delay = Math.min(maxDelayMs, base + jitterMs);
|
|
200
216
|
options.onRetry?.(err, attempt, delay);
|
|
@@ -299,6 +315,105 @@ var TokenBucketRateLimiter = class {
|
|
|
299
315
|
return this.tokens;
|
|
300
316
|
}
|
|
301
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
|
+
}
|
|
302
417
|
|
|
303
418
|
exports.AuthError = AuthError;
|
|
304
419
|
exports.EUR = EUR;
|
|
@@ -314,10 +429,13 @@ exports.TokenBucketRateLimiter = TokenBucketRateLimiter;
|
|
|
314
429
|
exports.USD = USD;
|
|
315
430
|
exports.ValidationError = ValidationError;
|
|
316
431
|
exports.addMoney = addMoney;
|
|
432
|
+
exports.composeSignal = composeSignal;
|
|
317
433
|
exports.consoleLogger = consoleLogger;
|
|
434
|
+
exports.createRequester = createRequester;
|
|
318
435
|
exports.createStatusNormalizer = createStatusNormalizer;
|
|
319
436
|
exports.isLoncaError = isLoncaError;
|
|
320
437
|
exports.isRetryableError = isRetryableError;
|
|
438
|
+
exports.isRetryableIdempotentOnly = isRetryableIdempotentOnly;
|
|
321
439
|
exports.isSameCurrency = isSameCurrency;
|
|
322
440
|
exports.isValidCurrencyCode = isValidCurrencyCode;
|
|
323
441
|
exports.money = money;
|
|
@@ -326,7 +444,9 @@ exports.moneyToMajor = moneyToMajor;
|
|
|
326
444
|
exports.noopLogger = noopLogger;
|
|
327
445
|
exports.paginate = paginate;
|
|
328
446
|
exports.paginateOffset = paginateOffset;
|
|
447
|
+
exports.parseRetryAfter = parseRetryAfter;
|
|
329
448
|
exports.retry = retry;
|
|
449
|
+
exports.safeJson = safeJson;
|
|
330
450
|
exports.subMoney = subMoney;
|
|
331
451
|
//# sourceMappingURL=index.cjs.map
|
|
332
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/order-status.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\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","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":"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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\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
|
@@ -224,6 +224,30 @@ declare class TimeoutError extends LoncaError {
|
|
|
224
224
|
}
|
|
225
225
|
declare function isLoncaError(value: unknown): value is LoncaError;
|
|
226
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;
|
|
227
251
|
|
|
228
252
|
interface RetryOptions {
|
|
229
253
|
/** Total number of attempts including the initial call. Defaults to 3. */
|
|
@@ -294,4 +318,66 @@ declare class TokenBucketRateLimiter {
|
|
|
294
318
|
available(): number;
|
|
295
319
|
}
|
|
296
320
|
|
|
297
|
-
|
|
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
|
@@ -224,6 +224,30 @@ declare class TimeoutError extends LoncaError {
|
|
|
224
224
|
}
|
|
225
225
|
declare function isLoncaError(value: unknown): value is LoncaError;
|
|
226
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;
|
|
227
251
|
|
|
228
252
|
interface RetryOptions {
|
|
229
253
|
/** Total number of attempts including the initial call. Defaults to 3. */
|
|
@@ -294,4 +318,66 @@ declare class TokenBucketRateLimiter {
|
|
|
294
318
|
available(): number;
|
|
295
319
|
}
|
|
296
320
|
|
|
297
|
-
|
|
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";
|
|
@@ -150,6 +152,20 @@ function isLoncaError(value) {
|
|
|
150
152
|
function isRetryableError(value) {
|
|
151
153
|
return isLoncaError(value) && value.retryable;
|
|
152
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
|
+
}
|
|
153
169
|
|
|
154
170
|
// src/retry.ts
|
|
155
171
|
var DEFAULTS = {
|
|
@@ -192,7 +208,7 @@ async function retry(fn, options = {}) {
|
|
|
192
208
|
}
|
|
193
209
|
const retryAfterMs = isLoncaError(err) ? err.retryAfterMs : void 0;
|
|
194
210
|
const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
|
|
195
|
-
const base = retryAfterMs
|
|
211
|
+
const base = retryAfterMs && retryAfterMs > 0 ? retryAfterMs : exponential;
|
|
196
212
|
const jitterMs = jitter ? Math.random() * base * 0.5 : 0;
|
|
197
213
|
const delay = Math.min(maxDelayMs, base + jitterMs);
|
|
198
214
|
options.onRetry?.(err, attempt, delay);
|
|
@@ -297,7 +313,106 @@ var TokenBucketRateLimiter = class {
|
|
|
297
313
|
return this.tokens;
|
|
298
314
|
}
|
|
299
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
|
+
}
|
|
300
415
|
|
|
301
|
-
export { AuthError, EUR, GBP, LoncaError, NetworkError, NotFoundError, RateLimitError, ServerError, TRY, TimeoutError, TokenBucketRateLimiter, USD, ValidationError, addMoney, consoleLogger, createStatusNormalizer, 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 };
|
|
302
417
|
//# sourceMappingURL=index.js.map
|
|
303
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/order-status.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\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","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"]}
|