@sanity/client 6.20.1 → 6.20.2-beta.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/client",
3
- "version": "6.20.1",
3
+ "version": "6.20.2-beta.1",
4
4
  "description": "Client for retrieving, creating and patching data from Sanity.io",
5
5
  "keywords": [
6
6
  "sanity",
@@ -1,5 +1,6 @@
1
1
  import {Observable} from 'rxjs'
2
2
 
3
+ import {domainSharder as sharder} from '../http/domainSharding'
3
4
  import type {ObservableSanityClient, SanityClient} from '../SanityClient'
4
5
  import type {Any, ListenEvent, ListenOptions, ListenParams, MutationEvent} from '../types'
5
6
  import defaults from '../util/defaults'
@@ -64,7 +65,11 @@ export function _listen<R extends Record<string, Any> = Record<string, Any>>(
64
65
  const listenOpts = pick(options, possibleOptions)
65
66
  const qs = encodeQueryString({query, params, options: {tag, ...listenOpts}})
66
67
 
67
- const uri = `${url}${_getDataUrl(this, 'listen', qs)}`
68
+ let uri = `${url}${_getDataUrl(this, 'listen', qs)}`
69
+ if (this.config().useDomainSharding) {
70
+ uri = sharder.getShardedUrl(uri)
71
+ }
72
+
68
73
  if (uri.length > MAX_URL_LENGTH) {
69
74
  return new Observable((observer) => observer.error(new Error('Query too large for listener')))
70
75
  }
@@ -91,6 +96,12 @@ export function _listen<R extends Record<string, Any> = Record<string, Any>>(
91
96
  // Once it is`true`, it will never be `false` again.
92
97
  let unsubscribed = false
93
98
 
99
+ // We're about to connect, and will reuse the same shard/bucket for every reconnect henceforth.
100
+ // This may seem inoptimal, but once connected we should just consider this as a "permanent"
101
+ // connection, since we'll automatically retry on failures/disconnects. Once we explicitly
102
+ // unsubsccribe, we can decrement the bucket and free up the shard.
103
+ sharder.incrementBucketForUrl(uri)
104
+
94
105
  open()
95
106
 
96
107
  function onError() {
@@ -187,6 +198,7 @@ export function _listen<R extends Record<string, Any> = Record<string, Any>>(
187
198
  stopped = true
188
199
  unsubscribe()
189
200
  unsubscribed = true
201
+ sharder.decrementBucketForUrl(uri)
190
202
  }
191
203
 
192
204
  return stop
@@ -1 +1,3 @@
1
- export default []
1
+ import {domainSharder} from './domainSharding'
2
+
3
+ export default [domainSharder.middleware]
@@ -0,0 +1,96 @@
1
+ import type {Middleware, RequestOptions} from 'get-it'
2
+
3
+ const UNSHARDED_URL_RE = /^https:\/\/([a-z0-9]+)\.api\.(sanity\..*)/
4
+ const SHARDED_URL_RE = /^https:\/\/[a-z0-9]+\.api\.s(\d+)\.sanity\.(.*)/
5
+
6
+ /**
7
+ * Get a default sharding implementation where buckets are reused across instances.
8
+ * Helps prevent the case when multiple clients are instantiated, each having their
9
+ * own state of which buckets are least used.
10
+ */
11
+ export const domainSharder = getDomainSharder()
12
+
13
+ /**
14
+ * @internal
15
+ */
16
+ export function getDomainSharder(initialBuckets?: number[]) {
17
+ const buckets: number[] = initialBuckets || new Array(10).fill(0, 0)
18
+
19
+ function incrementBucketForUrl(url: string) {
20
+ const shard = getShardFromUrl(url)
21
+ if (shard !== null) {
22
+ buckets[shard]++
23
+ }
24
+ }
25
+
26
+ function decrementBucketForUrl(url: string) {
27
+ const shard = getShardFromUrl(url)
28
+ if (shard !== null) {
29
+ buckets[shard]--
30
+ }
31
+ }
32
+
33
+ function getShardedUrl(url: string): string {
34
+ const [isMatch, projectId, rest] = url.match(UNSHARDED_URL_RE) || []
35
+ if (!isMatch) {
36
+ return url
37
+ }
38
+
39
+ // Find index of bucket with fewest requests
40
+ const bucket = buckets.reduce(
41
+ (smallest, count, index) => (count < buckets[smallest] ? index : smallest),
42
+ 0,
43
+ )
44
+
45
+ return `https://${projectId}.api.s${bucket}.${rest}`
46
+ }
47
+
48
+ function getShardFromUrl(url: string): number | null {
49
+ const [isMatch, shard] = url.match(SHARDED_URL_RE) || []
50
+ return isMatch ? parseInt(shard, 10) : null
51
+ }
52
+
53
+ const middleware = {
54
+ processOptions: (options: {useDomainSharding?: boolean; url: string}) => {
55
+ if (!useDomainSharding(options)) {
56
+ return options
57
+ }
58
+
59
+ const url = getShardedUrl(options.url)
60
+ options.url = url
61
+
62
+ return options
63
+ },
64
+
65
+ onRequest(req: {
66
+ options: Partial<RequestOptions> & {useDomainSharding?: boolean; url: string}
67
+ }) {
68
+ if (useDomainSharding(req.options)) {
69
+ incrementBucketForUrl(req.options.url)
70
+ }
71
+ return req
72
+ },
73
+
74
+ onResponse(
75
+ res,
76
+ context: {options: Partial<RequestOptions> & {useDomainSharding?: boolean; url: string}},
77
+ ) {
78
+ if (useDomainSharding(context.options)) {
79
+ decrementBucketForUrl(context.options.url)
80
+ }
81
+ return res
82
+ },
83
+ } satisfies Middleware
84
+
85
+ return {
86
+ middleware,
87
+ incrementBucketForUrl,
88
+ decrementBucketForUrl,
89
+ getShardedUrl,
90
+ getBuckets: () => buckets,
91
+ }
92
+ }
93
+
94
+ function useDomainSharding(options: RequestOptions | {useDomainSharding?: boolean}): boolean {
95
+ return 'useDomainSharding' in options && options.useDomainSharding === true
96
+ }
@@ -1,8 +1,11 @@
1
1
  import {agent, debug, headers} from 'get-it/middleware'
2
2
 
3
3
  import {name, version} from '../../package.json'
4
+ import {domainSharder} from './domainSharding'
4
5
 
5
6
  const middleware = [
7
+ domainSharder.middleware,
8
+
6
9
  debug({verbose: true, namespace: 'sanity:client'}),
7
10
  headers({'User-Agent': `${name} ${version}`}),
8
11
 
@@ -29,6 +29,7 @@ export function requestOptions(config: Any, overrides: Any = {}): Omit<RequestOp
29
29
  proxy: overrides.proxy || config.proxy,
30
30
  json: true,
31
31
  withCredentials,
32
+ useDomainSharding: config.useDomainSharding,
32
33
  fetch:
33
34
  typeof overrides.fetch === 'object' && typeof config.fetch === 'object'
34
35
  ? {...config.fetch, ...overrides.fetch}
package/src/types.ts CHANGED
@@ -46,6 +46,14 @@ export interface ClientConfig {
46
46
  apiVersion?: string
47
47
  proxy?: string
48
48
 
49
+ /**
50
+ * Spread the requests over a number of hostnames to work around HTTP/1.1 limitations.
51
+ * Only applicable in browsers, and for certain allowed projects.
52
+ *
53
+ * @alpha
54
+ */
55
+ useDomainSharding?: boolean
56
+
49
57
  /**
50
58
  * Optional request tag prefix for all request tags
51
59
  */
@@ -2393,6 +2393,7 @@ ${selectionOpts}`);
2393
2393
  proxy: overrides.proxy || config.proxy,
2394
2394
  json: !0,
2395
2395
  withCredentials,
2396
+ useDomainSharding: config.useDomainSharding,
2396
2397
  fetch: typeof overrides.fetch == "object" && typeof config.fetch == "object" ? { ...config.fetch, ...overrides.fetch } : overrides.fetch || config.fetch
2397
2398
  });
2398
2399
  }
@@ -2662,6 +2663,55 @@ ${selectionOpts}`);
2662
2663
  opts
2663
2664
  );
2664
2665
  }
2666
+ const UNSHARDED_URL_RE = /^https:\/\/([a-z0-9]+)\.api\.(sanity\..*)/, SHARDED_URL_RE = /^https:\/\/[a-z0-9]+\.api\.s(\d+)\.sanity\.(.*)/, domainSharder = getDomainSharder();
2667
+ function getDomainSharder(initialBuckets) {
2668
+ const buckets = new Array(10).fill(0, 0);
2669
+ function incrementBucketForUrl(url) {
2670
+ const shard = getShardFromUrl(url);
2671
+ shard !== null && buckets[shard]++;
2672
+ }
2673
+ function decrementBucketForUrl(url) {
2674
+ const shard = getShardFromUrl(url);
2675
+ shard !== null && buckets[shard]--;
2676
+ }
2677
+ function getShardedUrl(url) {
2678
+ const [isMatch, projectId2, rest] = url.match(UNSHARDED_URL_RE) || [];
2679
+ if (!isMatch)
2680
+ return url;
2681
+ const bucket = buckets.reduce(
2682
+ (smallest, count, index) => count < buckets[smallest] ? index : smallest,
2683
+ 0
2684
+ );
2685
+ return `https://${projectId2}.api.s${bucket}.${rest}`;
2686
+ }
2687
+ function getShardFromUrl(url) {
2688
+ const [isMatch, shard] = url.match(SHARDED_URL_RE) || [];
2689
+ return isMatch ? parseInt(shard, 10) : null;
2690
+ }
2691
+ return {
2692
+ middleware: {
2693
+ processOptions: (options) => {
2694
+ if (!useDomainSharding(options))
2695
+ return options;
2696
+ const url = getShardedUrl(options.url);
2697
+ return options.url = url, options;
2698
+ },
2699
+ onRequest(req) {
2700
+ return useDomainSharding(req.options) && incrementBucketForUrl(req.options.url), req;
2701
+ },
2702
+ onResponse(res, context) {
2703
+ return useDomainSharding(context.options) && decrementBucketForUrl(context.options.url), res;
2704
+ }
2705
+ },
2706
+ incrementBucketForUrl,
2707
+ decrementBucketForUrl,
2708
+ getShardedUrl,
2709
+ getBuckets: () => buckets
2710
+ };
2711
+ }
2712
+ function useDomainSharding(options) {
2713
+ return "useDomainSharding" in options && options.useDomainSharding === !0;
2714
+ }
2665
2715
  var defaults = (obj, defaults2) => Object.keys(defaults2).concat(Object.keys(obj)).reduce((target, prop) => (target[prop] = typeof obj[prop] > "u" ? defaults2[prop] : obj[prop], target), {});
2666
2716
  const pick = (obj, props) => props.reduce((selection, prop) => (typeof obj[prop] > "u" || (selection[prop] = obj[prop]), selection), {}), MAX_URL_LENGTH = 14800, possibleOptions = [
2667
2717
  "includePreviousRevision",
@@ -2673,15 +2723,16 @@ ${selectionOpts}`);
2673
2723
  includeResult: !0
2674
2724
  };
2675
2725
  function _listen(query, params, opts = {}) {
2676
- const { url, token, withCredentials, requestTagPrefix } = this.config(), tag = opts.tag && requestTagPrefix ? [requestTagPrefix, opts.tag].join(".") : opts.tag, options = { ...defaults(opts, defaultOptions), tag }, listenOpts = pick(options, possibleOptions), qs = encodeQueryString({ query, params, options: { tag, ...listenOpts } }), uri = `${url}${_getDataUrl(this, "listen", qs)}`;
2677
- if (uri.length > MAX_URL_LENGTH)
2726
+ const { url, token, withCredentials, requestTagPrefix } = this.config(), tag = opts.tag && requestTagPrefix ? [requestTagPrefix, opts.tag].join(".") : opts.tag, options = { ...defaults(opts, defaultOptions), tag }, listenOpts = pick(options, possibleOptions), qs = encodeQueryString({ query, params, options: { tag, ...listenOpts } });
2727
+ let uri = `${url}${_getDataUrl(this, "listen", qs)}`;
2728
+ if (this.config().useDomainSharding && (uri = domainSharder.getShardedUrl(uri)), uri.length > MAX_URL_LENGTH)
2678
2729
  return new Observable((observer) => observer.error(new Error("Query too large for listener")));
2679
2730
  const listenFor = options.events ? options.events : ["mutation"], shouldEmitReconnect = listenFor.indexOf("reconnect") !== -1, esOptions = {};
2680
2731
  return (token || withCredentials) && (esOptions.withCredentials = !0), token && (esOptions.headers = {
2681
2732
  Authorization: `Bearer ${token}`
2682
2733
  }), new Observable((observer) => {
2683
2734
  let es, reconnectTimer, stopped = !1, unsubscribed = !1;
2684
- open();
2735
+ domainSharder.incrementBucketForUrl(uri), open();
2685
2736
  function onError() {
2686
2737
  stopped || (emitReconnect(), !stopped && es.readyState === es.CLOSED && (unsubscribe(), clearTimeout(reconnectTimer), reconnectTimer = setTimeout(open, 100)));
2687
2738
  }
@@ -2716,7 +2767,7 @@ ${selectionOpts}`);
2716
2767
  });
2717
2768
  }
2718
2769
  function stop() {
2719
- stopped = !0, unsubscribe(), unsubscribed = !0;
2770
+ stopped = !0, unsubscribe(), unsubscribed = !0, domainSharder.decrementBucketForUrl(uri);
2720
2771
  }
2721
2772
  return stop;
2722
2773
  });
@@ -3337,7 +3388,7 @@ ${selectionOpts}`);
3337
3388
  return printNoDefaultExport(), createClient2(config);
3338
3389
  };
3339
3390
  }
3340
- var envMiddleware = [];
3391
+ var envMiddleware = [domainSharder.middleware];
3341
3392
  const exp = defineCreateClientExports(envMiddleware, SanityClient), requester = exp.requester, createClient = exp.createClient, deprecatedCreateClient = defineDeprecatedCreateClient(createClient);
3342
3393
 
3343
3394
  const reKeySegment = /_key\s*==\s*['"](.*)['"]/;