@lonca/core 0.3.0 → 0.5.0

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