@lonca/core 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,16 @@
1
+ <p align="left">
2
+ <img src="https://raw.githubusercontent.com/loncadev/lonca/main/assets/brand/icon.svg" alt="Lonca" height="32">
3
+ </p>
4
+
1
5
  # @lonca/core
2
6
 
3
7
  Shared primitives for Lonca marketplace SDKs.
4
8
 
5
9
  > Type-safe building blocks reused across every `@lonca/<marketplace>` SDK: money, errors, pagination, retry, logger, rate limiter.
6
10
 
11
+ > [!IMPORTANT]
12
+ > **Unofficial.** Part of Lonca, an independent, community-maintained project — not affiliated with, endorsed by, or supported by any marketplace. All marketplace names and trademarks belong to their respective owners.
13
+
7
14
  ## Install
8
15
 
9
16
  ```bash
@@ -12,18 +19,20 @@ pnpm add @lonca/core
12
19
 
13
20
  ## What's inside
14
21
 
15
- | Primitive | Purpose |
16
- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
- | `Money`, `Currency` | ISO 4217 currency codes; integer minor-unit money representation |
18
- | `CursorPage`, `paginate` | Cursor-based pagination + async iterator helper |
19
- | `LoncaError` hierarchy | Structured errors (`AuthError`, `RateLimitError`, `ValidationError`, `NotFoundError`, `ServerError`, `NetworkError`, `TimeoutError`) with `retryable` flag |
20
- | `retry` | Exponential backoff with jitter, honors `retryAfterMs`, supports `AbortSignal` |
21
- | `Logger` | Structured logger interface (`debug`/`info`/`warn`/`error`/`child`) with `noopLogger` and `consoleLogger` |
22
- | `TokenBucketRateLimiter` | Async token-bucket rate limiter with `AbortSignal` support |
22
+ | Primitive | Purpose |
23
+ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
24
+ | `Money`, `Currency` | ISO 4217 currency codes; integer minor-unit money representation; `moneyFromMajor`/`moneyToMajor` lira↔kuruş converters |
25
+ | `CursorPage`, `paginate` | Cursor-based pagination + async iterator helper |
26
+ | `LoncaError` hierarchy | Structured errors (`AuthError`, `RateLimitError`, `ValidationError`, `NotFoundError`, `ServerError`, `NetworkError`, `TimeoutError`) with `retryable` flag and a normalized `issues` array |
27
+ | `NormalizedOrderStatus` | Closed cross-marketplace order-status vocabulary + `createStatusNormalizer` (surfaces unmapped statuses via `mapped: false`, never a silent default) |
28
+ | `retry` | Exponential backoff with jitter, honors `retryAfterMs`, supports `AbortSignal`. `parseRetryAfter` parses `Retry-After`; `isRetryableIdempotentOnly` gates non-idempotent writes to 429-only replays |
29
+ | `createRequester` | Shared HTTP request lifecycle (rate-limit → fetch under a composed timeout → 204/error mapping → idempotency-aware retry → structured logging) that each SDK transport is built on; inject marketplace-specific URL/header/error-map seams |
30
+ | `Logger` | Structured logger interface (`debug`/`info`/`warn`/`error`/`child`) with `noopLogger` and `consoleLogger` |
31
+ | `TokenBucketRateLimiter` | Async token-bucket rate limiter with `AbortSignal` support |
23
32
 
24
33
  ## Design principles
25
34
 
26
- - **Money is an integer in minor units** — `{ amount: 12550, currency: 'TRY' }` means 125.50 TRY. No floats, no surprises.
35
+ - **Money is an integer in minor units** — `{ amount: 12550, currency: 'TRY' }` means 125.50 TRY. No floats, no surprises. Marketplace SDKs hand you raw major-unit numbers (e.g. `199.9` lira); wrap them once with `moneyFromMajor(price, TRY)` instead of a hand-rolled `Math.round(price * 100)`.
27
36
  - **Cursor pagination over offset** — opaque cursors compose better with async iterators and stay stable as datasets grow.
28
37
  - **Errors are typed and tagged** — `err.code` plus a `retryable` boolean tells retry helpers what to do without sniffing messages.
29
38
  - **Logger is an interface, not an implementation** — wire your own (pino, winston, console). A no-op default is provided.
package/dist/index.cjs CHANGED
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ var crypto = require('crypto');
4
+
3
5
  // src/currency.ts
4
6
  var TRY = "TRY";
5
7
  var USD = "USD";
@@ -71,6 +73,15 @@ async function* paginateOffset(fetchPage, params = {}) {
71
73
  }
72
74
  }
73
75
 
76
+ // src/order-status.ts
77
+ function createStatusNormalizer(map) {
78
+ const lookup = map;
79
+ return (raw) => {
80
+ const normalized = lookup[raw];
81
+ return normalized !== void 0 ? { normalized, raw, mapped: true } : { normalized: "unknown", raw, mapped: false };
82
+ };
83
+ }
84
+
74
85
  // src/errors.ts
75
86
  var LoncaError = class extends Error {
76
87
  code;
@@ -78,6 +89,12 @@ var LoncaError = class extends Error {
78
89
  status;
79
90
  retryAfterMs;
80
91
  data;
92
+ /**
93
+ * Normalized, field-level error details mapped from the marketplace's raw
94
+ * error body by the SDK. Always an array (never `undefined`) so callers can
95
+ * iterate without a presence check; empty when nothing was parseable.
96
+ */
97
+ issues;
81
98
  constructor(opts) {
82
99
  super(opts.message, opts.cause !== void 0 ? { cause: opts.cause } : void 0);
83
100
  this.name = "LoncaError";
@@ -86,6 +103,7 @@ var LoncaError = class extends Error {
86
103
  this.status = opts.status;
87
104
  this.retryAfterMs = opts.retryAfterMs;
88
105
  this.data = opts.data;
106
+ this.issues = opts.issues ?? [];
89
107
  }
90
108
  };
91
109
  var AuthError = class extends LoncaError {
@@ -136,6 +154,20 @@ function isLoncaError(value) {
136
154
  function isRetryableError(value) {
137
155
  return isLoncaError(value) && value.retryable;
138
156
  }
157
+ function isRetryableIdempotentOnly(value) {
158
+ return isLoncaError(value) && value.retryable && value.code === "RATE_LIMITED";
159
+ }
160
+ function parseRetryAfter(header) {
161
+ if (!header) return void 0;
162
+ const seconds = Number(header);
163
+ if (!Number.isNaN(seconds)) return seconds > 0 ? seconds * 1e3 : void 0;
164
+ const epoch = Date.parse(header);
165
+ if (!Number.isNaN(epoch)) {
166
+ const delta = epoch - Date.now();
167
+ return delta > 0 ? delta : void 0;
168
+ }
169
+ return void 0;
170
+ }
139
171
 
140
172
  // src/retry.ts
141
173
  var DEFAULTS = {
@@ -178,7 +210,7 @@ async function retry(fn, options = {}) {
178
210
  }
179
211
  const retryAfterMs = isLoncaError(err) ? err.retryAfterMs : void 0;
180
212
  const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
181
- const base = retryAfterMs ?? exponential;
213
+ const base = retryAfterMs && retryAfterMs > 0 ? retryAfterMs : exponential;
182
214
  const jitterMs = jitter ? Math.random() * base * 0.5 : 0;
183
215
  const delay = Math.min(maxDelayMs, base + jitterMs);
184
216
  options.onRetry?.(err, attempt, delay);
@@ -283,6 +315,105 @@ var TokenBucketRateLimiter = class {
283
315
  return this.tokens;
284
316
  }
285
317
  };
318
+ function createRequester(config) {
319
+ const logger = config.logger ?? noopLogger;
320
+ return function request(opts) {
321
+ const safeToReplay = opts.method === "GET" || opts.idempotent === true;
322
+ const extra = () => config.logFields?.(opts) ?? {};
323
+ return retry(
324
+ async (attempt) => {
325
+ if (opts.rateLimiter) await opts.rateLimiter.acquire(opts.signal);
326
+ const correlationId = crypto.randomUUID();
327
+ const url = config.buildUrl(opts);
328
+ const headers = { ...config.buildHeaders(correlationId), ...opts.headers };
329
+ const init = {
330
+ method: opts.method,
331
+ headers,
332
+ signal: composeSignal(opts.signal, config.timeoutMs)
333
+ };
334
+ if (opts.body !== void 0 && opts.method !== "GET") {
335
+ if (opts.body instanceof FormData) {
336
+ init.body = opts.body;
337
+ delete headers["Content-Type"];
338
+ } else {
339
+ init.body = JSON.stringify(opts.body);
340
+ }
341
+ }
342
+ logger.debug(`${config.logPrefix}.request`, {
343
+ method: opts.method,
344
+ url,
345
+ correlationId,
346
+ attempt,
347
+ ...extra()
348
+ });
349
+ let response;
350
+ try {
351
+ response = await config.fetch(url, init);
352
+ } catch (err) {
353
+ if (err instanceof Error && err.name === "AbortError") {
354
+ throw new TimeoutError({
355
+ message: `${config.label} request timed out after ${config.timeoutMs}ms`,
356
+ cause: err
357
+ });
358
+ }
359
+ throw new NetworkError({ message: `${config.label} network failure`, cause: err });
360
+ }
361
+ if (!response.ok) {
362
+ const body = await safeJson(response);
363
+ const retryAfterMs = parseRetryAfter(response.headers.get("retry-after"));
364
+ const error = config.mapHttpError(response.status, body, retryAfterMs);
365
+ logger.warn(`${config.logPrefix}.error`, {
366
+ method: opts.method,
367
+ url,
368
+ correlationId,
369
+ status: response.status,
370
+ code: error.code,
371
+ retryable: error.retryable,
372
+ ...extra()
373
+ });
374
+ throw error;
375
+ }
376
+ logger.debug(`${config.logPrefix}.response`, {
377
+ correlationId,
378
+ status: response.status
379
+ });
380
+ if (response.status === 204) return void 0;
381
+ return await safeJson(response);
382
+ },
383
+ {
384
+ signal: opts.signal,
385
+ // Non-idempotent writes only retry rate-limit (429) errors; ambiguous
386
+ // 5xx/network/timeout failures are not replayed to avoid duplicate
387
+ // side-effects. GET (and explicitly idempotent requests) retry normally.
388
+ isRetryable: safeToReplay ? void 0 : isRetryableIdempotentOnly,
389
+ onRetry: (err, attempt, delay) => {
390
+ if (err instanceof LoncaError) {
391
+ logger.warn(`${config.logPrefix}.retry`, {
392
+ attempt,
393
+ delayMs: delay,
394
+ code: err.code,
395
+ status: err.status
396
+ });
397
+ }
398
+ }
399
+ }
400
+ );
401
+ };
402
+ }
403
+ function composeSignal(external, timeoutMs) {
404
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
405
+ if (!external) return timeoutSignal;
406
+ return AbortSignal.any([external, timeoutSignal]);
407
+ }
408
+ async function safeJson(response) {
409
+ const text = await response.text();
410
+ if (!text) return void 0;
411
+ try {
412
+ return JSON.parse(text);
413
+ } catch {
414
+ return text;
415
+ }
416
+ }
286
417
 
287
418
  exports.AuthError = AuthError;
288
419
  exports.EUR = EUR;
@@ -298,9 +429,13 @@ exports.TokenBucketRateLimiter = TokenBucketRateLimiter;
298
429
  exports.USD = USD;
299
430
  exports.ValidationError = ValidationError;
300
431
  exports.addMoney = addMoney;
432
+ exports.composeSignal = composeSignal;
301
433
  exports.consoleLogger = consoleLogger;
434
+ exports.createRequester = createRequester;
435
+ exports.createStatusNormalizer = createStatusNormalizer;
302
436
  exports.isLoncaError = isLoncaError;
303
437
  exports.isRetryableError = isRetryableError;
438
+ exports.isRetryableIdempotentOnly = isRetryableIdempotentOnly;
304
439
  exports.isSameCurrency = isSameCurrency;
305
440
  exports.isValidCurrencyCode = isValidCurrencyCode;
306
441
  exports.money = money;
@@ -309,7 +444,9 @@ exports.moneyToMajor = moneyToMajor;
309
444
  exports.noopLogger = noopLogger;
310
445
  exports.paginate = paginate;
311
446
  exports.paginateOffset = paginateOffset;
447
+ exports.parseRetryAfter = parseRetryAfter;
312
448
  exports.retry = retry;
449
+ exports.safeJson = safeJson;
313
450
  exports.subMoney = subMoney;
314
451
  //# sourceMappingURL=index.cjs.map
315
452
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/currency.ts","../src/money.ts","../src/pagination.ts","../src/errors.ts","../src/retry.ts","../src/logger.ts","../src/rate-limiter.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * ISO 4217 three-letter currency code.\n *\n * Typed as a string for flexibility — Trendyol and similar marketplaces may\n * expose currencies beyond the common set, so we don't lock down to a union.\n * Use `isValidCurrencyCode` for runtime validation.\n */\nexport type Currency = string;\n\nexport const TRY = 'TRY';\nexport const USD = 'USD';\nexport const EUR = 'EUR';\nexport const GBP = 'GBP';\n\nconst ISO_4217_RE = /^[A-Z]{3}$/;\n\nexport function isValidCurrencyCode(code: string): boolean {\n return ISO_4217_RE.test(code);\n}\n","import type { Currency } from './currency.js';\n\n/**\n * A monetary amount in integer minor units (e.g., kuruş for TRY, cents for USD).\n *\n * Using integers avoids floating-point surprises. Convert to/from major units\n * with `moneyFromMajor` and `moneyToMajor`.\n */\nexport interface Money {\n amount: number;\n currency: Currency;\n}\n\nexport function money(amount: number, currency: Currency): Money {\n if (!Number.isInteger(amount)) {\n throw new TypeError(`Money amount must be an integer minor unit, got ${amount}`);\n }\n return { amount, currency };\n}\n\nexport function moneyFromMajor(major: number, currency: Currency, minorScale = 2): Money {\n const factor = 10 ** minorScale;\n const amount = Math.round(major * factor);\n return money(amount, currency);\n}\n\nexport function moneyToMajor(value: Money, minorScale = 2): number {\n return value.amount / 10 ** minorScale;\n}\n\nexport function isSameCurrency(a: Money, b: Money): boolean {\n return a.currency === b.currency;\n}\n\nexport function addMoney(a: Money, b: Money): Money {\n if (!isSameCurrency(a, b)) {\n throw new TypeError(`Cannot add Money of different currencies: ${a.currency} vs ${b.currency}`);\n }\n return money(a.amount + b.amount, a.currency);\n}\n\nexport function subMoney(a: Money, b: Money): Money {\n if (!isSameCurrency(a, b)) {\n throw new TypeError(\n `Cannot subtract Money of different currencies: ${a.currency} vs ${b.currency}`,\n );\n }\n return money(a.amount - b.amount, a.currency);\n}\n","export interface CursorPaginationParams {\n /** Opaque cursor from previous response's `nextCursor`. Omit for the first page. */\n cursor?: string;\n /** Maximum items in the page. Marketplace SDKs may clamp this to their own bounds. */\n limit?: number;\n}\n\nexport interface CursorPage<T> {\n items: T[];\n /** Opaque cursor for the next page. Absent when there are no more pages. */\n nextCursor?: string;\n}\n\n/**\n * Drive a cursor-paginated endpoint as an async iterator.\n *\n * @example\n * for await (const order of paginate((p) => client.orders.list(p))) {\n * console.log(order.id);\n * }\n */\nexport async function* paginate<T>(\n fetchPage: (params: CursorPaginationParams) => Promise<CursorPage<T>>,\n params: Omit<CursorPaginationParams, 'cursor'> = {},\n): AsyncGenerator<T, void, undefined> {\n let cursor: string | undefined;\n do {\n const page = await fetchPage({ ...params, cursor });\n for (const item of page.items) {\n yield item;\n }\n cursor = page.nextCursor;\n } while (cursor);\n}\n\n// ─── Offset pagination ─────────────────────────────────────────────────────\n\nexport interface OffsetPaginationParams {\n /** Zero-based offset. Default depends on the SDK; usually `0`. */\n offset?: number;\n /** Maximum items in the page. */\n limit?: number;\n}\n\n/**\n * Offset-based pagination envelope returned by marketplaces that expose\n * `{ totalCount, limit, offset, pageCount, items[] }` instead of opaque\n * cursors. Hepsiburada's OMS uses this shape; some legacy Trendyol\n * endpoints do too. Distinct from `CursorPage<T>` so callers can tell at\n * the type level which pagination model an endpoint uses.\n */\nexport interface OffsetPage<T> {\n /** Total number of items matching the query across all pages. */\n totalCount: number;\n /** Echo of the request `limit`. */\n limit: number;\n /** Echo of the request `offset`. */\n offset: number;\n /** Total number of pages at the requested `limit`. */\n pageCount: number;\n /** Page rows. */\n items: T[];\n}\n\n/**\n * Drive an offset-paginated endpoint as an async iterator. Stops when the\n * server's `pageCount` is exhausted (or after the configured `maxPages`\n * to bound runaway iteration). Default page size is whatever the\n * underlying endpoint clamps to — pass `limit` explicitly for control.\n *\n * @example\n * for await (const order of paginateOffset((p) =>\n * client.orders.list(p),\n * )) {\n * console.log(order.orderNumber);\n * }\n */\nexport async function* paginateOffset<T>(\n fetchPage: (params: OffsetPaginationParams) => Promise<OffsetPage<T>>,\n params: { limit?: number; maxPages?: number } = {},\n): AsyncGenerator<T, void, undefined> {\n const limit = params.limit;\n const maxPages = params.maxPages ?? Infinity;\n let offset = 0;\n let pages = 0;\n while (pages < maxPages) {\n const page = await fetchPage({ offset, limit });\n for (const item of page.items) {\n yield item;\n }\n pages += 1;\n if (page.items.length === 0) return;\n if (page.pageCount > 0 && pages >= page.pageCount) return;\n // Step by the server-echoed limit (when present) so requests stay aligned with how\n // the server batches; fall back to the caller's `limit` or the observed batch size.\n offset += page.limit || limit || page.items.length;\n }\n}\n","export type LoncaErrorCode =\n | 'AUTH_FAILED'\n | 'RATE_LIMITED'\n | 'VALIDATION_FAILED'\n | 'NOT_FOUND'\n | 'SERVER_ERROR'\n | 'NETWORK_ERROR'\n | 'TIMEOUT'\n | 'UNKNOWN';\n\nexport interface LoncaErrorOptions {\n code: LoncaErrorCode;\n message: string;\n cause?: unknown;\n retryable?: boolean;\n status?: number;\n retryAfterMs?: number;\n data?: Record<string, unknown>;\n}\n\n/**\n * Root of the Lonca error hierarchy.\n *\n * Carries a tagged `code` and a `retryable` boolean so retry helpers\n * can decide what to do without sniffing message strings.\n */\nexport class LoncaError extends Error {\n readonly code: LoncaErrorCode;\n readonly retryable: boolean;\n readonly status?: number;\n readonly retryAfterMs?: number;\n readonly data?: Record<string, unknown>;\n\n constructor(opts: LoncaErrorOptions) {\n super(opts.message, opts.cause !== undefined ? { cause: opts.cause } : undefined);\n this.name = 'LoncaError';\n this.code = opts.code;\n this.retryable = opts.retryable ?? false;\n this.status = opts.status;\n this.retryAfterMs = opts.retryAfterMs;\n this.data = opts.data;\n }\n}\n\ntype SubclassOptions = Omit<LoncaErrorOptions, 'code' | 'retryable'>;\n\nexport class AuthError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'AUTH_FAILED', retryable: false });\n this.name = 'AuthError';\n }\n}\n\nexport class RateLimitError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'RATE_LIMITED', retryable: true });\n this.name = 'RateLimitError';\n }\n}\n\nexport class ValidationError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'VALIDATION_FAILED', retryable: false });\n this.name = 'ValidationError';\n }\n}\n\nexport class NotFoundError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'NOT_FOUND', retryable: false });\n this.name = 'NotFoundError';\n }\n}\n\nexport class ServerError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'SERVER_ERROR', retryable: true });\n this.name = 'ServerError';\n }\n}\n\nexport class NetworkError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'NETWORK_ERROR', retryable: true });\n this.name = 'NetworkError';\n }\n}\n\nexport class TimeoutError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'TIMEOUT', retryable: true });\n this.name = 'TimeoutError';\n }\n}\n\nexport function isLoncaError(value: unknown): value is LoncaError {\n return value instanceof LoncaError;\n}\n\nexport function isRetryableError(value: unknown): boolean {\n return isLoncaError(value) && value.retryable;\n}\n","import { isLoncaError, isRetryableError } from './errors.js';\n\nexport interface RetryOptions {\n /** Total number of attempts including the initial call. Defaults to 3. */\n maxAttempts?: number;\n /** Initial delay before the second attempt, in ms. Defaults to 200. */\n baseDelayMs?: number;\n /** Upper bound on delay between any two attempts, in ms. Defaults to 10_000. */\n maxDelayMs?: number;\n /** Add up to 50% random jitter on each delay. Defaults to true. */\n jitter?: boolean;\n /** Override the default predicate. Default treats `retryable` LoncaErrors as retryable. */\n isRetryable?: (err: unknown) => boolean;\n /** Called before each retry sleep. Useful for structured logging. */\n onRetry?: (err: unknown, attempt: number, nextDelayMs: number) => void;\n /** Abort all pending sleeps and bubble the signal's reason. */\n signal?: AbortSignal;\n}\n\nconst DEFAULTS = {\n maxAttempts: 3,\n baseDelayMs: 200,\n maxDelayMs: 10_000,\n jitter: true,\n} as const;\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(signal.reason);\n return;\n }\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n 'abort',\n () => {\n clearTimeout(timer);\n reject(signal.reason);\n },\n { once: true },\n );\n });\n}\n\nexport async function retry<T>(\n fn: (attempt: number) => Promise<T>,\n options: RetryOptions = {},\n): Promise<T> {\n const maxAttempts = options.maxAttempts ?? DEFAULTS.maxAttempts;\n const baseDelayMs = options.baseDelayMs ?? DEFAULTS.baseDelayMs;\n const maxDelayMs = options.maxDelayMs ?? DEFAULTS.maxDelayMs;\n const jitter = options.jitter ?? DEFAULTS.jitter;\n const isRetryable = options.isRetryable ?? isRetryableError;\n\n let lastErr: unknown;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n return await fn(attempt);\n } catch (err) {\n lastErr = err;\n if (attempt === maxAttempts || !isRetryable(err)) {\n throw err;\n }\n\n const retryAfterMs = isLoncaError(err) ? err.retryAfterMs : undefined;\n const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));\n const base = retryAfterMs ?? exponential;\n const jitterMs = jitter ? Math.random() * base * 0.5 : 0;\n const delay = Math.min(maxDelayMs, base + jitterMs);\n\n options.onRetry?.(err, attempt, delay);\n await sleep(delay, options.signal);\n }\n }\n throw lastErr;\n}\n","export type LogLevel = 'debug' | 'info' | 'warn' | 'error';\n\nexport type LogContext = Record<string, unknown>;\n\nexport interface Logger {\n debug(message: string, context?: LogContext): void;\n info(message: string, context?: LogContext): void;\n warn(message: string, context?: LogContext): void;\n error(message: string, context?: LogContext): void;\n /** Create a child logger that merges `bindings` into every log entry. */\n child(bindings: LogContext): Logger;\n}\n\nexport const noopLogger: Logger = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n child: () => noopLogger,\n};\n\n/**\n * Minimal JSON-line logger to stdout/stderr. Suitable for development and\n * containerized environments. For production prefer wiring a real logger\n * (pino, winston) by implementing the `Logger` interface.\n */\nexport function consoleLogger(bindings: LogContext = {}): Logger {\n const emit = (level: LogLevel, message: string, context?: LogContext): void => {\n const entry = {\n level,\n message,\n timestamp: new Date().toISOString(),\n ...bindings,\n ...context,\n };\n const line = JSON.stringify(entry);\n if (level === 'error' || level === 'warn') {\n console.error(line);\n } else {\n console.log(line);\n }\n };\n return {\n debug: (m, c) => emit('debug', m, c),\n info: (m, c) => emit('info', m, c),\n warn: (m, c) => emit('warn', m, c),\n error: (m, c) => emit('error', m, c),\n child: (b) => consoleLogger({ ...bindings, ...b }),\n };\n}\n","export interface RateLimiterOptions {\n /** Maximum number of tokens the bucket holds. Each `acquire()` consumes 1. */\n capacity: number;\n /** Time window in milliseconds over which `capacity` tokens are refilled. */\n intervalMs: number;\n}\n\n/**\n * Token-bucket rate limiter. `acquire()` returns immediately when a token\n * is available, otherwise it sleeps until enough tokens have been refilled.\n *\n * @example\n * const limiter = new TokenBucketRateLimiter({ capacity: 50, intervalMs: 60_000 });\n * for (const item of items) {\n * await limiter.acquire();\n * await fetchSomething(item);\n * }\n */\nexport class TokenBucketRateLimiter {\n private tokens: number;\n private lastRefillAt: number;\n private readonly capacity: number;\n private readonly intervalMs: number;\n\n constructor(options: RateLimiterOptions) {\n if (options.capacity <= 0 || options.intervalMs <= 0) {\n throw new RangeError('capacity and intervalMs must be > 0');\n }\n this.capacity = options.capacity;\n this.intervalMs = options.intervalMs;\n this.tokens = options.capacity;\n this.lastRefillAt = Date.now();\n }\n\n /**\n * Refill tokens based on elapsed time, then return ms to wait until the next\n * token is available (0 if one is available right now).\n */\n private timeUntilNextToken(): number {\n const now = Date.now();\n const elapsed = now - this.lastRefillAt;\n if (elapsed > 0) {\n const refill = (elapsed / this.intervalMs) * this.capacity;\n this.tokens = Math.min(this.capacity, this.tokens + refill);\n this.lastRefillAt = now;\n }\n if (this.tokens >= 1) return 0;\n const needed = 1 - this.tokens;\n return Math.ceil((needed / this.capacity) * this.intervalMs);\n }\n\n async acquire(signal?: AbortSignal): Promise<void> {\n for (;;) {\n if (signal?.aborted) throw signal.reason as Error;\n const wait = this.timeUntilNextToken();\n if (wait === 0) {\n this.tokens -= 1;\n return;\n }\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(resolve, wait);\n signal?.addEventListener(\n 'abort',\n () => {\n clearTimeout(timer);\n reject(signal.reason);\n },\n { once: true },\n );\n });\n }\n }\n\n /** Snapshot of available tokens (refills the bucket as a side effect). */\n available(): number {\n this.timeUntilNextToken();\n return this.tokens;\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/currency.ts","../src/money.ts","../src/pagination.ts","../src/order-status.ts","../src/errors.ts","../src/retry.ts","../src/logger.ts","../src/rate-limiter.ts","../src/transport.ts"],"names":["randomUUID"],"mappings":"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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
@@ -23,7 +23,30 @@ interface Money {
23
23
  currency: Currency;
24
24
  }
25
25
  declare function money(amount: number, currency: Currency): Money;
26
+ /**
27
+ * Convert a major-unit amount (e.g. lira, dollars) to {@link Money} in integer
28
+ * minor units. This is the canonical lira→kuruş converter — prefer it over a
29
+ * hand-rolled `Math.round(x * 100)`, which scatters rounding logic and risks
30
+ * float drift.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import { moneyFromMajor, TRY } from '@lonca/core';
35
+ * // A marketplace price of 199.90 ₺:
36
+ * moneyFromMajor(199.9, TRY); // { amount: 19990, currency: 'TRY' } — kuruş
37
+ * ```
38
+ */
26
39
  declare function moneyFromMajor(major: number, currency: Currency, minorScale?: number): Money;
