pinggy 0.5.7 → 0.5.8

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/CLAUDE.md CHANGED
@@ -95,8 +95,12 @@ tsup bundles to `dist/` (CJS + ESM). tsc type-checks only (`npm run build:tsc`).
95
95
 
96
96
  ## English Style
97
97
 
98
- - **No em-dashes.** Use a period, colon, or restructure the sentence. No obvious characters or constructions used by LLMs and AI.
98
+ - **Hyphen only.** The hyphen (-) is the only dash. No em-dash and no en-dash anywhere in any file. This includes numeric ranges (write 1-20) and empty-cell placeholders in tables. Use a period, a colon, or restructure the sentence instead.
99
+ - **No AI-sounding text.** Do not use words, phrases, or symbols typical of LLM output.
100
+ - **No emoji in the codebase.** Documentation may use one only if it is very apt.
101
+ - **Simple English.** Say things in a straightforward way. Keep every technical term and its exact meaning. Use simple but accurate technical terms, not jargon.
99
102
  - **Short phrases.** Enough to convey technical meaning. Nothing more.
103
+ - **No metaphor or personification** in technical statements. State the behavior literally. Write "a partial report is scored and enters the benchmark", never "a partial is a full citizen".
100
104
  - **No filler words.** Cut: "in order to", "it is important to note", "please note that", "essentially", "basically", "simply".
101
105
  - **No passive voice** unless the subject is unknown or irrelevant.
102
106
  - **No nominalizations.** Prefer "detect" over "perform detection"; "configure" over "apply configuration".
@@ -110,3 +114,12 @@ tsup bundles to `dist/` (CJS + ESM). tsc type-checks only (`npm run build:tsc`).
110
114
  ## Code Style
111
115
  - **No dead code.** Remove unused imports, functions, and variables immediately. `ruff` enforces this.
112
116
  - **Small functions.** If a function needs a comment to explain its sections, split it.
117
+
118
+ ## Answering Questions
119
+
120
+ - **A question is not a change request.** When asked a question, do not change code. Answer it.
121
+
122
+ ## Commit Messages
123
+
124
+ - **Plain change description.** Write the message as a human author would: what changed and why.
125
+ - **No assistant attribution.** Do not mention Claude, AI, or any assistant. No `Co-Authored-By` assistant trailer, no "Generated with" line, no tool attribution.
package/README.md CHANGED
@@ -287,7 +287,7 @@ These flags apply to the CLI invocation. For daemon-wide log level and per-tunne
287
287
  ### **AutoReconnect**
288
288
  | Flag | Description |
289
289
  |------|-------------|
290
- | `--autoreconnect`, `-a` | Automatically reconnect tunnel on failure (enabled by default; pass `false` to disable) |
290
+ | `--no-autoreconnect`, `-a` | Disable automatic reconnection on failure. Enabled by default, and it keeps retrying until the tunnel is back (no attempt limit) |
291
291
 
292
292
  ---
293
293
 
