pinggy 0.5.7 → 0.5.9

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 : "",
@@ -303,7 +305,10 @@ function pinggyOptionsToTunnelConfigV1(opts, configStoredInCli) {
303
305
  originalRequestUrl: opts.originalRequestUrl ?? false,
304
306
  allowPreflight: opts.allowPreflight ?? false,
305
307
  haProxy: opts.haProxy,
306
- optional: opts.optional || {}
308
+ optional: {
309
+ ...configStoredInCli.optional || {},
310
+ ...opts.optional || {}
311
+ }
307
312
  };
308
313
  }
309
314
  function tunnelConfigToPinggyOptions(config) {
@@ -1677,7 +1682,7 @@ async function ensureDaemonRunning() {
1677
1682
  return existing;
1678
1683
  }
1679
1684
  if (process.versions.electron) {
1680
- const { runDaemonChild } = await import("./daemonChild-STFB5OQL.js");
1685
+ const { runDaemonChild } = await import("./daemonChild-MT6TRMRR.js");
1681
1686
  const handle = await runDaemonChild({
1682
1687
  installSignalHandlers: false,
1683
1688
  exitOnFailure: false,
@@ -1902,6 +1907,9 @@ var WsStream = class {
1902
1907
  subscriptionCount() {
1903
1908
  return this.subscribedTunnels.size;
1904
1909
  }
1910
+ isSubscribed(tunnelId) {
1911
+ return this.subscribedTunnels.has(tunnelId);
1912
+ }
1905
1913
  /** Snapshot of current subscriptions, for the reconnect path to replay. */
1906
1914
  snapshotSubscriptions() {
1907
1915
  return Array.from(this.subscribedTunnels.entries());
@@ -1935,6 +1943,16 @@ var WsStream = class {
1935
1943
  onDisconnect(cb) {
1936
1944
  this.callbacks.disconnect.push(cb);
1937
1945
  }
1946
+ // Removal replaces the array instead of splicing it, so a callback that
1947
+ // unregisters itself while handleMessage is iterating cannot make the
1948
+ // dispatch loop skip the next callback. The removal applies from the next
1949
+ // event on.
1950
+ offStats(cb) {
1951
+ this.callbacks.stats = this.callbacks.stats.filter((c) => c !== cb);
1952
+ }
1953
+ offDisconnect(cb) {
1954
+ this.callbacks.disconnect = this.callbacks.disconnect.filter((c) => c !== cb);
1955
+ }
1938
1956
  onReconnecting(cb) {
1939
1957
  this.callbacks.reconnecting.push(cb);
1940
1958
  }
@@ -2143,6 +2161,12 @@ var DaemonHealth = class {
2143
2161
  var TunnelClient = class _TunnelClient {
2144
2162
  constructor(options = {}) {
2145
2163
  this.ipc = null;
2164
+ // App-compat shim state: registered listeners by id, and the tunnels whose
2165
+ // stream subscription those registrations opened (as opposed to attach()).
2166
+ this.shimStatsListeners = /* @__PURE__ */ new Map();
2167
+ this.shimDisconnectListeners = /* @__PURE__ */ new Map();
2168
+ this.shimSubscribedTunnels = /* @__PURE__ */ new Set();
2169
+ this.shimListenerSeq = 0;
2146
2170
  this.origin = options.origin ?? "cli";
2147
2171
  this.stream = new WsStream(() => {
2148
2172
  if (!this.ipc) throw new Error("TunnelClient not initialized. Call ensureDaemon() first.");
@@ -2290,6 +2314,12 @@ var TunnelClient = class _TunnelClient {
2290
2314
  onDisconnect(cb) {
2291
2315
  this.stream.onDisconnect(cb);
2292
2316
  }
2317
+ offStats(cb) {
2318
+ this.stream.offStats(cb);
2319
+ }
2320
+ offDisconnect(cb) {
2321
+ this.stream.offDisconnect(cb);
2322
+ }
2293
2323
  onReconnecting(cb) {
2294
2324
  this.stream.onReconnecting(cb);
2295
2325
  }
@@ -2327,24 +2357,73 @@ var TunnelClient = class _TunnelClient {
2327
2357
  isDaemonLost() {
2328
2358
  return this.health.isLost();
2329
2359
  }
2330
- // App-compat shims (register listener + auto-attach in detached mode)
2360
+ // App-compat shims. Each registration is scoped to ONE tunnel, returns an
2361
+ // id, and is undone by the matching unregister call. Before this, every
2362
+ // register appended a callback that saw every subscribed tunnel's events
2363
+ // and could never be removed, so a consumer that registered per page view
2364
+ // received each stats update N times, forever.
2365
+ //
2366
+ // The first registration for a tunnel opens its stream subscription and
2367
+ // the last unregister drops it with a plain unsubscribe. detach() is not
2368
+ // used here on purpose: it closes the client once nothing is subscribed,
2369
+ // which also stops the daemon-health heartbeat, and a stats consumer must
2370
+ // not have that side effect.
2331
2371
  handleRegisterStatsListener(tunnelId, listener) {
2332
- this.onStats(listener);
2333
- this.attach(tunnelId, "detached").catch(() => {
2334
- });
2372
+ const wrapped = (id, stats) => {
2373
+ if (id === tunnelId) listener(id, stats);
2374
+ };
2375
+ const listenerId = this.nextShimListenerId("stats");
2376
+ this.shimStatsListeners.set(listenerId, { tunnelId, wrapped });
2377
+ this.stream.onStats(wrapped);
2378
+ this.subscribeForShim(tunnelId);
2379
+ return listenerId;
2380
+ }
2381
+ handleUnregisterStatsListener(_tunnelId, listenerId) {
2382
+ const entry = this.shimStatsListeners.get(listenerId);
2383
+ if (!entry) return;
2384
+ this.shimStatsListeners.delete(listenerId);
2385
+ this.stream.offStats(entry.wrapped);
2386
+ this.releaseShimSubscription(entry.tunnelId);
2335
2387
  }
2336
2388
  handleRegisterDisconnectListener(tunnelId, listener) {
2337
- this.onDisconnect(listener);
2338
- this.attach(tunnelId, "detached").catch(() => {
2339
- });
2340
- }
2341
- handleUnregisterStatsListener(_tunnelId, _listenerId) {
2389
+ const wrapped = (id, error, messages) => {
2390
+ if (id === tunnelId) listener(id, error, messages);
2391
+ };
2392
+ const listenerId = this.nextShimListenerId("disconnect");
2393
+ this.shimDisconnectListeners.set(listenerId, { tunnelId, wrapped });
2394
+ this.stream.onDisconnect(wrapped);
2395
+ this.subscribeForShim(tunnelId);
2396
+ return listenerId;
2397
+ }
2398
+ handleUnregisterDisconnectListener(_tunnelId, listenerId) {
2399
+ const entry = this.shimDisconnectListeners.get(listenerId);
2400
+ if (!entry) return;
2401
+ this.shimDisconnectListeners.delete(listenerId);
2402
+ this.stream.offDisconnect(entry.wrapped);
2403
+ this.releaseShimSubscription(entry.tunnelId);
2342
2404
  }
2343
2405
  async handleGetTunnelStats(tunnelId) {
2344
2406
  this.assertClient();
2345
2407
  return this.ipc.getTunnelStats(tunnelId);
2346
2408
  }
2347
2409
  // Private
2410
+ nextShimListenerId(kind) {
2411
+ this.shimListenerSeq += 1;
2412
+ return `${kind}-${this.shimListenerSeq}`;
2413
+ }
2414
+ subscribeForShim(tunnelId) {
2415
+ if (!this.stream.isSubscribed(tunnelId)) this.shimSubscribedTunnels.add(tunnelId);
2416
+ this.attach(tunnelId, SessionMode.Detached).catch((err) => {
2417
+ logger.debug("Listener subscribe failed", { tunnelId, error: errorMessage(err) });
2418
+ });
2419
+ }
2420
+ releaseShimSubscription(tunnelId) {
2421
+ if (!this.shimSubscribedTunnels.has(tunnelId)) return;
2422
+ for (const entry of this.shimStatsListeners.values()) if (entry.tunnelId === tunnelId) return;
2423
+ for (const entry of this.shimDisconnectListeners.values()) if (entry.tunnelId === tunnelId) return;
2424
+ this.shimSubscribedTunnels.delete(tunnelId);
2425
+ this.stream.unsubscribe(tunnelId);
2426
+ }
2348
2427
  assertClient() {
2349
2428
  if (!this.ipc) {
2350
2429
  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-2HAVQORM.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-MT6TRMRR.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-4J5YBB4H.js";
10
+ import "./chunk-2HAVQORM.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 : "",
@@ -2207,7 +2209,10 @@ function pinggyOptionsToTunnelConfigV1(opts, configStoredInCli) {
2207
2209
  originalRequestUrl: opts.originalRequestUrl ?? false,
2208
2210
  allowPreflight: opts.allowPreflight ?? false,
2209
2211
  haProxy: opts.haProxy,
2210
- optional: opts.optional || {}
2212
+ optional: {
2213
+ ...configStoredInCli.optional || {},
2214
+ ...opts.optional || {}
2215
+ }
2211
2216
  };
2212
2217
  }
2213
2218
  function tunnelConfigToPinggyOptions(config) {
@@ -4094,7 +4099,10 @@ var init_defaults = __esm({
4094
4099
  originalRequestUrl: false,
4095
4100
  allowPreflight: false,
4096
4101
  reverseProxy: true,
4097
- autoReconnect: true
4102
+ autoReconnect: true,
4103
+ // 0 = never give up: keep retrying every reconnectInterval until the tunnel
4104
+ // is back. Not user-configurable from the CLI on purpose.
4105
+ maxReconnectAttempts: 0
4098
4106
  };
4099
4107
  }
4100
4108
  });
@@ -4698,7 +4706,9 @@ function buildFinalConfig(values, positionals, baseConfig) {
4698
4706
  token: token || (configFromFile?.token || (typeof values.token === "string" ? values.token : "")),
4699
4707
  serverAddress: server ? removeIPv6Brackets(server) : configFromFile?.serverAddress || defaultOptions.serverAddress,
4700
4708
  isQRCode: qrCode || (configFromFile?.isQRCode || false),
4701
- autoReconnect: configFromFile?.autoReconnect ? configFromFile.autoReconnect : defaultOptions.autoReconnect,
4709
+ // `??`, not a truthiness check: a saved `autoReconnect: false` must not
4710
+ // flip back to the default.
4711
+ autoReconnect: configFromFile?.autoReconnect ?? defaultOptions.autoReconnect,
4702
4712
  optional: {
4703
4713
  serve: configFromFile?.optional?.serve || void 0,
4704
4714
  noTui: values.noTui || values.notui || hasRemoteManagement(values) || (configFromFile?.optional?.noTui || false)
@@ -9801,6 +9811,9 @@ var init_wsStream = __esm({
9801
9811
  subscriptionCount() {
9802
9812
  return this.subscribedTunnels.size;
9803
9813
  }
9814
+ isSubscribed(tunnelId) {
9815
+ return this.subscribedTunnels.has(tunnelId);
9816
+ }
9804
9817
  /** Snapshot of current subscriptions, for the reconnect path to replay. */
9805
9818
  snapshotSubscriptions() {
9806
9819
  return Array.from(this.subscribedTunnels.entries());
@@ -9834,6 +9847,16 @@ var init_wsStream = __esm({
9834
9847
  onDisconnect(cb) {
9835
9848
  this.callbacks.disconnect.push(cb);
9836
9849
  }
9850
+ // Removal replaces the array instead of splicing it, so a callback that
9851
+ // unregisters itself while handleMessage is iterating cannot make the
9852
+ // dispatch loop skip the next callback. The removal applies from the next
9853
+ // event on.
9854
+ offStats(cb) {
9855
+ this.callbacks.stats = this.callbacks.stats.filter((c) => c !== cb);
9856
+ }
9857
+ offDisconnect(cb) {
9858
+ this.callbacks.disconnect = this.callbacks.disconnect.filter((c) => c !== cb);
9859
+ }
9837
9860
  onReconnecting(cb) {
9838
9861
  this.callbacks.reconnecting.push(cb);
9839
9862
  }
@@ -10068,6 +10091,8 @@ var init_tunnelClient = __esm({
10068
10091
  init_cjs_shims();
10069
10092
  init_ipcClient();
10070
10093
  init_daemonManager();
10094
+ init_logger();
10095
+ init_util();
10071
10096
  init_wsStream();
10072
10097
  init_daemonHealth();
10073
10098
  init_daemonTunnelHandler();
@@ -10075,6 +10100,12 @@ var init_tunnelClient = __esm({
10075
10100
  TunnelClient = class _TunnelClient {
10076
10101
  constructor(options = {}) {
10077
10102
  this.ipc = null;
10103
+ // App-compat shim state: registered listeners by id, and the tunnels whose
10104
+ // stream subscription those registrations opened (as opposed to attach()).
10105
+ this.shimStatsListeners = /* @__PURE__ */ new Map();
10106
+ this.shimDisconnectListeners = /* @__PURE__ */ new Map();
10107
+ this.shimSubscribedTunnels = /* @__PURE__ */ new Set();
10108
+ this.shimListenerSeq = 0;
10078
10109
  this.origin = options.origin ?? "cli";
10079
10110
  this.stream = new WsStream(() => {
10080
10111
  if (!this.ipc) throw new Error("TunnelClient not initialized. Call ensureDaemon() first.");
@@ -10222,6 +10253,12 @@ var init_tunnelClient = __esm({
10222
10253
  onDisconnect(cb) {
10223
10254
  this.stream.onDisconnect(cb);
10224
10255
  }
10256
+ offStats(cb) {
10257
+ this.stream.offStats(cb);
10258
+ }
10259
+ offDisconnect(cb) {
10260
+ this.stream.offDisconnect(cb);
10261
+ }
10225
10262
  onReconnecting(cb) {
10226
10263
  this.stream.onReconnecting(cb);
10227
10264
  }
@@ -10259,24 +10296,73 @@ var init_tunnelClient = __esm({
10259
10296
  isDaemonLost() {
10260
10297
  return this.health.isLost();
10261
10298
  }
10262
- // App-compat shims (register listener + auto-attach in detached mode)
10299
+ // App-compat shims. Each registration is scoped to ONE tunnel, returns an
10300
+ // id, and is undone by the matching unregister call. Before this, every
10301
+ // register appended a callback that saw every subscribed tunnel's events
10302
+ // and could never be removed, so a consumer that registered per page view
10303
+ // received each stats update N times, forever.
10304
+ //
10305
+ // The first registration for a tunnel opens its stream subscription and
10306
+ // the last unregister drops it with a plain unsubscribe. detach() is not
10307
+ // used here on purpose: it closes the client once nothing is subscribed,
10308
+ // which also stops the daemon-health heartbeat, and a stats consumer must
10309
+ // not have that side effect.
10263
10310
  handleRegisterStatsListener(tunnelId, listener) {
10264
- this.onStats(listener);
10265
- this.attach(tunnelId, "detached").catch(() => {
10266
- });
10311
+ const wrapped = (id, stats) => {
10312
+ if (id === tunnelId) listener(id, stats);
10313
+ };
10314
+ const listenerId = this.nextShimListenerId("stats");
10315
+ this.shimStatsListeners.set(listenerId, { tunnelId, wrapped });
10316
+ this.stream.onStats(wrapped);
10317
+ this.subscribeForShim(tunnelId);
10318
+ return listenerId;
10319
+ }
10320
+ handleUnregisterStatsListener(_tunnelId, listenerId) {
10321
+ const entry = this.shimStatsListeners.get(listenerId);
10322
+ if (!entry) return;
10323
+ this.shimStatsListeners.delete(listenerId);
10324
+ this.stream.offStats(entry.wrapped);
10325
+ this.releaseShimSubscription(entry.tunnelId);
10267
10326
  }
10268
10327
  handleRegisterDisconnectListener(tunnelId, listener) {
10269
- this.onDisconnect(listener);
10270
- this.attach(tunnelId, "detached").catch(() => {
10271
- });
10272
- }
10273
- handleUnregisterStatsListener(_tunnelId, _listenerId) {
10328
+ const wrapped = (id, error, messages) => {
10329
+ if (id === tunnelId) listener(id, error, messages);
10330
+ };
10331
+ const listenerId = this.nextShimListenerId("disconnect");
10332
+ this.shimDisconnectListeners.set(listenerId, { tunnelId, wrapped });
10333
+ this.stream.onDisconnect(wrapped);
10334
+ this.subscribeForShim(tunnelId);
10335
+ return listenerId;
10336
+ }
10337
+ handleUnregisterDisconnectListener(_tunnelId, listenerId) {
10338
+ const entry = this.shimDisconnectListeners.get(listenerId);
10339
+ if (!entry) return;
10340
+ this.shimDisconnectListeners.delete(listenerId);
10341
+ this.stream.offDisconnect(entry.wrapped);
10342
+ this.releaseShimSubscription(entry.tunnelId);
10274
10343
  }
10275
10344
  async handleGetTunnelStats(tunnelId) {
10276
10345
  this.assertClient();
10277
10346
  return this.ipc.getTunnelStats(tunnelId);
10278
10347
  }
10279
10348
  // Private
10349
+ nextShimListenerId(kind) {
10350
+ this.shimListenerSeq += 1;
10351
+ return `${kind}-${this.shimListenerSeq}`;
10352
+ }
10353
+ subscribeForShim(tunnelId) {
10354
+ if (!this.stream.isSubscribed(tunnelId)) this.shimSubscribedTunnels.add(tunnelId);
10355
+ this.attach(tunnelId, SessionMode.Detached).catch((err) => {
10356
+ logger.debug("Listener subscribe failed", { tunnelId, error: errorMessage(err) });
10357
+ });
10358
+ }
10359
+ releaseShimSubscription(tunnelId) {
10360
+ if (!this.shimSubscribedTunnels.has(tunnelId)) return;
10361
+ for (const entry of this.shimStatsListeners.values()) if (entry.tunnelId === tunnelId) return;
10362
+ for (const entry of this.shimDisconnectListeners.values()) if (entry.tunnelId === tunnelId) return;
10363
+ this.shimSubscribedTunnels.delete(tunnelId);
10364
+ this.stream.unsubscribe(tunnelId);
10365
+ }
10280
10366
  assertClient() {
10281
10367
  if (!this.ipc) {
10282
10368
  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-2HAVQORM.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-KY2O5KNA.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-4J5YBB4H.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-2HAVQORM.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.9",
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",