@interopio/gateway-server
Version: 
[](https://www.npmjs.com/package/@interopio/gateway-server)
476 lines (469 loc) • 14.3 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __esm = (fn, res) => function __init() {
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
// src/logger.ts
import * as GatewayLogging from "@interopio/gateway/logging/core";
function getLogger2(name) {
  return GatewayLogging.getLogger(`gateway.server.${name}`);
}
var init_logger = __esm({
  "src/logger.ts"() {
    "use strict";
  }
});
// src/mesh/ws/broker/core.ts
var core_exports = {};
__export(core_exports, {
  default: () => core_default
});
import { IOGateway } from "@interopio/gateway";
function broadcastNodeAdded(nodes, newSocket, newNodeId) {
  Object.entries(nodes.nodes).forEach(([nodeId, socket]) => {
    if (nodeId !== newNodeId) {
      newSocket.send(codec.encode({ type: "node-added", "node-id": newNodeId, "new-node": nodeId }));
      socket.send(codec.encode({ type: "node-added", "node-id": nodeId, "new-node": newNodeId }));
    }
  });
}
function broadcastNodeRemoved(nodes, removedNodeId) {
  Object.entries(nodes.nodes).forEach(([nodeId, socket]) => {
    if (nodeId !== removedNodeId) {
      socket.send(codec.encode({ type: "node-removed", "node-id": nodeId, "removed-node": removedNodeId }));
    }
  });
}
function onOpen(connectedNodes, logPrefix) {
  logger.info(`${logPrefix}connection accepted`);
}
function onClose(connectedNodes, logPrefix, code, reason) {
  logger.info(`${logPrefix}connection closed [${code}](${reason})`);
  const nodeIds = connectedNodes.sockets[logPrefix];
  if (nodeIds) {
    delete connectedNodes.sockets[logPrefix];
    for (const nodeId of nodeIds) {
      delete connectedNodes.nodes[nodeId];
    }
    for (const nodeId of nodeIds) {
      broadcastNodeRemoved(connectedNodes, nodeId);
    }
  }
}
function processMessage(connectedNodes, socket, key, msg) {
  switch (msg.type) {
    case "hello": {
      const nodeId = msg["node-id"];
      connectedNodes.nodes[nodeId] = socket;
      connectedNodes.sockets[key] = connectedNodes.sockets[key] ?? [];
      connectedNodes.sockets[key].push(nodeId);
      logger.info(`[${key}] node ${nodeId} added.`);
      broadcastNodeAdded(connectedNodes, socket, nodeId);
      break;
    }
    case "bye": {
      const nodeId = msg["node-id"];
      delete connectedNodes[nodeId];
      logger.info(`[${key}] node ${nodeId} removed.`);
      broadcastNodeRemoved(connectedNodes, nodeId);
      break;
    }
    case "data": {
      const sourceNodeId = msg.from;
      const targetNodeId = msg.to;
      if ("all" === targetNodeId) {
        Object.entries(connectedNodes.nodes).forEach(([nodeId, socket2]) => {
          if (nodeId !== sourceNodeId) {
            socket2.send(codec.encode(msg));
          }
        });
      } else {
        const socket2 = connectedNodes.nodes[targetNodeId];
        if (socket2) {
          socket2.send(codec.encode(msg));
        } else {
          logger.warn(`unable to send to node ${targetNodeId} message ${JSON.stringify(msg)}`);
        }
      }
      break;
    }
    default: {
      logger.warn(`[${key}] ignoring unknown message ${JSON.stringify(msg)}`);
      break;
    }
  }
}
function onMessage(connectedNodes, socket, logPrefix, msg) {
  try {
    const decoded = codec.decode(msg);
    if (logger.enabledFor("debug")) {
      logger.debug(`${logPrefix}processing msg ${JSON.stringify(decoded)}`);
    }
    processMessage(connectedNodes, socket, logPrefix, decoded);
  } catch (ex) {
    logger.error(`${logPrefix}unable to process message`, ex);
  }
}
async function create(environment) {
  const connectedNodes = { nodes: {}, sockets: {} };
  logger.info(`mesh server is listening`);
  return async ({ socket, handshake }) => {
    const logPrefix = handshake.logPrefix;
    onOpen(connectedNodes, logPrefix);
    socket.on("error", (err) => {
      logger.error(`${logPrefix}websocket error: ${err}`, err);
    });
    socket.on("message", (data, _isBinary) => {
      if (Array.isArray(data)) {
        data = Buffer.concat(data);
      }
      onMessage(connectedNodes, socket, logPrefix, data);
    });
    socket.on("close", (code, reason) => {
      onClose(connectedNodes, logPrefix, code, reason);
    });
  };
}
var GatewayEncoders, logger, codec, core_default;
var init_core = __esm({
  "src/mesh/ws/broker/core.ts"() {
    "use strict";
    init_logger();
    GatewayEncoders = IOGateway.Encoding;
    logger = getLogger2("mesh.ws.broker");
    codec = GatewayEncoders.transit({
      keywordize: /* @__PURE__ */ new Map([
        ["/type", "*"],
        ["/message/body/type", "*"],
        ["/message/origin", "*"],
        ["/message/receiver/type", "*"],
        ["/message/source/type", "*"],
        ["/message/body/type", "*"]
      ])
    });
    core_default = create;
  }
});
// src/gateway/ent/logging.ts
import { format } from "node:util";
var LogInfoAdapter = class {
  #event;
  #parsed;
  #timestamp;
  #output;
  constructor(event) {
    this.#event = event;
  }
  parsed() {
    if (this.#parsed === void 0) {
      let err = void 0;
      let vargs = this.#event.data;
      if (this.#event.data[0] instanceof Error) {
        err = this.#event.data[0];
        vargs = vargs.slice(1);
      }
      const msg = format(this.#event.message, ...vargs);
      this.#parsed = { err, msg };
    }
    return this.#parsed;
  }
  get time() {
    return this.#event.time;
  }
  get level() {
    return this.#event.level;
  }
  get namespace() {
    return this.#event.name;
  }
  get file() {
    return void 0;
  }
  get line() {
    return void 0;
  }
  get message() {
    return this.parsed().msg;
  }
  get stacktrace() {
    return this.parsed().err;
  }
  get timestamp() {
    if (this.#timestamp === void 0) {
      this.#timestamp = this.time.toISOString();
    }
    return this.#timestamp;
  }
  get output() {
    if (this.#output === void 0) {
      const err = this.parsed().err;
      const stacktrace = err ? `
${err.stack ?? err}` : "";
      this.#output = `${this.timestamp} ${this.level.toUpperCase()} [${this.namespace}] - ${this.message}${stacktrace}`;
    }
    return this.#output;
  }
};
function toLogConfig(config) {
  let level = "info";
  if (config?.level) {
    if (config.level === "fatal") {
      level = "error";
    } else if (config.level !== "report") {
      level = config.level;
    }
  }
  const result = { level };
  const appenderFn = config?.appender;
  if (appenderFn) {
    result.appender = (event) => {
      appenderFn(new LogInfoAdapter(event));
    };
  }
  return result;
}
// src/gateway/ent/server.ts
import { GatewayServer } from "@interopio/gateway-server";
var ServerDelegate = class {
  constructor(config) {
    this.config = config;
  }
  server;
  async connect(cb) {
    if (!this.server) {
      throw new Error(`not started`);
    }
    const client = await this.server.gateway.connect((c, m) => cb(c, m));
    return client;
  }
  info() {
    return this.server?.gateway.info();
  }
  async start() {
    if (!this.server) {
      this.server = await GatewayServer.Factory(this.config);
    }
    return this;
  }
  async stop() {
    await this.server?.close();
    delete this.server;
    return this;
  }
};
// src/gateway/ent/config.ts
function toMetricsFilters(legacy) {
  if (legacy) {
    const publishers = legacy.publishers.map((publisher) => {
      return { identity: publisher.publisher, metrics: publisher.metrics };
    });
    const non_matched = legacy["non-matched"];
    return { publishers, non_matched };
  }
}
function toMetricsPublisherConfig(legacy) {
  const filters = toMetricsFilters(legacy?.filters);
  const conflation = toConflation(legacy?.conflation);
  return { ...legacy, filters, conflation };
}
function toConflation(legacy) {
  if (legacy) {
    return { interval: legacy.interval };
  }
}
function toMetricsConfig(legacy) {
  const metrics = { publishers: [] };
  legacy?.publishers?.forEach((publisher) => {
    if (typeof publisher === "string") {
      if (publisher === "rest") {
        metrics.publishers.push("rest");
        if (legacy.rest) {
          const conf = { ...legacy.rest };
          const userAgent = conf["user-agent"];
          delete conf["user-agent"];
          const headers = { ...conf.headers, ...userAgent ? { "user-agent": userAgent } : {} };
          delete conf.headers;
          metrics.rest = {
            endpoint: conf.endpoint,
            headers,
            ...toMetricsPublisherConfig(conf)
          };
          metrics.rest["publishFn"] ??= "@interopio/gateway-server/metrics/publisher/rest";
        }
      } else if (publisher === "file") {
        metrics.publishers.push("file");
        if (legacy.file) {
          const conf = { ...legacy.file };
          const status = conf["skip-status"] === void 0 ? true : !conf["skip-status"];
          delete conf["skip-status"];
          metrics.file = {
            location: conf.location,
            status,
            ...toMetricsPublisherConfig(conf)
          };
          metrics.file["publishFn"] ??= "@interopio/gateway/metrics/publisher/file";
        }
      } else {
      }
    } else {
      const configuration = { ...publisher.configuration };
      const splitSize = configuration["split-size"];
      delete configuration["split-size"];
      const file = publisher["file"];
      const custom = {
        split_size: splitSize,
        publisher: { file, configuration },
        ...toMetricsPublisherConfig(configuration)
      };
      metrics.publishers.push(custom);
    }
  });
  if (legacy?.filters) {
    metrics.filters = toMetricsFilters(legacy?.filters);
  }
  return metrics;
}
function trimTrailingSlash(url) {
  return url?.endsWith("/") ? url.slice(0, -1) : url;
}
function toCluster(legacy) {
  if (legacy?.directory) {
    const legacyDirectory = legacy.directory;
    const config = { endpoint: legacy?.["endpoint"] };
    if (legacyDirectory.type === "rest") {
      let directory = void 0;
      if (config.endpoint === void 0) {
        config.endpoint = trimTrailingSlash(legacyDirectory.config?.directory_uri);
      } else {
        directory = { uri: trimTrailingSlash(legacyDirectory.config?.directory_uri) };
      }
      if (legacyDirectory.config?.announce_interval) {
        directory ??= {};
        directory.interval = Number(legacyDirectory.config.announce_interval);
      }
      if (directory !== void 0) {
        config.directory = directory;
      }
      return config;
    } else if (legacyDirectory.type === "static") {
      config.directory = { members: legacyDirectory.members ?? [] };
      return config;
    }
  }
}
function toMeshConfig(legacy) {
  const mesh = {};
  if (legacy?.configuration?.node_id) mesh.node = legacy.configuration.node_id;
  if (legacy?.type === "broker") mesh.broker = { endpoint: legacy.broker?.endpoint ?? "<unresolved>" };
  if (legacy?.type === "p2p") {
    const cluster = toCluster(legacy.p2p);
    if (cluster) {
      mesh.cluster = cluster;
    }
  }
  return mesh;
}
function toAuthenticationConfig(legacy) {
  const authentication = { available: [] };
  if (legacy?.default) {
    authentication.default = legacy.default;
  }
  const as = legacy?.available;
  as.forEach((a) => {
    if (a === "basic" || a === "oauth2" || legacy?.[a]?.["authenticator"] !== void 0) {
      authentication.available.push(a);
      if (legacy?.[a] !== void 0) {
        authentication[a] = legacy[a];
      }
    }
  });
  return authentication;
}
function toServerConfig(config) {
  const gateway = {
    route: config.route,
    maxConnections: config.limits?.max_connections,
    origins: config.security?.origin_filters,
    authorize: config["authorize"] ?? { access: "permitted" },
    clients: config["clients"] ?? { inactive_seconds: 0, buffer_size: 100 },
    contexts: {
      lifetime: "retained",
      visibility: [
        { context: /___channel___.+/, restrictions: "cluster" },
        { context: /T42\..+/, restrictions: "local" }
      ]
    },
    methods: {
      visibility: [
        { method: /T42\..+/, restrictions: "local" }
      ]
    },
    peers: {
      visibility: [
        { domain: "context", restrictions: "cluster" },
        { domain: "agm", restrictions: "local" }
      ]
    },
    metrics: { publishers: [] }
  };
  if (config.authentication !== void 0) {
    if (config.authentication.token_ttl) gateway.token = { ttl: config.authentication?.token_ttl };
    if (config.authentication.available || config.authentication.default) gateway.authentication = toAuthenticationConfig(config.authentication);
  }
  if (config["globals"]) {
    gateway.globals = config["globals"];
  }
  if (config["contexts"]) {
    gateway.contexts = config["contexts"];
  }
  if (config["methods"]) {
    gateway.methods = config["methods"];
  }
  if (config["peers"]) {
    gateway.peers = config["peers"];
  }
  if (config.cluster?.enabled) {
    gateway.mesh = toMeshConfig(config.cluster);
  }
  if (config.metrics?.publishers) gateway.metrics = toMetricsConfig(config.metrics);
  return {
    port: config.port ?? 3434,
    host: config.ip ?? config["host"],
    memory: config["memory"],
    app: async (configurer) => {
      if (config.cluster?.embedded_broker?.enabled === true) {
        configurer.socket({
          path: config.cluster.embedded_broker.route ?? "/mesh-broker",
          options: {
            authorize: config.cluster.embedded_broker["authorize"] ?? { access: "permitted" }
          },
          factory: async (env) => {
            if (gateway.mesh?.broker?.endpoint === "<unresolved>") {
              gateway.mesh.broker.endpoint = env.endpoint;
            }
            const delegate = (await Promise.resolve().then(() => (init_core(), core_exports))).default;
            return await delegate(env);
          }
        });
      }
    },
    gateway
  };
}
// src/gateway/ent/index.ts
import { IOGateway as IOGateway2 } from "@interopio/gateway";
function create2(config) {
  return new ServerDelegate(toServerConfig(config));
}
function configure_logging(config) {
  IOGateway2.Logging.configure(toLogConfig(config));
}
export {
  configure_logging,
  create2 as create
};
//# sourceMappingURL=gateway-ent.js.map