@calimero-network/mero-js 27.4.0 → 27.5.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/README.md +32 -3
- package/dist/cloud-v2/client.d.ts +61 -8
- package/dist/cloud-v2/client.js +113 -22
- package/dist/cloud-v2/directory.d.ts +50 -0
- package/dist/cloud-v2/directory.js +115 -0
- package/dist/cloud-v2/index.d.ts +3 -1
- package/dist/cloud-v2/index.js +1 -0
- package/dist/index.browser.mjs +4 -4
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +183 -19
- package/dist/index.cjs.map +4 -4
- package/dist/index.mjs +183 -19
- package/dist/index.mjs.map +4 -4
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -40,8 +40,10 @@ __export(index_exports, {
|
|
|
40
40
|
ConfirmRequired: () => ConfirmRequired,
|
|
41
41
|
ConsentStale: () => ConsentStale,
|
|
42
42
|
DEFAULT_COLLATERAL_MIRROR: () => DEFAULT_COLLATERAL_MIRROR,
|
|
43
|
+
DEFAULT_DIRECTORY: () => DEFAULT_DIRECTORY,
|
|
43
44
|
DEFAULT_ORG_CAPABILITIES: () => DEFAULT_ORG_CAPABILITIES,
|
|
44
45
|
DEFAULT_RELEASE_MIRROR: () => DEFAULT_RELEASE_MIRROR,
|
|
46
|
+
DirectoryError: () => DirectoryError,
|
|
45
47
|
GITHUB_RELEASES: () => GITHUB_RELEASES,
|
|
46
48
|
HANDSHAKE_PATH: () => HANDSHAKE_PATH,
|
|
47
49
|
IdempotencyMismatch: () => IdempotencyMismatch,
|
|
@@ -1357,6 +1359,85 @@ async function fetchAttestedKey(baseUrl, policy, fetchImpl = (input, init) => fe
|
|
|
1357
1359
|
return transportKey;
|
|
1358
1360
|
}
|
|
1359
1361
|
|
|
1362
|
+
// src/cloud-v2/directory.ts
|
|
1363
|
+
var DEFAULT_DIRECTORY = "https://manager.cloud.calimero.network/api/v2/directory";
|
|
1364
|
+
var DEFAULT_REFRESH_SECONDS = 60;
|
|
1365
|
+
var DirectoryError = class extends Error {
|
|
1366
|
+
constructor(cluster, reason, message, status, cause) {
|
|
1367
|
+
super(`directory for cluster ${cluster}: ${message}`);
|
|
1368
|
+
this.cluster = cluster;
|
|
1369
|
+
this.reason = reason;
|
|
1370
|
+
this.status = status;
|
|
1371
|
+
this.cause = cause;
|
|
1372
|
+
this.name = "DirectoryError";
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
function directoryUrl(directory, cluster) {
|
|
1376
|
+
return `${directory.replace(/\/+$/, "")}/${encodeURIComponent(cluster)}`;
|
|
1377
|
+
}
|
|
1378
|
+
async function readDirectory(directory, cluster, fetcher) {
|
|
1379
|
+
let response;
|
|
1380
|
+
try {
|
|
1381
|
+
response = await fetcher(directoryUrl(directory, cluster), { method: "GET", headers: { accept: "application/json" } });
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
throw new DirectoryError(cluster, "unreachable", `could not be reached: ${String(error)}`, void 0, error);
|
|
1384
|
+
}
|
|
1385
|
+
if (response.status === 404) throw new DirectoryError(cluster, "not-found", "unknown cluster", 404);
|
|
1386
|
+
if (!response.ok) throw new DirectoryError(cluster, "unreachable", `answered ${response.status}`, response.status);
|
|
1387
|
+
let body;
|
|
1388
|
+
try {
|
|
1389
|
+
body = await response.json();
|
|
1390
|
+
} catch (error) {
|
|
1391
|
+
throw new DirectoryError(cluster, "bad-response", "answered something other than JSON", response.status, error);
|
|
1392
|
+
}
|
|
1393
|
+
const listing = body;
|
|
1394
|
+
if (!listing || typeof listing !== "object" || !Array.isArray(listing.nodes)) {
|
|
1395
|
+
throw new DirectoryError(cluster, "bad-response", "answered no node list", response.status);
|
|
1396
|
+
}
|
|
1397
|
+
const nodes = [];
|
|
1398
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1399
|
+
for (const entry of listing.nodes) {
|
|
1400
|
+
const node = usable(entry);
|
|
1401
|
+
if (node && !seen.has(node.url)) {
|
|
1402
|
+
seen.add(node.url);
|
|
1403
|
+
nodes.push(node);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
if (nodes.length === 0) throw new DirectoryError(cluster, "empty", "lists no usable node", response.status);
|
|
1407
|
+
const after = listing.refreshAfterSeconds;
|
|
1408
|
+
const refreshAfterSeconds = typeof after === "number" && Number.isFinite(after) && after >= 1 ? after : DEFAULT_REFRESH_SECONDS;
|
|
1409
|
+
return { nodes, refreshAfterSeconds };
|
|
1410
|
+
}
|
|
1411
|
+
function usable(entry) {
|
|
1412
|
+
if (!entry || typeof entry !== "object") return void 0;
|
|
1413
|
+
const { url: url2, zone, weight } = entry;
|
|
1414
|
+
if (typeof url2 !== "string" || typeof weight !== "number" || !Number.isInteger(weight) || weight < 1) return void 0;
|
|
1415
|
+
let parsed;
|
|
1416
|
+
try {
|
|
1417
|
+
parsed = new URL(url2);
|
|
1418
|
+
} catch {
|
|
1419
|
+
return void 0;
|
|
1420
|
+
}
|
|
1421
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return void 0;
|
|
1422
|
+
return { url: url2.replace(/\/+$/, ""), zone: typeof zone === "string" ? zone : void 0, weight };
|
|
1423
|
+
}
|
|
1424
|
+
function orderNodes(nodes, zone, random) {
|
|
1425
|
+
const near = zone === void 0 ? [] : nodes.filter((node) => node.zone === zone);
|
|
1426
|
+
const far = zone === void 0 ? nodes : nodes.filter((node) => node.zone !== zone);
|
|
1427
|
+
return [...weightedOrder(near, random), ...weightedOrder(far, random)];
|
|
1428
|
+
}
|
|
1429
|
+
function weightedOrder(nodes, random) {
|
|
1430
|
+
const left = [...nodes];
|
|
1431
|
+
const order = [];
|
|
1432
|
+
while (left.length > 0) {
|
|
1433
|
+
let roll = random() * left.reduce((sum, node) => sum + node.weight, 0);
|
|
1434
|
+
let pick = 0;
|
|
1435
|
+
while (pick < left.length - 1 && roll >= left[pick].weight) roll -= left[pick++].weight;
|
|
1436
|
+
order.push(left.splice(pick, 1)[0].url);
|
|
1437
|
+
}
|
|
1438
|
+
return order;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1360
1441
|
// src/cloud-v2/errors.ts
|
|
1361
1442
|
var CalimeroError = class extends Error {
|
|
1362
1443
|
constructor(status, code, message, details = {}) {
|
|
@@ -1743,7 +1824,7 @@ function retryDelayMs(retry, options) {
|
|
|
1743
1824
|
function ceremony(path) {
|
|
1744
1825
|
return path === "auth/refresh" || path.startsWith("auth/passkey/");
|
|
1745
1826
|
}
|
|
1746
|
-
var CalimeroClient = class {
|
|
1827
|
+
var CalimeroClient = class _CalimeroClient {
|
|
1747
1828
|
constructor(options) {
|
|
1748
1829
|
/** The one in use: an index into {@link nodeUrls}. */
|
|
1749
1830
|
this.at = 0;
|
|
@@ -1766,10 +1847,40 @@ var CalimeroClient = class {
|
|
|
1766
1847
|
};
|
|
1767
1848
|
this.raw = raw;
|
|
1768
1849
|
}
|
|
1769
|
-
/**
|
|
1850
|
+
/**
|
|
1851
|
+
* A client for the Calimero Cloud cluster named `cluster`: its node list read
|
|
1852
|
+
* from the public node directory, those in `zone` first, each group in
|
|
1853
|
+
* weighted-random order. The directory only picks which nodes to try: each is
|
|
1854
|
+
* attested under `policy` before anything is sent to it. Throws
|
|
1855
|
+
* {@link DirectoryError} when the directory names no usable node.
|
|
1856
|
+
*
|
|
1857
|
+
* The list is read again in the background after a failover, and at the
|
|
1858
|
+
* first request once the directory's `refreshAfterSeconds` has passed.
|
|
1859
|
+
*/
|
|
1860
|
+
static async connect(options) {
|
|
1861
|
+
const directory = options.directory ?? DEFAULT_DIRECTORY;
|
|
1862
|
+
const random = options.random ?? Math.random;
|
|
1863
|
+
const fetcher = options.fetch ?? ((input, init) => fetch(input, init));
|
|
1864
|
+
const listing = await readDirectory(directory, options.cluster, fetcher);
|
|
1865
|
+
const [baseUrl, ...nodes] = orderNodes(listing.nodes, options.zone, random);
|
|
1866
|
+
const client = new _CalimeroClient({ baseUrl, nodes, policy: options.policy, fetch: options.fetch, retry: options.retry });
|
|
1867
|
+
client.source = {
|
|
1868
|
+
directory,
|
|
1869
|
+
cluster: options.cluster,
|
|
1870
|
+
zone: options.zone,
|
|
1871
|
+
random,
|
|
1872
|
+
due: Date.now() + listing.refreshAfterSeconds * 1e3
|
|
1873
|
+
};
|
|
1874
|
+
return client;
|
|
1875
|
+
}
|
|
1876
|
+
/** The node requests go to now: `baseUrl`, until a `421 WRONG_NODE` or a failover moved the client on. */
|
|
1770
1877
|
get nodeUrl() {
|
|
1771
1878
|
return this.nodeUrls[this.at];
|
|
1772
1879
|
}
|
|
1880
|
+
/** Every node this client may use, in the order it moves through them. */
|
|
1881
|
+
get nodes() {
|
|
1882
|
+
return [...this.nodeUrls];
|
|
1883
|
+
}
|
|
1773
1884
|
/** The sealed channel to `url`, attested under the client's policy before its first request. */
|
|
1774
1885
|
channel(url2) {
|
|
1775
1886
|
let sealed = this.channels.get(url2);
|
|
@@ -1790,9 +1901,46 @@ var CalimeroClient = class {
|
|
|
1790
1901
|
*/
|
|
1791
1902
|
moveOn(from) {
|
|
1792
1903
|
if (this.nodeUrls.length < 2) return false;
|
|
1793
|
-
if (this.
|
|
1904
|
+
if (this.nodeUrl === from) this.at = (this.at + 1) % this.nodeUrls.length;
|
|
1794
1905
|
return true;
|
|
1795
1906
|
}
|
|
1907
|
+
/**
|
|
1908
|
+
* Fail over from node `from`, where a request's retries ran out, to the next
|
|
1909
|
+
* node it has not `tried`. False when it has tried them all. A request whose
|
|
1910
|
+
* node another request moved away from already goes to that node, if untried.
|
|
1911
|
+
*/
|
|
1912
|
+
failOver(from, tried) {
|
|
1913
|
+
if (this.nodeUrl !== from && !tried.has(this.nodeUrl)) return true;
|
|
1914
|
+
const count = this.nodeUrls.length;
|
|
1915
|
+
for (let step = 1; step < count; step++) {
|
|
1916
|
+
const next = (this.at + step) % count;
|
|
1917
|
+
if (!tried.has(this.nodeUrls[next])) {
|
|
1918
|
+
this.at = next;
|
|
1919
|
+
this.reread();
|
|
1920
|
+
return true;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
return false;
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Read the node list again from the directory, in the background, for a
|
|
1927
|
+
* client made by {@link connect}: the node in use stays first, then the
|
|
1928
|
+
* directory's order; a node no longer listed is dropped. A failed read is
|
|
1929
|
+
* ignored. One read at a time.
|
|
1930
|
+
*/
|
|
1931
|
+
reread() {
|
|
1932
|
+
const source = this.source;
|
|
1933
|
+
if (!source || source.reading) return;
|
|
1934
|
+
source.reading = readDirectory(source.directory, source.cluster, this.raw).then((listing) => {
|
|
1935
|
+
const current = this.nodeUrl;
|
|
1936
|
+
const listed = orderNodes(listing.nodes, source.zone, source.random);
|
|
1937
|
+
this.nodeUrls = [current, ...listed.filter((url2) => url2 !== current)];
|
|
1938
|
+
this.at = 0;
|
|
1939
|
+
source.due = Date.now() + listing.refreshAfterSeconds * 1e3;
|
|
1940
|
+
}).catch(() => void 0).finally(() => {
|
|
1941
|
+
source.reading = void 0;
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1796
1944
|
/** Authenticate every request with an API key. */
|
|
1797
1945
|
useApiKey(key) {
|
|
1798
1946
|
this.auth = key;
|
|
@@ -1820,6 +1968,7 @@ var CalimeroClient = class {
|
|
|
1820
1968
|
headers["content-type"] = "application/json";
|
|
1821
1969
|
payload = JSON.stringify(body);
|
|
1822
1970
|
}
|
|
1971
|
+
if (this.source && Date.now() >= this.source.due) this.reread();
|
|
1823
1972
|
const url2 = this.nodeUrl;
|
|
1824
1973
|
try {
|
|
1825
1974
|
return await this.channel(url2)(`${url2}/${path}`, { method, headers, body: payload, signal });
|
|
@@ -1851,7 +2000,7 @@ var CalimeroClient = class {
|
|
|
1851
2000
|
let refreshed = false;
|
|
1852
2001
|
let moved = false;
|
|
1853
2002
|
for (; ; ) {
|
|
1854
|
-
const [token, at] = [this.auth, this.
|
|
2003
|
+
const [token, at] = [this.auth, this.nodeUrl];
|
|
1855
2004
|
const response = await this.request(method, path, body, raw, contentType, signal);
|
|
1856
2005
|
if (response.ok) return response;
|
|
1857
2006
|
const error = await refusal(response);
|
|
@@ -1887,25 +2036,40 @@ var CalimeroClient = class {
|
|
|
1887
2036
|
* {@link json} for a call or a view (D70): while the node answers 503 or
|
|
1888
2037
|
* cannot be reached, the same request, with the same idempotency key, is sent
|
|
1889
2038
|
* again after {@link retryDelayMs} (or the pause the node asked for, if
|
|
1890
|
-
* longer), at most `retries` times. Every other answer is final.
|
|
2039
|
+
* longer), at most `retries` times. Every other answer is final.
|
|
2040
|
+
*
|
|
2041
|
+
* When a node's retries run out, the request fails over to the next node the
|
|
2042
|
+
* client knows and its retries start again there, with the same key: the
|
|
2043
|
+
* key is kept in the app's replicated log, so the cluster answers it once
|
|
2044
|
+
* whichever node it reaches. Each node is tried once per request. The last
|
|
1891
2045
|
* failure surfaces as {@link UnavailableError}: the node's 503, or reason
|
|
1892
2046
|
* `network` for a connection that failed, whose `committed` is `'unknown'`.
|
|
1893
|
-
*
|
|
2047
|
+
*
|
|
2048
|
+
* `once` sends the request exactly once, to the node in use (the confirmed
|
|
2049
|
+
* attempt of {@link callConfirmed}, whose challenge is single-use).
|
|
1894
2050
|
*/
|
|
1895
|
-
async retried(method, path, body,
|
|
1896
|
-
|
|
2051
|
+
async retried(method, path, body, once = false) {
|
|
2052
|
+
const retries = once ? 0 : this.retry.retries;
|
|
2053
|
+
const tried = /* @__PURE__ */ new Set();
|
|
2054
|
+
for (; ; ) {
|
|
1897
2055
|
let unavailable;
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
}
|
|
2056
|
+
let node = this.nodeUrl;
|
|
2057
|
+
for (let attempt = 0; ; attempt++) {
|
|
2058
|
+
node = this.nodeUrl;
|
|
2059
|
+
tried.add(node);
|
|
2060
|
+
try {
|
|
2061
|
+
return await this.json(method, path, body);
|
|
2062
|
+
} catch (error) {
|
|
2063
|
+
if (error instanceof UnavailableError) unavailable = error;
|
|
2064
|
+
else if (error instanceof TypeError && this.networkFailures.has(error)) {
|
|
2065
|
+
unavailable = new UnavailableError("NETWORK", `the node could not be reached: ${error.message}`, {}, void 0, error);
|
|
2066
|
+
} else throw error;
|
|
2067
|
+
}
|
|
2068
|
+
if (attempt >= retries) break;
|
|
2069
|
+
const backoff = retryDelayMs(attempt, this.retry);
|
|
2070
|
+
await new Promise((resolve) => setTimeout(resolve, Math.max(backoff, (unavailable.retryAfter ?? 0) * 1e3)));
|
|
1905
2071
|
}
|
|
1906
|
-
if (
|
|
1907
|
-
const backoff = retryDelayMs(attempt, this.retry);
|
|
1908
|
-
await new Promise((resolve) => setTimeout(resolve, Math.max(backoff, (unavailable.retryAfter ?? 0) * 1e3)));
|
|
2072
|
+
if (once || !this.failOver(node, tried)) throw unavailable;
|
|
1909
2073
|
}
|
|
1910
2074
|
}
|
|
1911
2075
|
post(path, body = {}) {
|
|
@@ -2200,7 +2364,7 @@ var CalimeroClient = class {
|
|
|
2200
2364
|
path,
|
|
2201
2365
|
{ method, args, idempotencyKey: key },
|
|
2202
2366
|
passkey,
|
|
2203
|
-
(body) => this.retried("POST", path, body, body.confirm
|
|
2367
|
+
(body) => this.retried("POST", path, body, body.confirm !== void 0)
|
|
2204
2368
|
);
|
|
2205
2369
|
}
|
|
2206
2370
|
/** Run a read-only method: strong by default, or stale from any replica. */
|