@sanity/client 6.20.1 → 6.20.2-beta.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/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.0",
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
@@ -33,6 +33,7 @@ export default function defineCreateClientExports<
33
33
  maxRedirects: 0,
34
34
  maxRetries: config.maxRetries,
35
35
  retryDelay: config.retryDelay,
36
+ useDomainSharding: config.useDomainSharding,
36
37
  ...options,
37
38
  } as Any),
38
39
  config,
@@ -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
 
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
  */
@@ -2662,6 +2662,55 @@ ${selectionOpts}`);
2662
2662
  opts
2663
2663
  );
2664
2664
  }
2665
+ const UNSHARDED_URL_RE = /^https:\/\/([a-z0-9]+)\.api\.(sanity\..*)/, SHARDED_URL_RE = /^https:\/\/[a-z0-9]+\.api\.s(\d+)\.sanity\.(.*)/, domainSharder = getDomainSharder();
2666
+ function getDomainSharder(initialBuckets) {
2667
+ const buckets = new Array(10).fill(0, 0);
2668
+ function incrementBucketForUrl(url) {
2669
+ const shard = getShardFromUrl(url);
2670
+ shard !== null && buckets[shard]++;
2671
+ }
2672
+ function decrementBucketForUrl(url) {
2673
+ const shard = getShardFromUrl(url);
2674
+ shard !== null && buckets[shard]--;
2675
+ }
2676
+ function getShardedUrl(url) {
2677
+ const [isMatch, projectId2, rest] = url.match(UNSHARDED_URL_RE) || [];
2678
+ if (!isMatch)
2679
+ return url;
2680
+ const bucket = buckets.reduce(
2681
+ (smallest, count, index) => count < buckets[smallest] ? index : smallest,
2682
+ 0
2683
+ );
2684
+ return `https://${projectId2}.api.s${bucket}.${rest}`;
2685
+ }
2686
+ function getShardFromUrl(url) {
2687
+ const [isMatch, shard] = url.match(SHARDED_URL_RE) || [];
2688
+ return isMatch ? parseInt(shard, 10) : null;
2689
+ }
2690
+ return {
2691
+ middleware: {
2692
+ processOptions: (options) => {
2693
+ if (!useDomainSharding(options))
2694
+ return options;
2695
+ const url = getShardedUrl(options.url);
2696
+ return options.url = url, options;
2697
+ },
2698
+ onRequest(req) {
2699
+ return useDomainSharding(req.options) && incrementBucketForUrl(req.options.url), req;
2700
+ },
2701
+ onResponse(res, context) {
2702
+ return useDomainSharding(context.options) && decrementBucketForUrl(context.options.url), res;
2703
+ }
2704
+ },
2705
+ incrementBucketForUrl,
2706
+ decrementBucketForUrl,
2707
+ getShardedUrl,
2708
+ getBuckets: () => buckets
2709
+ };
2710
+ }
2711
+ function useDomainSharding(options) {
2712
+ return "useDomainSharding" in options && options.useDomainSharding === !0;
2713
+ }
2665
2714
  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
2715
  const pick = (obj, props) => props.reduce((selection, prop) => (typeof obj[prop] > "u" || (selection[prop] = obj[prop]), selection), {}), MAX_URL_LENGTH = 14800, possibleOptions = [
2667
2716
  "includePreviousRevision",
@@ -2673,15 +2722,16 @@ ${selectionOpts}`);
2673
2722
  includeResult: !0
2674
2723
  };
2675
2724
  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)
2725
+ 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 } });
2726
+ let uri = `${url}${_getDataUrl(this, "listen", qs)}`;
2727
+ if (this.config().useDomainSharding && (uri = domainSharder.getShardedUrl(uri)), uri.length > MAX_URL_LENGTH)
2678
2728
  return new Observable((observer) => observer.error(new Error("Query too large for listener")));
2679
2729
  const listenFor = options.events ? options.events : ["mutation"], shouldEmitReconnect = listenFor.indexOf("reconnect") !== -1, esOptions = {};
2680
2730
  return (token || withCredentials) && (esOptions.withCredentials = !0), token && (esOptions.headers = {
2681
2731
  Authorization: `Bearer ${token}`
2682
2732
  }), new Observable((observer) => {
2683
2733
  let es, reconnectTimer, stopped = !1, unsubscribed = !1;
2684
- open();
2734
+ domainSharder.incrementBucketForUrl(uri), open();
2685
2735
  function onError() {
2686
2736
  stopped || (emitReconnect(), !stopped && es.readyState === es.CLOSED && (unsubscribe(), clearTimeout(reconnectTimer), reconnectTimer = setTimeout(open, 100)));
2687
2737
  }
@@ -2716,7 +2766,7 @@ ${selectionOpts}`);
2716
2766
  });
2717
2767
  }
2718
2768
  function stop() {
2719
- stopped = !0, unsubscribe(), unsubscribed = !0;
2769
+ stopped = !0, unsubscribe(), unsubscribed = !0, domainSharder.decrementBucketForUrl(uri);
2720
2770
  }
2721
2771
  return stop;
2722
2772
  });
@@ -3327,6 +3377,7 @@ ${selectionOpts}`);
3327
3377
  maxRedirects: 0,
3328
3378
  maxRetries: config.maxRetries,
3329
3379
  retryDelay: config.retryDelay,
3380
+ useDomainSharding: config.useDomainSharding,
3330
3381
  ...options
3331
3382
  }),
3332
3383
  config
@@ -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*['"](.*)['"]/;