@arbidocs/client 0.5.76 → 0.5.78

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
@@ -1525,6 +1525,88 @@ function createAutoReloginMiddleware(config) {
1525
1525
  };
1526
1526
  }
1527
1527
 
1528
+ // src/middleware/request-dedup.ts
1529
+ function authTokenOf(input, init) {
1530
+ if (input instanceof Request) {
1531
+ const fromRequest = input.headers.get("authorization");
1532
+ if (fromRequest) return fromRequest;
1533
+ }
1534
+ if (init?.headers) {
1535
+ return new Headers(init.headers).get("authorization") ?? "";
1536
+ }
1537
+ return "";
1538
+ }
1539
+ function createDedupFetch(originalFetch) {
1540
+ const inFlightRequests = /* @__PURE__ */ new Map();
1541
+ return function dedupFetch(input, init) {
1542
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
1543
+ if (method !== "GET") {
1544
+ return originalFetch(input, init);
1545
+ }
1546
+ const url = input instanceof Request ? input.url : String(input);
1547
+ const key = `GET:${url}\0${authTokenOf(input, init)}`;
1548
+ const existing = inFlightRequests.get(key);
1549
+ if (existing) {
1550
+ return existing.then((r) => r.clone());
1551
+ }
1552
+ const promise = originalFetch(input, init).finally(() => {
1553
+ inFlightRequests.delete(key);
1554
+ });
1555
+ inFlightRequests.set(key, promise);
1556
+ return promise.then((r) => r.clone());
1557
+ };
1558
+ }
1559
+
1560
+ // src/middleware/settings-write-breaker.ts
1561
+ var WINDOW_MS = 6e4;
1562
+ var MAX_WRITES_PER_WINDOW = 30;
1563
+ var SETTINGS_WRITE_BREAKER_CODE = "CLIENT_SETTINGS_WRITE_BREAKER";
1564
+ function isSettingsWrite(input, init) {
1565
+ const method = (input instanceof Request ? input.method : init?.method ?? "GET").toUpperCase();
1566
+ if (method !== "PATCH") return false;
1567
+ const url = input instanceof Request ? input.url : String(input);
1568
+ try {
1569
+ const base = typeof location !== "undefined" ? location.origin : "http://localhost";
1570
+ return new URL(url, base).pathname.endsWith("/v1/user/settings");
1571
+ } catch {
1572
+ return false;
1573
+ }
1574
+ }
1575
+ function breakerResponse() {
1576
+ return new Response(
1577
+ JSON.stringify({
1578
+ detail: {
1579
+ code: SETTINGS_WRITE_BREAKER_CODE,
1580
+ message: "Too many settings updates from this tab \u2014 write dropped by the client circuit breaker."
1581
+ }
1582
+ }),
1583
+ { status: 429, headers: { "Content-Type": "application/json" } }
1584
+ );
1585
+ }
1586
+ function createSettingsWriteBreakerFetch(originalFetch) {
1587
+ let acceptedAt = [];
1588
+ let trippedThisEpisode = false;
1589
+ return function settingsWriteBreakerFetch(input, init) {
1590
+ if (!isSettingsWrite(input, init)) {
1591
+ return originalFetch(input, init);
1592
+ }
1593
+ const now = Date.now();
1594
+ acceptedAt = acceptedAt.filter((t) => now - t < WINDOW_MS);
1595
+ if (acceptedAt.length >= MAX_WRITES_PER_WINDOW) {
1596
+ if (!trippedThisEpisode) {
1597
+ trippedThisEpisode = true;
1598
+ console.error(
1599
+ `[settings-write-breaker] over ${MAX_WRITES_PER_WINDOW} PATCH /v1/user/settings in ${WINDOW_MS / 1e3}s \u2014 dropping further writes. This indicates a client-side write loop.`
1600
+ );
1601
+ }
1602
+ return Promise.resolve(breakerResponse());
1603
+ }
1604
+ trippedThisEpisode = false;
1605
+ acceptedAt.push(now);
1606
+ return originalFetch(input, init);
1607
+ };
1608
+ }
1609
+
1528
1610
  // src/relogin/handler.ts
1529
1611
  var RELOGIN_COOLDOWN_MS = 5e3;
1530
1612
  function createReloginHandler(deps) {
@@ -2065,11 +2147,12 @@ function createTransport(options) {
2065
2147
  const baseUrlOption = options.baseUrl;
2066
2148
  const base = typeof baseUrlOption === "function" ? baseUrlOption : () => baseUrlOption;
2067
2149
  const transport = createHttpTransport(options.transport);
2068
- const retryFetch = createRetryFetch(transport.fetch);
2150
+ const guardedFetch = createSettingsWriteBreakerFetch(createDedupFetch(transport.fetch));
2151
+ const retryFetch = createRetryFetch(guardedFetch);
2069
2152
  const gate = createSessionGate();
2070
2153
  const sessionFetch = gatedFetch(gate, retryFetch);
2071
2154
  const http = {
2072
- fetch: gatedFetch(gate, transport.fetch),
2155
+ fetch: gatedFetch(gate, guardedFetch),
2073
2156
  customFetch: transport.customFetch,
2074
2157
  withFetch: (adapter) => gatedFetch(gate, transport.withFetch(adapter))
2075
2158
  };
@@ -2437,6 +2520,7 @@ exports.CHAT_KEY_BYTES = CHAT_KEY_BYTES;
2437
2520
  exports.ENCRYPTED_RECOVERY_KEY_SIZE = ENCRYPTED_RECOVERY_KEY_SIZE;
2438
2521
  exports.RECOVERY_KEY_ERRORS = RECOVERY_KEY_ERRORS;
2439
2522
  exports.SECURE_MAIL_KEY_BYTES = SECURE_MAIL_KEY_BYTES;
2523
+ exports.SETTINGS_WRITE_BREAKER_CODE = SETTINGS_WRITE_BREAKER_CODE;
2440
2524
  exports.UNENCRYPTED_RECOVERY_KEY_SIZE = UNENCRYPTED_RECOVERY_KEY_SIZE;
2441
2525
  exports.authRequestError = authRequestError;
2442
2526
  exports.base64Decode = base64Decode;