40
+ /**
41
+ * Convert {@link Money} (integer minor units) back to a major-unit number —
42
+ * the canonical kuruş→lira converter.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * import { moneyToMajor, TRY } from '@lonca/core';
47
+ * moneyToMajor({ amount: 19990, currency: TRY }); // 199.9 (lira)
48
+ * ```
49
+ */
27
50
  declare function moneyToMajor(value: Money, minorScale?: number): number;
28
51
  declare function isSameCurrency(a: Money, b: Money): boolean;
29
52
  declare function addMoney(a: Money, b: Money): Money;
@@ -92,7 +115,60 @@ declare function paginateOffset<T>(fetchPage: (params: OffsetPaginationParams) =
92
115
  maxPages?: number;
93
116
  }): AsyncGenerator<T, void, undefined>;
94
117
 
118
+ /**
119
+ * The closed, marketplace-agnostic order/shipment status vocabulary that every
120
+ * SDK normalizes its raw statuses into.
121
+ *
122
+ * This union is intentionally **closed** (no `(string & {})` catch-all): a value
123
+ * a marketplace SDK can't map lands on `'unknown'` at runtime via
124
+ * {@link createStatusNormalizer}, surfaced by `mapped: false` — never silently
125
+ * coerced to a valid-looking default.
126
+ */
127
+ type NormalizedOrderStatus = 'created' | 'picking' | 'invoiced' | 'shipped' | 'delivered' | 'cancelled' | 'returned' | 'unknown';
128
+ /**
129
+ * Result of normalizing a raw marketplace status. The raw value is always
130
+ * preserved, and `mapped` makes an unrecognized status visible at runtime
131
+ * instead of letting it disappear into a default.
132
+ */
133
+ interface NormalizedStatusResult {
134
+ /** Mapped normalized status, or `'unknown'` when the raw value wasn't recognized. */
135
+ normalized: NormalizedOrderStatus;
136
+ /** The original raw status string from the marketplace, preserved verbatim. */
137
+ raw: string;
138
+ /** `false` when the raw value had no mapping (normalized fell back to `'unknown'`). */
139
+ mapped: boolean;
140
+ }
141
+ /**
142
+ * Build a status normalizer from an SDK-owned, **exhaustive** map of that
143
+ * marketplace's known statuses. Because the map key type is a closed union,
144
+ * forgetting to map a newly added known status is a compile-time error in the
145
+ * SDK — while an unknown value arriving on the wire stays safe at runtime
146
+ * (`{ normalized: 'unknown', mapped: false }`).
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * const statusMap = { Created: 'created', Shipped: 'shipped' } as const;
151
+ * const normalizeStatus = createStatusNormalizer(statusMap);
152
+ * normalizeStatus('Shipped'); // { normalized: 'shipped', raw: 'Shipped', mapped: true }
153
+ * normalizeStatus('Martian'); // { normalized: 'unknown', raw: 'Martian', mapped: false }
154
+ * ```
155
+ */
156
+ declare function createStatusNormalizer<Known extends string>(map: Record<Known, NormalizedOrderStatus>): (raw: string) => NormalizedStatusResult;
157
+
95
158
  type LoncaErrorCode = 'AUTH_FAILED' | 'RATE_LIMITED' | 'VALIDATION_FAILED' | 'NOT_FOUND' | 'SERVER_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT' | 'UNKNOWN';
