wire-mesh-core 1.58.6 → 1.59.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.
@@ -7,12 +7,21 @@ function deviceKey(device) {
7
7
  for (const byte of device) key += byte.toString(HEX_RADIX).padStart(HEX_DIGITS_PER_BYTE, "0");
8
8
  return key;
9
9
  }
10
- function createRelayHub() {
10
+ /** The exact inverse of deviceKey, so a pairing map's own hex keys can be exported as the device-ids they were built from without holding a second copy of the bytes on the forwarding path. */
11
+ function deviceFromKey(key) {
12
+ const device = new Uint8Array(key.length / HEX_DIGITS_PER_BYTE);
13
+ for (let i = 0; i < device.length; i++) device[i] = Number.parseInt(key.slice(i * HEX_DIGITS_PER_BYTE, (i + 1) * HEX_DIGITS_PER_BYTE), HEX_RADIX);
14
+ return device;
15
+ }
16
+ function createRelayHub(options = {}) {
11
17
  const connections = /* @__PURE__ */ new Set();
12
18
  const devices = /* @__PURE__ */ new Map();
13
19
  const connectionDevice = /* @__PURE__ */ new Map();
14
20
  const pairings = /* @__PURE__ */ new Map();
15
21
  const mostRecentPairing = /* @__PURE__ */ new Map();
22
+ function stateChanged(connection) {
23
+ options.onConnectionStateChanged?.(connection);
24
+ }
16
25
  function pairingsOf(connection) {
17
26
  const existing = pairings.get(connection);
18
27
  if (existing) return existing;
@@ -38,20 +47,75 @@ function createRelayHub() {
38
47
  const peerOwn = pairings.get(peer);
39
48
  if (peerOwn && ownDevice !== void 0) peerOwn.delete(deviceKey(ownDevice));
40
49
  if (mostRecentPairing.get(peer) === connection) mostRecentPairing.delete(peer);
50
+ stateChanged(peer);
41
51
  }
42
52
  pairings.delete(connection);
43
53
  }
44
54
  }
45
- async function handleFrame(connection, frame) {
46
- if (frame.type === "gossip") {
47
- for (const advert of frame.peers) devices.set(deviceKey(advert.device), {
55
+ function exportConnection(connection) {
56
+ const device = connectionDevice.get(connection);
57
+ if (device === void 0) return;
58
+ const adverts = [];
59
+ for (const registration of devices.values()) if (registration.connection === connection) adverts.push(registration.advert);
60
+ const own = pairings.get(connection);
61
+ const pairedDevices = [...own?.keys() ?? []].map(deviceFromKey);
62
+ const recentPeer = mostRecentPairing.get(connection);
63
+ let mostRecentDevice;
64
+ if (own && recentPeer) {
65
+ for (const [key, peer] of own) if (peer === recentPeer) {
66
+ mostRecentDevice = deviceFromKey(key);
67
+ break;
68
+ }
69
+ }
70
+ return {
71
+ device,
72
+ adverts,
73
+ pairedDevices,
74
+ ...mostRecentDevice !== void 0 ? { mostRecentDevice } : {}
75
+ };
76
+ }
77
+ function restoreConnections(entries) {
78
+ const byDevice = /* @__PURE__ */ new Map();
79
+ for (const { connection, state } of entries) {
80
+ connections.add(connection);
81
+ connectionDevice.set(connection, state.device);
82
+ byDevice.set(deviceKey(state.device), connection);
83
+ for (const advert of state.adverts) devices.set(deviceKey(advert.device), {
48
84
  connection,
49
85
  advert
50
86
  });
87
+ }
88
+ for (const { connection, state } of entries) for (const paired of state.pairedDevices) {
89
+ const peer = byDevice.get(deviceKey(paired));
90
+ if (peer === void 0) continue;
91
+ pairingsOf(connection).set(deviceKey(paired), peer);
92
+ pairingsOf(peer).set(deviceKey(state.device), connection);
93
+ }
94
+ for (const { connection, state } of entries) {
95
+ const recent = state.mostRecentDevice;
96
+ if (recent === void 0) continue;
97
+ const peer = byDevice.get(deviceKey(recent));
98
+ if (peer !== void 0) mostRecentPairing.set(connection, peer);
99
+ }
100
+ }
101
+ async function handleFrame(connection, frame) {
102
+ if (frame.type === "gossip") {
103
+ const supplanted = /* @__PURE__ */ new Set();
104
+ for (const advert of frame.peers) {
105
+ const key = deviceKey(advert.device);
106
+ const previous = devices.get(key);
107
+ if (previous !== void 0 && previous.connection !== connection) supplanted.add(previous.connection);
108
+ devices.set(key, {
109
+ connection,
110
+ advert
111
+ });
112
+ }
51
113
  for (const registration of devices.values()) if (registration.connection === connection) {
52
114
  connectionDevice.set(connection, registration.advert.device);
53
115
  break;
54
116
  }
117
+ stateChanged(connection);
118
+ for (const other of supplanted) stateChanged(other);
55
119
  for (const other of connections) {
56
120
  if (other === connection) continue;
57
121
  try {
@@ -74,6 +138,8 @@ function createRelayHub() {
74
138
  const initiatorDevice = connectionDevice.get(connection);
75
139
  if (initiatorDevice === void 0) return;
76
140
  addPairing(connection, initiatorDevice, registration.connection, registration.advert.device);
141
+ stateChanged(connection);
142
+ stateChanged(registration.connection);
77
143
  await registration.connection.send({
78
144
  type: "relay-inbound",
79
145
  "source-device": initiatorDevice
@@ -116,6 +182,8 @@ function createRelayHub() {
116
182
  },
117
183
  onFrame: handleFrame,
118
184
  onDisconnect: forgetConnection,
185
+ exportConnection,
186
+ restoreConnections,
119
187
  stop() {
120
188
  connections.clear();
121
189
  devices.clear();
@@ -1,6 +1,28 @@
1
- import { N as Frame } from "../protocol-BnbpCk0Z.cjs";
1
+ import { D as DeviceId, N as Frame, et as PeerAdvert } from "../protocol-BnbpCk0Z.cjs";
2
2
  import { t as Connection } from "../transport-Ca_ZyyZe.cjs";
3
3
  //#region src/domain/relay-hub.d.ts
4
+ /**
5
+ * One connection's relay state in serialisable form: everything a hub that never saw the connection register needs, alongside the still-open connection itself, to answer its next frame exactly as the hub that established the state would have. Only device-ids and adverts appear, never a Connection, so a host can persist this wherever it keeps per-connection state.
6
+ */
7
+ export interface RelayConnectionState {
8
+ /** The device-id the hub attributes to the connection itself: what its relay-data frames are stamped with as `from-device`, and what a peer's own pairing map keys this connection by. */
9
+ readonly device: DeviceId;
10
+ /** Every peer-advert still registered against this connection in the hub's device directory, in registration order, so a restored hub can resolve a relay-connect naming one of them and can replay them in its gossip catch-up. Empty when a later connection has taken over every device-id this one advertised. */
11
+ readonly adverts: readonly PeerAdvert[];
12
+ /** The device-id of each peer this connection currently holds a relay pairing with. Order carries no meaning; the unaddressed-relay-data route is `mostRecentDevice` alone. */
13
+ readonly pairedDevices: readonly DeviceId[];
14
+ /** The peer a relay-data frame carrying no `to-device` routes to. Absent when the connection has no such fallback, either because it holds no pairings or because the peer that was most recent has since disconnected. */
15
+ readonly mostRecentDevice?: DeviceId;
16
+ }
17
+ /** One surviving connection paired with what a previous hub instance knew about it, as restoreConnections takes it. */
18
+ export interface RelayConnectionRestore {
19
+ readonly connection: Readonly<Connection>;
20
+ readonly state: RelayConnectionState;
21
+ }
22
+ export interface RelayHubOptions {
23
+ /** Called whenever the value exportConnection returns for a connection changes: it gossiped an advert, gained or lost a pairing, or had one of its adverts taken over by another connection. A host whose own instance can be torn down while its connections stay open (the Cloudflare hub's hibernating Durable Object) reads the new value back with exportConnection, persists it against the connection, and hands it to a later instance through restoreConnections. */
24
+ readonly onConnectionStateChanged?: (connection: Readonly<Connection>) => void;
25
+ }
4
26
  export interface RelayHub {
5
27
  /** Drives one accepted connection until it closes: registers gossip-advertised devices, re-broadcasts each gossip frame to every other connected client and replies to the gossiping connection with a catch-up frame of every other already-known device, answers relay-connect by pairing and notifying the target, forwards relay-data within established pairings, and replies to a bare ping with a bare pong (wire-mesh#181) so a client can time its own sender-to-hub leg independent of anything being relayed. Resolves when the connection's frame stream ends. Internally just registerConnection followed by a loop of onFrame calls and a final onDisconnect -- kept as its own method since a dedicated hub deployment (wire-mesh/cloudflare-hub) has nothing else driving the connection and wants the whole lifecycle in one call. */
6
28
  handleConnection: (connection: Readonly<Connection>) => Promise<void>;
@@ -10,8 +32,12 @@ export interface RelayHub {
10
32
  onFrame: (connection: Readonly<Connection>, frame: Frame) => Promise<void>;
11
33
  /** Cleans up all registry and pairing state for one connection once its own frame stream has ended -- the onSessionEnd counterpart registerConnection/onFrame needs for the shared-consumption case, since this hub's registry is keyed by Connection and has no other way to learn a connection is gone. */
12
34
  onDisconnect: (connection: Readonly<Connection>) => void;
35
+ /** The serialisable relay state of one registered connection, or undefined when it holds none worth keeping: a connection that has never gossiped an advert can neither be named by a relay-connect nor send a routable relay-data frame, so there is nothing for a later hub to restore. */
36
+ exportConnection: (connection: Readonly<Connection>) => RelayConnectionState | undefined;
37
+ /** Rebuilds device-registry and pairing state for connections that outlived the hub instance which established it, so a frame arriving on one of them routes as it did before rather than being dropped as unknown. Each entry's connection is registered as if registerConnection had been called for it. Pairings resolve within the batch: a paired device-id no entry claims belonged to a peer that disconnected while no instance was running, and its pairing is correctly left out. Meant for a freshly created hub, before it handles any frame. */
38
+ restoreConnections: (entries: readonly RelayConnectionRestore[]) => void;
13
39
  /** Drops all registry and pairing state -- used by tests and by transport teardown. */
14
40
  stop: () => void;
15
41
  }
16
- export declare function createRelayHub(): RelayHub;
42
+ export declare function createRelayHub(options?: RelayHubOptions): RelayHub;
17
43
  //#endregion
@@ -1,6 +1,28 @@
1
- import { N as Frame } from "../protocol-BnbpCk0Z.mjs";
1
+ import { D as DeviceId, N as Frame, et as PeerAdvert } from "../protocol-BnbpCk0Z.mjs";
2
2
  import { t as Connection } from "../transport-BH8kGWXt.mjs";
3
3
  //#region src/domain/relay-hub.d.ts
4
+ /**
5
+ * One connection's relay state in serialisable form: everything a hub that never saw the connection register needs, alongside the still-open connection itself, to answer its next frame exactly as the hub that established the state would have. Only device-ids and adverts appear, never a Connection, so a host can persist this wherever it keeps per-connection state.
6
+ */
7
+ export interface RelayConnectionState {
8
+ /** The device-id the hub attributes to the connection itself: what its relay-data frames are stamped with as `from-device`, and what a peer's own pairing map keys this connection by. */
9
+ readonly device: DeviceId;
10
+ /** Every peer-advert still registered against this connection in the hub's device directory, in registration order, so a restored hub can resolve a relay-connect naming one of them and can replay them in its gossip catch-up. Empty when a later connection has taken over every device-id this one advertised. */
11
+ readonly adverts: readonly PeerAdvert[];
12
+ /** The device-id of each peer this connection currently holds a relay pairing with. Order carries no meaning; the unaddressed-relay-data route is `mostRecentDevice` alone. */
13
+ readonly pairedDevices: readonly DeviceId[];
14
+ /** The peer a relay-data frame carrying no `to-device` routes to. Absent when the connection has no such fallback, either because it holds no pairings or because the peer that was most recent has since disconnected. */
15
+ readonly mostRecentDevice?: DeviceId;
16
+ }
17
+ /** One surviving connection paired with what a previous hub instance knew about it, as restoreConnections takes it. */
18
+ export interface RelayConnectionRestore {
19
+ readonly connection: Readonly<Connection>;
20
+ readonly state: RelayConnectionState;
21
+ }
22
+ export interface RelayHubOptions {
23
+ /** Called whenever the value exportConnection returns for a connection changes: it gossiped an advert, gained or lost a pairing, or had one of its adverts taken over by another connection. A host whose own instance can be torn down while its connections stay open (the Cloudflare hub's hibernating Durable Object) reads the new value back with exportConnection, persists it against the connection, and hands it to a later instance through restoreConnections. */
24
+ readonly onConnectionStateChanged?: (connection: Readonly<Connection>) => void;
25
+ }
4
26
  export interface RelayHub {
5
27
  /** Drives one accepted connection until it closes: registers gossip-advertised devices, re-broadcasts each gossip frame to every other connected client and replies to the gossiping connection with a catch-up frame of every other already-known device, answers relay-connect by pairing and notifying the target, forwards relay-data within established pairings, and replies to a bare ping with a bare pong (wire-mesh#181) so a client can time its own sender-to-hub leg independent of anything being relayed. Resolves when the connection's frame stream ends. Internally just registerConnection followed by a loop of onFrame calls and a final onDisconnect -- kept as its own method since a dedicated hub deployment (wire-mesh/cloudflare-hub) has nothing else driving the connection and wants the whole lifecycle in one call. */
6
28
  handleConnection: (connection: Readonly<Connection>) => Promise<void>;
@@ -10,8 +32,12 @@ export interface RelayHub {
10
32
  onFrame: (connection: Readonly<Connection>, frame: Frame) => Promise<void>;
11
33
  /** Cleans up all registry and pairing state for one connection once its own frame stream has ended -- the onSessionEnd counterpart registerConnection/onFrame needs for the shared-consumption case, since this hub's registry is keyed by Connection and has no other way to learn a connection is gone. */
12
34
  onDisconnect: (connection: Readonly<Connection>) => void;
35
+ /** The serialisable relay state of one registered connection, or undefined when it holds none worth keeping: a connection that has never gossiped an advert can neither be named by a relay-connect nor send a routable relay-data frame, so there is nothing for a later hub to restore. */
36
+ exportConnection: (connection: Readonly<Connection>) => RelayConnectionState | undefined;
37
+ /** Rebuilds device-registry and pairing state for connections that outlived the hub instance which established it, so a frame arriving on one of them routes as it did before rather than being dropped as unknown. Each entry's connection is registered as if registerConnection had been called for it. Pairings resolve within the batch: a paired device-id no entry claims belonged to a peer that disconnected while no instance was running, and its pairing is correctly left out. Meant for a freshly created hub, before it handles any frame. */
38
+ restoreConnections: (entries: readonly RelayConnectionRestore[]) => void;
13
39
  /** Drops all registry and pairing state -- used by tests and by transport teardown. */
14
40
  stop: () => void;
15
41
  }
16
- export declare function createRelayHub(): RelayHub;
42
+ export declare function createRelayHub(options?: RelayHubOptions): RelayHub;
17
43
  //#endregion
@@ -6,12 +6,21 @@ function deviceKey(device) {
6
6
  for (const byte of device) key += byte.toString(HEX_RADIX).padStart(HEX_DIGITS_PER_BYTE, "0");
7
7
  return key;
8
8
  }
9
- function createRelayHub() {
9
+ /** The exact inverse of deviceKey, so a pairing map's own hex keys can be exported as the device-ids they were built from without holding a second copy of the bytes on the forwarding path. */
10
+ function deviceFromKey(key) {
11
+ const device = new Uint8Array(key.length / HEX_DIGITS_PER_BYTE);
12
+ for (let i = 0; i < device.length; i++) device[i] = Number.parseInt(key.slice(i * HEX_DIGITS_PER_BYTE, (i + 1) * HEX_DIGITS_PER_BYTE), HEX_RADIX);
13
+ return device;
14
+ }
15
+ function createRelayHub(options = {}) {
10
16
  const connections = /* @__PURE__ */ new Set();
11
17
  const devices = /* @__PURE__ */ new Map();
12
18
  const connectionDevice = /* @__PURE__ */ new Map();
13
19
  const pairings = /* @__PURE__ */ new Map();
14
20
  const mostRecentPairing = /* @__PURE__ */ new Map();
21
+ function stateChanged(connection) {
22
+ options.onConnectionStateChanged?.(connection);
23
+ }
15
24
  function pairingsOf(connection) {
16
25
  const existing = pairings.get(connection);
17
26
  if (existing) return existing;
@@ -37,20 +46,75 @@ function createRelayHub() {
37
46
  const peerOwn = pairings.get(peer);
38
47
  if (peerOwn && ownDevice !== void 0) peerOwn.delete(deviceKey(ownDevice));
39
48
  if (mostRecentPairing.get(peer) === connection) mostRecentPairing.delete(peer);
49
+ stateChanged(peer);
40
50
  }
41
51
  pairings.delete(connection);
42
52
  }
43
53
  }
44
- async function handleFrame(connection, frame) {
45
- if (frame.type === "gossip") {
46
- for (const advert of frame.peers) devices.set(deviceKey(advert.device), {
54
+ function exportConnection(connection) {
55
+ const device = connectionDevice.get(connection);
56
+ if (device === void 0) return;
57
+ const adverts = [];
58
+ for (const registration of devices.values()) if (registration.connection === connection) adverts.push(registration.advert);
59
+ const own = pairings.get(connection);
60
+ const pairedDevices = [...own?.keys() ?? []].map(deviceFromKey);
61
+ const recentPeer = mostRecentPairing.get(connection);
62
+ let mostRecentDevice;
63
+ if (own && recentPeer) {
64
+ for (const [key, peer] of own) if (peer === recentPeer) {
65
+ mostRecentDevice = deviceFromKey(key);
66
+ break;
67
+ }
68
+ }
69
+ return {
70
+ device,
71
+ adverts,
72
+ pairedDevices,
73
+ ...mostRecentDevice !== void 0 ? { mostRecentDevice } : {}
74
+ };
75
+ }
76
+ function restoreConnections(entries) {
77
+ const byDevice = /* @__PURE__ */ new Map();
78
+ for (const { connection, state } of entries) {
79
+ connections.add(connection);
80
+ connectionDevice.set(connection, state.device);
81
+ byDevice.set(deviceKey(state.device), connection);
82
+ for (const advert of state.adverts) devices.set(deviceKey(advert.device), {
47
83
  connection,
48
84
  advert
49
85
  });
86
+ }
87
+ for (const { connection, state } of entries) for (const paired of state.pairedDevices) {
88
+ const peer = byDevice.get(deviceKey(paired));
89
+ if (peer === void 0) continue;
90
+ pairingsOf(connection).set(deviceKey(paired), peer);
91
+ pairingsOf(peer).set(deviceKey(state.device), connection);
92
+ }
93
+ for (const { connection, state } of entries) {
94
+ const recent = state.mostRecentDevice;
95
+ if (recent === void 0) continue;
96
+ const peer = byDevice.get(deviceKey(recent));
97
+ if (peer !== void 0) mostRecentPairing.set(connection, peer);
98
+ }
99
+ }
100
+ async function handleFrame(connection, frame) {
101
+ if (frame.type === "gossip") {
102
+ const supplanted = /* @__PURE__ */ new Set();
103
+ for (const advert of frame.peers) {
104
+ const key = deviceKey(advert.device);
105
+ const previous = devices.get(key);
106
+ if (previous !== void 0 && previous.connection !== connection) supplanted.add(previous.connection);
107
+ devices.set(key, {
108
+ connection,
109
+ advert
110
+ });
111
+ }
50
112
  for (const registration of devices.values()) if (registration.connection === connection) {
51
113
  connectionDevice.set(connection, registration.advert.device);
52
114
  break;
53
115
  }
116
+ stateChanged(connection);
117
+ for (const other of supplanted) stateChanged(other);
54
118
  for (const other of connections) {
55
119
  if (other === connection) continue;
56
120
  try {
@@ -73,6 +137,8 @@ function createRelayHub() {
73
137
  const initiatorDevice = connectionDevice.get(connection);
74
138
  if (initiatorDevice === void 0) return;
75
139
  addPairing(connection, initiatorDevice, registration.connection, registration.advert.device);
140
+ stateChanged(connection);
141
+ stateChanged(registration.connection);
76
142
  await registration.connection.send({
77
143
  type: "relay-inbound",
78
144
  "source-device": initiatorDevice
@@ -115,6 +181,8 @@ function createRelayHub() {
115
181
  },
116
182
  onFrame: handleFrame,
117
183
  onDisconnect: forgetConnection,
184
+ exportConnection,
185
+ restoreConnections,
118
186
  stop() {
119
187
  connections.clear();
120
188
  devices.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wire-mesh-core",
3
- "version": "1.58.6",
3
+ "version": "1.59.0",
4
4
  "dependencies": {
5
5
  "cbor2": "2.3.0",
6
6
  "cddl.js": "1.0.1",