@@ -290,6 +290,8 @@ function pinggyOptionsToTunnelConfigV1(opts, configStoredInCli) {
290
290
  serverAddress: opts.serverAddress || "a.pinggy.io:443",
291
291
  token: opts.token || "",
292
292
  autoReconnect: opts.autoReconnect ?? true,
293
+ reconnectInterval: opts.reconnectInterval,
294
+ maxReconnectAttempts: opts.maxReconnectAttempts,
293
295
  force: opts.force ?? false,
294
296
  webDebugger: opts.webDebugger || "",
295
297
  forwarding: opts.forwarding ? opts.forwarding : "",
@@ -1677,7 +1679,7 @@ async function ensureDaemonRunning() {
1677
1679
  return existing;
1678
1680
  }
1679
1681
  if (process.versions.electron) {
1680
- const { runDaemonChild } = await import("./daemonChild-STFB5OQL.js");
1682
+ const { runDaemonChild } = await import("./daemonChild-ZPZK5F4Q.js");
1681
1683
  const handle = await runDaemonChild({
1682
1684
  installSignalHandlers: false,
1683
1685
  exitOnFailure: false,
@@ -1902,6 +1904,9 @@ var WsStream = class {
1902
1904
  subscriptionCount() {
1903
1905
  return this.subscribedTunnels.size;
1904
1906
  }
1907
+ isSubscribed(tunnelId) {
1908
+ return this.subscribedTunnels.has(tunnelId);
1909
+ }
1905
1910
  /** Snapshot of current subscriptions, for the reconnect path to replay. */
1906
1911
  snapshotSubscriptions() {
1907
1912
  return Array.from(this.subscribedTunnels.entries());
@@ -1935,6 +1940,16 @@ var WsStream = class {
1935
1940
  onDisconnect(cb) {
1936
1941
  this.callbacks.disconnect.push(cb);
1937
1942
  }
1943
+ // Removal replaces the array instead of splicing it, so a callback that
1944
+ // unregisters itself while handleMessage is iterating cannot make the
1945
+ // dispatch loop skip the next callback. The removal applies from the next
1946
+ // event on.
1947
+ offStats(cb) {
1948
+ this.callbacks.stats = this.callbacks.stats.filter((c) => c !== cb);
1949
+ }
1950
+ offDisconnect(cb) {
1951
+ this.callbacks.disconnect = this.callbacks.disconnect.filter((c) => c !== cb);
1952
+ }
1938
1953
  onReconnecting(cb) {
1939
1954
  this.callbacks.reconnecting.push(cb);
1940
1955
  }
@@ -2143,6 +2158,12 @@ var DaemonHealth = class {
2143
2158
  var TunnelClient = class _TunnelClient {
2144
2159
  constructor(options = {}) {
2145
2160
  this.ipc = null;
2161
+ // App-compat shim state: registered listeners by id, and the tunnels whose
2162
+ // stream subscription those registrations opened (as opposed to attach()).
2163
+ this.shimStatsListeners = /* @__PURE__ */ new Map();
2164
+ this.shimDisconnectListeners = /* @__PURE__ */ new Map();
2165
+ this.shimSubscribedTunnels = /* @__PURE__ */ new Set();
2166
+ this.shimListenerSeq = 0;
2146
2167
  this.origin = options.origin ?? "cli";
2147
2168
  this.stream = new WsStream(() => {
2148
2169
  if (!this.ipc) throw new Error("TunnelClient not initialized. Call ensureDaemon() first.");
@@ -2290,6 +2311,12 @@ var TunnelClient = class _TunnelClient {
2290
2311
  onDisconnect(cb) {
2291
2312
  this.stream.onDisconnect(cb);
2292
2313
  }
2314
+ offStats(cb) {
2315
+ this.stream.offStats(cb);
2316
+ }
2317
+ offDisconnect(cb) {
2318
+ this.stream.offDisconnect(cb);
2319
+ }
2293
2320
  onReconnecting(cb) {
2294
2321
  this.stream.onReconnecting(cb);
2295
2322
  }
@@ -2327,24 +2354,73 @@ var TunnelClient = class _TunnelClient {
2327
2354
  isDaemonLost() {
2328
2355
  return this.health.isLost();
2329
2356
  }
2330
- // App-compat shims (register listener + auto-attach in detached mode)
2357
+ // App-compat shims. Each registration is scoped to ONE tunnel, returns an
2358
+ // id, and is undone by the matching unregister call. Before this, every
2359
+ // register appended a callback that saw every subscribed tunnel's events
2360
+ // and could never be removed, so a consumer that registered per page view
2361
+ // received each stats update N times, forever.
2362
+ //
2363
+ // The first registration for a tunnel opens its stream subscription and
2364
+ // the last unregister drops it with a plain unsubscribe. detach() is not
2365
+ // used here on purpose: it closes the client once nothing is subscribed,
2366
+ // which also stops the daemon-health heartbeat, and a stats consumer must
2367
+ // not have that side effect.
2331
2368
  handleRegisterStatsListener(tunnelId, listener) {
2332
- this.onStats(listener);
2333
- this.attach(tunnelId, "detached").catch(() => {
2334
- });
2369
+ const wrapped = (id, stats) => {
2370
+ if (id === tunnelId) listener(id, stats);
2371
+ };
2372
+ const listenerId = this.nextShimListenerId("stats");
2373
+ this.shimStatsListeners.set(listenerId, { tunnelId, wrapped });
2374
+ this.stream.onStats(wrapped);
2375
+ this.subscribeForShim(tunnelId);
2376
+ return listenerId;
2377
+ }
2378
+ handleUnregisterStatsListener(_tunnelId, listenerId) {
2379
+ const entry = this.shimStatsListeners.get(listenerId);
2380
+ if (!entry) return;
2381
+ this.shimStatsListeners.delete(listenerId);
2382
+ this.stream.offStats(entry.wrapped);
2383
+ this.releaseShimSubscription(entry.tunnelId);
2335
2384
  }
2336
2385
  handleRegisterDisconnectListener(tunnelId, listener) {
2337
- this.onDisconnect(listener);
2338
- this.attach(tunnelId, "detached").catch(() => {
2339
- });
2340
- }
2341
- handleUnregisterStatsListener(_tunnelId, _listenerId) {
2386
+ const wrapped = (id, error, messages) => {
2387
+ if (id === tunnelId) listener(id, error, messages);
2388
+ };
2389
+ const listenerId = this.nextShimListenerId("disconnect");
2390
+ this.shimDisconnectListeners.set(listenerId, { tunnelId, wrapped });
2391
+ this.stream.onDisconnect(wrapped);
2392
+ this.subscribeForShim(tunnelId);
2393
+ return listenerId;
2394
+ }
2395
+ handleUnregisterDisconnectListener(_tunnelId, listenerId) {
2396
+ const entry = this.shimDisconnectListeners.get(listenerId);
2397
+ if (!entry) return;
2398
+ this.shimDisconnectListeners.delete(listenerId);
2399
+ this.stream.offDisconnect(entry.wrapped);
2400
+ this.releaseShimSubscription(entry.tunnelId);
2342
2401
  }
2343
2402
  async handleGetTunnelStats(tunnelId) {
2344
2403
  this.assertClient();
2345
2404
  return this.ipc.getTunnelStats(tunnelId);
2346
2405
  }
2347
2406
  // Private
2407
+ nextShimListenerId(kind) {
2408
+ this.shimListenerSeq += 1;
2409
+ return `${kind}-${this.shimListenerSeq}`;
2410
+ }
2411
+ subscribeForShim(tunnelId) {
2412
+ if (!this.stream.isSubscribed(tunnelId)) this.shimSubscribedTunnels.add(tunnelId);
2413
+ this.attach(tunnelId, SessionMode.Detached).catch((err) => {
2414
+ logger.debug("Listener subscribe failed", { tunnelId, error: errorMessage(err) });
2415
+ });
2416
+ }
2417
+ releaseShimSubscription(tunnelId) {
2418
+ if (!this.shimSubscribedTunnels.has(tunnelId)) return;
2419
+ for (const entry of this.shimStatsListeners.values()) if (entry.tunnelId === tunnelId) return;
2420
+ for (const entry of this.shimDisconnectListeners.values()) if (entry.tunnelId === tunnelId) return;
2421
+ this.shimSubscribedTunnels.delete(tunnelId);
2422
+ this.stream.unsubscribe(tunnelId);
2423
+ }
2348
2424
  assertClient() {
2349
2425
  if (!this.ipc) {
2350
2426
  throw new Error("TunnelClient not initialized. Call ensureDaemon() first.");
@@ -13,7 +13,7 @@ import {
13
13
  startDaemon,
14
14
  startRemoteManagement,
15
15
  stopDaemon
16
- } from "./chunk-HS3OI4OL.js";
16
+ } from "./chunk-B6IKYBLU.js";
17
17
  import {
18
18
  readDaemonConfig
19
19
  } from "./chunk-MT44NAXX.js";
@@ -275,7 +275,10 @@ var defaultOptions = {
275
275
  originalRequestUrl: false,
276
276
  allowPreflight: false,
277
277
  reverseProxy: true,
278
- autoReconnect: true
278
+ autoReconnect: true,
279
+ // 0 = never give up: keep retrying every reconnectInterval until the tunnel
280
+ // is back. Not user-configurable from the CLI on purpose.
281
+ maxReconnectAttempts: 0
279
282
  };
280
283
 
281
284
  // src/cli/extendedOptions.ts
@@ -883,7 +886,9 @@ function buildFinalConfig(values, positionals, baseConfig) {
883
886
  token: token || (configFromFile?.token || (typeof values.token === "string" ? values.token : "")),
884
887
  serverAddress: server ? removeIPv6Brackets(server) : configFromFile?.serverAddress || defaultOptions.serverAddress,
885
888
  isQRCode: qrCode || (configFromFile?.isQRCode || false),
886
- autoReconnect: configFromFile?.autoReconnect ? configFromFile.autoReconnect : defaultOptions.autoReconnect,
889
+ // `??`, not a truthiness check: a saved `autoReconnect: false` must not
890
+ // flip back to the default.
891
+ autoReconnect: configFromFile?.autoReconnect ?? defaultOptions.autoReconnect,
887
892
  optional: {
888
893
  serve: configFromFile?.optional?.serve || void 0,
889
894
  noTui: values.noTui || values.notui || hasRemoteManagement(values) || (configFromFile?.optional?.noTui || false)
@@ -3458,7 +3463,7 @@ async function main() {
3458
3463
  const { values, positionals, hasAnyArgs } = parseCliArgs(cliOptions);
3459
3464
  configureLogger(values);
3460
3465
  if (values["_daemon-child"]) {
3461
- const { runDaemonChild: runDaemonChild2 } = await import("./daemonChild-STFB5OQL.js");
3466
+ const { runDaemonChild: runDaemonChild2 } = await import("./daemonChild-ZPZK5F4Q.js");
3462
3467
  await runDaemonChild2();
3463
3468
  return;
3464
3469
  }
@@ -6,8 +6,8 @@ import {
6
6
  trackIPCTunnelStart,
7
7
  trackTunnelStart,
8
8
  trackTunnelStop
9
- } from "./chunk-GA6UTBOU.js";
10
- import "./chunk-HS3OI4OL.js";
9
+ } from "./chunk-P7XRY5N4.js";
10
+ import "./chunk-B6IKYBLU.js";
11
11
  import "./chunk-MT44NAXX.js";
12
12
  import {
13
13
  DaemonHost
package/dist/index.cjs CHANGED
@@ -2194,6 +2194,8 @@ function pinggyOptionsToTunnelConfigV1(opts, configStoredInCli) {
2194
2194
  serverAddress: opts.serverAddress || "a.pinggy.io:443",
2195
2195
  token: opts.token || "",
2196
2196
  autoReconnect: opts.autoReconnect ?? true,
2197
+ reconnectInterval: opts.reconnectInterval,
2198
+ maxReconnectAttempts: opts.maxReconnectAttempts,
2197
2199
  force: opts.force ?? false,
2198
2200
  webDebugger: opts.webDebugger || "",
2199
2201
  forwarding: opts.forwarding ? opts.forwarding : "",
@@ -4094,7 +4096,10 @@ var init_defaults = __esm({
4094
4096
  originalRequestUrl: false,
4095
4097
  allowPreflight: false,
4096
4098
  reverseProxy: true,
4097
- autoReconnect: true
4099
+ autoReconnect: true,
4100
+ // 0 = never give up: keep retrying every reconnectInterval until the tunnel
4101
+ // is back. Not user-configurable from the CLI on purpose.
4102
+ maxReconnectAttempts: 0
4098
4103
  };
4099
4104
  }
4100
4105
  });
@@ -4698,7 +4703,9 @@ function buildFinalConfig(values, positionals, baseConfig) {
4698
4703
  token: token || (configFromFile?.token || (typeof values.token === "string" ? values.token : "")),
4699
4704
  serverAddress: server ? removeIPv6Brackets(server) : configFromFile?.serverAddress || defaultOptions.serverAddress,
4700
4705
  isQRCode: qrCode || (configFromFile?.isQRCode || false),
4701
- autoReconnect: configFromFile?.autoReconnect ? configFromFile.autoReconnect : defaultOptions.autoReconnect,
4706
+ // `??`, not a truthiness check: a saved `autoReconnect: false` must not
4707
+ // flip back to the default.
4708
+ autoReconnect: configFromFile?.autoReconnect ?? defaultOptions.autoReconnect,
4702
4709
  optional: {
4703
4710
  serve: configFromFile?.optional?.serve || void 0,
4704
4711
  noTui: values.noTui || values.notui || hasRemoteManagement(values) || (configFromFile?.optional?.noTui || false)
@@ -9801,6 +9808,9 @@ var init_wsStream = __esm({
9801
9808
  subscriptionCount() {
9802
9809
  return this.subscribedTunnels.size;
9803
9810
  }
9811
+ isSubscribed(tunnelId) {
9812
+ return this.subscribedTunnels.has(tunnelId);
9813
+ }
9804
9814
  /** Snapshot of current subscriptions, for the reconnect path to replay. */
9805
9815
  snapshotSubscriptions() {
9806
9816
  return Array.from(this.subscribedTunnels.entries());
@@ -9834,6 +9844,16 @@ var init_wsStream = __esm({
9834
9844
  onDisconnect(cb) {
9835
9845
  this.callbacks.disconnect.push(cb);
9836
9846
  }
9847
+ // Removal replaces the array instead of splicing it, so a callback that
9848
+ // unregisters itself while handleMessage is iterating cannot make the
9849
+ // dispatch loop skip the next callback. The removal applies from the next
9850
+ // event on.
9851
+ offStats(cb) {
9852
+ this.callbacks.stats = this.callbacks.stats.filter((c) => c !== cb);
9853
+ }
9854
+ offDisconnect(cb) {
9855
+ this.callbacks.disconnect = this.callbacks.disconnect.filter((c) => c !== cb);
9856
+ }
9837
9857
  onReconnecting(cb) {
9838
9858
  this.callbacks.reconnecting.push(cb);
9839
9859
  }
@@ -10068,6 +10088,8 @@ var init_tunnelClient = __esm({
10068
10088
  init_cjs_shims();
10069
10089
  init_ipcClient();
10070
10090
  init_daemonManager();
10091
+ init_logger();
10092
+ init_util();
10071
10093
  init_wsStream();
10072
10094
  init_daemonHealth();
10073
10095
  init_daemonTunnelHandler();
@@ -10075,6 +10097,12 @@ var init_tunnelClient = __esm({
10075
10097
  TunnelClient = class _TunnelClient {
10076
10098
  constructor(options = {}) {
10077
10099
  this.ipc = null;
10100
+ // App-compat shim state: registered listeners by id, and the tunnels whose
10101
+ // stream subscription those registrations opened (as opposed to attach()).
10102
+ this.shimStatsListeners = /* @__PURE__ */ new Map();
10103
+ this.shimDisconnectListeners = /* @__PURE__ */ new Map();
10104
+ this.shimSubscribedTunnels = /* @__PURE__ */ new Set();
10105
+ this.shimListenerSeq = 0;
10078
10106
  this.origin = options.origin ?? "cli";
10079
10107
  this.stream = new WsStream(() => {
10080
10108
  if (!this.ipc) throw new Error("TunnelClient not initialized. Call ensureDaemon() first.");
@@ -10222,6 +10250,12 @@ var init_tunnelClient = __esm({
10222
10250
  onDisconnect(cb) {
10223
10251
  this.stream.onDisconnect(cb);
10224
10252
  }
10253
+ offStats(cb) {
10254
+ this.stream.offStats(cb);
10255
+ }
10256
+ offDisconnect(cb) {
10257
+ this.stream.offDisconnect(cb);
10258
+ }
10225
10259
  onReconnecting(cb) {
10226
10260
  this.stream.onReconnecting(cb);
10227
10261
  }
@@ -10259,24 +10293,73 @@ var init_tunnelClient = __esm({
10259
10293
  isDaemonLost() {
10260
10294
  return this.health.isLost();
10261
10295
  }
10262
- // App-compat shims (register listener + auto-attach in detached mode)
10296
+ // App-compat shims. Each registration is scoped to ONE tunnel, returns an
10297
+ // id, and is undone by the matching unregister call. Before this, every
10298
+ // register appended a callback that saw every subscribed tunnel's events
10299
+ // and could never be removed, so a consumer that registered per page view
10300
+ // received each stats update N times, forever.
10301
+ //
10302
+ // The first registration for a tunnel opens its stream subscription and
10303
+ // the last unregister drops it with a plain unsubscribe. detach() is not
10304
+ // used here on purpose: it closes the client once nothing is subscribed,
10305
+ // which also stops the daemon-health heartbeat, and a stats consumer must
10306
+ // not have that side effect.
10263
10307
  handleRegisterStatsListener(tunnelId, listener) {
10264
- this.onStats(listener);
10265
- this.attach(tunnelId, "detached").catch(() => {
10266
- });
10308
+ const wrapped = (id, stats) => {
10309
+ if (id === tunnelId) listener(id, stats);
10310
+ };
10311
+ const listenerId = this.nextShimListenerId("stats");
10312
+ this.shimStatsListeners.set(listenerId, { tunnelId, wrapped });
10313
+ this.stream.onStats(wrapped);
10314
+ this.subscribeForShim(tunnelId);
10315
+ return listenerId;
10316
+ }
10317
+ handleUnregisterStatsListener(_tunnelId, listenerId) {
10318
+ const entry = this.shimStatsListeners.get(listenerId);
10319
+ if (!entry) return;
10320
+ this.shimStatsListeners.delete(listenerId);
10321
+ this.stream.offStats(entry.wrapped);
10322
+ this.releaseShimSubscription(entry.tunnelId);
10267
10323
  }
10268
10324
  handleRegisterDisconnectListener(tunnelId, listener) {
10269
- this.onDisconnect(listener);
10270
- this.attach(tunnelId, "detached").catch(() => {
10271
- });
10272
- }
10273
- handleUnregisterStatsListener(_tunnelId, _listenerId) {
10325
+ const wrapped = (id, error, messages) => {
10326
+ if (id === tunnelId) listener(id, error, messages);
10327
+ };
10328
+ const listenerId = this.nextShimListenerId("disconnect");
10329
+ this.shimDisconnectListeners.set(listenerId, { tunnelId, wrapped });
10330
+ this.stream.onDisconnect(wrapped);
10331
+ this.subscribeForShim(tunnelId);
10332
+ return listenerId;
10333
+ }
10334
+ handleUnregisterDisconnectListener(_tunnelId, listenerId) {
10335
+ const entry = this.shimDisconnectListeners.get(listenerId);
10336
+ if (!entry) return;
10337
+ this.shimDisconnectListeners.delete(listenerId);
10338
+ this.stream.offDisconnect(entry.wrapped);
10339
+ this.releaseShimSubscription(entry.tunnelId);
10274
10340
  }
10275
10341
  async handleGetTunnelStats(tunnelId) {
10276
10342
  this.assertClient();
10277
10343
  return this.ipc.getTunnelStats(tunnelId);
10278
10344
  }
10279
10345
  // Private
10346
+ nextShimListenerId(kind) {
10347
+ this.shimListenerSeq += 1;
10348
+ return `${kind}-${this.shimListenerSeq}`;
10349
+ }
10350
+ subscribeForShim(tunnelId) {
10351
+ if (!this.stream.isSubscribed(tunnelId)) this.shimSubscribedTunnels.add(tunnelId);
10352
+ this.attach(tunnelId, SessionMode.Detached).catch((err) => {
10353
+ logger.debug("Listener subscribe failed", { tunnelId, error: errorMessage(err) });
10354
+ });
10355
+ }
10356
+ releaseShimSubscription(tunnelId) {
10357
+ if (!this.shimSubscribedTunnels.has(tunnelId)) return;
10358
+ for (const entry of this.shimStatsListeners.values()) if (entry.tunnelId === tunnelId) return;
10359
+ for (const entry of this.shimDisconnectListeners.values()) if (entry.tunnelId === tunnelId) return;
10360
+ this.shimSubscribedTunnels.delete(tunnelId);
10361
+ this.stream.unsubscribe(tunnelId);
10362
+ }
10280
10363
  assertClient() {
10281
10364
  if (!this.ipc) {
10282
10365
  throw new Error("TunnelClient not initialized. Call ensureDaemon() first.");
package/dist/index.d.cts CHANGED
@@ -992,6 +992,7 @@ declare class WsStream {
992
992
  unsubscribe(tunnelId: string): boolean;
993
993
  hasSubscriptions(): boolean;
994
994
  subscriptionCount(): number;
995
+ isSubscribed(tunnelId: string): boolean;
995
996
  /** Snapshot of current subscriptions, for the reconnect path to replay. */
996
997
  snapshotSubscriptions(): Array<[string, SubscriptionInfo]>;
997
998
  /**
@@ -1003,6 +1004,8 @@ declare class WsStream {
1003
1004
  onClose(cb: (code: number) => void): void;
1004
1005
  onStats(cb: StatsCallback): void;
1005
1006
  onDisconnect(cb: DisconnectCallback): void;
1007
+ offStats(cb: StatsCallback): void;
1008
+ offDisconnect(cb: DisconnectCallback): void;
1006
1009
  onReconnecting(cb: ReconnectingCallback): void;
1007
1010
  onReconnected(cb: ReconnectedCallback): void;
1008
1011
  onReconnectionFailed(cb: ReconnectionFailedCallback): void;
@@ -1079,6 +1082,10 @@ declare class TunnelClient {
1079
1082
  private origin;
1080
1083
  private stream;
1081
1084
  health: DaemonHealth;
1085
+ private shimStatsListeners;
1086
+ private shimDisconnectListeners;
1087
+ private shimSubscribedTunnels;
1088
+ private shimListenerSeq;
1082
1089
  constructor(options?: TunnelClientOptions);
1083
1090
  ensureDaemon(): Promise<void>;
1084
1091
  static forRemoteManagement(): Promise<TunnelClient>;
@@ -1116,6 +1123,8 @@ declare class TunnelClient {
1116
1123
  detach(tunnelId: string): void;
1117
1124
  onStats(cb: StatsCallback): void;
1118
1125
  onDisconnect(cb: DisconnectCallback): void;
1126
+ offStats(cb: StatsCallback): void;
1127
+ offDisconnect(cb: DisconnectCallback): void;
1119
1128
  onReconnecting(cb: ReconnectingCallback): void;
1120
1129
  onReconnected(cb: ReconnectedCallback): void;
1121
1130
  onReconnectionFailed(cb: ReconnectionFailedCallback): void;
@@ -1128,10 +1137,14 @@ declare class TunnelClient {
1128
1137
  onDaemonReconnecting(cb: DaemonReconnectingCallback): void;
1129
1138
  onDaemonReconnected(cb: DaemonReconnectedCallback): void;
1130
1139
  isDaemonLost(): boolean;
1131
- handleRegisterStatsListener(tunnelId: string, listener: (tunnelId: string, stats: TunnelUsageType) => void): void;
1132
- handleRegisterDisconnectListener(tunnelId: string, listener: (tunnelId: string, error: string, messages: string[]) => void): void;
1133
- handleUnregisterStatsListener(_tunnelId: string, _listenerId: string): void;
1140
+ handleRegisterStatsListener(tunnelId: string, listener: StatsCallback): string;
1141
+ handleUnregisterStatsListener(_tunnelId: string, listenerId: string): void;
1142
+ handleRegisterDisconnectListener(tunnelId: string, listener: DisconnectCallback): string;
1143
+ handleUnregisterDisconnectListener(_tunnelId: string, listenerId: string): void;
1134
1144
  handleGetTunnelStats(tunnelId: string): Promise<TunnelUsageType[] | ErrorResponse>;
1145
+ private nextShimListenerId;
1146
+ private subscribeForShim;
1147
+ private releaseShimSubscription;
1135
1148
  private assertClient;
1136
1149
  }
1137
1150
 
package/dist/index.d.ts CHANGED
@@ -992,6 +992,7 @@ declare class WsStream {
992
992
  unsubscribe(tunnelId: string): boolean;
993
993
  hasSubscriptions(): boolean;
994
994
  subscriptionCount(): number;
995
+ isSubscribed(tunnelId: string): boolean;
995
996
  /** Snapshot of current subscriptions, for the reconnect path to replay. */
996
997
  snapshotSubscriptions(): Array<[string, SubscriptionInfo]>;
997
998
  /**
@@ -1003,6 +1004,8 @@ declare class WsStream {
1003
1004
  onClose(cb: (code: number) => void): void;
1004
1005
  onStats(cb: StatsCallback): void;
1005
1006
  onDisconnect(cb: DisconnectCallback): void;
1007
+ offStats(cb: StatsCallback): void;
1008
+ offDisconnect(cb: DisconnectCallback): void;
1006
1009
  onReconnecting(cb: ReconnectingCallback): void;
1007
1010
  onReconnected(cb: ReconnectedCallback): void;
1008
1011
  onReconnectionFailed(cb: ReconnectionFailedCallback): void;
@@ -1079,6 +1082,10 @@ declare class TunnelClient {
1079
1082
  private origin;
1080
1083
  private stream;
1081
1084
  health: DaemonHealth;
1085
+ private shimStatsListeners;
1086
+ private shimDisconnectListeners;
1087
+ private shimSubscribedTunnels;
1088
+ private shimListenerSeq;
1082
1089
  constructor(options?: TunnelClientOptions);
1083
1090
  ensureDaemon(): Promise<void>;
1084
1091
  static forRemoteManagement(): Promise<TunnelClient>;
@@ -1116,6 +1123,8 @@ declare class TunnelClient {
1116
1123
  detach(tunnelId: string): void;
1117
1124
  onStats(cb: StatsCallback): void;
1118
1125
  onDisconnect(cb: DisconnectCallback): void;
1126
+ offStats(cb: StatsCallback): void;
1127
+ offDisconnect(cb: DisconnectCallback): void;
1119
1128
  onReconnecting(cb: ReconnectingCallback): void;
1120
1129
  onReconnected(cb: ReconnectedCallback): void;
1121
1130
  onReconnectionFailed(cb: ReconnectionFailedCallback): void;
@@ -1128,10 +1137,14 @@ declare class TunnelClient {
1128
1137
  onDaemonReconnecting(cb: DaemonReconnectingCallback): void;
1129
1138
  onDaemonReconnected(cb: DaemonReconnectedCallback): void;
1130
1139
  isDaemonLost(): boolean;
1131
- handleRegisterStatsListener(tunnelId: string, listener: (tunnelId: string, stats: TunnelUsageType) => void): void;
1132
- handleRegisterDisconnectListener(tunnelId: string, listener: (tunnelId: string, error: string, messages: string[]) => void): void;
1133
- handleUnregisterStatsListener(_tunnelId: string, _listenerId: string): void;
1140
+ handleRegisterStatsListener(tunnelId: string, listener: StatsCallback): string;
1141
+ handleUnregisterStatsListener(_tunnelId: string, listenerId: string): void;
1142
+ handleRegisterDisconnectListener(tunnelId: string, listener: DisconnectCallback): string;
1143
+ handleUnregisterDisconnectListener(_tunnelId: string, listenerId: string): void;
1134
1144
  handleGetTunnelStats(tunnelId: string): Promise<TunnelUsageType[] | ErrorResponse>;
1145
+ private nextShimListenerId;
1146
+ private subscribeForShim;
1147
+ private releaseShimSubscription;
1135
1148
  private assertClient;
1136
1149
  }
1137
1150
 
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  initiateRemoteManagement,
15
15
  isDaemonRunning,
16
16
  startRemoteManagement
17
- } from "./chunk-HS3OI4OL.js";
17
+ } from "./chunk-B6IKYBLU.js";
18
18
  import "./chunk-I4HXBP5G.js";
19
19
  import {
20
20
  bulkReplace,
@@ -139,7 +139,7 @@ async function verifyAndLoad() {
139
139
  process.exit(1);
140
140
  }
141
141
  }
142
- await import("./main-M5PCTJTB.js");
142
+ await import("./main-VBWX7HTB.js");
143
143
  }
144
144
  verifyAndLoad().catch((err) => {
145
145
  printer_default.fatal(`Failed to start CLI:, ${err}`);
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runDaemonChild
4
- } from "./chunk-GA6UTBOU.js";
4
+ } from "./chunk-P7XRY5N4.js";
5
5
  import {
6
6
  RemoteManagementUnauthorizedError,
7
7
  TunnelOperations,
@@ -13,7 +13,7 @@ import {
13
13
  getRemoteManagementState,
14
14
  initiateRemoteManagement,
15
15
  isDaemonRunning
16
- } from "./chunk-HS3OI4OL.js";
16
+ } from "./chunk-B6IKYBLU.js";
17
17
  import "./chunk-MT44NAXX.js";
18
18
  import {
19
19
  DaemonHost
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinggy",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "description": "Create secure, shareable tunnels to your localhost and manage them from the command line. ",
@@ -52,7 +52,7 @@
52
52
  ]
53
53
  },
54
54
  "dependencies": {
55
- "@pinggy/pinggy": "^0.5.1",
55
+ "@pinggy/pinggy": "^0.5.2",
56
56
  "blessed": "^0.1.81",
57
57
  "clipboardy": "^5.0.0",
58
58
  "mime": "^4.1.0",