159
+ /**
160
+ * A single normalized, field-level error detail extracted from a marketplace's
161
+ * raw error body. Each SDK maps its own (inconsistent) error JSON into this
162
+ * shape so consumers don't have to sniff marketplace-specific payloads.
163
+ */
164
+ interface LoncaErrorIssue {
165
+ /** The offending field/path, when the marketplace reports one. */
166
+ field?: string;
167
+ /** Marketplace-specific error code, when present. */
168
+ code?: string;
169
+ /** Human-readable message — always present. */
170
+ message: string;
171
+ }
96
172
  interface LoncaErrorOptions {
97
173
  code: LoncaErrorCode;
98
174
  message: string;
@@ -101,6 +177,8 @@ interface LoncaErrorOptions {
101
177
  status?: number;
102
178
  retryAfterMs?: number;
103
179
  data?: Record<string, unknown>;
180
+ /** Normalized, field-level error details. Defaults to `[]` when omitted. */
181
+ issues?: LoncaErrorIssue[];
104
182
  }
105
183
  /**
106
184
  * Root of the Lonca error hierarchy.
@@ -114,6 +192,12 @@ declare class LoncaError extends Error {
114
192
  readonly status?: number;
115
193
  readonly retryAfterMs?: number;
116
194
  readonly data?: Record<string, unknown>;
195
+ /**
196
+ * Normalized, field-level error details mapped from the marketplace's raw
197
+ * error body by the SDK. Always an array (never `undefined`) so callers can
198
+ * iterate without a presence check; empty when nothing was parseable.
199
+ */
200
+ readonly issues: LoncaErrorIssue[];
117
201
  constructor(opts: LoncaErrorOptions);
118
202
  }
119
203
  type SubclassOptions = Omit<LoncaErrorOptions, 'code' | 'retryable'>;
@@ -140,6 +224,30 @@ declare class TimeoutError extends LoncaError {
140
224
  }
141
225
  declare function isLoncaError(value: unknown): value is LoncaError;
142
226
  declare function isRetryableError(value: unknown): boolean;
227
+ /**
228
+ * Retry predicate for **non-idempotent** requests (POST/PUT/DELETE/PATCH).
229
+ *
230
+ * Only `RateLimitError` (HTTP 429) is replayed, because a 429 means the server
231
+ * rejected the request *before* processing it, so a retry cannot duplicate a
232
+ * side-effect. Ambiguous failures — 5xx, network drops, and client-side
233
+ * timeouts — are deliberately NOT retried here: the write may already have
234
+ * taken effect server-side, and a blind replay would duplicate it (double
235
+ * order split, double cancel, re-pushed price/stock batch).
236
+ *
237
+ * Callers that have made a write idempotent (e.g. by sending an idempotency
238
+ * key) should opt back into full retries via `isRetryableError` instead.
239
+ */
240
+ declare function isRetryableIdempotentOnly(value: unknown): boolean;
241
+ /**
242
+ * Parse a `Retry-After` header value (delta-seconds OR an HTTP-date) into
243
+ * milliseconds. Returns `undefined` when the header is absent, unparseable, or
244
+ * non-positive.
245
+ *
246
+ * A non-positive value is treated as "no hint" rather than `0`: a literal
247
+ * `Retry-After: 0` must not collapse exponential backoff into a zero-delay
248
+ * retry storm against the very endpoint that is rate-limiting the client.
249
+ */
250
+ declare function parseRetryAfter(header: string | null): number | undefined;
143
251
 
144
252
  interface RetryOptions {
145
253
  /** Total number of attempts including the initial call. Defaults to 3. */
@@ -210,4 +318,66 @@ declare class TokenBucketRateLimiter {
210
318
  available(): number;
211
319
  }
212
320
 
213
- export { AuthError, type Currency, type CursorPage, type CursorPaginationParams, EUR, GBP, type LogContext, type LogLevel, type Logger, LoncaError, type LoncaErrorCode, type LoncaErrorOptions, type Money, NetworkError, NotFoundError, type OffsetPage, type OffsetPaginationParams, RateLimitError, type RateLimiterOptions, type RetryOptions, ServerError, TRY, TimeoutError, TokenBucketRateLimiter, USD, ValidationError, addMoney, consoleLogger, isLoncaError, isRetryableError, isSameCurrency, isValidCurrencyCode, money, moneyFromMajor, moneyToMajor, noopLogger, paginate, paginateOffset, retry, subMoney };
321
+ /**
322
+ * The request lifecycle shared by every marketplace SDK transport: acquire a
323
+ * rate-limit token, build the URL/headers, fire `fetch` under a composed
324
+ * timeout signal, map non-2xx responses to `LoncaError`s, and replay through
325
+ * {@link retry}. Only the marketplace-specific seams (URL building, headers,
326
+ * status→error mapping, log labels) are injected via {@link RequesterConfig},
327
+ * so the loop itself lives in one place instead of being copy-pasted — and
328
+ * drifting — across SDKs.
329
+ */
330
+ /** The request fields the shared lifecycle reads. SDKs extend this with their own (e.g. `path`, `service`, `query`). */
331
+ interface BaseRequestOptions {
332
+ /** HTTP method. `GET` is treated as idempotent for retry purposes. */
333
+ method: string;
334
+ /** Request body. Serialized as JSON unless it is a `FormData` (sent as multipart). Skipped for `GET`. */
335
+ body?: unknown;
336
+ /** Caller abort signal, composed with the per-request timeout. */
337
+ signal?: AbortSignal;
338
+ /** Per-endpoint rate limiter; one token is acquired before each attempt. */
339
+ rateLimiter?: TokenBucketRateLimiter;
340
+ /**
341
+ * Whether this request is safe to auto-replay on an ambiguous transient
342
+ * failure (5xx / network drop / client timeout). `GET` is always idempotent.
343
+ * For writes this defaults to `false`: a timed-out or 5xx write may already
344
+ * have committed server-side, so only a `429` (provably rejected before
345
+ * processing) is retried. Set `true` to opt a write back into full retries.
346
+ */
347
+ idempotent?: boolean;
348
+ /** Extra per-request headers merged over the default header set (caller headers win). */
349
+ headers?: Record<string, string>;
350
+ }
351
+ /** Marketplace-specific seams plus runtime dependencies for {@link createRequester}. */
352
+ interface RequesterConfig<O extends BaseRequestOptions> {
353
+ /** Underlying `fetch` (tests inject a mock). */
354
+ fetch: typeof fetch;
355
+ /** Structured logger. Defaults to a no-op. */
356
+ logger?: Logger;
357
+ /** Per-request timeout in ms. */
358
+ timeoutMs: number;
359
+ /** Human-readable marketplace name for error messages, e.g. `'Trendyol'`. */
360
+ label: string;
361
+ /** Log-event prefix, e.g. `'trendyol'` → `trendyol.request` / `trendyol.error` / `trendyol.retry`. */
362
+ logPrefix: string;
363
+ /** Build the absolute request URL from the SDK's options. */
364
+ buildUrl(opts: O): string;
365
+ /** Build the default header set; receives the generated correlation id. */
366
+ buildHeaders(correlationId: string): Record<string, string>;
367
+ /** Map a non-2xx response to a `LoncaError`. */
368
+ mapHttpError(status: number, body: unknown, retryAfterMs?: number): LoncaError;
369
+ /** Optional extra structured fields merged into request/error log lines (e.g. `{ service }`). */
370
+ logFields?(opts: O): Record<string, unknown>;
371
+ }
372
+ /**
373
+ * Build a `request<T>(opts)` function that runs the shared transport lifecycle
374
+ * for a given marketplace. Each SDK calls this once (per transport instance)
375
+ * and exposes the returned function from its own transport class.
376
+ */
377
+ declare function createRequester<O extends BaseRequestOptions>(config: RequesterConfig<O>): <T>(opts: O) => Promise<T>;
378
+ /** Compose a caller signal with a per-request timeout signal. */
379
+ declare function composeSignal(external: AbortSignal | undefined, timeoutMs: number): AbortSignal;
380
+ /** Read a response body as JSON, falling back to raw text (then `undefined` when empty). */
381
+ declare function safeJson(response: Response): Promise<unknown>;
382
+
383
+ export { AuthError, type BaseRequestOptions, type Currency, type CursorPage, type CursorPaginationParams, EUR, GBP, type LogContext, type LogLevel, type Logger, LoncaError, type LoncaErrorCode, type LoncaErrorIssue, type LoncaErrorOptions, type Money, NetworkError, type NormalizedOrderStatus, type NormalizedStatusResult, NotFoundError, type OffsetPage, type OffsetPaginationParams, RateLimitError, type RateLimiterOptions, type RequesterConfig, type RetryOptions, ServerError, TRY, TimeoutError, TokenBucketRateLimiter, USD, ValidationError, addMoney, composeSignal, consoleLogger, createRequester, createStatusNormalizer, isLoncaError, isRetryableError, isRetryableIdempotentOnly, isSameCurrency, isValidCurrencyCode, money, moneyFromMajor, moneyToMajor, noopLogger, paginate, paginateOffset, parseRetryAfter, retry, safeJson, subMoney };
package/dist/index.d.ts CHANGED
@@ -23,7 +23,30 @@ interface Money {
23
23
  currency: Currency;
24
24
  }
25
25
  declare function money(amount: number, currency: Currency): Money;
26
+ /**
27
+ * Convert a major-unit amount (e.g. lira, dollars) to {@link Money} in integer
28
+ * minor units. This is the canonical lira→kuruş converter — prefer it over a
29
+ * hand-rolled `Math.round(x * 100)`, which scatters rounding logic and risks
30
+ * float drift.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import { moneyFromMajor, TRY } from '@lonca/core';
35
+ * // A marketplace price of 199.90 ₺:
36
+ * moneyFromMajor(199.9, TRY); // { amount: 19990, currency: 'TRY' } — kuruş
37
+ * ```
38
+ */
26
39
  declare function moneyFromMajor(major: number, currency: Currency, minorScale?: number): Money;
40
+ /**
41
+ * Convert {@link Money} (integer minor units) back to a major-unit number —
42
+ * the canonical kuruş→lira converter.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * import { moneyToMajor, TRY } from '@lonca/core';
47
+ * moneyToMajor({ amount: 19990, currency: TRY }); // 199.9 (lira)
48
+ * ```
49
+ */
27
50
  declare function moneyToMajor(value: Money, minorScale?: number): number;
28
51
  declare function isSameCurrency(a: Money, b: Money): boolean;
29
52
  declare function addMoney(a: Money, b: Money): Money;
@@ -92,7 +115,60 @@ declare function paginateOffset<T>(fetchPage: (params: OffsetPaginationParams) =
92
115
  maxPages?: number;
93
116
  }): AsyncGenerator<T, void, undefined>;
