@peers-app/peers-sdk 0.30.3 → 0.30.4

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.
@@ -5,20 +5,30 @@ export type MeshDialReason = "own-device" | "group-network-info" | "broadcast-an
5
5
  /** Bounded path categories used for lifetime counters; attempts also retain the address. */
6
6
  export type MeshPath = "websocket" | "webrtc" | "cloud" | "discovery";
7
7
  /** Failure categories, independent of free-form transport error messages. */
8
- export type MeshFailureReason = "ttl0" | "no-route" | "signaling-timeout" | "socket-error" | "socket-timeout" | "handshake" | "rejected" | "closed" | "superseded" | "unsupported";
8
+ export type MeshFailureReason = "ttl0" | "no-route" | "signaling-timeout" | "socket-error" | "socket-timeout" | "handshake" | "rejected" | "closed" | "superseded" | "unsupported" | "sender-not-known" | "trust-refused" | "identity-mismatch" | "cancelled" | "capacity";
9
9
  /** Observed stage; timestamps are offsets in milliseconds from the attempt's queued time. */
10
10
  export type MeshStage = "queued" | "started" | "authenticated" | "admitted" | "ready" | "firstSync";
11
- /** Optional observational metadata passed through a dial without affecting its result. */
11
+ /** Local dial metadata and cancellation; never serialized into device messages. */
12
12
  export interface IMeshDial {
13
+ /** Cancels pending transport work; an admitted winner is retained. */
14
+ signal?: AbortSignal;
15
+ /** Scheduler priority: bootstrap, coverage, repair, then redundancy. */
16
+ priority?: 1 | 2 | 3 | 4;
13
17
  attemptId?: string;
14
18
  reason?: MeshDialReason;
15
19
  targetDeviceId?: string;
20
+ /** One-based retry number; absent for the initial dial. */
21
+ retry?: number;
16
22
  }
17
23
  /** One dial or inbound connection. Missing stages have not been observed. */
