@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/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/http/browserMiddleware.ts +3 -1
- package/src/http/domainSharding.ts +96 -0
- package/src/http/nodeMiddleware.ts +3 -0
- package/src/http/requestOptions.ts +1 -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
|
|
|
@@ -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
|
*/
|
package/umd/sanityClient.js
CHANGED
|
@@ -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 } })
|
|
2677
|
-
|
|
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*['"](.*)['"]/;
|