94
117
 
118
+ /**
119
+ * The closed, marketplace-agnostic order/shipment status vocabulary that every
120
+ * SDK normalizes its raw statuses into.
121
+ *
122
+ * This union is intentionally **closed** (no `(string & {})` catch-all): a value
123
+ * a marketplace SDK can't map lands on `'unknown'` at runtime via
124
+ * {@link createStatusNormalizer}, surfaced by `mapped: false` — never silently
125
+ * coerced to a valid-looking default.
126
+ */
127
+ type NormalizedOrderStatus = 'created' | 'picking' | 'invoiced' | 'shipped' | 'delivered' | 'cancelled' | 'returned' | 'unknown';
128
+ /**
129
+ * Result of normalizing a raw marketplace status. The raw value is always
130
+ * preserved, and `mapped` makes an unrecognized status visible at runtime
131
+ * instead of letting it disappear into a default.
132
+ */
133
+ interface NormalizedStatusResult {
134
+ /** Mapped normalized status, or `'unknown'` when the raw value wasn't recognized. */
135
+ normalized: NormalizedOrderStatus;
136
+ /** The original raw status string from the marketplace, preserved verbatim. */
137
+ raw: string;
138
+ /** `false` when the raw value had no mapping (normalized fell back to `'unknown'`). */
139
+ mapped: boolean;
140
+ }
141
+ /**
142
+ * Build a status normalizer from an SDK-owned, **exhaustive** map of that
143
+ * marketplace's known statuses. Because the map key type is a closed union,
144
+ * forgetting to map a newly added known status is a compile-time error in the
145
+ * SDK — while an unknown value arriving on the wire stays safe at runtime
146
+ * (`{ normalized: 'unknown', mapped: false }`).
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * const statusMap = { Created: 'created', Shipped: 'shipped' } as const;
151
+ * const normalizeStatus = createStatusNormalizer(statusMap);
152
+ * normalizeStatus('Shipped'); // { normalized: 'shipped', raw: 'Shipped', mapped: true }
153
+ * normalizeStatus('Martian'); // { normalized: 'unknown', raw: 'Martian', mapped: false }
154
+ * ```
155
+ */
156
+ declare function createStatusNormalizer<Known extends string>(map: Record<Known, NormalizedOrderStatus>): (raw: string) => NormalizedStatusResult;
157
+
95
158
  type LoncaErrorCode = 'AUTH_FAILED' | 'RATE_LIMITED' | 'VALIDATION_FAILED' | 'NOT_FOUND' | 'SERVER_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT' | 'UNKNOWN';
159
+ /**
160
+ * A single normalized, field-level error detail extracted from a marketplace's
161
+ * raw error body. Each SDK maps its own (inconsistent) error JSON into this
162
+ * shape so consumers don't have to sniff marketplace-specific payloads.
163
+ */
164
+ interface LoncaErrorIssue {
165
+ /** The offending field/path, when the marketplace reports one. */
166
+ field?: string;
167
+ /** Marketplace-specific error code, when present. */
168
+ code?: string;
169
+ /** Human-readable message — always present. */
170
+ message: string;
171
+ }
96
172
  interface LoncaErrorOptions {
97
173
  code: LoncaErrorCode;
98
174
  message: string;
@@ -101,6 +177,8 @@ interface LoncaErrorOptions {
101
177
  status?: number;
102
178
  retryAfterMs?: number;
103
179
  data?: Record<string, unknown>;
180
+ /** Normalized, field-level error details. Defaults to `[]` when omitted. */
181
+ issues?: LoncaErrorIssue[];
104
182
  }
105
183
  /**
106
184
  * Root of the Lonca error hierarchy.
@@ -114,6 +192,12 @@ declare class LoncaError extends Error {
114
192
  readonly status?: number;
115
193
  readonly retryAfterMs?: number;
116
194
  readonly data?: Record<string, unknown>;
195
+ /**
196
+ * Normalized, field-level error details mapped from the marketplace's raw
197
+ * error body by the SDK. Always an array (never `undefined`) so callers can
198
+ * iterate without a presence check; empty when nothing was parseable.
199
+ */
200
+ readonly issues: LoncaErrorIssue[];
117
201
  constructor(opts: LoncaErrorOptions);
118
202
  }
119
203
  type SubclassOptions = Omit<LoncaErrorOptions, 'code' | 'retryable'>;
@@ -140,6 +224,30 @@ declare class TimeoutError extends LoncaError {
140
224
  }
141
225
  declare function isLoncaError(value: unknown): value is LoncaError;
142
226
  declare function isRetryableError(value: unknown): boolean;
227
+ /**
228
+ * Retry predicate for **non-idempotent** requests (POST/PUT/DELETE/PATCH).
229
+ *
230
+ * Only `RateLimitError` (HTTP 429) is replayed, because a 429 means the server
231
+ * rejected the request *before* processing it, so a retry cannot duplicate a
232
+ * side-effect. Ambiguous failures — 5xx, network drops, and client-side
233
+ * timeouts — are deliberately NOT retried here: the write may already have
234
+ * taken effect server-side, and a blind replay would duplicate it (double
235
+ * order split, double cancel, re-pushed price/stock batch).
236
+ *
237
+ * Callers that have made a write idempotent (e.g. by sending an idempotency
238
+ * key) should opt back into full retries via `isRetryableError` instead.
239
+ */
240
+ declare function isRetryableIdempotentOnly(value: unknown): boolean;
241
+ /**
242
+ * Parse a `Retry-After` header value (delta-seconds OR an HTTP-date) into
243
+ * milliseconds. Returns `undefined` when the header is absent, unparseable, or
244
+ * non-positive.
245
+ *
246
+ * A non-positive value is treated as "no hint" rather than `0`: a literal
247
+ * `Retry-After: 0` must not collapse exponential backoff into a zero-delay
248
+ * retry storm against the very endpoint that is rate-limiting the client.
249
+ */
250
+ declare function parseRetryAfter(header: string | null): number | undefined;
143
251
 
144
252
  interface RetryOptions {
145
253
  /** Total number of attempts including the initial call. Defaults to 3. */
@@ -210,4 +318,66 @@ declare class TokenBucketRateLimiter {
210
318
  available(): number;
211
319
  }
212
320
 
213
- export { AuthError, type Currency, type CursorPage, type CursorPaginationParams, EUR, GBP, type LogContext, type LogLevel, type Logger, LoncaError, type LoncaErrorCode, type LoncaErrorOptions, type Money, NetworkError, NotFoundError, type OffsetPage, type OffsetPaginationParams, RateLimitError, type RateLimiterOptions, type RetryOptions, ServerError, TRY, TimeoutError, TokenBucketRateLimiter, USD, ValidationError, addMoney, consoleLogger, isLoncaError, isRetryableError, isSameCurrency, isValidCurrencyCode, money, moneyFromMajor, moneyToMajor, noopLogger, paginate, paginateOffset, retry, subMoney };
321
+ /**
322
+ * The request lifecycle shared by every marketplace SDK transport: acquire a
323
+ * rate-limit token, build the URL/headers, fire `fetch` under a composed
324
+ * timeout signal, map non-2xx responses to `LoncaError`s, and replay through
325
+ * {@link retry}. Only the marketplace-specific seams (URL building, headers,
326
+ * status→error mapping, log labels) are injected via {@link RequesterConfig},
327
+ * so the loop itself lives in one place instead of being copy-pasted — and
328
+ * drifting — across SDKs.
329
+ */
330
+ /** The request fields the shared lifecycle reads. SDKs extend this with their own (e.g. `path`, `service`, `query`). */
331
+ interface BaseRequestOptions {
332
+ /** HTTP method. `GET` is treated as idempotent for retry purposes. */
333
+ method: string;
334
+ /** Request body. Serialized as JSON unless it is a `FormData` (sent as multipart). Skipped for `GET`. */
335
+ body?: unknown;
336
+ /** Caller abort signal, composed with the per-request timeout. */
337
+ signal?: AbortSignal;
338
+ /** Per-endpoint rate limiter; one token is acquired before each attempt. */
339
+ rateLimiter?: TokenBucketRateLimiter;
340
+ /**
341
+ * Whether this request is safe to auto-replay on an ambiguous transient
342
+ * failure (5xx / network drop / client timeout). `GET` is always idempotent.
343
+ * For writes this defaults to `false`: a timed-out or 5xx write may already
344
+ * have committed server-side, so only a `429` (provably rejected before
345
+ * processing) is retried. Set `true` to opt a write back into full retries.
346
+ */
347
+ idempotent?: boolean;
348
+ /** Extra per-request headers merged over the default header set (caller headers win). */
349
+ headers?: Record<string, string>;
350
+ }
351
+ /** Marketplace-specific seams plus runtime dependencies for {@link createRequester}. */
352
+ interface RequesterConfig<O extends BaseRequestOptions> {
353
+ /** Underlying `fetch` (tests inject a mock). */
354
+ fetch: typeof fetch;
355
+ /** Structured logger. Defaults to a no-op. */
356
+ logger?: Logger;
357
+ /** Per-request timeout in ms. */
358
+ timeoutMs: number;
359
+ /** Human-readable marketplace name for error messages, e.g. `'Trendyol'`. */
360
+ label: string;
361
+ /** Log-event prefix, e.g. `'trendyol'` → `trendyol.request` / `trendyol.error` / `trendyol.retry`. */
362
+ logPrefix: string;
363
+ /** Build the absolute request URL from the SDK's options. */
364
+ buildUrl(opts: O): string;
365
+ /** Build the default header set; receives the generated correlation id. */
366
+ buildHeaders(correlationId: string): Record<string, string>;
367
+ /** Map a non-2xx response to a `LoncaError`. */
368
+ mapHttpError(status: number, body: unknown, retryAfterMs?: number): LoncaError;
369
+ /** Optional extra structured fields merged into request/error log lines (e.g. `{ service }`). */
370
+ logFields?(opts: O): Record<string, unknown>;
371
+ }
372
+ /**
373
+ * Build a `request<T>(opts)` function that runs the shared transport lifecycle
374
+ * for a given marketplace. Each SDK calls this once (per transport instance)
375
+ * and exposes the returned function from its own transport class.
376
+ */
377
+ declare function createRequester<O extends BaseRequestOptions>(config: RequesterConfig<O>): <T>(opts: O) => Promise<T>;
378
+ /** Compose a caller signal with a per-request timeout signal. */
379
+ declare function composeSignal(external: AbortSignal | undefined, timeoutMs: number): AbortSignal;
380
+ /** Read a response body as JSON, falling back to raw text (then `undefined` when empty). */
381
+ declare function safeJson(response: Response): Promise<unknown>;
382
+
383
+ export { AuthError, type BaseRequestOptions, type Currency, type CursorPage, type CursorPaginationParams, EUR, GBP, type LogContext, type LogLevel, type Logger, LoncaError, type LoncaErrorCode, type LoncaErrorIssue, type LoncaErrorOptions, type Money, NetworkError, type NormalizedOrderStatus, type NormalizedStatusResult, NotFoundError, type OffsetPage, type OffsetPaginationParams, RateLimitError, type RateLimiterOptions, type RequesterConfig, type RetryOptions, ServerError, TRY, TimeoutError, TokenBucketRateLimiter, USD, ValidationError, addMoney, composeSignal, consoleLogger, createRequester, createStatusNormalizer, isLoncaError, isRetryableError, isRetryableIdempotentOnly, isSameCurrency, isValidCurrencyCode, money, moneyFromMajor, moneyToMajor, noopLogger, paginate, paginateOffset, parseRetryAfter, retry, safeJson, subMoney };
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { randomUUID } from 'crypto';
2
+
1
3
  // src/currency.ts
2
4
  var TRY = "TRY";
3
5
  var USD = "USD";
@@ -69,6 +71,15 @@ async function* paginateOffset(fetchPage, params = {}) {
69
71
  }
70
72
  }
