@actyx/sdk
Version:
Actyx SDK
199 lines • 8.57 kB
JavaScript
;
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/*
* Actyx SDK: Functions for writing distributed apps
* deployed on peer-to-peer networks, without any servers.
*
* Copyright (C) 2021 Actyx AG
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.v2WaitForSwarmSync = exports.mkHeaders = exports.v2getNodeId = exports.checkToken = exports.getToken = exports.getApiLocation = exports.GlobalInternalSymbol = void 0;
const cross_fetch_1 = require("cross-fetch");
const errors_1 = require("../internal_common/errors");
const log_1 = require("../internal_common/log");
const util_1 = require("../util");
exports.GlobalInternalSymbol = Symbol('GlobalInternalSymbol');
const defaultApiLocation = (util_1.isNode && process.env.AX_STORE_URI) || 'localhost:4454/api/v2';
const getApiLocation = (host, port) => {
if (host || port) {
return (host || 'localhost') + ':' + (port || 4454) + '/api/v2';
}
return defaultApiLocation;
};
exports.getApiLocation = getApiLocation;
const getToken = async (opts, manifest) => {
const apiLocation = (0, exports.getApiLocation)(opts.actyxHost, opts.actyxPort);
const authUrl = 'http://' + apiLocation + '/auth';
const res = await (0, cross_fetch_1.default)(authUrl, {
method: 'post',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(manifest),
});
if (!res.ok) {
const errResponse = await res.text();
if (errResponse && errResponse.includes('message')) {
const errObj = JSON.parse(errResponse);
throw new Error(errObj.message);
}
else {
throw new Error(`Could not authenticate with server, got status ${res.status}, content ${errResponse}`);
}
}
const jsonContent = await res.json();
return jsonContent.token;
};
exports.getToken = getToken;
const checkToken = async (opts, token) => {
log_1.log.actyx.debug('checking token');
const apiLocation = (0, exports.getApiLocation)(opts.actyxHost, opts.actyxPort);
const url = 'http://' + apiLocation + '/events/offsets';
const res = await (0, cross_fetch_1.default)(url, {
method: 'get',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
},
});
if (res.ok) {
await res.json();
return true;
}
if (res.status === 401) {
const body = await res.json();
if (body.code === 'ERR_TOKEN_EXPIRED')
return false;
}
throw new Error(`token check inconclusive, status was ${res.status}`);
};
exports.checkToken = checkToken;
const v2getNodeId = async (config) => {
const path = `http://${(0, exports.getApiLocation)(config.actyxHost, config.actyxPort)}/node/id`;
return await (0, cross_fetch_1.default)(path)
.then((resp) => {
// null indicates the endpoint was reachable but did not react with OK response -> probably V1.
return resp.ok ? resp.text() : null;
})
.catch((err) => {
// ECONNREFUSED is probably not a CORS issue, at least...
if (err.message && err.message.includes('ECONNREFUSED')) {
throw new Error((0, errors_1.decorateEConnRefused)(err.message, path));
}
log_1.log.actyx.info('Attempt to connect to V2 failed with unclear cause. Gonna try go connect to V1 now. Error was:', err);
// HACK: V1 has broken CORS policy, this blocks our request if it reaches the WS port (4243) instead of the default port (4454).
// So if we got an error, but the error is (probably) not due to the port being closed, we assume: Probably V1.
// (Would be awesome if JS API gave a clear and proper indication of CORS block...)
return null;
});
};
exports.v2getNodeId = v2getNodeId;
const mkHeaders = (token) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
});
exports.mkHeaders = mkHeaders;
var SyncStage;
(function (SyncStage) {
SyncStage[SyncStage["WaitingForPeers"] = 0] = "WaitingForPeers";
SyncStage[SyncStage["WaitingForRootMap"] = 1] = "WaitingForRootMap";
SyncStage[SyncStage["WaitingForSync"] = 2] = "WaitingForSync";
SyncStage[SyncStage["InSync"] = 3] = "InSync";
})(SyncStage || (SyncStage = {}));
// Wait at most 30 secs after the node's startup time
const NODE_MAX_STARTED_MS = 30000;
// Once probably a first root map was received, wait up to
// `NODE_REPLICATION_WAIT_MS`. After which we yield if the number of streams to
// replicate is below `NODE_REPLICATION_TARGET_THRESHOLD`.
const NODE_REPLICATION_WAIT_MS = 5000;
const NODE_REPLICATION_TARGET_THRESHOLD = 3;
const v2WaitForSwarmSync = async (config, token, getOffsets) => {
const uri = `http://${(0, exports.getApiLocation)(config.actyxHost, config.actyxPort)}/node/info`;
const getInfo = () => (0, cross_fetch_1.default)(uri, {
method: 'get',
headers: (0, exports.mkHeaders)(token),
})
.then((resp) => {
if (resp.status === 404) {
throw new Error('The targeted node seems not to support the `/api/v2/node/info` endpoint. Consider updating to the latest version.');
}
else {
return resp.json().then((i) => i);
}
})
.catch((err) => {
if (err.message) {
throw new Error((0, errors_1.decorateEConnRefused)(err.message, uri));
}
else {
throw new Error(`Unknown error trying to contact Actyx node, please diagnose manually by trying to reach ${uri} from where this process is running.`);
}
});
const info = await getInfo();
let firstNodeSeenAt = null;
let waitingForSyncSince = null;
let syncStage = SyncStage.WaitingForPeers;
while (info.uptime.secs * 1000 < NODE_MAX_STARTED_MS) {
const info = await getInfo();
switch (syncStage) {
case SyncStage.WaitingForPeers: {
if (info.connectedNodes === 0) {
// Wait a bit and retry.
await new Promise((res) => setTimeout(res, 500));
}
else {
// First time there are some peers!
firstNodeSeenAt = Date.now().valueOf();
syncStage += 1;
}
break;
}
case SyncStage.WaitingForRootMap: {
// TODO: A more robust approach could be to wait for movements in the
// offset's response with a cap of 20 s or so.
//
// Wait at most up to `firstNodeSeenAt + waitForRootMap`:
// Default for root map update interval is 10 secs. Assuming an equal
// distribution of the connected nodes', we can approximate how long to
// avoid (+standard deviation):
const waitForRootMap = 1e4 / info.connectedNodes + 2890;
if (Date.now() - firstNodeSeenAt - waitForRootMap < 0) {
// Wait a bit and retry.
await new Promise((res) => setTimeout(res, 250));
}
else {
// We should have seen at least one root map update by now.
waitingForSyncSince = Date.now();
syncStage += 1;
}
break;
}
case SyncStage.WaitingForSync: {
const replicationTarget = (await getOffsets()).toReplicate;
const missingTargets = Object.entries(replicationTarget).length;
if (missingTargets == 0) {
// Node has peers, we waited a bit to get some root updates AND the
// replication target is empty. Ignition!
return;
}
else if (missingTargets < NODE_REPLICATION_TARGET_THRESHOLD &&
Date.now() - waitingForSyncSince > NODE_REPLICATION_WAIT_MS) {
// Don't let a few bad nodes draw us down
return;
}
else {
// Wait a bit and retry
await new Promise((res) => setTimeout(res, 250));
break;
}
}
default: {
return;
}
}
}
};
exports.v2WaitForSwarmSync = v2WaitForSwarmSync;
//# sourceMappingURL=utils.js.map