@interopio/bridge 0.1.0-beta.0 → 1.0.1

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/main.js CHANGED
@@ -35,8 +35,8 @@ var InternalRelays = class {
35
35
  for (const [node, k] of this.#links) {
36
36
  if (k.key === key && (from === void 0 || k.node === from)) {
37
37
  const links = k.linksByNode;
38
- if (this.#logger.enabledFor("debug")) {
39
- this.#logger.debug(`${key} unregisters node ${node}, linked to ${Array.from(links.keys()).join(", ")}`);
38
+ if (this.#logger.enabledFor("info")) {
39
+ this.#logger.info(`${key} unregisters node ${node}, linked to ${Array.from(links.keys()).join(", ")}`);
40
40
  }
41
41
  this.#links.delete(node);
42
42
  this.#eventEmitter.emit("disconnect", key, node, links);
@@ -44,8 +44,8 @@ var InternalRelays = class {
44
44
  }
45
45
  }
46
46
  #connect(key, node) {
47
- if (this.#logger.enabledFor("debug")) {
48
- this.#logger.debug(`${key} registers node ${node}`);
47
+ if (this.#logger.enabledFor("info")) {
48
+ this.#logger.info(`${key} registers node ${node}`);
49
49
  }
50
50
  this.#links.set(node, { key, node, linksByNode: /* @__PURE__ */ new Map() });
51
51
  this.#eventEmitter.emit("connect", key, node);
@@ -72,6 +72,25 @@ var InternalRelays = class {
72
72
  }
73
73
  }
74
74
  return { decoded };
75
+ } else {
76
+ const node = decoded.to;
77
+ const link = decoded.from;
78
+ const type2 = decoded.type;
79
+ const senderLinkData = this.#links.get(link);
80
+ switch (type2) {
81
+ case "hello":
82
+ if (this.#logger.enabledFor("debug")) {
83
+ this.#logger.debug(`${key} connecting ${link} to ${node}`);
84
+ }
85
+ senderLinkData?.linksByNode.set(decoded.to, key);
86
+ break;
87
+ case "bye":
88
+ if (this.#logger.enabledFor("debug")) {
89
+ this.#logger.debug(`${key} disconnecting ${link} from ${node}`);
90
+ }
91
+ senderLinkData?.linksByNode.delete(decoded.to);
92
+ break;
93
+ }
75
94
  }
76
95
  const encoded = isEncoded(msg) ? msg : codec.encode(msg);
77
96
  return { node: from, decoded, encoded };
@@ -86,23 +105,28 @@ var InternalRelays = class {
86
105
  return this;
87
106
  }
88
107
  send(key, node, msg, cb) {
89
- if (this.#logger.enabledFor("debug")) {
108
+ if (this.#logger.enabledFor("trace")) {
90
109
  this.#logger.debug(`${key} sending msg to ${node}`);
91
110
  }
92
111
  const encoded = isEncoded(msg) ? msg : codec.encode(msg);
93
112
  const decoded = isEncoded(msg) ? codec.decode(msg) : msg;
94
113
  {
95
- const senderLinkData = this.#links.get(decoded.from);
96
- switch (decoded.type) {
114
+ node ??= decoded.to;
115
+ const link = decoded.from;
116
+ const type = decoded.type;
117
+ const senderLinkData = this.#links.get(link);
118
+ switch (type) {
97
119
  case "hello":
98
- if (node === decoded.to) {
99
- senderLinkData?.linksByNode.set(decoded.to, key);
120
+ if (this.#logger.enabledFor("debug")) {
121
+ this.#logger.debug(`${key} connecting ${link} to ${node}`);
100
122
  }
123
+ senderLinkData?.linksByNode.set(node, key);
101
124
  break;
102
125
  case "bye":
103
- if (node === decoded.to) {
104
- senderLinkData?.linksByNode.delete(decoded.to);
126
+ if (this.#logger.enabledFor("debug")) {
127
+ this.#logger.debug(`${key} disconnecting ${link} from ${node}`);
105
128
  }
129
+ senderLinkData?.linksByNode.delete(node);
106
130
  break;
107
131
  }
108
132
  }
@@ -116,6 +140,9 @@ var InternalRelays = class {
116
140
  }
117
141
  }
118
142
  }
143
+ if (decoded.type === "bye") {
144
+ return;
145
+ }
119
146
  throw new Error(`${key} no active link for ${node}`);
120
147
  }
121
148
  };
@@ -1334,7 +1361,7 @@ function parseVersion(version) {
1334
1361
  // package.json
1335
1362
  var package_default = {
1336
1363
  name: "@interopio/bridge",
1337
- version: "0.1.0-beta.0",
1364
+ version: "1.0.1",
1338
1365
  license: "see license in license.md",
1339
1366
  author: "interop.io",
1340
1367
  homepage: "https://docs.interop.io/bridge",
@@ -1347,7 +1374,7 @@ var package_default = {
1347
1374
  ],
1348
1375
  repository: {
1349
1376
  type: "git",
1350
- url: "https://github.com/InteropIO/bridge.git",
1377
+ url: "git+https://github.com/InteropIO/bridge.git",
1351
1378
  directory: "packages/bridge"
1352
1379
  },
1353
1380
  type: "module",
@@ -1364,7 +1391,7 @@ var package_default = {
1364
1391
  }
1365
1392
  },
1366
1393
  bin: {
1367
- bridge: "./bin/bridge.js"
1394
+ bridge: "bin/bridge.js"
1368
1395
  },
1369
1396
  scripts: {
1370
1397
  test: "mocha test --recursive",
@@ -1374,13 +1401,14 @@ var package_default = {
1374
1401
  build: "npm run build:main && npm run build:index"
1375
1402
  },
1376
1403
  dependencies: {
1377
- "@interopio/gateway-server": "^0.13.0-beta.1",
1404
+ "@interopio/gateway-server": "^0.17.0",
1378
1405
  dotenv: "^17.2.3",
1379
1406
  jsrsasign: "^11.1.0",
1407
+ "hrw-hash": "^2.0.3",
1380
1408
  nanoid: "^5.1.6"
1381
1409
  },
1382
1410
  devDependencies: {
1383
- "@interopio/gateway": "^0.16.1-beta.0",
1411
+ "@interopio/gateway": "^0.20.0",
1384
1412
  "@types/jsrsasign": "^10.5.15",
1385
1413
  "@types/ws": "^8.18.1",
1386
1414
  "rand-seed": "^3.0.0"
@@ -1759,9 +1787,13 @@ function routes(config, { handle }) {
1759
1787
  );
1760
1788
  await response.end();
1761
1789
  } else {
1762
- const nodes = json.map((node) => fromRequestNode(node, request));
1790
+ const nodes = json.map((node) => {
1791
+ return fromRequestNode(node, request);
1792
+ });
1763
1793
  const result = connections.announce(nodes).map((connection) => {
1764
- const connect = connection.connect?.map((node) => toRequestNode(node, request));
1794
+ const connect = connection.connect?.map((node) => {
1795
+ return toRequestNode(node, request);
1796
+ });
1765
1797
  return { ...connection, connect };
1766
1798
  });
1767
1799
  const buffer = Buffer.from(JSON.stringify(result), "utf8");
@@ -1827,9 +1859,10 @@ async function create(log, internal, env) {
1827
1859
  log.info(`relays-${id} server is listening on ${env.endpoint}`);
1828
1860
  let keyId = 0;
1829
1861
  return async ({ socket, handshake }) => {
1830
- const logPrefix = handshake.logPrefix;
1862
+ const logPrefix = handshake.logPrefix ?? "";
1863
+ const query = handshake.url.searchParams;
1831
1864
  const key = `r.${id}.${++keyId}`;
1832
- log.info(`${logPrefix}connected on /relays with assigned key ${key}`);
1865
+ log.info(`${logPrefix}connected on /relays with ${query} and assigned key ${key}`);
1833
1866
  internal.add(key, (msg, c, cb) => {
1834
1867
  socket.send(msg, { binary: false }, (err) => {
1835
1868
  cb(key, err);
@@ -1869,7 +1902,7 @@ function onMessage(relays, log, key, node, socketsByNodeId, msg) {
1869
1902
  log.warn(`${k} error writing msg ${msg}: ${err}`);
1870
1903
  return;
1871
1904
  }
1872
- if (log.enabledFor("debug")) {
1905
+ if (log.enabledFor("trace")) {
1873
1906
  log.debug(`${k} sent msg ${msg}`);
1874
1907
  }
1875
1908
  });
@@ -1897,7 +1930,7 @@ async function create2(log, relays, env) {
1897
1930
  log.warn(`${key} error writing from ${k} msg ${msg}: ${err}`);
1898
1931
  return;
1899
1932
  }
1900
- if (log.enabledFor("debug")) {
1933
+ if (log.enabledFor("trace")) {
1901
1934
  log.debug(`${key} sent from ${k} msg ${msg}`);
1902
1935
  }
1903
1936
  });
@@ -1924,7 +1957,7 @@ async function create2(log, relays, env) {
1924
1957
  const logPrefix = handshake.logPrefix ?? "";
1925
1958
  const query = handshake.url.searchParams;
1926
1959
  const key = `c.${id}.${++keyId}`;
1927
- log.info(`${logPrefix}connected on /cluster with ${query} with assigned key ${key}`);
1960
+ log.info(`${logPrefix}connected on /cluster with ${query} and assigned key ${key}`);
1928
1961
  const node = query.get("node");
1929
1962
  if (node) {
1930
1963
  let sockets = socketsByNodeId.get(node);
@@ -1969,6 +2002,7 @@ import "@interopio/gateway-server";
1969
2002
 
1970
2003
  // ../bridge-mesh/src/mesh/gateway/mesh.ts
1971
2004
  import { nanoid as nanoid2 } from "nanoid";
2005
+ import gateway from "@interopio/gateway/package.json" with { type: "json" };
1972
2006
  var instanceId = 0;
1973
2007
  var BridgeMeshChannel = class {
1974
2008
  #logger;
@@ -1997,6 +2031,7 @@ var BridgeMeshChannel = class {
1997
2031
  throw new Error(`already subscribed to node ${node}`);
1998
2032
  }
1999
2033
  this.#state.set(node, { subscriber, users: /* @__PURE__ */ new Set(), members: /* @__PURE__ */ new Set() });
2034
+ this.#announce(node);
2000
2035
  this.#relays.receive(key, { type: "hello", from: node, to: "all" });
2001
2036
  this.#relays.add(key, this.createRelayClient(key, node));
2002
2037
  this.#intervalId ??= setInterval(() => this.#announce(), 3e4);
@@ -2136,23 +2171,38 @@ var BridgeMeshChannel = class {
2136
2171
  this.#state.delete(node);
2137
2172
  this.#connections.remove(node);
2138
2173
  }
2139
- close() {
2174
+ async init(gossipTimestamp = 0) {
2175
+ await this.#connections.init(gossipTimestamp);
2176
+ }
2177
+ async close() {
2140
2178
  for (const node of this.#state.keys()) {
2141
2179
  this.#delete(node);
2142
2180
  this.#relays.remove(`${this.#keyPrefix}-${node}`);
2143
2181
  }
2144
2182
  clearInterval(this.#intervalId);
2183
+ await this.#connections.close();
2145
2184
  }
2146
2185
  };
2147
2186
  var entryToNode = ([node, state]) => {
2148
2187
  const users = Array.from(state?.users);
2149
- const endpoint = `/cluster?node=${node}`;
2150
- return { node, endpoint, users };
2188
+ return { node, users, metadata: { version: gateway.version, type: "io.Bridge" } };
2151
2189
  };
2152
2190
 
2153
2191
  // ../bridge-mesh/src/index.ts
2154
2192
  function connectNodeRelays(logger, relays, connections) {
2193
+ relays.on("connect", (key, node) => {
2194
+ try {
2195
+ connections.add(node);
2196
+ } catch (e) {
2197
+ logger.warn(`${key} error adding node ${node} to connections: ${e}`);
2198
+ }
2199
+ });
2155
2200
  relays.on("disconnect", (key, node, links) => {
2201
+ try {
2202
+ connections.remove(node);
2203
+ } catch (e) {
2204
+ logger.warn(`${key} error removing node ${node} from connections: ${e}`);
2205
+ }
2156
2206
  for (const [linkNode, linkKey] of links) {
2157
2207
  try {
2158
2208
  relays.send(linkKey, linkNode, { type: "bye", from: node, to: linkNode }, (k, err) => {
@@ -2161,11 +2211,11 @@ function connectNodeRelays(logger, relays, connections) {
2161
2211
  return;
2162
2212
  }
2163
2213
  if (logger.enabledFor("debug")) {
2164
- logger.debug(`${k} sent 'bye' msg to ${linkNode}`);
2214
+ logger.debug(`${k} sent 'bye' msg to ${linkNode} from ${node}`);
2165
2215
  }
2166
2216
  });
2167
2217
  } catch (err) {
2168
- logger.warn(`${linkKey} exception writing 'bye' msg to ${linkNode}: ${err}`);
2218
+ logger.warn(`${linkKey} exception writing 'bye' msg to ${linkNode} from ${node}: ${err}`);
2169
2219
  }
2170
2220
  }
2171
2221
  });
@@ -2198,123 +2248,205 @@ var mesh = async (options, configurer, config) => {
2198
2248
  };
2199
2249
 
2200
2250
  // ../bridge-mesh/src/mesh/connections.ts
2201
- import "@interopio/gateway";
2202
- import { EventEmitter as EventEmitter2 } from "node:events";
2203
- var OrderedMap = class {
2204
- #elements = [];
2205
- #binarySearch(key) {
2206
- let low = 0;
2207
- let high = this.#elements.length - 1;
2208
- while (low <= high) {
2209
- const mid = low + high >> 1;
2210
- const cmp = this.#elements[mid][0].localeCompare(key);
2211
- if (cmp === 0) return mid;
2212
- if (cmp < 0) low = mid + 1;
2213
- else high = mid - 1;
2214
- }
2215
- return -low - 1;
2216
- }
2217
- set(key, value) {
2218
- const idx = this.#binarySearch(key);
2219
- if (idx >= 0) {
2220
- this.#elements[idx][1] = value;
2221
- } else {
2222
- this.#elements.splice(~idx, 0, [key, value]);
2223
- }
2251
+ import { hrwHash } from "hrw-hash";
2252
+
2253
+ // ../bridge-mesh/src/mesh/lamport.ts
2254
+ import { appendFile, readFile, rename, writeFile } from "node:fs/promises";
2255
+ var LamportClock = class _LamportClock {
2256
+ static #FLUSH_INTERVAL = 5e3;
2257
+ // 5 seconds
2258
+ static #CLOCK_FILE = "./lamport.clock";
2259
+ static #WAL_FILE = "./lamport.wal";
2260
+ #timestamp;
2261
+ #instanceId;
2262
+ #flushIntervalId;
2263
+ #pendingWrites = 0;
2264
+ constructor(instanceId2) {
2265
+ this.#instanceId = instanceId2;
2266
+ this.#timestamp = 0;
2267
+ this.#flushIntervalId = setInterval(() => {
2268
+ this.#flush().catch((err) => {
2269
+ });
2270
+ }, _LamportClock.#FLUSH_INTERVAL);
2224
2271
  }
2225
- get(key) {
2226
- const idx = this.#binarySearch(key);
2227
- return idx >= 0 ? this.#elements[idx][1] : void 0;
2272
+ async init(gossipTimestamp = 0) {
2273
+ const baseTimestamp = await this.#loadSnapshot();
2274
+ const journaledIncrements = await this.#replayWal();
2275
+ this.#timestamp = Math.max(baseTimestamp + journaledIncrements, gossipTimestamp);
2228
2276
  }
2229
- has(key) {
2230
- return this.#binarySearch(key) >= 0;
2277
+ tick() {
2278
+ this.#timestamp += 1;
2279
+ this.#pendingWrites += 1;
2280
+ this.#appendToWal().catch((_err) => {
2281
+ });
2282
+ return { timestamp: this.#timestamp, instanceId: this.#instanceId };
2231
2283
  }
2232
- delete(key) {
2233
- const idx = this.#binarySearch(key);
2234
- if (idx >= 0) {
2235
- this.#elements.splice(idx, 1);
2236
- return true;
2284
+ observe(gossipTimestamp) {
2285
+ this.#timestamp = Math.max(this.#timestamp, gossipTimestamp) + 1;
2286
+ this.#pendingWrites += 1;
2287
+ this.#appendToWal().catch((_err) => {
2288
+ });
2289
+ return { timestamp: this.#timestamp, instanceId: this.#instanceId };
2290
+ }
2291
+ timestamp() {
2292
+ return { timestamp: this.#timestamp, instanceId: this.#instanceId };
2293
+ }
2294
+ async shutdown() {
2295
+ clearInterval(this.#flushIntervalId);
2296
+ await this.#flush();
2297
+ }
2298
+ async #appendToWal() {
2299
+ const data = `${this.#timestamp}
2300
+ `;
2301
+ await appendFile(_LamportClock.#WAL_FILE, data, "utf-8");
2302
+ }
2303
+ async #replayWal() {
2304
+ try {
2305
+ const data = await readFile(_LamportClock.#WAL_FILE, "utf-8");
2306
+ return data.trim().split("\n").filter(Boolean).length;
2307
+ } catch {
2237
2308
  }
2238
- return false;
2309
+ return 0;
2239
2310
  }
2240
- entries() {
2241
- return this.#elements[Symbol.iterator]();
2311
+ async #loadSnapshot() {
2312
+ try {
2313
+ const data = await readFile(_LamportClock.#CLOCK_FILE, "utf-8");
2314
+ return parseInt(data, 10) || 0;
2315
+ } catch {
2316
+ }
2317
+ return 0;
2242
2318
  }
2243
- values() {
2244
- return this.#elements.map((e) => e[1]);
2319
+ async #flush() {
2320
+ if (this.#pendingWrites === 0) {
2321
+ return;
2322
+ }
2323
+ const tmp = `${_LamportClock.#CLOCK_FILE}.tmp`;
2324
+ const data = `${this.#timestamp}
2325
+ `;
2326
+ await writeFile(tmp, data, { flag: "w" });
2327
+ await rename(tmp, _LamportClock.#CLOCK_FILE);
2328
+ await writeFile(_LamportClock.#WAL_FILE, "", { flag: "w" });
2329
+ this.#pendingWrites = 0;
2245
2330
  }
2246
2331
  };
2332
+
2333
+ // ../bridge-mesh/src/mesh/connections.ts
2334
+ import { EventEmitter as EventEmitter2 } from "node:events";
2247
2335
  var InMemoryNodeConnections = class {
2248
2336
  #logger;
2249
2337
  #eventEmitter = new EventEmitter2();
2250
- #nodes = new OrderedMap();
2251
- #nodesByEndpoint = /* @__PURE__ */ new Map();
2338
+ #nodes = /* @__PURE__ */ new Map();
2339
+ #clusterNodes;
2340
+ #lamport;
2252
2341
  #timeout;
2253
- constructor(logger, timeout = 6e4) {
2342
+ constructor(logger, instanceId2, timeout = 6e4) {
2254
2343
  this.#logger = logger;
2344
+ this.#clusterNodes = [instanceId2];
2345
+ this.#lamport = new LamportClock(instanceId2);
2255
2346
  this.#timeout = timeout;
2256
2347
  }
2257
- announce(nodes) {
2348
+ async init(gossipTimestamp = 0) {
2349
+ await this.#lamport.init(gossipTimestamp);
2350
+ }
2351
+ async close() {
2352
+ await this.#lamport.shutdown();
2353
+ }
2354
+ announce(announcements) {
2258
2355
  const now = Date.now();
2259
- for (const node of nodes) {
2260
- const { node: nodeId, users, endpoint } = node;
2261
- const foundId = this.#nodesByEndpoint.get(endpoint);
2262
- if (foundId) {
2263
- if (foundId !== nodeId) {
2264
- this.#logger.warn(`endpoint ${endpoint} clash. replacing node ${foundId} with ${nodeId}`);
2265
- this.#nodesByEndpoint.set(endpoint, nodeId);
2266
- this.#nodes.delete(foundId);
2267
- this.#eventEmitter.emit("endpoint-deleted", foundId, endpoint, "clashed");
2268
- }
2269
- } else {
2270
- this.#logger.info(`endpoint ${endpoint} announced for ${nodeId}`);
2271
- this.#nodesByEndpoint.set(endpoint, nodeId);
2356
+ for (const announcement of announcements) {
2357
+ const { node: nodeId, users, endpoint: announcedEndpoint, metadata } = announcement;
2358
+ const replicas = hrwHash(nodeId, this.#clusterNodes);
2359
+ const owner = replicas[0];
2360
+ const externalEndpoint = announcedEndpoint !== void 0 && announcedEndpoint !== `/cluster?node=${nodeId}`;
2361
+ const endpoint = externalEndpoint ? announcedEndpoint : `/cluster?node=${nodeId}&owner=${owner}`;
2362
+ const lamport = this.#lamport.tick();
2363
+ const userSet = new Set(users ?? []);
2364
+ let node = this.#nodes.get(nodeId);
2365
+ if (!node) {
2366
+ node = { node: nodeId, users: userSet, firstSeen: lamport, lastSeen: now, owner, replicas, endpoint, metadata, status: "announced" };
2367
+ this.#nodes.set(nodeId, node);
2368
+ this.#logger.info(`${nodeId} announced at endpoint ${endpoint} with meta: ${JSON.stringify(metadata)}`);
2369
+ }
2370
+ node.owner = owner;
2371
+ if (!externalEndpoint) {
2372
+ node.endpoint = endpoint;
2272
2373
  }
2273
- const set = new Set(users ?? []);
2274
- this.#nodes.set(nodeId, this.updateNode(node, set, nodeId, now, this.#nodes.get(nodeId)));
2275
- this.#eventEmitter.emit("endpoint-announced", nodeId, endpoint, set);
2276
- }
2277
- this.cleanupOldNodes();
2278
- return nodes.map((e) => {
2279
- const { node } = e;
2280
- const connect = this.findConnections(this.#nodes.get(node));
2281
- return { node, connect };
2374
+ node.replicas = replicas;
2375
+ node.lastSeen = now;
2376
+ if (!(node.status === "announced" || node.status === "added")) {
2377
+ node.status = "announced";
2378
+ }
2379
+ node.users = userSet;
2380
+ this.#eventEmitter.emit("node-announced", nodeId, endpoint, userSet);
2381
+ }
2382
+ const sortedNodes = this.#sortedNodeValues();
2383
+ return announcements.map((e) => {
2384
+ const node = this.#nodes.get(e.node);
2385
+ const connect = this.#findConnections(sortedNodes, node);
2386
+ return { node: node.node, owner: node.owner, connect };
2282
2387
  });
2283
2388
  }
2284
2389
  find(nodeId) {
2285
2390
  const e = this.#nodes.get(nodeId);
2286
2391
  if (e !== void 0) {
2287
- return this.findConnections(e);
2392
+ const sortedNodes = this.#sortedNodeValues();
2393
+ return this.#findConnections(sortedNodes, e);
2288
2394
  }
2289
2395
  return void 0;
2290
2396
  }
2397
+ add(nodeId) {
2398
+ const added = this.#nodes.get(nodeId);
2399
+ if (added !== void 0 && (added.status === "announced" || added.status === "removed")) {
2400
+ added.status = "added";
2401
+ this.#logger.info(`endpoint ${added.endpoint} connected for ${nodeId}`);
2402
+ this.#eventEmitter.emit("node-connected", nodeId);
2403
+ return true;
2404
+ }
2405
+ return false;
2406
+ }
2291
2407
  remove(nodeId) {
2292
2408
  const removed = this.#nodes.get(nodeId);
2293
- if (removed !== void 0) {
2294
- this.#nodes.delete(nodeId);
2295
- const endpoint = removed.endpoint;
2296
- this.#nodesByEndpoint.delete(endpoint);
2297
- this.#logger.info(`endpoint ${endpoint} removed for ${nodeId}`);
2298
- this.#eventEmitter.emit("endpoint-deleted", nodeId, endpoint, "removed");
2409
+ if (removed !== void 0 && (removed.status === "announced" || removed.status === "added")) {
2410
+ removed.status = "removed";
2411
+ this.#logger.info(`endpoint ${removed.endpoint} removed for ${nodeId}`);
2412
+ this.#eventEmitter.emit("node-disconnected", nodeId, "removed");
2299
2413
  return true;
2300
2414
  }
2301
2415
  return false;
2302
2416
  }
2303
- updateNode(newNode, users, _key, lastAccess, oldNode) {
2304
- const node = oldNode ?? { ...newNode };
2305
- return { ...node, users, lastAccess };
2417
+ #sortedNodeValues() {
2418
+ this.#cleanupOldNodes();
2419
+ return Array.from(this.#nodes.values()).filter((n) => n.status === "announced" || n.status === "added").sort((a, b) => {
2420
+ const diff = a.firstSeen.timestamp - b.firstSeen.timestamp;
2421
+ if (diff === 0) {
2422
+ return a.node.localeCompare(b.node);
2423
+ }
2424
+ return diff;
2425
+ });
2306
2426
  }
2307
- cleanupOldNodes() {
2308
- const threshold = Date.now() - this.#timeout;
2309
- for (const [nodeId, v] of this.#nodes.entries()) {
2310
- if (v.lastAccess < threshold) {
2427
+ #cleanupOldNodes() {
2428
+ const expiredTimestamp = Date.now() - this.#timeout;
2429
+ const removedTimestamp = Date.now() - this.#timeout * 2;
2430
+ const compactTimestamp = Date.now() - this.#timeout * 10;
2431
+ for (const [nodeId, v] of this.#nodes) {
2432
+ if (v.lastSeen < compactTimestamp && v.status === "removed") {
2311
2433
  if (this.#logger.enabledFor("debug")) {
2312
- this.#logger.debug(`${nodeId} expired - no announcement since ${new Date(v.lastAccess).toISOString()}, timeout is ${this.#timeout} ms.`);
2434
+ this.#logger.debug(`${nodeId} compact - no announcement since ${new Date(v.lastSeen).toISOString()}, timeout is ${this.#timeout} ms and status is ${v.status}.`);
2313
2435
  }
2314
2436
  this.#nodes.delete(nodeId);
2315
- const endpoint = v.endpoint;
2316
- this.#nodesByEndpoint.delete(endpoint);
2317
- this.#eventEmitter.emit("endpoint-deleted", nodeId, endpoint, "expired");
2437
+ }
2438
+ if (v.lastSeen < removedTimestamp && v.status === "expired") {
2439
+ if (this.#logger.enabledFor("debug")) {
2440
+ this.#logger.debug(`${nodeId} removed - no announcement since ${new Date(v.lastSeen).toISOString()}, timeout is ${this.#timeout} ms.`);
2441
+ }
2442
+ v.status = "removed";
2443
+ }
2444
+ if (v.lastSeen < expiredTimestamp && (v.status === "announced" || v.status === "added")) {
2445
+ if (this.#logger.enabledFor("debug")) {
2446
+ this.#logger.debug(`${nodeId} expired - no announcement since ${new Date(v.lastSeen).toISOString()}, timeout is ${this.#timeout} ms.`);
2447
+ }
2448
+ v.status = "expired";
2449
+ this.#eventEmitter.emit("node-disconnected", nodeId, "expired");
2318
2450
  }
2319
2451
  }
2320
2452
  }
@@ -2326,26 +2458,29 @@ var InMemoryNodeConnections = class {
2326
2458
  this.#eventEmitter.off(event, listener);
2327
2459
  return this;
2328
2460
  }
2329
- findConnections(node) {
2330
- const connections = new Array();
2331
- for (const [key, value] of this.#nodes.entries()) {
2332
- if (node !== void 0 && key < node.node) {
2333
- const intersection = new Set(value.users);
2461
+ #findConnections(sortedNodes, node) {
2462
+ const connections = sortedNodes.reduce((l, c) => {
2463
+ if (node !== void 0 && c.firstSeen.timestamp < node.firstSeen.timestamp) {
2464
+ const intersection = new Set(c.users);
2334
2465
  node.users.forEach((user) => {
2335
- if (!value.users.has(user)) {
2466
+ if (!c.users.has(user)) {
2336
2467
  intersection.delete(user);
2337
2468
  }
2338
2469
  });
2339
- value.users.forEach((user) => {
2470
+ c.users.forEach((user) => {
2340
2471
  if (!node.users.has(user)) {
2341
2472
  intersection.delete(user);
2342
2473
  }
2343
2474
  });
2344
2475
  if (intersection.size > 0) {
2345
- const e = { node: value.node, endpoint: value.endpoint };
2346
- connections.push(e);
2476
+ const e = { node: c.node, endpoint: c.endpoint, owner: c.owner };
2477
+ return l.concat(e);
2347
2478
  }
2348
2479
  }
2480
+ return l;
2481
+ }, new Array());
2482
+ if (this.#logger.enabledFor("debug")) {
2483
+ this.#logger.debug(`found ${connections.length} connection(s) for node ${node?.node}: [${connections.map((e) => `${e.node}@${e.endpoint}`).join(", ")}]`);
2349
2484
  }
2350
2485
  return connections;
2351
2486
  }
@@ -2359,7 +2494,7 @@ function parseStartingContext(env = process.env) {
2359
2494
  if (!isRunningInConnectDesktop(env)) {
2360
2495
  throw new Error("Not running in io.Connect Desktop");
2361
2496
  }
2362
- return JSON.parse(env._GD_STARTING_CONTEXT_).applicationConfig?.customProperties;
2497
+ return JSON.parse(env._GD_STARTING_CONTEXT_);
2363
2498
  }
2364
2499
 
2365
2500
  // ../bridge-gateway/src/index.ts
@@ -2369,8 +2504,8 @@ import { join } from "node:path";
2369
2504
  import "@interopio/gateway";
2370
2505
  import "@interopio/gateway-server";
2371
2506
  function onGatewayStarted(log) {
2372
- return (gateway) => {
2373
- const info = JSON.stringify(gateway.info());
2507
+ return (gateway2) => {
2508
+ const info = JSON.stringify({ ...gateway2.info(), pid: process.pid });
2374
2509
  if (isRunningInConnectDesktop()) {
2375
2510
  const env = process.env["GLUE-ENV"] || "DEMO";
2376
2511
  const region = process.env["GLUE-REGION"] || "INTEROP.IO";
@@ -2391,14 +2526,15 @@ function onGatewayStarted(log) {
2391
2526
  }
2392
2527
  };
2393
2528
  }
2394
- async function serverGatewayConfig(gateway, bridge, env = process.env) {
2395
- let enabled = gateway.enabled;
2529
+ async function serverGatewayConfig(gateway2, bridge, env = process.env) {
2530
+ let enabled = gateway2.enabled;
2396
2531
  if (enabled === void 0) {
2397
2532
  if (isRunningInConnectDesktop(env)) {
2398
- enabled = parseStartingContext(env)?.gatewayApp === true;
2533
+ const applicationConfig = parseStartingContext(env)?.applicationConfig;
2534
+ enabled = applicationConfig?.customProperties?.gatewayApp === true || applicationConfig?.name === "io-connect-gateway";
2399
2535
  }
2400
2536
  }
2401
- let contextsLifetime = gateway.contexts.lifetime;
2537
+ let contextsLifetime = gateway2.contexts.lifetime;
2402
2538
  if (contextsLifetime === void 0) {
2403
2539
  if (isRunningInConnectDesktop(env)) {
2404
2540
  contextsLifetime = "retained";
@@ -2412,6 +2548,8 @@ async function serverGatewayConfig(gateway, bridge, env = process.env) {
2412
2548
  },
2413
2549
  ping: bridge.wsPingInterval ?? void 0,
2414
2550
  mesh: {
2551
+ auth: { user: null },
2552
+ node: "bridge-gateway",
2415
2553
  channel: bridge.meshChannel
2416
2554
  },
2417
2555
  contexts: {
@@ -2493,7 +2631,7 @@ var BridgeNode = class {
2493
2631
  const discoveryConfig = joinConfig.discovery;
2494
2632
  this.discoveryService = createDiscoveryService(getLogger("discovery"), discoveryConfig, isAutoDetectionEnabled, localMemberPromise);
2495
2633
  const relays = new InternalRelays(this.getLogger("relays"));
2496
- const connections = new InMemoryNodeConnections(this.getLogger("nodes"), this.config.mesh.timeout ?? 6e4);
2634
+ const connections = new InMemoryNodeConnections(this.getLogger("nodes"), this.uuid, this.config.mesh.timeout ?? 6e4);
2497
2635
  this.meshChannel = new BridgeMeshChannel(relays, connections, this.getLogger("channel"));
2498
2636
  }
2499
2637
  getLogger(name) {
@@ -2513,6 +2651,7 @@ var BridgeNode = class {
2513
2651
  return setInterval(task, licenseCheckInterval);
2514
2652
  }
2515
2653
  async start() {
2654
+ await this.meshChannel.init();
2516
2655
  const config = {
2517
2656
  port: this.config.server.port,
2518
2657
  host: this.config.server.host,
@@ -2550,7 +2689,7 @@ var BridgeNode = class {
2550
2689
  await this.discoveryService.close();
2551
2690
  } catch (e) {
2552
2691
  }
2553
- this.meshChannel.close();
2692
+ await this.meshChannel.close();
2554
2693
  }
2555
2694
  };
2556
2695