18
24
  export interface IMeshAttempt {
19
25
  attemptId: string;
20
26
  targetDeviceId?: string;
21
27
  reason: MeshDialReason;
28
+ /** One-based retry number; absent for initial and incoming attempts. */
29
+ retry?: number;
30
+ /** Scheduler priority when this was planned locally. */
31
+ priority?: 1 | 2 | 3 | 4;
22
32
  direction: "outgoing" | "incoming";
23
33
  path: MeshPath;
24
34
  address?: string;
@@ -33,7 +43,7 @@ export interface IMeshAttempt {
33
43
  };
34
44
  ready: Record<string, number>;
35
45
  firstSync: Record<string, number>;
36
- outcome: "pending" | "admitted" | "failed" | "unfinished";
46
+ outcome: "pending" | "admitted" | "failed" | "cancelled" | "unfinished";
37
47
  endReason?: "buffer-evicted" | "disposed";
38
48
  endedAt?: number;
39
49
  durationMs?: number;
@@ -60,6 +70,10 @@ export interface IMeshContextCoverage {
60
70
  possibleSourceDeviceIds: string[];
61
71
  usableDeviceIds: string[];
62
72
  excluded: boolean;
73
+ /** True when an unmet context cannot currently fit at the connection cap. */
74
+ limitedByCapacity?: boolean;
75
+ /** Planner target; zero means no possible source. */
76
+ target?: number;
63
77
  }
64
78
  /** One connectivity episode, independent of any other episode in flight. */
65
79
  export interface IMeshEpisode {
@@ -69,8 +83,8 @@ export interface IMeshEpisode {
69
83
  startedAt: number;
70
84
  endedAt?: number;
71
85
  durationMs?: number;
72
- outcome: "active" | "covered" | "unfinished";
73
- endReason?: "covered" | "deadline" | "buffer-evicted" | "disposed";
86
+ outcome: "active" | "covered" | "unfinished" | "not-measured";
87
+ endReason?: "covered" | "deadline" | "buffer-evicted" | "disposed" | "page-hidden" | "still-covered";
74
88
  metrics: IMeshDurations;
75
89
  contexts: IMeshContextCoverage[];
76
90
  }
@@ -121,6 +135,11 @@ export interface IMeshStats {
121
135
  probes: number;
122
136
  connected: number;
123
137
  };
138
+ /** Lifetime episodes excluded from latency percentiles, by reason. */
139
+ unmeasuredEpisodes?: {
140
+ pageHidden: number;
141
+ stillCovered: number;
142
+ };
124
143
  unfinishedAttempts: number;
125
144
  doubleDials: number;
126
145
  offlineDials: number;
@@ -137,6 +156,8 @@ export interface IMeshStats {
137
156
  };
138
157
  recentAttempts: IMeshAttempt[];
139
158
  recentBroadcasts: IMeshBroadcast[];
159
+ /** Latest planner coverage, including zero targets and capacity limitations. */
160
+ coverage?: IMeshContextCoverage[];
140
161
  }
141
162
  /** A discovery broadcast and its observed reply, bounded with the attempt history. */
142
163
  export interface IMeshBroadcast {
@@ -42,6 +42,8 @@ export declare class Connection {
42
42
  private pendingSendArm;
43
43
  /** Bumped by every {@link Connection.reset}, so a handshake can tell it was cut short. */
44
44
  private handshakeGeneration;
45
+ /** Local close intent, retained through reset so retry owners do not penalize orderly shedding. */
46
+ closeIntent?: "remote" | "policy";
45
47
  constructor(socket: ISocket, localDevice: Device, localDeviceServerAddresses?: string[] | undefined, getTrustLevel?: GetTrustLevel);
46
48
  /** True once the remote identity has been authenticated by the handshake. */
47
49
  get verified(): boolean;
@@ -151,5 +153,8 @@ export declare class Connection {
151
153
  */
152
154
  close(): Promise<void>;
153
155
  }
154
- /** Normalizes a peer address for transport and security comparisons. */
156
+ /** Lowercases a peer address and removes a trailing slash; preserves its scheme. */
155
157
  export declare function normalizeAddress(address: string): string;
158
+ /** Local handshake refusal. Its existing Error text remains compatible with older peers. */
159
+ export declare class ConnectionTrustError extends Error {
160
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Connection = void 0;
3
+ exports.ConnectionTrustError = exports.Connection = void 0;
4
4
  exports.normalizeAddress = normalizeAddress;
5
5
  const user_context_singleton_1 = require("../context/user-context-singleton");
6
6
  const data_1 = require("../data");
@@ -62,6 +62,8 @@ class Connection {
62
62
  pendingSendArm;
63
63
  /** Bumped by every {@link Connection.reset}, so a handshake can tell it was cut short. */
64
64
  handshakeGeneration = 0;
65
+ /** Local close intent, retained through reset so retry owners do not penalize orderly shedding. */
66
+ closeIntent;
65
67
  constructor(socket, localDevice, localDeviceServerAddresses, getTrustLevel = () => Promise.resolve(socket_type_1.TrustLevel.Unknown)) {
66
68
  this.localDevice = localDevice;
67
69
  this.localDeviceServerAddresses = localDeviceServerAddresses;
@@ -76,6 +78,7 @@ class Connection {
76
78
  socket.setPlaintextStreamsAllowed?.(false);
77
79
  // Only the server side of a connection should expose these methods
78
80
  this.exposeRPC("close", () => {
81
+ this.closeIntent ??= "remote";
79
82
  (0, utils_1.sleep)(100).then(() => {
80
83
  this.closeLocal();
81
84
  });
@@ -404,7 +407,7 @@ class Connection {
404
407
  this.assertHandshakeCurrent(generation);
405
408
  if (trustLevel < socket_type_1.TrustLevel.Unknown) {
406
409
  this.resetRemote();
407
- throw new Error("Untrusted connection");
410
+ throw new ConnectionTrustError("Untrusted connection");
408
411
  }
409
412
  // The client seals everything after it reads our response, so receive
410
413
  // can arm now; send arms after the response frame goes out.
@@ -466,7 +469,7 @@ class Connection {
466
469
  this.trustLevel = trustLevel;
467
470
  if (this.trustLevel < socket_type_1.TrustLevel.Unknown) {
468
471
  this.resetRemote();
469
- throw new Error("Untrusted connection");
472
+ throw new ConnectionTrustError("Untrusted connection");
470
473
  }
471
474
  return handshakeResponse;
472
475
  }
@@ -613,7 +616,11 @@ async function noteHandshakeContest(contest) {
613
616
  console.warn("Could not apply a key contest from the handshake", err);
614
617
  }
615
618
  }
616
- /** Normalizes a peer address for transport and security comparisons. */
619
+ /** Lowercases a peer address and removes a trailing slash; preserves its scheme. */
617
620
  function normalizeAddress(address) {
618
621
  return address.toLowerCase().replace(/\/$/, "");
619
622
  }
623
+ /** Local handshake refusal. Its existing Error text remains compatible with older peers. */
624
+ class ConnectionTrustError extends Error {
625
+ }
626
+ exports.ConnectionTrustError = ConnectionTrustError;
@@ -14,6 +14,7 @@ import type { IBinaryPeer } from "./binary-peer-connection-v2";
14
14
  interface ISocketIO {
15
15
  id?: string;
16
16
  on(event: string, handler: (...args: any[]) => void): void;
17
+ off(event: string, handler: (...args: any[]) => void): void;
17
18
  emit(event: string, ...args: any[]): void;
18
19
  removeAllListeners(event?: string): void;
19
20
  disconnect(): void;
@@ -22,8 +22,11 @@ function wrapSocketIOAsBinaryPeer(socket) {
22
22
  const closeHandlers = [];
23
23
  const drainHandlers = [];
24
24
  let pendingBytes = 0;
25
+ let closed = false;
25
26
  // Listen for binary data
26
- socket.on(BINARY_EVENT, (data) => {
27
+ const onData = (data) => {
28
+ if (closed)
29
+ return;
27
30
  // Ensure we have a proper Uint8Array
28
31
  const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
29
32
  dataHandlers.forEach((handler) => {
@@ -34,9 +37,16 @@ function wrapSocketIOAsBinaryPeer(socket) {
34
37
  console.error("[socket-io-binary] Error in data handler:", err);
35
38
  }
36
39
  });
37
- });
40
+ };
38
41
  // Listen for disconnect
39
- socket.on("disconnect", () => {
42
+ const onDisconnect = () => {
43
+ if (closed)
44
+ return;
45
+ closed = true;
46
+ // This adapter belongs to one connection incarnation. A delayed close from it
47
+ // must never disconnect a reconnected Socket.IO socket or remove its listeners.
48
+ socket.off(BINARY_EVENT, onData);
49
+ socket.off("disconnect", onDisconnect);
40
50
  closeHandlers.forEach((handler) => {
41
51
  try {
42
52
  handler();
@@ -45,7 +55,9 @@ function wrapSocketIOAsBinaryPeer(socket) {
45
55
  console.error("[socket-io-binary] Error in close handler:", err);
46
56
  }
47
57
  });
48
- });
58
+ };
59
+ socket.on(BINARY_EVENT, onData);
60
+ socket.on("disconnect", onDisconnect);
49
61
  return {
50
62
  on(event, handler) {
51
63
  if (event === "data") {
@@ -59,6 +71,8 @@ function wrapSocketIOAsBinaryPeer(socket) {
59
71
  }
60
72
  },
61
73
  send(data) {
74
+ if (closed)
75
+ return;
62
76
  const bytes = typeof data === "string"
63
77
  ? Buffer.from(data)
64
78
  : data instanceof Uint8Array
@@ -79,7 +93,9 @@ function wrapSocketIOAsBinaryPeer(socket) {
79
93
  });
80
94
  },
81
95
  destroy() {
82
- socket.removeAllListeners(BINARY_EVENT);
96
+ if (closed)
97
+ return;
98
+ onDisconnect();
83
99
  socket.disconnect();
84
100
  },
85
101
  getBufferedAmount() {
@@ -26,7 +26,11 @@ export interface IDeviceMessage<T = any> {
26
26
  deviceMessageId: string;
27
27
  /** Device that originated the message. */
28
28
  fromDeviceId: string;
29
- /** Signed source identity used to bootstrap fresh same-user devices. */
29
+ /**
30
+ * Signed source identity. Lets a receiver accept a device it hasn't met yet when the device
31
+ * belongs to the receiver's own user (personal context) or to a counted member of the
32
+ * message's group.
33
+ */
30
34
  fromDeviceInfo?: ISignedObject<IDeviceInfo>;
31
35
  /** Intended destination device. */
32
36
  toDeviceId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.30.3",
3
+ "version": "0.30.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"