71
73
 
74
+ // src/order-status.ts
75
+ function createStatusNormalizer(map) {
76
+ const lookup = map;
77
+ return (raw) => {
78
+ const normalized = lookup[raw];
79
+ return normalized !== void 0 ? { normalized, raw, mapped: true } : { normalized: "unknown", raw, mapped: false };
80
+ };
81
+ }
82
+
72
83
  // src/errors.ts
73
84
  var LoncaError = class extends Error {
74
85
  code;
@@ -76,6 +87,12 @@ var LoncaError = class extends Error {
76
87
  status;
77
88
  retryAfterMs;
78
89
  data;
90
+ /**
91
+ * Normalized, field-level error details mapped from the marketplace's raw
92
+ * error body by the SDK. Always an array (never `undefined`) so callers can
93
+ * iterate without a presence check; empty when nothing was parseable.
94
+ */
95
+ issues;
79
96
  constructor(opts) {
80
97
  super(opts.message, opts.cause !== void 0 ? { cause: opts.cause } : void 0);
81
98
  this.name = "LoncaError";
@@ -84,6 +101,7 @@ var LoncaError = class extends Error {
84
101
  this.status = opts.status;
85
102
  this.retryAfterMs = opts.retryAfterMs;
86
103
  this.data = opts.data;
104
+ this.issues = opts.issues ?? [];
87
105
  }
88
106
  };
89
107
  var AuthError = class extends LoncaError {
@@ -134,6 +152,20 @@ function isLoncaError(value) {
134
152
  function isRetryableError(value) {
135
153
  return isLoncaError(value) && value.retryable;
136
154
  }
155
+ function isRetryableIdempotentOnly(value) {
156
+ return isLoncaError(value) && value.retryable && value.code === "RATE_LIMITED";
157
+ }
158
+ function parseRetryAfter(header) {
159
+ if (!header) return void 0;
160
+ const seconds = Number(header);
161
+ if (!Number.isNaN(seconds)) return seconds > 0 ? seconds * 1e3 : void 0;
162
+ const epoch = Date.parse(header);
163
+ if (!Number.isNaN(epoch)) {
164
+ const delta = epoch - Date.now();
165
+ return delta > 0 ? delta : void 0;
166
+ }
167
+ return void 0;
168
+ }
137
169
 
138
170
  // src/retry.ts
139
171
  var DEFAULTS = {
@@ -176,7 +208,7 @@ async function retry(fn, options = {}) {
176
208
  }
177
209
  const retryAfterMs = isLoncaError(err) ? err.retryAfterMs : void 0;
178
210
  const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
179
- const base = retryAfterMs ?? exponential;
211
+ const base = retryAfterMs && retryAfterMs > 0 ? retryAfterMs : exponential;
180
212
  const jitterMs = jitter ? Math.random() * base * 0.5 : 0;
181
213
  const delay = Math.min(maxDelayMs, base + jitterMs);
182
214
  options.onRetry?.(err, attempt, delay);
@@ -281,7 +313,106 @@ var TokenBucketRateLimiter = class {
281
313
  return this.tokens;
282
314
  }
283
315
  };
316
+ function createRequester(config) {
317
+ const logger = config.logger ?? noopLogger;
318
+ return function request(opts) {
319
+ const safeToReplay = opts.method === "GET" || opts.idempotent === true;
320
+ const extra = () => config.logFields?.(opts) ?? {};
321
+ return retry(
322
+ async (attempt) => {
323
+ if (opts.rateLimiter) await opts.rateLimiter.acquire(opts.signal);
324
+ const correlationId = randomUUID();
325
+ const url = config.buildUrl(opts);
326
+ const headers = { ...config.buildHeaders(correlationId), ...opts.headers };
327
+ const init = {
328
+ method: opts.method,
329
+ headers,
330
+ signal: composeSignal(opts.signal, config.timeoutMs)
331
+ };
332
+ if (opts.body !== void 0 && opts.method !== "GET") {
333
+ if (opts.body instanceof FormData) {
334
+ init.body = opts.body;
335
+ delete headers["Content-Type"];
336
+ } else {
337
+ init.body = JSON.stringify(opts.body);
338
+ }
339
+ }
340
+ logger.debug(`${config.logPrefix}.request`, {
341
+ method: opts.method,
342
+ url,
343
+ correlationId,
344
+ attempt,
345
+ ...extra()
346
+ });
347
+ let response;
348
+ try {
349
+ response = await config.fetch(url, init);
350
+ } catch (err) {
351
+ if (err instanceof Error && err.name === "AbortError") {
352
+ throw new TimeoutError({
353
+ message: `${config.label} request timed out after ${config.timeoutMs}ms`,
354
+ cause: err
355
+ });
356
+ }
357
+ throw new NetworkError({ message: `${config.label} network failure`, cause: err });
358
+ }
359
+ if (!response.ok) {
360
+ const body = await safeJson(response);
361
+ const retryAfterMs = parseRetryAfter(response.headers.get("retry-after"));
362
+ const error = config.mapHttpError(response.status, body, retryAfterMs);
363
+ logger.warn(`${config.logPrefix}.error`, {
364
+ method: opts.method,
365
+ url,
366
+ correlationId,
367
+ status: response.status,
368
+ code: error.code,
369
+ retryable: error.retryable,
370
+ ...extra()
371
+ });
372
+ throw error;
373
+ }
374
+ logger.debug(`${config.logPrefix}.response`, {
375
+ correlationId,
376
+ status: response.status
377
+ });
378
+ if (response.status === 204) return void 0;
379
+ return await safeJson(response);
380
+ },
381
+ {
382
+ signal: opts.signal,
383
+ // Non-idempotent writes only retry rate-limit (429) errors; ambiguous
384
+ // 5xx/network/timeout failures are not replayed to avoid duplicate
385
+ // side-effects. GET (and explicitly idempotent requests) retry normally.
386
+ isRetryable: safeToReplay ? void 0 : isRetryableIdempotentOnly,
387
+ onRetry: (err, attempt, delay) => {
388
+ if (err instanceof LoncaError) {
389
+ logger.warn(`${config.logPrefix}.retry`, {
390
+ attempt,
391
+ delayMs: delay,
392
+ code: err.code,
393
+ status: err.status
394
+ });
395
+ }
396
+ }
397
+ }
398
+ );
399
+ };
400
+ }
401
+ function composeSignal(external, timeoutMs) {
402
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
403
+ if (!external) return timeoutSignal;
404
+ return AbortSignal.any([external, timeoutSignal]);
405
+ }
406
+ async function safeJson(response) {
407
+ const text = await response.text();
408
+ if (!text) return void 0;
409
+ try {
410
+ return JSON.parse(text);
411
+ } catch {
412
+ return text;
413
+ }
414
+ }
284
415
 
285
- export { AuthError, EUR, GBP, LoncaError, NetworkError, NotFoundError, RateLimitError, ServerError, TRY, TimeoutError, TokenBucketRateLimiter, USD, ValidationError, addMoney, consoleLogger, isLoncaError, isRetryableError, isSameCurrency, isValidCurrencyCode, money, moneyFromMajor, moneyToMajor, noopLogger, paginate, paginateOffset, retry, subMoney };
416
+ export { AuthError, EUR, GBP, LoncaError, NetworkError, NotFoundError, RateLimitError, ServerError, TRY, TimeoutError, TokenBucketRateLimiter, USD, ValidationError, addMoney, composeSignal, consoleLogger, createRequester, createStatusNormalizer, isLoncaError, isRetryableError, isRetryableIdempotentOnly, isSameCurrency, isValidCurrencyCode, money, moneyFromMajor, moneyToMajor, noopLogger, paginate, paginateOffset, parseRetryAfter, retry, safeJson, subMoney };
286
417
  //# sourceMappingURL=index.js.map
287
418
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/currency.ts","../src/money.ts","../src/pagination.ts","../src/errors.ts","../src/retry.ts","../src/logger.ts","../src/rate-limiter.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * ISO 4217 three-letter currency code.\n *\n * Typed as a string for flexibility — Trendyol and similar marketplaces may\n * expose currencies beyond the common set, so we don't lock down to a union.\n * Use `isValidCurrencyCode` for runtime validation.\n */\nexport type Currency = string;\n\nexport const TRY = 'TRY';\nexport const USD = 'USD';\nexport const EUR = 'EUR';\nexport const GBP = 'GBP';\n\nconst ISO_4217_RE = /^[A-Z]{3}$/;\n\nexport function isValidCurrencyCode(code: string): boolean {\n return ISO_4217_RE.test(code);\n}\n","import type { Currency } from './currency.js';\n\n/**\n * A monetary amount in integer minor units (e.g., kuruş for TRY, cents for USD).\n *\n * Using integers avoids floating-point surprises. Convert to/from major units\n * with `moneyFromMajor` and `moneyToMajor`.\n */\nexport interface Money {\n amount: number;\n currency: Currency;\n}\n\nexport function money(amount: number, currency: Currency): Money {\n if (!Number.isInteger(amount)) {\n throw new TypeError(`Money amount must be an integer minor unit, got ${amount}`);\n }\n return { amount, currency };\n}\n\nexport function moneyFromMajor(major: number, currency: Currency, minorScale = 2): Money {\n const factor = 10 ** minorScale;\n const amount = Math.round(major * factor);\n return money(amount, currency);\n}\n\nexport function moneyToMajor(value: Money, minorScale = 2): number {\n return value.amount / 10 ** minorScale;\n}\n\nexport function isSameCurrency(a: Money, b: Money): boolean {\n return a.currency === b.currency;\n}\n\nexport function addMoney(a: Money, b: Money): Money {\n if (!isSameCurrency(a, b)) {\n throw new TypeError(`Cannot add Money of different currencies: ${a.currency} vs ${b.currency}`);\n }\n return money(a.amount + b.amount, a.currency);\n}\n\nexport function subMoney(a: Money, b: Money): Money {\n if (!isSameCurrency(a, b)) {\n throw new TypeError(\n `Cannot subtract Money of different currencies: ${a.currency} vs ${b.currency}`,\n );\n }\n return money(a.amount - b.amount, a.currency);\n}\n","export interface CursorPaginationParams {\n /** Opaque cursor from previous response's `nextCursor`. Omit for the first page. */\n cursor?: string;\n /** Maximum items in the page. Marketplace SDKs may clamp this to their own bounds. */\n limit?: number;\n}\n\nexport interface CursorPage<T> {\n items: T[];\n /** Opaque cursor for the next page. Absent when there are no more pages. */\n nextCursor?: string;\n}\n\n/**\n * Drive a cursor-paginated endpoint as an async iterator.\n *\n * @example\n * for await (const order of paginate((p) => client.orders.list(p))) {\n * console.log(order.id);\n * }\n */\nexport async function* paginate<T>(\n fetchPage: (params: CursorPaginationParams) => Promise<CursorPage<T>>,\n params: Omit<CursorPaginationParams, 'cursor'> = {},\n): AsyncGenerator<T, void, undefined> {\n let cursor: string | undefined;\n do {\n const page = await fetchPage({ ...params, cursor });\n for (const item of page.items) {\n yield item;\n }\n cursor = page.nextCursor;\n } while (cursor);\n}\n\n// ─── Offset pagination ─────────────────────────────────────────────────────\n\nexport interface OffsetPaginationParams {\n /** Zero-based offset. Default depends on the SDK; usually `0`. */\n offset?: number;\n /** Maximum items in the page. */\n limit?: number;\n}\n\n/**\n * Offset-based pagination envelope returned by marketplaces that expose\n * `{ totalCount, limit, offset, pageCount, items[] }` instead of opaque\n * cursors. Hepsiburada's OMS uses this shape; some legacy Trendyol\n * endpoints do too. Distinct from `CursorPage<T>` so callers can tell at\n * the type level which pagination model an endpoint uses.\n */\nexport interface OffsetPage<T> {\n /** Total number of items matching the query across all pages. */\n totalCount: number;\n /** Echo of the request `limit`. */\n limit: number;\n /** Echo of the request `offset`. */\n offset: number;\n /** Total number of pages at the requested `limit`. */\n pageCount: number;\n /** Page rows. */\n items: T[];\n}\n\n/**\n * Drive an offset-paginated endpoint as an async iterator. Stops when the\n * server's `pageCount` is exhausted (or after the configured `maxPages`\n * to bound runaway iteration). Default page size is whatever the\n * underlying endpoint clamps to — pass `limit` explicitly for control.\n *\n * @example\n * for await (const order of paginateOffset((p) =>\n * client.orders.list(p),\n * )) {\n * console.log(order.orderNumber);\n * }\n */\nexport async function* paginateOffset<T>(\n fetchPage: (params: OffsetPaginationParams) => Promise<OffsetPage<T>>,\n params: { limit?: number; maxPages?: number } = {},\n): AsyncGenerator<T, void, undefined> {\n const limit = params.limit;\n const maxPages = params.maxPages ?? Infinity;\n let offset = 0;\n let pages = 0;\n while (pages < maxPages) {\n const page = await fetchPage({ offset, limit });\n for (const item of page.items) {\n yield item;\n }\n pages += 1;\n if (page.items.length === 0) return;\n if (page.pageCount > 0 && pages >= page.pageCount) return;\n // Step by the server-echoed limit (when present) so requests stay aligned with how\n // the server batches; fall back to the caller's `limit` or the observed batch size.\n offset += page.limit || limit || page.items.length;\n }\n}\n","export type LoncaErrorCode =\n | 'AUTH_FAILED'\n | 'RATE_LIMITED'\n | 'VALIDATION_FAILED'\n | 'NOT_FOUND'\n | 'SERVER_ERROR'\n | 'NETWORK_ERROR'\n | 'TIMEOUT'\n | 'UNKNOWN';\n\nexport interface LoncaErrorOptions {\n code: LoncaErrorCode;\n message: string;\n cause?: unknown;\n retryable?: boolean;\n status?: number;\n retryAfterMs?: number;\n data?: Record<string, unknown>;\n}\n\n/**\n * Root of the Lonca error hierarchy.\n *\n * Carries a tagged `code` and a `retryable` boolean so retry helpers\n * can decide what to do without sniffing message strings.\n */\nexport class LoncaError extends Error {\n readonly code: LoncaErrorCode;\n readonly retryable: boolean;\n readonly status?: number;\n readonly retryAfterMs?: number;\n readonly data?: Record<string, unknown>;\n\n constructor(opts: LoncaErrorOptions) {\n super(opts.message, opts.cause !== undefined ? { cause: opts.cause } : undefined);\n this.name = 'LoncaError';\n this.code = opts.code;\n this.retryable = opts.retryable ?? false;\n this.status = opts.status;\n this.retryAfterMs = opts.retryAfterMs;\n this.data = opts.data;\n }\n}\n\ntype SubclassOptions = Omit<LoncaErrorOptions, 'code' | 'retryable'>;\n\nexport class AuthError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'AUTH_FAILED', retryable: false });\n this.name = 'AuthError';\n }\n}\n\nexport class RateLimitError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'RATE_LIMITED', retryable: true });\n this.name = 'RateLimitError';\n }\n}\n\nexport class ValidationError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'VALIDATION_FAILED', retryable: false });\n this.name = 'ValidationError';\n }\n}\n\nexport class NotFoundError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'NOT_FOUND', retryable: false });\n this.name = 'NotFoundError';\n }\n}\n\nexport class ServerError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'SERVER_ERROR', retryable: true });\n this.name = 'ServerError';\n }\n}\n\nexport class NetworkError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'NETWORK_ERROR', retryable: true });\n this.name = 'NetworkError';\n }\n}\n\nexport class TimeoutError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'TIMEOUT', retryable: true });\n this.name = 'TimeoutError';\n }\n}\n\nexport function isLoncaError(value: unknown): value is LoncaError {\n return value instanceof LoncaError;\n}\n\nexport function isRetryableError(value: unknown): boolean {\n return isLoncaError(value) && value.retryable;\n}\n","import { isLoncaError, isRetryableError } from './errors.js';\n\nexport interface RetryOptions {\n /** Total number of attempts including the initial call. Defaults to 3. */\n maxAttempts?: number;\n /** Initial delay before the second attempt, in ms. Defaults to 200. */\n baseDelayMs?: number;\n /** Upper bound on delay between any two attempts, in ms. Defaults to 10_000. */\n maxDelayMs?: number;\n /** Add up to 50% random jitter on each delay. Defaults to true. */\n jitter?: boolean;\n /** Override the default predicate. Default treats `retryable` LoncaErrors as retryable. */\n isRetryable?: (err: unknown) => boolean;\n /** Called before each retry sleep. Useful for structured logging. */\n onRetry?: (err: unknown, attempt: number, nextDelayMs: number) => void;\n /** Abort all pending sleeps and bubble the signal's reason. */\n signal?: AbortSignal;\n}\n\nconst DEFAULTS = {\n maxAttempts: 3,\n baseDelayMs: 200,\n maxDelayMs: 10_000,\n jitter: true,\n} as const;\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(signal.reason);\n return;\n }\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n 'abort',\n () => {\n clearTimeout(timer);\n reject(signal.reason);\n },\n { once: true },\n );\n });\n}\n\nexport async function retry<T>(\n fn: (attempt: number) => Promise<T>,\n options: RetryOptions = {},\n): Promise<T> {\n const maxAttempts = options.maxAttempts ?? DEFAULTS.maxAttempts;\n const baseDelayMs = options.baseDelayMs ?? DEFAULTS.baseDelayMs;\n const maxDelayMs = options.maxDelayMs ?? DEFAULTS.maxDelayMs;\n const jitter = options.jitter ?? DEFAULTS.jitter;\n const isRetryable = options.isRetryable ?? isRetryableError;\n\n let lastErr: unknown;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n return await fn(attempt);\n } catch (err) {\n lastErr = err;\n if (attempt === maxAttempts || !isRetryable(err)) {\n throw err;\n }\n\n const retryAfterMs = isLoncaError(err) ? err.retryAfterMs : undefined;\n const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));\n const base = retryAfterMs ?? exponential;\n const jitterMs = jitter ? Math.random() * base * 0.5 : 0;\n const delay = Math.min(maxDelayMs, base + jitterMs);\n\n options.onRetry?.(err, attempt, delay);\n await sleep(delay, options.signal);\n }\n }\n throw lastErr;\n}\n","export type LogLevel = 'debug' | 'info' | 'warn' | 'error';\n\nexport type LogContext = Record<string, unknown>;\n\nexport interface Logger {\n debug(message: string, context?: LogContext): void;\n info(message: string, context?: LogContext): void;\n warn(message: string, context?: LogContext): void;\n error(message: string, context?: LogContext): void;\n /** Create a child logger that merges `bindings` into every log entry. */\n child(bindings: LogContext): Logger;\n}\n\nexport const noopLogger: Logger = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n child: () => noopLogger,\n};\n\n/**\n * Minimal JSON-line logger to stdout/stderr. Suitable for development and\n * containerized environments. For production prefer wiring a real logger\n * (pino, winston) by implementing the `Logger` interface.\n */\nexport function consoleLogger(bindings: LogContext = {}): Logger {\n const emit = (level: LogLevel, message: string, context?: LogContext): void => {\n const entry = {\n level,\n message,\n timestamp: new Date().toISOString(),\n ...bindings,\n ...context,\n };\n const line = JSON.stringify(entry);\n if (level === 'error' || level === 'warn') {\n console.error(line);\n } else {\n console.log(line);\n }\n };\n return {\n debug: (m, c) => emit('debug', m, c),\n info: (m, c) => emit('info', m, c),\n warn: (m, c) => emit('warn', m, c),\n error: (m, c) => emit('error', m, c),\n child: (b) => consoleLogger({ ...bindings, ...b }),\n };\n}\n","export interface RateLimiterOptions {\n /** Maximum number of tokens the bucket holds. Each `acquire()` consumes 1. */\n capacity: number;\n /** Time window in milliseconds over which `capacity` tokens are refilled. */\n intervalMs: number;\n}\n\n/**\n * Token-bucket rate limiter. `acquire()` returns immediately when a token\n * is available, otherwise it sleeps until enough tokens have been refilled.\n *\n * @example\n * const limiter = new TokenBucketRateLimiter({ capacity: 50, intervalMs: 60_000 });\n * for (const item of items) {\n * await limiter.acquire();\n * await fetchSomething(item);\n * }\n */\nexport class TokenBucketRateLimiter {\n private tokens: number;\n private lastRefillAt: number;\n private readonly capacity: number;\n private readonly intervalMs: number;\n\n constructor(options: RateLimiterOptions) {\n if (options.capacity <= 0 || options.intervalMs <= 0) {\n throw new RangeError('capacity and intervalMs must be > 0');\n }\n this.capacity = options.capacity;\n this.intervalMs = options.intervalMs;\n this.tokens = options.capacity;\n this.lastRefillAt = Date.now();\n }\n\n /**\n * Refill tokens based on elapsed time, then return ms to wait until the next\n * token is available (0 if one is available right now).\n */\n private timeUntilNextToken(): number {\n const now = Date.now();\n const elapsed = now - this.lastRefillAt;\n if (elapsed > 0) {\n const refill = (elapsed / this.intervalMs) * this.capacity;\n this.tokens = Math.min(this.capacity, this.tokens + refill);\n this.lastRefillAt = now;\n }\n if (this.tokens >= 1) return 0;\n const needed = 1 - this.tokens;\n return Math.ceil((needed / this.capacity) * this.intervalMs);\n }\n\n async acquire(signal?: AbortSignal): Promise<void> {\n for (;;) {\n if (signal?.aborted) throw signal.reason as Error;\n const wait = this.timeUntilNextToken();\n if (wait === 0) {\n this.tokens -= 1;\n return;\n }\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(resolve, wait);\n signal?.addEventListener(\n 'abort',\n () => {\n clearTimeout(timer);\n reject(signal.reason);\n },\n { once: true },\n );\n });\n }\n }\n\n /** Snapshot of available tokens (refills the bucket as a side effect). */\n available(): number {\n this.timeUntilNextToken();\n return this.tokens;\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/currency.ts","../src/money.ts","../src/pagination.ts","../src/order-status.ts","../src/errors.ts","../src/retry.ts","../src/logger.ts","../src/rate-limiter.ts","../src/transport.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * ISO 4217 three-letter currency code.\n *\n * Typed as a string for flexibility — Trendyol and similar marketplaces may\n * expose currencies beyond the common set, so we don't lock down to a union.\n * Use `isValidCurrencyCode` for runtime validation.\n */\nexport type Currency = string;\n\nexport const TRY = 'TRY';\nexport const USD = 'USD';\nexport const EUR = 'EUR';\nexport const GBP = 'GBP';\n\nconst ISO_4217_RE = /^[A-Z]{3}$/;\n\nexport function isValidCurrencyCode(code: string): boolean {\n return ISO_4217_RE.test(code);\n}\n","import type { Currency } from './currency.js';\n\n/**\n * A monetary amount in integer minor units (e.g., kuruş for TRY, cents for USD).\n *\n * Using integers avoids floating-point surprises. Convert to/from major units\n * with `moneyFromMajor` and `moneyToMajor`.\n */\nexport interface Money {\n amount: number;\n currency: Currency;\n}\n\nexport function money(amount: number, currency: Currency): Money {\n if (!Number.isInteger(amount)) {\n throw new TypeError(`Money amount must be an integer minor unit, got ${amount}`);\n }\n return { amount, currency };\n}\n\n/**\n * Convert a major-unit amount (e.g. lira, dollars) to {@link Money} in integer\n * minor units. This is the canonical lira→kuruş converter — prefer it over a\n * hand-rolled `Math.round(x * 100)`, which scatters rounding logic and risks\n * float drift.\n *\n * @example\n * ```ts\n * import { moneyFromMajor, TRY } from '@lonca/core';\n * // A marketplace price of 199.90 ₺:\n * moneyFromMajor(199.9, TRY); // { amount: 19990, currency: 'TRY' } — kuruş\n * ```\n */\nexport function moneyFromMajor(major: number, currency: Currency, minorScale = 2): Money {\n const factor = 10 ** minorScale;\n const amount = Math.round(major * factor);\n return money(amount, currency);\n}\n\n/**\n * Convert {@link Money} (integer minor units) back to a major-unit number —\n * the canonical kuruş→lira converter.\n *\n * @example\n * ```ts\n * import { moneyToMajor, TRY } from '@lonca/core';\n * moneyToMajor({ amount: 19990, currency: TRY }); // 199.9 (lira)\n * ```\n */\nexport function moneyToMajor(value: Money, minorScale = 2): number {\n return value.amount / 10 ** minorScale;\n}\n\nexport function isSameCurrency(a: Money, b: Money): boolean {\n return a.currency === b.currency;\n}\n\nexport function addMoney(a: Money, b: Money): Money {\n if (!isSameCurrency(a, b)) {\n throw new TypeError(`Cannot add Money of different currencies: ${a.currency} vs ${b.currency}`);\n }\n return money(a.amount + b.amount, a.currency);\n}\n\nexport function subMoney(a: Money, b: Money): Money {\n if (!isSameCurrency(a, b)) {\n throw new TypeError(\n `Cannot subtract Money of different currencies: ${a.currency} vs ${b.currency}`,\n );\n }\n return money(a.amount - b.amount, a.currency);\n}\n","export interface CursorPaginationParams {\n /** Opaque cursor from previous response's `nextCursor`. Omit for the first page. */\n cursor?: string;\n /** Maximum items in the page. Marketplace SDKs may clamp this to their own bounds. */\n limit?: number;\n}\n\nexport interface CursorPage<T> {\n items: T[];\n /** Opaque cursor for the next page. Absent when there are no more pages. */\n nextCursor?: string;\n}\n\n/**\n * Drive a cursor-paginated endpoint as an async iterator.\n *\n * @example\n * for await (const order of paginate((p) => client.orders.list(p))) {\n * console.log(order.id);\n * }\n */\nexport async function* paginate<T>(\n fetchPage: (params: CursorPaginationParams) => Promise<CursorPage<T>>,\n params: Omit<CursorPaginationParams, 'cursor'> = {},\n): AsyncGenerator<T, void, undefined> {\n let cursor: string | undefined;\n do {\n const page = await fetchPage({ ...params, cursor });\n for (const item of page.items) {\n yield item;\n }\n cursor = page.nextCursor;\n } while (cursor);\n}\n\n// ─── Offset pagination ─────────────────────────────────────────────────────\n\nexport interface OffsetPaginationParams {\n /** Zero-based offset. Default depends on the SDK; usually `0`. */\n offset?: number;\n /** Maximum items in the page. */\n limit?: number;\n}\n\n/**\n * Offset-based pagination envelope returned by marketplaces that expose\n * `{ totalCount, limit, offset, pageCount, items[] }` instead of opaque\n * cursors. Hepsiburada's OMS uses this shape; some legacy Trendyol\n * endpoints do too. Distinct from `CursorPage<T>` so callers can tell at\n * the type level which pagination model an endpoint uses.\n */\nexport interface OffsetPage<T> {\n /** Total number of items matching the query across all pages. */\n totalCount: number;\n /** Echo of the request `limit`. */\n limit: number;\n /** Echo of the request `offset`. */\n offset: number;\n /** Total number of pages at the requested `limit`. */\n pageCount: number;\n /** Page rows. */\n items: T[];\n}\n\n/**\n * Drive an offset-paginated endpoint as an async iterator. Stops when the\n * server's `pageCount` is exhausted (or after the configured `maxPages`\n * to bound runaway iteration). Default page size is whatever the\n * underlying endpoint clamps to — pass `limit` explicitly for control.\n *\n * @example\n * for await (const order of paginateOffset((p) =>\n * client.orders.list(p),\n * )) {\n * console.log(order.orderNumber);\n * }\n */\nexport async function* paginateOffset<T>(\n fetchPage: (params: OffsetPaginationParams) => Promise<OffsetPage<T>>,\n params: { limit?: number; maxPages?: number } = {},\n): AsyncGenerator<T, void, undefined> {\n const limit = params.limit;\n const maxPages = params.maxPages ?? Infinity;\n let offset = 0;\n let pages = 0;\n while (pages < maxPages) {\n const page = await fetchPage({ offset, limit });\n for (const item of page.items) {\n yield item;\n }\n pages += 1;\n if (page.items.length === 0) return;\n if (page.pageCount > 0 && pages >= page.pageCount) return;\n // Step by the server-echoed limit (when present) so requests stay aligned with how\n // the server batches; fall back to the caller's `limit` or the observed batch size.\n offset += page.limit || limit || page.items.length;\n }\n}\n","/**\n * The closed, marketplace-agnostic order/shipment status vocabulary that every\n * SDK normalizes its raw statuses into.\n *\n * This union is intentionally **closed** (no `(string & {})` catch-all): a value\n * a marketplace SDK can't map lands on `'unknown'` at runtime via\n * {@link createStatusNormalizer}, surfaced by `mapped: false` — never silently\n * coerced to a valid-looking default.\n */\nexport type NormalizedOrderStatus =\n | 'created'\n | 'picking'\n | 'invoiced'\n | 'shipped'\n | 'delivered'\n | 'cancelled'\n | 'returned'\n | 'unknown';\n\n/**\n * Result of normalizing a raw marketplace status. The raw value is always\n * preserved, and `mapped` makes an unrecognized status visible at runtime\n * instead of letting it disappear into a default.\n */\nexport interface NormalizedStatusResult {\n /** Mapped normalized status, or `'unknown'` when the raw value wasn't recognized. */\n normalized: NormalizedOrderStatus;\n /** The original raw status string from the marketplace, preserved verbatim. */\n raw: string;\n /** `false` when the raw value had no mapping (normalized fell back to `'unknown'`). */\n mapped: boolean;\n}\n\n/**\n * Build a status normalizer from an SDK-owned, **exhaustive** map of that\n * marketplace's known statuses. Because the map key type is a closed union,\n * forgetting to map a newly added known status is a compile-time error in the\n * SDK — while an unknown value arriving on the wire stays safe at runtime\n * (`{ normalized: 'unknown', mapped: false }`).\n *\n * @example\n * ```ts\n * const statusMap = { Created: 'created', Shipped: 'shipped' } as const;\n * const normalizeStatus = createStatusNormalizer(statusMap);\n * normalizeStatus('Shipped'); // { normalized: 'shipped', raw: 'Shipped', mapped: true }\n * normalizeStatus('Martian'); // { normalized: 'unknown', raw: 'Martian', mapped: false }\n * ```\n */\nexport function createStatusNormalizer<Known extends string>(\n map: Record<Known, NormalizedOrderStatus>,\n): (raw: string) => NormalizedStatusResult {\n const lookup = map as Record<string, NormalizedOrderStatus | undefined>;\n return (raw: string): NormalizedStatusResult => {\n const normalized = lookup[raw];\n return normalized !== undefined\n ? { normalized, raw, mapped: true }\n : { normalized: 'unknown', raw, mapped: false };\n };\n}\n","export type LoncaErrorCode =\n | 'AUTH_FAILED'\n | 'RATE_LIMITED'\n | 'VALIDATION_FAILED'\n | 'NOT_FOUND'\n | 'SERVER_ERROR'\n | 'NETWORK_ERROR'\n | 'TIMEOUT'\n | 'UNKNOWN';\n\n/**\n * A single normalized, field-level error detail extracted from a marketplace's\n * raw error body. Each SDK maps its own (inconsistent) error JSON into this\n * shape so consumers don't have to sniff marketplace-specific payloads.\n */\nexport interface LoncaErrorIssue {\n /** The offending field/path, when the marketplace reports one. */\n field?: string;\n /** Marketplace-specific error code, when present. */\n code?: string;\n /** Human-readable message — always present. */\n message: string;\n}\n\nexport interface LoncaErrorOptions {\n code: LoncaErrorCode;\n message: string;\n cause?: unknown;\n retryable?: boolean;\n status?: number;\n retryAfterMs?: number;\n data?: Record<string, unknown>;\n /** Normalized, field-level error details. Defaults to `[]` when omitted. */\n issues?: LoncaErrorIssue[];\n}\n\n/**\n * Root of the Lonca error hierarchy.\n *\n * Carries a tagged `code` and a `retryable` boolean so retry helpers\n * can decide what to do without sniffing message strings.\n */\nexport class LoncaError extends Error {\n readonly code: LoncaErrorCode;\n readonly retryable: boolean;\n readonly status?: number;\n readonly retryAfterMs?: number;\n readonly data?: Record<string, unknown>;\n /**\n * Normalized, field-level error details mapped from the marketplace's raw\n * error body by the SDK. Always an array (never `undefined`) so callers can\n * iterate without a presence check; empty when nothing was parseable.\n */\n readonly issues: LoncaErrorIssue[];\n\n constructor(opts: LoncaErrorOptions) {\n super(opts.message, opts.cause !== undefined ? { cause: opts.cause } : undefined);\n this.name = 'LoncaError';\n this.code = opts.code;\n this.retryable = opts.retryable ?? false;\n this.status = opts.status;\n this.retryAfterMs = opts.retryAfterMs;\n this.data = opts.data;\n this.issues = opts.issues ?? [];\n }\n}\n\ntype SubclassOptions = Omit<LoncaErrorOptions, 'code' | 'retryable'>;\n\nexport class AuthError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'AUTH_FAILED', retryable: false });\n this.name = 'AuthError';\n }\n}\n\nexport class RateLimitError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'RATE_LIMITED', retryable: true });\n this.name = 'RateLimitError';\n }\n}\n\nexport class ValidationError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'VALIDATION_FAILED', retryable: false });\n this.name = 'ValidationError';\n }\n}\n\nexport class NotFoundError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'NOT_FOUND', retryable: false });\n this.name = 'NotFoundError';\n }\n}\n\nexport class ServerError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'SERVER_ERROR', retryable: true });\n this.name = 'ServerError';\n }\n}\n\nexport class NetworkError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'NETWORK_ERROR', retryable: true });\n this.name = 'NetworkError';\n }\n}\n\nexport class TimeoutError extends LoncaError {\n constructor(opts: SubclassOptions) {\n super({ ...opts, code: 'TIMEOUT', retryable: true });\n this.name = 'TimeoutError';\n }\n}\n\nexport function isLoncaError(value: unknown): value is LoncaError {\n return value instanceof LoncaError;\n}\n\nexport function isRetryableError(value: unknown): boolean {\n return isLoncaError(value) && value.retryable;\n}\n\n/**\n * Retry predicate for **non-idempotent** requests (POST/PUT/DELETE/PATCH).\n *\n * Only `RateLimitError` (HTTP 429) is replayed, because a 429 means the server\n * rejected the request *before* processing it, so a retry cannot duplicate a\n * side-effect. Ambiguous failures — 5xx, network drops, and client-side\n * timeouts — are deliberately NOT retried here: the write may already have\n * taken effect server-side, and a blind replay would duplicate it (double\n * order split, double cancel, re-pushed price/stock batch).\n *\n * Callers that have made a write idempotent (e.g. by sending an idempotency\n * key) should opt back into full retries via `isRetryableError` instead.\n */\nexport function isRetryableIdempotentOnly(value: unknown): boolean {\n return isLoncaError(value) && value.retryable && value.code === 'RATE_LIMITED';\n}\n\n/**\n * Parse a `Retry-After` header value (delta-seconds OR an HTTP-date) into\n * milliseconds. Returns `undefined` when the header is absent, unparseable, or\n * non-positive.\n *\n * A non-positive value is treated as \"no hint\" rather than `0`: a literal\n * `Retry-After: 0` must not collapse exponential backoff into a zero-delay\n * retry storm against the very endpoint that is rate-limiting the client.\n */\nexport function parseRetryAfter(header: string | null): number | undefined {\n if (!header) return undefined;\n const seconds = Number(header);\n if (!Number.isNaN(seconds)) return seconds > 0 ? seconds * 1000 : undefined;\n const epoch = Date.parse(header);\n if (!Number.isNaN(epoch)) {\n const delta = epoch - Date.now();\n return delta > 0 ? delta : undefined;\n }\n return undefined;\n}\n","import { isLoncaError, isRetryableError } from './errors.js';\n\nexport interface RetryOptions {\n /** Total number of attempts including the initial call. Defaults to 3. */\n maxAttempts?: number;\n /** Initial delay before the second attempt, in ms. Defaults to 200. */\n baseDelayMs?: number;\n /** Upper bound on delay between any two attempts, in ms. Defaults to 10_000. */\n maxDelayMs?: number;\n /** Add up to 50% random jitter on each delay. Defaults to true. */\n jitter?: boolean;\n /** Override the default predicate. Default treats `retryable` LoncaErrors as retryable. */\n isRetryable?: (err: unknown) => boolean;\n /** Called before each retry sleep. Useful for structured logging. */\n onRetry?: (err: unknown, attempt: number, nextDelayMs: number) => void;\n /** Abort all pending sleeps and bubble the signal's reason. */\n signal?: AbortSignal;\n}\n\nconst DEFAULTS = {\n maxAttempts: 3,\n baseDelayMs: 200,\n maxDelayMs: 10_000,\n jitter: true,\n} as const;\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(signal.reason);\n return;\n }\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n 'abort',\n () => {\n clearTimeout(timer);\n reject(signal.reason);\n },\n { once: true },\n );\n });\n}\n\nexport async function retry<T>(\n fn: (attempt: number) => Promise<T>,\n options: RetryOptions = {},\n): Promise<T> {\n const maxAttempts = options.maxAttempts ?? DEFAULTS.maxAttempts;\n const baseDelayMs = options.baseDelayMs ?? DEFAULTS.baseDelayMs;\n const maxDelayMs = options.maxDelayMs ?? DEFAULTS.maxDelayMs;\n const jitter = options.jitter ?? DEFAULTS.jitter;\n const isRetryable = options.isRetryable ?? isRetryableError;\n\n let lastErr: unknown;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n return await fn(attempt);\n } catch (err) {\n lastErr = err;\n if (attempt === maxAttempts || !isRetryable(err)) {\n throw err;\n }\n\n const retryAfterMs = isLoncaError(err) ? err.retryAfterMs : undefined;\n const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));\n // A non-positive `retryAfterMs` is ignored (not used as the base) so a\n // `Retry-After: 0` can never collapse backoff to an immediate retry.\n const base = retryAfterMs && retryAfterMs > 0 ? retryAfterMs : exponential;\n const jitterMs = jitter ? Math.random() * base * 0.5 : 0;\n const delay = Math.min(maxDelayMs, base + jitterMs);\n\n options.onRetry?.(err, attempt, delay);\n await sleep(delay, options.signal);\n }\n }\n throw lastErr;\n}\n","export type LogLevel = 'debug' | 'info' | 'warn' | 'error';\n\nexport type LogContext = Record<string, unknown>;\n\nexport interface Logger {\n debug(message: string, context?: LogContext): void;\n info(message: string, context?: LogContext): void;\n warn(message: string, context?: LogContext): void;\n error(message: string, context?: LogContext): void;\n /** Create a child logger that merges `bindings` into every log entry. */\n child(bindings: LogContext): Logger;\n}\n\nexport const noopLogger: Logger = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n child: () => noopLogger,\n};\n\n/**\n * Minimal JSON-line logger to stdout/stderr. Suitable for development and\n * containerized environments. For production prefer wiring a real logger\n * (pino, winston) by implementing the `Logger` interface.\n */\nexport function consoleLogger(bindings: LogContext = {}): Logger {\n const emit = (level: LogLevel, message: string, context?: LogContext): void => {\n const entry = {\n level,\n message,\n timestamp: new Date().toISOString(),\n ...bindings,\n ...context,\n };\n const line = JSON.stringify(entry);\n if (level === 'error' || level === 'warn') {\n console.error(line);\n } else {\n console.log(line);\n }\n };\n return {\n debug: (m, c) => emit('debug', m, c),\n info: (m, c) => emit('info', m, c),\n warn: (m, c) => emit('warn', m, c),\n error: (m, c) => emit('error', m, c),\n child: (b) => consoleLogger({ ...bindings, ...b }),\n };\n}\n","export interface RateLimiterOptions {\n /** Maximum number of tokens the bucket holds. Each `acquire()` consumes 1. */\n capacity: number;\n /** Time window in milliseconds over which `capacity` tokens are refilled. */\n intervalMs: number;\n}\n\n/**\n * Token-bucket rate limiter. `acquire()` returns immediately when a token\n * is available, otherwise it sleeps until enough tokens have been refilled.\n *\n * @example\n * const limiter = new TokenBucketRateLimiter({ capacity: 50, intervalMs: 60_000 });\n * for (const item of items) {\n * await limiter.acquire();\n * await fetchSomething(item);\n * }\n */\nexport class TokenBucketRateLimiter {\n private tokens: number;\n private lastRefillAt: number;\n private readonly capacity: number;\n private readonly intervalMs: number;\n\n constructor(options: RateLimiterOptions) {\n if (options.capacity <= 0 || options.intervalMs <= 0) {\n throw new RangeError('capacity and intervalMs must be > 0');\n }\n this.capacity = options.capacity;\n this.intervalMs = options.intervalMs;\n this.tokens = options.capacity;\n this.lastRefillAt = Date.now();\n }\n\n /**\n * Refill tokens based on elapsed time, then return ms to wait until the next\n * token is available (0 if one is available right now).\n */\n private timeUntilNextToken(): number {\n const now = Date.now();\n const elapsed = now - this.lastRefillAt;\n if (elapsed > 0) {\n const refill = (elapsed / this.intervalMs) * this.capacity;\n this.tokens = Math.min(this.capacity, this.tokens + refill);\n this.lastRefillAt = now;\n }\n if (this.tokens >= 1) return 0;\n const needed = 1 - this.tokens;\n return Math.ceil((needed / this.capacity) * this.intervalMs);\n }\n\n async acquire(signal?: AbortSignal): Promise<void> {\n for (;;) {\n if (signal?.aborted) throw signal.reason as Error;\n const wait = this.timeUntilNextToken();\n if (wait === 0) {\n this.tokens -= 1;\n return;\n }\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(resolve, wait);\n signal?.addEventListener(\n 'abort',\n () => {\n clearTimeout(timer);\n reject(signal.reason);\n },\n { once: true },\n );\n });\n }\n }\n\n /** Snapshot of available tokens (refills the bucket as a side effect). */\n available(): number {\n this.timeUntilNextToken();\n return this.tokens;\n }\n}\n","import { randomUUID } from 'node:crypto';\nimport {\n LoncaError,\n NetworkError,\n TimeoutError,\n parseRetryAfter,\n isRetryableIdempotentOnly,\n} from './errors.js';\nimport { retry } from './retry.js';\nimport { noopLogger, type Logger } from './logger.js';\nimport type { TokenBucketRateLimiter } from './rate-limiter.js';\n\n/**\n * The request lifecycle shared by every marketplace SDK transport: acquire a\n * rate-limit token, build the URL/headers, fire `fetch` under a composed\n * timeout signal, map non-2xx responses to `LoncaError`s, and replay through\n * {@link retry}. Only the marketplace-specific seams (URL building, headers,\n * status→error mapping, log labels) are injected via {@link RequesterConfig},\n * so the loop itself lives in one place instead of being copy-pasted — and\n * drifting — across SDKs.\n */\n\n/** The request fields the shared lifecycle reads. SDKs extend this with their own (e.g. `path`, `service`, `query`). */\nexport interface BaseRequestOptions {\n /** HTTP method. `GET` is treated as idempotent for retry purposes. */\n method: string;\n /** Request body. Serialized as JSON unless it is a `FormData` (sent as multipart). Skipped for `GET`. */\n body?: unknown;\n /** Caller abort signal, composed with the per-request timeout. */\n signal?: AbortSignal;\n /** Per-endpoint rate limiter; one token is acquired before each attempt. */\n rateLimiter?: TokenBucketRateLimiter;\n /**\n * Whether this request is safe to auto-replay on an ambiguous transient\n * failure (5xx / network drop / client timeout). `GET` is always idempotent.\n * For writes this defaults to `false`: a timed-out or 5xx write may already\n * have committed server-side, so only a `429` (provably rejected before\n * processing) is retried. Set `true` to opt a write back into full retries.\n */\n idempotent?: boolean;\n /** Extra per-request headers merged over the default header set (caller headers win). */\n headers?: Record<string, string>;\n}\n\n/** Marketplace-specific seams plus runtime dependencies for {@link createRequester}. */\nexport interface RequesterConfig<O extends BaseRequestOptions> {\n /** Underlying `fetch` (tests inject a mock). */\n fetch: typeof fetch;\n /** Structured logger. Defaults to a no-op. */\n logger?: Logger;\n /** Per-request timeout in ms. */\n timeoutMs: number;\n /** Human-readable marketplace name for error messages, e.g. `'Trendyol'`. */\n label: string;\n /** Log-event prefix, e.g. `'trendyol'` → `trendyol.request` / `trendyol.error` / `trendyol.retry`. */\n logPrefix: string;\n /** Build the absolute request URL from the SDK's options. */\n buildUrl(opts: O): string;\n /** Build the default header set; receives the generated correlation id. */\n buildHeaders(correlationId: string): Record<string, string>;\n /** Map a non-2xx response to a `LoncaError`. */\n mapHttpError(status: number, body: unknown, retryAfterMs?: number): LoncaError;\n /** Optional extra structured fields merged into request/error log lines (e.g. `{ service }`). */\n logFields?(opts: O): Record<string, unknown>;\n}\n\n/**\n * Build a `request<T>(opts)` function that runs the shared transport lifecycle\n * for a given marketplace. Each SDK calls this once (per transport instance)\n * and exposes the returned function from its own transport class.\n */\nexport function createRequester<O extends BaseRequestOptions>(\n config: RequesterConfig<O>,\n): <T>(opts: O) => Promise<T> {\n const logger = config.logger ?? noopLogger;\n\n return function request<T>(opts: O): Promise<T> {\n const safeToReplay = opts.method === 'GET' || opts.idempotent === true;\n const extra = () => config.logFields?.(opts) ?? {};\n\n return retry(\n async (attempt) => {\n if (opts.rateLimiter) await opts.rateLimiter.acquire(opts.signal);\n\n const correlationId = randomUUID();\n const url = config.buildUrl(opts);\n const headers = { ...config.buildHeaders(correlationId), ...opts.headers };\n const init: RequestInit = {\n method: opts.method,\n headers,\n signal: composeSignal(opts.signal, config.timeoutMs),\n };\n if (opts.body !== undefined && opts.method !== 'GET') {\n if (opts.body instanceof FormData) {\n // multipart: let fetch set Content-Type (it includes the boundary).\n init.body = opts.body;\n delete (headers as Record<string, string>)['Content-Type'];\n } else {\n init.body = JSON.stringify(opts.body);\n }\n }\n\n logger.debug(`${config.logPrefix}.request`, {\n method: opts.method,\n url,\n correlationId,\n attempt,\n ...extra(),\n });\n\n let response: Response;\n try {\n response = await config.fetch(url, init);\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n throw new TimeoutError({\n message: `${config.label} request timed out after ${config.timeoutMs}ms`,\n cause: err,\n });\n }\n throw new NetworkError({ message: `${config.label} network failure`, cause: err });\n }\n\n if (!response.ok) {\n const body = await safeJson(response);\n const retryAfterMs = parseRetryAfter(response.headers.get('retry-after'));\n const error = config.mapHttpError(response.status, body, retryAfterMs);\n logger.warn(`${config.logPrefix}.error`, {\n method: opts.method,\n url,\n correlationId,\n status: response.status,\n code: error.code,\n retryable: error.retryable,\n ...extra(),\n });\n throw error;\n }\n\n logger.debug(`${config.logPrefix}.response`, {\n correlationId,\n status: response.status,\n });\n\n if (response.status === 204) return undefined as T;\n return (await safeJson(response)) as T;\n },\n {\n signal: opts.signal,\n // Non-idempotent writes only retry rate-limit (429) errors; ambiguous\n // 5xx/network/timeout failures are not replayed to avoid duplicate\n // side-effects. GET (and explicitly idempotent requests) retry normally.\n isRetryable: safeToReplay ? undefined : isRetryableIdempotentOnly,\n onRetry: (err, attempt, delay) => {\n if (err instanceof LoncaError) {\n logger.warn(`${config.logPrefix}.retry`, {\n attempt,\n delayMs: delay,\n code: err.code,\n status: err.status,\n });\n }\n },\n },\n );\n };\n}\n\n/** Compose a caller signal with a per-request timeout signal. */\nexport function composeSignal(external: AbortSignal | undefined, timeoutMs: number): AbortSignal {\n const timeoutSignal = AbortSignal.timeout(timeoutMs);\n if (!external) return timeoutSignal;\n return AbortSignal.any([external, timeoutSignal]);\n}\n\n/** Read a response body as JSON, falling back to raw text (then `undefined` when empty). */\nexport async function safeJson(response: Response): Promise<unknown> {\n const text = await response.text();\n if (!text) return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lonca/core",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Shared primitives for Lonca marketplace SDKs: money, errors, retry, logger, rate limiter.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,7 +38,7 @@
38
38
  "@types/node": "^24.0.0",
39
39
  "tsup": "^8.3.5",
40
40
  "typescript": "^6.0.3",
41
- "vitest": "^4.1.7"
41
+ "vitest": "^4.1.8"
42
42
  },
43
43
  "scripts": {
44
44
  "build": "tsup",