scorezilla 0.1.0-next.3 → 0.2.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/dist/index.cjs CHANGED
@@ -33,6 +33,7 @@ function validateConfig(cfg) {
33
33
  timeoutMs: cfg.timeoutMs,
34
34
  maxRetries: cfg.maxRetries,
35
35
  sleepImpl: cfg.sleepImpl,
36
+ warn: cfg.warn,
36
37
  userAgent: cfg.userAgent,
37
38
  auth
38
39
  };
@@ -119,7 +120,9 @@ var ScorezillaError = class _ScorezillaError extends Error {
119
120
  * {@link ScorezillaErrorCode}. For network errors, this is `'network_error'`;
120
121
  * for aborts, `'aborted'`; for timeouts, `'timeout'`. */
121
122
  code;
122
- /** Sub-classifier — present on `out_of_bounds` (`'below_min' | 'above_max'`)
123
+ /** Sub-classifier — present on:
124
+ * - `out_of_bounds`: `'below_min' | 'above_max'`
125
+ * - `usage_cap_exceeded`: `'over_cap' | 'suspended'`
123
126
  * and possibly other codes in future minor releases. */
124
127
  reason;
125
128
  /** Seconds — present on `rate_limited`. Honored by the transport's retry
@@ -132,6 +135,20 @@ var ScorezillaError = class _ScorezillaError extends Error {
132
135
  bound;
133
136
  /** Which rate-limit layer fired on `rate_limited`. */
134
137
  layer;
138
+ /** Tenant's billing tier — present on `usage_cap_exceeded`. */
139
+ tier;
140
+ /** The cap value crossed on `usage_cap_exceeded`. `0` indicates a
141
+ * suspended tenant. `undefined` on all other error codes. */
142
+ cap;
143
+ /** The post-increment submit count on `usage_cap_exceeded`. Always
144
+ * `> cap` when `reason === 'over_cap'`. */
145
+ count;
146
+ /** The period the count belongs to on `usage_cap_exceeded`, in `YYYY-MM`
147
+ * UTC form. */
148
+ period;
149
+ /** ISO-8601 timestamp of midnight UTC on the 1st of the next month —
150
+ * the counter's natural reset point on `usage_cap_exceeded`. */
151
+ resetsAt;
135
152
  /** The underlying cause (e.g., a `TypeError: fetch failed`) for
136
153
  * network/abort/timeout paths. `undefined` when the error came from a
137
154
  * successfully-parsed API error body. */
@@ -146,6 +163,11 @@ var ScorezillaError = class _ScorezillaError extends Error {
146
163
  this.requestId = truncateField(init.requestId);
147
164
  this.bound = init.bound;
148
165
  this.layer = truncateField(init.layer);
166
+ this.tier = truncateField(init.tier);
167
+ this.cap = init.cap;
168
+ this.count = init.count;
169
+ this.period = truncateField(init.period);
170
+ this.resetsAt = truncateField(init.resetsAt);
149
171
  this.cause = init.cause;
150
172
  Object.setPrototypeOf(this, _ScorezillaError.prototype);
151
173
  if (typeof Error.captureStackTrace === "function") {
@@ -159,6 +181,22 @@ var ScorezillaError = class _ScorezillaError extends Error {
159
181
  isRateLimited() {
160
182
  return this.code === "rate_limited";
161
183
  }
184
+ /**
185
+ * `true` when this error is a 402 / `usage_cap_exceeded`. The tenant
186
+ * has either hit their tier's monthly submit cap (`reason ===
187
+ * 'over_cap'`) or is suspended (`reason === 'suspended'`).
188
+ *
189
+ * Consumers SHOULD NOT auto-retry on this error — the cap doesn't lift
190
+ * until `resetsAt`. Surface to the developer with an upgrade prompt
191
+ * (over_cap) or contact-support message (suspended).
192
+ */
193
+ isUsageCapExceeded() {
194
+ return this.code === "usage_cap_exceeded";
195
+ }
196
+ /** `true` when this error is a 402 + reason 'suspended' (vs over-cap). */
197
+ isSuspended() {
198
+ return this.code === "usage_cap_exceeded" && this.reason === "suspended";
199
+ }
162
200
  /** `true` when this error is a 401 / `unauthorized` (or 403 / `forbidden`). */
163
201
  isAuth() {
164
202
  return this.code === "unauthorized" || this.code === "forbidden";
@@ -171,13 +209,21 @@ var ScorezillaError = class _ScorezillaError extends Error {
171
209
  isOutOfBounds() {
172
210
  return this.code === "out_of_bounds";
173
211
  }
174
- /** `true` for transient / retryable conditions: network errors, timeouts,
175
- * 5xx, and 429. The transport layer relies on this for its retry policy. */
212
+ /** `true` for the SDK's retryable conditions: pure network errors, 5xx, and
213
+ * 429. Deliberately excludes `timeout` and `aborted` those are caller-
214
+ * observable terminal states, not transient. Aligned with `shouldRetryError`
215
+ * in `retry.ts` so a consumer mirroring the SDK's retry policy gets the
216
+ * same answer the transport does. */
176
217
  isTransient() {
177
- if (this.status === STATUS_NETWORK_ERROR) return true;
218
+ if (this.code === "network_error") return true;
178
219
  if (this.status >= 500 && this.status < 600) return true;
179
220
  return this.isRateLimited();
180
221
  }
222
+ /** `true` when this error is a 409 / `conflict` (idempotency-key conflict
223
+ * on retry). */
224
+ isConflict() {
225
+ return this.code === "conflict";
226
+ }
181
227
  // ─── Factory ─────────────────────────────────────────────────────────
182
228
  /**
183
229
  * Build a `ScorezillaError` from a fetch round-trip outcome.
@@ -198,6 +244,13 @@ var ScorezillaError = class _ScorezillaError extends Error {
198
244
  retryAfter: body.retryAfter,
199
245
  bound: body.bound,
200
246
  layer: body.layer,
247
+ // Usage-cap fields from `ApiError` (populated by the server on
248
+ // 402 responses; undefined on other errors).
249
+ tier: body.tier,
250
+ cap: body.cap,
251
+ count: body.count,
252
+ period: body.period,
253
+ resetsAt: body.resetsAt,
201
254
  requestId,
202
255
  cause
203
256
  });
@@ -239,8 +292,10 @@ var ScorezillaError = class _ScorezillaError extends Error {
239
292
  };
240
293
  function codeForStatus(status) {
241
294
  if (status === 401) return "unauthorized";
295
+ if (status === 402) return "usage_cap_exceeded";
242
296
  if (status === 403) return "forbidden";
243
297
  if (status === 404) return "not_found";
298
+ if (status === 409) return "conflict";
244
299
  if (status === 422) return "out_of_bounds";
245
300
  if (status === 429) return "rate_limited";
246
301
  if (status >= 500) return "internal_error";
@@ -351,6 +406,7 @@ async function request(opts) {
351
406
  }
352
407
  const response = await fetchImpl(url, init);
353
408
  if (response.ok) {
409
+ warnOnDeprecationOnce(response, opts.warnImpl);
354
410
  return await parseJson(response);
355
411
  }
356
412
  const body = await safelyParseErrorBody(response);
@@ -462,6 +518,29 @@ function readRetryAfter(response) {
462
518
  const n = Number(raw);
463
519
  return Number.isFinite(n) && n >= 0 ? n : void 0;
464
520
  }
521
+ var seenDeprecations = /* @__PURE__ */ new Set();
522
+ function warnOnDeprecationOnce(response, warnImpl) {
523
+ const deprecation = response.headers.get("Deprecation");
524
+ const sunset = response.headers.get("Sunset");
525
+ if (!deprecation && !sunset) return;
526
+ const link = response.headers.get("Link") ?? "";
527
+ const key = `${deprecation ?? ""}|${sunset ?? ""}|${link}`;
528
+ if (seenDeprecations.has(key)) return;
529
+ seenDeprecations.add(key);
530
+ const detail = [];
531
+ if (deprecation === "true" || deprecation) detail.push(`Deprecation: ${deprecation}`);
532
+ if (sunset) detail.push(`Sunset: ${sunset}`);
533
+ if (link) {
534
+ const m = link.match(/<([^>]+)>/);
535
+ if (m) detail.push(`Docs: ${m[1]}`);
536
+ }
537
+ const message = `[scorezilla-sdk] API responded with deprecation signal: ${detail.join(" \xB7 ")}. Upgrade your SDK before the sunset date.`;
538
+ if (warnImpl) {
539
+ warnImpl(message);
540
+ } else {
541
+ console.warn(message);
542
+ }
543
+ }
465
544
  function combineSignalsWithTimeout(caller, timeoutMs) {
466
545
  const ctrl = new AbortController();
467
546
  let didTimeOut = false;
@@ -550,10 +629,11 @@ function validateMetadata(metadata) {
550
629
  `scorezilla: metadata exceeds ${METADATA_MAX_BYTES} bytes (got ${byteLength} bytes when JSON-stringified)`
551
630
  );
552
631
  }
632
+ return serialized;
553
633
  }
554
634
  var Scorezilla = class _Scorezilla {
555
635
  /** The package version, injected at build time from `package.json`. */
556
- static version = "0.1.0-next.3";
636
+ static version = "0.2.0";
557
637
  #config;
558
638
  #userAgent;
559
639
  #authHeader;
@@ -611,7 +691,8 @@ var Scorezilla = class _Scorezilla {
611
691
  return this.#request({
612
692
  path: submitScorePath(input.boardId),
613
693
  method: "POST",
614
- body
694
+ body,
695
+ signal: input.signal
615
696
  });
616
697
  }
617
698
  /**
@@ -635,7 +716,8 @@ var Scorezilla = class _Scorezilla {
635
716
  if (input.offset !== void 0) q.offset = input.offset;
636
717
  return this.#request({
637
718
  path: getLeaderboardPath(input.boardId, q),
638
- method: "GET"
719
+ method: "GET",
720
+ signal: input.signal
639
721
  });
640
722
  }
641
723
  /**
@@ -664,7 +746,8 @@ var Scorezilla = class _Scorezilla {
664
746
  async getPlayerRank(input) {
665
747
  return this.#request({
666
748
  path: getPlayerRankPath(input.boardId, input.playerId),
667
- method: "GET"
749
+ method: "GET",
750
+ signal: input.signal
668
751
  });
669
752
  }
670
753
  /**
@@ -689,7 +772,8 @@ var Scorezilla = class _Scorezilla {
689
772
  if (input.after !== void 0) q.after = input.after;
690
773
  return this.#request({
691
774
  path: getWindowAroundPath(input.boardId, input.playerId, q),
692
- method: "GET"
775
+ method: "GET",
776
+ signal: input.signal
693
777
  });
694
778
  }
695
779
  // ─── Internal ────────────────────────────────────────────────────────
@@ -716,7 +800,9 @@ var Scorezilla = class _Scorezilla {
716
800
  headers
717
801
  };
718
802
  if (opts.body !== void 0) requestOpts.body = opts.body;
803
+ if (opts.signal !== void 0) requestOpts.signal = opts.signal;
719
804
  if (this.#config.fetch !== void 0) requestOpts.fetchImpl = this.#config.fetch;
805
+ if (this.#config.warn !== void 0) requestOpts.warnImpl = this.#config.warn;
720
806
  if (this.#config.timeoutMs !== void 0) requestOpts.timeoutMs = this.#config.timeoutMs;
721
807
  if (this.#config.maxRetries !== void 0 || this.#config.sleepImpl !== void 0) {
722
808
  requestOpts.retry = {
@@ -732,7 +818,7 @@ function createClient(config) {
732
818
  }
733
819
 
734
820
  // src/index.ts
735
- var SDK_VERSION = "0.1.0-next.3";
821
+ var SDK_VERSION = "0.2.0";
736
822
 
737
823
  exports.SDK_VERSION = SDK_VERSION;
738
824
  exports.Scorezilla = Scorezilla;