scorezilla 0.1.0-next.3 → 0.3.0-next.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/server.cjs CHANGED
@@ -152,7 +152,9 @@ var ScorezillaError = class _ScorezillaError extends Error {
152
152
  * {@link ScorezillaErrorCode}. For network errors, this is `'network_error'`;
153
153
  * for aborts, `'aborted'`; for timeouts, `'timeout'`. */
154
154
  code;
155
- /** Sub-classifier — present on `out_of_bounds` (`'below_min' | 'above_max'`)
155
+ /** Sub-classifier — present on:
156
+ * - `out_of_bounds`: `'below_min' | 'above_max'`
157
+ * - `usage_cap_exceeded`: `'over_cap' | 'suspended'`
156
158
  * and possibly other codes in future minor releases. */
157
159
  reason;
158
160
  /** Seconds — present on `rate_limited`. Honored by the transport's retry
@@ -165,6 +167,20 @@ var ScorezillaError = class _ScorezillaError extends Error {
165
167
  bound;
166
168
  /** Which rate-limit layer fired on `rate_limited`. */
167
169
  layer;
170
+ /** Tenant's billing tier — present on `usage_cap_exceeded`. */
171
+ tier;
172
+ /** The cap value crossed on `usage_cap_exceeded`. `0` indicates a
173
+ * suspended tenant. `undefined` on all other error codes. */
174
+ cap;
175
+ /** The post-increment submit count on `usage_cap_exceeded`. Always
176
+ * `> cap` when `reason === 'over_cap'`. */
177
+ count;
178
+ /** The period the count belongs to on `usage_cap_exceeded`, in `YYYY-MM`
179
+ * UTC form. */
180
+ period;
181
+ /** ISO-8601 timestamp of midnight UTC on the 1st of the next month —
182
+ * the counter's natural reset point on `usage_cap_exceeded`. */
183
+ resetsAt;
168
184
  /** The underlying cause (e.g., a `TypeError: fetch failed`) for
169
185
  * network/abort/timeout paths. `undefined` when the error came from a
170
186
  * successfully-parsed API error body. */
@@ -179,6 +195,11 @@ var ScorezillaError = class _ScorezillaError extends Error {
179
195
  this.requestId = truncateField(init.requestId);
180
196
  this.bound = init.bound;
181
197
  this.layer = truncateField(init.layer);
198
+ this.tier = truncateField(init.tier);
199
+ this.cap = init.cap;
200
+ this.count = init.count;
201
+ this.period = truncateField(init.period);
202
+ this.resetsAt = truncateField(init.resetsAt);
182
203
  this.cause = init.cause;
183
204
  Object.setPrototypeOf(this, _ScorezillaError.prototype);
184
205
  if (typeof Error.captureStackTrace === "function") {
@@ -192,6 +213,22 @@ var ScorezillaError = class _ScorezillaError extends Error {
192
213
  isRateLimited() {
193
214
  return this.code === "rate_limited";
194
215
  }
216
+ /**
217
+ * `true` when this error is a 402 / `usage_cap_exceeded`. The tenant
218
+ * has either hit their tier's monthly submit cap (`reason ===
219
+ * 'over_cap'`) or is suspended (`reason === 'suspended'`).
220
+ *
221
+ * Consumers SHOULD NOT auto-retry on this error — the cap doesn't lift
222
+ * until `resetsAt`. Surface to the developer with an upgrade prompt
223
+ * (over_cap) or contact-support message (suspended).
224
+ */
225
+ isUsageCapExceeded() {
226
+ return this.code === "usage_cap_exceeded";
227
+ }
228
+ /** `true` when this error is a 402 + reason 'suspended' (vs over-cap). */
229
+ isSuspended() {
230
+ return this.code === "usage_cap_exceeded" && this.reason === "suspended";
231
+ }
195
232
  /** `true` when this error is a 401 / `unauthorized` (or 403 / `forbidden`). */
196
233
  isAuth() {
197
234
  return this.code === "unauthorized" || this.code === "forbidden";
@@ -204,13 +241,21 @@ var ScorezillaError = class _ScorezillaError extends Error {
204
241
  isOutOfBounds() {
205
242
  return this.code === "out_of_bounds";
206
243
  }
207
- /** `true` for transient / retryable conditions: network errors, timeouts,
208
- * 5xx, and 429. The transport layer relies on this for its retry policy. */
244
+ /** `true` for the SDK's retryable conditions: pure network errors, 5xx, and
245
+ * 429. Deliberately excludes `timeout` and `aborted` those are caller-
246
+ * observable terminal states, not transient. Aligned with `shouldRetryError`
247
+ * in `retry.ts` so a consumer mirroring the SDK's retry policy gets the
248
+ * same answer the transport does. */
209
249
  isTransient() {
210
- if (this.status === STATUS_NETWORK_ERROR) return true;
250
+ if (this.code === "network_error") return true;
211
251
  if (this.status >= 500 && this.status < 600) return true;
212
252
  return this.isRateLimited();
213
253
  }
254
+ /** `true` when this error is a 409 / `conflict` (idempotency-key conflict
255
+ * on retry). */
256
+ isConflict() {
257
+ return this.code === "conflict";
258
+ }
214
259
  // ─── Factory ─────────────────────────────────────────────────────────
215
260
  /**
216
261
  * Build a `ScorezillaError` from a fetch round-trip outcome.
@@ -231,6 +276,13 @@ var ScorezillaError = class _ScorezillaError extends Error {
231
276
  retryAfter: body.retryAfter,
232
277
  bound: body.bound,
233
278
  layer: body.layer,
279
+ // Usage-cap fields from `ApiError` (populated by the server on
280
+ // 402 responses; undefined on other errors).
281
+ tier: body.tier,
282
+ cap: body.cap,
283
+ count: body.count,
284
+ period: body.period,
285
+ resetsAt: body.resetsAt,
234
286
  requestId,
235
287
  cause
236
288
  });
@@ -272,8 +324,10 @@ var ScorezillaError = class _ScorezillaError extends Error {
272
324
  };
273
325
  function codeForStatus(status) {
274
326
  if (status === 401) return "unauthorized";
327
+ if (status === 402) return "usage_cap_exceeded";
275
328
  if (status === 403) return "forbidden";
276
329
  if (status === 404) return "not_found";
330
+ if (status === 409) return "conflict";
277
331
  if (status === 422) return "out_of_bounds";
278
332
  if (status === 429) return "rate_limited";
279
333
  if (status >= 500) return "internal_error";
@@ -384,6 +438,7 @@ async function request(opts) {
384
438
  }
385
439
  const response = await fetchImpl(url, init);
386
440
  if (response.ok) {
441
+ warnOnDeprecationOnce(response, opts.warnImpl);
387
442
  return await parseJson(response);
388
443
  }
389
444
  const body = await safelyParseErrorBody(response);
@@ -495,6 +550,29 @@ function readRetryAfter(response) {
495
550
  const n = Number(raw);
496
551
  return Number.isFinite(n) && n >= 0 ? n : void 0;
497
552
  }
553
+ var seenDeprecations = /* @__PURE__ */ new Set();
554
+ function warnOnDeprecationOnce(response, warnImpl) {
555
+ const deprecation = response.headers.get("Deprecation");
556
+ const sunset = response.headers.get("Sunset");
557
+ if (!deprecation && !sunset) return;
558
+ const link = response.headers.get("Link") ?? "";
559
+ const key = `${deprecation ?? ""}|${sunset ?? ""}|${link}`;
560
+ if (seenDeprecations.has(key)) return;
561
+ seenDeprecations.add(key);
562
+ const detail = [];
563
+ if (deprecation === "true" || deprecation) detail.push(`Deprecation: ${deprecation}`);
564
+ if (sunset) detail.push(`Sunset: ${sunset}`);
565
+ if (link) {
566
+ const m = link.match(/<([^>]+)>/);
567
+ if (m) detail.push(`Docs: ${m[1]}`);
568
+ }
569
+ const message = `[scorezilla-sdk] API responded with deprecation signal: ${detail.join(" \xB7 ")}. Upgrade your SDK before the sunset date.`;
570
+ if (warnImpl) {
571
+ warnImpl(message);
572
+ } else {
573
+ console.warn(message);
574
+ }
575
+ }
498
576
  function combineSignalsWithTimeout(caller, timeoutMs) {
499
577
  const ctrl = new AbortController();
500
578
  let didTimeOut = false;
@@ -552,7 +630,7 @@ var Scorezilla = class _Scorezilla {
552
630
  * Mirrors the static on the public-key client so consumers can log
553
631
  * the running SDK build the same way regardless of which surface
554
632
  * they imported. */
555
- static version = "0.1.0-next.3";
633
+ static version = "0.3.0-next.0";
556
634
  #keyId;
557
635
  #secret;
558
636
  #baseUrl;
@@ -564,6 +642,7 @@ var Scorezilla = class _Scorezilla {
564
642
  #timeoutMs;
565
643
  #maxRetries;
566
644
  #sleepImpl;
645
+ #warnImpl;
567
646
  #userAgent;
568
647
  constructor(config) {
569
648
  if (isRealBrowserEnvironment()) {
@@ -586,6 +665,7 @@ var Scorezilla = class _Scorezilla {
586
665
  this.#timeoutMs = config.timeoutMs;
587
666
  this.#maxRetries = config.maxRetries;
588
667
  this.#sleepImpl = config.sleepImpl;
668
+ this.#warnImpl = config.warn;
589
669
  this.#userAgent = config.userAgent ?? defaultUserAgent(_Scorezilla.version);
590
670
  }
591
671
  /**
@@ -610,7 +690,8 @@ var Scorezilla = class _Scorezilla {
610
690
  playerId: input.playerId,
611
691
  score: input.score,
612
692
  ...input.metadata !== void 0 ? { metadata: input.metadata } : {}
613
- }
693
+ },
694
+ signal: input.signal
614
695
  });
615
696
  }
616
697
  /** Fetch the top-N entries on a board. */
@@ -620,14 +701,16 @@ var Scorezilla = class _Scorezilla {
620
701
  ...input.top !== void 0 ? { top: input.top } : {},
621
702
  ...input.offset !== void 0 ? { offset: input.offset } : {}
622
703
  }),
623
- method: "GET"
704
+ method: "GET",
705
+ signal: input.signal
624
706
  });
625
707
  }
626
708
  /** Look up a single player's rank on a board. */
627
709
  async getPlayerRank(input) {
628
710
  return this.#request({
629
711
  path: getPlayerRankPath(input.boardId, input.playerId),
630
- method: "GET"
712
+ method: "GET",
713
+ signal: input.signal
631
714
  });
632
715
  }
633
716
  /** Fetch the slice of entries surrounding a player. */
@@ -637,7 +720,8 @@ var Scorezilla = class _Scorezilla {
637
720
  ...input.before !== void 0 ? { before: input.before } : {},
638
721
  ...input.after !== void 0 ? { after: input.after } : {}
639
722
  }),
640
- method: "GET"
723
+ method: "GET",
724
+ signal: input.signal
641
725
  });
642
726
  }
643
727
  /**
@@ -668,7 +752,9 @@ var Scorezilla = class _Scorezilla {
668
752
  })
669
753
  };
670
754
  if (opts.body !== void 0) requestOpts.body = opts.body;
755
+ if (opts.signal !== void 0) requestOpts.signal = opts.signal;
671
756
  if (this.#fetchImpl !== void 0) requestOpts.fetchImpl = this.#fetchImpl;
757
+ if (this.#warnImpl !== void 0) requestOpts.warnImpl = this.#warnImpl;
672
758
  if (this.#timeoutMs !== void 0) requestOpts.timeoutMs = this.#timeoutMs;
673
759
  if (this.#maxRetries !== void 0 || this.#sleepImpl !== void 0) {
674
760
  requestOpts.retry = {