@gibs/common 1.18.0 → 1.20.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/assert.js +0 -16
- package/dist/cache.js +0 -26
- package/dist/cjs/assert.js +0 -16
- package/dist/cjs/cache.js +0 -26
- package/dist/cjs/client.js +0 -91
- package/dist/cjs/erc20.js +0 -8
- package/dist/cjs/multicall.js +0 -124
- package/dist/cjs/rpcPacer.js +0 -85
- package/dist/cjs/serialize.js +0 -19
- package/dist/cjs/url.js +0 -139
- package/dist/client.js +0 -91
- package/dist/erc20.js +0 -8
- package/dist/multicall.js +0 -124
- package/dist/rpcPacer.js +0 -85
- package/dist/serialize.js +0 -19
- package/dist/url.js +0 -139
- package/package.json +1 -1
package/dist/rpcPacer.js
CHANGED
|
@@ -1,78 +1,16 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Holds outgoing JSON-RPC requests back so no method crosses the endpoint's
|
|
3
|
-
* per-second ceiling.
|
|
4
|
-
*
|
|
5
|
-
* WHY BATCH SIZE WAS NOT ENOUGH, WHICH IS THE WHOLE POINT OF THIS FILE. The
|
|
6
|
-
* transport already caps a single post at the ceiling, and that stops one post
|
|
7
|
-
* being refused for being too big. It says nothing about how many posts leave
|
|
8
|
-
* per second. Measured on one page load: nine `eth_call` inside a single rolling
|
|
9
|
-
* second, spread across four posts of which none was individually too large, and
|
|
10
|
-
* the fourth came back `429`.
|
|
11
|
-
*
|
|
12
|
-
* A ROLLING WINDOW, NOT A BUCKET. The first measurement of this counted calls
|
|
13
|
-
* into whole seconds aligned to the page load and reported nothing over the
|
|
14
|
-
* ceiling while a refusal sat in the same run — six calls at 1.9 seconds and
|
|
15
|
-
* seven at 2.1 fall in different buckets and land 200 milliseconds apart. A rate
|
|
16
|
-
* limiter does not know when our page started, so neither does this.
|
|
17
|
-
*
|
|
18
|
-
* ONE LIMITER FOR THE WHOLE APPLICATION, not one per client — but keyed on the
|
|
19
|
-
* HOST as well as the method.
|
|
20
|
-
*
|
|
21
|
-
* The budget is the host's, and it is shared across chains: six
|
|
22
|
-
* `eth_blockNumber` on PulseChain plus six on Ethereum were answered exactly
|
|
23
|
-
* eight times, the same as twelve on one chain. So a limiter per client would
|
|
24
|
-
* let two chains spend one budget twice and neither would know.
|
|
25
|
-
*
|
|
26
|
-
* But two DIFFERENT hosts have two different budgets, and counting them
|
|
27
|
-
* together would throttle a spare endpoint for traffic it never received —
|
|
28
|
-
* which is exactly backwards once requests are spread across hosts on purpose.
|
|
29
|
-
*/
|
|
30
|
-
/**
|
|
31
|
-
* The most calls of ONE method the proxy answers in one second. MEASURED.
|
|
32
|
-
*
|
|
33
|
-
* `one.valve.city` refuses past this with HTTP 429 and the body
|
|
34
|
-
* `Rate limit exceeded (per method, per second)`. Probed 2026-09-12 at
|
|
35
|
-
* concurrency 1, 2, 3, 4, 6, 8, 12, 16 and 24: every request up to eight was
|
|
36
|
-
* answered and everything past eight was refused, with no variance across
|
|
37
|
-
* twenty rounds. The budget is shared across CHAINS — six calls on chain 369
|
|
38
|
-
* plus six on chain 1 were answered exactly eight times, the same as twelve on
|
|
39
|
-
* one chain — so it is keyed on the method, not the chain.
|
|
40
|
-
*
|
|
41
|
-
* A BATCH THAT CROSSES THE CEILING IS REFUSED WHOLE, which is why this bounds
|
|
42
|
-
* the batch size. One post carrying sixteen `eth_blockNumber` calls came back
|
|
43
|
-
* with all sixteen refused; the same sixteen sent as separate posts had eight
|
|
44
|
-
* answered. Coalescing past the ceiling therefore converts a partial refusal
|
|
45
|
-
* into a total one, and viem's default of thirty-two calls per post is four
|
|
46
|
-
* times over it.
|
|
47
|
-
*/
|
|
48
1
|
export const MAX_SAME_METHOD_CALLS_PER_SECOND = 8;
|
|
49
2
|
const systemClock = {
|
|
50
3
|
now: () => Date.now(),
|
|
51
4
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
52
5
|
};
|
|
53
|
-
/** The window the ceiling is counted over. */
|
|
54
6
|
const WINDOW_MS = 1_000;
|
|
55
|
-
/**
|
|
56
|
-
* Builds a pacer.
|
|
57
|
-
*
|
|
58
|
-
* @param options - the ceiling, and optionally a clock to drive it.
|
|
59
|
-
* @returns a pacer.
|
|
60
|
-
*/
|
|
61
7
|
export const createRpcPacer = ({ ceiling, clock = systemClock, }) => {
|
|
62
|
-
/** When each still-counting call was sent, per host-and-method, oldest first. */
|
|
63
8
|
const sent = new Map();
|
|
64
|
-
/**
|
|
65
|
-
* Reservations run ONE AT A TIME, chained. Two callers checking the window
|
|
66
|
-
* concurrently both see room and both take it — the classic check-then-act
|
|
67
|
-
* race, and the reason a limiter that looks right can still emit bursts. The
|
|
68
|
-
* chain makes "is there room" and "take it" a single step.
|
|
69
|
-
*/
|
|
70
9
|
let queue = Promise.resolve();
|
|
71
10
|
const forget = (method, now) => {
|
|
72
11
|
const times = sent.get(method);
|
|
73
12
|
if (!times)
|
|
74
13
|
return [];
|
|
75
|
-
// Times are appended in order, so everything expired is a prefix.
|
|
76
14
|
let keep = 0;
|
|
77
15
|
while (keep < times.length && now - times[keep] >= WINDOW_MS)
|
|
78
16
|
keep += 1;
|
|
@@ -81,11 +19,6 @@ export const createRpcPacer = ({ ceiling, clock = systemClock, }) => {
|
|
|
81
19
|
return live;
|
|
82
20
|
};
|
|
83
21
|
const reserveOne = async (key, count) => {
|
|
84
|
-
// A request carrying MORE than the ceiling of one method cannot ever fit,
|
|
85
|
-
// and waiting for room that will never exist would hang the page forever.
|
|
86
|
-
// The transport caps a post at the ceiling, so this is a guard against a
|
|
87
|
-
// caller that bypassed it rather than an expected path: spend what there is
|
|
88
|
-
// and let the endpoint answer for the rest.
|
|
89
22
|
const want = Math.min(count, ceiling);
|
|
90
23
|
for (;;) {
|
|
91
24
|
const now = clock.now();
|
|
@@ -96,9 +29,6 @@ export const createRpcPacer = ({ ceiling, clock = systemClock, }) => {
|
|
|
96
29
|
sent.set(key, live);
|
|
97
30
|
return;
|
|
98
31
|
}
|
|
99
|
-
// Wait exactly until the oldest call leaves the window — not a fixed
|
|
100
|
-
// retry interval, which would either spin or overshoot. The `+ 1` is so
|
|
101
|
-
// the comparison above is past the boundary rather than on it.
|
|
102
32
|
const oldest = live[0];
|
|
103
33
|
await clock.sleep(Math.max(1, oldest + WINDOW_MS - now + 1));
|
|
104
34
|
}
|
|
@@ -106,31 +36,16 @@ export const createRpcPacer = ({ ceiling, clock = systemClock, }) => {
|
|
|
106
36
|
return {
|
|
107
37
|
reserve: (host, counts) => {
|
|
108
38
|
const run = queue.then(async () => {
|
|
109
|
-
// Ordered so the same body always reserves its methods in the same
|
|
110
|
-
// sequence; two bodies reserving the same pair in opposite orders is
|
|
111
|
-
// how a queue deadlocks itself.
|
|
112
39
|
for (const method of [...counts.keys()].sort()) {
|
|
113
40
|
await reserveOne(`${host}|${method}`, counts.get(method) ?? 0);
|
|
114
41
|
}
|
|
115
42
|
});
|
|
116
|
-
// The queue must continue even when a reservation throws, or one failure
|
|
117
|
-
// stops every later request on the page.
|
|
118
43
|
queue = run.catch(() => undefined);
|
|
119
44
|
return run;
|
|
120
45
|
},
|
|
121
46
|
spent: (host, method) => forget(`${host}|${method}`, clock.now()).length,
|
|
122
47
|
};
|
|
123
48
|
};
|
|
124
|
-
/**
|
|
125
|
-
* Counts the calls per method in a JSON-RPC body.
|
|
126
|
-
*
|
|
127
|
-
* A batch is an ARRAY and a single call is an object; both arrive here, and a
|
|
128
|
-
* body that cannot be read at all counts as nothing rather than throwing —
|
|
129
|
-
* pacing is an optimisation and must never be the reason a request fails.
|
|
130
|
-
*
|
|
131
|
-
* @param body - the request body, as text.
|
|
132
|
-
* @returns method to how many calls of it the body carries.
|
|
133
|
-
*/
|
|
134
49
|
export const countMethods = (body) => {
|
|
135
50
|
const counts = new Map();
|
|
136
51
|
if (!body)
|
package/dist/serialize.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const BIGINT_TYPE = 'bigint';
|
|
2
|
-
/** The json any stringify for the given key and value */
|
|
3
2
|
export const jsonAnyStringify = (_key, value) => {
|
|
4
3
|
if (typeof value === BIGINT_TYPE) {
|
|
5
4
|
return {
|
|
@@ -9,28 +8,10 @@ export const jsonAnyStringify = (_key, value) => {
|
|
|
9
8
|
}
|
|
10
9
|
return value;
|
|
11
10
|
};
|
|
12
|
-
/** The is serialized big int for the given value */
|
|
13
11
|
export const isSerializedBigInt = (value) => {
|
|
14
12
|
return (!!value && typeof value === 'object' && value.__type__ === BIGINT_TYPE);
|
|
15
13
|
};
|
|
16
|
-
/**
|
|
17
|
-
* The only strings `jsonAnyStringify` ever writes: what `bigint.toString()`
|
|
18
|
-
* produces, which is an optional minus sign followed by decimal digits.
|
|
19
|
-
*
|
|
20
|
-
* Anything else in the `value` field means the record is corrupt, and `BigInt`
|
|
21
|
-
* is too permissive to notice. `BigInt('')` is 0n and `BigInt('0x10')` is 16n,
|
|
22
|
-
* so a truncated or garbled record parses into a plausible-looking balance
|
|
23
|
-
* instead of an error — and a wrong balance is spent, displayed and believed.
|
|
24
|
-
*/
|
|
25
14
|
const DECIMAL_BIG_INT = /^-?\d+$/;
|
|
26
|
-
/**
|
|
27
|
-
* The json any parse for the given key and value.
|
|
28
|
-
*
|
|
29
|
-
* Throws on a corrupt encoded value rather than returning a number for it. Every
|
|
30
|
-
* caller reads from local storage inside a try/catch and falls back to a
|
|
31
|
-
* default, so a throw discards one unusable record; a silent zero would survive
|
|
32
|
-
* into the arithmetic.
|
|
33
|
-
*/
|
|
34
15
|
export const jsonAnyParse = (_key, value) => {
|
|
35
16
|
if (isSerializedBigInt(value)) {
|
|
36
17
|
if (typeof value.value !== 'string' || !DECIMAL_BIG_INT.test(value.value)) {
|
package/dist/url.js
CHANGED
|
@@ -1,30 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Request addresses, built and read with plain string work.
|
|
3
|
-
*
|
|
4
|
-
* WHY THIS EXISTS: React Native 0.77 installs its own `URL` and
|
|
5
|
-
* `URLSearchParams` (react-native/Libraries/Blob/URL.js), and both are partial.
|
|
6
|
-
* `new URL(x).protocol`, `.host`, `.pathname` and `.search` throw "not
|
|
7
|
-
* implemented". `searchParams` is always a new, empty list, so a reader of it
|
|
8
|
-
* silently sees no parameters. `URLSearchParams` can only append and
|
|
9
|
-
* serialize: `get`, `set`, `has` and `delete` throw, and a string passed to
|
|
10
|
-
* its constructor is ignored. A package that uses either class needs a
|
|
11
|
-
* polyfill in a wallet. These functions need nothing from the runtime but
|
|
12
|
-
* `encodeURIComponent` and `decodeURIComponent`.
|
|
13
|
-
*
|
|
14
|
-
* Their output matches what the WHATWG classes produce for the addresses the
|
|
15
|
-
* Gibs packages build: a lowercase `http:` or `https:` origin with a plain
|
|
16
|
-
* path. A query is encoded exactly as `URLSearchParams` encodes it, as
|
|
17
|
-
* `application/x-www-form-urlencoded`: a space becomes `+`, and only letters,
|
|
18
|
-
* digits and `*-._` stay as they are. They do not normalize the address
|
|
19
|
-
* itself: an uppercase host or a `..` path segment passes through as written.
|
|
20
|
-
*/
|
|
21
|
-
/** A surrogate pair, or a lone surrogate half. */
|
|
22
1
|
const surrogates = /[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDFFF]/g;
|
|
23
|
-
/** The characters `encodeURIComponent` keeps that a form encoder escapes. */
|
|
24
2
|
const formEscapedCharacters = /[!'()~]/g;
|
|
25
|
-
/** A run of percent-encoded bytes. */
|
|
26
3
|
const percentEncodedRun = /(?:%[0-9A-Fa-f]{2})+/g;
|
|
27
|
-
/** The port each special scheme leaves out of its host. */
|
|
28
4
|
const defaultPorts = Object.freeze({
|
|
29
5
|
http: '80',
|
|
30
6
|
https: '443',
|
|
@@ -32,38 +8,11 @@ const defaultPorts = Object.freeze({
|
|
|
32
8
|
wss: '443',
|
|
33
9
|
ftp: '21',
|
|
34
10
|
});
|
|
35
|
-
/**
|
|
36
|
-
* Replaces each lone surrogate half with U+FFFD, as the WHATWG classes do
|
|
37
|
-
* before they encode. `encodeURIComponent` would throw on one instead.
|
|
38
|
-
*
|
|
39
|
-
* @param text - any string
|
|
40
|
-
* @returns the string, well formed
|
|
41
|
-
*/
|
|
42
11
|
const toWellFormed = (text) => text.replace(surrogates, (match) => (match.length === 2 ? match : '�'));
|
|
43
|
-
/**
|
|
44
|
-
* Percent-encodes one character as two uppercase hex digits.
|
|
45
|
-
*
|
|
46
|
-
* @param character - a single ASCII character
|
|
47
|
-
* @returns the character as `%XX`
|
|
48
|
-
*/
|
|
49
12
|
const percentEncodeCharacter = (character) => `%${character.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0')}`;
|
|
50
|
-
/**
|
|
51
|
-
* Encodes a query name or value exactly as `URLSearchParams.toString` does.
|
|
52
|
-
*
|
|
53
|
-
* @param text - the raw name or value
|
|
54
|
-
* @returns the encoded text
|
|
55
|
-
*/
|
|
56
13
|
export const encodeQueryComponent = (text) => encodeURIComponent(toWellFormed(text))
|
|
57
14
|
.replace(formEscapedCharacters, percentEncodeCharacter)
|
|
58
15
|
.replace(/%20/g, '+');
|
|
59
|
-
/**
|
|
60
|
-
* Counts the continuation bytes a UTF-8 lead byte announces, and the bounds
|
|
61
|
-
* the first of them must fall in. The bounds reject overlong forms and
|
|
62
|
-
* surrogates, as the WHATWG decoder does.
|
|
63
|
-
*
|
|
64
|
-
* @param lead - the first byte of a sequence
|
|
65
|
-
* @returns the count, the lead byte's own bits, and the first continuation byte's bounds; null for a byte that cannot start one
|
|
66
|
-
*/
|
|
67
16
|
const utf8Lead = (lead) => {
|
|
68
17
|
if (lead >= 0xc2 && lead <= 0xdf)
|
|
69
18
|
return { count: 1, bits: lead & 0x1f, lower: 0x80, upper: 0xbf };
|
|
@@ -81,14 +30,6 @@ const utf8Lead = (lead) => {
|
|
|
81
30
|
return { count: 3, bits: lead & 0x07, lower: 0x80, upper: 0xbf };
|
|
82
31
|
return null;
|
|
83
32
|
};
|
|
84
|
-
/**
|
|
85
|
-
* Decodes UTF-8 bytes as the WHATWG decoder does: each malformed part becomes
|
|
86
|
-
* one U+FFFD, and a byte that ends a malformed part is read again as the
|
|
87
|
-
* start of the next. Only reached when `decodeURIComponent` refuses a run.
|
|
88
|
-
*
|
|
89
|
-
* @param bytes - the bytes
|
|
90
|
-
* @returns the decoded text
|
|
91
|
-
*/
|
|
92
33
|
const decodeUtf8Leniently = (bytes) => {
|
|
93
34
|
const decode = (index, decoded) => {
|
|
94
35
|
if (index >= bytes.length)
|
|
@@ -116,12 +57,6 @@ const decodeUtf8Leniently = (bytes) => {
|
|
|
116
57
|
};
|
|
117
58
|
return decode(0, '');
|
|
118
59
|
};
|
|
119
|
-
/**
|
|
120
|
-
* Decodes one run of `%XX` bytes.
|
|
121
|
-
*
|
|
122
|
-
* @param run - consecutive `%XX` sequences
|
|
123
|
-
* @returns the text they encode
|
|
124
|
-
*/
|
|
125
60
|
const decodePercentRun = (run) => {
|
|
126
61
|
try {
|
|
127
62
|
return decodeURIComponent(run);
|
|
@@ -130,22 +65,7 @@ const decodePercentRun = (run) => {
|
|
|
130
65
|
return decodeUtf8Leniently(run.slice(1).split('%').map((pair) => Number.parseInt(pair, 16)));
|
|
131
66
|
}
|
|
132
67
|
};
|
|
133
|
-
/**
|
|
134
|
-
* Decodes a query name or value as the WHATWG URL standard says
|
|
135
|
-
* `URLSearchParams` must: `+` is a space, a valid `%XX` is a byte, a `%` that
|
|
136
|
-
* starts no valid sequence stays as it is, and each malformed UTF-8 part, or
|
|
137
|
-
* lone surrogate half, becomes U+FFFD.
|
|
138
|
-
*
|
|
139
|
-
* @param text - the encoded name or value
|
|
140
|
-
* @returns the decoded text
|
|
141
|
-
*/
|
|
142
68
|
export const decodeQueryComponent = (text) => toWellFormed(text).replace(/\+/g, ' ').replace(percentEncodedRun, decodePercentRun);
|
|
143
|
-
/**
|
|
144
|
-
* Lists the parameters of a query, decoded and in order.
|
|
145
|
-
*
|
|
146
|
-
* @param query - a query, with or without its leading `?`
|
|
147
|
-
* @returns each parameter's name and value; a parameter with no `=` has an empty value
|
|
148
|
-
*/
|
|
149
69
|
export const parseQuery = (query) => query
|
|
150
70
|
.replace(/^\?/, '')
|
|
151
71
|
.split('&')
|
|
@@ -156,36 +76,14 @@ export const parseQuery = (query) => query
|
|
|
156
76
|
const value = equalsAt === -1 ? '' : part.slice(equalsAt + 1);
|
|
157
77
|
return [decodeQueryComponent(name), decodeQueryComponent(value)];
|
|
158
78
|
});
|
|
159
|
-
/**
|
|
160
|
-
* Turns query parameters into ordered pairs.
|
|
161
|
-
*
|
|
162
|
-
* @param parameters - a record or pairs; see {@link QueryParameters}
|
|
163
|
-
* @returns the pairs, with every `undefined` record value left out
|
|
164
|
-
*/
|
|
165
79
|
const toEntries = (parameters) => {
|
|
166
80
|
if (Array.isArray(parameters))
|
|
167
81
|
return parameters;
|
|
168
82
|
return Object.entries(parameters).filter((entry) => entry[1] !== undefined);
|
|
169
83
|
};
|
|
170
|
-
/**
|
|
171
|
-
* Writes a query, as `new URLSearchParams(parameters).toString()` does.
|
|
172
|
-
*
|
|
173
|
-
* @param parameters - a record or pairs; see {@link QueryParameters}
|
|
174
|
-
* @returns the encoded query, with no leading `?`; empty for no parameters
|
|
175
|
-
*/
|
|
176
84
|
export const buildQuery = (parameters) => toEntries(parameters)
|
|
177
85
|
.map(([name, value]) => `${encodeQueryComponent(name)}=${encodeQueryComponent(value)}`)
|
|
178
86
|
.join('&');
|
|
179
|
-
/**
|
|
180
|
-
* Sets one parameter in a query, as `URLSearchParams.set` does: the first
|
|
181
|
-
* parameter of that name takes the value and any later ones are removed, or
|
|
182
|
-
* the parameter is appended when none has the name.
|
|
183
|
-
*
|
|
184
|
-
* @param query - an encoded query, with or without its leading `?`
|
|
185
|
-
* @param name - the parameter's name
|
|
186
|
-
* @param value - its value
|
|
187
|
-
* @returns the encoded query, with no leading `?`
|
|
188
|
-
*/
|
|
189
87
|
export const setQueryParameter = (query, name, value) => {
|
|
190
88
|
const entries = parseQuery(query);
|
|
191
89
|
const firstAt = entries.findIndex(([entryName]) => entryName === name);
|
|
@@ -197,12 +95,6 @@ export const setQueryParameter = (query, name, value) => {
|
|
|
197
95
|
return entry[0] === name ? [] : [entry];
|
|
198
96
|
}));
|
|
199
97
|
};
|
|
200
|
-
/**
|
|
201
|
-
* Splits an address at its first `#` and then at its first `?`.
|
|
202
|
-
*
|
|
203
|
-
* @param address - any address
|
|
204
|
-
* @returns everything before the query, the query and the fragment, each without its delimiter
|
|
205
|
-
*/
|
|
206
98
|
const splitAtQuery = (address) => {
|
|
207
99
|
const fragmentAt = address.indexOf('#');
|
|
208
100
|
const beforeFragment = fragmentAt === -1 ? address : address.slice(0, fragmentAt);
|
|
@@ -212,15 +104,6 @@ const splitAtQuery = (address) => {
|
|
|
212
104
|
return { beforeQuery: beforeFragment, query: '', fragment };
|
|
213
105
|
return { beforeQuery: beforeFragment.slice(0, queryAt), query: beforeFragment.slice(queryAt + 1), fragment };
|
|
214
106
|
};
|
|
215
|
-
/**
|
|
216
|
-
* Adds query parameters to an address, as `url.searchParams.set` does for
|
|
217
|
-
* each one in turn. A query already on the address is kept, and a fragment
|
|
218
|
-
* stays at the end.
|
|
219
|
-
*
|
|
220
|
-
* @param address - the address, with or without a query
|
|
221
|
-
* @param parameters - a record or pairs; see {@link QueryParameters}
|
|
222
|
-
* @returns the address with the parameters set; the address unchanged when there are none
|
|
223
|
-
*/
|
|
224
107
|
export const buildUrl = (address, parameters) => {
|
|
225
108
|
const entries = toEntries(parameters);
|
|
226
109
|
if (entries.length === 0)
|
|
@@ -229,15 +112,6 @@ export const buildUrl = (address, parameters) => {
|
|
|
229
112
|
const updated = entries.reduce((current, [name, value]) => setQueryParameter(current, name, value), query);
|
|
230
113
|
return `${beforeQuery}?${updated}${fragment === '' ? '' : `#${fragment}`}`;
|
|
231
114
|
};
|
|
232
|
-
/**
|
|
233
|
-
* Reads the host the WHATWG `URL` reports from an authority: lowercase, with
|
|
234
|
-
* any user name and password dropped, and the port dropped when it is the
|
|
235
|
-
* scheme's default.
|
|
236
|
-
*
|
|
237
|
-
* @param authority - the text between `//` and the path
|
|
238
|
-
* @param scheme - the lowercase scheme
|
|
239
|
-
* @returns the host
|
|
240
|
-
*/
|
|
241
115
|
const hostOfAuthority = (authority, scheme) => {
|
|
242
116
|
const hostAndPort = authority.slice(authority.lastIndexOf('@') + 1).toLowerCase();
|
|
243
117
|
const port = /:(\d*)$/.exec(hostAndPort);
|
|
@@ -249,12 +123,6 @@ const hostOfAuthority = (authority, scheme) => {
|
|
|
249
123
|
const portNumber = String(Number(port[1]));
|
|
250
124
|
return portNumber === defaultPorts[scheme] ? hostname : `${hostname}:${portNumber}`;
|
|
251
125
|
};
|
|
252
|
-
/**
|
|
253
|
-
* Splits an address into its parts, without the WHATWG `URL` class.
|
|
254
|
-
*
|
|
255
|
-
* @param address - any address; leading and trailing spaces and control characters are ignored, as the WHATWG parser ignores them
|
|
256
|
-
* @returns the parts; see {@link UrlParts}
|
|
257
|
-
*/
|
|
258
126
|
export const splitUrl = (address) => {
|
|
259
127
|
const trimmed = address.replace(/^[\u0000- ]+|[\u0000- ]+$/g, '');
|
|
260
128
|
const { beforeQuery, query, fragment } = splitAtQuery(trimmed);
|
|
@@ -268,13 +136,6 @@ export const splitUrl = (address) => {
|
|
|
268
136
|
const path = pathAt === -1 ? '/' : rest.slice(pathAt);
|
|
269
137
|
return { scheme, host: hostOfAuthority(authority, scheme), path, query, fragment };
|
|
270
138
|
};
|
|
271
|
-
/**
|
|
272
|
-
* Says whether a value is an absolute `http:` or `https:` address with a host.
|
|
273
|
-
* It reads the scheme by its prefix, so it needs no `URL` class.
|
|
274
|
-
*
|
|
275
|
-
* @param value - any string
|
|
276
|
-
* @returns true for an absolute `http:` or `https:` address
|
|
277
|
-
*/
|
|
278
139
|
export const isHttpUrl = (value) => {
|
|
279
140
|
const { scheme, host } = splitUrl(value);
|
|
280
141
|
return (scheme === 'http' || scheme === 'https') && host !== '';
|