UNPKG

@actyx/sdk

Version:
199 lines 8.57 kB
"use strict"; /* 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