@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/dist/index.browser.cjs +56 -5
- package/dist/index.browser.cjs.map +1 -1
- package/dist/index.browser.d.cts +7 -0
- package/dist/index.browser.d.ts +7 -0
- package/dist/index.browser.js +56 -5
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +57 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +57 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/data/listen.ts +13 -1
- package/src/defineCreateClient.ts +1 -0
- package/src/http/browserMiddleware.ts +3 -1
- package/src/http/domainSharding.ts +96 -0
- package/src/http/nodeMiddleware.ts +3 -0
- package/src/types.ts +8 -0
- package/umd/sanityClient.js +56 -5
- package/umd/sanityClient.min.js +3 -3
package/package.json
CHANGED
package/src/data/listen.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
@@ -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
|
*/
|
package/umd/sanityClient.js
CHANGED
|
@@ -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 } })
|
|
2677
|
-
|
|
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*['"](.*)['"]/;
|